diff --git a/backend/package.json b/backend/package.json index bbeda78..84e98ae 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "packageManager": "pnpm@10.10.0", "dependencies": { "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "bcrypt": "^5.1.1", "better-sqlite3": "^11.10.0", "cors": "^2.8.5", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 022a404..81f0f1c 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: ajv: specifier: ^8.17.1 version: 8.17.1 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.17.1) bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -98,6 +101,14 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -1000,6 +1011,10 @@ snapshots: agent-base@7.1.3: {} + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 diff --git a/backend/src/models/user.js b/backend/src/models/user.js index 0683ffd..a573214 100644 --- a/backend/src/models/user.js +++ b/backend/src/models/user.js @@ -10,7 +10,7 @@ async function getAllUsers() { } async function getUserByName(name) { - return await db.query("SELECT username, display_name, profile_picture, role FROM Users WHERE username = ?;", [name]); + return await db.query("SELECT username, display_name, email, profile_picture, role FROM Users WHERE username = ?;", [name]); } async function getUserByEmail(email) { @@ -25,10 +25,15 @@ async function getUserPassword(name) { return await db.query("SELECT username, password FROM Users WHERE username = ?;", [name]); } +//TODO test async function exists(name) { return await db.exists("Users", "username", name); } +async function existsEmail(email) { + return await db.exists("Users", "email", email); +} + // --- Create --- @@ -122,4 +127,4 @@ module.exports = { getAllUsers, getUserByName, getUserByEmail, getFullUserInfos, createUser, addFavoriteMods, updateUser, deleteUser, deleteFavoriteMods, - exists } \ No newline at end of file + exists, existsEmail } \ No newline at end of file diff --git a/backend/src/schemas/user.js b/backend/src/schemas/user.js index 0c009a1..3dba1ad 100644 --- a/backend/src/schemas/user.js +++ b/backend/src/schemas/user.js @@ -1,5 +1,6 @@ const Ajv = require("ajv"); -const ajv = new Ajv(); +const addFormats = require("ajv-formats") +const ajv = new Ajv({formats: {email: true}}); // --- Schemas --- //TODO @@ -7,14 +8,17 @@ const ajv = new Ajv(); const newUserSchema = { type: 'object', properties: { - email: { type: 'string', format: 'email' }, - name: { type: 'string' }, - password: { type: 'string', minLength: 3, maxLength: 30 }, + username: { type: 'string' }, //TODO + display_name: { type: 'string'}, + email: { type: 'string', format: 'email' }, //TODO + password: { type: 'string', minLength: 6, maxLength: 255 }, + profile_picture: {type: 'string'} //TODO }, required: ['name', 'email', 'password'], additionalProperties: false }; +addFormats(ajv, ['email']); const validateNewUserData = ajv.compile(newUserSchema); diff --git a/backend/src/services/userService.js b/backend/src/services/userService.js index 8020e25..cd666db 100644 --- a/backend/src/services/userService.js +++ b/backend/src/services/userService.js @@ -1,7 +1,7 @@ const model = require("../models/user"); const AppError = require("../utils/appError"); const cryptoUtils = require("../utils/crypto"); -const { validateUserData } = require("../utils/validate_legacy"); +const { validateNewUserData} = require("../schemas/user"); const { sanitizeUserData } = require("../utils/sanitize"); async function getAllUsers() { @@ -10,22 +10,43 @@ async function getAllUsers() { async function getUserByName(name) { const res = await model.getUserByName(name); + if (res.length == 0) { + throw new AppError(404, "No user with this name", "Not found"); + } return res[0]; } async function createUser(user_data) { // Check body validity - // TODO + try { + validateNewUserData(user_data); + } catch (err) { + throw new AppError(400, "Invalid fields", "Bad request"); + } // Sanitize // TODO - // Gather data - const { username, email, password, display_name, profile_picture, settings } = user_data - const password_hash = await cryptoUtils.hashPassword(password); + // Generate data - await model.createUser(username, email, password_hash, display_name, null, null); + const { username, email, password, display_name, profile_picture, settings } = user_data + + const password_hash = cryptoUtils.hashPassword(password); + + if (await model.exists(username) ) { + throw new AppError(403, "A user with this username already exists", "Already exists"); + } + if (await model.existsEmail(email)) { + throw new AppError(403, "A user with this email already exists", "Already exists"); + } + + if (!display_name) { + display_name = username; + } + + // Write changes + await model.createUser(username, email, await password_hash, display_name, null, null); return model.getUserByName(username); } diff --git a/frontend/src/components/Fields/search.jsx b/frontend/src/components/Fields/search.jsx index 8ea3e6b..99d5a8f 100644 --- a/frontend/src/components/Fields/search.jsx +++ b/frontend/src/components/Fields/search.jsx @@ -9,10 +9,11 @@ import SearchIcon from '../../assets/search.svg' import styles from './search.module.css'; // Optional: CSS Modules -function SearchBar({ onSearch }) { +function SearchBar({ onSearch, timeout }) { const [search_input, setSearchInput] = useState(''); - const timeout_id = useRef(null); + const timeout_id = useRef(null); + timeout = timeout | 0; const handleInputChange = (event) => { @@ -25,7 +26,7 @@ function SearchBar({ onSearch }) { timeout_id.current = setTimeout(() => { onSearch(new_search_input); - }, 500); + }, timeout); }; useEffect(() => { diff --git a/frontend/src/styles/home.css b/frontend/src/styles/home.css index ec67126..47019ea 100644 --- a/frontend/src/styles/home.css +++ b/frontend/src/styles/home.css @@ -1,10 +1,35 @@ .halo { - position: fixed; + /* position: fixed; */ + position: relative; z-index: -1; height: 100vh; width: 100vw; - background: radial-gradient(circle at 50% 200%,#353be5 0%, #353be5 45%, #00000000 80%); +} + +.halo::before, +.halo::after { + content: ''; + position: absolute; + top: bottom; + left: 0; + width: 100%; + height: 100%; + + background-size: 100% 100%; + background-repeat: no-repeat; +} + +.halo::before { + background: radial-gradient(circle at 60% 200%,#353be5 0%, #353be5 45%, #00000000 85%); + opacity: 1; + animation: haloBlue 10s infinite alternate; +} + +.halo::after { + background: radial-gradient(circle at 0% 210%,#353be5 10%, #4935e5 45%, #00000000 80%); + opacity: 0; + animation: haloPurple 7s infinite alternate; } .background { @@ -86,4 +111,37 @@ font-size: 1.5em; text-align: center; +} + + +/* Animations ---------------- */ + +@keyframes haloBlue { + 0% { + opacity: 0; + /* background-size: 50% 50%; */ + } + 50% { + opacity: 1; + /* background-size: 150% 150%; */ + } + 100% { + opacity: 0; + /* background-size: 50% 50%; */ + } +} + +@keyframes haloPurple { + 0% { + opacity: 1; + /* background-size: 100% 50%; */ + } + 50% { + opacity: 0; + /* background-size: 100% 100%; */ + } + 100% { + opacity: 1; + /* background-size: 100% 50%; */ + } } \ No newline at end of file