@storyshelf/app 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-sync.mjs","names":[],"sources":["../../src/routers/auth-sync.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport {\n MemberModel,\n ProjectGroupMappingModel,\n ProjectModel,\n UserModel,\n} from \"@storyshelf/core/models\";\nimport type {
|
|
1
|
+
{"version":3,"file":"auth-sync.mjs","names":[],"sources":["../../src/routers/auth-sync.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport {\n MemberModel,\n ProjectGroupMappingModel,\n ProjectModel,\n UserModel,\n} from \"@storyshelf/core/models\";\nimport type { Table } from \"@storyshelf/core/orm\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\n\n/** Tables required to sync a login. */\nexport interface LoginSyncTables {\n users: Table;\n projects: Table;\n projectMembers: Table;\n projectGroupMappings: Table;\n}\n\nconst ROLE_RANK: Record<ProjectRole, number> = {\n viewer: 0,\n developer: 1,\n approver: 2,\n admin: 3,\n};\n\n/**\n * Persist the authenticated identity and reconcile OIDC group memberships.\n *\n * Always upserts the user row. Group mappings apply per project: the highest\n * matched role wins (recorded as `oidc:<group>`); memberships from groups the\n * user no longer matches are removed — manual grants are never touched.\n */\nexport async function syncLoginMemberships(\n db: DatabaseAdapter,\n _tables: LoginSyncTables,\n user: AuthUser,\n): Promise<void> {\n await new UserModel(db).upsert({\n id: user.id,\n email: user.email,\n name: user.name,\n avatarUrl: user.avatarUrl ?? null,\n role: user.role,\n });\n const groups = user.groups ?? [];\n const projects = await new ProjectModel(db).list();\n const members = new MemberModel(db);\n const mappings = new ProjectGroupMappingModel(db);\n await Promise.all(\n projects.map(\n // oxlint-disable-next-line typescript/promise-function-async -- returns the inner promise directly\n (project) => syncProjectMembership(members, mappings, project.id, user.id, groups),\n ),\n );\n}\n\n/** Highest-ranked matched mapping, or null when nothing matches. */\nfunction topMatch(\n rows: { groupName: string; role: ProjectRole }[],\n groups: string[],\n): { groupName: string; role: ProjectRole } | null {\n let top: { groupName: string; role: ProjectRole } | null = null;\n for (const row of rows) {\n if (!groups.includes(row.groupName)) {\n continue;\n }\n if (!top || ROLE_RANK[row.role] > ROLE_RANK[top.role]) {\n top = { groupName: row.groupName, role: row.role };\n }\n }\n return top;\n}\n\n/** Remove a synced grant the user no longer matches; manual grants survive. */\nasync function revokeIfStale(\n members: MemberModel,\n projectId: string,\n userId: string,\n): Promise<void> {\n const existing = await members.get(projectId, userId);\n if (existing && existing.source !== \"manual\") {\n await members.remove(projectId, userId);\n }\n}\n\nasync function syncProjectMembership(\n members: MemberModel,\n mappings: ProjectGroupMappingModel,\n projectId: string,\n userId: string,\n groups: string[],\n): Promise<void> {\n const rows = await mappings.list(projectId);\n if (rows.length === 0) {\n return;\n }\n const top = topMatch(rows, groups);\n if (!top) {\n await revokeIfStale(members, projectId, userId);\n return;\n }\n const existing = await members.get(projectId, userId);\n if (!existing || existing.role !== top.role || existing.source !== `oidc:${top.groupName}`) {\n await members.set(projectId, userId, top.role, `oidc:${top.groupName}`);\n }\n}\n"],"mappings":";;;;;AAmBA,MAAM,YAAyC;CAC7C,QAAQ;CACR,WAAW;CACX,UAAU;CACV,OAAO;AACT;;;;;;;;AASA,eAAsB,qBACpB,IACA,SACA,MACe;CACf,MAAM,IAAI,UAAU,EAAE,CAAC,CAAC,OAAO;EAC7B,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,MAAM,KAAK;EACX,WAAW,KAAK,aAAa;EAC7B,MAAM,KAAK;CACb,CAAC;CACD,MAAM,SAAS,KAAK,UAAU,CAAC;CAC/B,MAAM,WAAW,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,KAAK;CACjD,MAAM,UAAU,IAAI,YAAY,EAAE;CAClC,MAAM,WAAW,IAAI,yBAAyB,EAAE;CAChD,MAAM,QAAQ,IACZ,SAAS,KAEN,YAAY,sBAAsB,SAAS,UAAU,QAAQ,IAAI,KAAK,IAAI,MAAM,CACnF,CACF;AACF;;AAGA,SAAS,SACP,MACA,QACiD;CACjD,IAAI,MAAuD;CAC3D,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,CAAC,OAAO,SAAS,IAAI,SAAS,GAChC;EAEF,IAAI,CAAC,OAAO,UAAU,IAAI,QAAQ,UAAU,IAAI,OAC9C,MAAM;GAAE,WAAW,IAAI;GAAW,MAAM,IAAI;EAAK;CAErD;CACA,OAAO;AACT;;AAGA,eAAe,cACb,SACA,WACA,QACe;CACf,MAAM,WAAW,MAAM,QAAQ,IAAI,WAAW,MAAM;CACpD,IAAI,YAAY,SAAS,WAAW,UAClC,MAAM,QAAQ,OAAO,WAAW,MAAM;AAE1C;AAEA,eAAe,sBACb,SACA,UACA,WACA,QACA,QACe;CACf,MAAM,OAAO,MAAM,SAAS,KAAK,SAAS;CAC1C,IAAI,KAAK,WAAW,GAClB;CAEF,MAAM,MAAM,SAAS,MAAM,MAAM;CACjC,IAAI,CAAC,KAAK;EACR,MAAM,cAAc,SAAS,WAAW,MAAM;EAC9C;CACF;CACA,MAAM,WAAW,MAAM,QAAQ,IAAI,WAAW,MAAM;CACpD,IAAI,CAAC,YAAY,SAAS,SAAS,IAAI,QAAQ,SAAS,WAAW,QAAQ,IAAI,aAC7E,MAAM,QAAQ,IAAI,WAAW,QAAQ,IAAI,MAAM,QAAQ,IAAI,WAAW;AAE1E"}
|
package/dist/routers/profile.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { renderProfilePage } from "../pages/profile.mjs";
|
|
|
9
9
|
import { NotificationSubscriptionModel, ProjectModel, UserModel } from "@storyshelf/core/models";
|
|
10
10
|
import { SESSION_COOKIE } from "@storyshelf/core/types";
|
|
11
11
|
import { MIN_PASSWORD_LENGTH, unsignedToken } from "@storyshelf/auth";
|
|
12
|
-
import { eq, getTableColumns } from "
|
|
12
|
+
import { eq, getTableColumns } from "@storyshelf/core/orm";
|
|
13
13
|
//#region src/routers/profile.ts
|
|
14
14
|
function field(form, name) {
|
|
15
15
|
const raw = form.get(name);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.mjs","names":[],"sources":["../../src/routers/profile.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/auth\";\nimport { MIN_PASSWORD_LENGTH, unsignedToken } from \"@storyshelf/auth\";\nimport { NotificationSubscriptionModel, ProjectModel, UserModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { SESSION_COOKIE, type AuthUser } from \"@storyshelf/core/types\";\nimport { eq, getTableColumns } from \"drizzle-orm\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { renderProfilePage } from \"../pages/profile.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { forwardToEngine } from \"./auth.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** User columns needed to render the profile page. */\ninterface ProfileRow {\n email: string;\n name: string;\n displayNameOverride: string | null;\n authProvider: string;\n lastLoginAt: string | null;\n createdAt: string;\n}\n\n/** Membership entry for the profile page. */\ninterface ProfileMembership {\n projectName: string;\n projectSlug: string;\n role: string;\n source: string;\n}\n\nfunction field(form: FormData, name: string): string {\n const raw = form.get(name);\n return typeof raw === \"string\" ? raw : \"\";\n}\n\nfunction toDbUser(row: ProfileRow | null): ProfilePage[\"dbUser\"] {\n if (!row) {\n return undefined;\n }\n return {\n email: row.email,\n name: row.name,\n displayNameOverride: row.displayNameOverride,\n authProvider: row.authProvider,\n lastLoginAt: row.lastLoginAt,\n createdAt: row.createdAt,\n };\n}\n\ntype ProfilePage = Parameters<typeof renderProfilePage>[0];\n\nasync function loadProfileData(userId: string): Promise<{\n userRow: ProfileRow | null;\n memberships: ProfileMembership[];\n subscribedSlugs: string[];\n}> {\n const { db } = getStore();\n const userRow = (await new UserModel(db).get(userId)) as unknown as ProfileRow | null;\n const members = (await db.list(db.tables.projectMembers, {\n where: eq(getTableColumns(db.tables.projectMembers)[\"userId\"] as never, userId),\n })) as unknown as Array<{ projectId: string; role: string; source: string }>;\n const projects = (await db.list(db.tables.projects)) as unknown as Array<{\n id: string;\n name: string;\n slug: string;\n }>;\n const byId = new Map(projects.map((project) => [project.id, project]));\n const memberships = members.flatMap((member) => {\n const project = byId.get(member.projectId);\n return project\n ? [\n {\n projectName: project.name,\n projectSlug: project.slug,\n role: member.role,\n source: member.source,\n },\n ]\n : [];\n });\n const subs = await new NotificationSubscriptionModel(db).listForUser(userId);\n const subscribedIds = new Set(subs.filter((sub) => sub.enabled).map((sub) => sub.projectId));\n const subscribedSlugs = [...byId.values()]\n .filter((project) => subscribedIds.has(project.id))\n .map((project) => project.slug);\n return { userRow, memberships, subscribedSlugs };\n}\n\nfunction readSessionToken(c: Context): string {\n const header = c.req.header(\"cookie\") ?? \"\";\n for (const part of header.split(\";\")) {\n const eqIndex = part.indexOf(\"=\");\n if (eqIndex !== -1 && part.slice(0, eqIndex).trim() === SESSION_COOKIE) {\n // Engine cookies are signed (`token.signature`); the table holds the raw token.\n return unsignedToken(part.slice(eqIndex + 1).trim());\n }\n }\n return \"\";\n}\n\n/** Engine security overview for the profile page. */\nasync function loadEngineSecurity(\n c: Context,\n auth: Auth,\n user: AuthUser,\n): Promise<NonNullable<ProfilePage[\"security\"]>> {\n const [sessions, passkeys, hasPassword] = await Promise.all([\n auth.listSessions(user.id),\n auth.listPasskeys(user.id),\n auth.hasPassword(user.id),\n ]);\n const current = readSessionToken(c);\n return {\n sessions: sessions.map((session) => ({\n id: session.id,\n ipAddress: session.ipAddress,\n userAgent: session.userAgent,\n createdAt: session.createdAt,\n expiresAt: session.expiresAt,\n current: session.token === current,\n })),\n passkeys,\n passkeysEnabled: auth.passkeysEnabled(),\n hasPassword,\n };\n}\n\nasync function profileView(\n c: Context,\n user: AuthUser,\n auth: Auth,\n extra?: { error?: string; success?: string; local?: boolean; status?: 200 | 400 },\n): Promise<Response> {\n const { userRow, memberships, subscribedSlugs } = await loadProfileData(user.id);\n const security = await loadEngineSecurity(c, auth, user);\n const html = await renderProfilePage({\n user,\n dbUser: toDbUser(userRow),\n memberships,\n subscribedSlugs,\n isLocal: extra?.local ?? security.hasPassword,\n security,\n error: extra?.error,\n });\n if (extra?.success) {\n flash(c, extra.success);\n }\n // Device tokens render into the page (per-device revoke forms): never store.\n c.header(\"Cache-Control\", \"no-store\");\n return c.html(html, extra?.status ?? 200);\n}\n\n/** Register the personal profile page and its mutations. */\nexport function registerProfile(app: ShelfRouter, auth: Auth): void {\n app.get(\"/profile\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await profileView(c, user, auth);\n });\n\n app.post(\"/profile\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n const displayName = field(await c.req.formData(), \"displayName\").trim();\n if (!displayName) {\n return profileView(c, user, auth, { error: \"Display name is required\", status: 400 });\n }\n await new UserModel(getStore().db).setDisplayNameOverride(user.id, displayName);\n flash(c, \"Profile updated\");\n return hxRedirect(c, \"/profile\");\n });\n\n registerPasswordRoute(app, auth);\n registerSessionRoutes(app, auth);\n registerPasskeyRoutes(app, auth);\n registerNotificationsRoute(app, auth);\n}\n\nfunction registerNotificationsRoute(app: ShelfRouter, auth: Auth): void {\n app.post(\"/profile/notifications\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await saveProfileNotifications(c, user, auth);\n });\n}\n\n/** Toggle the viewer's own email subscription for one member project. */\nasync function saveProfileNotifications(c: Context, user: AuthUser, auth: Auth): Promise<Response> {\n const form = await c.req.formData();\n const project = await memberProject(field(form, \"slug\"), user.id);\n if (!project) {\n return profileView(c, user, auth, { error: \"Unknown project\", status: 400 });\n }\n await applyProfileToggle(project.id, user.id, field(form, \"enabled\") === \"1\");\n flash(c, \"Notification preference saved\");\n return hxRedirect(c, \"/profile\");\n}\n\n/** Resolve a project the viewer belongs to (null otherwise). */\nasync function memberProject(slug: string, userId: string): Promise<Project | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { memberships } = await loadProfileData(userId);\n const isMember = memberships.some((membership) => membership.projectSlug === slug);\n return isMember ? project : null;\n}\n\n/** Enable (opt-in email) or disable (remove) a subscription. */\nasync function applyProfileToggle(\n projectId: string,\n userId: string,\n enable: boolean,\n): Promise<void> {\n const model = new NotificationSubscriptionModel(getStore().db);\n if (enable) {\n await model.upsert(projectId, userId, { via: [\"email\"] });\n return;\n }\n await model.remove(projectId, userId);\n}\n\nfunction registerPasswordRoute(app: ShelfRouter, auth: Auth): void {\n app.post(\"/profile/password\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await handleEnginePasswordChange(c, auth, user);\n });\n}\n\nfunction registerSessionRoutes(app: ShelfRouter, auth: Auth): void {\n app.post(\"/profile/sessions/revoke\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await handleSessionRevoke(c, auth, user);\n });\n\n app.post(\"/profile/sessions/revoke-others\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await handleRevokeOthers(c, auth, user);\n });\n}\n\nfunction registerPasskeyRoutes(app: ShelfRouter, auth: Auth): void {\n app.post(\"/profile/passkeys/:passkeyId/delete\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await handlePasskeyDelete(c, auth, user);\n });\n}\n\nasync function handleSessionRevoke(c: Context, auth: Auth, user: AuthUser): Promise<Response> {\n const id = await readSessionId(c);\n if (!id) {\n return profileView(c, user, auth, { error: \"Missing session\", status: 400 });\n }\n const token = await resolveSessionToken(auth, user, id);\n if (!token) {\n return profileView(c, user, auth, { error: \"Could not revoke that session\", status: 400 });\n }\n const res = await forwardToEngine(c, auth, \"/api/auth/revoke-session\", { token });\n if (res.status !== 200) {\n return profileView(c, user, auth, { error: \"Could not revoke that session\", status: 400 });\n }\n return profileView(c, user, auth, { success: \"Session revoked\" });\n}\n\nasync function readSessionId(c: Context): Promise<string> {\n return field(await c.req.formData(), \"sessionId\");\n}\n\nasync function resolveSessionToken(auth: Auth, user: AuthUser, id: string): Promise<string | null> {\n const sessions = await auth.listSessions(user.id);\n return sessions.find((session) => session.id === id)?.token ?? null;\n}\n\nasync function handleRevokeOthers(c: Context, auth: Auth, user: AuthUser): Promise<Response> {\n const res = await forwardToEngine(c, auth, \"/api/auth/revoke-other-sessions\", {});\n if (res.status !== 200) {\n return profileView(c, user, auth, { error: \"Could not sign out other devices\", status: 400 });\n }\n return profileView(c, user, auth, { success: \"Signed out of other devices\" });\n}\n\nasync function handlePasskeyDelete(c: Context, auth: Auth, user: AuthUser): Promise<Response> {\n const id = c.req.param(\"passkeyId\") ?? \"\";\n if (!/^[A-Za-z0-9_-]{1,64}$/u.test(id)) {\n return profileView(c, user, auth, { error: \"Unknown passkey\", status: 400 });\n }\n const res = await forwardToEngine(c, auth, \"/api/auth/passkey/delete-passkey\", { id });\n if (res.status !== 200) {\n return profileView(c, user, auth, { error: \"Could not remove that passkey\", status: 400 });\n }\n return profileView(c, user, auth, { success: \"Passkey removed\" });\n}\n\nasync function engineErrorMessage(res: Response, fallback: string): Promise<string> {\n try {\n const body = (await res.json()) as { message?: unknown };\n return typeof body.message === \"string\" && body.message ? body.message : fallback;\n } catch {\n return fallback;\n }\n}\n\n/** Validate the password form and change it through the engine. */\nasync function handleEnginePasswordChange(\n c: Context,\n auth: Auth,\n user: AuthUser,\n): Promise<Response> {\n const { current, next, error } = readPasswordChange(await c.req.formData());\n if (error) {\n return profileView(c, user, auth, { error, local: true, status: 400 });\n }\n const res = await forwardToEngine(c, auth, \"/api/auth/change-password\", {\n currentPassword: current,\n newPassword: next,\n revokeOtherSessions: true,\n });\n if (res.status !== 200) {\n const message = await engineErrorMessage(res, \"Password change failed\");\n return profileView(c, user, auth, { error: message, local: true, status: 400 });\n }\n return profileView(c, user, auth, { success: \"Password changed\", local: true });\n}\n\nfunction readPasswordChange(form: FormData): { current: string; next: string; error?: string } {\n const next = field(form, \"newPassword\");\n if (next !== field(form, \"confirmPassword\")) {\n return { current: \"\", next: \"\", error: \"Passwords do not match\" };\n }\n if (next.length < MIN_PASSWORD_LENGTH) {\n return {\n current: \"\",\n next: \"\",\n error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters`,\n };\n }\n return { current: field(form, \"currentPassword\"), next };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"profile.mjs","names":[],"sources":["../../src/routers/profile.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/auth\";\nimport { MIN_PASSWORD_LENGTH, unsignedToken } from \"@storyshelf/auth\";\nimport { NotificationSubscriptionModel, ProjectModel, UserModel } from \"@storyshelf/core/models\";\nimport { eq, getTableColumns } from \"@storyshelf/core/orm\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { SESSION_COOKIE, type AuthUser } from \"@storyshelf/core/types\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { renderProfilePage } from \"../pages/profile.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { forwardToEngine } from \"./auth.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** User columns needed to render the profile page. */\ninterface ProfileRow {\n email: string;\n name: string;\n displayNameOverride: string | null;\n authProvider: string;\n lastLoginAt: string | null;\n createdAt: string;\n}\n\n/** Membership entry for the profile page. */\ninterface ProfileMembership {\n projectName: string;\n projectSlug: string;\n role: string;\n source: string;\n}\n\nfunction field(form: FormData, name: string): string {\n const raw = form.get(name);\n return typeof raw === \"string\" ? raw : \"\";\n}\n\nfunction toDbUser(row: ProfileRow | null): ProfilePage[\"dbUser\"] {\n if (!row) {\n return undefined;\n }\n return {\n email: row.email,\n name: row.name,\n displayNameOverride: row.displayNameOverride,\n authProvider: row.authProvider,\n lastLoginAt: row.lastLoginAt,\n createdAt: row.createdAt,\n };\n}\n\ntype ProfilePage = Parameters<typeof renderProfilePage>[0];\n\nasync function loadProfileData(userId: string): Promise<{\n userRow: ProfileRow | null;\n memberships: ProfileMembership[];\n subscribedSlugs: string[];\n}> {\n const { db } = getStore();\n const userRow = (await new UserModel(db).get(userId)) as unknown as ProfileRow | null;\n const members = (await db.list(db.tables.projectMembers, {\n where: eq(getTableColumns(db.tables.projectMembers)[\"userId\"] as never, userId),\n })) as unknown as Array<{ projectId: string; role: string; source: string }>;\n const projects = (await db.list(db.tables.projects)) as unknown as Array<{\n id: string;\n name: string;\n slug: string;\n }>;\n const byId = new Map(projects.map((project) => [project.id, project]));\n const memberships = members.flatMap((member) => {\n const project = byId.get(member.projectId);\n return project\n ? [\n {\n projectName: project.name,\n projectSlug: project.slug,\n role: member.role,\n source: member.source,\n },\n ]\n : [];\n });\n const subs = await new NotificationSubscriptionModel(db).listForUser(userId);\n const subscribedIds = new Set(subs.filter((sub) => sub.enabled).map((sub) => sub.projectId));\n const subscribedSlugs = [...byId.values()]\n .filter((project) => subscribedIds.has(project.id))\n .map((project) => project.slug);\n return { userRow, memberships, subscribedSlugs };\n}\n\nfunction readSessionToken(c: Context): string {\n const header = c.req.header(\"cookie\") ?? \"\";\n for (const part of header.split(\";\")) {\n const eqIndex = part.indexOf(\"=\");\n if (eqIndex !== -1 && part.slice(0, eqIndex).trim() === SESSION_COOKIE) {\n // Engine cookies are signed (`token.signature`); the table holds the raw token.\n return unsignedToken(part.slice(eqIndex + 1).trim());\n }\n }\n return \"\";\n}\n\n/** Engine security overview for the profile page. */\nasync function loadEngineSecurity(\n c: Context,\n auth: Auth,\n user: AuthUser,\n): Promise<NonNullable<ProfilePage[\"security\"]>> {\n const [sessions, passkeys, hasPassword] = await Promise.all([\n auth.listSessions(user.id),\n auth.listPasskeys(user.id),\n auth.hasPassword(user.id),\n ]);\n const current = readSessionToken(c);\n return {\n sessions: sessions.map((session) => ({\n id: session.id,\n ipAddress: session.ipAddress,\n userAgent: session.userAgent,\n createdAt: session.createdAt,\n expiresAt: session.expiresAt,\n current: session.token === current,\n })),\n passkeys,\n passkeysEnabled: auth.passkeysEnabled(),\n hasPassword,\n };\n}\n\nasync function profileView(\n c: Context,\n user: AuthUser,\n auth: Auth,\n extra?: { error?: string; success?: string; local?: boolean; status?: 200 | 400 },\n): Promise<Response> {\n const { userRow, memberships, subscribedSlugs } = await loadProfileData(user.id);\n const security = await loadEngineSecurity(c, auth, user);\n const html = await renderProfilePage({\n user,\n dbUser: toDbUser(userRow),\n memberships,\n subscribedSlugs,\n isLocal: extra?.local ?? security.hasPassword,\n security,\n error: extra?.error,\n });\n if (extra?.success) {\n flash(c, extra.success);\n }\n // Device tokens render into the page (per-device revoke forms): never store.\n c.header(\"Cache-Control\", \"no-store\");\n return c.html(html, extra?.status ?? 200);\n}\n\n/** Register the personal profile page and its mutations. */\nexport function registerProfile(app: ShelfRouter, auth: Auth): void {\n app.get(\"/profile\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await profileView(c, user, auth);\n });\n\n app.post(\"/profile\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n const displayName = field(await c.req.formData(), \"displayName\").trim();\n if (!displayName) {\n return profileView(c, user, auth, { error: \"Display name is required\", status: 400 });\n }\n await new UserModel(getStore().db).setDisplayNameOverride(user.id, displayName);\n flash(c, \"Profile updated\");\n return hxRedirect(c, \"/profile\");\n });\n\n registerPasswordRoute(app, auth);\n registerSessionRoutes(app, auth);\n registerPasskeyRoutes(app, auth);\n registerNotificationsRoute(app, auth);\n}\n\nfunction registerNotificationsRoute(app: ShelfRouter, auth: Auth): void {\n app.post(\"/profile/notifications\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await saveProfileNotifications(c, user, auth);\n });\n}\n\n/** Toggle the viewer's own email subscription for one member project. */\nasync function saveProfileNotifications(c: Context, user: AuthUser, auth: Auth): Promise<Response> {\n const form = await c.req.formData();\n const project = await memberProject(field(form, \"slug\"), user.id);\n if (!project) {\n return profileView(c, user, auth, { error: \"Unknown project\", status: 400 });\n }\n await applyProfileToggle(project.id, user.id, field(form, \"enabled\") === \"1\");\n flash(c, \"Notification preference saved\");\n return hxRedirect(c, \"/profile\");\n}\n\n/** Resolve a project the viewer belongs to (null otherwise). */\nasync function memberProject(slug: string, userId: string): Promise<Project | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { memberships } = await loadProfileData(userId);\n const isMember = memberships.some((membership) => membership.projectSlug === slug);\n return isMember ? project : null;\n}\n\n/** Enable (opt-in email) or disable (remove) a subscription. */\nasync function applyProfileToggle(\n projectId: string,\n userId: string,\n enable: boolean,\n): Promise<void> {\n const model = new NotificationSubscriptionModel(getStore().db);\n if (enable) {\n await model.upsert(projectId, userId, { via: [\"email\"] });\n return;\n }\n await model.remove(projectId, userId);\n}\n\nfunction registerPasswordRoute(app: ShelfRouter, auth: Auth): void {\n app.post(\"/profile/password\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await handleEnginePasswordChange(c, auth, user);\n });\n}\n\nfunction registerSessionRoutes(app: ShelfRouter, auth: Auth): void {\n app.post(\"/profile/sessions/revoke\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await handleSessionRevoke(c, auth, user);\n });\n\n app.post(\"/profile/sessions/revoke-others\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await handleRevokeOthers(c, auth, user);\n });\n}\n\nfunction registerPasskeyRoutes(app: ShelfRouter, auth: Auth): void {\n app.post(\"/profile/passkeys/:passkeyId/delete\", async (c) => {\n const { user } = getStore();\n if (!user) {\n return c.redirect(\"/auth/login\", 302);\n }\n return await handlePasskeyDelete(c, auth, user);\n });\n}\n\nasync function handleSessionRevoke(c: Context, auth: Auth, user: AuthUser): Promise<Response> {\n const id = await readSessionId(c);\n if (!id) {\n return profileView(c, user, auth, { error: \"Missing session\", status: 400 });\n }\n const token = await resolveSessionToken(auth, user, id);\n if (!token) {\n return profileView(c, user, auth, { error: \"Could not revoke that session\", status: 400 });\n }\n const res = await forwardToEngine(c, auth, \"/api/auth/revoke-session\", { token });\n if (res.status !== 200) {\n return profileView(c, user, auth, { error: \"Could not revoke that session\", status: 400 });\n }\n return profileView(c, user, auth, { success: \"Session revoked\" });\n}\n\nasync function readSessionId(c: Context): Promise<string> {\n return field(await c.req.formData(), \"sessionId\");\n}\n\nasync function resolveSessionToken(auth: Auth, user: AuthUser, id: string): Promise<string | null> {\n const sessions = await auth.listSessions(user.id);\n return sessions.find((session) => session.id === id)?.token ?? null;\n}\n\nasync function handleRevokeOthers(c: Context, auth: Auth, user: AuthUser): Promise<Response> {\n const res = await forwardToEngine(c, auth, \"/api/auth/revoke-other-sessions\", {});\n if (res.status !== 200) {\n return profileView(c, user, auth, { error: \"Could not sign out other devices\", status: 400 });\n }\n return profileView(c, user, auth, { success: \"Signed out of other devices\" });\n}\n\nasync function handlePasskeyDelete(c: Context, auth: Auth, user: AuthUser): Promise<Response> {\n const id = c.req.param(\"passkeyId\") ?? \"\";\n if (!/^[A-Za-z0-9_-]{1,64}$/u.test(id)) {\n return profileView(c, user, auth, { error: \"Unknown passkey\", status: 400 });\n }\n const res = await forwardToEngine(c, auth, \"/api/auth/passkey/delete-passkey\", { id });\n if (res.status !== 200) {\n return profileView(c, user, auth, { error: \"Could not remove that passkey\", status: 400 });\n }\n return profileView(c, user, auth, { success: \"Passkey removed\" });\n}\n\nasync function engineErrorMessage(res: Response, fallback: string): Promise<string> {\n try {\n const body = (await res.json()) as { message?: unknown };\n return typeof body.message === \"string\" && body.message ? body.message : fallback;\n } catch {\n return fallback;\n }\n}\n\n/** Validate the password form and change it through the engine. */\nasync function handleEnginePasswordChange(\n c: Context,\n auth: Auth,\n user: AuthUser,\n): Promise<Response> {\n const { current, next, error } = readPasswordChange(await c.req.formData());\n if (error) {\n return profileView(c, user, auth, { error, local: true, status: 400 });\n }\n const res = await forwardToEngine(c, auth, \"/api/auth/change-password\", {\n currentPassword: current,\n newPassword: next,\n revokeOtherSessions: true,\n });\n if (res.status !== 200) {\n const message = await engineErrorMessage(res, \"Password change failed\");\n return profileView(c, user, auth, { error: message, local: true, status: 400 });\n }\n return profileView(c, user, auth, { success: \"Password changed\", local: true });\n}\n\nfunction readPasswordChange(form: FormData): { current: string; next: string; error?: string } {\n const next = field(form, \"newPassword\");\n if (next !== field(form, \"confirmPassword\")) {\n return { current: \"\", next: \"\", error: \"Passwords do not match\" };\n }\n if (next.length < MIN_PASSWORD_LENGTH) {\n return {\n current: \"\",\n next: \"\",\n error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters`,\n };\n }\n return { current: field(form, \"currentPassword\"), next };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyshelf/app",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "StoryShelf app: Hono app, API routes, and server-rendered UI over @storyshelf/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chromatic-alternative",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@hono/swagger-ui": "^0.6.1",
|
|
59
59
|
"@hono/zod-openapi": "^1.1.5",
|
|
60
|
-
"@storyshelf/core": "0.5.
|
|
61
|
-
"@storyshelf/observability": "0.5.
|
|
60
|
+
"@storyshelf/core": "0.5.5",
|
|
61
|
+
"@storyshelf/observability": "0.5.5",
|
|
62
62
|
"hono": "^4.11.0",
|
|
63
63
|
"zod": "^4.2.0"
|
|
64
64
|
},
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"@opentelemetry/context-async-hooks": "^2.11.0",
|
|
68
68
|
"@opentelemetry/core": "^2.11.0",
|
|
69
69
|
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
70
|
-
"@storyshelf/auth": "0.5.
|
|
71
|
-
"@storyshelf/db-sqlite": "0.5.
|
|
70
|
+
"@storyshelf/auth": "0.5.5",
|
|
71
|
+
"@storyshelf/db-sqlite": "0.5.5",
|
|
72
72
|
"@types/node": "^26.0.0",
|
|
73
73
|
"@vitest/coverage-v8": "^4.0.0",
|
|
74
74
|
"adm-zip": "0.6.0",
|