From 2c27f003c93cab884b77b9eff1a66feaa67f5f60 Mon Sep 17 00:00:00 2001 From: Andy Burke Date: Tue, 24 Jun 2025 15:40:30 -0700 Subject: [PATCH] feature: initial commit --- .gitignore | 2 + README.md | 0 deno.json | 41 ++ deno.lock | 92 ++++ models/password_entry.ts | 16 + models/session.ts | 25 ++ models/totp_entry.ts | 15 + models/user.ts | 37 ++ models/user_permissions.ts | 15 + public/api/auth/README.md | 16 + public/api/auth/index.ts | 192 ++++++++ public/api/users/:id/index.ts | 149 +++++++ public/api/users/index.ts | 164 +++++++ public/index.html | 819 ++++++++++++++++++++++++++++++++++ utils/bodyparser.ts | 18 + 15 files changed, 1601 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 models/password_entry.ts create mode 100644 models/session.ts create mode 100644 models/totp_entry.ts create mode 100644 models/user.ts create mode 100644 models/user_permissions.ts create mode 100644 public/api/auth/README.md create mode 100644 public/api/auth/index.ts create mode 100644 public/api/users/:id/index.ts create mode 100644 public/api/users/index.ts create mode 100644 public/index.html create mode 100644 utils/bodyparser.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..401422f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +data/ +.fsdb diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..89e4bcd --- /dev/null +++ b/deno.json @@ -0,0 +1,41 @@ +{ + "name": "@andyburke/autonomous.contact", + "description": "An experiment.", + "version": "0.0.1", + "license": "MIT", + "exports": {}, + "tasks": { + "lint": "deno lint", + "fmt": "deno fmt", + "serve": "FSDB_ROOT=$PWD/.fsdb deno --allow-env --allow-read --allow-write --allow-net jsr:@andyburke/serverus --root ./public" + }, + "test": { + "exclude": ["tests/data/"] + }, + "fmt": { + "include": ["**/*.ts"], + "options": { + "useTabs": true, + "lineWidth": 140, + "indentWidth": 4, + "singleQuote": true, + "proseWrap": "preserve", + "trailingCommas": "never" + } + }, + "lint": { + "include": ["**/*.ts"], + "rules": { + "tags": ["recommended"], + "exclude": ["no-explicit-any"] + } + }, + "imports": { + "@andyburke/fsdb": "jsr:@andyburke/fsdb@^0.4.0", + "@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0", + "@andyburke/serverus": "jsr:@andyburke/serverus@^0.0.12", + "@std/encoding": "jsr:@std/encoding@^1.0.10", + "@std/path": "jsr:@std/path@^1.1.0", + "@stdext/crypto": "jsr:@stdext/crypto@^0.1.0" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..4246f0c --- /dev/null +++ b/deno.lock @@ -0,0 +1,92 @@ +{ + "version": "5", + "specifiers": { + "jsr:@andyburke/fsdb@0.4": "0.4.0", + "jsr:@andyburke/lurid@0.2": "0.2.0", + "jsr:@andyburke/serverus@^0.0.12": "0.0.12", + "jsr:@std/async@^1.0.13": "1.0.13", + "jsr:@std/cli@^1.0.19": "1.0.20", + "jsr:@std/cli@^1.0.20": "1.0.20", + "jsr:@std/encoding@1": "1.0.10", + "jsr:@std/encoding@^1.0.10": "1.0.10", + "jsr:@std/fmt@^1.0.6": "1.0.8", + "jsr:@std/fs@^1.0.14": "1.0.18", + "jsr:@std/fs@^1.0.18": "1.0.18", + "jsr:@std/http@^1.0.13": "1.0.17", + "jsr:@std/media-types@^1.1.0": "1.1.0", + "jsr:@std/path@^1.0.8": "1.1.0", + "jsr:@std/path@^1.1.0": "1.1.0", + "jsr:@stdext/crypto@0.1": "0.1.0" + }, + "jsr": { + "@andyburke/fsdb@0.4.0": { + "integrity": "13ff46528835e6eaf5ff57fcd1bdd97020d608e6d1e03a38be0d162d1bbbace1", + "dependencies": [ + "jsr:@std/cli@^1.0.20", + "jsr:@std/fs@^1.0.18", + "jsr:@std/path@^1.1.0" + ] + }, + "@andyburke/lurid@0.2.0": { + "integrity": "c5b51e56ef8457b9ef56c060bd9db817a90d8e4784506e348110900286574ce5", + "dependencies": [ + "jsr:@std/cli@^1.0.19" + ] + }, + "@andyburke/serverus@0.0.12": { + "integrity": "051cbffd30577e39ca604e009c3870c4b32b7e4118f2da58fc18ec05afa5b5bb", + "dependencies": [ + "jsr:@std/async", + "jsr:@std/cli@^1.0.19", + "jsr:@std/fmt", + "jsr:@std/fs@^1.0.14", + "jsr:@std/http", + "jsr:@std/media-types", + "jsr:@std/path@^1.0.8" + ] + }, + "@std/async@1.0.13": { + "integrity": "1d76ca5d324aef249908f7f7fe0d39aaf53198e5420604a59ab5c035adc97c96" + }, + "@std/cli@1.0.20": { + "integrity": "a8c384a2c98cec6ec6a2055c273a916e2772485eb784af0db004c5ab8ba52333" + }, + "@std/encoding@1.0.10": { + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" + }, + "@std/fmt@1.0.8": { + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" + }, + "@std/fs@1.0.18": { + "integrity": "24bcad99eab1af4fde75e05da6e9ed0e0dce5edb71b7e34baacf86ffe3969f3a", + "dependencies": [ + "jsr:@std/path@^1.1.0" + ] + }, + "@std/http@1.0.17": { + "integrity": "98aec8ab4080d95c21f731e3008f69c29c5012d12f1b4e553f85935db601569f" + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/path@1.1.0": { + "integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886" + }, + "@stdext/crypto@0.1.0": { + "integrity": "05dc9e754c2529574d8bf98bd40c7dc468a02dcb2fa5e8644fff6813ceab66a4", + "dependencies": [ + "jsr:@std/encoding@1" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@andyburke/fsdb@0.4", + "jsr:@andyburke/lurid@0.2", + "jsr:@andyburke/serverus@^0.0.12", + "jsr:@std/encoding@^1.0.10", + "jsr:@std/path@^1.1.0", + "jsr:@stdext/crypto@0.1" + ] + } +} diff --git a/models/password_entry.ts b/models/password_entry.ts new file mode 100644 index 0000000..18d2bbb --- /dev/null +++ b/models/password_entry.ts @@ -0,0 +1,16 @@ +import { FSDB_COLLECTION } from 'jsr:@andyburke/fsdb'; + +export type PASSWORD_ENTRY = { + user_id: string; + hash: string; + salt: string; + timestamps: { + created: string; + updated: string; + }; +}; + +export const PASSWORD_ENTRY_STORE = new FSDB_COLLECTION({ + name: 'password_entries', + id_field: 'user_id' +}); diff --git a/models/session.ts b/models/session.ts new file mode 100644 index 0000000..b88241f --- /dev/null +++ b/models/session.ts @@ -0,0 +1,25 @@ +import { FSDB_COLLECTION } from 'jsr:@andyburke/fsdb'; +import { FSDB_INDEXER_SYMLINKS } from 'jsr:@andyburke/fsdb/indexers'; +import { by_lurid } from 'jsr:@andyburke/fsdb/organizers'; + +export type SESSION = { + id: string; + user_id: string; + secret: string; + timestamps: { + created: string; + expires: string; + ended: string; + }; +}; + +export const SESSIONS = new FSDB_COLLECTION({ + name: 'sessions', + indexers: { + user_id: new FSDB_INDEXER_SYMLINKS({ + name: 'user_id', + field: 'user_id', + organize: by_lurid + }) + } +}); diff --git a/models/totp_entry.ts b/models/totp_entry.ts new file mode 100644 index 0000000..e8dd6ff --- /dev/null +++ b/models/totp_entry.ts @@ -0,0 +1,15 @@ +import { FSDB_COLLECTION } from 'jsr:@andyburke/fsdb'; + +export type TOTP_ENTRY = { + user_id: string; + secret: string; + timestamps: { + created: string; + updated: string; + }; +}; + +export const TOTP_ENTRY_STORE = new FSDB_COLLECTION({ + name: 'totp_entries', + id_field: 'user_id' +}); diff --git a/models/user.ts b/models/user.ts new file mode 100644 index 0000000..d2de256 --- /dev/null +++ b/models/user.ts @@ -0,0 +1,37 @@ +import { FSDB_COLLECTION } from 'jsr:@andyburke/fsdb'; +import { FSDB_INDEXER_SYMLINKS } from 'jsr:@andyburke/fsdb/indexers'; +import { by_character } from 'jsr:@andyburke/fsdb/organizers'; + +export type USER = { + id: string; + username: string; + timestamps: { + created: string; + updated: string; + }; +}; + +export const USER_STORE = new FSDB_COLLECTION({ + name: 'users', + indexers: { + // email: new FSDB_INDEXER_SYMLINKS({ + // name: 'email', + // field: 'email', + // organize: by_email + // }), + + username: new FSDB_INDEXER_SYMLINKS({ + name: 'username', + field: 'username', + organize: by_character + }), + + normalized_username: new FSDB_INDEXER_SYMLINKS({ + name: 'normalized_username', + get_values_to_index: (user) => { + return [user.username.toLowerCase()]; + }, + organize: by_character + }) + } +}); diff --git a/models/user_permissions.ts b/models/user_permissions.ts new file mode 100644 index 0000000..f939c66 --- /dev/null +++ b/models/user_permissions.ts @@ -0,0 +1,15 @@ +import { FSDB_COLLECTION } from 'jsr:@andyburke/fsdb'; + +export type USER_PERMISSIONS = { + user_id: string; + permissions: string[]; + timestamps: { + created: string; + updated: string; + }; +}; + +export const USER_PERMISSIONS_STORE = new FSDB_COLLECTION({ + name: 'user_permissions', + id_field: 'user_id' +}); diff --git a/public/api/auth/README.md b/public/api/auth/README.md new file mode 100644 index 0000000..0aea2b1 --- /dev/null +++ b/public/api/auth/README.md @@ -0,0 +1,16 @@ +# /api/auth + +Authentication for the service. + +## POST /api/auth + +Log into the service. + +``` +{ + email?: string; // either email or username must be specified + username?: string; + password_hash: string; //should be a base64-encoded SHA-256 hash of the user's client-entered pw + totp?: string; // TOTP if configured on account +} +``` diff --git a/public/api/auth/index.ts b/public/api/auth/index.ts new file mode 100644 index 0000000..d5e6fce --- /dev/null +++ b/public/api/auth/index.ts @@ -0,0 +1,192 @@ +import { PASSWORD_ENTRY_STORE } from '../../../models/password_entry.ts'; +import { USER, USER_STORE } from '../../../models/user.ts'; +import { generateSecret } from 'jsr:@stdext/crypto/utils'; +import { encodeBase32 } from 'jsr:@std/encoding'; +import { verify } from 'jsr:@stdext/crypto/hash'; +import lurid from 'jsr:@andyburke/lurid'; +import { SESSION, SESSIONS } from '../../../models/session.ts'; +import { TOTP_ENTRY_STORE } from '../../../models/totp_entry.ts'; +import { verifyTotp } from 'jsr:@stdext/crypto/totp'; +import { encodeBase64 } from 'jsr:@std/encoding/base64'; +import parse_body from '../../../utils/bodyparser.ts'; + +const DEFAULT_SESSION_TIME: number = 60 * 60; // 1 Hour + +// POST /api/auth - Authenticate +export async function POST(req: Request, meta: Record): Promise { + try { + const body = await parse_body(req); + + const email: string = body.email?.toLowerCase().trim() ?? ''; + const username: string = body.username?.toLowerCase() ?? ''; + const password: string | undefined = body.password; + const password_hash: string | undefined = body.password_hash ?? (typeof password === 'string' + ? encodeBase64( + await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password ?? '')) + ) + : undefined); + const totp: string | undefined = body.totp; + + if ((!email.length && !username.length) || (email.length && username.length)) { + return Response.json({ + error: { + message: 'You must specify either an email or username to log in.', + cause: 'email_or_username_required' + } + }, { + status: 400 + }); + } + + // password has should be a base64-encoded SHA-256 hash + if (typeof password_hash !== 'string') { + return Response.json({ + error: { + message: 'invalid password hash', + cause: 'invalid_password_hash' + } + }, { + status: 400 + }); + } + + let user: USER | undefined = undefined; + if (email.length) { + user = (await USER_STORE.find({ + email + })).shift(); + } else if (username.length) { + user = (await USER_STORE.find({ + username + })).shift(); + } + + if (!user) { + return Response.json({ + error: { + message: 'Could not locate an account with this email or username.', + cause: 'missing_account' + } + }, { + status: 400 + }); + } + + const password_entry = await PASSWORD_ENTRY_STORE.get(user.id); + if (!password_entry) { + return Response.json({ + error: { + message: 'Missing password entry for this account, please contact support.', + cause: 'missing_password_entry' + } + }, { + status: 500 + }); + } + + const verified = verify('bcrypt', `${password_hash}${password_entry.salt}`, password_entry.hash); + if (!verified) { + return Response.json({ + error: { + message: 'Incorrect password.', + cause: 'incorrect_password' + } + }, { + status: 400 + }); + } + + const totp_entry = await TOTP_ENTRY_STORE.get(user.id); + if (totp_entry) { + if (typeof totp !== 'string' || !totp.length) { + return Response.json({ + two_factor: true, + totp: true + }, { + status: 202, + headers: { + 'Set-Cookie': `checklist_observer_totp_user_id=${user.id}; Path=/; Max-Age=300` + } + }); + } + + const valid_totp: boolean = await verifyTotp(totp, totp_entry.secret); + if (!valid_totp) { + return Response.json({ + error: { + message: 'Incorrect TOTP.', + cause: 'incorrect_totp' + } + }, { + status: 400 + }); + } + } + + const session_result: SESSION_RESULT = await get_session({ + user, + expires: body.session?.expires + }); + + // TODO: verify this redirect is relative? + const headers = session_result.headers; + let status = 201; + if (typeof meta?.query?.redirect === 'string') { + const url = new URL(req.url); + session_result.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: 400 }); + } +} + +export type SESSION_RESULT = { + session: SESSION; + headers: Headers; +}; + +export type SESSION_INFO = { + user: USER; + expires: string | undefined; +}; + +export async function get_session(session_settings: SESSION_INFO): Promise { + const now = new Date().toISOString(); + const expires: string = session_settings.expires ?? + new Date(new Date(now).valueOf() + DEFAULT_SESSION_TIME).toISOString(); + + const session: SESSION = { + id: lurid(), + user_id: session_settings.user.id, + secret: encodeBase32(generateSecret()), + timestamps: { + created: now, + expires, + ended: '' + } + }; + + await SESSIONS.create(session); + + const headers = new Headers(); + headers.append('Set-Cookie', `checklist_observer_session_id=${session.id}; Path=/; Expires=${expires}`); + + return { + session, + headers + }; +} diff --git a/public/api/users/:id/index.ts b/public/api/users/:id/index.ts new file mode 100644 index 0000000..5f55ac6 --- /dev/null +++ b/public/api/users/:id/index.ts @@ -0,0 +1,149 @@ +import { PASSWORD_ENTRY, PASSWORD_ENTRY_STORE } from '../../../../models/password_entry.ts'; +import { SESSIONS } from '../../../../models/session.ts'; +import { USER, USER_STORE } from '../../../../models/user.ts'; +import { USER_PERMISSIONS, USER_PERMISSIONS_STORE } from '../../../../models/user_permissions.ts'; +import parse_body from '../../../../utils/bodyparser.ts'; + +export const PERMISSIONS: Record) => Promise> = {}; + +// GET /api/users/:id - Get single user +PERMISSIONS.GET = (_req: Request, meta: Record): Promise => { + const user_is_self = !!meta.user && !!meta.params && meta.user.id === meta.params.id; + const can_read_self = meta.user_permissions?.permissions.includes('self.read'); + const can_read_others = meta.user_permissions?.permissions?.includes('users.read'); + + return can_read_others || (can_read_self && user_is_self); +}; +export async function GET(_req: Request, meta: Record): Promise { + const user_id: string = meta.params?.id?.toLowerCase().trim() ?? ''; + const user: USER | null = user_id.length === 49 ? await USER_STORE.get(user_id) : null; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" + + if (!user) { + return Response.json({ + error: { + message: `Could not locate a user with id: "${user_id}"`, + cause: 'unknown_user' + } + }, { + status: 404 + }); + } + + const user_is_self = meta.user?.id === user.id; + const has_permission_to_read = (user_is_self && meta.user_permissions?.permissions?.includes('self.read')) || + (meta.user_permissions?.permissions?.includes('users.read')); + + if (!has_permission_to_read) { + return Response.json({ + error: { + message: 'Permission denied.', + cause: 'permission_denied' + } + }, { + status: 400 + }); + } + + return Response.json(user, { + status: 200 + }); +} + +// PUT /api/users/:id - Update user +PERMISSIONS.PUT = (_req: Request, meta: Record): Promise => { + const user_is_self = !!meta.user && !!meta.params && meta.user.id === meta.params.id; + const can_write_self = meta.user_permissions?.permissions.includes('self.write'); + const can_write_others = meta.user_permissions?.permissions?.includes('users.write'); + + return can_write_others || (can_write_self && user_is_self); +}; +export async function PUT(req: Request, meta: { params: Record }): Promise { + const now = new Date().toISOString(); + const id: string = meta.params.id ?? ''; + const existing = await USER_STORE.get(id); + + if (!existing) { + return Response.json({ + error: { + message: 'User not found', + cause: 'unknown_user' + } + }, { + status: 404 + }); + } + + try { + const body = await parse_body(req); + const updated = { + ...existing, + username: body.username || existing.username, + timestamps: { + created: existing.timestamps.created, + updated: now + } + }; + + await USER_STORE.update(updated); + return Response.json(updated, { + status: 200 + }); + } catch (err) { + return Response.json({ + error: { + message: (err as Error)?.message ?? 'Unknown error due to invalid user data.', + cause: (err as Error)?.cause ?? 'invalid_user_data' + } + }, { + status: 400 + }); + } +} + +// DELETE /api/users/:id - Delete user +PERMISSIONS.DELETE = (_req: Request, meta: Record): Promise => { + const user_is_self = !!meta.user && !!meta.params && meta.user.id === meta.params.id; + const can_write_self = meta.user_permissions?.permissions.includes('self.write'); + const can_write_others = meta.user_permissions?.permissions?.includes('users.write'); + + return can_write_others || (can_write_self && user_is_self); +}; +export async function DELETE(_req: Request, meta: { params: Record }): Promise { + const user_id: string = meta.params.id ?? ''; + + const user: USER | null = await USER_STORE.get(user_id); + if (!user) { + return Response.json({ + error: { + message: 'Error deleting user.', + cause: 'unknown_user' + } + }, { + status: 404 + }); + } + + const password_entry: PASSWORD_ENTRY | null = await PASSWORD_ENTRY_STORE.get(user_id); + if (password_entry) { + await PASSWORD_ENTRY_STORE.delete(password_entry); + } + const user_permissions: USER_PERMISSIONS | null = await USER_PERMISSIONS_STORE.get(user_id); + if (user_permissions) { + await USER_PERMISSIONS_STORE.delete(user_permissions); + } + + const sessions = await SESSIONS.find({ + user_id + }); + for (const session of sessions) { + await SESSIONS.delete(session); + } + + await USER_STORE.delete(user); + + return Response.json({ + deleted: true + }, { + status: 200 + }); +} diff --git a/public/api/users/index.ts b/public/api/users/index.ts new file mode 100644 index 0000000..f23d1e7 --- /dev/null +++ b/public/api/users/index.ts @@ -0,0 +1,164 @@ +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 }); + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3342ab7 --- /dev/null +++ b/public/index.html @@ -0,0 +1,819 @@ + + + + + + Social UX + + + +
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+ +
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + +
+
+ + +
This is the home tab.
+
+ +
+ + +
+ + +
This is a talk room.
+
+
+ +
+ + +
This is the exchange tab.
+
+ +
+ + +
This is the work tab.
+
+ +
+ + +
This is the resources tab.
+
+ +
+ + +
This is the calendar tab.
+
+ +
+ + +
This is the profile tab.
+
+
+ + + diff --git a/utils/bodyparser.ts b/utils/bodyparser.ts new file mode 100644 index 0000000..cdee52f --- /dev/null +++ b/utils/bodyparser.ts @@ -0,0 +1,18 @@ +export default async function parse_body(req: Request): Promise { + switch (req.headers.get('content-type')) { + case 'application/x-www-form-urlencoded': { + const form_data = await req.formData(); + const body: Record = {}; + const keys = form_data.keys(); + + for (const key of keys) { + body[key] = form_data.get(key); + } + + return body; + } + case 'application/json': + default: + return req.json(); + } +}