import { PASSWORD_ENTRIES, PASSWORD_ENTRY } from '../../../models/password_entry.ts'; import { USER, USERS } from '../../../models/user.ts'; import { SIGNUP, SIGNUPS } from '../../../models/signups.ts'; import lurid from '@andyburke/lurid'; import { encodeBase64 } from '@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 '@da/bcrypt'; import { WALK_ENTRY } from '@andyburke/fsdb'; import { INVITE_CODE, INVITE_CODES } from '../../../models/invites.ts'; // TODO: figure out a better solution for doling out permissions const DEFAULT_USER_PERMISSIONS: string[] = [ 'files.write.own', 'self.read', 'self.write', 'topics.read', 'topics.blurbs.create', 'topics.blurbs.read', 'topics.blurbs.write', 'topics.chat.write', 'topics.chat.read', 'topics.essays.create', 'topics.essays.read', 'topics.essays.write', 'topics.posts.create', 'topics.posts.write', 'topics.posts.read', 'users.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 new_user_id = lurid(); const now = new Date().toISOString(); const body = await parse_body(req); const username: string = body.username?.trim() ?? ''; const normalized_username = username.toLowerCase(); const submitted_invite_code = body.invite_code?.trim(); 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 invite_code: INVITE_CODE | undefined = typeof submitted_invite_code === 'string' && submitted_invite_code.length ? (await INVITE_CODES.find({ code: submitted_invite_code })).shift()?.load() : undefined; const is_expired = invite_code?.timestamps.expires ? now <= invite_code.timestamps.expires : true; const is_limited = invite_code?.limit ? (await SIGNUPS.find({ referring_invite_code_id: invite_code.id }, { limit: invite_code.limit })).length >= invite_code.limit : false; if (!invite_code || is_expired || is_limited) { return Response.json({ error: { cause: 'invalid_signup_code', message: 'Could not find an active signup code given this information.', meta: { exists: !!invite_code, is_expired, is_limited } } }, { status: 400 }); } const signup: SIGNUP = { id: lurid(), user_id: new_user_id, invite_code_id: invite_code.id, referring_user_id: invite_code.creator_id, timestamps: { created: now } }; await SIGNUPS.create(signup); const user: USER = { id: new_user_id, username, permissions: DEFAULT_USER_PERMISSIONS, timestamps: { created: now, updated: now } }; await USERS.create(user); // automatically salted const hashed_password_value = bcrypt.hashSync(password_hash); const password_entry: PASSWORD_ENTRY = { user_id: user.id, hash: hashed_password_value, 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 }); } }