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.
Files changed (120) hide show
  1. package/dist/index.js +240 -67
  2. package/package.json +14 -8
  3. package/template/{AGENTS.md → base/DEVELOPMENT.md} +73 -62
  4. package/template/{backend → base/backend}/package.json +13 -10
  5. package/template/{backend → base/backend}/src/config/env.ts +2 -2
  6. package/template/{backend → base/backend}/src/domains/auth/auth.model.ts +5 -3
  7. package/template/{backend → base/backend}/src/domains/auth/auth.repo.ts +23 -5
  8. package/template/{backend → base/backend}/src/domains/auth/auth.routes.ts +77 -22
  9. package/template/{backend → base/backend}/src/domains/auth/auth.service.test.ts +51 -19
  10. package/template/{backend → base/backend}/src/domains/auth/auth.service.ts +15 -10
  11. package/template/{backend → base/backend}/src/domains/auth/auth.types.ts +1 -1
  12. package/template/{backend → base/backend}/src/domains/user/user.model.ts +2 -1
  13. package/template/{backend → base/backend}/src/domains/user/user.repo.ts +3 -2
  14. package/template/base/backend/src/domains/user/user.routes.test.ts +127 -0
  15. package/template/{backend → base/backend}/src/domains/user/user.routes.ts +19 -9
  16. package/template/{backend → base/backend}/src/domains/user/user.service.ts +8 -2
  17. package/template/{backend → base/backend}/src/hono-app.ts +6 -11
  18. package/template/base/backend/src/infra/middlewares/require-auth.test.ts +91 -0
  19. package/template/base/backend/src/infra/middlewares/require-auth.ts +63 -0
  20. package/template/base/backend/src/infra/middlewares/require-role.test.ts +66 -0
  21. package/template/base/backend/src/infra/middlewares/require-role.ts +34 -0
  22. package/template/{backend → base/backend}/src/utils/logger.ts +7 -10
  23. package/template/base/backend/src/utils/uuid.test.ts +37 -0
  24. package/template/{backend → base/backend}/tsconfig.json +4 -0
  25. package/template/base/frontend/AGENTS.md +11 -0
  26. package/template/{frontend → base/frontend}/app/(auth)/login/page.tsx +1 -1
  27. package/template/{frontend → base/frontend}/app/(auth)/register/page.tsx +1 -1
  28. package/template/{frontend → base/frontend}/app/globals.css +1 -1
  29. package/template/{frontend → base/frontend}/app/layout.tsx +9 -12
  30. package/template/{frontend → base/frontend}/app/page.tsx +2 -2
  31. package/template/{frontend → base/frontend}/app/profile/page.tsx +3 -3
  32. package/template/{frontend → base/frontend}/components/require-auth.tsx +2 -2
  33. package/template/{frontend → base/frontend}/components/ui/button.tsx +2 -2
  34. package/template/{frontend → base/frontend}/components/ui/card.tsx +8 -23
  35. package/template/{frontend → base/frontend}/components/ui/input.tsx +2 -2
  36. package/template/{frontend → base/frontend}/components/ui/label.tsx +3 -6
  37. package/template/{frontend → base/frontend}/domains/auth/components/login-form.tsx +10 -3
  38. package/template/{frontend → base/frontend}/domains/auth/components/logout-button.tsx +2 -2
  39. package/template/{frontend → base/frontend}/domains/auth/components/register-form.tsx +10 -3
  40. package/template/{frontend → base/frontend}/domains/auth/hooks/use-login.ts +2 -2
  41. package/template/{frontend → base/frontend}/domains/auth/hooks/use-register.ts +6 -3
  42. package/template/{frontend → base/frontend}/domains/auth/services/auth.api.ts +5 -7
  43. package/template/{frontend → base/frontend}/domains/user/components/profile-card.tsx +2 -4
  44. package/template/{frontend → base/frontend}/domains/user/services/user.api.ts +2 -3
  45. package/template/base/frontend/env.ts +12 -0
  46. package/template/{frontend → base/frontend}/hooks/use-auth-mutation.ts +6 -3
  47. package/template/{frontend → base/frontend}/lib/api-client.ts +40 -5
  48. package/template/{frontend → base/frontend}/lib/utils.ts +1 -1
  49. package/template/base/frontend/next.config.ts +11 -0
  50. package/template/{frontend → base/frontend}/package.json +15 -14
  51. package/template/{frontend → base/frontend}/tsconfig.json +4 -0
  52. package/template/{gitignore → base/gitignore} +16 -2
  53. package/template/base/package.json +25 -0
  54. package/template/{packages → base/packages}/shared/package.json +1 -1
  55. package/template/{packages → base/packages}/shared/src/index.ts +1 -0
  56. package/template/{packages → base/packages}/shared/src/schemas/auth.schema.ts +8 -0
  57. package/template/{packages → base/packages}/shared/src/schemas/user.schema.ts +1 -0
  58. package/template/base/packages/shared/src/utils/uuid.ts +40 -0
  59. package/template/{packages → base/packages}/shared/tsconfig.json +26 -0
  60. package/template/base/pnpm-workspace.yaml +12 -0
  61. package/template/overlays/db-postgres/backend/.env.example +6 -0
  62. package/template/overlays/db-postgres/backend/drizzle.config.ts +11 -0
  63. package/template/overlays/db-postgres/backend/scripts/db-check.js +27 -0
  64. package/template/overlays/db-postgres/backend/src/infra/db/index.ts +6 -0
  65. package/template/overlays/db-sqlite/backend/.env.example +6 -0
  66. package/template/overlays/db-sqlite/backend/drizzle.config.ts +11 -0
  67. package/template/overlays/db-sqlite/backend/scripts/db-check.js +20 -0
  68. package/template/overlays/db-sqlite/backend/src/infra/db/index.ts +8 -0
  69. package/template/overlays/id-serial/packages/shared/src/schemas/user.schema.ts +14 -0
  70. package/template/overlays/id-uuid/packages/shared/src/schemas/user.schema.ts +14 -0
  71. package/template/overlays/models/postgres-serial/backend/src/domains/auth/auth.model.ts +12 -0
  72. package/template/overlays/models/postgres-serial/backend/src/domains/user/user.model.ts +10 -0
  73. package/template/overlays/models/postgres-uuid/backend/src/domains/auth/auth.model.ts +15 -0
  74. package/template/overlays/models/postgres-uuid/backend/src/domains/user/user.model.ts +13 -0
  75. package/template/overlays/models/sqlite-serial/backend/src/domains/auth/auth.model.ts +14 -0
  76. package/template/overlays/models/sqlite-serial/backend/src/domains/user/user.model.ts +12 -0
  77. package/template/overlays/models/sqlite-uuid/backend/src/domains/auth/auth.model.ts +17 -0
  78. package/template/overlays/models/sqlite-uuid/backend/src/domains/user/user.model.ts +15 -0
  79. package/template/CLAUDE.md +0 -62
  80. package/template/backend/src/domains/user/user.routes.test.ts +0 -82
  81. package/template/backend/src/infra/middlewares/require-auth.test.ts +0 -51
  82. package/template/backend/src/infra/middlewares/require-auth.ts +0 -36
  83. package/template/frontend/AGENTS.md +0 -5
  84. package/template/frontend/next.config.ts +0 -7
  85. package/template/package.json +0 -17
  86. package/template/pnpm-workspace.yaml +0 -4
  87. package/template/{backend → base/backend}/.env.example +0 -0
  88. package/template/{backend → base/backend}/README.md +0 -0
  89. package/template/{backend → base/backend}/api/index.ts +0 -0
  90. package/template/{backend → base/backend}/drizzle/0000_grey_lethal_legion.sql +0 -0
  91. package/template/{backend → base/backend}/drizzle/meta/0000_snapshot.json +0 -0
  92. package/template/{backend → base/backend}/drizzle/meta/_journal.json +0 -0
  93. package/template/{backend → base/backend}/drizzle.config.ts +0 -0
  94. package/template/{backend → base/backend}/gitignore +0 -0
  95. package/template/{backend → base/backend}/public/.gitkeep +0 -0
  96. package/template/{backend → base/backend}/scripts/db-check.js +0 -0
  97. package/template/{backend → base/backend}/src/config/.gitkeep +0 -0
  98. package/template/{backend → base/backend}/src/domains/auth/.gitkeep +0 -0
  99. package/template/{backend → base/backend}/src/domains/user/.gitkeep +0 -0
  100. package/template/{backend → base/backend}/src/index.ts +1 -1
  101. package/template/{backend → base/backend}/src/infra/db/.gitkeep +0 -0
  102. package/template/{backend → base/backend}/src/infra/db/index.ts +0 -0
  103. package/template/{backend → base/backend}/src/infra/middlewares/.gitkeep +0 -0
  104. package/template/{backend → base/backend}/vercel.json +0 -0
  105. package/template/{frontend → base/frontend}/.env.example +0 -0
  106. package/template/{frontend → base/frontend}/CLAUDE.md +0 -0
  107. package/template/{frontend → base/frontend}/README.md +0 -0
  108. package/template/{frontend → base/frontend}/app/dashboard/page.tsx +2 -2
  109. package/template/{frontend → base/frontend}/app/icon.png +0 -0
  110. package/template/{frontend → base/frontend}/components.json +0 -0
  111. package/template/{frontend → base/frontend}/domains/auth/components/.gitkeep +0 -0
  112. package/template/{frontend → base/frontend}/domains/auth/hooks/.gitkeep +0 -0
  113. package/template/{frontend → base/frontend}/domains/auth/services/.gitkeep +0 -0
  114. package/template/{frontend → base/frontend}/domains/user/components/.gitkeep +0 -0
  115. package/template/{frontend → base/frontend}/domains/user/hooks/use-profile.ts +2 -2
  116. /package/template/{frontend → base/frontend}/eslint.config.mjs +0 -0
  117. /package/template/{frontend → base/frontend}/gitignore +0 -0
  118. /package/template/{frontend → base/frontend}/postcss.config.mjs +0 -0
  119. /package/template/{frontend → base/frontend}/public/favicon.ico +0 -0
  120. /package/template/{frontend → base/frontend}/public/logo.png +0 -0
@@ -0,0 +1,20 @@
1
+ import Database from "better-sqlite3"
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 || "sqlite.db"
10
+ console.log("\x1b[36mChecking SQLite database...\x1b[0m")
11
+ try {
12
+ const db = new Database(dbUrl)
13
+ db.exec("SELECT 1")
14
+ db.close()
15
+ console.log("\x1b[32m✔ SQLite database ready!\x1b[0m")
16
+ process.exit(0)
17
+ } catch (err) {
18
+ console.error("\x1b[31m[ERROR] SQLite database check failed:\x1b[0m", err.message)
19
+ process.exit(1)
20
+ }
@@ -0,0 +1,8 @@
1
+ import Database from "better-sqlite3"
2
+ import { drizzle } from "drizzle-orm/better-sqlite3"
3
+ import { env } from "../../config/env.js"
4
+
5
+ const sqlite = new Database(env.DATABASE_URL || "sqlite.db")
6
+ sqlite.pragma("journal_mode = WAL")
7
+ sqlite.pragma("foreign_keys = ON")
8
+ export const db = drizzle(sqlite)
@@ -0,0 +1,14 @@
1
+ import { z } from "zod"
2
+
3
+ export interface AuthUser {
4
+ id: number
5
+ name: string
6
+ email: string
7
+ role: string
8
+ }
9
+
10
+ export const UpdateProfileSchema = z.object({
11
+ name: z.string().min(1).max(100).optional(),
12
+ })
13
+
14
+ export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>
@@ -0,0 +1,14 @@
1
+ import { z } from "zod"
2
+
3
+ export interface AuthUser {
4
+ id: string
5
+ name: string
6
+ email: string
7
+ role: string
8
+ }
9
+
10
+ export const UpdateProfileSchema = z.object({
11
+ name: z.string().min(1).max(100).optional(),
12
+ })
13
+
14
+ export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>
@@ -0,0 +1,12 @@
1
+ import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"
2
+ import { usersTable } from "../user/user.model.js"
3
+
4
+ export const authSessionsTable = pgTable("auth_sessions", {
5
+ id: serial("id").primaryKey(),
6
+ userId: integer("user_id")
7
+ .notNull()
8
+ .references(() => usersTable.id, { onDelete: "cascade" }),
9
+ refreshTokenHash: text("refresh_token_hash").notNull().unique(),
10
+ expiresAt: timestamp("expires_at").notNull(),
11
+ createdAt: timestamp("created_at").notNull().defaultNow(),
12
+ })
@@ -0,0 +1,10 @@
1
+ import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"
2
+
3
+ export const usersTable = pgTable("users", {
4
+ id: serial("id").primaryKey(),
5
+ name: text("name").notNull(),
6
+ email: text("email").notNull().unique(),
7
+ password: text("password").notNull(),
8
+ role: text("role").default("user").notNull(),
9
+ createdAt: timestamp("created_at").notNull().defaultNow(),
10
+ })
@@ -0,0 +1,15 @@
1
+ import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
2
+ import { uuidv7 } from "@restack/shared"
3
+ import { usersTable } from "../user/user.model.js"
4
+
5
+ export const authSessionsTable = pgTable("auth_sessions", {
6
+ id: uuid("id")
7
+ .primaryKey()
8
+ .$defaultFn(() => uuidv7()),
9
+ userId: uuid("user_id")
10
+ .notNull()
11
+ .references(() => usersTable.id, { onDelete: "cascade" }),
12
+ refreshTokenHash: text("refresh_token_hash").notNull().unique(),
13
+ expiresAt: timestamp("expires_at").notNull(),
14
+ createdAt: timestamp("created_at").notNull().defaultNow(),
15
+ })
@@ -0,0 +1,13 @@
1
+ import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
2
+ import { uuidv7 } from "@restack/shared"
3
+
4
+ export const usersTable = pgTable("users", {
5
+ id: uuid("id")
6
+ .primaryKey()
7
+ .$defaultFn(() => uuidv7()),
8
+ name: text("name").notNull(),
9
+ email: text("email").notNull().unique(),
10
+ password: text("password").notNull(),
11
+ role: text("role").default("user").notNull(),
12
+ createdAt: timestamp("created_at").notNull().defaultNow(),
13
+ })
@@ -0,0 +1,14 @@
1
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
2
+ import { usersTable } from "../user/user.model.js"
3
+
4
+ export const authSessionsTable = sqliteTable("auth_sessions", {
5
+ id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
6
+ userId: integer("user_id")
7
+ .notNull()
8
+ .references(() => usersTable.id, { onDelete: "cascade" }),
9
+ refreshTokenHash: text("refresh_token_hash").notNull().unique(),
10
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
11
+ createdAt: integer("created_at", { mode: "timestamp" })
12
+ .notNull()
13
+ .$defaultFn(() => new Date()),
14
+ })
@@ -0,0 +1,12 @@
1
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
2
+
3
+ export const usersTable = sqliteTable("users", {
4
+ id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
5
+ name: text("name").notNull(),
6
+ email: text("email").notNull().unique(),
7
+ password: text("password").notNull(),
8
+ role: text("role").default("user").notNull(),
9
+ createdAt: integer("created_at", { mode: "timestamp" })
10
+ .notNull()
11
+ .$defaultFn(() => new Date()),
12
+ })
@@ -0,0 +1,17 @@
1
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
2
+ import { uuidv7 } from "@restack/shared"
3
+ import { usersTable } from "../user/user.model.js"
4
+
5
+ export const authSessionsTable = sqliteTable("auth_sessions", {
6
+ id: text("id")
7
+ .primaryKey()
8
+ .$defaultFn(() => uuidv7()),
9
+ userId: text("user_id")
10
+ .notNull()
11
+ .references(() => usersTable.id, { onDelete: "cascade" }),
12
+ refreshTokenHash: text("refresh_token_hash").notNull().unique(),
13
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
14
+ createdAt: integer("created_at", { mode: "timestamp" })
15
+ .notNull()
16
+ .$defaultFn(() => new Date()),
17
+ })
@@ -0,0 +1,15 @@
1
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
2
+ import { uuidv7 } from "@restack/shared"
3
+
4
+ export const usersTable = sqliteTable("users", {
5
+ id: text("id")
6
+ .primaryKey()
7
+ .$defaultFn(() => uuidv7()),
8
+ name: text("name").notNull(),
9
+ email: text("email").notNull().unique(),
10
+ password: text("password").notNull(),
11
+ role: text("role").default("user").notNull(),
12
+ createdAt: integer("created_at", { mode: "timestamp" })
13
+ .notNull()
14
+ .$defaultFn(() => new Date()),
15
+ })
@@ -1,62 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides architecture, build, and styling guidance to Claude Code (claude.ai/code) and other AI assistants working in this repository.
4
-
5
- ## Project Stack
6
- - **Frontend**: Next.js 16 (App Router)
7
- - **Backend**: Hono
8
- - **ORM**: Drizzle ORM
9
- - **Database**: PostgreSQL (Postgres-js driver)
10
- - **Validation**: Zod (shared schemas)
11
- - **Package Manager**: pnpm workspaces
12
-
13
- ## Monorepo Layout
14
- - `frontend/` — Next.js 16 App Router
15
- - `backend/` — Hono API
16
- - `packages/shared/` — Shared Zod schemas (the single source of truth)
17
-
18
- ## Build and Dev Commands
19
- All commands should be run from the repository root:
20
- - `pnpm install` — Install all dependencies
21
- - `pnpm --filter @restack/shared build` — Build shared Zod schemas (run first!)
22
- - `pnpm --filter backend dev` — Start Hono dev server on port 3001
23
- - `pnpm --filter frontend dev` — Start Next.js dev server on port 3000
24
- - `pnpm --filter backend db:push` — Push Drizzle schema to PostgreSQL database
25
- - `pnpm --filter backend db:studio` — Open Drizzle Studio database viewer
26
- - `pnpm --filter backend test` — Run Vitest tests
27
-
28
- ## Architecture Rules: Semi-DDD (Feature-Driven)
29
- Code is organized by business domain/feature, not by file type.
30
-
31
- ### Backend Layout (`backend/src/domains/[feature]/`)
32
- Each domain has 4 primary files:
33
- - `[feature].routes.ts` — HTTP endpoints & input validation (imports Zod schema from `@restack/shared`)
34
- - `[feature].service.ts` — Pure business logic only (no direct DB access)
35
- - `[feature].repo.ts` — Drizzle queries only (no business logic)
36
- - `[feature].model.ts` — Drizzle table definitions
37
-
38
- **Cross-Domain Dependency Rule**: The `auth` domain may import from `user` (e.g. `usersTable` from `user.model.ts`). The reverse (user importing from auth) is STRICTLY FORBIDDEN.
39
-
40
- ### Frontend Layout (`frontend/domains/[feature]/`)
41
- - `components/` — UI components (no direct data fetching)
42
- - `hooks/` — Reactive state & data fetching logic
43
- - `services/` — Thin HTTP wrappers around `apiClient`
44
- - `types.ts` — Local TS type declarations
45
- - `frontend/app/` is strictly for layout, routing config, metadata, or entry gates. Do not implement custom JSX layouts, forms, state, or complex components here. Always delegate rendering to domain feature components.
46
-
47
- ## Critical Gotcha: Shared Package Build Requirement
48
- `@restack/shared`'s `package.json` resolves to `dist/`, which is gitignored. When editing Zod schemas in `packages/shared/src/schemas/`, you MUST compile them by running:
49
- `pnpm --filter @restack/shared build`
50
- (or keep `pnpm --filter @restack/shared dev` watch mode running in a terminal) or the frontend and backend will fail to resolve the updated types or throw compilation errors.
51
-
52
- ## Auth & Cookie Strategy
53
- - Short-lived JWT `access_token` (15m) and opaque `refresh_token` (30d) are both set as `httpOnly` cookies at `Path=/`.
54
- - Access tokens are automatically refreshed by the client-side `apiClient` on a 401 response.
55
- - Cookie configuration: `SameSite=None` in production (requires `Secure=true`), and `SameSite=Lax` in development (`Secure=false` since local dev runs on HTTP).
56
- - There is no server-side auth gate (`middleware.ts`/`proxy.ts`) because cookies cannot be read server-side when frontend and backend deploy to unrelated domains (Topology B). Authenticated routes are protected via backend `requireAuth` middleware, and frontend pages are protected via `<RequireAuth>` or client-side checks in hooks.
57
-
58
- ## Structured & Correlation Logging
59
- - Every incoming HTTP request is tagged with a unique `requestId` via Hono's `requestId` middleware.
60
- - A request-scoped child logger (`logger.child({ requestId })`) is bound to Hono's context.
61
- - In route handlers, always extract the request-specific child logger from Hono's context (`c.get("logger") || logger`) to log events. This ensures all logs generated during a request carry the correct `requestId` and `userId` context (which is automatically enriched by the `requireAuth` middleware).
62
-
@@ -1,82 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from "vitest"
2
- import { eq } from "drizzle-orm"
3
- import { userRoutes } from "./user.routes.js"
4
- import { authService } from "../auth/auth.service.js"
5
- import { db } from "../../infra/db/index.js"
6
- import { usersTable } from "./user.model.js"
7
-
8
- const TEST_EMAIL = `user-routes-test-${Date.now()}@example.com`
9
- let accessToken: string
10
- let userId: number
11
-
12
- beforeAll(async () => {
13
- const user = await authService.register({ name: "Route Test", email: TEST_EMAIL, password: "password123" })
14
- userId = user.id
15
- const tokens = await authService.issueTokenPair(user)
16
- accessToken = tokens.accessToken
17
- })
18
-
19
- afterAll(async () => {
20
- await db.delete(usersTable).where(eq(usersTable.id, userId))
21
- })
22
-
23
- const authHeaders = (token?: string) => ({
24
- "Content-Type": "application/json",
25
- ...(token ? { Cookie: `access_token=${token}` } : {}),
26
- })
27
-
28
- describe("GET /me", () => {
29
- it("returns 401 without a token", async () => {
30
- const res = await userRoutes.request("/me")
31
- expect(res.status).toBe(401)
32
- })
33
-
34
- it("returns the profile with a valid token", async () => {
35
- const res = await userRoutes.request("/me", { headers: authHeaders(accessToken) })
36
- expect(res.status).toBe(200)
37
- const body = await res.json()
38
- expect(body.user.email).toBe(TEST_EMAIL)
39
- })
40
- })
41
-
42
- describe("PUT /profile", () => {
43
- it("returns 401 without a token", async () => {
44
- const res = await userRoutes.request("/profile", {
45
- method: "PUT",
46
- headers: authHeaders(),
47
- body: JSON.stringify({ name: "x" }),
48
- })
49
- expect(res.status).toBe(401)
50
- })
51
-
52
- it("updates the name with a valid token and body", async () => {
53
- const res = await userRoutes.request("/profile", {
54
- method: "PUT",
55
- headers: authHeaders(accessToken),
56
- body: JSON.stringify({ name: "Updated Name" }),
57
- })
58
- expect(res.status).toBe(200)
59
- const body = await res.json()
60
- expect(body.user.name).toBe("Updated Name")
61
- })
62
-
63
- it("rejects an invalid body (empty name)", async () => {
64
- const res = await userRoutes.request("/profile", {
65
- method: "PUT",
66
- headers: authHeaders(accessToken),
67
- body: JSON.stringify({ name: "" }),
68
- })
69
- expect(res.status).toBe(400)
70
- })
71
-
72
- it("treats an empty body as a no-op instead of erroring", async () => {
73
- const res = await userRoutes.request("/profile", {
74
- method: "PUT",
75
- headers: authHeaders(accessToken),
76
- body: JSON.stringify({}),
77
- })
78
- expect(res.status).toBe(200)
79
- const body = await res.json()
80
- expect(body.user.email).toBe(TEST_EMAIL)
81
- })
82
- })
@@ -1,51 +0,0 @@
1
- import { describe, it, expect } from "vitest"
2
- import { Hono } from "hono"
3
- import { SignJWT } from "jose"
4
- import { requireAuth } from "./require-auth.js"
5
- import type { AuthVariables } from "./require-auth.js"
6
- import { env } from "../../config/env.js"
7
-
8
- const secret = new TextEncoder().encode(env.JWT_SECRET)
9
-
10
- const signTestToken = (payload: Record<string, unknown>, expiresIn = "15m") =>
11
- new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setExpirationTime(expiresIn).sign(secret)
12
-
13
- function buildApp() {
14
- const app = new Hono<{ Variables: AuthVariables }>()
15
- app.use("/protected", requireAuth)
16
- app.get("/protected", (c) => c.json({ user: c.get("user") }))
17
- return app
18
- }
19
-
20
- describe("requireAuth", () => {
21
- it("returns 401 when no access_token cookie is present", async () => {
22
- const res = await buildApp().request("/protected")
23
- expect(res.status).toBe(401)
24
- })
25
-
26
- it("returns 401 when the token is garbage", async () => {
27
- const res = await buildApp().request("/protected", {
28
- headers: { Cookie: "access_token=not-a-real-token" },
29
- })
30
- expect(res.status).toBe(401)
31
- })
32
-
33
- it("returns 401 when the token is expired", async () => {
34
- const expired = await signTestToken({ sub: "user-1", email: "a@b.com" }, "-1s")
35
- const res = await buildApp().request("/protected", {
36
- headers: { Cookie: `access_token=${expired}` },
37
- })
38
- expect(res.status).toBe(401)
39
- })
40
-
41
- it("calls next() and exposes the payload when the token is valid", async () => {
42
- const token = await signTestToken({ sub: "user-1", email: "a@b.com" })
43
- const res = await buildApp().request("/protected", {
44
- headers: { Cookie: `access_token=${token}` },
45
- })
46
- expect(res.status).toBe(200)
47
- const body = await res.json()
48
- expect(body.user.sub).toBe("user-1")
49
- expect(body.user.email).toBe("a@b.com")
50
- })
51
- })
@@ -1,36 +0,0 @@
1
- import type { MiddlewareHandler } from "hono"
2
- import { getCookie } from "hono/cookie"
3
- import { authService } from "../../domains/auth/auth.service.js"
4
- import type { AppVariables } from "../../utils/logger.js"
5
-
6
- /**
7
- * Authentication guard middleware.
8
- * Enforces the presence and validity of the short-lived JWT access token in request cookies.
9
- * Resolves the payload, binds it to Hono context variables (`c.get("user")`),
10
- * and attaches the authenticated userId to the request logger context.
11
- *
12
- * Usage:
13
- * Apply to any endpoint requiring authentication (e.g. `userRoutes.get("/me", requireAuth, ...)`).
14
- */
15
- export const requireAuth: MiddlewareHandler<{ Variables: AppVariables }> = async (c, next) => {
16
- // Extract token from cookie (JWT)
17
- const token = getCookie(c, "access_token")
18
- if (!token) return c.json({ error: "Unauthorized" }, 401)
19
-
20
- try {
21
- // Decode and verify signature & expiration
22
- const payload = await authService.verifyAccessToken(token)
23
- c.set("user", payload)
24
-
25
- // Enrich the child logger with the user's ID for all downstream actions
26
- const reqLogger = c.get("logger")
27
- if (reqLogger) {
28
- c.set("logger", reqLogger.child({ userId: Number(payload.sub) }))
29
- }
30
- } catch {
31
- // Return standard 401 response on invalid / expired signature
32
- return c.json({ error: "Unauthorized" }, 401)
33
- }
34
-
35
- await next()
36
- }
@@ -1,5 +0,0 @@
1
- <!-- BEGIN:nextjs-agent-rules -->
2
- # This is NOT the Next.js you know
3
-
4
- This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
- <!-- END:nextjs-agent-rules -->
@@ -1,7 +0,0 @@
1
- import type { NextConfig } from "next";
2
-
3
- const nextConfig: NextConfig = {
4
- transpilePackages: ["@restack/shared"],
5
- };
6
-
7
- export default nextConfig;
@@ -1,17 +0,0 @@
1
- {
2
- "name": "restack-pattern",
3
- "private": true,
4
- "license": "MIT",
5
- "scripts": {
6
- "dev": "pnpm --parallel dev",
7
- "build": "pnpm --parallel build"
8
- },
9
- "workspaces": [
10
- "frontend",
11
- "backend",
12
- "packages/*"
13
- ],
14
- "engines": {
15
- "node": ">=20"
16
- }
17
- }
@@ -1,4 +0,0 @@
1
- packages:
2
- - "frontend"
3
- - "backend"
4
- - "packages/*"
File without changes
File without changes
File without changes
File without changes
@@ -1,7 +1,7 @@
1
1
  import { serve } from "@hono/node-server"
2
2
  import { env } from "./config/env.js"
3
- import { logger } from "./utils/logger.js"
4
3
  import { app } from "./hono-app.js"
4
+ import { logger } from "./utils/logger.js"
5
5
 
6
6
  /**
7
7
  * Local dev / persistent-server entry point (tsx watch, Railway, Render, any VPS).
File without changes
File without changes
File without changes
@@ -1,7 +1,7 @@
1
1
  import Link from "next/link"
2
- import { LogoutButton } from "@/domains/auth/components/logout-button"
3
- import { Button } from "@/components/ui/button"
4
2
  import { RequireAuth } from "@/components/require-auth"
3
+ import { Button } from "@/components/ui/button"
4
+ import { LogoutButton } from "@/domains/auth/components/logout-button"
5
5
 
6
6
  /**
7
7
  * Next.js Router Page: Dashboard
@@ -1,8 +1,8 @@
1
1
  "use client"
2
2
 
3
- import { useEffect, useState } from "react"
4
- import { useRouter } from "next/navigation"
5
3
  import type { AuthUser } from "@restack/shared"
4
+ import { useRouter } from "next/navigation"
5
+ import { useEffect, useState } from "react"
6
6
  import { userApi } from "../services/user.api"
7
7
 
8
8
  /**
File without changes