update username regex, normalize usernames, add user info query

This commit is contained in:
creations 2025-03-06 08:30:18 -05:00
parent 774c8e22ce
commit 6fdc82dd49
Signed by: creations
GPG key ID: 8F553AA4320FC711
6 changed files with 127 additions and 7 deletions

View file

@ -66,7 +66,7 @@ export const userNameRestrictions: {
regex: RegExp; regex: RegExp;
} = { } = {
length: { min: 3, max: 20 }, length: { min: 3, max: 20 },
regex: /^[\p{L}0-9._-]+$/u, regex: /^[\p{L}\p{N}._-]+$/u,
}; };
export const passwordRestrictions: { export const passwordRestrictions: {

View file

@ -14,8 +14,8 @@
"@types/bun": "^1.2.4", "@types/bun": "^1.2.4",
"@types/ejs": "^3.1.5", "@types/ejs": "^3.1.5",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@typescript-eslint/eslint-plugin": "^8.25.0", "@typescript-eslint/eslint-plugin": "^8.26.0",
"@typescript-eslint/parser": "^8.25.0", "@typescript-eslint/parser": "^8.26.0",
"eslint": "^9.21.0", "eslint": "^9.21.0",
"eslint-plugin-prettier": "^5.2.3", "eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-promise": "^7.2.1", "eslint-plugin-promise": "^7.2.1",
@ -23,7 +23,7 @@
"eslint-plugin-unicorn": "^56.0.1", "eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"globals": "^15.15.0", "globals": "^15.15.0",
"prettier": "^3.5.2" "prettier": "^3.5.3"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.7.3" "typescript": "^5.7.3"

View file

@ -53,6 +53,7 @@ async function handler(
} }
}); });
const normalizedUsername: string = username.normalize("NFC");
const reservation: ReservedSQL = await sql.reserve(); const reservation: ReservedSQL = await sql.reserve();
let firstUser: boolean = false; let firstUser: boolean = false;
let inviteData: Invite | null = null; let inviteData: Invite | null = null;
@ -91,7 +92,7 @@ async function handler(
const result: { usernameExists: boolean; emailExists: boolean }[] = const result: { usernameExists: boolean; emailExists: boolean }[] =
await reservation` await reservation`
SELECT 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"; EXISTS(SELECT 1 FROM users WHERE LOWER(email) = LOWER(${email})) AS "emailExists";
`; `;
@ -137,7 +138,7 @@ async function handler(
try { try {
const result: User[] = await reservation` const result: User[] = await reservation`
INSERT INTO users (username, email, password, invited_by, roles, timezone) 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 *; RETURNING *;
`; `;

View file

@ -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<Response> {
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 };

View file

@ -3,6 +3,6 @@
<head> <head>
</head> </head>
<body> <body>
<h1> hello </h1>
</body> </body>
</html> </html>

16
types/session.d.ts vendored
View file

@ -37,3 +37,19 @@ type Invite = {
max_uses: number; max_uses: number;
role: string; 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[];
};