create-restack-pattern 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +284 -74
- package/package.json +14 -8
- package/template/{DEVELOPMENT.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/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} +15 -0
- 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/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/require-auth.tsx +1 -1
- 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
|
@@ -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,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 -->
|
package/template/package.json
DELETED
|
@@ -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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|
|
File without changes
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|