From ee152a514c462b842d3fbcda938d8c7cfbe2d1b0 Mon Sep 17 00:00:00 2001 From: Andy Burke Date: Fri, 4 Jul 2025 15:16:51 -0700 Subject: [PATCH] fix: more login/session issues addressed --- public/api/auth/index.ts | 26 +++++++++- public/base.css | 1 + public/index.html | 2 +- public/js/api.js | 6 +-- public/js/smartforms.js | 17 ++++--- public/tabs/{user.html => profile.html} | 68 ++++++++++++++----------- public/tabs/tabs.html | 2 +- public/tabs/talk.html | 14 ++--- 8 files changed, 85 insertions(+), 51 deletions(-) rename public/tabs/{user.html => profile.html} (57%) diff --git a/public/api/auth/index.ts b/public/api/auth/index.ts index 810c542..1bacd9d 100644 --- a/public/api/auth/index.ts +++ b/public/api/auth/index.ts @@ -6,12 +6,14 @@ import { SESSION, SESSIONS } from '../../../models/session.ts'; import { TOTP_ENTRIES } from '../../../models/totp_entry.ts'; 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'; +import { get_session, get_user, PRECHECK_TABLE, require_user, SESSION_ID_TOKEN, SESSION_SECRET_TOKEN } from '../../../utils/prechecks.ts'; import * as bcrypt from 'jsr:@da/bcrypt'; import { verifyTotp } from '../../../utils/totp.ts'; const DEFAULT_SESSION_TIME: number = 60 * 60 * 1_000; // 1 Hour +export const PRECHECKS: PRECHECK_TABLE = {}; + // POST /api/auth - Authenticate export async function POST(req: Request, meta: Record): Promise { try { @@ -158,6 +160,28 @@ export type SESSION_INFO = { expires: string | undefined; }; +// DELETE /api/auth - log out (delete session) +PRECHECKS.DELETE = [get_session, get_user, require_user]; +const back_then = new Date(0).toISOString(); +export async function DELETE(_request: Request, meta: Record): Promise { + await SESSIONS.delete(meta.session); + + const headers = new Headers(); + + headers.append('Set-Cookie', `${SESSION_ID_TOKEN}=; Path=/; Expires=${back_then}`); + + // TODO: this wasn't really intended to be persisted in a cookie, but we are using it to + // generate the TOTP for the call to /api/users/me + headers.append('Set-Cookie', `${SESSION_SECRET_TOKEN}=; Path=/; Expires=${back_then}`); + + return Response.json({ + deleted: true + }, { + status: 200, + headers + }); +} + const session_secret_buffer = new Uint8Array(20); export async function create_new_session(session_settings: SESSION_INFO): Promise { const now = new Date().toISOString(); diff --git a/public/base.css b/public/base.css index d1aaa0f..8116945 100644 --- a/public/base.css +++ b/public/base.css @@ -131,6 +131,7 @@ button { padding: 0.5rem; border: 1px solid var(--text); border-radius: 4px; + cursor: pointer; } button.primary { diff --git a/public/index.html b/public/index.html index ba6f626..6721ebb 100644 --- a/public/index.html +++ b/public/index.html @@ -28,7 +28,7 @@ (async () => { try { console.log( 'hi'); - const session_response = await api.fetch("/users/me"); + const session_response = await api.fetch("/api/users/me"); if (!session_response.ok) { const error_body = await session_response.json(); diff --git a/public/js/api.js b/public/js/api.js index 3c6b4ce..c9b0683 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -11,8 +11,8 @@ const api = { const headers = { Accept: "application/json", - "x-session_id": session_id, - "x-totp": await otp_totp(session_secret), + "x-session_id": session_id ?? "", + "x-totp": session_secret ? await otp_totp(session_secret) : "", ...(options.headers ?? {}), }; @@ -26,7 +26,7 @@ const api = { fetch_options.body = JSON.stringify(options.json); } - const response = await fetch(`/api${url}`, fetch_options); + const response = await fetch(url, fetch_options); return response; }, diff --git a/public/js/smartforms.js b/public/js/smartforms.js index 506f24e..ef4f23c 100644 --- a/public/js/smartforms.js +++ b/public/js/smartforms.js @@ -7,11 +7,15 @@ document.addEventListener("DOMContentLoaded", () => { form.onsubmit = async (event) => { event.preventDefault(); + const url = form.action; + const method = form.dataset.method ?? "POST"; + + const json = {}; + const form_data = new FormData(form); - const body = {}; for (const [key, value] of form_data.entries()) { const elements = key.split("."); - let current = body; + let current = json; for (const element of elements.slice(0, elements.length - 1)) { current[element] = current[element] ?? {}; current = current[element]; @@ -20,12 +24,10 @@ document.addEventListener("DOMContentLoaded", () => { current[elements.slice(elements.length - 1).shift()] = value; } - const url = form.action; - const method = form.dataset.method ?? "POST"; - console.dir({ method, form, + json, }); try { // TODO: send session header @@ -37,11 +39,10 @@ document.addEventListener("DOMContentLoaded", () => { }; if (["POST", "PUT", "PATCH"].includes(method)) { - options.headers["Content-Type"] = "application/json"; - options.body = JSON.stringify(body); + options.json = json; } - const response = await fetch(url, options); + const response = await api.fetch(url, options); if (!response.ok) { const error_body = await response.json(); diff --git a/public/tabs/user.html b/public/tabs/profile.html similarity index 57% rename from public/tabs/user.html rename to public/tabs/profile.html index cd7d1ce..420a60c 100644 --- a/public/tabs/user.html +++ b/public/tabs/profile.html @@ -28,6 +28,12 @@ attributeFilter: ["data-user"], }); +
Profile
-
- User Avatar +
+
+ User Avatar +
+ +
+ +
+ +
+ + +
- -
- -
- -
- - -
diff --git a/public/tabs/tabs.html b/public/tabs/tabs.html index c41ad36..8c73247 100644 --- a/public/tabs/tabs.html +++ b/public/tabs/tabs.html @@ -133,5 +133,5 @@ - + diff --git a/public/tabs/talk.html b/public/tabs/talk.html index aa71672..11646c2 100644 --- a/public/tabs/talk.html +++ b/public/tabs/talk.html @@ -211,7 +211,7 @@ users[event.creator_id] = users[event.creator_id] ?? - (await api.fetch(`/users/${event.creator_id}`)); + (await api.fetch(`/api/users/${event.creator_id}`)); room_chat_content.insertAdjacentHTML( "beforeend", @@ -233,7 +233,7 @@ return; } - const message_polling_url = `/rooms/${room_id}/events?type=chat&limit=100&sort=newest&wait=true${last_message_id ? `&after_id=${last_message_id}` : ""}`; + const message_polling_url = `/api/rooms/${room_id}/events?type=chat&limit=100&sort=newest&wait=true${last_message_id ? `&after_id=${last_message_id}` : ""}`; room_polling_request_abort_controller = room_polling_request_abort_controller || new AbortController(); @@ -264,7 +264,7 @@ delete room_chat_content.dataset.last_message_id; } - const room_response = await api.fetch(`/rooms/${room_id}`); + const room_response = await api.fetch(`/api/rooms/${room_id}`); if (!room_response.ok) { const error = await room_response.json(); alert(error.message ?? JSON.stringify(error)); @@ -285,7 +285,7 @@ } const events_response = await api.fetch( - `/rooms/${room_id}/events?type=chat&limit=100&sort=newest`, + `/api/rooms/${room_id}/events?type=chat&limit=100&sort=newest`, ); if (!events_response.ok) { const error = await events_response.json(); @@ -309,7 +309,7 @@ return; } - const rooms_response = await api.fetch("/rooms"); + const rooms_response = await api.fetch("/api/rooms"); if (rooms_response.ok) { const room_list = document.getElementById("room-list"); room_list.innerHTML = ""; @@ -411,7 +411,7 @@ render_text_event(message_event, user), ); - const event_response = await api.fetch(`/rooms/${room_id}/events`, { + const event_response = await api.fetch(`/api/rooms/${room_id}/events`, { method: "POST", json: message_event, }); @@ -484,7 +484,7 @@ editable.addEventListener("focusout", async () => { const name = editable.textContent ?? editable.innerText; - const new_room_response = await api.fetch("/rooms", { + const new_room_response = await api.fetch("/api/rooms", { method: "POST", json: { name,