From 3d42591ee571f9c8f6ab32dd361b5c56b12dd2c3 Mon Sep 17 00:00:00 2001 From: Andy Burke Date: Wed, 25 Jun 2025 20:51:29 -0700 Subject: [PATCH] feature: signup and login work --- README.md | 1 + deno.json | 7 +- deno.lock | 101 ++++++++++++++++-- models/session.ts | 1 + public/api/auth/index.ts | 38 +++---- public/api/users/:id/index.ts | 46 ++++---- public/api/users/index.ts | 23 ++-- public/api/users/me/README.md | 7 ++ public/api/users/me/index.ts | 25 +++++ public/index.html | 99 ++++++++++++++++++ tests/api/users/create_user.test.ts | 77 ++++++++++++++ tests/api/users/delete_user.test.ts | 84 +++++++++++++++ tests/api/users/login.test.ts | 157 ++++++++++++++++++++++++++++ tests/api/users/update_user.test.ts | 82 +++++++++++++++ tests/helpers.ts | 96 +++++++++++++++++ utils/api.ts | 121 +++++++++++++++++++++ utils/canned_responses.ts | 13 +++ utils/prechecks.ts | 43 ++++++++ 18 files changed, 956 insertions(+), 65 deletions(-) create mode 100644 public/api/users/me/README.md create mode 100644 public/api/users/me/index.ts create mode 100644 tests/api/users/create_user.test.ts create mode 100644 tests/api/users/delete_user.test.ts create mode 100644 tests/api/users/login.test.ts create mode 100644 tests/api/users/update_user.test.ts create mode 100644 tests/helpers.ts create mode 100644 utils/api.ts create mode 100644 utils/canned_responses.ts create mode 100644 utils/prechecks.ts diff --git a/README.md b/README.md index b583423..2c93bc7 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ ## TODO - [x] sign up +- [ ] check for logged in user session - [ ] log in - [ ] chat rooms - [ ] chat messages diff --git a/deno.json b/deno.json index 89e4bcd..0c12e83 100644 --- a/deno.json +++ b/deno.json @@ -7,7 +7,8 @@ "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" + "serve": "FSDB_ROOT=$PWD/.fsdb SERVERUS_TYPESCRIPT_IMPORT_LOGGING=1 deno --allow-env --allow-read --allow-write --allow-net jsr:@andyburke/serverus --root ./public", + "test": "DENO_ENV=test FSDB_ROOT=$PWD/tests/data/$(date --iso-8601=seconds) SERVERUS_TYPESCRIPT_IMPORT_LOGGING=1 SERVERUS_ROOT=$PWD/public deno test --allow-env --allow-read --allow-write --allow-net --trace-leaks --fail-fast tests/" }, "test": { "exclude": ["tests/data/"] @@ -33,8 +34,10 @@ "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", + "@andyburke/serverus": "jsr:@andyburke/serverus@^0.6.0", + "@std/assert": "jsr:@std/assert@^1.0.13", "@std/encoding": "jsr:@std/encoding@^1.0.10", + "@std/http": "jsr:@std/http@^1.0.18", "@std/path": "jsr:@std/path@^1.1.0", "@stdext/crypto": "jsr:@stdext/crypto@^0.1.0" } diff --git a/deno.lock b/deno.lock index 4246f0c..f3037bd 100644 --- a/deno.lock +++ b/deno.lock @@ -1,21 +1,34 @@ { "version": "5", "specifiers": { + "jsr:@andyburke/fsdb@*": "0.4.0", "jsr:@andyburke/fsdb@0.4": "0.4.0", + "jsr:@andyburke/lurid@*": "0.2.0", "jsr:@andyburke/lurid@0.2": "0.2.0", - "jsr:@andyburke/serverus@^0.0.12": "0.0.12", + "jsr:@andyburke/serverus@0.6": "0.6.0", + "jsr:@std/assert@*": "1.0.13", + "jsr:@std/assert@^1.0.13": "1.0.13", "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.0.10", "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/fmt@^1.0.8": "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/html@^1.0.4": "1.0.4", + "jsr:@std/http@*": "1.0.18", + "jsr:@std/http@^1.0.13": "1.0.18", + "jsr:@std/http@^1.0.18": "1.0.18", + "jsr:@std/internal@^1.0.6": "1.0.8", "jsr:@std/media-types@^1.1.0": "1.1.0", + "jsr:@std/net@^1.0.4": "1.0.4", "jsr:@std/path@^1.0.8": "1.1.0", "jsr:@std/path@^1.1.0": "1.1.0", + "jsr:@std/streams@^1.0.10": "1.0.10", + "jsr:@stdext/crypto@*": "0.1.0", "jsr:@stdext/crypto@0.1": "0.1.0" }, "jsr": { @@ -33,18 +46,24 @@ "jsr:@std/cli@^1.0.19" ] }, - "@andyburke/serverus@0.0.12": { - "integrity": "051cbffd30577e39ca604e009c3870c4b32b7e4118f2da58fc18ec05afa5b5bb", + "@andyburke/serverus@0.6.0": { + "integrity": "89f013c1d77e3d5d2c4e0908b29cc4a1acd19ebf22fa2890a6c5aa777e7b0de3", "dependencies": [ "jsr:@std/async", "jsr:@std/cli@^1.0.19", - "jsr:@std/fmt", + "jsr:@std/fmt@^1.0.6", "jsr:@std/fs@^1.0.14", - "jsr:@std/http", + "jsr:@std/http@^1.0.13", "jsr:@std/media-types", "jsr:@std/path@^1.0.8" ] }, + "@std/assert@1.0.13": { + "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", + "dependencies": [ + "jsr:@std/internal" + ] + }, "@std/async@1.0.13": { "integrity": "1d76ca5d324aef249908f7f7fe0d39aaf53198e5420604a59ab5c035adc97c96" }, @@ -63,15 +82,41 @@ "jsr:@std/path@^1.1.0" ] }, + "@std/html@1.0.4": { + "integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e" + }, "@std/http@1.0.17": { "integrity": "98aec8ab4080d95c21f731e3008f69c29c5012d12f1b4e553f85935db601569f" }, + "@std/http@1.0.18": { + "integrity": "8d9546aa532c52a0cf318c74616db0638b4c1073405355d1b14f9e1591dccf20", + "dependencies": [ + "jsr:@std/cli@^1.0.20", + "jsr:@std/encoding@^1.0.10", + "jsr:@std/fmt@^1.0.8", + "jsr:@std/fs@^1.0.18", + "jsr:@std/html", + "jsr:@std/media-types", + "jsr:@std/net", + "jsr:@std/path@^1.1.0", + "jsr:@std/streams" + ] + }, + "@std/internal@1.0.8": { + "integrity": "fc66e846d8d38a47cffd274d80d2ca3f0de71040f855783724bb6b87f60891f5" + }, "@std/media-types@1.1.0": { "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" }, + "@std/net@1.0.4": { + "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" + }, "@std/path@1.1.0": { "integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886" }, + "@std/streams@1.0.10": { + "integrity": "75c0b1431873cd0d8b3d679015220204d36d3c7420d93b60acfc379eb0dc30af" + }, "@stdext/crypto@0.1.0": { "integrity": "05dc9e754c2529574d8bf98bd40c7dc468a02dcb2fa5e8644fff6813ceab66a4", "dependencies": [ @@ -79,12 +124,54 @@ ] } }, + "remote": { + "https://deno.land/std@0.167.0/_util/asserts.ts": "d0844e9b62510f89ce1f9878b046f6a57bf88f208a10304aab50efcb48365272", + "https://deno.land/std@0.167.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", + "https://deno.land/std@0.167.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.167.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.167.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677", + "https://deno.land/std@0.167.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.167.0/path/glob.ts": "81cc6c72be002cd546c7a22d1f263f82f63f37fe0035d9726aa96fc8f6e4afa1", + "https://deno.land/std@0.167.0/path/mod.ts": "cf7cec7ac11b7048bb66af8ae03513e66595c279c65cfa12bfc07d9599608b78", + "https://deno.land/std@0.167.0/path/posix.ts": "b859684bc4d80edfd4cad0a82371b50c716330bed51143d6dcdbe59e6278b30c", + "https://deno.land/std@0.167.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.167.0/path/win32.ts": "7cebd2bda6657371adc00061a1d23fdd87bcdf64b4843bb148b0b24c11b40f69", + "https://deno.land/std@0.184.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.184.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.184.0/async/abortable.ts": "fd682fa46f3b7b16b4606a5ab52a7ce309434b76f820d3221bdfb862719a15d7", + "https://deno.land/std@0.184.0/async/deadline.ts": "c5facb0b404eede83e38bd2717ea8ab34faa2ffb20ef87fd261fcba32ba307aa", + "https://deno.land/std@0.184.0/async/debounce.ts": "adab11d04ca38d699444ac8a9d9856b4155e8dda2afd07ce78276c01ea5a4332", + "https://deno.land/std@0.184.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", + "https://deno.land/std@0.184.0/async/delay.ts": "73aa04cec034c84fc748c7be49bb15cac3dd43a57174bfdb7a4aec22c248f0dd", + "https://deno.land/std@0.184.0/async/mod.ts": "f04344fa21738e5ad6bea37a6bfffd57c617c2d372bb9f9dcfd118a1b622e576", + "https://deno.land/std@0.184.0/async/mux_async_iterator.ts": "70c7f2ee4e9466161350473ad61cac0b9f115cff4c552eaa7ef9d50c4cbb4cc9", + "https://deno.land/std@0.184.0/async/pool.ts": "fd082bd4aaf26445909889435a5c74334c017847842ec035739b4ae637ae8260", + "https://deno.land/std@0.184.0/async/retry.ts": "dd19d93033d8eaddbfcb7654c0366e9d3b0a21448bdb06eba4a7d8a8cf936a92", + "https://deno.land/std@0.184.0/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757", + "https://deno.land/std@0.184.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.184.0/fs/_util.ts": "579038bebc3bd35c43a6a7766f7d91fbacdf44bc03468e9d3134297bb99ed4f9", + "https://deno.land/std@0.184.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", + "https://deno.land/std@0.184.0/fs/walk.ts": "920be35a7376db6c0b5b1caf1486fb962925e38c9825f90367f8f26b5e5d0897", + "https://deno.land/std@0.184.0/http/server.ts": "cbb17b594651215ba95c01a395700684e569c165a567e4e04bba327f41197433", + "https://deno.land/std@0.184.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.184.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.184.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.184.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.184.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.184.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c", + "https://deno.land/std@0.184.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.184.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.184.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/x/ulid@v0.3.0/mod.ts": "f7ff065b66abd485051fc68af23becef6ccc7e81f7774d7fcfd894a4b2da1984" + }, "workspace": { "dependencies": [ "jsr:@andyburke/fsdb@0.4", "jsr:@andyburke/lurid@0.2", - "jsr:@andyburke/serverus@^0.0.12", + "jsr:@andyburke/serverus@0.6", + "jsr:@std/assert@^1.0.13", "jsr:@std/encoding@^1.0.10", + "jsr:@std/http@^1.0.18", "jsr:@std/path@^1.1.0", "jsr:@stdext/crypto@0.1" ] diff --git a/models/session.ts b/models/session.ts index b88241f..75903de 100644 --- a/models/session.ts +++ b/models/session.ts @@ -19,6 +19,7 @@ export const SESSIONS = new FSDB_COLLECTION({ user_id: new FSDB_INDEXER_SYMLINKS({ name: 'user_id', field: 'user_id', + to_many: true, organize: by_lurid }) } diff --git a/public/api/auth/index.ts b/public/api/auth/index.ts index aa749dc..3a8743d 100644 --- a/public/api/auth/index.ts +++ b/public/api/auth/index.ts @@ -9,15 +9,15 @@ import { TOTP_ENTRIES } 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'; +import { SESSION_ID_TOKEN, SESSION_SECRET_TOKEN } from '../../../utils/prechecks.ts'; -const DEFAULT_SESSION_TIME: number = 60 * 60; // 1 Hour +const DEFAULT_SESSION_TIME: number = 60 * 60 * 1_000; // 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' @@ -27,11 +27,11 @@ export async function POST(req: Request, meta: Record): Promise): Promise): Promise): Promise { +export async function create_new_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(); @@ -183,7 +177,13 @@ export async function get_session(session_settings: SESSION_INFO): Promise) => Promise> = {}; +export const PRECHECKS: PRECHECK_TABLE = {}; // GET /api/users/:id - Get single user -PERMISSIONS.GET = (_req: Request, meta: Record): Promise => { +PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { 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); -}; + const has_permission = can_read_others || (can_read_self && user_is_self); + if (!has_permission) { + return CANNED_RESPONSES.permission_denied(); + } +}]; 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 USERS.get(user_id) : null; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" @@ -29,34 +34,22 @@ export async function GET(_req: Request, meta: Record): Promise): Promise => { +PRECHECKS.PUT = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { 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); -}; + const has_permission = can_write_others || (can_write_self && user_is_self); + if (!has_permission) { + return CANNED_RESPONSES.permission_denied(); + } +}]; export async function PUT(req: Request, meta: { params: Record }): Promise { const now = new Date().toISOString(); const id: string = meta.params.id ?? ''; @@ -101,13 +94,16 @@ export async function PUT(req: Request, meta: { params: Record }): } // DELETE /api/users/:id - Delete user -PERMISSIONS.DELETE = (_req: Request, meta: Record): Promise => { +PRECHECKS.DELETE = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { 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); -}; + const has_permission = can_write_others || (can_write_self && user_is_self); + if (!has_permission) { + return CANNED_RESPONSES.permission_denied(); + } +}]; export async function DELETE(_req: Request, meta: { params: Record }): Promise { const user_id: string = meta.params.id ?? ''; diff --git a/public/api/users/index.ts b/public/api/users/index.ts index 68e4705..558dfe3 100644 --- a/public/api/users/index.ts +++ b/public/api/users/index.ts @@ -6,28 +6,27 @@ 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'; +import { create_new_session, SESSION_RESULT } from '../auth/index.ts'; +import { PRECHECKS } from './me/index.ts'; +import { get_session, get_user, require_user } from '../../../utils/prechecks.ts'; +import { CANNED_RESPONSES } from '../../../utils/canned_responses.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' + 'self.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 => { +PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { const can_read_others = meta.user_permissions?.permissions?.includes('users.read'); - return can_read_others; -}; + 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(); @@ -132,7 +131,7 @@ export async function POST(req: Request, meta: Record): Promise) => Promise> = {}; +export const PRECHECKS: PRECHECK_TABLE = {}; + +// GET /api/users/me - Get the current user +PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { + const can_read_self = meta.user_permissions?.permissions.includes('self.read'); + + const has_permission = can_read_self; + console.dir({ + meta, + can_read_self, + has_permission + }); + if (!has_permission) { + return CANNED_RESPONSES.permission_denied(); + } +}]; +export function GET(_req: Request, meta: Record): Response { + return Response.json(meta.user, { + status: 200 + }); +} diff --git a/public/index.html b/public/index.html index 3342ab7..a6180cf 100644 --- a/public/index.html +++ b/public/index.html @@ -468,6 +468,54 @@ top: 8px; } + +
@@ -578,6 +626,13 @@ action="/api/auth" onreply="(user)=>{ document.body.dataset.user = user; }" > +