chore: seminarhof customer project
This commit is contained in:
72
packages/functions/src/wirings/auth.wiring.ts
Normal file
72
packages/functions/src/wirings/auth.wiring.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { betterAuth } from 'better-auth'
|
||||
import { admin } from 'better-auth/plugins'
|
||||
import { actor, fabric } from '@pikku/better-auth'
|
||||
import { pikkuBetterAuth } from '#pikku/pikku-types.gen.js'
|
||||
|
||||
export const auth = pikkuBetterAuth(async ({ kysely, secrets, variables }) => {
|
||||
const BETTER_AUTH_SECRET = await secrets.getSecret('BETTER_AUTH_SECRET')
|
||||
// Genuinely optional: unset simply disables /api/auth/sign-in/actor (scenarios
|
||||
// off for this deployment) — the actor plugin refuses all sign-ins
|
||||
// without it.
|
||||
const SCENARIO_ACTOR_SECRET = await secrets
|
||||
.getSecret('SCENARIO_ACTOR_SECRET')
|
||||
.catch(() => undefined)
|
||||
// Fabric operator admin: the RSA public key the control plane's token is
|
||||
// verified against. The Fabric deployer pushes FABRIC_AUTH_PUBLIC_KEY onto
|
||||
// every stage; locally it's simply absent, which disables /sign-in/fabric.
|
||||
// Asymmetric — the app verifies, it can never forge an operator login.
|
||||
const FABRIC_AUTH_PUBLIC_KEY = await variables.get('FABRIC_AUTH_PUBLIC_KEY')
|
||||
|
||||
return betterAuth({
|
||||
secret: BETTER_AUTH_SECRET,
|
||||
database: { db: kysely as any, type: 'sqlite' },
|
||||
emailAndPassword: { enabled: true },
|
||||
session: {
|
||||
cookieCache: { enabled: true, maxAge: 5 * 60 },
|
||||
},
|
||||
advanced: { database: { generateId: 'uuid' } },
|
||||
user: {
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
defaultValue: 'client',
|
||||
input: false,
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
defaultValue: 'de',
|
||||
input: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Scenario actors: synthetic users (user.actor = true, see
|
||||
// db/sqlite/0012-user-actor.sql) signed in by pikkuScenario via
|
||||
// POST /api/auth/sign-in/actor { email, secret }. Never signs in real users.
|
||||
//
|
||||
// admin(): exposes /api/auth/admin/* (listUsers, setRole, impersonateUser,
|
||||
// …) so an app admin can list and "view as" their end-users — this is what
|
||||
// the Fabric console's Users tab drives. Adds role/banned/impersonatedBy
|
||||
// columns (see db/sqlite/0013-admin.sql). No user is an admin by default:
|
||||
// grant it by setting a user's `role` to 'admin' (or pass
|
||||
// `adminUserIds: [...]` here) — the admin API refuses non-admins.
|
||||
//
|
||||
// fabric(): exposes /api/auth/sign-in/fabric — the Fabric control plane
|
||||
// mints a short-lived RS256 token and signs in as a synthetic `fabric: true`
|
||||
// admin operator (db/sqlite/0014-fabric.sql), so the console Users tab can
|
||||
// list/impersonate real users without the operator being one of them. It
|
||||
// pairs with admin() and verifies against FABRIC_AUTH_PUBLIC_KEY; missing
|
||||
// key disables the endpoint.
|
||||
plugins: [
|
||||
actor({ secret: SCENARIO_ACTOR_SECRET }),
|
||||
// New users must get a role the user.role CHECK ('admin','client','owner')
|
||||
// accepts. The admin plugin otherwise defaults them to 'user', which the
|
||||
// CHECK rejects → every sign-up fails with SQLITE_CONSTRAINT_CHECK. Keep
|
||||
// adminRoles at its default ('admin') — a custom role like 'owner' would
|
||||
// need a matching `roles` access-control definition.
|
||||
admin({ defaultRole: 'client' }),
|
||||
fabric({ publicKey: FABRIC_AUTH_PUBLIC_KEY }),
|
||||
],
|
||||
})
|
||||
})
|
||||
48
packages/functions/src/wirings/middleware.ts
Normal file
48
packages/functions/src/wirings/middleware.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { addHTTPMiddleware } from "#pikku";
|
||||
import { authBearer, cors } from "@pikku/core/middleware";
|
||||
import { betterAuthStatelessSession } from "@pikku/better-auth";
|
||||
import { sessionCookieMiddleware } from "../middleware/session-cookie.js";
|
||||
|
||||
// sessionCookieMiddleware runs first so that booking-form clients
|
||||
// (sh_session cookie) are resolved before better-auth is checked.
|
||||
addHTTPMiddleware("*", [
|
||||
cors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
headers: ["Content-Type", "Authorization", "x-api-key", "X-Auth-Return-Redirect"],
|
||||
}),
|
||||
sessionCookieMiddleware,
|
||||
// Stateless (cookie-cache) session so the CLI detects our own registration and
|
||||
// does NOT also generate a bare betterAuthStatelessSession() (no mapSession),
|
||||
// which would strip the session to { userId } and 403 every admin RPC.
|
||||
betterAuthStatelessSession({
|
||||
mapSession: (result: any) => {
|
||||
// A logged-in user must always carry a role (Better Auth defaults it to
|
||||
// 'client'). A missing role means the auth wiring/schema is stale — fail
|
||||
// loudly instead of silently downgrading to 'client', which would show the
|
||||
// real role in the header (DB-read) while every admin RPC 403s.
|
||||
const role = result.user.role;
|
||||
if (!role) {
|
||||
throw new Error(
|
||||
`Session for user ${result.user.id} has no role — auth wiring/schema is out of date`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
userId: result.user.id,
|
||||
role,
|
||||
locale: result.user.locale ?? "de",
|
||||
};
|
||||
},
|
||||
}),
|
||||
// External consoles (sandbox/woven-build view) can't carry the session cookie,
|
||||
// so they authenticate with `Authorization: Bearer <PIKKU_CONSOLE_TOKEN>`. The
|
||||
// CLI normally emits this alongside its session middleware, but because we
|
||||
// register our own global session middleware above it steps aside entirely —
|
||||
// so we must add the console-token bridge here or console:* RPCs 403.
|
||||
authBearer({
|
||||
token: {
|
||||
secretId: "PIKKU_CONSOLE_TOKEN",
|
||||
userSession: { userId: "pikku-console-token" },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
15
packages/functions/src/wirings/public.http.ts
Normal file
15
packages/functions/src/wirings/public.http.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineHTTPRoutes, wireHTTPRoutes } from '#pikku'
|
||||
import { getAvailability } from '../functions/get-availability.function.js'
|
||||
import { getEventsListing } from '../functions/get-events-listing.function.js'
|
||||
import { submitEnquiry } from '../functions/submit-enquiry.function.js'
|
||||
|
||||
export const publicRoutes = defineHTTPRoutes({
|
||||
auth: false,
|
||||
routes: {
|
||||
getAvailability: { method: 'get', route: '/public/availability', func: getAvailability },
|
||||
getEventsListing: { method: 'get', route: '/public/events', func: getEventsListing },
|
||||
submitEnquiry: { method: 'post', route: '/public/enquiry', func: submitEnquiry },
|
||||
},
|
||||
})
|
||||
|
||||
wireHTTPRoutes({ routes: { public: publicRoutes } })
|
||||
11
packages/functions/src/wirings/scheduled.wiring.ts
Normal file
11
packages/functions/src/wirings/scheduled.wiring.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { wireScheduler } from '#pikku'
|
||||
import { bookingLifecycleDaily } from '../functions/booking-lifecycle.function.js'
|
||||
|
||||
// Daily at 06:00. The job's date math is Europe/Berlin regardless of the
|
||||
// platform's clock, so the exact server-local fire time only sets cadence.
|
||||
wireScheduler({
|
||||
name: 'booking-lifecycle-daily',
|
||||
schedule: '0 6 * * *',
|
||||
func: bookingLifecycleDaily,
|
||||
tags: ['daily', 'booking-lifecycle'],
|
||||
})
|
||||
Reference in New Issue
Block a user