chore: seminarhof customer project

This commit is contained in:
e2e
2026-07-11 08:11:01 +02:00
commit f0853ebe73
188 changed files with 20784 additions and 0 deletions

View 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 }),
],
})
})

View 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" },
},
}),
]);

View 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 } })

View 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'],
})