diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01f6ae5..a8c8e62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 registry-url: https://registry.npmjs.org/ - name: Determine package and version @@ -68,5 +68,3 @@ jobs: - name: Publish to npm working-directory: packages/${{ steps.meta.outputs.package }} run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index f3bed51..aa30f95 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@seamless-auth/core", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@seamless-auth/core", - "version": "0.2.1", + "version": "0.3.0", "license": "AGPL-3.0-only", "dependencies": { "jose": "^6.1.3", @@ -1345,9 +1345,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3108,9 +3108,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3315,9 +3315,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index b68d452..c52c814 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@seamless-auth/core", - "version": "0.2.1", + "version": "0.3.0", "description": "Framework-agnostic core authentication logic for SeamlessAuth", "license": "AGPL-3.0-only", "author": "Fells Code, LLC", diff --git a/packages/core/src/ensureCookies.ts b/packages/core/src/ensureCookies.ts index 61714f8..0dae9a3 100644 --- a/packages/core/src/ensureCookies.ts +++ b/packages/core/src/ensureCookies.ts @@ -80,6 +80,42 @@ const COOKIE_REQUIREMENTS: Record< }, "/logout": { name: "accessCookieName", required: true }, "/users/me": { name: "accessCookieName", required: true }, + "/internal/metrics/dashboard": { name: "accessCookieName", required: true }, + "/internal/auth-events/timeseries": { + name: "accessCookieName", + required: true, + }, + + "/internal/auth-events/grouped": { name: "accessCookieName", required: true }, + "/internal/auth-events/login-stats": { + name: "accessCookieName", + required: true, + }, + + "/internal/security/anomalies": { name: "accessCookieName", required: true }, + + "/admin/user": { + name: "accessCookieName", + required: true, + }, + "/admin/sessions": { + name: "accessCookieName", + required: true, + }, + "/admin/auth-events": { + name: "accessCookieName", + required: true, + }, + + "/system-config/admin": { + name: "accessCookieName", + required: true, + }, + + "/system-config/roles": { + name: "accessCookieName", + required: true, + }, }; export async function ensureCookies( diff --git a/packages/core/src/handlers/admin.ts b/packages/core/src/handlers/admin.ts new file mode 100644 index 0000000..1be1dd9 --- /dev/null +++ b/packages/core/src/handlers/admin.ts @@ -0,0 +1,94 @@ +import { authFetch } from "../authFetch.js"; + +type BaseOpts = { + authServerUrl: string; + authorization?: string; +}; + +type WithQuery = BaseOpts & { + query?: Record; +}; + +type WithBody = BaseOpts & { + body?: any; +}; + +type Result = { + status: number; + body?: any; + error?: string; +}; + +function buildUrl(base: string, query?: Record) { + if (!query) return base; + + const qs = new URLSearchParams( + Object.entries(query) + .filter(([, v]) => v !== undefined && v !== null) + .map(([k, v]) => [k, String(v)]), + ).toString(); + + return qs ? `${base}?${qs}` : base; +} + +async function request( + method: "GET" | "POST" | "PATCH" | "DELETE", + path: string, + opts: WithQuery & WithBody, +): Promise { + const up = await authFetch( + buildUrl(`${opts.authServerUrl}${path}`, opts.query), + { + method, + authorization: opts.authorization, + body: opts.body, + }, + ); + + const data = await up.json(); + + if (!up.ok) { + return { + status: up.status, + error: data?.error || "admin_request_failed", + }; + } + + return { + status: up.status, + body: data, + }; +} + +export const getUsersHandler = (opts: BaseOpts) => + request("GET", "/admin/users", opts); + +export const createUserHandler = (opts: WithBody) => + request("POST", "/admin/users", opts); + +export const deleteUserHandler = (opts: BaseOpts) => + request("DELETE", "/admin/users", opts); + +export const updateUserHandler = (userId: string, opts: WithBody) => + request("PATCH", `/admin/users/${userId}`, opts); + +export const getUserDetailHandler = (userId: string, opts: BaseOpts) => + request("GET", `/admin/users/${userId}`, opts); + +export const getUserAnomaliesHandler = (userId: string, opts: BaseOpts) => + request("GET", `/admin/users/${userId}/anomalies`, opts); + +export const getAuthEventsHandler = (opts: WithQuery) => + request("GET", "/admin/auth-events", opts); + +export const getCredentialCountHandler = (opts: BaseOpts) => + request("GET", "/admin/credential-count", opts); + +export const listAllSessionsHandler = (opts: WithQuery) => + request("GET", "/admin/sessions", opts); + +export const listUserSessionsHandler = (userId: string, opts: BaseOpts) => + request("GET", `/admin/sessions/${userId}`, opts); + +export const revokeAllUserSessionsHandler = (userId: string, opts: BaseOpts) => + request("DELETE", `/admin/sessions/${userId}/revoke-all`, opts); diff --git a/packages/core/src/handlers/bootstrapAdminInvite.ts b/packages/core/src/handlers/bootstrapAdminInvite.ts new file mode 100644 index 0000000..9c47caa --- /dev/null +++ b/packages/core/src/handlers/bootstrapAdminInvite.ts @@ -0,0 +1,46 @@ +import { authFetch } from "../authFetch.js"; + +export interface BootstrapAdminInviteOptions { + authServerUrl: string; + email: string; + authorization?: string; +} + +export interface BootstrapAdminInviteResult { + status: number; + body?: { + url?: string; + expiresAt: string; + token?: string; + }; + error?: string; +} + +export async function bootstrapAdminInviteHandler( + opts: BootstrapAdminInviteOptions, +): Promise { + const up = await authFetch( + `${opts.authServerUrl}/internal/bootstrap/admin-invite`, + { + method: "POST", + headers: { + authorization: opts.authorization || "", + }, + body: { email: opts.email }, + }, + ); + + const data = await up.json(); + + if (!up.ok) { + return { + status: up.status, + error: data?.error?.message || "bootstrap_failed", + }; + } + + return { + status: up.status, + body: data?.data, + }; +} diff --git a/packages/core/src/handlers/finishRegister.ts b/packages/core/src/handlers/finishRegister.ts index 5cca91d..667b5f3 100644 --- a/packages/core/src/handlers/finishRegister.ts +++ b/packages/core/src/handlers/finishRegister.ts @@ -4,6 +4,7 @@ import { verifySignedAuthResponse } from "../verifySignedAuthResponse.js"; export interface FinishRegisterInput { authorization?: string; + headers?: Record; body: unknown; } @@ -31,8 +32,9 @@ export async function finishRegisterHandler( ): Promise { const up = await authFetch(`${opts.authServerUrl}/webAuthn/register/finish`, { method: "POST", - body: input.body, authorization: input.authorization, + headers: input.headers, + body: input.body, }); const data = await up.json(); diff --git a/packages/core/src/handlers/internalMetrics.ts b/packages/core/src/handlers/internalMetrics.ts new file mode 100644 index 0000000..4d95186 --- /dev/null +++ b/packages/core/src/handlers/internalMetrics.ts @@ -0,0 +1,69 @@ +import { authFetch } from "../authFetch.js"; + +type BaseOpts = { + authServerUrl: string; + authorization?: string; +}; + +type WithQuery = BaseOpts & { + query?: Record; +}; + +type Result = { + status: number; + body?: any; + error?: string; +}; + +function buildUrl(base: string, query?: WithQuery["query"]) { + if (!query) return base; + const qs = new URLSearchParams( + Object.entries(query) + .filter(([, v]) => v !== undefined && v !== null) + .map(([k, v]) => [k, String(v)]), + ).toString(); + + return qs ? `${base}?${qs}` : base; +} + +async function get(path: string, opts: WithQuery): Promise { + const up = await authFetch( + buildUrl(`${opts.authServerUrl}${path}`, opts.query), + { + method: "GET", + authorization: opts.authorization, + }, + ); + + const data = await up.json(); + + if (!up.ok) { + return { + status: up.status, + error: data?.error || "internal_request_failed", + }; + } + + return { + status: up.status, + body: data, + }; +} + +export const getAuthEventSummaryHandler = (opts: WithQuery) => + get("/internal/auth-events/summary", opts); + +export const getAuthEventTimeseriesHandler = (opts: WithQuery) => + get("/internal/auth-events/timeseries", opts); + +export const getLoginStatsHandler = (opts: BaseOpts) => + get("/internal/auth-events/login-stats", opts); + +export const getSecurityAnomaliesHandler = (opts: BaseOpts) => + get("/internal/security/anomalies", opts); + +export const getDashboardMetricsHandler = (opts: BaseOpts) => + get("/internal/metrics/dashboard", opts); + +export const getGroupedEventSummaryHandler = (opts: BaseOpts) => + get("/internal/auth-events/grouped", opts); diff --git a/packages/core/src/handlers/register.ts b/packages/core/src/handlers/register.ts index 1b9e775..b204595 100644 --- a/packages/core/src/handlers/register.ts +++ b/packages/core/src/handlers/register.ts @@ -41,16 +41,44 @@ export async function registerHandler( }; } + const rawCookies = + (up.headers as any).getSetCookie?.() || + up.headers.get?.("set-cookie")?.split(",") || + []; + + let bootstrapCookie; + + for (const cookie of rawCookies) { + if (cookie.startsWith("seamless_bootstrap_token=")) { + const value = cookie.split(";")[0].split("=")[1]; + + bootstrapCookie = { + name: "seamless_bootstrap_token", + value: { sub: value }, + ttl: "900", + domain: opts.cookieDomain, + }; + + break; + } + } + + const setCookies = [ + { + name: opts.registrationCookieName, + value: { sub: data.sub }, + ttl: data.ttl, + domain: opts.cookieDomain, + }, + ]; + + if (bootstrapCookie) { + setCookies.push(bootstrapCookie); + } + return { status: 200, body: data, - setCookies: [ - { - name: opts.registrationCookieName, - value: { sub: data.sub }, - ttl: data.ttl, - domain: opts.cookieDomain, - }, - ], + setCookies, }; } diff --git a/packages/core/src/handlers/sessions.ts b/packages/core/src/handlers/sessions.ts new file mode 100644 index 0000000..878222e --- /dev/null +++ b/packages/core/src/handlers/sessions.ts @@ -0,0 +1,46 @@ +import { authFetch } from "../authFetch.js"; + +type BaseOpts = { + authServerUrl: string; + authorization?: string; +}; + +type Result = { + status: number; + body?: any; + error?: string; +}; + +async function request( + method: "GET" | "DELETE", + path: string, + opts: BaseOpts, +): Promise { + const up = await authFetch(`${opts.authServerUrl}${path}`, { + method, + authorization: opts.authorization, + }); + + const data = await up.json(); + + if (!up.ok) { + return { + status: up.status, + error: data?.error || "session_request_failed", + }; + } + + return { + status: up.status, + body: data, + }; +} + +export const listSessionsHandler = (opts: BaseOpts) => + request("GET", "/sessions", opts); + +export const revokeSessionHandler = (id: string, opts: BaseOpts) => + request("DELETE", `/sessions/${id}`, opts); + +export const revokeAllSessionsHandler = (opts: BaseOpts) => + request("DELETE", "/sessions", opts); diff --git a/packages/core/src/handlers/systemConfig.ts b/packages/core/src/handlers/systemConfig.ts new file mode 100644 index 0000000..ded230c --- /dev/null +++ b/packages/core/src/handlers/systemConfig.ts @@ -0,0 +1,82 @@ +import { authFetch } from "../authFetch.js"; + +export interface SystemConfigOptions { + authServerUrl: string; + authorization?: string; +} + +export interface SystemConfigResult { + status: number; + body?: any; + error?: string; +} + +export async function getAvailableRolesHandler( + opts: SystemConfigOptions, +): Promise { + const up = await authFetch(`${opts.authServerUrl}/system-config/roles`, { + method: "GET", + authorization: opts.authorization, + }); + + const data = await up.json(); + + if (!up.ok) { + return { + status: up.status, + error: data?.error || "failed_to_fetch_roles", + }; + } + + return { + status: up.status, + body: data, + }; +} + +export async function getSystemConfigAdminHandler( + opts: SystemConfigOptions, +): Promise { + const up = await authFetch(`${opts.authServerUrl}/system-config/admin`, { + method: "GET", + authorization: opts.authorization, + }); + + const data = await up.json(); + + if (!up.ok) { + return { + status: up.status, + error: data?.error || "failed_to_fetch_config", + }; + } + + return { + status: up.status, + body: data, + }; +} + +export async function updateSystemConfigHandler( + opts: SystemConfigOptions & { payload: any }, +): Promise { + const up = await authFetch(`${opts.authServerUrl}/system-config/admin`, { + method: "PATCH", + authorization: opts.authorization, + body: opts.payload, + }); + + const data = await up.json(); + + if (!up.ok) { + return { + status: up.status, + error: data?.error || "failed_to_update_config", + }; + } + + return { + status: up.status, + body: data, + }; +} diff --git a/packages/express/package-lock.json b/packages/express/package-lock.json index 5fb0431..97f95b8 100644 --- a/packages/express/package-lock.json +++ b/packages/express/package-lock.json @@ -1,12 +1,12 @@ { "name": "@seamless-auth/express", - "version": "0.1.2", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@seamless-auth/express", - "version": "0.1.2", + "version": "0.2.0", "license": "AGPL-3.0-only", "dependencies": { "@seamless-auth/core": "^0.2.1", diff --git a/packages/express/package.json b/packages/express/package.json index 784e5ec..2240674 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -1,6 +1,6 @@ { "name": "@seamless-auth/express", - "version": "0.1.2", + "version": "0.2.0", "description": "Express adapter for Seamless Auth passwordless authentication", "license": "AGPL-3.0-only", "type": "module", @@ -37,7 +37,7 @@ "express": ">=4.18.0" }, "dependencies": { - "@seamless-auth/core": "^0.2.1", + "@seamless-auth/core": "^0.3.0", "cookie-parser": "^1.4.6", "jsonwebtoken": "^9.0.3" }, diff --git a/packages/express/src/createServer.ts b/packages/express/src/createServer.ts index 69b42b5..031f75e 100644 --- a/packages/express/src/createServer.ts +++ b/packages/express/src/createServer.ts @@ -10,13 +10,32 @@ import { finishRegister } from "./handlers/finishRegister"; import { me } from "./handlers/me"; import { logout } from "./handlers/logout"; import { pollMagicLinkConfirmation } from "./handlers/pollMagicLinkConfirmation"; - +import * as admin from "./handlers/admin"; import { authFetch, EnsureCookiesOptions, AuthFetchOptions, } from "@seamless-auth/core"; import { buildServiceAuthorization } from "./internal/buildAuthorization"; +import { bootstrapAdminInvite } from "./handlers/bootstrapAdmininvite"; +import { + getAvailableRoles, + getSystemConfigAdmin, + updateSystemConfig, +} from "./handlers/systemConfig"; +import { + getAuthEventSummary, + getAuthEventTimeseries, + getDashboardMetrics, + getGroupedEventSummary, + getLoginStats, + getSecurityAnomalies, +} from "./handlers/internalMetrics"; +import { + listSessions, + revokeAllSessions, + revokeSession, +} from "./handlers/sessions"; type ResolvedSeamlessAuthServerOptions = { authServerUrl: string; @@ -262,6 +281,88 @@ export function createSeamlessAuthServer( r.get("/magic-link/check", (req, res) => pollMagicLinkConfirmation(req, res, resolvedOpts), ); + r.post("/internal/bootstrap/admin-invite", (req, res) => + bootstrapAdminInvite(req, res, resolvedOpts), + ); + r.get("/system-config/roles", (req, res) => + getAvailableRoles(req, res, resolvedOpts), + ); + + r.get("/system-config/admin", (req, res) => + getSystemConfigAdmin(req, res, resolvedOpts), + ); + + r.patch("/system-config/admin", (req, res) => + updateSystemConfig(req, res, resolvedOpts), + ); + + r.get("/internal/auth-events/summary", (req, res) => + getAuthEventSummary(req, res, resolvedOpts), + ); + + r.get("/internal/auth-events/timeseries", (req, res) => + getAuthEventTimeseries(req, res, resolvedOpts), + ); + + r.get("/internal/auth-events/login-stats", (req, res) => + getLoginStats(req, res, resolvedOpts), + ); + + r.get("/internal/security/anomalies", (req, res) => + getSecurityAnomalies(req, res, resolvedOpts), + ); + + r.get("/internal/metrics/dashboard", (req, res) => + getDashboardMetrics(req, res, resolvedOpts), + ); + + r.get("/internal/auth-events/grouped", (req, res) => + getGroupedEventSummary(req, res, resolvedOpts), + ); + + r.get("/admin/users", (req, res) => admin.getUsers(req, res, resolvedOpts)); + r.post("/admin/users", (req, res) => + admin.createUser(req, res, resolvedOpts), + ); + r.delete("/admin/users", (req, res) => + admin.deleteUser(req, res, resolvedOpts), + ); + r.patch("/admin/users/:userId", (req, res) => + admin.updateUser(req, res, resolvedOpts), + ); + r.get("/admin/users/:userId", (req, res) => + admin.getUserDetail(req, res, resolvedOpts), + ); + r.get("/admin/users/:userId/anomalies", (req, res) => + admin.getUserAnomalies(req, res, resolvedOpts), + ); + + r.get("/admin/auth-events", (req, res) => + admin.getAuthEvents(req, res, resolvedOpts), + ); + r.get("/admin/credential-count", (req, res) => + admin.getCredentialCount(req, res, resolvedOpts), + ); + + r.get("/admin/sessions", (req, res) => + admin.listAllSessions(req, res, resolvedOpts), + ); + r.get("/admin/sessions/:userId", (req, res) => + admin.listUserSessions(req, res, resolvedOpts), + ); + r.delete("/admin/sessions/:userId/revoke-all", (req, res) => + admin.revokeAllUserSessions(req, res, resolvedOpts), + ); + + r.get("/sessions", (req, res) => listSessions(req, res, resolvedOpts)); + + r.delete("/sessions/:id", (req, res) => + revokeSession(req, res, resolvedOpts), + ); + + r.delete("/sessions", (req, res) => + revokeAllSessions(req, res, resolvedOpts), + ); return r; } diff --git a/packages/express/src/handlers/admin.ts b/packages/express/src/handlers/admin.ts new file mode 100644 index 0000000..bbdcda7 --- /dev/null +++ b/packages/express/src/handlers/admin.ts @@ -0,0 +1,171 @@ +import { Request, Response } from "express"; +import { + getUsersHandler, + createUserHandler, + deleteUserHandler, + updateUserHandler, + getUserDetailHandler, + getUserAnomaliesHandler, + getAuthEventsHandler, + getCredentialCountHandler, + listAllSessionsHandler, + listUserSessionsHandler, + revokeAllUserSessionsHandler, +} from "@seamless-auth/core/handlers/admin"; + +import { buildServiceAuthorization } from "../internal/buildAuthorization"; +import { SeamlessAuthServerOptions } from "../createServer"; + +function handle(res: Response, result: any) { + if (result.error) { + return res.status(result.status).json({ error: result.error }); + } + return res.status(result.status).json(result.body); +} + +export const getUsers = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await getUsersHandler({ + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + }), + ); + +export const createUser = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await createUserHandler({ + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + body: req.body, + }), + ); + +export const deleteUser = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await deleteUserHandler({ + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + }), + ); + +export const updateUser = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await updateUserHandler(req.params.userId as string, { + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + body: req.body, + }), + ); + +export const getUserDetail = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await getUserDetailHandler(req.params.userId as string, { + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + }), + ); + +export const getUserAnomalies = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await getUserAnomaliesHandler(req.params.userId as string, { + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + }), + ); + +export const getAuthEvents = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await getAuthEventsHandler({ + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + query: req.query, + }), + ); + +export const getCredentialCount = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await getCredentialCountHandler({ + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + }), + ); + +export const listAllSessions = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await listAllSessionsHandler({ + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + query: req.query, + }), + ); + +export const listUserSessions = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await listUserSessionsHandler(req.params.userId as string, { + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + }), + ); + +export const revokeAllUserSessions = async ( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) => + handle( + res, + await revokeAllUserSessionsHandler(req.params.userId as string, { + authServerUrl: opts.authServerUrl, + authorization: buildServiceAuthorization(req, opts), + }), + ); diff --git a/packages/express/src/handlers/bootstrapAdmininvite.ts b/packages/express/src/handlers/bootstrapAdmininvite.ts new file mode 100644 index 0000000..5e43029 --- /dev/null +++ b/packages/express/src/handlers/bootstrapAdmininvite.ts @@ -0,0 +1,22 @@ +import { Request, Response } from "express"; +import { bootstrapAdminInviteHandler } from "@seamless-auth/core/handlers/bootstrapAdminInvite"; +import { buildServiceAuthorization } from "../internal/buildAuthorization"; +import { SeamlessAuthServerOptions } from "../createServer"; + +export async function bootstrapAdminInvite( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const result = await bootstrapAdminInviteHandler({ + authServerUrl: opts.authServerUrl, + email: req.body.email, + authorization: req.headers["authorization"], + }); + + if (result.error) { + return res.status(result.status).json({ error: result.error }); + } + + res.status(result.status).json(result.body); +} diff --git a/packages/express/src/handlers/finishRegister.ts b/packages/express/src/handlers/finishRegister.ts index da6b0ec..0b8c39e 100644 --- a/packages/express/src/handlers/finishRegister.ts +++ b/packages/express/src/handlers/finishRegister.ts @@ -3,6 +3,7 @@ import { finishRegisterHandler } from "@seamless-auth/core/handlers/finishRegist import { setSessionCookie } from "../internal/cookie"; import { buildServiceAuthorization } from "../internal/buildAuthorization"; import { SeamlessAuthServerOptions } from "../createServer"; +import { verifyCookieJwt } from "@seamless-auth/core"; export async function finishRegister( req: Request & { cookiePayload?: any }, @@ -20,8 +21,27 @@ export async function finishRegister( const authorization = buildServiceAuthorization(req, opts); + const bootstrapToken = req.cookies?.["seamless_bootstrap_token"]; + + const headers: Record = {}; + + if (bootstrapToken) { + const payload = verifyCookieJwt(bootstrapToken, opts.cookieSecret); + if (!payload || !payload.sub) { + res.status(401).json({ + error: "Invalid or expired session", + }); + return; + } + headers["cookie"] = `seamless_bootstrap_token=${payload.sub}`; + } + const result = await finishRegisterHandler( - { body: req.body, authorization }, + { + body: req.body, + authorization, + headers, + }, { authServerUrl: opts.authServerUrl, cookieDomain: opts.cookieDomain, diff --git a/packages/express/src/handlers/internalMetrics.ts b/packages/express/src/handlers/internalMetrics.ts new file mode 100644 index 0000000..9116c2f --- /dev/null +++ b/packages/express/src/handlers/internalMetrics.ts @@ -0,0 +1,111 @@ +import { Request, Response } from "express"; +import { + getAuthEventSummaryHandler, + getAuthEventTimeseriesHandler, + getLoginStatsHandler, + getSecurityAnomaliesHandler, + getDashboardMetricsHandler, + getGroupedEventSummaryHandler, +} from "@seamless-auth/core/handlers/internalMetrics"; + +import { buildServiceAuthorization } from "../internal/buildAuthorization"; +import { SeamlessAuthServerOptions } from "../createServer"; + +function handle(res: Response, result: any) { + if (result.error) { + return res.status(result.status).json({ error: result.error }); + } + return res.status(result.status).json(result.body); +} + +export async function getAuthEventSummary( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await getAuthEventSummaryHandler({ + authServerUrl: opts.authServerUrl, + authorization, + query: req.query as any, + }); + + return handle(res, result); +} + +export async function getAuthEventTimeseries( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await getAuthEventTimeseriesHandler({ + authServerUrl: opts.authServerUrl, + authorization, + query: req.query as any, + }); + + return handle(res, result); +} + +export async function getLoginStats( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await getLoginStatsHandler({ + authServerUrl: opts.authServerUrl, + authorization, + }); + + return handle(res, result); +} + +export async function getSecurityAnomalies( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await getSecurityAnomaliesHandler({ + authServerUrl: opts.authServerUrl, + authorization, + }); + + return handle(res, result); +} + +export async function getDashboardMetrics( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await getDashboardMetricsHandler({ + authServerUrl: opts.authServerUrl, + authorization, + }); + + return handle(res, result); +} + +export async function getGroupedEventSummary( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await getGroupedEventSummaryHandler({ + authServerUrl: opts.authServerUrl, + authorization, + }); + + return handle(res, result); +} diff --git a/packages/express/src/handlers/register.ts b/packages/express/src/handlers/register.ts index da04f19..653cac4 100644 --- a/packages/express/src/handlers/register.ts +++ b/packages/express/src/handlers/register.ts @@ -35,9 +35,9 @@ export async function register( setSessionCookie( res, { - name: opts.registrationCookieName || "seamless-auth-registraion", + name: c.name, payload: c.value, - domain: c.domain, + domain: c.domain ?? opts.cookieDomain, ttlSeconds: c.ttl, }, cookieSigner, diff --git a/packages/express/src/handlers/sessions.ts b/packages/express/src/handlers/sessions.ts new file mode 100644 index 0000000..ff067d7 --- /dev/null +++ b/packages/express/src/handlers/sessions.ts @@ -0,0 +1,61 @@ +import { Request, Response } from "express"; +import { + listSessionsHandler, + revokeSessionHandler, + revokeAllSessionsHandler, +} from "@seamless-auth/core/handlers/sessions"; + +import { buildServiceAuthorization } from "../internal/buildAuthorization"; +import { SeamlessAuthServerOptions } from "../createServer"; + +function handle(res: Response, result: any) { + if (result.error) { + return res.status(result.status).json({ error: result.error }); + } + return res.status(result.status).json(result.body); +} + +export async function listSessions( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await listSessionsHandler({ + authServerUrl: opts.authServerUrl, + authorization, + }); + + return handle(res, result); +} + +export async function revokeSession( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await revokeSessionHandler(req.params.id as string, { + authServerUrl: opts.authServerUrl, + authorization, + }); + + return handle(res, result); +} + +export async function revokeAllSessions( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await revokeAllSessionsHandler({ + authServerUrl: opts.authServerUrl, + authorization, + }); + + return handle(res, result); +} diff --git a/packages/express/src/handlers/systemConfig.ts b/packages/express/src/handlers/systemConfig.ts new file mode 100644 index 0000000..d71eba9 --- /dev/null +++ b/packages/express/src/handlers/systemConfig.ts @@ -0,0 +1,67 @@ +import { Request, Response } from "express"; +import { + getAvailableRolesHandler, + getSystemConfigAdminHandler, + updateSystemConfigHandler, +} from "@seamless-auth/core/handlers/systemConfig"; + +import { buildServiceAuthorization } from "../internal/buildAuthorization"; +import { SeamlessAuthServerOptions } from "../createServer"; + +export async function getAvailableRoles( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await getAvailableRolesHandler({ + authServerUrl: opts.authServerUrl, + authorization, + }); + + if (result.error) { + return res.status(result.status).json({ error: result.error }); + } + + res.status(result.status).json(result.body); +} + +export async function getSystemConfigAdmin( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await getSystemConfigAdminHandler({ + authServerUrl: opts.authServerUrl, + authorization, + }); + + if (result.error) { + return res.status(result.status).json({ error: result.error }); + } + + res.status(result.status).json(result.body); +} + +export async function updateSystemConfig( + req: Request, + res: Response, + opts: SeamlessAuthServerOptions, +) { + const authorization = buildServiceAuthorization(req, opts); + + const result = await updateSystemConfigHandler({ + authServerUrl: opts.authServerUrl, + authorization, + payload: req.body, + }); + + if (result.error) { + return res.status(result.status).json({ error: result.error }); + } + + res.status(result.status).json(result.body); +}