import { drizzle } from 'drizzle-orm/bun-sqlite'; import { mkdir } from "node:fs/promises"; import path from "node:path"; import { eq, lte } from 'drizzle-orm'; import { SESSION_TOKEN_DURATION_MS } from '@/lib/constants.ts'; import * as schema from './schema/schema.ts'; import type { User, Session, Blog } from '@/lib/types.ts'; import { UserEmailExistsError, InvalidPasswordError, UserNotFoundError, SubdomainAlreadyUsedError, InvalidSessionError } from '@/lib/errors.ts'; await mkdir(path.dirname(Bun.env.DB_URL), { recursive: true }); export const db = drizzle(process.env.DB_URL, schema); /** * Creates a new user, PASSWORD UNHASHED. Will error with `UserEmailExistsError()` if email is already taken. * @param input */ export async function new_user(input: User) { input.password = await Bun.password.hash(input.password); const result = db.insert(schema.users).values(input).onConflictDoNothing().returning().get(); // if it works, great! if (!result) throw new UserEmailExistsError(); else return result; } /** * Deletes an user. * @param {number} id */ export async function delete_user(id: number) { const result = db.delete(schema.users).where(eq(schema.users.id, id)).returning() } /** * Retrive a user by it's ID. Will error with `UserNotFoundError` if the id doesn't exist. * @param id User's ID. * @returns User. */ export async function get_user(id: number) { const [user] = await db.select().from(schema.users).where(eq(schema.users.id, id)); if (!user) throw new UserNotFoundError(); return user; } /** * Creates a new blog. Will error with `SubdomainAlreadyUsedError` if the subdomain's taken. * @param blog Blog without id. * @returns Blog with id. */ export async function new_blog(blog: Blog) { const result = db.insert(schema.blogs).values(blog).onConflictDoNothing().returning().get(); // if it works, great! if (!result) throw new SubdomainAlreadyUsedError(); return result as Blog; } /** * Takes in the email and alleged RAW PASSWORD, returns User. Might error with `UserNotFoundError` or `InvalidPasswordError` * @param {string} email email. * @param {string} password * @returns User. */ export async function validate_login(email: string, password: string): Promise { const user = db.select().from(schema.users).where(eq(schema.users.email, email)).get(); if (!user) throw new UserNotFoundError(); if (await Bun.password.verify(password, user.password)) { return user as User; } else { throw new InvalidPasswordError() } } /** * Creates a long-lived session token for a user to store in HTTP-ONLY COOKIES. * @param user_id User's ID. * @returns Session object. */ export async function create_session(user_id: number): Promise { const new_refresh_token = { user_id, token: Bun.randomUUIDv7() // issued_at is handled by the database's default `unixepoch()` }; try { await db.insert(schema.sessions).values(new_refresh_token).returning(); } catch (e) { console.error(e); } return new_refresh_token as Session; }; /** * Checks if a session is valid. * @param session_token UUID. * @returns {Session} Session or throws `InvalidSessionError` */ export async function validate_session(session_token: string): Promise { const [session] = await db.select().from(schema.sessions).where(eq(schema.sessions.token, session_token)); if (!session) throw new InvalidSessionError(); if (session.issued_at < new Date(Date.now() - SESSION_TOKEN_DURATION_MS)) { await delete_session(session.token); throw new InvalidSessionError(); }; return session; } /** * Deletes a given session. * @param session Session token/UUID. * @returns {boolean} True if deleted, false if there wasn't anything like it. */ export async function delete_session(session: string): Promise { const result = await db.delete(schema.sessions).where(eq(schema.sessions.token, session)).returning(); if (result === undefined) return true; else return false; } /** * Gets rid of all expired sessions. * @returns A number with the amount of expired sessions removed. */ export async function cleanup_sessions(): Promise { const result = await db.delete(schema.sessions) .where(lte(schema.sessions.issued_at, new Date(Date.now() - SESSION_TOKEN_DURATION_MS))).returning() // dates lower (older) than current date - expiry date are removed return result.length; }