create-tigra 2.8.0 → 3.0.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 (44) hide show
  1. package/README.md +10 -3
  2. package/bin/create-tigra.js +77 -37
  3. package/package.json +5 -5
  4. package/template/_claude/commands/create-server.md +8 -2
  5. package/template/_claude/rules/client/01-project-structure.md +12 -0
  6. package/template/_claude/rules/client/03-data-and-state.md +1 -1
  7. package/template/_claude/rules/client/04-design-system.md +23 -0
  8. package/template/_claude/rules/client/07-deployment.md +99 -0
  9. package/template/_claude/rules/client/08-lockfile-cross-platform.md +79 -0
  10. package/template/_claude/rules/client/core.md +1 -0
  11. package/template/_claude/rules/global/core.md +20 -1
  12. package/template/_claude/rules/global/investigation-before-conclusions.md +57 -0
  13. package/template/_claude/rules/server/core.md +2 -0
  14. package/template/_claude/rules/server/deployment.md +78 -0
  15. package/template/client/next.config.ts +12 -2
  16. package/template/client/package-lock.json +12345 -0
  17. package/template/client/package.json +3 -2
  18. package/template/client/src/components/common/SafeImage.tsx +2 -1
  19. package/template/client/src/lib/api/axios.config.ts +19 -4
  20. package/template/client/src/middleware.ts +7 -0
  21. package/template/gitignore +1 -0
  22. package/template/server/.env.example +42 -0
  23. package/template/server/.env.example.production +40 -0
  24. package/template/server/Dockerfile +29 -5
  25. package/template/server/docker-compose.yml +15 -4
  26. package/template/server/package-lock.json +6544 -6823
  27. package/template/server/package.json +76 -75
  28. package/template/server/prisma/seed.ts +20 -4
  29. package/template/server/src/app.ts +40 -8
  30. package/template/server/src/config/env.ts +72 -28
  31. package/template/server/src/config/rate-limit.config.ts +16 -0
  32. package/template/server/src/libs/__tests__/http.test.ts +23 -9
  33. package/template/server/src/libs/__tests__/origin-check.test.ts +53 -0
  34. package/template/server/src/libs/auth.ts +6 -16
  35. package/template/server/src/libs/cookies.ts +1 -1
  36. package/template/server/src/libs/duration.ts +30 -0
  37. package/template/server/src/libs/ip-block.ts +10 -4
  38. package/template/server/src/libs/origin-check.ts +38 -0
  39. package/template/server/src/libs/redis.ts +1 -1
  40. package/template/server/src/modules/auth/__tests__/auth.service.test.ts +274 -44
  41. package/template/server/src/modules/auth/auth.repo.ts +2 -0
  42. package/template/server/src/modules/auth/auth.service.ts +103 -12
  43. package/template/server/src/test/setup.ts +22 -2
  44. package/template/server/vitest.config.ts +43 -43
@@ -7,6 +7,7 @@
7
7
  "build": "next build",
8
8
  "start": "next start",
9
9
  "lint": "eslint src/",
10
+ "typecheck": "tsc --noEmit",
10
11
  "generate:env": "node -e \"import('fs').then(f=>{if(f.existsSync('.env'))console.log('.env already exists, skipping');else{f.copyFileSync('.env.example','.env');console.log('.env created from .env.example')}})\""
11
12
  },
12
13
  "dependencies": {
@@ -17,7 +18,7 @@
17
18
  "class-variance-authority": "^0.7.1",
18
19
  "clsx": "^2.1.1",
19
20
  "lucide-react": "^0.563.0",
20
- "next": "16.1.6",
21
+ "next": "16.2.9",
21
22
  "next-themes": "^0.4.6",
22
23
  "radix-ui": "^1.4.3",
23
24
  "react": "19.2.3",
@@ -38,7 +39,7 @@
38
39
  "@types/react": "^19",
39
40
  "@types/react-dom": "^19",
40
41
  "eslint": "^9",
41
- "eslint-config-next": "16.1.6",
42
+ "eslint-config-next": "16.2.9",
42
43
  "shadcn": "^3.8.4",
43
44
  "tailwindcss": "^4",
44
45
  "tw-animate-css": "^1.4.0",
@@ -36,12 +36,13 @@ function isLoopbackUrl(src: ImageProps['src']): boolean {
36
36
  }
37
37
  }
38
38
 
39
- export const SafeImage = (props: ImageProps): React.ReactElement => {
39
+ export const SafeImage = ({ alt, ...props }: ImageProps): React.ReactElement => {
40
40
  const shouldSkipOptimization = isLoopbackUrl(props.src);
41
41
 
42
42
  return (
43
43
  <Image
44
44
  {...props}
45
+ alt={alt}
45
46
  unoptimized={props.unoptimized || shouldSkipOptimization}
46
47
  />
47
48
  );
@@ -108,15 +108,30 @@ apiClient.interceptors.response.use(
108
108
  refreshError.response.status < 500;
109
109
 
110
110
  if (isAuthFailure) {
111
- // Trip the circuit breaker FIRST prevents any subsequent 401s
112
- // from re-entering this flow while the redirect is pending.
113
- isSessionDead = true;
111
+ // The bootstrap probe (AuthInitializer's getMe) runs on EVERY page,
112
+ // including public ones. For an anonymous visitor, getMe 401 →
113
+ // refresh 401 is the NORMAL "not logged in" outcome — not a dead
114
+ // session. The probe must fail silently to "anonymous": clear any
115
+ // stale Redux state, but never trip the circuit breaker, clear the
116
+ // session cookie, or hard-redirect a visitor off a public page.
117
+ // (On protected pages, AuthInitializer handles the redirect itself.)
118
+ // Note: a genuine request queued behind a probe-initiated refresh is
119
+ // rejected via processQueue without tripping the breaker or
120
+ // redirecting — AuthInitializer covers the redirect on protected pages.
121
+ const isBootstrapProbe = originalRequest.url === API_ENDPOINTS.AUTH.ME;
122
+
123
+ if (!isBootstrapProbe) {
124
+ // A genuine authenticated action failed mid-session — the session
125
+ // is dead. Trip the circuit breaker FIRST — prevents any subsequent
126
+ // 401s from re-entering this flow while the redirect is pending.
127
+ isSessionDead = true;
128
+ }
114
129
 
115
130
  const { logout } = await import('@/features/auth/store/authSlice');
116
131
  const { store } = await import('@/store');
117
132
  store.dispatch(logout());
118
133
 
119
- if (typeof window !== 'undefined') {
134
+ if (!isBootstrapProbe && typeof window !== 'undefined') {
120
135
  // Clear session indicator so middleware won't let user through
121
136
  // to protected pages — prevents redirect loops
122
137
  document.cookie = 'auth_session=; Max-Age=0; path=/; SameSite=Strict';
@@ -16,6 +16,13 @@ function isTokenExpired(token: string): boolean {
16
16
 
17
17
  export function middleware(request: NextRequest): NextResponse {
18
18
  const { pathname } = request.nextUrl;
19
+
20
+ // NOTE: This middleware only checks COOKIE PRESENCE (and a best-effort,
21
+ // unverified exp claim) for UX-level routing — redirecting users who are
22
+ // obviously logged out away from protected pages. It does NOT verify the
23
+ // JWT signature and is NOT a security boundary. Real authorization happens
24
+ // server-side on every API call; a forged cookie gets past this middleware
25
+ // but every API request it makes will be rejected with 401/403.
19
26
  const accessToken = request.cookies.get('access_token')?.value;
20
27
  const authSession = request.cookies.get('auth_session')?.value;
21
28
 
@@ -5,6 +5,7 @@ node_modules/
5
5
  dist/
6
6
  .next/
7
7
  out/
8
+ *.tsbuildinfo
8
9
 
9
10
  # Environment files
10
11
  .env
@@ -21,6 +21,19 @@ PORT=8000
21
21
  # Server host (0.0.0.0 = listen on all interfaces)
22
22
  HOST=0.0.0.0
23
23
 
24
+ # ===================================================================
25
+ # SERVER TIMEOUTS
26
+ # ===================================================================
27
+
28
+ # Fastify request timeout in milliseconds (default: 30000 = 30s)
29
+ # Long-running routes (LLM calls, big exports) may need 180000+ (180s).
30
+ # IMPORTANT: the reverse proxy (Nginx/Coolify) timeout must be raised to
31
+ # match, or the proxy cuts the connection before the server does.
32
+ REQUEST_TIMEOUT_MS=30000
33
+
34
+ # Fastify connection timeout in milliseconds (default: 60000 = 60s)
35
+ CONNECTION_TIMEOUT_MS=60000
36
+
24
37
  # ===================================================================
25
38
  # DATABASE CONFIGURATION (MySQL 8.0+)
26
39
  # ===================================================================
@@ -67,6 +80,25 @@ RATE_LIMIT_MULTIPLIER=1
67
80
  # RATE_LIMIT_AUTH_LOGIN_MAX=10
68
81
  # RATE_LIMIT_AUTH_REGISTER_MAX=5
69
82
 
83
+ # ===================================================================
84
+ # IP AUTO-BLOCK
85
+ # ===================================================================
86
+ #
87
+ # An IP that exceeds rate limits IP_AUTO_BLOCK_THRESHOLD times within
88
+ # IP_AUTO_BLOCK_WINDOW_SECONDS is blocked for IP_AUTO_BLOCK_DURATION_SECONDS.
89
+ # The threshold targets SUSTAINED abuse — keep it high enough that a
90
+ # retry-looping legitimate client or a NAT'd office sharing one IP cannot
91
+ # self-ban. See src/config/rate-limit.config.ts for the interaction notes.
92
+
93
+ # Rate-limit violations before an IP is auto-blocked (default: 20)
94
+ IP_AUTO_BLOCK_THRESHOLD=20
95
+
96
+ # Sliding window for counting violations, in seconds (default: 300 = 5 min)
97
+ IP_AUTO_BLOCK_WINDOW_SECONDS=300
98
+
99
+ # How long an auto-blocked IP stays blocked, in seconds (default: 3600 = 1 hour)
100
+ IP_AUTO_BLOCK_DURATION_SECONDS=3600
101
+
70
102
  # ===================================================================
71
103
  # ACCOUNT ACTIVATION
72
104
  # ===================================================================
@@ -183,6 +215,16 @@ CLIENT_URL="http://localhost:3000"
183
215
  # Staging: info
184
216
  LOG_LEVEL=info
185
217
 
218
+ # ===================================================================
219
+ # DATABASE SEEDING (npm run prisma:seed — dev/test only)
220
+ # ===================================================================
221
+
222
+ # Passwords for the seeded demo accounts (admin@example.com / user@example.com).
223
+ # Optional in development — well-known dev defaults (Admin123! / User123!) are
224
+ # used when unset. The seed script REFUSES to run when NODE_ENV=production.
225
+ # SEED_ADMIN_PASSWORD="choose-a-dev-admin-password"
226
+ # SEED_USER_PASSWORD="choose-a-dev-user-password"
227
+
186
228
  # ===================================================================
187
229
  # ERROR TRACKING (Optional)
188
230
  # ===================================================================
@@ -23,6 +23,19 @@ NODE_ENV=production
23
23
  PORT=3000
24
24
  HOST=0.0.0.0
25
25
 
26
+ # ===================================================================
27
+ # SERVER TIMEOUTS
28
+ # ===================================================================
29
+
30
+ # Fastify request timeout in milliseconds (default: 30000 = 30s)
31
+ # Long-running routes (LLM calls, big exports) may need 180000+ (180s).
32
+ # CRITICAL: the reverse proxy (Nginx/Coolify) timeout must be raised to
33
+ # match, or the proxy cuts the connection before the server does.
34
+ REQUEST_TIMEOUT_MS=30000
35
+
36
+ # Fastify connection timeout in milliseconds (default: 60000 = 60s)
37
+ CONNECTION_TIMEOUT_MS=60000
38
+
26
39
  # ===================================================================
27
40
  # DATABASE (MySQL 8.0+)
28
41
  # ===================================================================
@@ -65,6 +78,22 @@ RATE_LIMIT_MULTIPLIER=1
65
78
  RATE_LIMIT_AUTH_LOGIN_MAX=10
66
79
  RATE_LIMIT_AUTH_REGISTER_MAX=5
67
80
 
81
+ # ===================================================================
82
+ # IP AUTO-BLOCK
83
+ # ===================================================================
84
+
85
+ # Rate-limit violations before an IP is auto-blocked for every route.
86
+ # Keep HIGH enough that a retry-looping legitimate client or a NAT'd
87
+ # office sharing one egress IP cannot self-ban (sustained abuse only).
88
+ # See src/config/rate-limit.config.ts before lowering.
89
+ IP_AUTO_BLOCK_THRESHOLD=20
90
+
91
+ # Sliding window for counting violations, in seconds (5 minutes)
92
+ IP_AUTO_BLOCK_WINDOW_SECONDS=300
93
+
94
+ # How long an auto-blocked IP stays blocked, in seconds (1 hour)
95
+ IP_AUTO_BLOCK_DURATION_SECONDS=3600
96
+
68
97
  # ===================================================================
69
98
  # ACCOUNT ACTIVATION
70
99
  # ===================================================================
@@ -142,6 +171,17 @@ CLIENT_URL="https://yourdomain.com"
142
171
  # Use warn to reduce costs in high-traffic scenarios
143
172
  LOG_LEVEL=info
144
173
 
174
+ # ===================================================================
175
+ # DATABASE SEEDING
176
+ # ===================================================================
177
+
178
+ # DO NOT SEED PRODUCTION. The seed script (npm run prisma:seed) creates
179
+ # well-known demo accounts (admin@example.com) and hard-refuses to run when
180
+ # NODE_ENV=production. These variables exist only to keep .env files in sync
181
+ # across environments — leave them commented out in production.
182
+ # SEED_ADMIN_PASSWORD=
183
+ # SEED_USER_PASSWORD=
184
+
145
185
  # ===================================================================
146
186
  # ERROR TRACKING
147
187
  # ===================================================================
@@ -23,6 +23,18 @@ RUN if [ -f pnpm-lock.yaml ]; then \
23
23
  npm ci --omit=dev; \
24
24
  fi
25
25
 
26
+ # Add the Prisma CLI to the production node_modules so the runtime image can
27
+ # run `npx prisma migrate deploy` at container start (see CMD in stage 3).
28
+ # `prisma` is a devDependency (correct for local dev), so the prod-only install
29
+ # above does NOT include it — without this step, npx would try to download the
30
+ # CLI from the registry on every container boot. Pinned to the exact
31
+ # @prisma/client version the lockfile installed, so CLI and client never drift.
32
+ # --no-save keeps package.json and the lockfile untouched. Because this stage
33
+ # runs on the same Alpine (musl) base as the runtime stage, the engine binaries
34
+ # downloaded here are the correct ones for production.
35
+ RUN npm install --no-save --no-audit --no-fund \
36
+ "prisma@$(node -p "require('@prisma/client/package.json').version")"
37
+
26
38
  # ===================================================================
27
39
  # Stage 2: Build (compile TypeScript)
28
40
  # ===================================================================
@@ -69,7 +81,9 @@ RUN addgroup -g 1001 -S nodejs && \
69
81
 
70
82
  WORKDIR /app
71
83
 
72
- # Copy production dependencies from dependencies stage
84
+ # Copy production dependencies from dependencies stage.
85
+ # Includes the Prisma CLI (+ its engines) installed in stage 1, which the
86
+ # CMD below needs for `npx prisma migrate deploy`.
73
87
  COPY --from=dependencies --chown=nodejs:nodejs /app/node_modules ./node_modules
74
88
 
75
89
  # Copy generated Prisma client from builder stage (not present in prod deps)
@@ -91,12 +105,22 @@ USER nodejs
91
105
  # Expose port (matches default PORT in env.ts; override via PORT env var)
92
106
  EXPOSE 8000
93
107
 
94
- # Health check for container orchestration
95
- HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
108
+ # Health check for container orchestration.
109
+ # start-period is 60s because CMD runs `prisma migrate deploy` BEFORE the server
110
+ # starts listening — a slow migration must not be counted as unhealthy, or
111
+ # Coolify restart-loops the container mid-migration.
112
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
96
113
  CMD node -e "const p=process.env.PORT||8000;require('http').get('http://localhost:'+p+'/api/v1/live',(r)=>{process.exit(r.statusCode===200?0:1)})"
97
114
 
98
115
  # Use dumb-init to handle signals properly
99
116
  ENTRYPOINT ["dumb-init", "--"]
100
117
 
101
- # Start the application
102
- CMD ["node", "dist/server.js"]
118
+ # Start the application — apply any pending Prisma migrations first, then boot.
119
+ # Runs inside Coolify on every container start, where DATABASE_URL is reachable
120
+ # (it isn't from CI, which is why migrations belong here and not in a workflow).
121
+ # `migrate deploy` is idempotent: it applies only pending migrations and exits 0
122
+ # when there is nothing to do. Requirements, all satisfied above:
123
+ # - prisma CLI in ./node_modules (installed in stage 1, copied with node_modules)
124
+ # - ./prisma/schema.prisma + ./prisma/migrations (copied from builder)
125
+ # `exec` replaces the shell so node receives signals from dumb-init directly.
126
+ CMD ["sh", "-c", "npx prisma migrate deploy && exec node dist/server.js"]
@@ -1,5 +1,14 @@
1
1
  # WARNING: These credentials are for LOCAL DEVELOPMENT ONLY.
2
2
  # Change all passwords and secrets before using in any shared or production environment.
3
+ #
4
+ # Security posture:
5
+ # - All published ports are bound to 127.0.0.1 — nothing is reachable from the
6
+ # network (an unpassworded Redis or root-password MySQL on 0.0.0.0 is an open
7
+ # door on shared networks/VPS dev boxes). Don't remove the 127.0.0.1 prefix.
8
+ # - The admin UIs (phpMyAdmin, Redis Commander) are behind the "tools" profile
9
+ # and do NOT start by default:
10
+ # docker compose up -d → MySQL + Redis only
11
+ # docker compose --profile tools up -d → also phpMyAdmin + Redis Commander
3
12
 
4
13
  name: {{PROJECT_NAME}}
5
14
 
@@ -9,7 +18,7 @@ services:
9
18
  container_name: {{PROJECT_NAME}}-mysql
10
19
  restart: unless-stopped
11
20
  ports:
12
- - '${MYSQL_PORT:-{{MYSQL_PORT}}}:3306'
21
+ - '127.0.0.1:${MYSQL_PORT:-{{MYSQL_PORT}}}:3306'
13
22
  environment:
14
23
  MYSQL_ROOT_PASSWORD: rootpassword
15
24
  MYSQL_DATABASE: {{DATABASE_NAME}}
@@ -27,8 +36,9 @@ services:
27
36
  image: phpmyadmin:latest
28
37
  container_name: {{PROJECT_NAME}}-phpmyadmin
29
38
  restart: unless-stopped
39
+ profiles: ["tools"]
30
40
  ports:
31
- - '${PHPMYADMIN_PORT:-{{PHPMYADMIN_PORT}}}:80'
41
+ - '127.0.0.1:${PHPMYADMIN_PORT:-{{PHPMYADMIN_PORT}}}:80'
32
42
  environment:
33
43
  PMA_HOST: mysql
34
44
  PMA_PORT: 3306
@@ -48,7 +58,7 @@ services:
48
58
  container_name: {{PROJECT_NAME}}-redis
49
59
  restart: unless-stopped
50
60
  ports:
51
- - '${REDIS_PORT:-{{REDIS_PORT}}}:6379'
61
+ - '127.0.0.1:${REDIS_PORT:-{{REDIS_PORT}}}:6379'
52
62
  volumes:
53
63
  - redis_data:/data
54
64
  networks:
@@ -63,8 +73,9 @@ services:
63
73
  image: rediscommander/redis-commander:latest
64
74
  container_name: {{PROJECT_NAME}}-redis-commander
65
75
  restart: unless-stopped
76
+ profiles: ["tools"]
66
77
  ports:
67
- - '${REDIS_COMMANDER_PORT:-{{REDIS_COMMANDER_PORT}}}:8081'
78
+ - '127.0.0.1:${REDIS_COMMANDER_PORT:-{{REDIS_COMMANDER_PORT}}}:8081'
68
79
  environment:
69
80
  REDIS_HOSTS: local:redis:6379
70
81
  depends_on: