atums.world/config/sql/users.ts

174 lines
4.2 KiB
TypeScript

import { logger } from "@helpers/logger";
import { type ReservedSQL, sql } from "bun";
export async function createTable(reservation?: ReservedSQL): Promise<void> {
let selfReservation: boolean = false;
if (!reservation) {
reservation = await sql.reserve();
selfReservation = true;
}
try {
await reservation`
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
authorization_token UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
username VARCHAR(20) NOT NULL UNIQUE,
email VARCHAR(254) NOT NULL UNIQUE,
email_verified boolean NOT NULL DEFAULT false,
password TEXT NOT NULL,
avatar boolean NOT NULL DEFAULT false,
roles TEXT[] NOT NULL DEFAULT ARRAY['user'],
timezone VARCHAR(64) DEFAULT NULL,
invited_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
last_seen TIMESTAMPTZ DEFAULT NOW() NOT NULL
);`;
} catch (error) {
logger.error(["Could not create the users table:", error as Error]);
throw error;
} finally {
if (selfReservation) {
reservation.release();
}
}
}
export async function drop(
cascade: boolean,
reservation?: ReservedSQL,
): Promise<void> {
let selfReservation: boolean = false;
if (!reservation) {
reservation = await sql.reserve();
selfReservation = true;
}
try {
await reservation`DROP TABLE IF EXISTS users ${cascade ? "CASCADE" : ""};`;
} catch (error) {
logger.error(["Could not drop the users table:", error as Error]);
throw error;
} finally {
if (selfReservation) {
reservation.release();
}
}
}
// * Validation functions
// ? should support non english characters but wont mess up the url
export const userNameRestrictions: {
length: { min: number; max: number };
regex: RegExp;
} = {
length: { min: 3, max: 20 },
regex: /^[\p{L}0-9._-]+$/u,
};
export const passwordRestrictions: {
length: { min: number; max: number };
regex: RegExp;
} = {
length: { min: 12, max: 64 },
regex: /^(?=.*\p{Ll})(?=.*\p{Lu})(?=.*\d)(?=.*[^\w\s]).{12,64}$/u,
};
export const emailRestrictions: { regex: RegExp } = {
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
};
export const inviteRestrictions: { min: number; max: number; regex: RegExp } = {
min: 4,
max: 15,
regex: /^[a-zA-Z0-9]+$/,
};
export function isValidUsername(username: string): {
valid: boolean;
error?: string;
} {
if (username.length < userNameRestrictions.length.min) {
return { valid: false, error: "Username is too short" };
}
if (username.length > userNameRestrictions.length.max) {
return { valid: false, error: "Username is too long" };
}
if (!userNameRestrictions.regex.test(username)) {
return { valid: false, error: "Username contains invalid characters" };
}
return { valid: true };
}
export function isValidPassword(password: string): {
valid: boolean;
error?: string;
} {
if (password.length < passwordRestrictions.length.min) {
return {
valid: false,
error: `Password must be at least ${passwordRestrictions.length.min} characters long`,
};
}
if (password.length > passwordRestrictions.length.max) {
return {
valid: false,
error: `Password can't be longer than ${passwordRestrictions.length.max} characters`,
};
}
if (!passwordRestrictions.regex.test(password)) {
return {
valid: false,
error: "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character",
};
}
return { valid: true };
}
export function isValidEmail(email: string): {
valid: boolean;
error?: string;
} {
if (!emailRestrictions.regex.test(email)) {
return { valid: false, error: "Invalid email address" };
}
return { valid: true };
}
export function isValidInvite(invite: string): {
valid: boolean;
error?: string;
} {
if (invite.length < inviteRestrictions.min) {
return {
valid: false,
error: `Invite code must be at least ${inviteRestrictions.min} characters long`,
};
}
if (invite.length > inviteRestrictions.max) {
return {
valid: false,
error: `Invite code can't be longer than ${inviteRestrictions.max} characters`,
};
}
if (!inviteRestrictions.regex.test(invite)) {
return {
valid: false,
error: "Invite code contains invalid characters",
};
}
return { valid: true };
}