create-restack-pattern 0.1.7 → 0.2.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/dist/index.js +240 -67
- package/package.json +14 -8
- package/template/{AGENTS.md → base/DEVELOPMENT.md} +73 -62
- package/template/{backend → base/backend}/package.json +13 -10
- package/template/{backend → base/backend}/src/config/env.ts +2 -2
- package/template/{backend → base/backend}/src/domains/auth/auth.model.ts +5 -3
- package/template/{backend → base/backend}/src/domains/auth/auth.repo.ts +23 -5
- package/template/{backend → base/backend}/src/domains/auth/auth.routes.ts +77 -22
- package/template/{backend → base/backend}/src/domains/auth/auth.service.test.ts +51 -19
- package/template/{backend → base/backend}/src/domains/auth/auth.service.ts +15 -10
- package/template/{backend → base/backend}/src/domains/auth/auth.types.ts +1 -1
- package/template/{backend → base/backend}/src/domains/user/user.model.ts +2 -1
- package/template/{backend → base/backend}/src/domains/user/user.repo.ts +3 -2
- package/template/base/backend/src/domains/user/user.routes.test.ts +127 -0
- package/template/{backend → base/backend}/src/domains/user/user.routes.ts +19 -9
- package/template/{backend → base/backend}/src/domains/user/user.service.ts +8 -2
- package/template/{backend → base/backend}/src/hono-app.ts +6 -11
- package/template/base/backend/src/infra/middlewares/require-auth.test.ts +91 -0
- package/template/base/backend/src/infra/middlewares/require-auth.ts +63 -0
- package/template/base/backend/src/infra/middlewares/require-role.test.ts +66 -0
- package/template/base/backend/src/infra/middlewares/require-role.ts +34 -0
- package/template/{backend → base/backend}/src/utils/logger.ts +7 -10
- package/template/base/backend/src/utils/uuid.test.ts +37 -0
- package/template/{backend → base/backend}/tsconfig.json +4 -0
- package/template/base/frontend/AGENTS.md +11 -0
- package/template/{frontend → base/frontend}/app/(auth)/login/page.tsx +1 -1
- package/template/{frontend → base/frontend}/app/(auth)/register/page.tsx +1 -1
- package/template/{frontend → base/frontend}/app/globals.css +1 -1
- package/template/{frontend → base/frontend}/app/layout.tsx +9 -12
- package/template/{frontend → base/frontend}/app/page.tsx +2 -2
- package/template/{frontend → base/frontend}/app/profile/page.tsx +3 -3
- package/template/{frontend → base/frontend}/components/require-auth.tsx +2 -2
- package/template/{frontend → base/frontend}/components/ui/button.tsx +2 -2
- package/template/{frontend → base/frontend}/components/ui/card.tsx +8 -23
- package/template/{frontend → base/frontend}/components/ui/input.tsx +2 -2
- package/template/{frontend → base/frontend}/components/ui/label.tsx +3 -6
- package/template/{frontend → base/frontend}/domains/auth/components/login-form.tsx +10 -3
- package/template/{frontend → base/frontend}/domains/auth/components/logout-button.tsx +2 -2
- package/template/{frontend → base/frontend}/domains/auth/components/register-form.tsx +10 -3
- package/template/{frontend → base/frontend}/domains/auth/hooks/use-login.ts +2 -2
- package/template/{frontend → base/frontend}/domains/auth/hooks/use-register.ts +6 -3
- package/template/{frontend → base/frontend}/domains/auth/services/auth.api.ts +5 -7
- package/template/{frontend → base/frontend}/domains/user/components/profile-card.tsx +2 -4
- package/template/{frontend → base/frontend}/domains/user/services/user.api.ts +2 -3
- package/template/base/frontend/env.ts +12 -0
- package/template/{frontend → base/frontend}/hooks/use-auth-mutation.ts +6 -3
- package/template/{frontend → base/frontend}/lib/api-client.ts +40 -5
- package/template/{frontend → base/frontend}/lib/utils.ts +1 -1
- package/template/base/frontend/next.config.ts +11 -0
- package/template/{frontend → base/frontend}/package.json +15 -14
- package/template/{frontend → base/frontend}/tsconfig.json +4 -0
- package/template/{gitignore → base/gitignore} +16 -2
- package/template/base/package.json +25 -0
- package/template/{packages → base/packages}/shared/package.json +1 -1
- package/template/{packages → base/packages}/shared/src/index.ts +1 -0
- package/template/{packages → base/packages}/shared/src/schemas/auth.schema.ts +8 -0
- package/template/{packages → base/packages}/shared/src/schemas/user.schema.ts +1 -0
- package/template/base/packages/shared/src/utils/uuid.ts +40 -0
- package/template/{packages → base/packages}/shared/tsconfig.json +26 -0
- package/template/base/pnpm-workspace.yaml +12 -0
- package/template/overlays/db-postgres/backend/.env.example +6 -0
- package/template/overlays/db-postgres/backend/drizzle.config.ts +11 -0
- package/template/overlays/db-postgres/backend/scripts/db-check.js +27 -0
- package/template/overlays/db-postgres/backend/src/infra/db/index.ts +6 -0
- package/template/overlays/db-sqlite/backend/.env.example +6 -0
- package/template/overlays/db-sqlite/backend/drizzle.config.ts +11 -0
- package/template/overlays/db-sqlite/backend/scripts/db-check.js +20 -0
- package/template/overlays/db-sqlite/backend/src/infra/db/index.ts +8 -0
- package/template/overlays/id-serial/packages/shared/src/schemas/user.schema.ts +14 -0
- package/template/overlays/id-uuid/packages/shared/src/schemas/user.schema.ts +14 -0
- package/template/overlays/models/postgres-serial/backend/src/domains/auth/auth.model.ts +12 -0
- package/template/overlays/models/postgres-serial/backend/src/domains/user/user.model.ts +10 -0
- package/template/overlays/models/postgres-uuid/backend/src/domains/auth/auth.model.ts +15 -0
- package/template/overlays/models/postgres-uuid/backend/src/domains/user/user.model.ts +13 -0
- package/template/overlays/models/sqlite-serial/backend/src/domains/auth/auth.model.ts +14 -0
- package/template/overlays/models/sqlite-serial/backend/src/domains/user/user.model.ts +12 -0
- package/template/overlays/models/sqlite-uuid/backend/src/domains/auth/auth.model.ts +17 -0
- package/template/overlays/models/sqlite-uuid/backend/src/domains/user/user.model.ts +15 -0
- package/template/CLAUDE.md +0 -62
- package/template/backend/src/domains/user/user.routes.test.ts +0 -82
- package/template/backend/src/infra/middlewares/require-auth.test.ts +0 -51
- package/template/backend/src/infra/middlewares/require-auth.ts +0 -36
- package/template/frontend/AGENTS.md +0 -5
- package/template/frontend/next.config.ts +0 -7
- package/template/package.json +0 -17
- package/template/pnpm-workspace.yaml +0 -4
- package/template/{backend → base/backend}/.env.example +0 -0
- package/template/{backend → base/backend}/README.md +0 -0
- package/template/{backend → base/backend}/api/index.ts +0 -0
- package/template/{backend → base/backend}/drizzle/0000_grey_lethal_legion.sql +0 -0
- package/template/{backend → base/backend}/drizzle/meta/0000_snapshot.json +0 -0
- package/template/{backend → base/backend}/drizzle/meta/_journal.json +0 -0
- package/template/{backend → base/backend}/drizzle.config.ts +0 -0
- package/template/{backend → base/backend}/gitignore +0 -0
- package/template/{backend → base/backend}/public/.gitkeep +0 -0
- package/template/{backend → base/backend}/scripts/db-check.js +0 -0
- package/template/{backend → base/backend}/src/config/.gitkeep +0 -0
- package/template/{backend → base/backend}/src/domains/auth/.gitkeep +0 -0
- package/template/{backend → base/backend}/src/domains/user/.gitkeep +0 -0
- package/template/{backend → base/backend}/src/index.ts +1 -1
- package/template/{backend → base/backend}/src/infra/db/.gitkeep +0 -0
- package/template/{backend → base/backend}/src/infra/db/index.ts +0 -0
- package/template/{backend → base/backend}/src/infra/middlewares/.gitkeep +0 -0
- package/template/{backend → base/backend}/vercel.json +0 -0
- package/template/{frontend → base/frontend}/.env.example +0 -0
- package/template/{frontend → base/frontend}/CLAUDE.md +0 -0
- package/template/{frontend → base/frontend}/README.md +0 -0
- package/template/{frontend → base/frontend}/app/dashboard/page.tsx +2 -2
- package/template/{frontend → base/frontend}/app/icon.png +0 -0
- package/template/{frontend → base/frontend}/components.json +0 -0
- package/template/{frontend → base/frontend}/domains/auth/components/.gitkeep +0 -0
- package/template/{frontend → base/frontend}/domains/auth/hooks/.gitkeep +0 -0
- package/template/{frontend → base/frontend}/domains/auth/services/.gitkeep +0 -0
- package/template/{frontend → base/frontend}/domains/user/components/.gitkeep +0 -0
- package/template/{frontend → base/frontend}/domains/user/hooks/use-profile.ts +2 -2
- /package/template/{frontend → base/frontend}/eslint.config.mjs +0 -0
- /package/template/{frontend → base/frontend}/gitignore +0 -0
- /package/template/{frontend → base/frontend}/postcss.config.mjs +0 -0
- /package/template/{frontend → base/frontend}/public/favicon.ico +0 -0
- /package/template/{frontend → base/frontend}/public/logo.png +0 -0
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState } from "react"
|
|
4
3
|
import Link from "next/link"
|
|
4
|
+
import { useState } from "react"
|
|
5
5
|
import { Button } from "@/components/ui/button"
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardFooter,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from "@/components/ui/card"
|
|
6
14
|
import { Input } from "@/components/ui/input"
|
|
7
15
|
import { Label } from "@/components/ui/label"
|
|
8
|
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
|
9
16
|
import { useLogin } from "../hooks/use-login"
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
19
|
* LoginForm UI Component (Auth Domain).
|
|
13
20
|
* Handles rendering the login form and calling the `useLogin` hook.
|
|
14
|
-
*
|
|
21
|
+
*
|
|
15
22
|
* Semi-DDD rules:
|
|
16
23
|
* - UI components should stay isolated from direct backend API fetching logic.
|
|
17
24
|
* - Local react state controls input values, and mutation runs through the domain hook.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState } from "react"
|
|
4
3
|
import { useRouter } from "next/navigation"
|
|
4
|
+
import { useState } from "react"
|
|
5
5
|
import { Button } from "@/components/ui/button"
|
|
6
6
|
import { authApi } from "../services/auth.api"
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* LogoutButton Component (Auth Domain).
|
|
10
10
|
* Simple action component to trigger sessions termination and redirect.
|
|
11
|
-
*
|
|
11
|
+
*
|
|
12
12
|
* Semi-DDD rules:
|
|
13
13
|
* - Simple action triggers can invoke services directly if no reactive local states/business orchestration is required.
|
|
14
14
|
*/
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState } from "react"
|
|
4
3
|
import Link from "next/link"
|
|
4
|
+
import { useState } from "react"
|
|
5
5
|
import { Button } from "@/components/ui/button"
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardFooter,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from "@/components/ui/card"
|
|
6
14
|
import { Input } from "@/components/ui/input"
|
|
7
15
|
import { Label } from "@/components/ui/label"
|
|
8
|
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
|
9
16
|
import { useRegister } from "../hooks/use-register"
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
19
|
* RegisterForm UI Component (Auth Domain).
|
|
13
20
|
* Handles rendering the register form and calling the `useRegister` hook.
|
|
14
|
-
*
|
|
21
|
+
*
|
|
15
22
|
* Semi-DDD rules:
|
|
16
23
|
* - UI components should stay isolated from direct backend API fetching logic.
|
|
17
24
|
* - Local react state controls input values, and mutation runs through the domain hook.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type LoginInput, LoginSchema } from "@restack/shared"
|
|
4
4
|
import { useAuthMutation } from "@/hooks/use-auth-mutation"
|
|
5
5
|
import { authApi } from "../services/auth.api"
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Domain-specific hook for Login (Auth Domain).
|
|
9
9
|
* Orchestrates login form mutation by wrapping the reusable useAuthMutation hook.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* Semi-DDD rules:
|
|
12
12
|
* - Domain hooks hide specific Zod validation schemas, API calls, and loading states from components.
|
|
13
13
|
*/
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type RegisterInput, RegisterSchema } from "@restack/shared"
|
|
4
4
|
import { useAuthMutation } from "@/hooks/use-auth-mutation"
|
|
5
5
|
import { authApi } from "../services/auth.api"
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Domain-specific hook for Registration (Auth Domain).
|
|
9
9
|
* Orchestrates registration form mutation by wrapping the reusable useAuthMutation hook.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* Semi-DDD rules:
|
|
12
12
|
* - Domain hooks hide specific Zod validation schemas, API calls, and loading states from components.
|
|
13
13
|
*/
|
|
14
14
|
export function useRegister() {
|
|
15
|
-
const { submit, error, isPending } = useAuthMutation<RegisterInput>(
|
|
15
|
+
const { submit, error, isPending } = useAuthMutation<RegisterInput>(
|
|
16
|
+
RegisterSchema,
|
|
17
|
+
authApi.register,
|
|
18
|
+
)
|
|
16
19
|
return { register: submit, error, isPending }
|
|
17
20
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AuthResponse, LoginInput, RegisterInput } from "@restack/shared"
|
|
2
2
|
import { client, unwrap } from "@/lib/api-client"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Service API layer for the Auth domain.
|
|
6
6
|
* Thin wrappers that perform API calls to the Hono backend endpoints.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* Semi-DDD rules:
|
|
9
9
|
* - Domain services (`domains/[feature]/services/*.api.ts`) handle only raw HTTP calls.
|
|
10
10
|
* - Request and response structures are mapped using schemas shared from `@restack/shared`.
|
|
11
11
|
*/
|
|
12
12
|
export const authApi = {
|
|
13
13
|
register: (data: RegisterInput) =>
|
|
14
|
-
unwrap<
|
|
14
|
+
unwrap<AuthResponse>(client.api.auth.register.$post({ json: data })),
|
|
15
15
|
|
|
16
|
-
login: (data: LoginInput) =>
|
|
17
|
-
unwrap<{ user: AuthUser }>(client.api.auth.login.$post({ json: data })),
|
|
16
|
+
login: (data: LoginInput) => unwrap<AuthResponse>(client.api.auth.login.$post({ json: data })),
|
|
18
17
|
|
|
19
|
-
logout: () =>
|
|
20
|
-
unwrap<{ success: boolean }>(client.api.auth.logout.$post()),
|
|
18
|
+
logout: () => unwrap<{ success: boolean }>(client.api.auth.logout.$post()),
|
|
21
19
|
}
|
|
@@ -6,7 +6,7 @@ import { useProfile } from "../hooks/use-profile"
|
|
|
6
6
|
/**
|
|
7
7
|
* ProfileCard UI component.
|
|
8
8
|
* Displays the loaded user profile information in a card layout.
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* Semi-DDD rules:
|
|
11
11
|
* - Domain components (`domains/[feature]/components/*`) are responsible for UI layout and rendering.
|
|
12
12
|
* - They should never perform direct HTTP calls or manage global state; instead, they consume data
|
|
@@ -25,9 +25,7 @@ export function ProfileCard() {
|
|
|
25
25
|
<CardTitle>{profile.name}</CardTitle>
|
|
26
26
|
<CardDescription>{profile.email}</CardDescription>
|
|
27
27
|
</CardHeader>
|
|
28
|
-
<CardContent className="text-xs text-muted-foreground">
|
|
29
|
-
User ID: {profile.id}
|
|
30
|
-
</CardContent>
|
|
28
|
+
<CardContent className="text-xs text-muted-foreground">User ID: {profile.id}</CardContent>
|
|
31
29
|
</Card>
|
|
32
30
|
)
|
|
33
31
|
}
|
|
@@ -4,15 +4,14 @@ import { client, unwrap } from "@/lib/api-client"
|
|
|
4
4
|
/**
|
|
5
5
|
* Service API layer for the User domain.
|
|
6
6
|
* Thin wrappers that perform API calls to the Hono backend endpoints.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* Semi-DDD rules:
|
|
9
9
|
* - Domain services (`domains/[feature]/services/*.api.ts`) handle only raw HTTP calls.
|
|
10
10
|
* - They must not contain UI state or react state; delegate that strictly to domain hooks.
|
|
11
11
|
* - Request and response structures are mapped using schemas shared from `@restack/shared`.
|
|
12
12
|
*/
|
|
13
13
|
export const userApi = {
|
|
14
|
-
me: () =>
|
|
15
|
-
unwrap<{ user: AuthUser }>(client.api.user.me.$get()),
|
|
14
|
+
me: () => unwrap<{ user: AuthUser }>(client.api.user.me.$get()),
|
|
16
15
|
|
|
17
16
|
updateProfile: (data: UpdateProfileInput) =>
|
|
18
17
|
unwrap<{ user: AuthUser }>(client.api.user.profile.$put({ json: data })),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createEnv } from "@t3-oss/env-nextjs"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
server: {},
|
|
6
|
+
client: {
|
|
7
|
+
NEXT_PUBLIC_API_URL: z.string().url().default("http://localhost:3001"),
|
|
8
|
+
},
|
|
9
|
+
experimental__runtimeEnv: {
|
|
10
|
+
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
|
11
|
+
},
|
|
12
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState } from "react"
|
|
4
3
|
import { useRouter } from "next/navigation"
|
|
4
|
+
import { useState } from "react"
|
|
5
5
|
import type { ZodType } from "zod"
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -9,7 +9,10 @@ import type { ZodType } from "zod"
|
|
|
9
9
|
* Standardizes Zod input validation, pending state tracking, error handling,
|
|
10
10
|
* and automatic client-side redirection on successful mutation.
|
|
11
11
|
*/
|
|
12
|
-
export function useAuthMutation<TInput>(
|
|
12
|
+
export function useAuthMutation<TInput>(
|
|
13
|
+
schema: ZodType<TInput>,
|
|
14
|
+
mutate: (data: TInput) => Promise<unknown>,
|
|
15
|
+
) {
|
|
13
16
|
const router = useRouter()
|
|
14
17
|
const [error, setError] = useState<string | null>(null)
|
|
15
18
|
const [isPending, setIsPending] = useState(false)
|
|
@@ -17,7 +20,7 @@ export function useAuthMutation<TInput>(schema: ZodType<TInput>, mutate: (data:
|
|
|
17
20
|
const submit = async (data: TInput) => {
|
|
18
21
|
const parsed = schema.safeParse(data)
|
|
19
22
|
if (!parsed.success) {
|
|
20
|
-
setError(parsed.error.issues[0]
|
|
23
|
+
setError(parsed.error.issues[0]?.message ?? "Validation failed")
|
|
21
24
|
return
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
import { hc } from "hono/client"
|
|
2
|
+
import { env } from "@/env"
|
|
2
3
|
import type { AppType } from "../../backend/src/hono-app"
|
|
3
4
|
|
|
4
|
-
const BASE_URL =
|
|
5
|
+
const BASE_URL = env.NEXT_PUBLIC_API_URL
|
|
5
6
|
|
|
6
7
|
export class ApiError extends Error {
|
|
7
|
-
constructor(
|
|
8
|
+
constructor(
|
|
9
|
+
message: string,
|
|
10
|
+
public status: number,
|
|
11
|
+
) {
|
|
8
12
|
super(message)
|
|
9
13
|
}
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
// In-memory Bearer token storage (for mobile clients, native apps, or Bearer auth mode)
|
|
17
|
+
let bearerToken: string | null = null
|
|
18
|
+
|
|
19
|
+
export const setAuthToken = (token: string | null) => {
|
|
20
|
+
bearerToken = token
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getAuthToken = () => bearerToken
|
|
24
|
+
|
|
12
25
|
// Share a single refresh request promise to prevent TOCTOU race conditions
|
|
13
26
|
let refreshPromise: Promise<Response> | null = null
|
|
14
27
|
|
|
15
28
|
function refreshSession() {
|
|
16
29
|
if (!refreshPromise) {
|
|
30
|
+
const headers: Record<string, string> = {}
|
|
31
|
+
if (bearerToken) {
|
|
32
|
+
headers.Authorization = `Bearer ${bearerToken}`
|
|
33
|
+
}
|
|
17
34
|
refreshPromise = fetch(`${BASE_URL}/api/auth/refresh`, {
|
|
18
35
|
method: "POST",
|
|
19
36
|
credentials: "include",
|
|
37
|
+
headers,
|
|
20
38
|
}).finally(() => {
|
|
21
39
|
refreshPromise = null
|
|
22
40
|
})
|
|
@@ -25,22 +43,39 @@ function refreshSession() {
|
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
const customFetch: typeof fetch = async (input, init) => {
|
|
28
|
-
const
|
|
46
|
+
const headers = new Headers(init?.headers)
|
|
47
|
+
|
|
48
|
+
if (bearerToken && !headers.has("Authorization")) {
|
|
49
|
+
headers.set("Authorization", `Bearer ${bearerToken}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const newInit: RequestInit = {
|
|
29
53
|
...init,
|
|
30
54
|
credentials: "include" as const,
|
|
55
|
+
headers,
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
const res = await fetch(input, newInit)
|
|
34
59
|
|
|
35
60
|
// Silently rotate access token on 401, unless it's the auth endpoints themselves
|
|
36
|
-
const url =
|
|
61
|
+
const url =
|
|
62
|
+
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
|
|
37
63
|
const isAuthRoute = url.includes("/api/auth/")
|
|
38
64
|
|
|
39
65
|
if (res.status === 401 && !isAuthRoute) {
|
|
40
66
|
const refreshRes = await refreshSession()
|
|
41
67
|
if (refreshRes.ok) {
|
|
68
|
+
try {
|
|
69
|
+
const data = (await refreshRes.clone().json()) as { accessToken?: string }
|
|
70
|
+
if (data.accessToken && bearerToken) {
|
|
71
|
+
bearerToken = data.accessToken
|
|
72
|
+
headers.set("Authorization", `Bearer ${bearerToken}`)
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore JSON parse error
|
|
76
|
+
}
|
|
42
77
|
// Retry the original request
|
|
43
|
-
return fetch(input, newInit)
|
|
78
|
+
return fetch(input, { ...newInit, headers })
|
|
44
79
|
}
|
|
45
80
|
}
|
|
46
81
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
// Tell Next.js to compile and bundle the @restack/shared package as if it were part of the frontend's own source code
|
|
5
|
+
transpilePackages: ["@restack/shared"],
|
|
6
|
+
// Enables static route typing for Next JS App Router navigation
|
|
7
|
+
// when use <Link> or router.push(), the editor will check or autocomplete valid routes and catch typos at compile time.
|
|
8
|
+
typedRoutes: true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default nextConfig;
|
|
@@ -10,27 +10,28 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@restack/shared": "workspace:*",
|
|
13
|
+
"@t3-oss/env-nextjs": "^0.13.11",
|
|
13
14
|
"class-variance-authority": "^0.7.1",
|
|
14
15
|
"clsx": "^2.1.1",
|
|
15
|
-
"hono": "^4.
|
|
16
|
-
"next": "16.2
|
|
17
|
-
"radix-ui": "^1.
|
|
18
|
-
"react": "19.
|
|
19
|
-
"react-dom": "19.
|
|
20
|
-
"shadcn": "^4.
|
|
21
|
-
"sharp": "^0.35.
|
|
22
|
-
"tailwind-merge": "^3.
|
|
16
|
+
"hono": "^4.13.13",
|
|
17
|
+
"next": "16.4.2",
|
|
18
|
+
"radix-ui": "^1.8.1",
|
|
19
|
+
"react": "19.3.1",
|
|
20
|
+
"react-dom": "19.3.1",
|
|
21
|
+
"shadcn": "^4.21.5",
|
|
22
|
+
"sharp": "^0.35.5",
|
|
23
|
+
"tailwind-merge": "^3.7.0",
|
|
23
24
|
"tw-animate-css": "^1.4.0",
|
|
24
|
-
"zod": "^4.
|
|
25
|
+
"zod": "^4.6.7"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"@tailwindcss/postcss": "^4",
|
|
28
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
28
29
|
"@types/node": "^20",
|
|
29
|
-
"@types/react": "^19",
|
|
30
|
-
"@types/react-dom": "^19",
|
|
30
|
+
"@types/react": "^19.3.0",
|
|
31
|
+
"@types/react-dom": "^19.3.0",
|
|
31
32
|
"eslint": "^9",
|
|
32
|
-
"eslint-config-next": "16.2
|
|
33
|
-
"tailwindcss": "^4",
|
|
33
|
+
"eslint-config-next": "16.4.2",
|
|
34
|
+
"tailwindcss": "^4.3.3",
|
|
34
35
|
"typescript": "^5"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
"allowJs": true,
|
|
6
6
|
"skipLibCheck": true,
|
|
7
7
|
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"exactOptionalPropertyTypes": true,
|
|
10
|
+
"noImplicitReturns": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
8
12
|
"noEmit": true,
|
|
9
13
|
"esModuleInterop": true,
|
|
10
14
|
"module": "esnext",
|
|
@@ -36,8 +36,22 @@ Thumbs.db
|
|
|
36
36
|
test-project/
|
|
37
37
|
test-app/
|
|
38
38
|
|
|
39
|
-
# Exclude concerns
|
|
39
|
+
# Exclude concerns from git
|
|
40
40
|
docs/concerns.md
|
|
41
|
-
CLAUDE.md
|
|
42
41
|
|
|
43
42
|
docs/tutorial.md
|
|
43
|
+
|
|
44
|
+
# Graphify knowledge graph outputs
|
|
45
|
+
graphify-out/
|
|
46
|
+
.graphify*
|
|
47
|
+
|
|
48
|
+
node_modules/
|
|
49
|
+
.e2e/results/
|
|
50
|
+
.e2e/cache/
|
|
51
|
+
.e2e/sessions/
|
|
52
|
+
.e2e/report.json
|
|
53
|
+
.e2e/ai-trace.json
|
|
54
|
+
.e2e/junit.xml
|
|
55
|
+
.e2e/summary.md
|
|
56
|
+
.e2e/logs/
|
|
57
|
+
.e2e/videos/
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "restack-pattern",
|
|
3
|
+
"private": true,
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "pnpm --parallel dev",
|
|
7
|
+
"build": "pnpm --parallel build",
|
|
8
|
+
"lint": "biome check .",
|
|
9
|
+
"format": "biome format --write .",
|
|
10
|
+
"test:e2e": "e2e run"
|
|
11
|
+
},
|
|
12
|
+
"workspaces": [
|
|
13
|
+
"frontend",
|
|
14
|
+
"backend",
|
|
15
|
+
"packages/*"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@biomejs/biome": "^2.5.15",
|
|
22
|
+
"@e2e-dev/web": "^0.15.2",
|
|
23
|
+
"e2e": "^0.21.2"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
|
+
import type { AuthUser } from "./user.schema.js"
|
|
2
3
|
|
|
3
4
|
/** Zod validation schema for login form and request payload. */
|
|
4
5
|
export const LoginSchema = z.object({
|
|
@@ -18,3 +19,10 @@ export type LoginInput = z.infer<typeof LoginSchema>
|
|
|
18
19
|
|
|
19
20
|
/** TypeScript input type inferred from RegisterSchema. */
|
|
20
21
|
export type RegisterInput = z.infer<typeof RegisterSchema>
|
|
22
|
+
|
|
23
|
+
/** Auth response payload returned on register, login, and token rotation. */
|
|
24
|
+
export interface AuthResponse {
|
|
25
|
+
user: AuthUser
|
|
26
|
+
accessToken: string
|
|
27
|
+
refreshToken: string
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency RFC 9562 compliant UUIDv7 generator.
|
|
3
|
+
*
|
|
4
|
+
* UUIDv7 combines a 48-bit UNIX timestamp (milliseconds) with 74 bits of
|
|
5
|
+
* cryptographically secure randomness. Unlike UUIDv4, UUIDv7 is strictly
|
|
6
|
+
* time-ordered, making it ideal for primary keys in B-Tree database indexes
|
|
7
|
+
* while avoiding URL enumeration vulnerabilities of auto-incrementing serial IDs.
|
|
8
|
+
*
|
|
9
|
+
* Compatible with Node.js, browsers, and Edge runtimes via globalThis.crypto.
|
|
10
|
+
*/
|
|
11
|
+
export function uuidv7(): string {
|
|
12
|
+
const bytes = new Uint8Array(16)
|
|
13
|
+
globalThis.crypto.getRandomValues(bytes)
|
|
14
|
+
const now = Date.now()
|
|
15
|
+
|
|
16
|
+
// 48-bit timestamp (milliseconds since epoch) in big-endian
|
|
17
|
+
bytes[0] = (now / 0x1_00_00_00_00_00) & 0xff
|
|
18
|
+
bytes[1] = (now / 0x1_00_00_00_00) & 0xff
|
|
19
|
+
bytes[2] = (now / 0x1_00_00_00) & 0xff
|
|
20
|
+
bytes[3] = (now / 0x1_00_00) & 0xff
|
|
21
|
+
bytes[4] = (now / 0x1_00) & 0xff
|
|
22
|
+
bytes[5] = now & 0xff
|
|
23
|
+
|
|
24
|
+
// Version 7: set 4 bits (0111) in byte 6
|
|
25
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x70
|
|
26
|
+
|
|
27
|
+
// Variant 1 (RFC 9562): set 2 bits (10) in byte 8
|
|
28
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80
|
|
29
|
+
|
|
30
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")
|
|
31
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validates whether a given string is a valid UUIDv7 format.
|
|
36
|
+
*/
|
|
37
|
+
export function isUuidv7(uuid: string): boolean {
|
|
38
|
+
const uuidv7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
39
|
+
return uuidv7Regex.test(uuid)
|
|
40
|
+
}
|
|
@@ -28,6 +28,32 @@
|
|
|
28
28
|
* @description Enables all strict type-checking options to prevent common runtime bugs.
|
|
29
29
|
*/
|
|
30
30
|
"strict": true,
|
|
31
|
+
/**
|
|
32
|
+
* @noUncheckedIndexedAccess true
|
|
33
|
+
* @description Adds 'undefined' to any un-declared index access (e.g. arr[i] or record[key]),
|
|
34
|
+
* forcing explicit null/undefined checks and preventing runtime crashes.
|
|
35
|
+
*/
|
|
36
|
+
"noUncheckedIndexedAccess": true,
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @exactOptionalPropertyTypes true
|
|
40
|
+
* @description Differentiates between omitted optional properties and properties explicitly set to 'undefined',
|
|
41
|
+
* preventing unintended database column mutations and serialization bugs.
|
|
42
|
+
*/
|
|
43
|
+
"exactOptionalPropertyTypes": true,
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @noImplicitReturns true
|
|
47
|
+
* @description Ensures all execution branches in a function explicitly return a value,
|
|
48
|
+
* preventing accidental implicit 'undefined' returns.
|
|
49
|
+
*/
|
|
50
|
+
"noImplicitReturns": true,
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @noFallthroughCasesInSwitch true
|
|
54
|
+
* @description Reports an error for fallthrough cases in switch statements that are missing a 'break' or 'return'.
|
|
55
|
+
*/
|
|
56
|
+
"noFallthroughCasesInSwitch": true,
|
|
31
57
|
|
|
32
58
|
/**
|
|
33
59
|
* @skipLibCheck true
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import postgres from "postgres"
|
|
2
|
+
import dotenv from "dotenv"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { fileURLToPath } from "url"
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
dotenv.config({ path: path.resolve(__dirname, "../.env") })
|
|
8
|
+
|
|
9
|
+
const dbUrl = process.env.DATABASE_URL
|
|
10
|
+
if (!dbUrl) {
|
|
11
|
+
console.error("\x1b[31m[ERROR] DATABASE_URL is not set in backend/.env\x1b[0m")
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log("\x1b[36mChecking PostgreSQL connection...\x1b[0m")
|
|
16
|
+
const sql = postgres(dbUrl, { max: 1, connect_timeout: 5 })
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await sql`SELECT 1`
|
|
20
|
+
console.log("\x1b[32m✔ PostgreSQL connection successful!\x1b[0m")
|
|
21
|
+
await sql.end()
|
|
22
|
+
process.exit(0)
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error("\x1b[31m[ERROR] Database connection failed!\x1b[0m", err.message)
|
|
25
|
+
await sql.end()
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit"
|
|
2
|
+
import "dotenv/config"
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
schema: "./src/domains/*/*.model.ts",
|
|
6
|
+
out: "./drizzle",
|
|
7
|
+
dialect: "sqlite",
|
|
8
|
+
dbCredentials: {
|
|
9
|
+
url: process.env.DATABASE_URL || "sqlite.db",
|
|
10
|
+
},
|
|
11
|
+
})
|