create-raredays-app 0.1.0
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 +89 -0
- package/bin/index.js +1024 -0
- package/package.json +43 -0
- package/scaffold-fragments/billing/files/app/api/billing/webhook/route.ts +5 -0
- package/scaffold-fragments/billing/module.json +31 -0
- package/scaffold-fragments/billing/runtime/config.ts +8 -0
- package/scaffold-fragments/billing/runtime/exports.ts +6 -0
- package/scaffold-fragments/billing/runtime/imports.ts +6 -0
- package/scaffold-fragments/billing/runtime/ports.ts +3 -0
- package/scaffold-fragments/cms/module.json +29 -0
- package/scaffold-fragments/cms/runtime/config.ts +4 -0
- package/scaffold-fragments/cms/runtime/exports.ts +1 -0
- package/scaffold-fragments/cms/runtime/imports.ts +1 -0
- package/scaffold-fragments/email/module.json +30 -0
- package/scaffold-fragments/email/runtime/config.ts +1 -0
- package/scaffold-fragments/email/runtime/imports.ts +1 -0
- package/scaffold-fragments/email/runtime/ports.ts +1 -0
- package/scaffold-fragments/feature-flags/files/app/lib/flags.ts +48 -0
- package/scaffold-fragments/feature-flags/module.json +66 -0
- package/scaffold-fragments/i18n/files/messages/en-US.json +15 -0
- package/scaffold-fragments/i18n/module.json +9 -0
- package/scaffold-fragments/i18n/runtime/config.ts +7 -0
- package/scaffold-fragments/i18n/runtime/exports.ts +1 -0
- package/scaffold-fragments/i18n/runtime/imports.ts +2 -0
- package/scaffold-fragments/i18n/runtime/ports.ts +1 -0
- package/scaffold-fragments/identity/module.json +30 -0
- package/scaffold-fragments/identity/vendors/better-auth/files/app/api/auth/[...all]/route.ts +15 -0
- package/scaffold-fragments/identity/vendors/better-auth/files/app/lib/auth.ts +20 -0
- package/scaffold-fragments/jobs/module.json +12 -0
- package/scaffold-fragments/jobs/runtime/config.ts +4 -0
- package/scaffold-fragments/jobs/runtime/imports.ts +1 -0
- package/scaffold-fragments/jobs/runtime/ports.ts +1 -0
- package/scaffold-fragments/notifications/module.json +12 -0
- package/scaffold-fragments/notifications/runtime/config.ts +8 -0
- package/scaffold-fragments/notifications/runtime/imports.ts +5 -0
- package/scaffold-fragments/notifications/runtime/ports.ts +1 -0
- package/scaffold-fragments/organizations/module.json +12 -0
- package/scaffold-fragments/telemetry/module.json +43 -0
- package/scaffold-fragments/telemetry/runtime/config.ts +7 -0
- package/scaffold-fragments/telemetry/runtime/imports.ts +7 -0
- package/scaffold-fragments/telemetry/runtime/ports.ts +1 -0
- package/skillex/private/contribute.md +193 -0
- package/skillex/public/consumer.md +157 -0
- package/templates/client/.storybook/main.ts +77 -0
- package/templates/client/.storybook/preview.ts +12 -0
- package/templates/client/CLAUDE.md +50 -0
- package/templates/client/_claude/settings.json +107 -0
- package/templates/client/_github/PULL_REQUEST_TEMPLATE.md +19 -0
- package/templates/client/_github/dependabot.yml +48 -0
- package/templates/client/_github/workflows/ci.yml +104 -0
- package/templates/client/_gitignore +15 -0
- package/templates/client/_npmrc +1 -0
- package/templates/client/app/(app)/layout.tsx +49 -0
- package/templates/client/app/(marketing)/layout.tsx +21 -0
- package/templates/client/app/(marketing)/page.tsx +34 -0
- package/templates/client/app/api/backend/[...path]/route.ts +37 -0
- package/templates/client/app/api/dev/login/route.ts +14 -0
- package/templates/client/app/api/dev/logout/route.ts +18 -0
- package/templates/client/app/globals.css +22 -0
- package/templates/client/app/home-auth-controls.tsx +28 -0
- package/templates/client/app/layout.tsx +38 -0
- package/templates/client/app/lib/backend-client.ts +55 -0
- package/templates/client/app/lib/runtime.test.ts +38 -0
- package/templates/client/app/lib/runtime.ts +54 -0
- package/templates/client/app/protected/page.tsx +31 -0
- package/templates/client/app/theme.override.css +9 -0
- package/templates/client/instrumentation.ts +10 -0
- package/templates/client/mocks/browser.tsx +28 -0
- package/templates/client/mocks/handlers.ts +20 -0
- package/templates/client/mocks/node.ts +7 -0
- package/templates/client/next.config.ts +8 -0
- package/templates/client/oxfmt.config.ts +8 -0
- package/templates/client/oxlint.config.ts +8 -0
- package/templates/client/package.json +61 -0
- package/templates/client/postcss.config.mjs +1 -0
- package/templates/client/public/mockServiceWorker.js +349 -0
- package/templates/client/scripts/build-theme.mjs +18 -0
- package/templates/client/site.spec.ts +58 -0
- package/templates/client/skillex.json +12 -0
- package/templates/client/skills/repo.md +56 -0
- package/templates/client/stylelint.config.mjs +4 -0
- package/templates/client/tsconfig.json +11 -0
- package/templates/client/vitest.config.ts +27 -0
- package/templates/client/vitest.setup.ts +18 -0
- package/templates/full-stack/.storybook/main.ts +77 -0
- package/templates/full-stack/.storybook/preview.ts +12 -0
- package/templates/full-stack/CLAUDE.md +50 -0
- package/templates/full-stack/_claude/settings.json +107 -0
- package/templates/full-stack/_github/PULL_REQUEST_TEMPLATE.md +19 -0
- package/templates/full-stack/_github/dependabot.yml +57 -0
- package/templates/full-stack/_github/workflows/ci.yml +109 -0
- package/templates/full-stack/_github/workflows/db-check.yml +36 -0
- package/templates/full-stack/_github/workflows/db-migrate.yml +39 -0
- package/templates/full-stack/_gitignore +15 -0
- package/templates/full-stack/_npmrc +1 -0
- package/templates/full-stack/app/(app)/layout.tsx +53 -0
- package/templates/full-stack/app/(marketing)/layout.tsx +21 -0
- package/templates/full-stack/app/(marketing)/page.tsx +38 -0
- package/templates/full-stack/app/api/dev/login/route.ts +14 -0
- package/templates/full-stack/app/api/dev/logout/route.ts +18 -0
- package/templates/full-stack/app/api/widgets/route.ts +10 -0
- package/templates/full-stack/app/demo/widgets/page.tsx +86 -0
- package/templates/full-stack/app/globals.css +22 -0
- package/templates/full-stack/app/home-auth-controls.tsx +28 -0
- package/templates/full-stack/app/layout.tsx +38 -0
- package/templates/full-stack/app/lib/runtime.test.ts +38 -0
- package/templates/full-stack/app/lib/runtime.ts +54 -0
- package/templates/full-stack/app/protected/page.tsx +31 -0
- package/templates/full-stack/app/theme.override.css +9 -0
- package/templates/full-stack/db/check.ts +46 -0
- package/templates/full-stack/db/index.ts +109 -0
- package/templates/full-stack/db/migrate.ts +66 -0
- package/templates/full-stack/db/migrations/.gitkeep +0 -0
- package/templates/full-stack/db/schema.ts +20 -0
- package/templates/full-stack/drizzle.config.ts +16 -0
- package/templates/full-stack/instrumentation.ts +10 -0
- package/templates/full-stack/mocks/browser.tsx +28 -0
- package/templates/full-stack/mocks/handlers.ts +34 -0
- package/templates/full-stack/mocks/node.ts +7 -0
- package/templates/full-stack/next.config.ts +8 -0
- package/templates/full-stack/oxfmt.config.ts +8 -0
- package/templates/full-stack/oxlint.config.ts +8 -0
- package/templates/full-stack/package.json +72 -0
- package/templates/full-stack/postcss.config.mjs +1 -0
- package/templates/full-stack/public/mockServiceWorker.js +349 -0
- package/templates/full-stack/scripts/build-theme.mjs +18 -0
- package/templates/full-stack/site.spec.ts +58 -0
- package/templates/full-stack/skillex.json +12 -0
- package/templates/full-stack/skills/repo.md +56 -0
- package/templates/full-stack/stylelint.config.mjs +4 -0
- package/templates/full-stack/tsconfig.json +11 -0
- package/templates/full-stack/vitest.config.ts +42 -0
- package/templates/full-stack/vitest.setup.ts +18 -0
- package/templates/static/.storybook/main.ts +77 -0
- package/templates/static/.storybook/preview.ts +12 -0
- package/templates/static/CLAUDE.md +50 -0
- package/templates/static/_claude/settings.json +107 -0
- package/templates/static/_github/PULL_REQUEST_TEMPLATE.md +18 -0
- package/templates/static/_github/dependabot.yml +43 -0
- package/templates/static/_github/workflows/ci.yml +91 -0
- package/templates/static/_gitignore +15 -0
- package/templates/static/_npmrc +1 -0
- package/templates/static/app/(marketing)/layout.tsx +21 -0
- package/templates/static/app/(marketing)/page.tsx +17 -0
- package/templates/static/app/globals.css +22 -0
- package/templates/static/app/layout.tsx +24 -0
- package/templates/static/app/theme.override.css +9 -0
- package/templates/static/next.config.ts +19 -0
- package/templates/static/oxfmt.config.ts +8 -0
- package/templates/static/oxlint.config.ts +8 -0
- package/templates/static/package.json +53 -0
- package/templates/static/postcss.config.mjs +1 -0
- package/templates/static/scripts/build-theme.mjs +18 -0
- package/templates/static/site.spec.ts +58 -0
- package/templates/static/skillex.json +12 -0
- package/templates/static/skills/repo.md +56 -0
- package/templates/static/stylelint.config.mjs +4 -0
- package/templates/static/tsconfig.json +11 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
# Mirrors the RareDays framework's CI discipline for consumer apps.
|
|
14
|
+
# Client template has no backend / db steps; otherwise identical
|
|
15
|
+
# verification chain to the full-stack template.
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
lint:
|
|
19
|
+
name: Lint
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
timeout-minutes: 10
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- uses: pnpm/action-setup@v4
|
|
25
|
+
- uses: actions/setup-node@v4
|
|
26
|
+
with: { node-version: 24, cache: pnpm }
|
|
27
|
+
- run: pnpm install --frozen-lockfile
|
|
28
|
+
- run: pnpm lint
|
|
29
|
+
|
|
30
|
+
format-check:
|
|
31
|
+
name: Format check
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
timeout-minutes: 10
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: pnpm/action-setup@v4
|
|
37
|
+
- uses: actions/setup-node@v4
|
|
38
|
+
with: { node-version: 24, cache: pnpm }
|
|
39
|
+
- run: pnpm install --frozen-lockfile
|
|
40
|
+
- run: pnpm format:check
|
|
41
|
+
|
|
42
|
+
style:
|
|
43
|
+
name: Style
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
timeout-minutes: 10
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
- uses: pnpm/action-setup@v4
|
|
49
|
+
- uses: actions/setup-node@v4
|
|
50
|
+
with: { node-version: 24, cache: pnpm }
|
|
51
|
+
- run: pnpm install --frozen-lockfile
|
|
52
|
+
- run: pnpm style
|
|
53
|
+
|
|
54
|
+
typecheck:
|
|
55
|
+
name: Typecheck
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
timeout-minutes: 10
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
- uses: pnpm/action-setup@v4
|
|
61
|
+
- uses: actions/setup-node@v4
|
|
62
|
+
with: { node-version: 24, cache: pnpm }
|
|
63
|
+
- run: pnpm install --frozen-lockfile
|
|
64
|
+
- run: pnpm theme:build
|
|
65
|
+
- run: pnpm typecheck
|
|
66
|
+
|
|
67
|
+
test:
|
|
68
|
+
name: Test
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
timeout-minutes: 15
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
- uses: pnpm/action-setup@v4
|
|
74
|
+
- uses: actions/setup-node@v4
|
|
75
|
+
with: { node-version: 24, cache: pnpm }
|
|
76
|
+
- run: pnpm install --frozen-lockfile
|
|
77
|
+
- run: pnpm theme:build
|
|
78
|
+
- run: pnpm test
|
|
79
|
+
|
|
80
|
+
build:
|
|
81
|
+
name: Build
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
timeout-minutes: 15
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
- uses: pnpm/action-setup@v4
|
|
87
|
+
- uses: actions/setup-node@v4
|
|
88
|
+
with: { node-version: 24, cache: pnpm }
|
|
89
|
+
- run: pnpm install --frozen-lockfile
|
|
90
|
+
- run: pnpm build
|
|
91
|
+
|
|
92
|
+
storybook:
|
|
93
|
+
name: Storybook build
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
timeout-minutes: 15
|
|
96
|
+
if: hashFiles('.storybook/main.ts', '.storybook/main.js') != ''
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v4
|
|
99
|
+
- uses: pnpm/action-setup@v4
|
|
100
|
+
- uses: actions/setup-node@v4
|
|
101
|
+
with: { node-version: 24, cache: pnpm }
|
|
102
|
+
- run: pnpm install --frozen-lockfile
|
|
103
|
+
- run: pnpm theme:build
|
|
104
|
+
- run: pnpm storybook:build
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@raredays:registry=http://localhost:4873
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppLayout,
|
|
3
|
+
AppTopBar,
|
|
4
|
+
Sidebar,
|
|
5
|
+
SidebarContent,
|
|
6
|
+
SidebarGroup,
|
|
7
|
+
SidebarGroupLabel,
|
|
8
|
+
SidebarGroupContent,
|
|
9
|
+
SidebarMenu,
|
|
10
|
+
SidebarMenuItem,
|
|
11
|
+
SidebarMenuButton,
|
|
12
|
+
SidebarHeader,
|
|
13
|
+
} from "@raredays/components";
|
|
14
|
+
import { ShieldIcon } from "lucide-react";
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
|
|
17
|
+
export default function AppGroupLayout({ children }: { children: ReactNode }) {
|
|
18
|
+
return (
|
|
19
|
+
<AppLayout
|
|
20
|
+
sidebar={
|
|
21
|
+
<Sidebar>
|
|
22
|
+
<SidebarHeader>
|
|
23
|
+
<span className="px-2 text-lg font-bold">RareDays</span>
|
|
24
|
+
</SidebarHeader>
|
|
25
|
+
<SidebarContent>
|
|
26
|
+
<SidebarGroup>
|
|
27
|
+
<SidebarGroupLabel>App</SidebarGroupLabel>
|
|
28
|
+
<SidebarGroupContent>
|
|
29
|
+
<SidebarMenu>
|
|
30
|
+
<SidebarMenuItem>
|
|
31
|
+
<SidebarMenuButton asChild>
|
|
32
|
+
<a href="/protected">
|
|
33
|
+
<ShieldIcon />
|
|
34
|
+
<span>Protected</span>
|
|
35
|
+
</a>
|
|
36
|
+
</SidebarMenuButton>
|
|
37
|
+
</SidebarMenuItem>
|
|
38
|
+
</SidebarMenu>
|
|
39
|
+
</SidebarGroupContent>
|
|
40
|
+
</SidebarGroup>
|
|
41
|
+
</SidebarContent>
|
|
42
|
+
</Sidebar>
|
|
43
|
+
}
|
|
44
|
+
topbar={<AppTopBar />}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</AppLayout>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MarketingLayout, MarketingHeader, MarketingFooter } from "@raredays/components";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export default function MarketingGroupLayout({ children }: { children: ReactNode }) {
|
|
5
|
+
return (
|
|
6
|
+
<MarketingLayout
|
|
7
|
+
header={
|
|
8
|
+
<MarketingHeader
|
|
9
|
+
logo={<span className="text-lg font-bold">RareDays</span>}
|
|
10
|
+
navigation={[
|
|
11
|
+
{ label: "Features", href: "/#features" },
|
|
12
|
+
{ label: "Pricing", href: "/#pricing" },
|
|
13
|
+
]}
|
|
14
|
+
/>
|
|
15
|
+
}
|
|
16
|
+
footer={<MarketingFooter copyright={`© ${new Date().getFullYear()} RareDays`} />}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</MarketingLayout>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HomeAuthControls } from "../home-auth-controls";
|
|
2
|
+
|
|
3
|
+
export default function HomePage(): React.ReactElement {
|
|
4
|
+
return (
|
|
5
|
+
<main className="mx-auto max-w-2xl p-8 font-sans">
|
|
6
|
+
<h1 className="text-3xl font-bold">RareDays — BFF + Client</h1>
|
|
7
|
+
<p className="mt-4 text-neutral-600">
|
|
8
|
+
Next.js BFF that authenticates users and proxies API calls to an external backend. Identity
|
|
9
|
+
lives here; data lives in the backend. Configure <code>BACKEND_URL</code> in your env to
|
|
10
|
+
point at the upstream service.
|
|
11
|
+
</p>
|
|
12
|
+
<HomeAuthControls />
|
|
13
|
+
<h2 className="mt-10 text-lg font-semibold">Next steps</h2>
|
|
14
|
+
<ul className="mt-3 list-disc space-y-1 pl-5 text-sm text-neutral-700">
|
|
15
|
+
<li>
|
|
16
|
+
Edit <code>app/lib/backend-client.ts</code> to customise headers, retries, observability.
|
|
17
|
+
</li>
|
|
18
|
+
<li>
|
|
19
|
+
Edit <code>app/api/backend/[...path]/route.ts</code> if your backend needs different
|
|
20
|
+
request shaping or response filtering.
|
|
21
|
+
</li>
|
|
22
|
+
<li>
|
|
23
|
+
The proxy forwards <code>x-rd-user-id</code>, <code>x-rd-organization-id</code>, and{" "}
|
|
24
|
+
<code>x-rd-roles</code> headers — your backend reads these instead of resolving auth
|
|
25
|
+
itself.
|
|
26
|
+
</li>
|
|
27
|
+
<li>
|
|
28
|
+
Run <code>pnpm test</code> to execute the sample test that exercises the in-memory
|
|
29
|
+
identity provider.
|
|
30
|
+
</li>
|
|
31
|
+
</ul>
|
|
32
|
+
</main>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// BFF proxy. Any request the SPA makes to /api/backend/<path> is forwarded
|
|
2
|
+
// to the configured backend with the authenticated user's context attached.
|
|
3
|
+
//
|
|
4
|
+
// Customise: pull session via `app.require("identityProvider").getSession(req)`
|
|
5
|
+
// and inject headers (user id, roles, tenant) before forwarding. Treat the
|
|
6
|
+
// backend as the source of truth; this layer is just routing + identity.
|
|
7
|
+
|
|
8
|
+
import { proxyToBackend } from "@/lib/backend-client";
|
|
9
|
+
import { app } from "@/lib/runtime";
|
|
10
|
+
|
|
11
|
+
async function forward(req: Request, { params }: { params: Promise<{ path: string[] }> }) {
|
|
12
|
+
const { path } = await params;
|
|
13
|
+
const session = await app.require("identityProvider").getSession(req);
|
|
14
|
+
|
|
15
|
+
// Strip hop-by-hop headers + the host header (would point at the BFF, not
|
|
16
|
+
// the backend); pass everything else through.
|
|
17
|
+
const outboundHeaders = new Headers(req.headers);
|
|
18
|
+
outboundHeaders.delete("host");
|
|
19
|
+
outboundHeaders.delete("connection");
|
|
20
|
+
if (session) {
|
|
21
|
+
outboundHeaders.set("x-rd-user-id", session.user.id);
|
|
22
|
+
outboundHeaders.set("x-rd-organization-id", session.user.organizationId);
|
|
23
|
+
outboundHeaders.set("x-rd-roles", session.user.roles.join(","));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return proxyToBackend(path.join("/"), {
|
|
27
|
+
method: req.method,
|
|
28
|
+
headers: outboundHeaders,
|
|
29
|
+
body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const GET = forward;
|
|
34
|
+
export const POST = forward;
|
|
35
|
+
export const PUT = forward;
|
|
36
|
+
export const PATCH = forward;
|
|
37
|
+
export const DELETE = forward;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { SEEDED } from "@/lib/runtime";
|
|
3
|
+
|
|
4
|
+
// Dev-only: drop a cookie matching the seeded session. In production this is
|
|
5
|
+
// replaced by Better Auth's sign-in flow at /api/auth/*.
|
|
6
|
+
export async function POST(): Promise<NextResponse> {
|
|
7
|
+
const res = NextResponse.json({ ok: true, userId: SEEDED.user.id });
|
|
8
|
+
res.cookies.set("rd-dev-session", SEEDED.sessionId, {
|
|
9
|
+
httpOnly: true,
|
|
10
|
+
sameSite: "lax",
|
|
11
|
+
path: "/",
|
|
12
|
+
});
|
|
13
|
+
return res;
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
|
|
3
|
+
export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
4
|
+
// Progressive enhancement: an HTML form post (Accept: text/html) gets a
|
|
5
|
+
// 303 redirect back to /sign-in; a fetch() call (Accept: application/json)
|
|
6
|
+
// gets the original JSON response.
|
|
7
|
+
const wantsHtml = request.headers.get("accept")?.includes("text/html") ?? false;
|
|
8
|
+
|
|
9
|
+
if (wantsHtml) {
|
|
10
|
+
const res = NextResponse.redirect(new URL("/sign-in", request.url), 303);
|
|
11
|
+
res.cookies.delete("rd-dev-session");
|
|
12
|
+
return res;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const res = NextResponse.json({ ok: true });
|
|
16
|
+
res.cookies.delete("rd-dev-session");
|
|
17
|
+
return res;
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* CSS @import lines must precede all other rules. If you add a remote-font
|
|
2
|
+
* import (e.g. @import url("https://rsms.me/inter/inter.css")), it MUST go
|
|
3
|
+
* BEFORE the lines below — @raredays/style inlines ~6800 lines of CSS,
|
|
4
|
+
* after which any further @import is mid-document and rejected by Lightning
|
|
5
|
+
* CSS / PostCSS / browsers per the CSS spec. */
|
|
6
|
+
@import "@raredays/style";
|
|
7
|
+
@import "./theme.override.css";
|
|
8
|
+
|
|
9
|
+
/* Scan compiled output from @raredays/* packages so Tailwind emits
|
|
10
|
+
* the arbitrary utilities used inside their components (e.g. underline
|
|
11
|
+
* positions, data-attribute variants). The relative `@source` paths in
|
|
12
|
+
* @raredays/style/index.css don't resolve under pnpm's strict
|
|
13
|
+
* node_modules layout — each @raredays/* package gets its own
|
|
14
|
+
* .pnpm/<pkg>/node_modules/<pkg>/ directory, so style's
|
|
15
|
+
* `../components/dist` points nowhere. These directives are relative
|
|
16
|
+
* to THIS file (`app/globals.css`), which is the consumer's cwd —
|
|
17
|
+
* resolves correctly.
|
|
18
|
+
*
|
|
19
|
+
* Add `@source "../node_modules/@raredays/<pkg>/dist"` for any
|
|
20
|
+
* additional @raredays/* package whose component utilities you use. */
|
|
21
|
+
@source "../node_modules/@raredays/components/dist";
|
|
22
|
+
@source "../node_modules/@raredays/core/dist";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export function HomeAuthControls(): React.ReactElement {
|
|
4
|
+
return (
|
|
5
|
+
<div className="mt-6 flex flex-wrap gap-2">
|
|
6
|
+
<button
|
|
7
|
+
type="button"
|
|
8
|
+
onClick={async () => {
|
|
9
|
+
await fetch("/api/dev/login", { method: "POST" });
|
|
10
|
+
window.location.href = "/protected";
|
|
11
|
+
}}
|
|
12
|
+
className="rounded-md bg-brand-primary px-4 py-2 text-white hover:bg-brand-secondary"
|
|
13
|
+
>
|
|
14
|
+
Sign in as Alice → /protected
|
|
15
|
+
</button>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
onClick={async () => {
|
|
19
|
+
await fetch("/api/dev/logout", { method: "POST" });
|
|
20
|
+
window.location.reload();
|
|
21
|
+
}}
|
|
22
|
+
className="rounded-md border border-neutral-300 px-4 py-2 hover:bg-neutral-50"
|
|
23
|
+
>
|
|
24
|
+
Sign out
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ThemeProvider, Toaster } from "@raredays/components";
|
|
2
|
+
import { RaredaysProvider } from "@raredays/core";
|
|
3
|
+
import type { Metadata, Viewport } from "next";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { StartMocks } from "../mocks/browser";
|
|
6
|
+
import "./globals.css";
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: "RareDays App",
|
|
10
|
+
description: "A RareDays Next.js application.",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const viewport: Viewport = {
|
|
14
|
+
width: "device-width",
|
|
15
|
+
initialScale: 1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
19
|
+
return (
|
|
20
|
+
<html lang="en" suppressHydrationWarning>
|
|
21
|
+
<body>
|
|
22
|
+
<StartMocks>
|
|
23
|
+
<RaredaysProvider>
|
|
24
|
+
<ThemeProvider
|
|
25
|
+
attribute="class"
|
|
26
|
+
defaultTheme="system"
|
|
27
|
+
enableSystem
|
|
28
|
+
disableTransitionOnChange
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
<Toaster position="bottom-center" />
|
|
32
|
+
</ThemeProvider>
|
|
33
|
+
</RaredaysProvider>
|
|
34
|
+
</StartMocks>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// BFF → external backend wiring. Two surfaces, one upstream:
|
|
2
|
+
//
|
|
3
|
+
// • `backend` — typed JSON client (`backend.get<T>("/widgets")`). Use
|
|
4
|
+
// this from server components, route handlers, and any code that
|
|
5
|
+
// calls the backend with a typed response.
|
|
6
|
+
//
|
|
7
|
+
// • `proxyToBackend` — raw `Response` passthrough used by the
|
|
8
|
+
// `app/api/backend/[...path]/route.ts` proxy. Necessary because the
|
|
9
|
+
// proxy streams the body untouched; the typed client buffers it.
|
|
10
|
+
//
|
|
11
|
+
// Configure `BACKEND_URL` in your env. Tokens, retry/timeout, and auth
|
|
12
|
+
// header injection live in this file — that's the one place that talks
|
|
13
|
+
// to the upstream service.
|
|
14
|
+
|
|
15
|
+
import { createFetchHttpClient, type HttpClient } from "@raredays/core";
|
|
16
|
+
|
|
17
|
+
const BACKEND_URL = process.env.BACKEND_URL ?? "";
|
|
18
|
+
|
|
19
|
+
// Lazy-initialized: warn once, on first actual use of the backend client,
|
|
20
|
+
// rather than at module-import time. This keeps `next build` quiet for
|
|
21
|
+
// apps that don't (yet) use backend calls, while still surfacing a clear
|
|
22
|
+
// signal the first time a missing config bites.
|
|
23
|
+
let warnedMissingBackendUrl = false;
|
|
24
|
+
function warnIfMissingBackendUrl(): void {
|
|
25
|
+
if (warnedMissingBackendUrl) return;
|
|
26
|
+
if (BACKEND_URL) return;
|
|
27
|
+
if (process.env.NODE_ENV !== "production") return;
|
|
28
|
+
warnedMissingBackendUrl = true;
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.warn("[backend-client] BACKEND_URL is not set — backend calls will fail.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const backend: HttpClient = createFetchHttpClient({
|
|
34
|
+
// `baseUrl` is read at call time by the underlying client, so the lazy
|
|
35
|
+
// warning above is sufficient — no module-load side effect needed.
|
|
36
|
+
baseUrl: BACKEND_URL,
|
|
37
|
+
// Default retry: 2 attempts on 5xx/network errors with 100ms backoff.
|
|
38
|
+
// Tune per-call via `backend.get(path, { retry: { ... } })` when you need
|
|
39
|
+
// different semantics (e.g. webhooks shouldn't retry, batch jobs should).
|
|
40
|
+
retry: { maxAttempts: 2 },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Raw passthrough for the `[...path]` proxy route. Keeps the streaming
|
|
45
|
+
* semantics that `fetch` provides — the typed `backend` client buffers
|
|
46
|
+
* the body and isn't appropriate for proxying.
|
|
47
|
+
*/
|
|
48
|
+
export async function proxyToBackend(path: string, init: RequestInit): Promise<Response> {
|
|
49
|
+
warnIfMissingBackendUrl();
|
|
50
|
+
if (!BACKEND_URL) {
|
|
51
|
+
return new Response("BACKEND_URL not configured", { status: 502 });
|
|
52
|
+
}
|
|
53
|
+
const url = `${BACKEND_URL.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
54
|
+
return fetch(url, init);
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
// Runs in the Node environment (not happy-dom) because the WHATWG `Request`
|
|
3
|
+
// in happy-dom strips the `cookie` header on construction (browsers forbid
|
|
4
|
+
// setting it from script). Server-side identity tests need the real Node
|
|
5
|
+
// `Request`, which preserves arbitrary headers.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { app, SEEDED } from "./runtime.js";
|
|
9
|
+
|
|
10
|
+
// Sample test demonstrating how to test app code against the framework's
|
|
11
|
+
// port contracts. The composition root provides the wired ports; tests
|
|
12
|
+
// drive them directly. For deeper testing of your own custom adapters,
|
|
13
|
+
// import the framework's conformance harnesses (e.g.
|
|
14
|
+
// `identityProviderConformance` from `@raredays/identity/testing`) and
|
|
15
|
+
// pass your factory.
|
|
16
|
+
|
|
17
|
+
describe("identity flow (in-memory provider)", () => {
|
|
18
|
+
it("resolves the seeded session when the dev cookie is present", async () => {
|
|
19
|
+
const provider = app.require("identityProvider");
|
|
20
|
+
const req = new Request("https://example.com/", {
|
|
21
|
+
headers: { cookie: `rd-dev-session=${SEEDED.sessionId}` },
|
|
22
|
+
});
|
|
23
|
+
const session = await provider.getSession(req);
|
|
24
|
+
expect(session).not.toBeNull();
|
|
25
|
+
expect(session?.user.id).toBe(SEEDED.user.id);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns null when no session cookie is present", async () => {
|
|
29
|
+
const provider = app.require("identityProvider");
|
|
30
|
+
const session = await provider.getSession(new Request("https://example.com/"));
|
|
31
|
+
expect(session).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("seeded user has content.read but not content.manage", () => {
|
|
35
|
+
expect(SEEDED.user.hasPermission("content.read")).toBe(true);
|
|
36
|
+
expect(SEEDED.user.hasPermission("content.manage")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Composition root for the BFF layer. This Next.js app sits in front of
|
|
2
|
+
// your real backend (Rails, Django, Go, Java, etc.) and handles:
|
|
3
|
+
// - authentication / session resolution
|
|
4
|
+
// - request proxying with auth context attached (see app/api/backend/[...path])
|
|
5
|
+
// - any client-side state / SSR shell the SPA needs
|
|
6
|
+
//
|
|
7
|
+
// Business logic (billing, persistence, notifications) lives in the backend.
|
|
8
|
+
// Identity is the one port the BFF owns. Default uses the in-memory provider
|
|
9
|
+
// so `pnpm dev` runs out of the box; swap to `@raredays/identity-better-auth`
|
|
10
|
+
// in production (and mount its handler at `app/api/auth/[...all]/route.ts`).
|
|
11
|
+
|
|
12
|
+
import { createRaredaysApp, definePermissions, Session, User } from "@raredays/core";
|
|
13
|
+
import { createInMemoryProvider } from "@raredays/identity/testing";
|
|
14
|
+
// CRDA:imports
|
|
15
|
+
|
|
16
|
+
// — Authorization (what each role can do) ─────────────────────────────────
|
|
17
|
+
definePermissions({
|
|
18
|
+
rolePermissions: {
|
|
19
|
+
member: ["content.read"],
|
|
20
|
+
admin: ["content.read", "content.manage"],
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// — Seeded dev user (replace with Better Auth in production) ──────────────
|
|
25
|
+
const ALICE = User.from({
|
|
26
|
+
id: "u_alice",
|
|
27
|
+
email: "alice@example.com",
|
|
28
|
+
organizationId: "default",
|
|
29
|
+
roles: ["member"],
|
|
30
|
+
emailVerifiedAt: new Date("2026-01-01T00:00:00Z"),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ALICE_SESSION = Session.from({
|
|
34
|
+
id: "s_alice",
|
|
35
|
+
userId: ALICE.id,
|
|
36
|
+
user: ALICE.toJSON(),
|
|
37
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const identityProvider = createInMemoryProvider({
|
|
41
|
+
users: [ALICE],
|
|
42
|
+
sessions: [ALICE_SESSION],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const SEEDED = { user: ALICE, sessionId: ALICE_SESSION.id };
|
|
46
|
+
|
|
47
|
+
// CRDA:config
|
|
48
|
+
|
|
49
|
+
export const app = createRaredaysApp({
|
|
50
|
+
identityProvider,
|
|
51
|
+
// CRDA:ports
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// CRDA:exports
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
import { UnauthenticatedError } from "@raredays/core";
|
|
3
|
+
import { getServerSession } from "@raredays/identity/server";
|
|
4
|
+
import { app } from "@/lib/runtime";
|
|
5
|
+
|
|
6
|
+
export default async function ProtectedPage(): Promise<React.ReactElement> {
|
|
7
|
+
const session = await getServerSession(app);
|
|
8
|
+
if (!session) {
|
|
9
|
+
redirect("/");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Framework-owned authz: gate on a permission, not a role string.
|
|
14
|
+
session.user.requirePermission("content.read");
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (err instanceof UnauthenticatedError) redirect("/");
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<main data-testid="protected" className="p-8 font-sans">
|
|
22
|
+
<h1 className="text-2xl font-bold">Protected</h1>
|
|
23
|
+
<p className="mt-3" data-testid="signed-in-as">
|
|
24
|
+
Signed in as {session.user.email}
|
|
25
|
+
</p>
|
|
26
|
+
<p className="mt-1 text-sm text-neutral-600" data-testid="user-id">
|
|
27
|
+
User id: {session.user.id}
|
|
28
|
+
</p>
|
|
29
|
+
</main>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Next.js calls `register()` early in the server boot, before any route
|
|
2
|
+
// handler runs. The Node-runtime guard avoids starting MSW in edge
|
|
3
|
+
// runtimes where setupServer isn't compatible.
|
|
4
|
+
|
|
5
|
+
export async function register(): Promise<void> {
|
|
6
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
7
|
+
if (process.env.MOCKS !== "on") return;
|
|
8
|
+
const { mockServer } = await import("./mocks/node");
|
|
9
|
+
mockServer.start();
|
|
10
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Browser-side mock worker. Wraps the app so that no in-page fetch fires
|
|
4
|
+
// until the Service Worker has claimed the page — otherwise the very
|
|
5
|
+
// first request races the SW registration and slips through unmocked.
|
|
6
|
+
//
|
|
7
|
+
// When `NEXT_PUBLIC_MOCKS` is unset (production), the wrapper renders
|
|
8
|
+
// children synchronously with zero overhead. Requires
|
|
9
|
+
// `public/mockServiceWorker.js` — shipped by the scaffold and pinned
|
|
10
|
+
// to the same `msw` version as this template. Regenerate with
|
|
11
|
+
// `npx msw init public/ --save` only if you bump the `msw` dep.
|
|
12
|
+
|
|
13
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
14
|
+
import { enableMockWorker } from "@raredays/mocks/browser";
|
|
15
|
+
import { handlers } from "./handlers";
|
|
16
|
+
|
|
17
|
+
const ENABLED = process.env.NEXT_PUBLIC_MOCKS === "on";
|
|
18
|
+
const worker = ENABLED ? enableMockWorker(handlers) : null;
|
|
19
|
+
|
|
20
|
+
export function StartMocks({ children }: { children: ReactNode }): ReactNode {
|
|
21
|
+
const [ready, setReady] = useState(!ENABLED);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!worker) return;
|
|
24
|
+
void worker.start().then(() => setReady(true));
|
|
25
|
+
return () => worker.stop();
|
|
26
|
+
}, []);
|
|
27
|
+
return ready ? children : null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Mock composition root. Imported by `mocks/node.ts`, `mocks/browser.tsx`,
|
|
2
|
+
// and the vitest setup file — same handlers, three environments.
|
|
3
|
+
//
|
|
4
|
+
// In a client/BFF app, the most common pattern is intercepting fetches to
|
|
5
|
+
// the upstream backend (proxied through `/api/backend/*`). Add those
|
|
6
|
+
// handlers to the trailing array below.
|
|
7
|
+
|
|
8
|
+
import { defineMocks, type RequestHandler } from "@raredays/mocks";
|
|
9
|
+
// import { http, HttpResponse } from "msw";
|
|
10
|
+
// CRDA:mocks-imports
|
|
11
|
+
|
|
12
|
+
// Explicit annotation — TS2742 otherwise points at MSW's internal
|
|
13
|
+
// HttpResponse-*.mjs path which isn't portable across consumer apps.
|
|
14
|
+
export const handlers: RequestHandler[] = defineMocks(
|
|
15
|
+
// CRDA:mocks-spread
|
|
16
|
+
[
|
|
17
|
+
// App-local handlers, e.g.:
|
|
18
|
+
// http.get("/api/backend/products", () => HttpResponse.json({ products: [] })),
|
|
19
|
+
],
|
|
20
|
+
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Node-side mock server. Activated by `instrumentation.ts` when
|
|
2
|
+
// `MOCKS=on`, and by `vitest.setup.ts` during tests.
|
|
3
|
+
|
|
4
|
+
import { setupMockServer } from "@raredays/mocks/node";
|
|
5
|
+
import { handlers } from "./handlers";
|
|
6
|
+
|
|
7
|
+
export const mockServer = setupMockServer(handlers);
|