umpordez 0.0.2 → 1.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 (151) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/build.mjs +409 -0
  3. package/cli.mjs +87 -109
  4. package/create.mjs +334 -0
  5. package/package.json +2 -7
  6. package/template/.claude/settings.local.json +20 -0
  7. package/template/.editorconfig +11 -0
  8. package/template/.github/copilot-instructions.md +186 -0
  9. package/template/.nvimrc +7 -0
  10. package/template/.vscode/extensions.json +9 -0
  11. package/template/.vscode/settings.json +28 -0
  12. package/template/AGENTS.md +305 -0
  13. package/template/CLAUDE.md +140 -0
  14. package/template/architecture.md +518 -0
  15. package/template/code.md +211 -0
  16. package/template/dev.sh +45 -0
  17. package/template/install.sh +47 -0
  18. package/template/misc/nginx.conf +151 -0
  19. package/template/misc/systemd/{{PROJECT_SLUG}}-api.service +31 -0
  20. package/template/misc/systemd/{{PROJECT_SLUG}}-site-api.service +31 -0
  21. package/template/misc/systemd/{{PROJECT_SLUG}}-site.service +30 -0
  22. package/template/seed.sh +23 -0
  23. package/template/server/.env.sample +40 -0
  24. package/template/server/.prettierrc +4 -0
  25. package/template/server/apps/api/app.ts +42 -0
  26. package/template/server/apps/api/routes/account.ts +98 -0
  27. package/template/server/apps/api/routes/admin.ts +80 -0
  28. package/template/server/apps/api/routes/auth.ts +222 -0
  29. package/template/server/apps/api/routes/files.ts +89 -0
  30. package/template/server/apps/api/routes/index.ts +19 -0
  31. package/template/server/apps/api/routes/user.ts +17 -0
  32. package/template/server/apps/shared/middlewares/demand-account-access.ts +52 -0
  33. package/template/server/apps/shared/middlewares/demand-admin-user.ts +25 -0
  34. package/template/server/apps/shared/middlewares/demand-user.ts +17 -0
  35. package/template/server/apps/shared/middlewares/error-handler.ts +28 -0
  36. package/template/server/apps/shared/middlewares/request-logger.ts +40 -0
  37. package/template/server/apps/shared/middlewares/try-set-user-by-token.ts +26 -0
  38. package/template/server/apps/shared/utils.ts +71 -0
  39. package/template/server/apps/site-api/app.ts +42 -0
  40. package/template/server/apps/site-api/routes/index.ts +10 -0
  41. package/template/server/apps/site-api/routes/public.ts +25 -0
  42. package/template/server/console/index.ts +26 -0
  43. package/template/server/console/runner.ts +67 -0
  44. package/template/server/console/tasks/example.ts +29 -0
  45. package/template/server/console/tasks/seed-admin.ts +71 -0
  46. package/template/server/core/context.ts +49 -0
  47. package/template/server/core/db.ts +12 -0
  48. package/template/server/core/email.ts +87 -0
  49. package/template/server/core/errors.ts +44 -0
  50. package/template/server/core/knexfile.ts +28 -0
  51. package/template/server/core/logger.ts +60 -0
  52. package/template/server/core/models/account.ts +319 -0
  53. package/template/server/core/models/auth.ts +317 -0
  54. package/template/server/core/models/base.ts +19 -0
  55. package/template/server/core/models/user.ts +343 -0
  56. package/template/server/core/s3.ts +183 -0
  57. package/template/server/emails/_styles.css +5 -0
  58. package/template/server/emails/_template.html +28 -0
  59. package/template/server/emails/accountWelcome.html +4 -0
  60. package/template/server/emails/forgetPassword.html +5 -0
  61. package/template/server/eslint.config.js +16 -0
  62. package/template/server/knex.sh +4 -0
  63. package/template/server/migrations/20260208000000_initial-schema.ts +56 -0
  64. package/template/server/migrations/20260208000001_seed-admin.ts +42 -0
  65. package/template/server/migrations/20260208000002_add-user-avatar.ts +13 -0
  66. package/template/server/package.json +58 -0
  67. package/template/server/tsconfig.json +27 -0
  68. package/template/server/types/api.ts +20 -0
  69. package/template/server/types/express.d.ts +35 -0
  70. package/template/ui/admin/.prettierrc +4 -0
  71. package/template/ui/admin/components.json +20 -0
  72. package/template/ui/admin/eslint.config.js +16 -0
  73. package/template/ui/admin/index.html +12 -0
  74. package/template/ui/admin/package.json +57 -0
  75. package/template/ui/admin/postcss.config.js +6 -0
  76. package/template/ui/admin/src/app.tsx +13 -0
  77. package/template/ui/admin/src/components/nav-user.tsx +95 -0
  78. package/template/ui/admin/src/components/theme-provider.tsx +65 -0
  79. package/template/ui/admin/src/components/theme-toggle.tsx +17 -0
  80. package/template/ui/admin/src/components/ui/badge.tsx +36 -0
  81. package/template/ui/admin/src/components/ui/button.tsx +56 -0
  82. package/template/ui/admin/src/components/ui/calendar.tsx +68 -0
  83. package/template/ui/admin/src/components/ui/card.tsx +79 -0
  84. package/template/ui/admin/src/components/ui/checkbox.tsx +28 -0
  85. package/template/ui/admin/src/components/ui/date-picker.tsx +78 -0
  86. package/template/ui/admin/src/components/ui/date-range-picker.tsx +208 -0
  87. package/template/ui/admin/src/components/ui/dialog.tsx +121 -0
  88. package/template/ui/admin/src/components/ui/dropdown-menu.tsx +200 -0
  89. package/template/ui/admin/src/components/ui/input.tsx +22 -0
  90. package/template/ui/admin/src/components/ui/label.tsx +24 -0
  91. package/template/ui/admin/src/components/ui/popover.tsx +29 -0
  92. package/template/ui/admin/src/components/ui/select.tsx +158 -0
  93. package/template/ui/admin/src/components/ui/separator.tsx +29 -0
  94. package/template/ui/admin/src/components/ui/sheet.tsx +136 -0
  95. package/template/ui/admin/src/components/ui/sidebar.tsx +759 -0
  96. package/template/ui/admin/src/components/ui/skeleton.tsx +15 -0
  97. package/template/ui/admin/src/components/ui/sonner.tsx +26 -0
  98. package/template/ui/admin/src/components/ui/switch.tsx +27 -0
  99. package/template/ui/admin/src/components/ui/table.tsx +117 -0
  100. package/template/ui/admin/src/components/ui/tabs.tsx +53 -0
  101. package/template/ui/admin/src/components/ui/textarea.tsx +22 -0
  102. package/template/ui/admin/src/components/ui/tooltip.tsx +28 -0
  103. package/template/ui/admin/src/hooks/queries/use-accounts.ts +68 -0
  104. package/template/ui/admin/src/hooks/queries/use-auth.ts +61 -0
  105. package/template/ui/admin/src/hooks/queries/use-dashboard.ts +70 -0
  106. package/template/ui/admin/src/hooks/queries/use-members.ts +90 -0
  107. package/template/ui/admin/src/hooks/queries/use-users.ts +99 -0
  108. package/template/ui/admin/src/hooks/use-confirm.tsx +92 -0
  109. package/template/ui/admin/src/hooks/use-mobile.tsx +22 -0
  110. package/template/ui/admin/src/hooks/use-sidebar.tsx +4 -0
  111. package/template/ui/admin/src/hooks/use-user.tsx +86 -0
  112. package/template/ui/admin/src/index.css +22 -0
  113. package/template/ui/admin/src/layouts/account.tsx +278 -0
  114. package/template/ui/admin/src/layouts/admin.tsx +182 -0
  115. package/template/ui/admin/src/layouts/public.tsx +9 -0
  116. package/template/ui/admin/src/layouts/user.tsx +107 -0
  117. package/template/ui/admin/src/lib/config.ts +9 -0
  118. package/template/ui/admin/src/lib/fetch-api.ts +101 -0
  119. package/template/ui/admin/src/lib/query-client.ts +12 -0
  120. package/template/ui/admin/src/lib/query-keys.ts +44 -0
  121. package/template/ui/admin/src/lib/utils.ts +24 -0
  122. package/template/ui/admin/src/main.tsx +17 -0
  123. package/template/ui/admin/src/pages/account/dashboard.tsx +128 -0
  124. package/template/ui/admin/src/pages/account/members.tsx +414 -0
  125. package/template/ui/admin/src/pages/account/settings.tsx +210 -0
  126. package/template/ui/admin/src/pages/admin/accounts.tsx +164 -0
  127. package/template/ui/admin/src/pages/admin/dashboard.tsx +243 -0
  128. package/template/ui/admin/src/pages/admin/users.tsx +395 -0
  129. package/template/ui/admin/src/pages/public/error.tsx +24 -0
  130. package/template/ui/admin/src/pages/public/forgot-password.tsx +74 -0
  131. package/template/ui/admin/src/pages/public/login.tsx +102 -0
  132. package/template/ui/admin/src/pages/public/reset-password.tsx +106 -0
  133. package/template/ui/admin/src/pages/public/signup.tsx +118 -0
  134. package/template/ui/admin/src/pages/user/profile.tsx +313 -0
  135. package/template/ui/admin/src/pages/user/select-account.tsx +114 -0
  136. package/template/ui/admin/src/router.tsx +71 -0
  137. package/template/ui/admin/tailwind.config.ts +71 -0
  138. package/template/ui/admin/tsconfig.json +24 -0
  139. package/template/ui/admin/vite.config.ts +15 -0
  140. package/template/ui/site/.env.sample +3 -0
  141. package/template/ui/site/.prettierrc +4 -0
  142. package/template/ui/site/app.ts +50 -0
  143. package/template/ui/site/eslint.config.js +16 -0
  144. package/template/ui/site/package.json +33 -0
  145. package/template/ui/site/styles/input.css +3 -0
  146. package/template/ui/site/tailwind.config.js +8 -0
  147. package/template/ui/site/tsconfig.json +15 -0
  148. package/template/ui/site/views/pages/home.ejs +305 -0
  149. package/template/ui/site/views/partials/footer.ejs +44 -0
  150. package/template/ui/site/views/partials/header.ejs +83 -0
  151. package/template-variables.mjs +186 -0
@@ -0,0 +1,518 @@
1
+ # Architecture
2
+
3
+ This document describes the architecture patterns and how-tos for
4
+ the project. Read `code.md` for coding standards and style rules.
5
+
6
+ ---
7
+
8
+ ## Context-Based Dependency Injection
9
+
10
+ Every request gets a fresh `Context` (created in middleware). The
11
+ Context holds all model instances. **ALL data access goes through
12
+ models — never raw queries in routes.**
13
+
14
+ ```typescript
15
+ // How it works:
16
+ req.context = new Context();
17
+
18
+ // In a route handler:
19
+ const users = await req.context.user.list({ page: 1 });
20
+ const account = await req.context.account.findById(id);
21
+
22
+ // Models access each other via context:
23
+ class SomeModel extends BaseModel {
24
+ async doSomething() {
25
+ const user = await this.context.user.findById(userId);
26
+ }
27
+ }
28
+ ```
29
+
30
+ **Adding a new model:**
31
+ 1. Create `server/core/models/my-entity.ts` extending `BaseModel`
32
+ 2. Add to `server/core/context.ts`:
33
+ `myEntity: MyEntityModel;` +
34
+ `this.myEntity = new MyEntityModel(this);`
35
+ 3. Access everywhere via `req.context.myEntity` or
36
+ `this.context.myEntity`
37
+
38
+ ---
39
+
40
+ ## Model Pattern
41
+
42
+ ```typescript
43
+ import BaseModel from './base.js';
44
+ import { NotFoundError, ValidationError } from '#core/errors.js';
45
+
46
+ class MyEntityModel extends BaseModel {
47
+ // this.knex — Knex instance for queries
48
+ // this.context — access to other models
49
+ // this.db — Db singleton
50
+
51
+ async list(options: {
52
+ page?: number;
53
+ limit?: number;
54
+ search?: string;
55
+ } = {}) {
56
+ const page = Math.max(1, options.page || 1);
57
+ const limit = Math.min(100, Math.max(1, options.limit || 20));
58
+ const offset = (page - 1) * limit;
59
+
60
+ const baseQuery = this.knex('my_entities')
61
+ .where('account_id', accountId);
62
+
63
+ if (options.search?.trim()) {
64
+ baseQuery.whereILike(
65
+ 'name',
66
+ `%${options.search.trim()}%`
67
+ );
68
+ }
69
+
70
+ const [{ count }] = await baseQuery.clone()
71
+ .count('id as count');
72
+ const total = parseInt(String(count), 10);
73
+ const rows = await baseQuery.clone()
74
+ .select('*')
75
+ .orderBy('utc_created_on', 'desc')
76
+ .limit(limit)
77
+ .offset(offset);
78
+
79
+ return {
80
+ rows,
81
+ total,
82
+ page,
83
+ limit,
84
+ totalPages: Math.ceil(total / limit),
85
+ };
86
+ }
87
+
88
+ async findById(id: string) {
89
+ const row = await this.knex('my_entities')
90
+ .where({ id })
91
+ .first();
92
+
93
+ if (!row) {
94
+ throw new NotFoundError('Entidade não encontrada');
95
+ }
96
+
97
+ return row;
98
+ }
99
+ }
100
+ ```
101
+
102
+ **Key rules:**
103
+ - Always filter by `account_id` in multi-tenant queries
104
+ - Use `PaginatedResult<T>` pattern:
105
+ `{ rows, total, page, limit, totalPages }`
106
+ - Use `whereILike` for case-insensitive search
107
+ - Parse JSON fields when reading, stringify when writing
108
+ - Throw `AppError` subclasses
109
+ (`ValidationError`, `NotFoundError`, etc.)
110
+
111
+ ---
112
+
113
+ ## Route Handler Pattern
114
+
115
+ Routes use `buildHandler()` which catches errors and handles Zod
116
+ validation automatically.
117
+
118
+ ```typescript
119
+ import type { Express, Request, Response } from 'express';
120
+ import express from 'express';
121
+ import { z } from 'zod';
122
+ import trySetUserByTokenMiddleware
123
+ from '#shared/middlewares/try-set-user-by-token.js';
124
+ import demandUserMiddleware
125
+ from '#shared/middlewares/demand-user.js';
126
+ import demandAccountAccessMiddleware
127
+ from '#shared/middlewares/demand-account-access.js';
128
+ import { buildHandler } from '#shared/utils.js';
129
+
130
+ const router = express.Router();
131
+
132
+ const createSchema = z.object({
133
+ name: z.string().min(1, 'Nome é obrigatório'),
134
+ });
135
+
136
+ async function handleCreate(
137
+ req: Request,
138
+ res: Response,
139
+ ): Promise<void> {
140
+ const data = createSchema.parse(req.body);
141
+ const result = await req.context.myEntity.create(
142
+ req.accountId!,
143
+ data,
144
+ );
145
+ res.json({ ok: true, result });
146
+ }
147
+
148
+ router.post('/my-entities', buildHandler(handleCreate));
149
+
150
+ // Export pattern — ALWAYS use makeEndpoint or makeRoutes
151
+ export default function makeEndpoint(app: Express): void {
152
+ app.use(
153
+ '/api/account/:accountId',
154
+ trySetUserByTokenMiddleware,
155
+ demandUserMiddleware,
156
+ demandAccountAccessMiddleware,
157
+ router
158
+ );
159
+ }
160
+ ```
161
+
162
+ **Adding new routes:**
163
+ 1. Create route file in `apps/api/routes/` following the pattern
164
+ 2. Import and call from `apps/api/routes/index.ts`
165
+ 3. Use appropriate middleware chain:
166
+ - Public routes: just `trySetUserByTokenMiddleware`
167
+ - User routes: `+ demandUserMiddleware`
168
+ - Admin routes: `+ demandAdminUserMiddleware`
169
+ - Account routes: `+ demandAccountAccessMiddleware`
170
+ (sets `req.accountId`, `req.accountRole`)
171
+
172
+ ---
173
+
174
+ ## Middleware Chain
175
+
176
+ ```
177
+ requestLogger → contextInjection → trySetUserByToken
178
+ → [demand*] → routes → errorHandler
179
+ ```
180
+
181
+ | Middleware | Does |
182
+ |---|---|
183
+ | `try-set-user-by-token` | Reads `auth_token` cookie, sets `req.user` if valid, always continues |
184
+ | `demand-user` | Requires `req.user`, returns 401 if missing |
185
+ | `demand-admin-user` | Requires `req.user.role === 'admin'`, returns 403 otherwise |
186
+ | `demand-account-access` | Checks user has access to `:accountId`, sets `req.accountId` + `req.accountRole`. Admins bypass check |
187
+ | `error-handler` | Final handler, catches all errors, returns JSON |
188
+
189
+ ---
190
+
191
+ ## Error Classes
192
+
193
+ ```typescript
194
+ import {
195
+ ValidationError,
196
+ NotFoundError,
197
+ ConflictError,
198
+ ForbiddenError,
199
+ UnauthorizedError,
200
+ } from '#core/errors.js';
201
+
202
+ // 400
203
+ throw new ValidationError(
204
+ 'A senha deve ter pelo menos 6 caracteres'
205
+ );
206
+ // 401
207
+ throw new UnauthorizedError('Não autorizado');
208
+ // 403
209
+ throw new ForbiddenError('Acesso negado');
210
+ // 404
211
+ throw new NotFoundError('Usuário não encontrado');
212
+ // 409
213
+ throw new ConflictError('Este email já está em uso');
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Transactions
219
+
220
+ ```typescript
221
+ await req.context.knexTransaction(async () => {
222
+ // All model operations here share the same transaction
223
+ const user = await req.context.user.create(data);
224
+ await req.context.account.addMember(accountId, { ... });
225
+ // If anything throws, everything rolls back
226
+ });
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Migrations
232
+
233
+ **Always use raw SQL:**
234
+ ```typescript
235
+ export async function up(knex: Knex): Promise<void> {
236
+ await knex.schema.raw(`
237
+ CREATE TABLE my_entities (
238
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
239
+ account_id UUID NOT NULL REFERENCES accounts(id),
240
+ name VARCHAR(255) NOT NULL,
241
+ status VARCHAR(50) NOT NULL DEFAULT 'active',
242
+ settings JSONB DEFAULT '{}',
243
+ utc_created_on TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
244
+ utc_updated_on TIMESTAMP WITH TIME ZONE DEFAULT NOW()
245
+ );
246
+
247
+ CREATE INDEX idx_my_entities_account
248
+ ON my_entities(account_id);
249
+ `);
250
+ }
251
+ ```
252
+
253
+ **Conventions:** Always include `id` (UUID), `utc_created_on`,
254
+ `utc_updated_on`. Use PostgreSQL ENUMs for roles/statuses.
255
+ Filename: `YYYYMMDDHHMMSS_description.ts`.
256
+
257
+ ---
258
+
259
+ ## Authentication
260
+
261
+ - Backend sets `auth_token` httpOnly cookie on login/signup via
262
+ `setAuthCookie()`
263
+ - Frontend uses `credentials: 'include'` (handled by `fetchApi()`)
264
+ - Token read from `req.cookies?.auth_token` in
265
+ `try-set-user-by-token` middleware
266
+ - JWT contains `user_id`, 30-day expiration
267
+ - No Authorization header needed — cookies are automatic
268
+
269
+ ---
270
+
271
+ ## Multi-Tenancy
272
+
273
+ ```
274
+ users (global identity)
275
+ ↓ (many-to-many)
276
+ user_in_accounts (user_id + account_id + role)
277
+
278
+ accounts (tenant)
279
+
280
+ [domain entities scoped by account_id]
281
+ ```
282
+
283
+ **Roles:** `admin` (platform-wide), `owner` (account owner),
284
+ `manager` (account manager), `user` (regular user)
285
+
286
+ **Access control:** `demand-account-access` middleware verifies
287
+ access and sets `req.accountId` + `req.accountRole`. Admin users
288
+ bypass the `user_in_accounts` check.
289
+
290
+ ---
291
+
292
+ ## File Uploads (S3)
293
+
294
+ ```typescript
295
+ import { createS3Upload, generateSignedUrl } from '#core/s3.js';
296
+
297
+ const upload = createS3Upload({
298
+ folder: 'documents',
299
+ maxFileSize: 10 * 1024 * 1024,
300
+ });
301
+
302
+ router.post(
303
+ '/upload',
304
+ upload.single('file'),
305
+ buildHandler(async (req, res) => {
306
+ const file = req.file as Express.MulterS3.File;
307
+ // Store file.key in the database — never file.location
308
+ res.json({ ok: true, key: file.key });
309
+ }),
310
+ );
311
+
312
+ // Serve securely via signed URL (1-hour expiry)
313
+ const signedUrl = await generateSignedUrl(s3Key, 3600);
314
+ ```
315
+
316
+ **Rules:**
317
+ - Store only the S3 **key** in the database, never the full URL
318
+ - Serve via **signed URLs** with short expiry
319
+ - Never expose raw S3 URLs to the frontend
320
+ - Delete old files from S3 when replacing
321
+
322
+ ---
323
+
324
+ ## Frontend: fetchApi
325
+
326
+ ```typescript
327
+ import fetchApi from '@/lib/fetch-api';
328
+
329
+ // GET (body becomes query params)
330
+ const data = await fetchApi('user/accounts');
331
+ const data = await fetchApi(
332
+ 'admin/users',
333
+ { page: 1, search: 'john' },
334
+ );
335
+
336
+ // POST/DELETE
337
+ const data = await fetchApi(
338
+ 'login',
339
+ { email, password },
340
+ 'POST',
341
+ );
342
+ const data = await fetchApi(
343
+ `admin/users/${id}`,
344
+ null,
345
+ 'DELETE',
346
+ );
347
+
348
+ // FormData (auto-detected, skips Content-Type)
349
+ const formData = new FormData();
350
+ formData.append('file', file);
351
+ const data = await fetchApi('files/upload', formData, 'POST');
352
+ ```
353
+
354
+ - Uses `credentials: 'include'` for cookie auth
355
+ - On 401/403: clears storage, redirects to `/login`
356
+ - Always check `res.ok` before using data
357
+
358
+ ---
359
+
360
+ ## Frontend: React Query Hooks
361
+
362
+ All server state uses TanStack Query v5 with a structured query
363
+ key factory.
364
+
365
+ ```typescript
366
+ // Query hook pattern
367
+ export function useMyEntities(
368
+ accountId: string,
369
+ filters: PaginationFilters,
370
+ ) {
371
+ return useQuery({
372
+ queryKey: queryKeys.myEntities.list(accountId, filters),
373
+ placeholderData: keepPreviousData,
374
+ queryFn: async () => {
375
+ const res = await fetchApi(
376
+ `account/${accountId}/my-entities`,
377
+ filters,
378
+ );
379
+ if (!res.ok) {
380
+ throw new Error(res.message);
381
+ }
382
+ return res;
383
+ },
384
+ });
385
+ }
386
+
387
+ // Mutation hook pattern
388
+ export function useCreateMyEntity(accountId: string) {
389
+ const queryClient = useQueryClient();
390
+ return useMutation({
391
+ mutationFn: async (data) => {
392
+ const res = await fetchApi(
393
+ `account/${accountId}/my-entities`,
394
+ data,
395
+ 'POST',
396
+ );
397
+ if (!res.ok) {
398
+ throw new Error(res.message);
399
+ }
400
+ return res;
401
+ },
402
+ onSuccess: () => {
403
+ queryClient.invalidateQueries({
404
+ queryKey: queryKeys.myEntities.all,
405
+ });
406
+ },
407
+ });
408
+ }
409
+ ```
410
+
411
+ **Key rules:**
412
+ - Use `keepPreviousData` on paginated/searchable queries
413
+ (prevents UI flash)
414
+ - Invalidate related queries in `onSuccess`
415
+ - Add query keys to `lib/query-keys.ts`
416
+
417
+ ---
418
+
419
+ ## Frontend: Layouts and Routing
420
+
421
+ Routes are nested under layouts by role scope. Each layout
422
+ provides context:
423
+
424
+ | Layout | Auth | Provides | Route prefix |
425
+ |---|---|---|---|
426
+ | `PublicLayout` | None | — | `/` |
427
+ | `UserLayout` | `useUser()` | user, logout | `/user` |
428
+ | `AdminLayout` | admin role | sidebar, user | `/admin` |
429
+ | `AccountLayout` | account access | `useAccountContext()` | `/account/:accountId` |
430
+
431
+ **Adding a new account page:**
432
+ 1. Create `src/pages/account/my-page.tsx`
433
+ 2. Add route to `src/router.tsx`:
434
+ `{ path: 'my-entities', element: <MyPage /> }`
435
+ 3. Add nav item to `src/layouts/account.tsx` in `getNavSections()`
436
+
437
+ ---
438
+
439
+ ## Frontend: Page Pattern (pagination + search)
440
+
441
+ ```typescript
442
+ export default function MyPage() {
443
+ const { accountId } = useParams<{ accountId: string }>();
444
+ const [search, setSearch] = useState('');
445
+ const [debouncedSearch, setDebouncedSearch] = useState('');
446
+ const [page, setPage] = useState(1);
447
+
448
+ useEffect(() => {
449
+ const timer = setTimeout(() => {
450
+ setDebouncedSearch(search);
451
+ setPage(1);
452
+ }, 300);
453
+ return () => clearTimeout(timer);
454
+ }, [search]);
455
+
456
+ const { data, isLoading } = useMyEntities(accountId!, {
457
+ page,
458
+ limit: 20,
459
+ search: debouncedSearch || undefined,
460
+ });
461
+
462
+ const rows = data?.rows || [];
463
+ const total = data?.total || 0;
464
+ const totalPages = data?.totalPages || 0;
465
+
466
+ // ... render table with search input and pagination
467
+ }
468
+ ```
469
+
470
+ ---
471
+
472
+ ## Frontend: Confirm Modal
473
+
474
+ Use `useConfirm()` instead of `window.confirm()`:
475
+
476
+ ```typescript
477
+ const confirm = useConfirm();
478
+
479
+ async function handleDelete(item) {
480
+ const ok = await confirm({
481
+ title: 'Excluir Item',
482
+ description: `Tem certeza que deseja excluir "${item.name}"?`,
483
+ confirmLabel: 'Excluir',
484
+ variant: 'destructive',
485
+ });
486
+
487
+ if (!ok) {
488
+ return;
489
+ }
490
+
491
+ await deleteMutation.mutateAsync(item.id);
492
+ }
493
+ ```
494
+
495
+ ---
496
+
497
+ ## Console Tasks
498
+
499
+ ```bash
500
+ npm run console -- --task=seed-admin
501
+ ```
502
+
503
+ Create tasks in `server/console/tasks/`:
504
+ ```typescript
505
+ import type { ConsoleTask } from '../runner.js';
506
+
507
+ const task: ConsoleTask = {
508
+ name: 'my-task',
509
+ description: 'Does something useful',
510
+ async run(context, args) {
511
+ // context has all models
512
+ const users = await context.user.list();
513
+ },
514
+ };
515
+ export default task;
516
+ ```
517
+
518
+ Register in `console/index.ts`.