158 lines
		
	
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			158 lines
		
	
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { PASSWORD_ENTRIES, PASSWORD_ENTRY } from '../../../models/password_entry.ts';
 | |
| import { USER, USERS } from '../../../models/user.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';
 | |
| 
 | |
| // 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.chat.write',
 | |
| 	'topics.chat.read',
 | |
| 	'topics.threads.write',
 | |
| 	'topics.threads.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<string, any>): 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<string, any>): Promise<Response> {
 | |
| 	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<string, any>): Promise<Response> {
 | |
| 	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);
 | |
| 
 | |
| 		// 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 });
 | |
| 	}
 | |
| }
 |