create-tigra 3.0.0 → 3.0.3

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/package.json +1 -1
  2. package/template/client/.env.example +19 -0
  3. package/template/client/Dockerfile +3 -3
  4. package/template/client/next.config.ts +7 -42
  5. package/template/client/package-lock.json +1896 -146
  6. package/template/client/package.json +2 -0
  7. package/template/client/src/app/layout.tsx +10 -3
  8. package/template/client/src/app/providers.tsx +15 -2
  9. package/template/client/src/instrumentation-client.ts +30 -0
  10. package/template/client/src/instrumentation.ts +38 -0
  11. package/template/client/src/lib/env.ts +13 -2
  12. package/template/client/src/middleware.ts +105 -18
  13. package/template/server/.env.example +269 -236
  14. package/template/server/.env.example.production +236 -208
  15. package/template/server/Dockerfile +7 -5
  16. package/template/server/docker-compose.yml +17 -0
  17. package/template/server/package-lock.json +2136 -86
  18. package/template/server/package.json +6 -0
  19. package/template/server/src/app.ts +335 -303
  20. package/template/server/src/config/env.ts +171 -143
  21. package/template/server/src/config/rate-limit.config.ts +6 -0
  22. package/template/server/src/libs/__tests__/auth-path.test.ts +24 -0
  23. package/template/server/src/libs/__tests__/client-ip.test.ts +121 -0
  24. package/template/server/src/libs/__tests__/ip-block.test.ts +62 -0
  25. package/template/server/src/libs/__tests__/url-safety.test.ts +80 -0
  26. package/template/server/src/libs/auth-path.ts +14 -0
  27. package/template/server/src/libs/client-ip.ts +77 -0
  28. package/template/server/src/libs/ip-block.ts +220 -212
  29. package/template/server/src/libs/logger.ts +15 -0
  30. package/template/server/src/libs/observability/sentry.ts +42 -0
  31. package/template/server/src/libs/query-counter.ts +59 -0
  32. package/template/server/src/libs/requestLogger.ts +8 -2
  33. package/template/server/src/libs/storage/file-storage.service.ts +144 -16
  34. package/template/server/src/libs/url-safety.ts +121 -0
  35. package/template/server/src/modules/admin/__tests__/admin.integration.test.ts +128 -0
  36. package/template/server/src/modules/auth/__tests__/auth.integration.test.ts +138 -0
  37. package/template/server/src/modules/auth/auth.controller.ts +128 -127
  38. package/template/server/src/modules/files/__tests__/files.integration.test.ts +157 -0
  39. package/template/server/src/modules/files/files.controller.ts +180 -0
  40. package/template/server/src/modules/files/files.routes.ts +46 -0
  41. package/template/server/src/server.ts +6 -0
  42. package/template/server/src/test/integration.setup.ts +170 -0
  43. package/template/server/vitest.config.ts +10 -1
  44. package/template/server/vitest.integration.config.ts +50 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Integration test setup — REAL Testcontainers, NOT mocks.
3
+ *
4
+ * This file is the load-bearing piece of the money-path integration harness.
5
+ * It is wired via `vitest.integration.config.ts` → `setupFiles`, and is SEPARATE
6
+ * from the Docker-free unit `src/test/setup.ts` (which mocks env/DB/Redis).
7
+ *
8
+ * ── The central gotcha: env-before-import ──────────────────────────────────
9
+ * `src/config/env.ts` validates `process.env` at IMPORT time and calls
10
+ * `process.exit(1)` if DATABASE_URL / JWT_SECRET / … are missing. So the
11
+ * container connection strings MUST be in `process.env` BEFORE anything that
12
+ * transitively imports `env.ts` (which is `@/app`, `@/libs/prisma`, etc.).
13
+ *
14
+ * How we guarantee that ordering:
15
+ * 1. Vitest evaluates `setupFiles` — INCLUDING their top-level `await` — to
16
+ * completion BEFORE it evaluates any test module. We start the containers
17
+ * and run the schema sync here with TOP-LEVEL await, then populate
18
+ * process.env.
19
+ * 2. The test files themselves import `@/app` / `@/libs/prisma` LAZILY
20
+ * (`await import(...)` inside `beforeAll`), so a test module's own static
21
+ * import graph never evaluates `env.ts` before this setup has finished.
22
+ *
23
+ * ── Why a process-global singleton ─────────────────────────────────────────
24
+ * Vitest runs `setupFiles` ONCE PER TEST FILE, not once for the whole run. With
25
+ * `singleFork: true` (vitest.integration.config.ts) every test file shares ONE
26
+ * worker process, so we boot the MySQL + Redis containers exactly once and cache
27
+ * them on `globalThis`. The second (and later) test files reuse the running
28
+ * containers instead of each spinning up their own pair and racing two
29
+ * concurrent `prisma db push` invocations against a saturated Docker daemon
30
+ * (which produced intermittent `P1001: can't reach database server`).
31
+ *
32
+ * ── Teardown ───────────────────────────────────────────────────────────────
33
+ * Because the containers are shared across files, we CANNOT tear them down from
34
+ * a per-file `afterAll` (the first file to finish would stop the DB out from
35
+ * under the others). Instead we stop them once on the worker process's
36
+ * `beforeExit`, and rely on Testcontainers' Ryuk reaper as the backstop — it
37
+ * removes any container we started the moment this process dies, even on crash.
38
+ *
39
+ * The committed template ships NO `prisma/migrations/` directory (apps run
40
+ * `prisma migrate dev` locally to author their first migration). `prisma migrate
41
+ * deploy` therefore has nothing to apply against a fresh container. We use
42
+ * `prisma db push` instead — it syncs the Prisma schema straight to the empty
43
+ * container database, which is exactly what an ephemeral test DB wants.
44
+ */
45
+ import { execSync } from 'node:child_process';
46
+ import { fileURLToPath } from 'node:url';
47
+ import path from 'node:path';
48
+ import os from 'node:os';
49
+ import { MySqlContainer, type StartedMySqlContainer } from '@testcontainers/mysql';
50
+ import { RedisContainer, type StartedRedisContainer } from '@testcontainers/redis';
51
+
52
+ const __filename = fileURLToPath(import.meta.url);
53
+ const __dirname = path.dirname(__filename);
54
+ const serverRoot = path.resolve(__dirname, '..', '..');
55
+
56
+ interface IntegrationContainers {
57
+ mysql: StartedMySqlContainer;
58
+ redis: StartedRedisContainer;
59
+ databaseUrl: string;
60
+ redisUrl: string;
61
+ uploadPublicDir: string;
62
+ uploadPrivateDir: string;
63
+ }
64
+
65
+ // Singleton cache on globalThis — survives across the per-file setup invocations
66
+ // that share one worker process (singleFork). Keyed under a unique symbol so it
67
+ // can't collide with anything else on the global object.
68
+ const CONTAINERS_KEY = Symbol.for('create-tigra.integration.containers');
69
+ const TEARDOWN_KEY = Symbol.for('create-tigra.integration.teardownRegistered');
70
+
71
+ type GlobalWithContainers = typeof globalThis & {
72
+ [CONTAINERS_KEY]?: Promise<IntegrationContainers>;
73
+ [TEARDOWN_KEY]?: boolean;
74
+ };
75
+
76
+ const g = globalThis as GlobalWithContainers;
77
+
78
+ async function bootContainers(): Promise<IntegrationContainers> {
79
+ const mysql = await new MySqlContainer('mysql:8.0')
80
+ .withDatabase('test_db')
81
+ .withUsername('test')
82
+ .withUserPassword('test')
83
+ .withRootPassword('rootpass')
84
+ .start();
85
+
86
+ const redis = await new RedisContainer('redis:7-alpine').start();
87
+
88
+ // MySqlContainer.getConnectionUri() returns `mysql://user:pass@host:port/db`
89
+ // — exactly Prisma's DATABASE_URL shape.
90
+ const databaseUrl = mysql.getConnectionUri();
91
+ const redisUrl = redis.getConnectionUrl();
92
+
93
+ // Populate env BEFORE any consumer of env.ts is imported.
94
+ process.env.NODE_ENV = 'test';
95
+ process.env.DATABASE_URL = databaseUrl;
96
+ process.env.REDIS_URL = redisUrl;
97
+ process.env.JWT_SECRET = 'integration-test-jwt-secret-at-least-32-chars-long';
98
+ process.env.JWT_ACCESS_EXPIRY = '15m';
99
+ process.env.JWT_REFRESH_EXPIRY = '7d';
100
+ // Tests don't send email and don't activate via the verify flow — register
101
+ // must mint tokens and login must work immediately, so disable verification.
102
+ process.env.REQUIRE_USER_VERIFICATION = 'false';
103
+ // This is a money-PATH harness, not a rate-limit test. Disabling the limiter
104
+ // keeps the suite deterministic (no per-IP counters, no auto-block self-ban
105
+ // across repeated register/login calls).
106
+ process.env.RATE_LIMIT_ENABLED = 'false';
107
+ // In NODE_ENV=test the app's CORS config (see app.ts) is NOT the dev "allow
108
+ // all" branch — it reads CORS_ORIGIN, and an undefined value makes
109
+ // @fastify/cors throw "Invalid CORS origin option" → 500 on EVERY request.
110
+ // Pin a concrete origin so the real app boots a valid CORS policy.
111
+ process.env.CORS_ORIGIN = 'http://localhost:3000';
112
+
113
+ // Two-tier upload dirs under an isolated temp root, set BEFORE env.ts is ever
114
+ // evaluated (the storage-service singleton reads env.UPLOAD_*_DIR at
115
+ // construction, which happens when a test lazily imports @/app). The
116
+ // files-route integration test writes fixtures directly into these dirs.
117
+ const uploadsRoot = path.join(os.tmpdir(), `create-tigra-uploads-${process.pid}`);
118
+ const uploadPublicDir = path.join(uploadsRoot, 'public');
119
+ const uploadPrivateDir = path.join(uploadsRoot, 'private');
120
+ process.env.UPLOAD_PUBLIC_DIR = uploadPublicDir;
121
+ process.env.UPLOAD_PRIVATE_DIR = uploadPrivateDir;
122
+
123
+ // Sync the Prisma schema into the fresh container DB. `db push` (not
124
+ // `migrate deploy`) because the template ships no migrations. The container's
125
+ // wait strategy already guarantees MySQL is accepting connections by here.
126
+ execSync('npx prisma db push --skip-generate --accept-data-loss', {
127
+ cwd: serverRoot,
128
+ env: process.env,
129
+ stdio: 'inherit',
130
+ });
131
+
132
+ return { mysql, redis, databaseUrl, redisUrl, uploadPublicDir, uploadPrivateDir };
133
+ }
134
+
135
+ // Boot once (or await the in-flight boot) — TOP-LEVEL await, completes before
136
+ // any test module is evaluated.
137
+ if (!g[CONTAINERS_KEY]) {
138
+ g[CONTAINERS_KEY] = bootContainers();
139
+ }
140
+ const containers = await g[CONTAINERS_KEY];
141
+
142
+ // Subsequent per-file setup runs need env populated too (the boot only set it on
143
+ // the first run). Re-assert from the cached values — cheap and idempotent.
144
+ process.env.DATABASE_URL = containers.databaseUrl;
145
+ process.env.REDIS_URL = containers.redisUrl;
146
+ process.env.NODE_ENV = 'test';
147
+ process.env.JWT_SECRET ??= 'integration-test-jwt-secret-at-least-32-chars-long';
148
+ process.env.REQUIRE_USER_VERIFICATION = 'false';
149
+ process.env.RATE_LIMIT_ENABLED = 'false';
150
+ process.env.CORS_ORIGIN = 'http://localhost:3000';
151
+ process.env.UPLOAD_PUBLIC_DIR = containers.uploadPublicDir;
152
+ process.env.UPLOAD_PRIVATE_DIR = containers.uploadPrivateDir;
153
+
154
+ // Register the teardown exactly once for the whole run. Tied to the worker
155
+ // process lifecycle (NOT a per-file afterAll, which would stop the shared
156
+ // containers while later files still need them). Ryuk is the crash-safe backstop.
157
+ if (!g[TEARDOWN_KEY]) {
158
+ g[TEARDOWN_KEY] = true;
159
+ process.once('beforeExit', () => {
160
+ void containers.redis.stop();
161
+ void containers.mysql.stop();
162
+ });
163
+ }
164
+
165
+ export const containerInfo = {
166
+ databaseUrl: containers.databaseUrl,
167
+ redisUrl: containers.redisUrl,
168
+ uploadPublicDir: containers.uploadPublicDir,
169
+ uploadPrivateDir: containers.uploadPrivateDir,
170
+ };
@@ -23,11 +23,20 @@ export default defineConfig({
23
23
  functions: 80,
24
24
  branches: 80,
25
25
  statements: 80,
26
+ // Money-path forward-contract: any future payments module is held to a
27
+ // stricter bar than the 80% global gate. The scaffold ships no payments
28
+ // module, so this glob matches nothing today and is vacuously satisfied;
29
+ // the moment an app adds `src/modules/payments/**`, these thresholds
30
+ // start enforcing. Verified to not red an empty match on vitest v4.
31
+ 'src/modules/payments/**': { lines: 95, branches: 90 },
26
32
  },
27
33
  },
28
34
  setupFiles: ['./src/test/setup.ts'],
29
35
  include: ['**/__tests__/**/*.test.ts', '**/*.test.ts'],
30
- exclude: ['node_modules', 'dist'],
36
+ // Keep the unit gate Docker-free: integration tests boot Testcontainers and
37
+ // live behind `npm run test:integration` (vitest.integration.config.ts).
38
+ // The `**/*.test.ts` include would otherwise sweep in `*.integration.test.ts`.
39
+ exclude: ['node_modules', 'dist', '**/*.integration.test.ts'],
31
40
  testTimeout: 10000,
32
41
  hookTimeout: 10000,
33
42
  },
@@ -0,0 +1,50 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * Integration test config — REAL Testcontainers (MySQL 8 + Redis), app.inject().
6
+ *
7
+ * Deliberately SEPARATE from the Docker-free unit `vitest.config.ts`:
8
+ * - includes ONLY `*.integration.test.ts`
9
+ * - uses `src/test/integration.setup.ts` (boots containers) — NOT the mocking
10
+ * unit setup `src/test/setup.ts`
11
+ * - runs serially in a single fork: the containers are shared process-wide and
12
+ * started once via the setup file's top-level await, so parallel workers
13
+ * must not fight over them or each re-boot Docker.
14
+ * - generous timeouts: container boot + `prisma db push` is slow on a cold
15
+ * Docker daemon / first image pull.
16
+ *
17
+ * Run with: npm run test:integration
18
+ */
19
+ export default defineConfig({
20
+ test: {
21
+ globals: true,
22
+ environment: 'node',
23
+ setupFiles: ['./src/test/integration.setup.ts'],
24
+ include: ['**/*.integration.test.ts'],
25
+ exclude: ['node_modules', 'dist'],
26
+ // Containers are shared — never parallelize across files or forks.
27
+ // Vitest 4 flattened `poolOptions.forks.singleFork` to the top-level
28
+ // `singleFork` option (the nested form is deprecated/removed).
29
+ pool: 'forks',
30
+ singleFork: true,
31
+ fileParallelism: false,
32
+ // Container boot + image pull + schema push can take well over a minute on
33
+ // a cold daemon; the setup file does all of that under hookTimeout.
34
+ testTimeout: 120_000,
35
+ hookTimeout: 120_000,
36
+ teardownTimeout: 60_000,
37
+ // No coverage gate on the integration suite — coverage thresholds live in
38
+ // the unit config. This suite is about real end-to-end behavior.
39
+ },
40
+ resolve: {
41
+ alias: {
42
+ '@': resolve(__dirname, './src'),
43
+ '@modules': resolve(__dirname, './src/modules'),
44
+ '@libs': resolve(__dirname, './src/libs'),
45
+ '@config': resolve(__dirname, './src/config'),
46
+ '@shared': resolve(__dirname, './src/shared'),
47
+ '@jobs': resolve(__dirname, './src/jobs'),
48
+ },
49
+ },
50
+ });