import { PASSWORD_ENTRIES, PASSWORD_ENTRY } from '../../../models/password_entry.ts'; import { USER, USERS } from '../../../models/user.ts'; import lurid from 'jsr:@andyburke/lurid'; import { encodeBase64 } from 'jsr:@std/encoding'; import parse_body from '../../../utils/bodyparser.ts'; import { create_new_session, SESSION_RESULT } from '../auth/index.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../utils/prechecks.ts'; import * as CANNED_RESPONSES from '../../../utils/canned_responses.ts'; import * as bcrypt from 'jsr:@da/bcrypt'; // TODO: figure out a better solution for doling out permissions const DEFAULT_USER_PERMISSIONS: string[] = [ 'self.read', 'self.write', 'rooms.read' ]; export const PRECHECKS: PRECHECK_TABLE = {}; // GET /api/users - get users // query parameters: // partial_id: the partial id subset you would like to match (remember, lurids are lexigraphically sorted) PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { const can_read_others = meta.user?.permissions?.includes('users.read'); if (!can_read_others) { return CANNED_RESPONSES.permission_denied(); } }]; export async function GET(_req: Request, meta: Record): Promise { const query: URLSearchParams = meta.query; const partial_id: string | undefined = query.get('partial_id')?.toLowerCase().trim(); const has_partial_id = typeof partial_id === 'string' && partial_id.length >= 2; if (!has_partial_id) { return Response.json({ error: { message: 'You must specify a `partial_id` query parameter.', cause: 'missing_query_parameter' } }, { status: 400 }); } const limit = Math.min(parseInt(query.get('limit') ?? '10'), 100); const users = await USERS.find({ id: partial_id }, { limit }); return Response.json(users, { status: 200 }); } // POST /api/users - Create user export async function POST(req: Request, meta: Record): Promise { try { const now = new Date().toISOString(); const body = await parse_body(req); const username: string = body.username?.trim() ?? ''; const normalized_username = username.toLowerCase(); const existing_user_with_username = (await USERS.find({ normalized_username })).shift(); if (existing_user_with_username) { return Response.json({ error: { cause: 'username_conflict', message: 'Username is already in use.' } }, { status: 400 }); } const password_hash: string = body.password_hash ?? (typeof body.password === 'string' ? encodeBase64( await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body.password)) ) : ''); if (password_hash.length < 32) { return Response.json({ error: { cause: 'invalid password hash', message: 'Password must be hashed with a stronger algorithm.' } }, { status: 400 }); } const user: USER = { id: lurid(), username, permissions: DEFAULT_USER_PERMISSIONS, timestamps: { created: now, updated: now } }; await USERS.create(user); const salt = await bcrypt.genSalt(); const hashed_password_value = await bcrypt.hash(password_hash, salt); const password_entry: PASSWORD_ENTRY = { user_id: user.id, hash: hashed_password_value, salt, timestamps: { created: now, updated: now } }; await PASSWORD_ENTRIES.create(password_entry); const session_result: SESSION_RESULT = await create_new_session({ user, expires: undefined }); // TODO: verify this redirect is ok? const headers = session_result.headers; let status = 201; if (typeof meta?.query?.redirect === 'string') { const url = new URL(req.url); headers.append('location', `${url.origin}${meta.query.redirect}`); status = 302; } return Response.json({ user, session: session_result.session }, { status, headers }); } catch (error) { return Response.json({ error: { message: (error as Error).message ?? 'Unknown Error!', cause: (error as Error).cause ?? 'unknown' } }, { status: 500 }); } }