@eventmodelers/cli 0.0.1
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.
- package/README.md +175 -0
- package/cli.js +676 -0
- package/package.json +35 -0
- package/shared/build-kit/code-export.mjs +560 -0
- package/shared/build-kit/lib/ollama-agent.js +147 -0
- package/shared/build-kit/package.json +11 -0
- package/shared/build-kit/ralph-ollama.js +39 -0
- package/shared/build-kit/realtime-agent.js +18 -0
- package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +407 -0
- package/stacks/axon/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +114 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/SKILL.md +496 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/axon-workflow-api.md +279 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/feature-flag-patterns.md +114 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +396 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/axon-test-fixture-patterns.md +162 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +56 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +115 -0
- package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +282 -0
- package/stacks/axon/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/axon/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +611 -0
- package/stacks/axon/templates/.claude/skills/load-slice/SKILL.md +141 -0
- package/stacks/axon/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
- package/stacks/axon/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/axon/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +158 -0
- package/stacks/axon/templates/build-kit/lib/prompt.md +126 -0
- package/stacks/axon/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/axon/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/axon/templates/build-kit/ralph.sh +98 -0
- package/stacks/axon/templates/root/.env.example +5 -0
- package/stacks/axon/templates/root/CLAUDE.md +60 -0
- package/stacks/axon/templates/root/README.md +44 -0
- package/stacks/axon/templates/root/docker-compose.yml +64 -0
- package/stacks/axon/templates/root/mvnw +259 -0
- package/stacks/axon/templates/root/mvnw.cmd +149 -0
- package/stacks/axon/templates/root/pom.xml +125 -0
- package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/QuickstartApplication.java +128 -0
- package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/config/WebConfig.java +155 -0
- package/stacks/axon/templates/root/src/main/resources/application.properties +2 -0
- package/stacks/axon/templates/root/src/main/resources/static/bg-body.png +0 -0
- package/stacks/axon/templates/root/src/main/resources/static/bg-header.png +0 -0
- package/stacks/axon/templates/root/src/main/resources/static/index.html +596 -0
- package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +251 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +99 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +115 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +163 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +234 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +120 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +166 -0
- package/stacks/cratis-csharp/templates/.claude/skills/connect/SKILL.md +169 -0
- package/stacks/cratis-csharp/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
- package/stacks/cratis-csharp/templates/.claude/skills/load-slice/SKILL.md +141 -0
- package/stacks/cratis-csharp/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +58 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +129 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +124 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/ralph.js +302 -0
- package/stacks/cratis-csharp/templates/build-kit/ralph-claude.js +37 -0
- package/stacks/cratis-csharp/templates/build-kit/ralph.sh +98 -0
- package/stacks/cratis-csharp/templates/root/.frontend/index.css +29 -0
- package/stacks/cratis-csharp/templates/root/.frontend/index.html +17 -0
- package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +18 -0
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +42 -0
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +11 -0
- package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +56 -0
- package/stacks/cratis-csharp/templates/root/App.tsx +23 -0
- package/stacks/cratis-csharp/templates/root/CLAUDE.md +62 -0
- package/stacks/cratis-csharp/templates/root/CratisApp.csproj +25 -0
- package/stacks/cratis-csharp/templates/root/CratisApp.sln +18 -0
- package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +3 -0
- package/stacks/cratis-csharp/templates/root/Home.tsx +102 -0
- package/stacks/cratis-csharp/templates/root/Program.cs +26 -0
- package/stacks/cratis-csharp/templates/root/README.md +192 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +47 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +11 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +12 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +17 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +51 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +18 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +27 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +22 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +3 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/appsettings.Development.json +9 -0
- package/stacks/cratis-csharp/templates/root/appsettings.json +26 -0
- package/stacks/cratis-csharp/templates/root/docker-compose.yml +23 -0
- package/stacks/cratis-csharp/templates/root/package.json +33 -0
- package/stacks/cratis-csharp/templates/root/tsconfig.json +3 -0
- package/stacks/modeling-kit/templates/.claude/settings.local.json +13 -0
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +215 -0
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +176 -0
- package/stacks/modeling-kit/templates/.claude/skills/connect/SKILL.md +182 -0
- package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +383 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-applying-conways-law/SKILL.md +465 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +587 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/facilitating-event-modeling-workshops.md +626 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +542 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-event-models/SKILL.md +497 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +783 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +604 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +699 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-integrating-legacy-systems/SKILL.md +674 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/SKILL.md +231 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/domain-patterns.md +150 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/patterns.md +329 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/snapshotting.md +204 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +352 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/project-planning-with-event-modeling.md +341 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +153 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +136 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +78 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/patterns.md +85 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +655 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-translating-external-events/SKILL.md +479 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +234 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +353 -0
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +200 -0
- package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +94 -0
- package/stacks/modeling-kit/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +441 -0
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +290 -0
- package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +212 -0
- package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +123 -0
- package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +353 -0
- package/stacks/modeling-kit/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +134 -0
- package/stacks/modeling-kit/templates/kit/lib/agent.sh +20 -0
- package/stacks/modeling-kit/templates/kit/lib/ollama-agent.js +147 -0
- package/stacks/modeling-kit/templates/kit/lib/ralph.js +286 -0
- package/stacks/modeling-kit/templates/kit/package.json +12 -0
- package/stacks/modeling-kit/templates/kit/ralph-claude.js +50 -0
- package/stacks/modeling-kit/templates/kit/ralph-ollama.js +38 -0
- package/stacks/modeling-kit/templates/kit/ralph.sh +61 -0
- package/stacks/modeling-kit/templates/kit/realtime-agent.js +18 -0
- package/stacks/modeling-kit/templates/root/CLAUDE.md +56 -0
- package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +266 -0
- package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +344 -0
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +412 -0
- package/stacks/node/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/node/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
- package/stacks/node/templates/.claude/skills/load-slice/SKILL.md +143 -0
- package/stacks/node/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/node/templates/build-kit/README.md +86 -0
- package/stacks/node/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/node/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +152 -0
- package/stacks/node/templates/build-kit/lib/prompt.md +122 -0
- package/stacks/node/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/node/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/node/templates/build-kit/ralph.sh +98 -0
- package/stacks/node/templates/root/.env.example +15 -0
- package/stacks/node/templates/root/CLAUDE.md +60 -0
- package/stacks/node/templates/root/docker-compose.yml +15 -0
- package/stacks/node/templates/root/flyway.conf +17 -0
- package/stacks/node/templates/root/migrations/V1__schema.sql.example +12 -0
- package/stacks/node/templates/root/package.json +50 -0
- package/stacks/node/templates/root/server.ts +130 -0
- package/stacks/node/templates/root/setup-env.sh +53 -0
- package/stacks/node/templates/root/src/common/assertions.ts +6 -0
- package/stacks/node/templates/root/src/common/db.ts +32 -0
- package/stacks/node/templates/root/src/common/loadPostgresEventstore.ts +23 -0
- package/stacks/node/templates/root/src/common/parseEndpoint.ts +51 -0
- package/stacks/node/templates/root/src/common/processorDlq.ts +28 -0
- package/stacks/node/templates/root/src/common/realtimeBroadcast.ts +12 -0
- package/stacks/node/templates/root/src/common/replay.ts +16 -0
- package/stacks/node/templates/root/src/common/routes.ts +19 -0
- package/stacks/node/templates/root/src/common/testHelpers.ts +44 -0
- package/stacks/node/templates/root/src/swagger.ts +34 -0
- package/stacks/node/templates/root/src/util/assertions.ts +6 -0
- package/stacks/node/templates/root/src/util/hash.ts +9 -0
- package/stacks/node/templates/root/src/util/sanitize.ts +23 -0
- package/stacks/node/templates/root/tsconfig.json +32 -0
- package/stacks/node/templates/root/vercel.json +8 -0
- package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +266 -0
- package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +344 -0
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +390 -0
- package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +303 -0
- package/stacks/supabase/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/supabase/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
- package/stacks/supabase/templates/.claude/skills/load-slice/SKILL.md +143 -0
- package/stacks/supabase/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/supabase/templates/build-kit/README.md +86 -0
- package/stacks/supabase/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/supabase/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +152 -0
- package/stacks/supabase/templates/build-kit/lib/prompt.md +123 -0
- package/stacks/supabase/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/supabase/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/supabase/templates/build-kit/ralph.sh +98 -0
- package/stacks/supabase/templates/root/.env.example +22 -0
- package/stacks/supabase/templates/root/CLAUDE.md +61 -0
- package/stacks/supabase/templates/root/flyway.conf +17 -0
- package/stacks/supabase/templates/root/package.json +52 -0
- package/stacks/supabase/templates/root/server.ts +177 -0
- package/stacks/supabase/templates/root/setup-env.sh +53 -0
- package/stacks/supabase/templates/root/src/common/assertions.ts +6 -0
- package/stacks/supabase/templates/root/src/common/db.ts +32 -0
- package/stacks/supabase/templates/root/src/common/loadPostgresEventstore.ts +23 -0
- package/stacks/supabase/templates/root/src/common/parseEndpoint.ts +51 -0
- package/stacks/supabase/templates/root/src/common/processorDlq.ts +28 -0
- package/stacks/supabase/templates/root/src/common/realtimeBroadcast.ts +19 -0
- package/stacks/supabase/templates/root/src/common/replay.ts +16 -0
- package/stacks/supabase/templates/root/src/common/routes.ts +23 -0
- package/stacks/supabase/templates/root/src/common/testHelpers.ts +54 -0
- package/stacks/supabase/templates/root/src/supabase/README.md +141 -0
- package/stacks/supabase/templates/root/src/supabase/api.ts +56 -0
- package/stacks/supabase/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
- package/stacks/supabase/templates/root/src/supabase/requireSysUser.ts +40 -0
- package/stacks/supabase/templates/root/src/supabase/requireUser.ts +72 -0
- package/stacks/supabase/templates/root/src/swagger.ts +34 -0
- package/stacks/supabase/templates/root/src/util/assertions.ts +6 -0
- package/stacks/supabase/templates/root/src/util/hash.ts +9 -0
- package/stacks/supabase/templates/root/src/util/sanitize.ts +23 -0
- package/stacks/supabase/templates/root/supabase/config.toml +295 -0
- package/stacks/supabase/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
- package/stacks/supabase/templates/root/supabase/seed.sql +1 -0
- package/stacks/supabase/templates/root/tsconfig.json +32 -0
- package/stacks/supabase/templates/root/vercel.json +8 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {join} from 'path';
|
|
2
|
+
import {getApplication, startAPI, WebApiSetup} from '@event-driven-io/emmett-expressjs';
|
|
3
|
+
import {glob} from "glob";
|
|
4
|
+
import express, {Application, Request, Response} from 'express';
|
|
5
|
+
import {jsonBigIntReplacer} from './src/util/sanitize';
|
|
6
|
+
import {requireUser} from "./src/supabase/requireUser";
|
|
7
|
+
import {getKnexInstance, closeDb} from "./src/common/db";
|
|
8
|
+
import swaggerUi from 'swagger-ui-express'
|
|
9
|
+
import {specs} from './src/swagger';
|
|
10
|
+
import cors from 'cors';
|
|
11
|
+
import {findEventstore} from "./src/common/loadPostgresEventstore";
|
|
12
|
+
import {PostgresEventStore} from "@event-driven-io/emmett-postgresql";
|
|
13
|
+
|
|
14
|
+
async function startServer() {
|
|
15
|
+
|
|
16
|
+
const eventStore = await findEventstore()
|
|
17
|
+
const slicesBase = join(__dirname, 'dist/src/slices');
|
|
18
|
+
const routesPattern = join(slicesBase, '**/routes{,-*}.js');
|
|
19
|
+
|
|
20
|
+
const routeFiles = await glob(routesPattern, {nodir: true});
|
|
21
|
+
console.log('Found route files:', routeFiles);
|
|
22
|
+
|
|
23
|
+
const processorPattern = join(slicesBase, '**/processor{,-*}.js');
|
|
24
|
+
const processorFiles = await glob(processorPattern, {nodir: true});
|
|
25
|
+
console.log('Found processor files:', processorFiles);
|
|
26
|
+
|
|
27
|
+
// Common routes (e.g. projection replay) are privileged/operational and are
|
|
28
|
+
// only mounted when explicitly enabled via env.
|
|
29
|
+
const commonRoutesEnabled = process.env.COMMON_ROUTES_ENABLED === 'true';
|
|
30
|
+
const commonPattern = join(__dirname, 'src/common/routes{,-*}.@(ts|js)');
|
|
31
|
+
const commonRouteFiles = commonRoutesEnabled
|
|
32
|
+
? await glob(commonPattern, {nodir: true})
|
|
33
|
+
: [];
|
|
34
|
+
console.log(commonRoutesEnabled
|
|
35
|
+
? `Found common route files: ${commonRouteFiles}`
|
|
36
|
+
: 'Common routes disabled (set COMMON_ROUTES_ENABLED=true to enable)');
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
const rootApp: Application = express();
|
|
40
|
+
rootApp.set('json replacer', jsonBigIntReplacer);
|
|
41
|
+
|
|
42
|
+
const corsOrigins = process.env.CORS_ORIGINS?.split(',').map(o => o.trim()) ?? ['http://localhost:3000', 'http://localhost:3001'];
|
|
43
|
+
rootApp.use(cors({
|
|
44
|
+
origin: corsOrigins,
|
|
45
|
+
credentials: true,
|
|
46
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
47
|
+
allowedHeaders: ['Content-Type', 'Content-Encoding', 'accept-encoding', 'Authorization','x-user-id','x-causation-id','x-correlation-id']
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const webApis: WebApiSetup[] = [];
|
|
51
|
+
|
|
52
|
+
for (const file of routeFiles.concat(commonRouteFiles)) {
|
|
53
|
+
const webApiModule: { api: () => WebApiSetup } = await import(file);
|
|
54
|
+
if (typeof webApiModule.api == 'function') {
|
|
55
|
+
var module = webApiModule.api()
|
|
56
|
+
webApis.push(module);
|
|
57
|
+
} else {
|
|
58
|
+
console.error(`Expected api function to be defined in ${file}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const startedProcessors: Array<{ stop: () => Promise<void> }> = [];
|
|
63
|
+
|
|
64
|
+
for (const processorFile of processorFiles) {
|
|
65
|
+
const processor: { processor: { start: (eventStore: PostgresEventStore) => Promise<void>; stop: () => Promise<void> } } = await import(processorFile);
|
|
66
|
+
if (typeof processor.processor.start == "function") {
|
|
67
|
+
console.log(`starting processor ${processorFile}`)
|
|
68
|
+
processor.processor.start(eventStore).catch(err => console.error(`Processor ${processorFile} failed:`, err));
|
|
69
|
+
startedProcessors.push(processor.processor);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const shutdown = async (signal: string) => {
|
|
74
|
+
console.log(`${signal} received, shutting down processors...`);
|
|
75
|
+
await Promise.allSettled(startedProcessors.map(p => p.stop()));
|
|
76
|
+
await eventStore.close();
|
|
77
|
+
await closeDb();
|
|
78
|
+
console.log('shutdown complete');
|
|
79
|
+
process.exit(0);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
83
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
84
|
+
|
|
85
|
+
// Get the main application from emmett
|
|
86
|
+
const childApp: Application = getApplication({
|
|
87
|
+
apis: webApis,
|
|
88
|
+
disableJsonMiddleware: false,
|
|
89
|
+
enableDefaultExpressEtag: true,
|
|
90
|
+
});
|
|
91
|
+
childApp.set('json replacer', jsonBigIntReplacer);
|
|
92
|
+
|
|
93
|
+
// Protected user info endpoint - requires JWT token in Authorization header
|
|
94
|
+
childApp.get('/api/user', async (req: Request, res: Response) => {
|
|
95
|
+
console.log('API user route hit'); // Debug log
|
|
96
|
+
try {
|
|
97
|
+
const result = await requireUser(req, res, false)
|
|
98
|
+
if (result.error) {
|
|
99
|
+
// Response already sent by requireUser if sendUnauthorized=true
|
|
100
|
+
if (!res.headersSent) {
|
|
101
|
+
res.status(401).json({error: result.error})
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
res.status(200).json({
|
|
105
|
+
user_id: result.user.id,
|
|
106
|
+
email: result.user.email,
|
|
107
|
+
metadata: result.user.user_metadata
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Error in /api/user:', error);
|
|
112
|
+
if (!res.headersSent) {
|
|
113
|
+
res.status(500).json({error: 'Internal server error'});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Swagger UI endpoints
|
|
119
|
+
childApp.use('/api-docs', swaggerUi.serve);
|
|
120
|
+
childApp.get('/api-docs', swaggerUi.setup(specs, {
|
|
121
|
+
swaggerOptions: {
|
|
122
|
+
urls: [
|
|
123
|
+
{
|
|
124
|
+
url: '/swagger.json',
|
|
125
|
+
name: 'JSON',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
// OpenAPI spec endpoint
|
|
132
|
+
childApp.get('/swagger.json', (req: Request, res: Response) => {
|
|
133
|
+
res.setHeader('Content-Type', 'application/json');
|
|
134
|
+
res.send(specs);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
138
|
+
console.log(`> Ready on port ${port}`);
|
|
139
|
+
|
|
140
|
+
rootApp.use((req: Request, _res: Response, next) => {
|
|
141
|
+
console.log(`[${req.method}] ${req.path}`);
|
|
142
|
+
next();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
rootApp.use(express.json());
|
|
146
|
+
|
|
147
|
+
// Closed-by-default auth gate: every route requires a valid Supabase JWT
|
|
148
|
+
// except the explicitly public paths below. This guarantees new slice
|
|
149
|
+
// routes are protected even if a handler forgets to call requireUser.
|
|
150
|
+
const PUBLIC_PATHS = ['/api-docs', '/swagger.json', '/health'];
|
|
151
|
+
const isPublicPath = (p: string): boolean =>
|
|
152
|
+
PUBLIC_PATHS.some(pub => p === pub || p.startsWith(pub + '/'));
|
|
153
|
+
|
|
154
|
+
rootApp.use(async (req: Request, res: Response, next) => {
|
|
155
|
+
if (req.method === 'OPTIONS') return next(); // CORS preflight
|
|
156
|
+
if (isPublicPath(req.path)) return next(); // docs / health
|
|
157
|
+
const {error} = await requireUser(req, res); // sends 401 on failure
|
|
158
|
+
if (error) return; // response already sent
|
|
159
|
+
next();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
rootApp.use(childApp)
|
|
163
|
+
// Start the main application
|
|
164
|
+
startAPI(rootApp, {port: port});
|
|
165
|
+
|
|
166
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
167
|
+
console.error('⛔ Unhandled Rejection:', reason);
|
|
168
|
+
if (reason instanceof Error && reason.stack) {
|
|
169
|
+
console.error('Stack trace:\n', reason.stack);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
startServer().catch(error => {
|
|
175
|
+
console.error('Failed to start server:', error);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
prompt() {
|
|
5
|
+
local var_name="$1"
|
|
6
|
+
local prompt_text="$2"
|
|
7
|
+
local value
|
|
8
|
+
read -rp "$prompt_text: " value
|
|
9
|
+
if [[ -z "$value" ]]; then
|
|
10
|
+
echo "Error: $var_name cannot be empty." >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
echo "$value"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
prompt_secret() {
|
|
17
|
+
local var_name="$1"
|
|
18
|
+
local prompt_text="$2"
|
|
19
|
+
local value
|
|
20
|
+
read -rsp "$prompt_text: " value
|
|
21
|
+
echo "" >&2
|
|
22
|
+
if [[ -z "$value" ]]; then
|
|
23
|
+
echo "Error: $var_name cannot be empty." >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
echo "$value"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
echo "=== Supabase .env setup ==="
|
|
30
|
+
echo ""
|
|
31
|
+
|
|
32
|
+
PROJECT_ID=$(prompt SUPABASE_PROJECT_ID "Supabase Project ID")
|
|
33
|
+
DB_PASSWORD=$(prompt_secret SUPABASE_DB_PASSWORD "Database Password")
|
|
34
|
+
PUBLISHABLE_KEY=$(prompt_secret SUPABASE_PUBLISHABLE_KEY "Supabase Publishable Key")
|
|
35
|
+
SECRET_KEY=$(prompt_secret SUPABASE_SECRET_KEY "Supabase Secret Key")
|
|
36
|
+
BACKEND_URL=$(prompt BACKEND_URL "Backend URL (e.g. https://api.eventmodelers.ai)")
|
|
37
|
+
|
|
38
|
+
cat > .env <<EOF
|
|
39
|
+
SUPABASE_URL=https://${PROJECT_ID}.supabase.co
|
|
40
|
+
SUPABASE_PUBLISHABLE_KEY=${PUBLISHABLE_KEY}
|
|
41
|
+
SUPABASE_DB_URL=postgresql://postgres.${PROJECT_ID}:${DB_PASSWORD}@aws-1-eu-central-1.pooler.supabase.com:5432/postgres?prepareThreshold=0
|
|
42
|
+
SUPABASE_SECRET_KEY=${SECRET_KEY}
|
|
43
|
+
|
|
44
|
+
BACKEND_URL=${BACKEND_URL}
|
|
45
|
+
|
|
46
|
+
# Flyway configuration
|
|
47
|
+
FLYWAY_URL=jdbc:postgresql://aws-1-eu-west-1.pooler.supabase.com:6543/postgres?user=postgres.${PROJECT_ID}&password=${DB_PASSWORD}
|
|
48
|
+
FLYWAY_USER=postgres.${PROJECT_ID}
|
|
49
|
+
FLYWAY_PASSWORD=${DB_PASSWORD}
|
|
50
|
+
EOF
|
|
51
|
+
|
|
52
|
+
echo ""
|
|
53
|
+
echo ".env created successfully."
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import knex, {Knex} from "knex";
|
|
2
|
+
import pg from "pg";
|
|
3
|
+
|
|
4
|
+
export const postgresUrl = process.env.SUPABASE_DB_URL ?? "missing-url"
|
|
5
|
+
|
|
6
|
+
let knexInstance: Knex | null = null;
|
|
7
|
+
let sharedPool: pg.Pool | null = null;
|
|
8
|
+
|
|
9
|
+
export const getKnexInstance = (): Knex => {
|
|
10
|
+
if (!knexInstance) {
|
|
11
|
+
knexInstance = knex({
|
|
12
|
+
client: 'pg',
|
|
13
|
+
connection: postgresUrl,
|
|
14
|
+
pool: { min: 0, max: 5 },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return knexInstance;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getSharedPool = (): pg.Pool => {
|
|
21
|
+
if (!sharedPool) {
|
|
22
|
+
sharedPool = new pg.Pool({ connectionString: postgresUrl, max: 5 });
|
|
23
|
+
}
|
|
24
|
+
return sharedPool;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const closeDb = async (): Promise<void> => {
|
|
28
|
+
await knexInstance?.destroy();
|
|
29
|
+
await sharedPool?.end();
|
|
30
|
+
knexInstance = null;
|
|
31
|
+
sharedPool = null;
|
|
32
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {getPostgreSQLEventStore} from "@event-driven-io/emmett-postgresql";
|
|
2
|
+
import {projections} from "@event-driven-io/emmett";
|
|
3
|
+
import {postgresUrl, getSharedPool} from "./db";
|
|
4
|
+
|
|
5
|
+
let eventStoreInstance: ReturnType<typeof getPostgreSQLEventStore> | null = null;
|
|
6
|
+
|
|
7
|
+
export const findEventstore = async () => {
|
|
8
|
+
if (!eventStoreInstance) {
|
|
9
|
+
eventStoreInstance = getPostgreSQLEventStore(postgresUrl, {
|
|
10
|
+
schema: {
|
|
11
|
+
autoMigration: "CreateOrUpdate"
|
|
12
|
+
},
|
|
13
|
+
connectionOptions: {
|
|
14
|
+
pooled: true,
|
|
15
|
+
pool: getSharedPool(),
|
|
16
|
+
},
|
|
17
|
+
projections: projections.inline([
|
|
18
|
+
]),
|
|
19
|
+
});
|
|
20
|
+
await eventStoreInstance.schema.migrate();
|
|
21
|
+
}
|
|
22
|
+
return eventStoreInstance;
|
|
23
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Nebulit GmbH
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const serviceURI = "http://localhost:3000"
|
|
7
|
+
|
|
8
|
+
export function parseEndpoint(endpoint: string, data?: any) {
|
|
9
|
+
var parsedEndpoint = endpoint?.startsWith("/") ? endpoint.substring(1) : endpoint
|
|
10
|
+
return serviceURI + "/" + lowercaseFirstCharacter(parsedEndpoint).replace(/{(\w+)}/g, (match, param) => {
|
|
11
|
+
return param && data && data[param] !== undefined ? data[param] : match;
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export function parseQueryEndpoint(
|
|
17
|
+
endpoint: string,
|
|
18
|
+
queries?: Record<string, string>
|
|
19
|
+
) {
|
|
20
|
+
const parsedEndpoint = endpoint.startsWith("/")
|
|
21
|
+
? endpoint.substring(1)
|
|
22
|
+
: endpoint;
|
|
23
|
+
|
|
24
|
+
const basePath =
|
|
25
|
+
serviceURI + "/api/query/" + parsedEndpoint;
|
|
26
|
+
|
|
27
|
+
const queryString = queries
|
|
28
|
+
? "?" + new URLSearchParams(filterEmptyEntries(queries)).toString()
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
return basePath + queryString;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function filterEmptyEntries(queries?: Record<string, string>): Record<string, string> {
|
|
35
|
+
if (!queries) return {};
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
Object.entries(queries).filter(([key, value]) => value !== "")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
function lowercaseFirstCharacter(inputString: string) {
|
|
43
|
+
// Check if the string is not empty
|
|
44
|
+
if (inputString?.length > 0) {
|
|
45
|
+
// Capitalize the first character and concatenate the rest of the string
|
|
46
|
+
return inputString.charAt(0).toLowerCase() + inputString.substring(1);
|
|
47
|
+
} else {
|
|
48
|
+
// Return an empty string if the input is empty
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {getKnexInstance} from './db';
|
|
2
|
+
import type {AnyRecordedMessageMetadata, RecordedMessage} from '@event-driven-io/emmett';
|
|
3
|
+
|
|
4
|
+
export const storeDlqMessage = async (
|
|
5
|
+
processorId: string,
|
|
6
|
+
message: RecordedMessage<any, AnyRecordedMessageMetadata>,
|
|
7
|
+
error: unknown,
|
|
8
|
+
): Promise<void> => {
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
console.log(`Processing DLQ ${JSON.stringify({ type: message.type, data: message.data, metadata: message.metadata } , (key, value) =>
|
|
12
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
13
|
+
)}`)
|
|
14
|
+
await getKnexInstance()('processor_dlq').insert({
|
|
15
|
+
processor_id: processorId,
|
|
16
|
+
stream_id: message.metadata.streamName,
|
|
17
|
+
event: JSON.parse(
|
|
18
|
+
JSON.stringify({ type: message.type, data: message.data, metadata: message.metadata } , (key, value) =>
|
|
19
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
20
|
+
)
|
|
21
|
+
),
|
|
22
|
+
error: error instanceof Error ? error.message : String(error),
|
|
23
|
+
});
|
|
24
|
+
} catch (dlqError) {
|
|
25
|
+
console.error('Failed to write to processor_dlq:', dlqError);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const url = `${process.env.SUPABASE_URL}/realtime/v1/api/broadcast`;
|
|
2
|
+
const key = process.env.SUPABASE_SECRET_KEY!;
|
|
3
|
+
|
|
4
|
+
export async function broadcastRealtime(topic: string, event: string, payload: unknown, privateChannel = true): Promise<void> {
|
|
5
|
+
const res = await fetch(url, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
'Authorization': `Bearer ${key}`,
|
|
10
|
+
'apikey': key,
|
|
11
|
+
},
|
|
12
|
+
body: JSON.stringify({
|
|
13
|
+
messages: [{ topic, event, payload, private: privateChannel }],
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
throw new Error(`realtime broadcast failed: ${res.status} ${await res.text()}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {PostgreSQLProjectionDefinition, rebuildPostgreSQLProjections} from "@event-driven-io/emmett-postgresql";
|
|
2
|
+
import {postgresUrl} from "./db";
|
|
3
|
+
import {glob} from "glob";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const slicesRoot = path.resolve(__dirname, '../slices');
|
|
7
|
+
|
|
8
|
+
export const replayProjection = async (projectionName: string): Promise<void> => {
|
|
9
|
+
const [filePath] = await glob(`**/${projectionName}.{ts|js}`, {cwd: slicesRoot, absolute: true});
|
|
10
|
+
if (!filePath) throw new Error(`Projection not found: ${projectionName}`);
|
|
11
|
+
|
|
12
|
+
const projectionImport = await import(filePath);
|
|
13
|
+
const projection: PostgreSQLProjectionDefinition = projectionImport[projectionName];
|
|
14
|
+
|
|
15
|
+
return rebuildPostgreSQLProjections({projection, connectionString: postgresUrl}).start();
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Request, Response, Router} from 'express';
|
|
2
|
+
import {WebApiSetup} from "@event-driven-io/emmett-expressjs";
|
|
3
|
+
import {assertNotEmpty} from "../util/assertions";
|
|
4
|
+
import {replayProjection} from "./replay";
|
|
5
|
+
import {requireSysUser} from "../supabase/requireSysUser";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export const api =
|
|
9
|
+
(
|
|
10
|
+
// external dependencies
|
|
11
|
+
): WebApiSetup =>
|
|
12
|
+
(router: Router): void => {
|
|
13
|
+
|
|
14
|
+
router.post('/api/replay/:projection', async (req: Request, res: Response) => {
|
|
15
|
+
// Replay is a privileged operational action — require a sys user.
|
|
16
|
+
const {error} = await requireSysUser(req, res); // sends 401/403 on failure
|
|
17
|
+
if (error) return;
|
|
18
|
+
const projection = assertNotEmpty(req.params.projection)
|
|
19
|
+
await replayProjection(projection)
|
|
20
|
+
res.status(200).json({"projection":projection})
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {execSync} from 'child_process';
|
|
2
|
+
import {readFileSync, writeFileSync, unlinkSync} from 'fs';
|
|
3
|
+
import {tmpdir} from 'os';
|
|
4
|
+
import {join} from 'path';
|
|
5
|
+
import knex from 'knex';
|
|
6
|
+
|
|
7
|
+
export async function runFlywayMigrations(connectionString: string): Promise<void> {
|
|
8
|
+
const stubsPath = join(process.cwd(), 'supabase', 'migrations', '_V0__supabase_stubs.sql');
|
|
9
|
+
const stubsSql = readFileSync(stubsPath, 'utf8');
|
|
10
|
+
const db = knex({client: 'pg', connection: connectionString});
|
|
11
|
+
try {
|
|
12
|
+
await db.raw(stubsSql);
|
|
13
|
+
} finally {
|
|
14
|
+
await db.destroy();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const url = new URL(connectionString);
|
|
18
|
+
const jdbcUrl = `jdbc:postgresql://${url.hostname}:${url.port || 5432}${url.pathname}`;
|
|
19
|
+
const user = url.username;
|
|
20
|
+
const password = url.password;
|
|
21
|
+
|
|
22
|
+
const tempConfigPath = join(tmpdir(), `flyway-test-${Date.now()}.conf`);
|
|
23
|
+
const migrationsPath = join(process.cwd(), 'supabase', 'migrations');
|
|
24
|
+
|
|
25
|
+
const config = `
|
|
26
|
+
flyway.url=${jdbcUrl}
|
|
27
|
+
flyway.user=${user}
|
|
28
|
+
flyway.password=${password}
|
|
29
|
+
flyway.locations=filesystem:${migrationsPath}
|
|
30
|
+
flyway.schemas=public
|
|
31
|
+
flyway.placeholderReplacement=false
|
|
32
|
+
flyway.validateOnMigrate=true
|
|
33
|
+
flyway.cleanDisabled=false
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
writeFileSync(tempConfigPath, config, 'utf8');
|
|
38
|
+
execSync(`flyway -configFiles=${tempConfigPath} migrate`, {
|
|
39
|
+
stdio: 'pipe',
|
|
40
|
+
encoding: 'utf8'
|
|
41
|
+
});
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
console.error('Flyway migration failed:', error.message);
|
|
44
|
+
if (error.stdout) console.error('STDOUT:', error.stdout);
|
|
45
|
+
if (error.stderr) console.error('STDERR:', error.stderr);
|
|
46
|
+
throw new Error(`Flyway migration failed: ${error.message}`);
|
|
47
|
+
} finally {
|
|
48
|
+
try {
|
|
49
|
+
unlinkSync(tempConfigPath);
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Supabase JWT Authentication for Backend API
|
|
2
|
+
|
|
3
|
+
This backend API uses Supabase JWT tokens for authentication. Clients must include a valid JWT token in the
|
|
4
|
+
`Authorization` header.
|
|
5
|
+
|
|
6
|
+
## Quick Start
|
|
7
|
+
|
|
8
|
+
1. **Set up environment variables** in `.env`:
|
|
9
|
+
```env
|
|
10
|
+
SUPABASE_URL=http://127.0.0.1:54321
|
|
11
|
+
SUPABASE_PUBLISHABLE_KEY=your-anon-key-here
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
2. **Start the server**: `npm run dev`
|
|
15
|
+
|
|
16
|
+
3. **Get a test JWT token**: sign in a user via the Supabase client SDK or `supabase auth` CLI and use the returned `access_token` in your API requests.
|
|
17
|
+
|
|
18
|
+
## How It Works
|
|
19
|
+
|
|
20
|
+
1. **Client obtains JWT token** from Supabase (via your frontend app)
|
|
21
|
+
2. **Client sends requests** with `Authorization: Bearer <jwt-token>` header
|
|
22
|
+
3. **Backend verifies JWT** using Supabase and extracts user info
|
|
23
|
+
4. **Protected routes** return user data or 401 Unauthorized
|
|
24
|
+
|
|
25
|
+
## Usage Examples
|
|
26
|
+
|
|
27
|
+
### Option 1: Using `requireUser` function
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import {requireUser} from './src/supabase/requireUser';
|
|
31
|
+
import {Request, Response} from 'express';
|
|
32
|
+
|
|
33
|
+
app.get('/api/protected', async (req: Request, res: Response) => {
|
|
34
|
+
const result = await requireUser(req, res, false);
|
|
35
|
+
|
|
36
|
+
if (result.error) {
|
|
37
|
+
return res.status(401).json({error: result.error});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const user = result.user;
|
|
41
|
+
res.json({
|
|
42
|
+
message: 'Protected data',
|
|
43
|
+
userId: user.id,
|
|
44
|
+
email: user.email
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Testing with curl
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Get JWT token from your Supabase client first
|
|
53
|
+
TOKEN="your-jwt-token-here"
|
|
54
|
+
|
|
55
|
+
# Test protected endpoint
|
|
56
|
+
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/user
|
|
57
|
+
|
|
58
|
+
# Expected response:
|
|
59
|
+
# {
|
|
60
|
+
# "userId": "...",
|
|
61
|
+
# "email": "user@example.com",
|
|
62
|
+
# "metadata": { ... }
|
|
63
|
+
# }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Testing with JavaScript fetch
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
const token = supabase.auth.session()?.access_token;
|
|
70
|
+
|
|
71
|
+
fetch('http://localhost:3000/api/user', {
|
|
72
|
+
headers: {
|
|
73
|
+
'Authorization': `Bearer ${token}`
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.then(res => res.json())
|
|
77
|
+
.then(data => console.log(data));
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Endpoints
|
|
81
|
+
|
|
82
|
+
### `GET /api/user`
|
|
83
|
+
|
|
84
|
+
Returns current authenticated user information.
|
|
85
|
+
|
|
86
|
+
**Headers:**
|
|
87
|
+
|
|
88
|
+
- `Authorization: Bearer <jwt-token>` (required)
|
|
89
|
+
|
|
90
|
+
**Success Response (200):**
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"userId": "uuid",
|
|
95
|
+
"email": "user@example.com",
|
|
96
|
+
"metadata": { ... }
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Error Responses:**
|
|
101
|
+
|
|
102
|
+
- `401 Unauthorized`: Missing or invalid token
|
|
103
|
+
- `500 Internal Server Error`: Server error
|
|
104
|
+
|
|
105
|
+
## Files
|
|
106
|
+
|
|
107
|
+
- **`api.ts`**: Supabase client creation
|
|
108
|
+
- **`requireUser.ts`**: JWT verification function
|
|
109
|
+
- **`requireSysUser.ts`**: Requires an authenticated system ("sys") user
|
|
110
|
+
- **`requireOrgaAdmin.ts`**: Requires the caller to be an org admin
|
|
111
|
+
- **`README.md`**: This documentation
|
|
112
|
+
|
|
113
|
+
## Architecture
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Client Request
|
|
117
|
+
|
|
|
118
|
+
v
|
|
119
|
+
Authorization: Bearer <JWT>
|
|
120
|
+
|
|
|
121
|
+
v
|
|
122
|
+
Express Route
|
|
123
|
+
|
|
|
124
|
+
v
|
|
125
|
+
requireUser() / requireSysUser() / requireOrgaAdmin()
|
|
126
|
+
|
|
|
127
|
+
v
|
|
128
|
+
Supabase JWT Verification
|
|
129
|
+
|
|
|
130
|
+
+---> Valid: Continue with user data
|
|
131
|
+
|
|
|
132
|
+
+---> Invalid: Return 401 Unauthorized
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Security Notes
|
|
136
|
+
|
|
137
|
+
- JWT tokens are verified with Supabase on every request
|
|
138
|
+
- No session storage on the backend (stateless)
|
|
139
|
+
- Tokens expire based on Supabase configuration
|
|
140
|
+
- Always use HTTPS in production
|
|
141
|
+
- Store the anon key securely (use environment variables)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {createClient as createSupabaseClient} from '@supabase/supabase-js'
|
|
2
|
+
import {Request} from 'express'
|
|
3
|
+
|
|
4
|
+
let _serviceClient: ReturnType<typeof createSupabaseClient> | null = null;
|
|
5
|
+
|
|
6
|
+
export const createServiceClient = () => {
|
|
7
|
+
if (!_serviceClient) {
|
|
8
|
+
_serviceClient = createSupabaseClient(
|
|
9
|
+
process.env.SUPABASE_URL!,
|
|
10
|
+
process.env.SUPABASE_SECRET_KEY!
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return _serviceClient;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a Supabase client for verifying JWT tokens
|
|
18
|
+
* Used in backend API endpoints
|
|
19
|
+
*/
|
|
20
|
+
export default function createClient() {
|
|
21
|
+
return createSupabaseClient(
|
|
22
|
+
process.env.SUPABASE_URL!,
|
|
23
|
+
process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
24
|
+
{
|
|
25
|
+
auth: {
|
|
26
|
+
persistSession: false,
|
|
27
|
+
autoRefreshToken: false,
|
|
28
|
+
detectSessionInUrl: false,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates an authenticated Supabase client with the user's JWT token
|
|
36
|
+
* This ensures Row Level Security (RLS) policies are applied with the user's context
|
|
37
|
+
*/
|
|
38
|
+
export async function createAuthenticatedClient(req: Request) {
|
|
39
|
+
const token = req.headers.authorization?.replace('Bearer ', '') || '';
|
|
40
|
+
return createSupabaseClient(
|
|
41
|
+
process.env.SUPABASE_URL!,
|
|
42
|
+
process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
43
|
+
{
|
|
44
|
+
auth: {
|
|
45
|
+
persistSession: false,
|
|
46
|
+
autoRefreshToken: false,
|
|
47
|
+
detectSessionInUrl: false,
|
|
48
|
+
},
|
|
49
|
+
global: {
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${token}`,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|