@quanticjs/create-app 0.1.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.
Files changed (136) hide show
  1. package/dist/deps.d.ts +4 -0
  2. package/dist/deps.js +97 -0
  3. package/dist/deps.js.map +1 -0
  4. package/dist/generators/backend.d.ts +2 -0
  5. package/dist/generators/backend.js +20 -0
  6. package/dist/generators/backend.js.map +1 -0
  7. package/dist/generators/bff.d.ts +2 -0
  8. package/dist/generators/bff.js +9 -0
  9. package/dist/generators/bff.js.map +1 -0
  10. package/dist/generators/claude.d.ts +2 -0
  11. package/dist/generators/claude.js +45 -0
  12. package/dist/generators/claude.js.map +1 -0
  13. package/dist/generators/docker.d.ts +2 -0
  14. package/dist/generators/docker.js +10 -0
  15. package/dist/generators/docker.js.map +1 -0
  16. package/dist/generators/e2e.d.ts +2 -0
  17. package/dist/generators/e2e.js +8 -0
  18. package/dist/generators/e2e.js.map +1 -0
  19. package/dist/generators/frontend.d.ts +2 -0
  20. package/dist/generators/frontend.js +28 -0
  21. package/dist/generators/frontend.js.map +1 -0
  22. package/dist/generators/module.d.ts +2 -0
  23. package/dist/generators/module.js +35 -0
  24. package/dist/generators/module.js.map +1 -0
  25. package/dist/generators/root.d.ts +2 -0
  26. package/dist/generators/root.js +7 -0
  27. package/dist/generators/root.js.map +1 -0
  28. package/dist/generators/scripts.d.ts +2 -0
  29. package/dist/generators/scripts.js +10 -0
  30. package/dist/generators/scripts.js.map +1 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +40 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/prompts.d.ts +8 -0
  35. package/dist/prompts.js +53 -0
  36. package/dist/prompts.js.map +1 -0
  37. package/dist/scaffold.d.ts +2 -0
  38. package/dist/scaffold.js +79 -0
  39. package/dist/scaffold.js.map +1 -0
  40. package/dist/utils/exec.d.ts +2 -0
  41. package/dist/utils/exec.js +14 -0
  42. package/dist/utils/exec.js.map +1 -0
  43. package/dist/utils/template.d.ts +10 -0
  44. package/dist/utils/template.js +20 -0
  45. package/dist/utils/template.js.map +1 -0
  46. package/dist/utils/validate.d.ts +4 -0
  47. package/dist/utils/validate.js +27 -0
  48. package/dist/utils/validate.js.map +1 -0
  49. package/package.json +50 -0
  50. package/templates/backend/app.module.ts.ejs +61 -0
  51. package/templates/backend/bff.controller.ts.ejs +60 -0
  52. package/templates/backend/bff.module.ts.ejs +10 -0
  53. package/templates/backend/bff.service.ts.ejs +6 -0
  54. package/templates/backend/create-schema.migration.ts.ejs +15 -0
  55. package/templates/backend/data-source.ts.ejs +13 -0
  56. package/templates/backend/env.example.ejs +30 -0
  57. package/templates/backend/main.ts.ejs +43 -0
  58. package/templates/backend/module.ts.ejs +14 -0
  59. package/templates/backend/nest-cli.json.ejs +8 -0
  60. package/templates/backend/package.json.ejs +23 -0
  61. package/templates/backend/tsconfig.build.json.ejs +4 -0
  62. package/templates/backend/tsconfig.json.ejs +24 -0
  63. package/templates/claude/CLAUDE.md.ejs +86 -0
  64. package/templates/claude/hooks/auto-format.sh +22 -0
  65. package/templates/claude/hooks/check-secrets.sh +49 -0
  66. package/templates/claude/hooks/guard-destructive.sh +42 -0
  67. package/templates/claude/hooks/on-compaction.sh +29 -0
  68. package/templates/claude/mcp.json +10 -0
  69. package/templates/claude/rules/api-patterns.md +86 -0
  70. package/templates/claude/rules/auth-patterns.md +109 -0
  71. package/templates/claude/rules/backend-patterns.md +421 -0
  72. package/templates/claude/rules/database-patterns.md +96 -0
  73. package/templates/claude/rules/docker-patterns.md +86 -0
  74. package/templates/claude/rules/frontend-patterns.md +262 -0
  75. package/templates/claude/rules/observability-backend.md +132 -0
  76. package/templates/claude/rules/observability-frontend.md +49 -0
  77. package/templates/claude/rules/playwright-mcp.md +80 -0
  78. package/templates/claude/rules/resilience-ops.md +103 -0
  79. package/templates/claude/rules/testing-e2e-ui.md +190 -0
  80. package/templates/claude/rules/testing-patterns.md +94 -0
  81. package/templates/claude/rules/workflow-backend.md +64 -0
  82. package/templates/claude/rules/workflow-frontend.md +60 -0
  83. package/templates/claude/settings.json +68 -0
  84. package/templates/claude/skills/add-api-endpoint/SKILL.md +59 -0
  85. package/templates/claude/skills/add-auth-endpoint/SKILL.md +68 -0
  86. package/templates/claude/skills/add-entity/SKILL.md +56 -0
  87. package/templates/claude/skills/add-event/SKILL.md +127 -0
  88. package/templates/claude/skills/add-feature/SKILL.md +20 -0
  89. package/templates/claude/skills/add-frontend-page/SKILL.md +75 -0
  90. package/templates/claude/skills/add-handler/SKILL.md +105 -0
  91. package/templates/claude/skills/add-integration/SKILL.md +176 -0
  92. package/templates/claude/skills/add-migration/SKILL.md +20 -0
  93. package/templates/claude/skills/add-module/SKILL.md +89 -0
  94. package/templates/claude/skills/add-realtime/SKILL.md +119 -0
  95. package/templates/claude/skills/audit-rules/SKILL.md +120 -0
  96. package/templates/claude/skills/debugging/SKILL.md +105 -0
  97. package/templates/claude/skills/docker-dev/SKILL.md +86 -0
  98. package/templates/claude/skills/e2e-audit/SKILL.md +85 -0
  99. package/templates/claude/skills/e2e-full/SKILL.md +132 -0
  100. package/templates/claude/skills/e2e-scan/SKILL.md +171 -0
  101. package/templates/claude/skills/e2e-verify/SKILL.md +145 -0
  102. package/templates/claude/skills/fix-bug/SKILL.md +33 -0
  103. package/templates/claude/skills/implement-spec/SKILL.md +98 -0
  104. package/templates/claude/skills/review-code/SKILL.md +109 -0
  105. package/templates/claude/skills/review-spec/SKILL.md +216 -0
  106. package/templates/claude/skills/run-tests/SKILL.md +37 -0
  107. package/templates/claude/skills/specify/SKILL.md +87 -0
  108. package/templates/claude/skills/write-backend-tests/SKILL.md +182 -0
  109. package/templates/claude/skills/write-ui-tests/SKILL.md +118 -0
  110. package/templates/docker/Dockerfile.client.ejs +14 -0
  111. package/templates/docker/Dockerfile.ejs +28 -0
  112. package/templates/docker/docker-compose.test.yml.ejs +54 -0
  113. package/templates/docker/docker-compose.yml.ejs +76 -0
  114. package/templates/docker/nginx.conf.ejs +21 -0
  115. package/templates/frontend/App.tsx.ejs +64 -0
  116. package/templates/frontend/DashboardPage.tsx.ejs +37 -0
  117. package/templates/frontend/LoginPage.tsx.ejs +20 -0
  118. package/templates/frontend/NotFoundPage.tsx.ejs +15 -0
  119. package/templates/frontend/api-client.ts.ejs +15 -0
  120. package/templates/frontend/index.css.ejs +57 -0
  121. package/templates/frontend/index.html.ejs +13 -0
  122. package/templates/frontend/main.tsx.ejs +10 -0
  123. package/templates/frontend/package.json.ejs +16 -0
  124. package/templates/frontend/playwright.config.ts.ejs +20 -0
  125. package/templates/frontend/postcss.config.js.ejs +3 -0
  126. package/templates/frontend/smoke.spec.ts.ejs +37 -0
  127. package/templates/frontend/tailwind.config.ts.ejs +56 -0
  128. package/templates/frontend/tsconfig.json.ejs +25 -0
  129. package/templates/frontend/tsconfig.node.json.ejs +15 -0
  130. package/templates/frontend/utils.ts.ejs +6 -0
  131. package/templates/frontend/vite-env.d.ts.ejs +1 -0
  132. package/templates/frontend/vite.config.ts.ejs +20 -0
  133. package/templates/root/gitignore.ejs +9 -0
  134. package/templates/root/prettierrc.ejs +7 -0
  135. package/templates/scripts/init-db.sh.ejs +8 -0
  136. package/templates/scripts/save-auth-state.ts.ejs +24 -0
@@ -0,0 +1,182 @@
1
+ # Write Tests — Backend
2
+
3
+ ## Usage
4
+ ```
5
+ /write-backend-tests src/project/commands/CreateItemHandler.ts
6
+ /write-backend-tests src/project/controllers/ItemsController.ts
7
+ ```
8
+
9
+ ## 1. Handler Unit Test (`*.spec.ts`)
10
+
11
+ Test the handler directly — mock repositories, assert `Result<T>`.
12
+
13
+ ```typescript
14
+ import { Test } from '@nestjs/testing';
15
+ import { getRepositoryToken } from '@nestjs/typeorm';
16
+ import { createMockRepository, ErrorType } from '@quanticjs/core';
17
+ import { CreateItemHandler } from './CreateItemHandler';
18
+ import { CreateItemCommand } from './CreateItemCommand';
19
+ import { Item } from '../entities/Item.entity';
20
+
21
+ describe('CreateItemHandler', () => {
22
+ let handler: CreateItemHandler;
23
+ let itemRepo: ReturnType<typeof createMockRepository>;
24
+
25
+ beforeEach(async () => {
26
+ itemRepo = createMockRepository();
27
+ itemRepo.create.mockImplementation((dto: any) => ({
28
+ ...dto,
29
+ id: 'item-1',
30
+ createdAt: new Date(),
31
+ updatedAt: new Date(),
32
+ }));
33
+ itemRepo.save.mockImplementation((entity: any) => Promise.resolve(entity));
34
+
35
+ const module = await Test.createTestingModule({
36
+ providers: [
37
+ CreateItemHandler,
38
+ { provide: getRepositoryToken(Item), useValue: itemRepo },
39
+ ],
40
+ }).compile();
41
+
42
+ handler = module.get(CreateItemHandler);
43
+ });
44
+
45
+ it('should create item and return success', async () => {
46
+ const command = new CreateItemCommand({ name: 'Widget', userId: 'user-1' });
47
+ const result = await handler.execute(command);
48
+
49
+ expect(result.isSuccess).toBe(true);
50
+ expect(result.value!.name).toBe('Widget');
51
+ expect(itemRepo.save).toHaveBeenCalledTimes(1);
52
+ });
53
+
54
+ it('should return conflict when name already exists', async () => {
55
+ itemRepo.findOne.mockResolvedValue({ id: 'existing', name: 'Widget' });
56
+ const command = new CreateItemCommand({ name: 'Widget', userId: 'user-1' });
57
+ const result = await handler.execute(command);
58
+
59
+ expect(result.isSuccess).toBe(false);
60
+ expect(result.errorType).toBe(ErrorType.Conflict);
61
+ expect(itemRepo.save).not.toHaveBeenCalled();
62
+ });
63
+ });
64
+ ```
65
+
66
+ ### Key Rules
67
+ - Use `createMockRepository()` from `@quanticjs/core` — never hand-roll mocks
68
+ - Override specific methods with `.mockImplementation()` for test scenarios
69
+ - Assert via `result.isSuccess`, `result.value`, `result.errorType` — never try/catch
70
+ - Test both success AND failure paths (NotFound, Conflict, Forbidden, ValidationError)
71
+
72
+ ## 2. Controller Integration Test (`*.spec.ts`)
73
+
74
+ Test HTTP request → validation → bus dispatch → response. Mock CommandBus/QueryBus.
75
+
76
+ ```typescript
77
+ import { Test } from '@nestjs/testing';
78
+ import { INestApplication, ValidationPipe } from '@nestjs/common';
79
+ import { CommandBus, QueryBus } from '@nestjs/cqrs';
80
+ import { APP_GUARD } from '@nestjs/core';
81
+ import request from 'supertest';
82
+ import { Result } from '@quanticjs/core';
83
+ import { ItemsController } from './ItemsController';
84
+
85
+ class MockAuthGuard {
86
+ canActivate(context: any) {
87
+ const req = context.switchToHttp().getRequest();
88
+ req.user = {
89
+ keycloakId: 'user-1',
90
+ email: 'test@test.com',
91
+ roles: ['admin'],
92
+ };
93
+ return true;
94
+ }
95
+ }
96
+
97
+ describe('ItemsController (integration)', () => {
98
+ let app: INestApplication;
99
+ let commandBus: { execute: jest.Mock };
100
+ let queryBus: { execute: jest.Mock };
101
+
102
+ beforeAll(async () => {
103
+ commandBus = { execute: jest.fn() };
104
+ queryBus = { execute: jest.fn() };
105
+
106
+ const module = await Test.createTestingModule({
107
+ controllers: [ItemsController],
108
+ providers: [
109
+ { provide: CommandBus, useValue: commandBus },
110
+ { provide: QueryBus, useValue: queryBus },
111
+ { provide: APP_GUARD, useClass: MockAuthGuard },
112
+ ],
113
+ }).compile();
114
+
115
+ app = module.createNestApplication();
116
+ app.useGlobalPipes(new ValidationPipe({
117
+ whitelist: true,
118
+ forbidNonWhitelisted: true,
119
+ transform: true,
120
+ }));
121
+ await app.init();
122
+ });
123
+
124
+ afterAll(() => app.close());
125
+
126
+ it('should create item with valid payload', async () => {
127
+ commandBus.execute.mockResolvedValue(Result.success({ id: 'item-1' }));
128
+
129
+ const res = await request(app.getHttpServer())
130
+ .post('/api/items')
131
+ .send({ name: 'Widget', description: 'A widget' });
132
+
133
+ expect(res.status).toBe(201);
134
+ expect(commandBus.execute).toHaveBeenCalledTimes(1);
135
+ });
136
+
137
+ it('should reject invalid payload', async () => {
138
+ const res = await request(app.getHttpServer())
139
+ .post('/api/items')
140
+ .send({});
141
+
142
+ expect(res.status).toBe(400);
143
+ expect(commandBus.execute).not.toHaveBeenCalled();
144
+ });
145
+ });
146
+ ```
147
+
148
+ ### Key Rules
149
+ - MockAuthGuard sets `req.user` with `keycloakId`, `email`, `roles`
150
+ - ValidationPipe with `whitelist + forbidNonWhitelisted` — tests class-validator DTOs
151
+ - Mock CommandBus/QueryBus return `Result.success()` or `Result.failure()`
152
+ - Test validation (400), auth (401/403), success (200/201), and not-found (404) cases
153
+ - Use `beforeAll` / `afterAll` for app lifecycle (not beforeEach — too slow)
154
+
155
+ ## 3. Validator Unit Test (`*.spec.ts`)
156
+
157
+ ```typescript
158
+ describe('CreateItemValidator', () => {
159
+ const validator = new CreateItemValidator();
160
+
161
+ it('should pass with valid input', () => {
162
+ const result = validator.validate(new CreateItemCommand({ name: 'Valid' }));
163
+ expect(result.isSuccess).toBe(true);
164
+ });
165
+
166
+ it('should fail when name is empty', () => {
167
+ const result = validator.validate(new CreateItemCommand({ name: '' }));
168
+ expect(result.isSuccess).toBe(false);
169
+ expect(result.errorType).toBe(ErrorType.ValidationError);
170
+ });
171
+ });
172
+ ```
173
+
174
+ ## Test File Naming
175
+ | Type | Pattern | Location |
176
+ |------|---------|----------|
177
+ | Handler unit | `CreateItemHandler.spec.ts` | Next to handler file |
178
+ | Validator unit | `CreateItemValidator.spec.ts` | Next to validator file |
179
+ | Controller integration | `ItemsController.spec.ts` | Next to controller file |
180
+
181
+ ## Mandatory Coverage
182
+ Every handler test must cover: happy path, validation failure, not found, conflict, permission check.
@@ -0,0 +1,118 @@
1
+ # Write UI Tests
2
+
3
+ Write mocked E2E specs using Playwright. All APIs are mocked with `page.route()` — tests run fast, need no backend, and verify the UI renders correctly for all states.
4
+
5
+ ## Usage
6
+ ```
7
+ /write-ui-tests /projects
8
+ /write-ui-tests /settings/profile
9
+ /write-ui-tests client/src/pages/SettingsPage.tsx
10
+ ```
11
+
12
+ ## Steps
13
+
14
+ ### Step 1: Write mocked E2E spec (`client/e2e/*.spec.ts`)
15
+
16
+ All 4 states are **mandatory**:
17
+
18
+ ```typescript
19
+ import { test, expect } from '@playwright/test';
20
+
21
+ test.describe('Projects Page', () => {
22
+ test.beforeEach(async ({ page }) => {
23
+ // Mock auth
24
+ await page.route('**/auth/me', route => route.fulfill({
25
+ status: 200,
26
+ body: JSON.stringify({ keycloakId: 'kc-1', roles: ['user'] }),
27
+ }));
28
+ });
29
+
30
+ // HAPPY PATH
31
+ test('shows projects on success', async ({ page }) => {
32
+ await page.route('**/api/projects*', route => route.fulfill({
33
+ status: 200, json: { data: [{ id: '1', name: 'Project A' }] },
34
+ }));
35
+ await page.goto('/projects');
36
+ await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible();
37
+ });
38
+
39
+ // ERROR STATE
40
+ test('shows error on API failure', async ({ page }) => {
41
+ await page.route('**/api/projects*', route => route.fulfill({ status: 500 }));
42
+ await page.goto('/projects');
43
+ await expect(page.getByText(/something went wrong|error|try again/i)).toBeVisible();
44
+ });
45
+
46
+ // EMPTY STATE
47
+ test('shows empty state when no projects', async ({ page }) => {
48
+ await page.route('**/api/projects*', route => route.fulfill({
49
+ status: 200, json: { data: [] },
50
+ }));
51
+ await page.goto('/projects');
52
+ await expect(page.getByText(/no projects/i)).toBeVisible();
53
+ });
54
+
55
+ // LOADING STATE
56
+ test('shows loading while fetching', async ({ page }) => {
57
+ await page.route('**/api/projects*', async route => {
58
+ await new Promise(r => setTimeout(r, 2000));
59
+ await route.fulfill({ status: 200, json: { data: [] } });
60
+ });
61
+ await page.goto('/projects');
62
+ // Assert skeleton or spinner visible before data loads
63
+ });
64
+ });
65
+ ```
66
+
67
+ ### Step 2: Write component test (`client/src/<path>/__tests__/<Component>.test.tsx`)
68
+
69
+ ```typescript
70
+ import { render, screen } from '@testing-library/react';
71
+ import { MemoryRouter } from 'react-router-dom';
72
+ import { createClient, QuanticProvider } from '@quanticjs/react-core';
73
+ import { QuanticQueryProvider } from '@quanticjs/react-query';
74
+ import { ToastProvider, ErrorBoundary } from '@quanticjs/react-ui';
75
+ import { describe, it, expect } from 'vitest';
76
+
77
+ const testClient = createClient({ baseUrl: '/api' });
78
+
79
+ function TestWrapper({ children }: { children: React.ReactNode }) {
80
+ return (
81
+ <QuanticProvider client={testClient}>
82
+ <QuanticQueryProvider>
83
+ <ToastProvider>
84
+ <ErrorBoundary fallback={<div>Error</div>}>
85
+ <MemoryRouter>{children}</MemoryRouter>
86
+ </ErrorBoundary>
87
+ </ToastProvider>
88
+ </QuanticQueryProvider>
89
+ </QuanticProvider>
90
+ );
91
+ }
92
+
93
+ describe('ProjectsPage', () => {
94
+ it('renders heading', () => {
95
+ render(<ProjectsPage />, { wrapper: TestWrapper });
96
+ expect(screen.getByRole('heading', { name: 'Projects' })).toBeInTheDocument();
97
+ });
98
+ });
99
+ ```
100
+
101
+ ## Selector Rules
102
+ - `getByRole` > `getByLabelText` > `getByText` > `getByPlaceholder` > `getByTestId`
103
+ - NEVER use CSS selectors (`.class`, `#id`, `div > span`)
104
+ - NEVER use `page.waitForTimeout()`
105
+ - NEVER use `fireEvent` — use `userEvent` in component tests
106
+
107
+ ## Mock Rules
108
+ - Mock all APIs with `page.route()`
109
+ - Auth mocking: mock `/auth/me` endpoint — NEVER use `localStorage`
110
+
111
+ ## Assertion Rules
112
+ - Assertions verify **behavior**, not just element existence
113
+ - Verify API calls were made with `waitForResponse` for mutation actions
114
+ - Test interactive outcomes: after clicking a filter, verify filtered results
115
+ - NEVER use `expect(await el.isVisible()).toBe(true)` — use `await expect(el).toBeVisible()`
116
+
117
+ ## Responsive Testing
118
+ Test at mobile viewport (375x812) and desktop for all pages.
@@ -0,0 +1,14 @@
1
+ FROM node:20-alpine AS builder
2
+ WORKDIR /app
3
+ COPY package*.json ./
4
+ RUN npm ci
5
+ COPY . .
6
+ RUN npm run build
7
+
8
+ FROM nginx:alpine AS production
9
+ COPY --from=builder /app/dist /usr/share/nginx/html
10
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
11
+ RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup
12
+ USER appuser
13
+ EXPOSE 80
14
+ CMD ["nginx", "-g", "daemon off;"]
@@ -0,0 +1,28 @@
1
+ # ---- Development ----
2
+ FROM node:20-alpine AS development
3
+ WORKDIR /app
4
+ COPY package*.json ./
5
+ RUN npm ci
6
+ COPY tsconfig*.json nest-cli.json ./
7
+ # src/ is volume-mounted at runtime
8
+ CMD ["npm", "run", "start:dev"]
9
+
10
+ # ---- Builder ----
11
+ FROM node:20-alpine AS builder
12
+ WORKDIR /app
13
+ COPY package*.json ./
14
+ RUN npm ci
15
+ COPY . .
16
+ RUN npm run build
17
+
18
+ # ---- Production ----
19
+ FROM node:20-alpine AS production
20
+ WORKDIR /app
21
+ ENV NODE_ENV=production
22
+ RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup
23
+ COPY package*.json ./
24
+ RUN npm ci --only=production && npm cache clean --force
25
+ COPY --from=builder /app/dist ./dist
26
+ USER appuser
27
+ EXPOSE 3000
28
+ CMD ["node", "dist/main.js"]
@@ -0,0 +1,54 @@
1
+ services:
2
+ postgres-test:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_USER: postgres
6
+ POSTGRES_PASSWORD: postgres
7
+ POSTGRES_DB: <%= projectNameUnderscored %>_test
8
+ healthcheck:
9
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
10
+ interval: 5s
11
+ timeout: 5s
12
+ retries: 5
13
+
14
+ redis-test:
15
+ image: redis:7-alpine
16
+ healthcheck:
17
+ test: ["CMD", "redis-cli", "ping"]
18
+ interval: 5s
19
+ timeout: 5s
20
+ retries: 5
21
+
22
+ keycloak-test:
23
+ image: quay.io/keycloak/keycloak:24.0
24
+ command: start-dev
25
+ environment:
26
+ KC_DB: dev-mem
27
+ KEYCLOAK_ADMIN: admin
28
+ KEYCLOAK_ADMIN_PASSWORD: admin
29
+ KC_HOSTNAME_STRICT: "false"
30
+ KC_HTTP_ENABLED: "true"
31
+ ports:
32
+ - "8099:8080"
33
+
34
+ backend-test:
35
+ build:
36
+ context: .
37
+ target: development
38
+ ports:
39
+ - "3099:3000"
40
+ environment:
41
+ NODE_ENV: test
42
+ PORT: 3000
43
+ DATABASE_HOST: postgres-test
44
+ DATABASE_PORT: 5432
45
+ DATABASE_USER: postgres
46
+ DATABASE_PASSWORD: postgres
47
+ DATABASE_NAME: <%= projectNameUnderscored %>_test
48
+ REDIS_URL: redis://redis-test:6379
49
+ KEYCLOAK_URL: http://keycloak-test:8080
50
+ depends_on:
51
+ postgres-test:
52
+ condition: service_healthy
53
+ redis-test:
54
+ condition: service_healthy
@@ -0,0 +1,76 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_USER: postgres
6
+ POSTGRES_PASSWORD: postgres
7
+ POSTGRES_DB: <%= projectNameUnderscored %>
8
+ volumes:
9
+ - postgres_data:/var/lib/postgresql/data
10
+ - ./scripts/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
11
+ healthcheck:
12
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
13
+ interval: 5s
14
+ timeout: 5s
15
+ retries: 5
16
+
17
+ redis:
18
+ image: redis:7-alpine
19
+ healthcheck:
20
+ test: ["CMD", "redis-cli", "ping"]
21
+ interval: 5s
22
+ timeout: 5s
23
+ retries: 5
24
+
25
+ keycloak:
26
+ image: quay.io/keycloak/keycloak:24.0
27
+ command: start-dev
28
+ environment:
29
+ KC_DB: dev-mem
30
+ KEYCLOAK_ADMIN: admin
31
+ KEYCLOAK_ADMIN_PASSWORD: admin
32
+ KC_HOSTNAME_STRICT: "false"
33
+ KC_HTTP_ENABLED: "true"
34
+ ports:
35
+ - "8080:8080"
36
+ healthcheck:
37
+ test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080; echo -e 'GET /health/ready HTTP/1.1\\r\\nhost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3; cat <&3 | grep -q '200'"]
38
+ interval: 10s
39
+ timeout: 5s
40
+ retries: 15
41
+
42
+ backend:
43
+ build:
44
+ context: .
45
+ target: development
46
+ ports:
47
+ - "3000:3000"
48
+ environment:
49
+ NODE_ENV: development
50
+ PORT: 3000
51
+ DATABASE_HOST: postgres
52
+ DATABASE_PORT: 5432
53
+ DATABASE_USER: postgres
54
+ DATABASE_PASSWORD: postgres
55
+ DATABASE_NAME: <%= projectNameUnderscored %>
56
+ REDIS_URL: redis://redis:6379
57
+ KEYCLOAK_URL: http://keycloak:8080
58
+ KEYCLOAK_REALM: <%= projectName %>
59
+ KEYCLOAK_CLIENT_ID: <%= projectName %>-backend
60
+ KEYCLOAK_CLIENT_SECRET: change-me
61
+ SESSION_SECRET: dev-secret-change-in-prod
62
+ volumes:
63
+ - ./src:/app/src:cached
64
+ - ./nest-cli.json:/app/nest-cli.json:ro
65
+ - ./tsconfig.json:/app/tsconfig.json:ro
66
+ - ./tsconfig.build.json:/app/tsconfig.build.json:ro
67
+ depends_on:
68
+ postgres:
69
+ condition: service_healthy
70
+ redis:
71
+ condition: service_healthy
72
+ keycloak:
73
+ condition: service_healthy
74
+
75
+ volumes:
76
+ postgres_data:
@@ -0,0 +1,21 @@
1
+ server {
2
+ listen 80;
3
+ root /usr/share/nginx/html;
4
+ index index.html;
5
+
6
+ location / {
7
+ try_files $uri $uri/ /index.html;
8
+ }
9
+
10
+ location /api/ {
11
+ proxy_pass http://backend:3000;
12
+ proxy_set_header Host $host;
13
+ proxy_set_header X-Real-IP $remote_addr;
14
+ }
15
+
16
+ location /auth/ {
17
+ proxy_pass http://backend:3000;
18
+ proxy_set_header Host $host;
19
+ proxy_set_header X-Real-IP $remote_addr;
20
+ }
21
+ }
@@ -0,0 +1,64 @@
1
+ import { lazy, Suspense } from 'react';
2
+ import { createBrowserRouter, RouterProvider } from 'react-router-dom';
3
+ import * as Sentry from '@sentry/react';
4
+ import { QuanticProvider, QuanticQueryProvider } from '@quanticjs/react-core';
5
+ import { ToastProvider, ErrorBoundary, Spinner } from '@quanticjs/react-ui';
6
+ import { apiClient } from './lib/api-client';
7
+
8
+ const DashboardPage = lazy(() => import('./pages/DashboardPage'));
9
+ const LoginPage = lazy(() => import('./pages/LoginPage'));
10
+ const NotFoundPage = lazy(() => import('./pages/NotFoundPage'));
11
+
12
+ const router = createBrowserRouter([
13
+ {
14
+ path: '/login',
15
+ element: (
16
+ <Suspense fallback={<Spinner size="lg" />}>
17
+ <LoginPage />
18
+ </Suspense>
19
+ ),
20
+ },
21
+ {
22
+ path: '/',
23
+ element: (
24
+ <Suspense fallback={<Spinner size="lg" />}>
25
+ <DashboardPage />
26
+ </Suspense>
27
+ ),
28
+ },
29
+ {
30
+ path: '*',
31
+ element: (
32
+ <Suspense fallback={<Spinner size="lg" />}>
33
+ <NotFoundPage />
34
+ </Suspense>
35
+ ),
36
+ },
37
+ ]);
38
+
39
+ export function App() {
40
+ return (
41
+ <Sentry.ErrorBoundary fallback={<div>Something went wrong.</div>}>
42
+ <QuanticProvider client={apiClient}>
43
+ <QuanticQueryProvider>
44
+ <ToastProvider>
45
+ <ErrorBoundary
46
+ fallback={(error, reset) => (
47
+ <div className="flex min-h-screen items-center justify-center">
48
+ <div className="text-center">
49
+ <h1 className="text-2xl font-bold">Something went wrong</h1>
50
+ <button onClick={reset} className="mt-4 underline">
51
+ Try again
52
+ </button>
53
+ </div>
54
+ </div>
55
+ )}
56
+ >
57
+ <RouterProvider router={router} />
58
+ </ErrorBoundary>
59
+ </ToastProvider>
60
+ </QuanticQueryProvider>
61
+ </QuanticProvider>
62
+ </Sentry.ErrorBoundary>
63
+ );
64
+ }
@@ -0,0 +1,37 @@
1
+ import { useApiQuery } from '@quanticjs/react-query';
2
+ import { Skeleton } from '@quanticjs/react-ui';
3
+
4
+ interface UserSession {
5
+ id: string;
6
+ email: string;
7
+ displayName: string;
8
+ role: string;
9
+ roles: string[];
10
+ permissions: string[];
11
+ }
12
+
13
+ export default function DashboardPage() {
14
+ const { data: session, isLoading } = useApiQuery<UserSession>(
15
+ ['auth', 'session'],
16
+ (api) => api.get('/auth/me'),
17
+ );
18
+
19
+ if (isLoading) {
20
+ return (
21
+ <div className="p-8 space-y-4">
22
+ <Skeleton className="h-8 w-48" />
23
+ <Skeleton className="h-4 w-64" />
24
+ <Skeleton className="h-32 w-full" />
25
+ </div>
26
+ );
27
+ }
28
+
29
+ return (
30
+ <div className="p-8">
31
+ <h1 className="text-2xl font-bold text-foreground">
32
+ Welcome, {session?.displayName}
33
+ </h1>
34
+ <p className="text-muted-foreground mt-2">Dashboard content goes here.</p>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,20 @@
1
+ export default function LoginPage() {
2
+ const handleLogin = () => {
3
+ window.location.href = '/auth/login?provider=keycloak&returnTo=/';
4
+ };
5
+
6
+ return (
7
+ <div className="flex min-h-screen items-center justify-center bg-background">
8
+ <div className="w-full max-w-sm space-y-6 text-center">
9
+ <h1 className="text-3xl font-bold text-foreground">Welcome</h1>
10
+ <p className="text-muted-foreground">Sign in to continue</p>
11
+ <button
12
+ onClick={handleLogin}
13
+ className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
14
+ >
15
+ Sign in with SSO
16
+ </button>
17
+ </div>
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,15 @@
1
+ import { Link } from 'react-router-dom';
2
+
3
+ export default function NotFoundPage() {
4
+ return (
5
+ <div className="flex min-h-screen items-center justify-center bg-background">
6
+ <div className="text-center">
7
+ <h1 className="text-6xl font-bold text-foreground">404</h1>
8
+ <p className="mt-4 text-muted-foreground">Page not found</p>
9
+ <Link to="/" className="mt-6 inline-block text-primary underline">
10
+ Go home
11
+ </Link>
12
+ </div>
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,15 @@
1
+ import { createClient, correlationId } from '@quanticjs/react-core';
2
+
3
+ export const apiClient = createClient({
4
+ baseUrl: '/api',
5
+ interceptors: [correlationId()],
6
+ auth: {
7
+ refresh: () =>
8
+ fetch('/auth/refresh', { method: 'POST', credentials: 'include' }).then((r) => {
9
+ if (!r.ok) throw r;
10
+ }),
11
+ onRefreshFailure: () => {
12
+ window.location.href = '/auth/login?returnTo=' + encodeURIComponent(window.location.pathname);
13
+ },
14
+ },
15
+ });