diff --git a/config/sql/users.ts b/config/sql/users.ts index 24f90ff..118c6aa 100644 --- a/config/sql/users.ts +++ b/config/sql/users.ts @@ -66,7 +66,7 @@ export const userNameRestrictions: { regex: RegExp; } = { length: { min: 3, max: 20 }, - regex: /^[\p{L}0-9._-]+$/u, + regex: /^[\p{L}\p{N}._-]+$/u, }; export const passwordRestrictions: { diff --git a/package.json b/package.json index 5e5ed9d..8570105 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "@types/bun": "^1.2.4", "@types/ejs": "^3.1.5", "@types/luxon": "^3.4.2", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "@typescript-eslint/parser": "^8.25.0", + "@typescript-eslint/eslint-plugin": "^8.26.0", + "@typescript-eslint/parser": "^8.26.0", "eslint": "^9.21.0", "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-promise": "^7.2.1", @@ -23,7 +23,7 @@ "eslint-plugin-unicorn": "^56.0.1", "eslint-plugin-unused-imports": "^4.1.4", "globals": "^15.15.0", - "prettier": "^3.5.2" + "prettier": "^3.5.3" }, "peerDependencies": { "typescript": "^5.7.3" diff --git a/src/routes/api/auth/register.ts b/src/routes/api/auth/register.ts index 20277a8..26b625d 100644 --- a/src/routes/api/auth/register.ts +++ b/src/routes/api/auth/register.ts @@ -53,6 +53,7 @@ async function handler( } }); + const normalizedUsername: string = username.normalize("NFC"); const reservation: ReservedSQL = await sql.reserve(); let firstUser: boolean = false; let inviteData: Invite | null = null; @@ -91,7 +92,7 @@ async function handler( const result: { usernameExists: boolean; emailExists: boolean }[] = await reservation` SELECT - EXISTS(SELECT 1 FROM users WHERE LOWER(username) = LOWER(${username})) AS "usernameExists", + EXISTS(SELECT 1 FROM users WHERE LOWER(username) = LOWER(${normalizedUsername})) AS "usernameExists", EXISTS(SELECT 1 FROM users WHERE LOWER(email) = LOWER(${email})) AS "emailExists"; `; @@ -137,7 +138,7 @@ async function handler( try { const result: User[] = await reservation` INSERT INTO users (username, email, password, invited_by, roles, timezone) - VALUES (${username}, ${email}, ${hashedPassword}, ${inviteData?.created_by}, ARRAY[${roles.join(",")}]::TEXT[], ${defaultTimezone}) + VALUES (${normalizedUsername}, ${email}, ${hashedPassword}, ${inviteData?.created_by}, ARRAY[${roles.join(",")}]::TEXT[], ${defaultTimezone}) RETURNING *; `; diff --git a/src/routes/api/user/info[query].ts b/src/routes/api/user/info[query].ts new file mode 100644 index 0000000..a9f4851 --- /dev/null +++ b/src/routes/api/user/info[query].ts @@ -0,0 +1,103 @@ +import { isValidUsername } from "@config/sql/users"; +import { sql } from "bun"; + +import { isUUID } from "@/helpers/char"; +import { logger } from "@/helpers/logger"; + +const routeDef: RouteDef = { + method: "GET", + accepts: "*/*", + returns: "application/json", +}; + +async function handler(request: ExtendedRequest): Promise { + const { query } = request.params as { query: string }; + const { invites: showInvites } = request.query as { invites: string }; + + if (!query) { + return Response.json( + { + success: false, + code: 400, + error: "Username or user ID is required", + }, + { status: 400 }, + ); + } + + let user: GetUser | null = null; + let isSelf: boolean = false; + const isId: boolean = isUUID(query); + const normalized: string = isId ? query : query.normalize("NFC"); + const isAdmin: boolean = request.session + ? request.session.roles.includes("admin") + : false; + + if (!isId && !isValidUsername(normalized).valid) { + return Response.json( + { + success: false, + code: 400, + error: "Invalid username", + }, + { status: 400 }, + ); + } + + try { + const result: GetUser[] = isId + ? await sql`SELECT * FROM users WHERE id = ${normalized}` + : await sql`SELECT * FROM users WHERE username = ${normalized}`; + + if (result.length === 0) { + return Response.json( + { + success: false, + code: 404, + error: "User not found", + }, + { status: 404 }, + ); + } + + user = result[0]; + isSelf = request.session ? user.id === request.session.id : false; + + if (showInvites === "true" && (isAdmin || isSelf)) { + const invites: Invite[] = + await sql`SELECT * FROM invites WHERE created_by = ${user.id}`; + user.invites = invites; + } + } catch (error) { + logger.error([ + "An error occurred while fetching user data", + error as Error, + ]); + + return Response.json( + { + success: false, + code: 500, + error: "An error occurred while fetching user data", + }, + { status: 500 }, + ); + } + + delete user.password; + delete user.authorization_token; + if (!isSelf) delete user.email; + + user.roles = user.roles ? user.roles[0].split(",") : []; + + return Response.json( + { + success: true, + code: 200, + user: user, + }, + { status: 200 }, + ); +} + +export { handler, routeDef }; diff --git a/src/views/index.ejs b/src/views/index.ejs index e8f6e0b..541eee1 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -3,6 +3,6 @@ -

hello

+ diff --git a/types/session.d.ts b/types/session.d.ts index 02415a4..ca8f840 100644 --- a/types/session.d.ts +++ b/types/session.d.ts @@ -37,3 +37,19 @@ type Invite = { max_uses: number; role: string; }; + +type GetUser = { + id?: UUID; + authorization_token?: UUID; + username?: string; + email?: string; + email_verified?: boolean; + password?: string; + avatar?: boolean; + roles?: string[]; + timezone?: string; + invited_by?: UUID; + created_at?: Date; + last_seen?: Date; + invites?: Invite[]; +};