@nebulit/embuilder 0.1.39
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 +254 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/package.json +49 -0
- package/templates/.claude/hooks/QUICKSTART.md +256 -0
- package/templates/.claude/hooks/README.md +533 -0
- package/templates/.claude/hooks/analyze-commit.sh +22 -0
- package/templates/.claude/hooks/analyze-commit.ts +518 -0
- package/templates/.claude/hooks/analyzers/README.md +198 -0
- package/templates/.claude/hooks/analyzers/code-quality-checker.ts +154 -0
- package/templates/.claude/hooks/analyzers/code-quality.md +54 -0
- package/templates/.claude/hooks/analyzers/commit-blocker-example.ts.disabled +110 -0
- package/templates/.claude/hooks/analyzers/commit-policy.md +49 -0
- package/templates/.claude/hooks/analyzers/event-model-validator.md +49 -0
- package/templates/.claude/hooks/analyzers/event-model-validator.ts +169 -0
- package/templates/.claude/hooks/analyzers/example-logger.ts +70 -0
- package/templates/.claude/hooks/analyzers/slice-scope-validator.md +81 -0
- package/templates/.claude/hooks/check-review-result.sh +47 -0
- package/templates/.claude/hooks/prepare-review.sh +34 -0
- package/templates/.claude/hooks/review-agent-prompt.md +42 -0
- package/templates/.claude/hooks/run-review-agent.sh +124 -0
- package/templates/.claude/settings.local.json +37 -0
- package/templates/.claude/skills/help/README.md +84 -0
- package/templates/.claude/skills/help/SKILL.md +393 -0
- package/templates/.claude/skills/help/templates/demo-config.json +6753 -0
- package/templates/.claude/skills/sample-slices/SKILL.md +8 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/code-slice.json +124 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/slice.json +255 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/availablebooks/slice.json +107 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/index.json +20 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/additem/slice.json +979 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/archiveitem/slice.json +529 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/cartitems/slice.json +1072 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/cartwithproducts/slice.json +394 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changedprices/slice.json +88 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changeinventory/slice.json +264 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changeprice/slice.json +308 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/clearcart/slice.json +358 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/inventories/slice.json +203 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/publishcart/slice.json +876 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/removeitem/slice.json +560 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/submitcart/slice.json +708 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/submittedcartdata/slice.json +399 -0
- package/templates/.claude/skills/sample-slices/templates/index.json +108 -0
- package/templates/.claude/skills/slice-automation/SKILL.md +49 -0
- package/templates/.claude/skills/slice-state-change/SKILL.md +369 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocation.test.ts.sample +76 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocationCommand.ts.sample +84 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/routes.ts.sample +73 -0
- package/templates/.claude/skills/slice-state-change/templates/README.md +46 -0
- package/templates/.claude/skills/slice-state-view/SKILL.md +336 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/Locations.test.ts.sample +84 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/LocationsProjection.ts.sample +50 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/routes.ts.sample +46 -0
- package/templates/.claude/skills/slice-state-view/templates/README.md +109 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/Tables.test.ts.sample +104 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/TablesProjection.ts.sample +59 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/routes.ts.sample +46 -0
- package/templates/.claude/skills/slice-state-view/templates/V2__tables.sql +7 -0
- package/templates/.claude/skills/slice-state-view/templates/V8__locations.sql +7 -0
- package/templates/.claude/skills/test-analyzer/SKILL.md +373 -0
- package/templates/.claude/skills/test-analyzer/examples/specification-format.md +143 -0
- package/templates/.claude/skills/test-analyzer/examples/state-change-example.md +111 -0
- package/templates/.claude/skills/test-analyzer/examples/state-view-example.md +122 -0
- package/templates/AGENTS.md +110 -0
- package/templates/Claude.md +58 -0
- package/templates/README.md +178 -0
- package/templates/backend/.env +9 -0
- package/templates/backend/BACKEND_AUTH_SETUP.md +183 -0
- package/templates/backend/SWAGGER.md +213 -0
- package/templates/backend/eslint.config.mjs +31 -0
- package/templates/backend/flyway.conf +17 -0
- package/templates/backend/package.json +44 -0
- package/templates/backend/prd.json.example +64 -0
- package/templates/backend/public/assets/images/banner.png +0 -0
- package/templates/backend/public/assets/logo.png +0 -0
- package/templates/backend/public/file.svg +4 -0
- package/templates/backend/public/globe.svg +12 -0
- package/templates/backend/public/next.svg +6 -0
- package/templates/backend/public/vercel.svg +3 -0
- package/templates/backend/public/window.svg +5 -0
- package/templates/backend/server.ts +129 -0
- package/templates/backend/setup-env.sh +50 -0
- package/templates/backend/src/common/assertions.ts +6 -0
- package/templates/backend/src/common/db.ts +1 -0
- package/templates/backend/src/common/loadPostgresEventstore.ts +16 -0
- package/templates/backend/src/common/parseEndpoint.ts +51 -0
- package/templates/backend/src/common/replay.ts +9 -0
- package/templates/backend/src/common/routes.ts +19 -0
- package/templates/backend/src/common/testHelpers.ts +53 -0
- package/templates/backend/src/core/readmodel.ts +28 -0
- package/templates/backend/src/core/types.ts +26 -0
- package/templates/backend/src/process/process.ts +53 -0
- package/templates/backend/src/supabase/LoginHandler.ts +36 -0
- package/templates/backend/src/supabase/ProtectedPageProps.ts +21 -0
- package/templates/backend/src/supabase/README.md +171 -0
- package/templates/backend/src/supabase/api.ts +63 -0
- package/templates/backend/src/supabase/authMiddleware.ts +53 -0
- package/templates/backend/src/supabase/component.ts +12 -0
- package/templates/backend/src/supabase/requireUser.ts +72 -0
- package/templates/backend/src/supabase/serverProps.ts +25 -0
- package/templates/backend/src/supabase/staticProps.ts +10 -0
- package/templates/backend/src/swagger.ts +34 -0
- package/templates/backend/src/util/assertions.ts +6 -0
- package/templates/backend/supabase/config.toml +295 -0
- package/templates/backend/supabase/migrations/20260121155918593_catalogentries.sql.sample +23 -0
- package/templates/backend/supabase/seed.sql +1 -0
- package/templates/backend/tsconfig.json +31 -0
- package/templates/frontend/.env.development +3 -0
- package/templates/frontend/AGENTS.md +7 -0
- package/templates/frontend/README.md +73 -0
- package/templates/frontend/components.json +20 -0
- package/templates/frontend/eslint.config.js +26 -0
- package/templates/frontend/index.html +18 -0
- package/templates/frontend/package-lock.json +8347 -0
- package/templates/frontend/package.json +94 -0
- package/templates/frontend/postcss.config.js +6 -0
- package/templates/frontend/public/favicon.ico +0 -0
- package/templates/frontend/public/logo.png +0 -0
- package/templates/frontend/public/placeholder.svg +1 -0
- package/templates/frontend/public/robots.txt +14 -0
- package/templates/frontend/src/App.css +42 -0
- package/templates/frontend/src/App.tsx +47 -0
- package/templates/frontend/src/components/NavLink.tsx +28 -0
- package/templates/frontend/src/components/ProtectedRoute.tsx +24 -0
- package/templates/frontend/src/components/calendar/Calendar.tsx +302 -0
- package/templates/frontend/src/components/layout/DashboardLayout.tsx +21 -0
- package/templates/frontend/src/components/layout/Header.tsx +45 -0
- package/templates/frontend/src/components/layout/Sidebar.tsx +82 -0
- package/templates/frontend/src/components/tables/ReservationTemplates.tsx +189 -0
- package/templates/frontend/src/components/ui/accordion.tsx +52 -0
- package/templates/frontend/src/components/ui/alert-dialog.tsx +104 -0
- package/templates/frontend/src/components/ui/alert.tsx +43 -0
- package/templates/frontend/src/components/ui/aspect-ratio.tsx +5 -0
- package/templates/frontend/src/components/ui/avatar.tsx +38 -0
- package/templates/frontend/src/components/ui/badge.tsx +29 -0
- package/templates/frontend/src/components/ui/breadcrumb.tsx +90 -0
- package/templates/frontend/src/components/ui/button.tsx +47 -0
- package/templates/frontend/src/components/ui/calendar.tsx +54 -0
- package/templates/frontend/src/components/ui/card.tsx +43 -0
- package/templates/frontend/src/components/ui/carousel.tsx +224 -0
- package/templates/frontend/src/components/ui/chart.tsx +303 -0
- package/templates/frontend/src/components/ui/checkbox.tsx +26 -0
- package/templates/frontend/src/components/ui/collapsible.tsx +9 -0
- package/templates/frontend/src/components/ui/command.tsx +132 -0
- package/templates/frontend/src/components/ui/context-menu.tsx +178 -0
- package/templates/frontend/src/components/ui/dialog.tsx +95 -0
- package/templates/frontend/src/components/ui/drawer.tsx +87 -0
- package/templates/frontend/src/components/ui/dropdown-menu.tsx +179 -0
- package/templates/frontend/src/components/ui/form.tsx +129 -0
- package/templates/frontend/src/components/ui/hover-card.tsx +27 -0
- package/templates/frontend/src/components/ui/input-otp.tsx +61 -0
- package/templates/frontend/src/components/ui/input.tsx +22 -0
- package/templates/frontend/src/components/ui/label.tsx +17 -0
- package/templates/frontend/src/components/ui/menubar.tsx +207 -0
- package/templates/frontend/src/components/ui/navigation-menu.tsx +120 -0
- package/templates/frontend/src/components/ui/pagination.tsx +81 -0
- package/templates/frontend/src/components/ui/popover.tsx +29 -0
- package/templates/frontend/src/components/ui/progress.tsx +23 -0
- package/templates/frontend/src/components/ui/radio-group.tsx +36 -0
- package/templates/frontend/src/components/ui/resizable.tsx +37 -0
- package/templates/frontend/src/components/ui/scroll-area.tsx +38 -0
- package/templates/frontend/src/components/ui/select.tsx +143 -0
- package/templates/frontend/src/components/ui/separator.tsx +20 -0
- package/templates/frontend/src/components/ui/sheet.tsx +107 -0
- package/templates/frontend/src/components/ui/sidebar.tsx +637 -0
- package/templates/frontend/src/components/ui/skeleton.tsx +7 -0
- package/templates/frontend/src/components/ui/slider.tsx +23 -0
- package/templates/frontend/src/components/ui/sonner.tsx +27 -0
- package/templates/frontend/src/components/ui/stat-card.tsx +44 -0
- package/templates/frontend/src/components/ui/switch.tsx +27 -0
- package/templates/frontend/src/components/ui/table.tsx +72 -0
- package/templates/frontend/src/components/ui/tabs.tsx +53 -0
- package/templates/frontend/src/components/ui/textarea.tsx +21 -0
- package/templates/frontend/src/components/ui/toast.tsx +111 -0
- package/templates/frontend/src/components/ui/toaster.tsx +24 -0
- package/templates/frontend/src/components/ui/toggle-group.tsx +49 -0
- package/templates/frontend/src/components/ui/toggle.tsx +37 -0
- package/templates/frontend/src/components/ui/tooltip.tsx +28 -0
- package/templates/frontend/src/components/ui/use-toast.ts +3 -0
- package/templates/frontend/src/contexts/AuthContext.tsx +94 -0
- package/templates/frontend/src/contexts/RefreshContext.tsx +236 -0
- package/templates/frontend/src/hooks/api/index.ts +2 -0
- package/templates/frontend/src/hooks/api/useLocations.ts +15 -0
- package/templates/frontend/src/hooks/use-mobile.tsx +19 -0
- package/templates/frontend/src/hooks/use-toast.ts +186 -0
- package/templates/frontend/src/hooks/useApiContext.ts +11 -0
- package/templates/frontend/src/index.css +118 -0
- package/templates/frontend/src/integrations/supabase/client.ts +9 -0
- package/templates/frontend/src/lib/api-client.ts +136 -0
- package/templates/frontend/src/lib/api.ts +1028 -0
- package/templates/frontend/src/lib/utils.ts +6 -0
- package/templates/frontend/src/main.tsx +5 -0
- package/templates/frontend/src/pages/Auth.tsx +408 -0
- package/templates/frontend/src/pages/Dashboard.tsx +168 -0
- package/templates/frontend/src/pages/Menus.tsx +224 -0
- package/templates/frontend/src/pages/NotFound.tsx +24 -0
- package/templates/frontend/src/pages/Register.tsx +285 -0
- package/templates/frontend/src/test/example.test.ts +0 -0
- package/templates/frontend/src/test/setup.ts +15 -0
- package/templates/frontend/src/types/index.ts +8 -0
- package/templates/frontend/src/vite-env.d.ts +1 -0
- package/templates/frontend/tailwind.config.ts +101 -0
- package/templates/frontend/tsconfig.app.json +31 -0
- package/templates/frontend/tsconfig.json +16 -0
- package/templates/frontend/tsconfig.node.json +22 -0
- package/templates/frontend/vite.config.ts +21 -0
- package/templates/frontend/vitest.config.ts +16 -0
- package/templates/init.sh +1 -0
- package/templates/prompt.md +139 -0
- package/templates/ralph.sh +120 -0
- package/templates/server.mjs +505 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "MyApp",
|
|
3
|
+
"branchName": "ralph/task-priority",
|
|
4
|
+
"description": "Task Priority System - Add priority levels to tasks",
|
|
5
|
+
"userStories": [
|
|
6
|
+
{
|
|
7
|
+
"id": "US-001",
|
|
8
|
+
"title": "Add priority field to database",
|
|
9
|
+
"description": "As a developer, I need to store task priority so it persists across sessions.",
|
|
10
|
+
"acceptanceCriteria": [
|
|
11
|
+
"Add priority column to tasks table: 'high' | 'medium' | 'low' (default 'medium')",
|
|
12
|
+
"Generate and run migration successfully",
|
|
13
|
+
"Typecheck passes"
|
|
14
|
+
],
|
|
15
|
+
"priority": 1,
|
|
16
|
+
"passes": false,
|
|
17
|
+
"notes": ""
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "US-002",
|
|
21
|
+
"title": "Display priority indicator on task cards",
|
|
22
|
+
"description": "As a user, I want to see task priority at a glance.",
|
|
23
|
+
"acceptanceCriteria": [
|
|
24
|
+
"Each task card shows colored priority badge (red=high, yellow=medium, gray=low)",
|
|
25
|
+
"Priority visible without hovering or clicking",
|
|
26
|
+
"Typecheck passes",
|
|
27
|
+
"Verify in browser using dev-browser skill"
|
|
28
|
+
],
|
|
29
|
+
"priority": 2,
|
|
30
|
+
"passes": false,
|
|
31
|
+
"notes": ""
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "US-003",
|
|
35
|
+
"title": "Add priority selector to task edit",
|
|
36
|
+
"description": "As a user, I want to change a task's priority when editing it.",
|
|
37
|
+
"acceptanceCriteria": [
|
|
38
|
+
"Priority dropdown in task edit modal",
|
|
39
|
+
"Shows current priority as selected",
|
|
40
|
+
"Saves immediately on selection change",
|
|
41
|
+
"Typecheck passes",
|
|
42
|
+
"Verify in browser using dev-browser skill"
|
|
43
|
+
],
|
|
44
|
+
"priority": 3,
|
|
45
|
+
"passes": false,
|
|
46
|
+
"notes": ""
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "US-004",
|
|
50
|
+
"title": "Filter tasks by priority",
|
|
51
|
+
"description": "As a user, I want to filter the task list to see only high-priority items.",
|
|
52
|
+
"acceptanceCriteria": [
|
|
53
|
+
"Filter dropdown with options: All | High | Medium | Low",
|
|
54
|
+
"Filter persists in URL params",
|
|
55
|
+
"Empty state message when no tasks match filter",
|
|
56
|
+
"Typecheck passes",
|
|
57
|
+
"Verify in browser using dev-browser skill"
|
|
58
|
+
],
|
|
59
|
+
"priority": 4,
|
|
60
|
+
"passes": false,
|
|
61
|
+
"notes": ""
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z"
|
|
3
|
+
clip-rule="evenodd" fill="#666" fill-rule="evenodd"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
|
2
|
+
<g clip-path="url(#a)">
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
4
|
+
d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1"
|
|
5
|
+
fill="#666"/>
|
|
6
|
+
</g>
|
|
7
|
+
<defs>
|
|
8
|
+
<clipPath id="a">
|
|
9
|
+
<path fill="#fff" d="M0 0h16v16H0z"/>
|
|
10
|
+
</clipPath>
|
|
11
|
+
</defs>
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80">
|
|
2
|
+
<path fill="#000"
|
|
3
|
+
d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/>
|
|
4
|
+
<path fill="#000"
|
|
5
|
+
d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
3
|
+
d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5"
|
|
4
|
+
fill="#666"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {join} from 'path';
|
|
2
|
+
import {getApplication, startAPI, WebApiSetup} from '@event-driven-io/emmett-expressjs';
|
|
3
|
+
import {glob} from "glob";
|
|
4
|
+
import {replayProjection} from "./src/common/replay";
|
|
5
|
+
import express, {Application, Request, Response} from 'express';
|
|
6
|
+
import {requireUser} from "./src/supabase/requireUser";
|
|
7
|
+
import swaggerUi from 'swagger-ui-express'
|
|
8
|
+
import {specs} from './src/swagger';
|
|
9
|
+
import cors from 'cors';
|
|
10
|
+
import {api as replayApi} from "./src/common/routes"
|
|
11
|
+
|
|
12
|
+
async function startServer() {
|
|
13
|
+
const routesPattern = join(__dirname, 'src/slices/**/routes{,-*}.@(ts|js)');
|
|
14
|
+
const routeFiles = await glob(routesPattern, {nodir: true});
|
|
15
|
+
console.log('Found route files:', routeFiles);
|
|
16
|
+
|
|
17
|
+
const processorPattern = join(__dirname, 'src/slices/**/processor{,-*}.@(ts|js)');
|
|
18
|
+
const processorFiles = await glob(processorPattern, {nodir: true});
|
|
19
|
+
console.log('Found processor files:', processorFiles);
|
|
20
|
+
|
|
21
|
+
const rootApp: Application = express();
|
|
22
|
+
|
|
23
|
+
// Configure CORS to allow requests from localhost:8080 and localhost:8081
|
|
24
|
+
rootApp.use(cors({
|
|
25
|
+
origin: ['http://localhost:8080', 'http://localhost:8081', '*'],
|
|
26
|
+
credentials: true,
|
|
27
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
28
|
+
allowedHeaders: ['Content-Type', 'Authorization','x-user-id']
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const webApis: WebApiSetup[] = [];
|
|
32
|
+
|
|
33
|
+
for (const file of routeFiles) {
|
|
34
|
+
const webApiModule: { api: () => WebApiSetup } = await import(file);
|
|
35
|
+
if (typeof webApiModule.api == 'function') {
|
|
36
|
+
var module = webApiModule.api()
|
|
37
|
+
webApis.push(module);
|
|
38
|
+
} else {
|
|
39
|
+
console.error(`Expected api function to be defined in ${file}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
webApis.push(replayApi)
|
|
44
|
+
|
|
45
|
+
for (const processorFile of processorFiles) {
|
|
46
|
+
const processor: { processor: { start: () => {} } } = await import(processorFile);
|
|
47
|
+
if (typeof processor.processor.start == "function") {
|
|
48
|
+
console.log(`starting processor ${processorFile}`)
|
|
49
|
+
processor.processor.start()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get the main application from emmett
|
|
54
|
+
const childApp: Application = getApplication({
|
|
55
|
+
apis: webApis,
|
|
56
|
+
disableJsonMiddleware: false,
|
|
57
|
+
enableDefaultExpressEtag: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Add your custom routes to the main application (BEFORE the catch-all)
|
|
61
|
+
childApp.post("/internal/replay/:slice/:projectionName", async (req: Request, resp: Response) => {
|
|
62
|
+
const {slice, projectionName} = req.params
|
|
63
|
+
await replayProjection(slice, projectionName);
|
|
64
|
+
return resp.status(200).json({status: 'ok'});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Protected user info endpoint - requires JWT token in Authorization header
|
|
68
|
+
childApp.get('/api/user', async (req: Request, res: Response) => {
|
|
69
|
+
console.log('API user route hit'); // Debug log
|
|
70
|
+
try {
|
|
71
|
+
const result = await requireUser(req, res, false)
|
|
72
|
+
if (result.error) {
|
|
73
|
+
// Response already sent by requireUser if sendUnauthorized=true
|
|
74
|
+
if (!res.headersSent) {
|
|
75
|
+
res.status(401).json({error: result.error})
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
res.status(200).json({
|
|
79
|
+
userId: result.user.id,
|
|
80
|
+
email: result.user.email,
|
|
81
|
+
metadata: result.user.user_metadata
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Error in /api/user:', error);
|
|
86
|
+
if (!res.headersSent) {
|
|
87
|
+
res.status(500).json({error: 'Internal server error'});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Swagger UI endpoints
|
|
93
|
+
childApp.use('/api-docs', swaggerUi.serve);
|
|
94
|
+
childApp.get('/api-docs', swaggerUi.setup(specs, {
|
|
95
|
+
swaggerOptions: {
|
|
96
|
+
urls: [
|
|
97
|
+
{
|
|
98
|
+
url: '/swagger.json',
|
|
99
|
+
name: 'JSON',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// OpenAPI spec endpoint
|
|
106
|
+
childApp.get('/swagger.json', (req: Request, res: Response) => {
|
|
107
|
+
res.setHeader('Content-Type', 'application/json');
|
|
108
|
+
res.send(specs);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
112
|
+
console.log(`> Ready on port ${port}`);
|
|
113
|
+
|
|
114
|
+
rootApp.use(childApp)
|
|
115
|
+
// Start the main application
|
|
116
|
+
startAPI(rootApp, {port: port});
|
|
117
|
+
|
|
118
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
119
|
+
console.error('⛔ Unhandled Rejection:', reason);
|
|
120
|
+
if (reason instanceof Error && reason.stack) {
|
|
121
|
+
console.error('Stack trace:\n', reason.stack);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
startServer().catch(error => {
|
|
127
|
+
console.error('Failed to start server:', error);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
|
|
37
|
+
cat > .env <<EOF
|
|
38
|
+
SUPABASE_URL=https://${PROJECT_ID}.supabase.co
|
|
39
|
+
SUPABASE_PUBLISHABLE_KEY=${PUBLISHABLE_KEY}
|
|
40
|
+
SUPABASE_DB_URL=postgresql://postgres.${PROJECT_ID}:${DB_PASSWORD}@aws-1-eu-central-1.pooler.supabase.com:5432/postgres?prepareThreshold=0
|
|
41
|
+
SUPABASE_SECRET_KEY=${SECRET_KEY}
|
|
42
|
+
|
|
43
|
+
# Flyway configuration
|
|
44
|
+
FLYWAY_URL=jdbc:postgresql://aws-1-eu-west-1.pooler.supabase.com:6543/postgres?user=postgres.${PROJECT_ID}&password=${DB_PASSWORD}
|
|
45
|
+
FLYWAY_USER=postgres.${PROJECT_ID}
|
|
46
|
+
FLYWAY_PASSWORD=${DB_PASSWORD}
|
|
47
|
+
EOF
|
|
48
|
+
|
|
49
|
+
echo ""
|
|
50
|
+
echo ".env created successfully."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const postgresUrl = process.env.SUPABASE_DB_URL ?? "missing-url"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {getPostgreSQLEventStore} from "@event-driven-io/emmett-postgresql";
|
|
2
|
+
import {projections} from "@event-driven-io/emmett";
|
|
3
|
+
import {postgresUrl} from "./db";
|
|
4
|
+
|
|
5
|
+
export const findEventstore = async () => {
|
|
6
|
+
|
|
7
|
+
return getPostgreSQLEventStore(postgresUrl, {
|
|
8
|
+
schema: {
|
|
9
|
+
autoMigration: "CreateOrUpdate"
|
|
10
|
+
},
|
|
11
|
+
projections: projections.inline([
|
|
12
|
+
]),
|
|
13
|
+
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
}
|
|
@@ -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,9 @@
|
|
|
1
|
+
import {PostgreSQLProjectionDefinition, rebuildPostgreSQLProjections} from "@event-driven-io/emmett-postgresql";
|
|
2
|
+
import {postgresUrl} from "./db";
|
|
3
|
+
|
|
4
|
+
export const replayProjection = async (slice: string, projectionName: string): Promise<void> => {
|
|
5
|
+
const projectionImport = await import((`../slices/${slice}/${projectionName}.ts`))
|
|
6
|
+
const projection: PostgreSQLProjectionDefinition = projectionImport[projectionName]
|
|
7
|
+
|
|
8
|
+
return rebuildPostgreSQLProjections({projection: projection, connectionString: postgresUrl}).start();
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
6
|
+
|
|
7
|
+
export const api =
|
|
8
|
+
(
|
|
9
|
+
// external dependencies
|
|
10
|
+
): WebApiSetup =>
|
|
11
|
+
(router: Router): void => {
|
|
12
|
+
|
|
13
|
+
router.post('/api/replay/:slice/:projection', async (req: Request, res: Response) => {
|
|
14
|
+
const slice= assertNotEmpty(req.params.slice)
|
|
15
|
+
const projection = assertNotEmpty(req.params.projection)
|
|
16
|
+
replayProjection(slice, projection)
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {execSync} from 'child_process';
|
|
2
|
+
import {writeFileSync, unlinkSync} from 'fs';
|
|
3
|
+
import {tmpdir} from 'os';
|
|
4
|
+
import {join} from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runs Flyway migrations against a test database
|
|
8
|
+
* @param connectionString PostgreSQL connection string
|
|
9
|
+
*/
|
|
10
|
+
export async function runFlywayMigrations(connectionString: string): Promise<void> {
|
|
11
|
+
// Parse connection string to extract components
|
|
12
|
+
const url = new URL(connectionString);
|
|
13
|
+
const jdbcUrl = `jdbc:postgresql://${url.hostname}:${url.port || 5432}${url.pathname}`;
|
|
14
|
+
const user = url.username;
|
|
15
|
+
const password = url.password;
|
|
16
|
+
|
|
17
|
+
// Create temporary Flyway config file
|
|
18
|
+
const tempConfigPath = join(tmpdir(), `flyway-test-${Date.now()}.conf`);
|
|
19
|
+
const migrationsPath = join(process.cwd(), 'supabase', 'migrations');
|
|
20
|
+
|
|
21
|
+
const config = `
|
|
22
|
+
flyway.url=${jdbcUrl}
|
|
23
|
+
flyway.user=${user}
|
|
24
|
+
flyway.password=${password}
|
|
25
|
+
flyway.locations=filesystem:${migrationsPath}
|
|
26
|
+
flyway.schemas=public
|
|
27
|
+
flyway.placeholderReplacement=false
|
|
28
|
+
flyway.validateOnMigrate=true
|
|
29
|
+
flyway.cleanDisabled=false
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
writeFileSync(tempConfigPath, config, 'utf8');
|
|
34
|
+
|
|
35
|
+
// Run Flyway migrate
|
|
36
|
+
execSync(`flyway -configFiles=${tempConfigPath} migrate`, {
|
|
37
|
+
stdio: 'pipe',
|
|
38
|
+
encoding: 'utf8'
|
|
39
|
+
});
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
console.error('Flyway migration failed:', error.message);
|
|
42
|
+
if (error.stdout) console.error('STDOUT:', error.stdout);
|
|
43
|
+
if (error.stderr) console.error('STDERR:', error.stderr);
|
|
44
|
+
throw new Error(`Flyway migration failed: ${error.message}`);
|
|
45
|
+
} finally {
|
|
46
|
+
// Cleanup temp config file
|
|
47
|
+
try {
|
|
48
|
+
unlinkSync(tempConfigPath);
|
|
49
|
+
} catch {
|
|
50
|
+
// Ignore cleanup errors
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {SupabaseClient} from "@supabase/supabase-js";
|
|
2
|
+
import {PostgrestFilterBuilder} from "@supabase/postgrest-js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const readmodel = (collection: string, supabase: SupabaseClient) => ({
|
|
6
|
+
findAll: async <T>(query?: Record<string, any>,
|
|
7
|
+
queryWrapper: (query: PostgrestFilterBuilder<any, any, any, any, any>) => PostgrestFilterBuilder<any, any, any, any, any> = (query) => query): Promise<T[]> => {
|
|
8
|
+
var query1 = supabase.from(collection).select("*");
|
|
9
|
+
Object.entries(query??{}).forEach((it,value) => {
|
|
10
|
+
query1 = query1.eq(it[0], it[1])
|
|
11
|
+
})
|
|
12
|
+
let qb = queryWrapper(query1);
|
|
13
|
+
const response = await qb;
|
|
14
|
+
if (response.error) throw response.error;
|
|
15
|
+
return response.data as T[]
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
findById: async <T>(idcolumn: string, id: string): Promise<T | null> => {
|
|
19
|
+
const response = await supabase
|
|
20
|
+
.from(collection)
|
|
21
|
+
.select("*")
|
|
22
|
+
.eq(idcolumn, id)
|
|
23
|
+
.maybeSingle();
|
|
24
|
+
if (response.error) throw response.error;
|
|
25
|
+
return response as T | null;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Nebulit GmbH
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
export type CommandConfig = {
|
|
9
|
+
command: string
|
|
10
|
+
endpoint: string,
|
|
11
|
+
schema: any // json schema
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ReadModelConfig = {
|
|
15
|
+
readModel: string
|
|
16
|
+
endpoint: string,
|
|
17
|
+
readModelView: React.FC<any>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ViewSelection = {
|
|
21
|
+
"slice": string,
|
|
22
|
+
"viewType": string,
|
|
23
|
+
"commandView": React.FC<any>,
|
|
24
|
+
"viewName": string
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import cron from "node-cron";
|
|
2
|
+
import {parseQueryEndpoint} from "../common/parseEndpoint";
|
|
3
|
+
|
|
4
|
+
export type ProcessorTodoItem = {
|
|
5
|
+
id: string,
|
|
6
|
+
status: string
|
|
7
|
+
createdAt: Date
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ProcessorConfig = {
|
|
11
|
+
schedule: string,
|
|
12
|
+
endpoint: string,
|
|
13
|
+
query?: Record<string, string>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const startProcessor = <T>(config: ProcessorConfig, handler: (item: T & ProcessorTodoItem) => void) => {
|
|
17
|
+
cron.schedule(config.schedule, async () => {
|
|
18
|
+
const data = await fetchData(parseQueryEndpoint(config.endpoint, config.query ? config.query : {
|
|
19
|
+
"status": "OPEN",
|
|
20
|
+
"_limit": "1"
|
|
21
|
+
}))
|
|
22
|
+
if (Array.isArray(data) && data?.length > 0) {
|
|
23
|
+
handler(data[0]);
|
|
24
|
+
} else {
|
|
25
|
+
//console.log("No item to process")
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async function fetchData(endpoint: string) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(endpoint, {
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
return Promise.reject(`HTTP error! status: ${response.status}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await response.json(); // This will be a list of objects
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error fetching data:', error);
|
|
47
|
+
return Promise.reject(`Error fetching data ${error}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {type EmailOtpType} from '@supabase/supabase-js'
|
|
2
|
+
import {Request, Response} from "express";
|
|
3
|
+
|
|
4
|
+
import createClient from './api'
|
|
5
|
+
|
|
6
|
+
function stringOrFirstString(item: string | string[] | undefined) {
|
|
7
|
+
return Array.isArray(item) ? item[0] : item
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default async function LoginHandler(req: Request, res: Response) {
|
|
11
|
+
if (req.method !== 'GET') {
|
|
12
|
+
res.status(405).appendHeader('Allow', 'GET').end()
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const queryParams = req.query
|
|
17
|
+
const token_hash = stringOrFirstString(queryParams["token_hash"]?.toString())
|
|
18
|
+
const type = stringOrFirstString(queryParams["type"]?.toString())
|
|
19
|
+
|
|
20
|
+
let next = '/error'
|
|
21
|
+
|
|
22
|
+
if (token_hash && type) {
|
|
23
|
+
const supabase = createClient()
|
|
24
|
+
const {error} = await supabase.auth.verifyOtp({
|
|
25
|
+
type: type as EmailOtpType,
|
|
26
|
+
token_hash,
|
|
27
|
+
})
|
|
28
|
+
if (error) {
|
|
29
|
+
console.error(error)
|
|
30
|
+
} else {
|
|
31
|
+
next = stringOrFirstString(queryParams["next"]?.toString()) || '/'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res.redirect(next)
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {type Request, type Response} from "express";
|
|
2
|
+
import {createClient} from "./serverProps";
|
|
3
|
+
|
|
4
|
+
export async function getAuthenticatedUser(req: Request, res: Response) {
|
|
5
|
+
const supabase = createClient(req, res)
|
|
6
|
+
const {data, error} = await supabase.auth.getUser()
|
|
7
|
+
if (error || !data) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
return data.user
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function requireAuthMiddleware(req: Request, res: Response, next: () => void) {
|
|
14
|
+
const user = await getAuthenticatedUser(req, res)
|
|
15
|
+
if (!user) {
|
|
16
|
+
res.redirect('/auth/login')
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
(req as any).user = user
|
|
20
|
+
next()
|
|
21
|
+
}
|