import { PASSWORD_ENTRY, PASSWORD_ENTRY_STORE } from '../../../models/password_entry.ts'; import { USER, USER_STORE } from '../../../models/user.ts'; import { USER_PERMISSIONS, USER_PERMISSIONS_STORE } from '../../../models/user_permissions.ts'; import { generateSecret } from 'jsr:@stdext/crypto/utils'; import { hash } from 'jsr:@stdext/crypto/hash'; import lurid from 'jsr:@andyburke/lurid'; import { encodeBase64 } from 'jsr:@std/encoding'; import parse_body from '../../../utils/bodyparser.ts'; import { get_session, SESSION_RESULT } from '../auth/index.ts'; // TODO: figure out a better solution for doling out permissions const DEFAULT_USER_PERMISSIONS: string[] = [ 'self.read', 'self.write', 'checklists.read', 'checklists.write', 'checklists.events.read', 'checklists.events.write' ]; export const PERMISSIONS: Record) => Promise> = {}; // GET /api/users - get users // query parameters: // partial_id: the partial id subset you would like to match (remember, lurids are lexigraphically sorted) PERMISSIONS.GET = (_req: Request, meta: Record): Promise => { const can_read_others = meta.user_permissions?.permissions?.includes('users.read'); return can_read_others; }; 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 USER_STORE.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 USER_STORE.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, timestamps: { created: now, updated: now } }; await USER_STORE.create(user); const salt = generateSecret(); const hashed_password_value = hash('bcrypt', `${password_hash}${salt}`); const password_entry: PASSWORD_ENTRY = { user_id: user.id, hash: hashed_password_value, salt, timestamps: { created: now, updated: now } }; await PASSWORD_ENTRY_STORE.create(password_entry); const user_permissions: USER_PERMISSIONS = { user_id: user.id, permissions: DEFAULT_USER_PERMISSIONS, timestamps: { created: now, updated: now } }; await USER_PERMISSIONS_STORE.create(user_permissions); const session_result: SESSION_RESULT = await get_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 }); } }