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 { 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', 'invites.create', 'invites.read.own', 'self.read', 'self.write', 'signups.read.own', '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', 'watches.create.own', 'watches.read.own', 'watches.write.own' ]; 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 at_least_one_existing_user = (await USERS.all({ limit: 1, offset: 0 })).shift()?.load(); let root_invite_code_secret = undefined; if (!at_least_one_existing_user) { root_invite_code_secret = lurid(); const root_invite_code: INVITE_CODE = { id: lurid(), code: root_invite_code_secret, creator_id: new_user_id, timestamps: { created: now, expires: new Date(new Date(now).valueOf() + 1_000).toISOString() } }; await INVITE_CODES.create(root_invite_code); } const secret_code = submitted_invite_code ?? root_invite_code_secret; if (typeof secret_code !== 'string' || secret_code.length < 3) { return Response.json({ error: { cause: 'missing_invite_code', message: 'You need to specify an invite code.' } }, { status: 400 }); } const invite_code: INVITE_CODE | undefined = (await INVITE_CODES.find({ code: secret_code })).shift()?.load(); const is_expired = now >= (invite_code?.timestamps.expires ?? '0000-01-01T00:00:00.000Z'); const is_used = (await SIGNUPS.find({ referring_invite_code_id: invite_code?.id }, { limit: 1 })).length > 0; const is_cancelled = !!invite_code?.timestamps?.cancelled; if (!invite_code || is_expired || is_used || is_cancelled) { 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_used, is_cancelled } } }, { status: 400 }); } const signup: SIGNUP = { id: lurid(), user_id: new_user_id, invite_code_id: invite_code?.id ?? 'able-able-able-able-able-able-able-able-able-able', referring_user_id: invite_code?.creator_id ?? new_user_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 }); } }