umpordez 1.5.1 → 1.6.2

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 (95) hide show
  1. package/.claude/settings.local.json +14 -1
  2. package/create.mjs +3 -3
  3. package/package.json +5 -5
  4. package/template/.github/copilot-instructions.md +1 -1
  5. package/template/AGENTS.md +1 -1
  6. package/template/mobile/{{PROJECT_SLUG}}/README.md +17 -2
  7. package/template/mobile/{{PROJECT_SLUG}}/package.json +23 -25
  8. package/template/server/apps/api/app.ts +1 -1
  9. package/template/server/apps/api/routes/account.ts +32 -10
  10. package/template/server/apps/api/routes/admin.ts +13 -6
  11. package/template/server/apps/api/routes/auth.ts +109 -29
  12. package/template/server/apps/api/routes/files.ts +14 -5
  13. package/template/server/apps/api/routes/health.ts +2 -2
  14. package/template/server/apps/api/routes/user.ts +2 -1
  15. package/template/server/apps/shared/middlewares/request-logger.ts +3 -1
  16. package/template/server/apps/shared/utils.ts +2 -2
  17. package/template/server/apps/site-api/app.ts +1 -1
  18. package/template/server/console/index.ts +1 -1
  19. package/template/server/console/tasks/seed-admin.ts +8 -2
  20. package/template/server/core/context.ts +2 -2
  21. package/template/server/core/email.ts +9 -2
  22. package/template/server/core/knexfile.ts +1 -1
  23. package/template/server/core/models/account.ts +43 -13
  24. package/template/server/core/models/auth.ts +10 -3
  25. package/template/server/core/models/user.ts +61 -27
  26. package/template/server/core/s3.ts +29 -14
  27. package/template/server/eslint.config.js +9 -1
  28. package/template/server/migrations/20260208000000_initial-schema.ts +4 -2
  29. package/template/server/package.json +27 -28
  30. package/template/server/types/api.ts +3 -0
  31. package/template/ui/admin/CLAUDE.md +5 -4
  32. package/template/ui/admin/eslint.config.js +29 -1
  33. package/template/ui/admin/package.json +31 -39
  34. package/template/ui/admin/src/components/nav-user.tsx +4 -2
  35. package/template/ui/admin/src/components/theme-provider.tsx +7 -1
  36. package/template/ui/admin/src/components/ui/badge.tsx +1 -1
  37. package/template/ui/admin/src/components/ui/button.tsx +1 -1
  38. package/template/ui/admin/src/components/ui/card.tsx +1 -1
  39. package/template/ui/admin/src/components/ui/checkbox.tsx +1 -1
  40. package/template/ui/admin/src/components/ui/date-picker.tsx +3 -3
  41. package/template/ui/admin/src/components/ui/dialog.tsx +1 -1
  42. package/template/ui/admin/src/components/ui/dropdown-menu.tsx +6 -6
  43. package/template/ui/admin/src/components/ui/input.tsx +1 -1
  44. package/template/ui/admin/src/components/ui/popover.tsx +1 -1
  45. package/template/ui/admin/src/components/ui/select.tsx +4 -4
  46. package/template/ui/admin/src/components/ui/separator.tsx +1 -1
  47. package/template/ui/admin/src/components/ui/sheet.tsx +1 -1
  48. package/template/ui/admin/src/components/ui/sidebar.tsx +24 -24
  49. package/template/ui/admin/src/components/ui/sonner.tsx +4 -4
  50. package/template/ui/admin/src/components/ui/switch.tsx +1 -1
  51. package/template/ui/admin/src/components/ui/table.tsx +3 -3
  52. package/template/ui/admin/src/components/ui/tabs.tsx +2 -2
  53. package/template/ui/admin/src/components/ui/textarea.tsx +1 -1
  54. package/template/ui/admin/src/hooks/queries/use-accounts.ts +29 -9
  55. package/template/ui/admin/src/hooks/queries/use-auth.ts +20 -6
  56. package/template/ui/admin/src/hooks/queries/use-dashboard.ts +13 -5
  57. package/template/ui/admin/src/hooks/queries/use-members.ts +32 -10
  58. package/template/ui/admin/src/hooks/queries/use-users.ts +32 -10
  59. package/template/ui/admin/src/hooks/use-confirm.tsx +1 -1
  60. package/template/ui/admin/src/hooks/use-user.tsx +4 -1
  61. package/template/ui/admin/src/index.css +68 -6
  62. package/template/ui/admin/src/layouts/account.tsx +19 -6
  63. package/template/ui/admin/src/layouts/user.tsx +3 -1
  64. package/template/ui/admin/src/lib/fetch-api.ts +38 -14
  65. package/template/ui/admin/src/lib/query-keys.ts +2 -1
  66. package/template/ui/admin/src/pages/account/dashboard.tsx +10 -3
  67. package/template/ui/admin/src/pages/account/members.tsx +22 -12
  68. package/template/ui/admin/src/pages/account/settings.tsx +22 -11
  69. package/template/ui/admin/src/pages/admin/accounts.tsx +2 -3
  70. package/template/ui/admin/src/pages/admin/dashboard.tsx +10 -3
  71. package/template/ui/admin/src/pages/admin/users.tsx +19 -10
  72. package/template/ui/admin/src/pages/public/error.tsx +4 -1
  73. package/template/ui/admin/src/pages/public/forgot-password.tsx +2 -2
  74. package/template/ui/admin/src/pages/public/login.tsx +13 -3
  75. package/template/ui/admin/src/pages/public/reset-password.tsx +5 -3
  76. package/template/ui/admin/src/pages/public/signup.tsx +11 -3
  77. package/template/ui/admin/src/pages/user/profile.tsx +16 -11
  78. package/template/ui/admin/src/pages/user/select-account.tsx +3 -1
  79. package/template/ui/admin/src/vite-env.d.ts +1 -0
  80. package/template/ui/admin/tsconfig.json +0 -1
  81. package/template/ui/admin/vite.config.ts +2 -1
  82. package/template/ui/site/CLAUDE.md +1 -2
  83. package/template/ui/site/_variants/app/home.ejs +6 -6
  84. package/template/ui/site/app.ts +1 -1
  85. package/template/ui/site/eslint.config.js +9 -1
  86. package/template/ui/site/i18n/index.ts +0 -1
  87. package/template/ui/site/package.json +17 -17
  88. package/template/ui/site/styles/input.css +11 -3
  89. package/template/ui/site/views/pages/home.ejs +4 -4
  90. package/template/ui/site/views/partials/header.ejs +1 -1
  91. package/template-variables.mjs +1 -1
  92. package/template/ui/admin/postcss.config.js +0 -6
  93. package/template/ui/admin/tailwind.config.ts +0 -71
  94. package/template/ui/site/tailwind.config.js +0 -16
  95. /package/template/mobile/{{PROJECT_SLUG}}/{eslint.config.js → eslint.config.mjs} +0 -0
@@ -63,7 +63,20 @@
63
63
  "Bash(bash -n template/scripts/clean-appledouble.sh)",
64
64
  "Bash(bash -n template/scripts/prebuild.sh)",
65
65
  "Bash(bash -n template/scripts/run-ios.sh)",
66
- "Bash(bash -n template/install.sh)"
66
+ "Bash(bash -n template/install.sh)",
67
+ "WebSearch",
68
+ "Bash(tar -xzf expo-57.0.7.tgz package/bundledNativeModules.json)",
69
+ "Bash(npm -v)",
70
+ "Read(//dev/**)",
71
+ "Bash(npm ls *)",
72
+ "Bash(LC_ALL=C sed -i '' -e 's/dotenv\\\\.config\\(\\)/dotenv.config\\({ quiet: true }\\)/' apps/api/app.ts apps/site-api/app.ts console/index.ts)",
73
+ "Bash(LC_ALL=C sed -i '' -e \"s/dotenv\\\\.config\\({ path: path.resolve\\(dirname, '.env'\\) }\\)/dotenv.config\\({ path: path.resolve\\(dirname, '.env'\\), quiet: true }\\)/\" core/knexfile.ts)",
74
+ "Bash(npx eslint *)",
75
+ "Bash(ln -sfn /private/tmp/claude-501/-Users-ligeiro-dev-umpordez-cli/12177a90-5ebe-47b5-977f-9bb132141080/scratchpad/acme/server/node_modules /Users/ligeiro/dev/umpordez-cli/template/server/node_modules)",
76
+ "Bash(rm -f tailwind.config.ts postcss.config.js)",
77
+ "Bash(cp ../../server/eslint.config.js eslint.config.js)",
78
+ "Bash(ln -sfn /private/tmp/claude-501/-Users-ligeiro-dev-umpordez-cli/12177a90-5ebe-47b5-977f-9bb132141080/scratchpad/acme/ui/admin/node_modules /Users/ligeiro/dev/umpordez-cli/template/ui/admin/node_modules)",
79
+ "Bash(awk '{print $2}')"
67
80
  ]
68
81
  }
69
82
  }
package/create.mjs CHANGED
@@ -126,7 +126,7 @@ async function promptComponents() {
126
126
  ));
127
127
  } else if (site && mobile && adminUi) {
128
128
  const { landing } = await inquirer.prompt([{
129
- type: 'list',
129
+ type: 'select',
130
130
  name: 'landing',
131
131
  message: 'Landing page flavor:',
132
132
  choices: [
@@ -298,7 +298,7 @@ async function promptProjectDetails() {
298
298
  filter: (v) => v.trim().startsWith('#') ? v.trim() : `#${v.trim()}`,
299
299
  },
300
300
  {
301
- type: 'list',
301
+ type: 'select',
302
302
  name: 'defaultTheme',
303
303
  message: 'Default theme:',
304
304
  choices: [
@@ -340,7 +340,7 @@ async function promptProjectDetails() {
340
340
  : 'Lowercase alphanumeric, dashes allowed',
341
341
  },
342
342
  {
343
- type: 'list',
343
+ type: 'select',
344
344
  name: 'splashStyle',
345
345
  message: 'Splash screen:',
346
346
  choices: [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "umpordez",
3
3
  "type": "module",
4
- "version": "1.5.1",
4
+ "version": "1.6.2",
5
5
  "description": "SaaS starter kit generator — server, admin SPA, public site (i18n), mobile app (offline-first, IAP, native push)",
6
6
  "main": "cli.mjs",
7
7
  "scripts": {
@@ -24,9 +24,9 @@
24
24
  },
25
25
  "homepage": "https://github.com/umpordez/cli#readme",
26
26
  "dependencies": {
27
- "chalk": "^5.4.1",
28
- "figlet": "^1.8.2",
29
- "inquirer": "^12.7.0",
30
- "sharp": "^0.33.5"
27
+ "chalk": "^5.6.2",
28
+ "figlet": "^1.11.2",
29
+ "inquirer": "^14.0.2",
30
+ "sharp": "^0.35.3"
31
31
  }
32
32
  }
@@ -9,7 +9,7 @@ Multi-tenant SaaS application built with the **umpordez** architecture pattern f
9
9
  ## Tech Stack
10
10
 
11
11
  - **Backend:** TypeScript, Express.js, PostgreSQL, Knex.js, Zod
12
- - **Admin UI:** React 18, Vite, TailwindCSS, shadcn/ui (Radix), React Router DOM 6, TanStack Query v5, React Hook Form + Zod
12
+ - **Admin UI:** React 19, Vite 8, TailwindCSS v4, shadcn/ui (Radix), React Router DOM 7, TanStack Query v5
13
13
  - **Site UI:** Express + EJS + TailwindCSS
14
14
  - **Auth:** httpOnly JWT cookies (30-day expiry)
15
15
  - **Uploads:** AWS S3 via multer-s3 with signed URLs
@@ -59,7 +59,7 @@ ui/site/ Public marketing site (Express + EJS, port 3000)
59
59
  ## Tech Stack
60
60
 
61
61
  - **Backend:** TypeScript, Express.js, PostgreSQL, Knex.js, Zod
62
- - **Admin UI:** React 18, Vite, TailwindCSS, shadcn/ui (Radix), React Router DOM 6, React Query (TanStack Query v5), React Hook Form + Zod, Recharts
62
+ - **Admin UI:** React 19, Vite 8, TailwindCSS v4, shadcn/ui (Radix), React Router DOM 7, React Query (TanStack Query v5)
63
63
  - **Site UI:** Express + EJS + TailwindCSS
64
64
  - **Auth:** httpOnly JWT cookies (30-day expiry)
65
65
  - **Uploads:** AWS S3 via multer-s3 with signed URLs
@@ -6,11 +6,26 @@ typed fetch client pointed at the project's admin API.
6
6
 
7
7
  ## Quick start
8
8
 
9
+ **This is a development-build app, not an Expo Go app.** It ships
10
+ native modules (Apple Sign-In, `expo-build-properties`, and optionally
11
+ `expo-iap`) and rides the newest Expo SDK, so the public Expo Go from
12
+ the App Store either won't run it (`"requires a newer version of Expo
13
+ Go"` right after an SDK release) or crashes on a native import. Use a
14
+ development build instead — `expo-dev-client` is already a dependency:
15
+
9
16
  ```bash
10
17
  npm install
11
- npm run start # Metro + QR open with Expo Go for the JS-only flow
12
- npm run ios # build + run on iOS (requires Xcode)
18
+ npm run ios # build + run a dev client on iOS (requires Xcode)
13
19
  npm run android # build + run on Android (requires Android Studio)
20
+ npm run start # Metro — connect from an installed dev build
21
+ ```
22
+
23
+ Need a device build without Xcode/Android Studio, or want the Expo Go
24
+ experience on the current SDK? Use EAS-hosted Expo Go, which always
25
+ matches the SDK you target (unlike the store build):
26
+
27
+ ```bash
28
+ npx eas go # installs a matching Expo Go for this SDK
14
29
  ```
15
30
 
16
31
  For a clean iOS run that picks a connected device automatically:
@@ -9,7 +9,7 @@
9
9
  "ios": "expo run:ios",
10
10
  "web": "expo start --web",
11
11
  "prebuild": "bash ../../scripts/prebuild.sh",
12
- "lint": "eslint src --ext .ts,.tsx",
12
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
13
13
  "typecheck": "tsc --noEmit"
14
14
  },
15
15
  "expo": {
@@ -20,41 +20,39 @@
20
20
  "dependencies": {
21
21
  "@noble/hashes": "^2.2.0",
22
22
  "@react-native-async-storage/async-storage": "2.2.0",
23
- "@react-navigation/bottom-tabs": "^7.7.3",
24
- "@react-navigation/native": "^7.1.19",
25
- "@react-navigation/native-stack": "^7.6.2",
26
- "expo": "~57.0.4",
27
- "expo-apple-authentication": "~57.0.0",
28
- "expo-auth-session": "~57.0.2",
29
- "expo-build-properties": "~57.0.3",
30
- "expo-constants": "~57.0.3",
31
- "expo-file-system": "~57.0.0",{{IAP_DEP_JSON}}
32
- "expo-linking": "~57.0.2",
33
- "expo-localization": "~57.0.0",
34
- "expo-splash-screen": "~57.0.2",
35
- "expo-sqlite": "~57.0.0",
36
- "expo-status-bar": "~57.0.0",
37
- "expo-system-ui": "~57.0.0",
38
- "expo-web-browser": "~57.0.0",
39
- "i18next": "^23.16.5",
40
- "lucide-react-native": "^1.24.0",
23
+ "@react-navigation/bottom-tabs": "^7.18.11",
24
+ "@react-navigation/native": "^7.3.11",
25
+ "@react-navigation/native-stack": "^7.18.3",
26
+ "expo": "~57.0.7",
27
+ "expo-apple-authentication": "~57.0.1",
28
+ "expo-build-properties": "~57.0.6",
29
+ "expo-constants": "~57.0.6",
30
+ "expo-dev-client": "~57.0.7",
31
+ "expo-file-system": "~57.0.1",{{IAP_DEP_JSON}}
32
+ "expo-localization": "~57.0.1",
33
+ "expo-splash-screen": "~57.0.4",
34
+ "expo-sqlite": "~57.0.1",
35
+ "expo-system-ui": "~57.0.1",
36
+ "expo-web-browser": "~57.0.1",
37
+ "i18next": "^23.16.8",
38
+ "lucide-react-native": "^1.25.0",
41
39
  "react": "19.2.3",
42
40
  "react-native": "0.86.0",
43
- "react-i18next": "^15.1.1",
41
+ "react-i18next": "^15.7.4",
44
42
  "react-native-gesture-handler": "~2.32.0",
45
43
  "react-native-reanimated": "4.5.0",
46
44
  "react-native-safe-area-context": "~5.7.0",
47
45
  "react-native-screens": "4.25.2",
48
46
  "react-native-svg": "15.15.4",
49
- "react-native-toast-message": "^2.3.3",
47
+ "react-native-toast-message": "^2.4.0",
50
48
  "react-native-worklets": "0.10.0"
51
49
  },
52
50
  "devDependencies": {
53
- "@stylistic/eslint-plugin": "^2.13.0",
51
+ "@stylistic/eslint-plugin": "^5.10.0",
54
52
  "@types/react": "~19.2.2",
55
- "babel-preset-expo": "~57.0.2",
56
- "eslint": "^9.20.1",
53
+ "babel-preset-expo": "~57.0.3",
54
+ "eslint": "^10.7.0",
57
55
  "typescript": "~5.9.3",
58
- "typescript-eslint": "^8.24.0"
56
+ "typescript-eslint": "^8.64.0"
59
57
  }
60
58
  }
@@ -1,5 +1,5 @@
1
1
  import dotenv from 'dotenv';
2
- dotenv.config();
2
+ dotenv.config({ quiet: true });
3
3
 
4
4
  import express from 'express';
5
5
  import cors from 'cors';
@@ -1,20 +1,22 @@
1
1
  import type { Express, Request, Response } from 'express';
2
2
  import express from 'express';
3
3
  import { z } from 'zod';
4
- import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
4
+ import trySetUserByTokenMiddleware
5
+ from '#shared/middlewares/try-set-user-by-token.js';
5
6
  import demandUserMiddleware from '#shared/middlewares/demand-user.js';
6
- import demandAccountAccessMiddleware from '#shared/middlewares/demand-account-access.js';
7
+ import demandAccountAccessMiddleware
8
+ from '#shared/middlewares/demand-account-access.js';
7
9
  import { buildHandler } from '#shared/utils.js';
8
10
 
9
11
  const router = express.Router();
10
12
 
11
13
  const updateSettingsSchema = z.object({
12
14
  name: z.string().min(1, 'Nome é obrigatório'),
13
- settings: z.record(z.unknown()).optional()
15
+ settings: z.record(z.string(), z.unknown()).optional()
14
16
  });
15
17
 
16
18
  const addMemberSchema = z.object({
17
- email: z.string().email('Email inválido'),
19
+ email: z.email('Email inválido'),
18
20
  role: z.enum(['owner', 'manager', 'user']).default('user'),
19
21
  name: z.string().min(1).optional(),
20
22
  password: z.string().min(6).optional()
@@ -41,14 +43,21 @@ async function handleGetSettings(req: Request, res: Response): Promise<void> {
41
43
  res.json({ ok: true, account });
42
44
  }
43
45
 
44
- async function handleUpdateSettings(req: Request, res: Response): Promise<void> {
46
+ async function handleUpdateSettings(
47
+ req: Request,
48
+ res: Response
49
+ ): Promise<void> {
45
50
  const data = updateSettingsSchema.parse(req.body);
46
51
  const account = await req.context.account.update(req.accountId!, data);
47
52
  res.json({ ok: true, account });
48
53
  }
49
54
 
50
55
  async function handleDeleteAccount(req: Request, res: Response): Promise<void> {
51
- await req.context.account.delete(req.accountId!, req.user!.id, req.accountRole!);
56
+ await req.context.account.delete(
57
+ req.accountId!,
58
+ req.user!.id,
59
+ req.accountRole!
60
+ );
52
61
  res.json({ ok: true });
53
62
  }
54
63
 
@@ -57,7 +66,10 @@ async function handleListMembers(req: Request, res: Response): Promise<void> {
57
66
  const limit = parseInt(req.query.limit as string) || 20;
58
67
  const search = (req.query.search as string) || '';
59
68
 
60
- const result = await req.context.account.listMembers(req.accountId!, { page, limit, search });
69
+ const result = await req.context.account.listMembers(
70
+ req.accountId!,
71
+ { page, limit, search }
72
+ );
61
73
  res.json({ ok: true, ...result });
62
74
  }
63
75
 
@@ -67,14 +79,24 @@ async function handleAddMember(req: Request, res: Response): Promise<void> {
67
79
  res.json({ ok: true, member });
68
80
  }
69
81
 
70
- async function handleUpdateMemberRole(req: Request, res: Response): Promise<void> {
82
+ async function handleUpdateMemberRole(
83
+ req: Request,
84
+ res: Response
85
+ ): Promise<void> {
71
86
  const { role } = updateMemberRoleSchema.parse(req.body);
72
- await req.context.account.updateMemberRole(req.accountId!, (req.params.userId as string), role);
87
+ await req.context.account.updateMemberRole(
88
+ req.accountId!,
89
+ req.params.userId as string,
90
+ role
91
+ );
73
92
  res.json({ ok: true });
74
93
  }
75
94
 
76
95
  async function handleRemoveMember(req: Request, res: Response): Promise<void> {
77
- await req.context.account.removeMember(req.accountId!, (req.params.userId as string));
96
+ await req.context.account.removeMember(
97
+ req.accountId!,
98
+ req.params.userId as string
99
+ );
78
100
  res.json({ ok: true });
79
101
  }
80
102
 
@@ -1,25 +1,29 @@
1
1
  import type { Express, Request, Response } from 'express';
2
2
  import express from 'express';
3
3
  import { z } from 'zod';
4
- import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
4
+ import trySetUserByTokenMiddleware
5
+ from '#shared/middlewares/try-set-user-by-token.js';
5
6
  import demandUserMiddleware from '#shared/middlewares/demand-user.js';
6
- import demandAdminUserMiddleware from '#shared/middlewares/demand-admin-user.js';
7
+ import demandAdminUserMiddleware
8
+ from '#shared/middlewares/demand-admin-user.js';
7
9
  import { buildHandler } from '#shared/utils.js';
8
10
 
9
11
  const router = express.Router();
10
12
 
11
13
  const createUserSchema = z.object({
12
14
  name: z.string().min(1, 'Nome é obrigatório'),
13
- email: z.string().email('Email inválido'),
15
+ email: z.email('Email inválido'),
14
16
  password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres'),
15
17
  role: z.enum(['admin', 'user']).default('user')
16
18
  });
17
19
 
18
20
  const updateUserSchema = z.object({
19
21
  name: z.string().min(1, 'Nome é obrigatório'),
20
- email: z.string().email('Email inválido'),
22
+ email: z.email('Email inválido'),
21
23
  role: z.enum(['admin', 'user']).optional(),
22
- password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres').optional()
24
+ password: z.string()
25
+ .min(6, 'Senha deve ter no mínimo 6 caracteres')
26
+ .optional()
23
27
  });
24
28
 
25
29
  async function handleGetDashboard(req: Request, res: Response): Promise<void> {
@@ -53,7 +57,10 @@ async function handleCreateUser(req: Request, res: Response): Promise<void> {
53
57
 
54
58
  async function handleUpdateUser(req: Request, res: Response): Promise<void> {
55
59
  const data = updateUserSchema.parse(req.body);
56
- const user = await req.context.user.adminUpdate((req.params.id as string), data);
60
+ const user = await req.context.user.adminUpdate(
61
+ req.params.id as string,
62
+ data
63
+ );
57
64
  res.json({ ok: true, user });
58
65
  }
59
66
 
@@ -1,7 +1,8 @@
1
1
  import type { Express, Request, Response } from 'express';
2
2
  import express from 'express';
3
3
  import { z } from 'zod';
4
- import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
4
+ import trySetUserByTokenMiddleware
5
+ from '#shared/middlewares/try-set-user-by-token.js';
5
6
  import demandUserMiddleware from '#shared/middlewares/demand-user.js';
6
7
  import { buildHandler } from '#shared/utils.js';
7
8
  import { ForbiddenError, ValidationError } from '#core/errors.js';
@@ -18,7 +19,9 @@ const router = express.Router();
18
19
  const COOKIE_OPTIONS = {
19
20
  httpOnly: true,
20
21
  secure: process.env.NODE_ENV === 'production',
21
- sameSite: (process.env.NODE_ENV === 'production' ? 'strict' : 'lax') as 'strict' | 'lax',
22
+ sameSite: (process.env.NODE_ENV === 'production'
23
+ ? 'strict'
24
+ : 'lax') as 'strict' | 'lax',
22
25
  maxAge: 30 * 24 * 60 * 60 * 1000,
23
26
  path: '/'
24
27
  };
@@ -32,19 +35,19 @@ function clearAuthCookie(res: Response): void {
32
35
  }
33
36
 
34
37
  const loginSchema = z.object({
35
- email: z.string().email('Email inválido'),
38
+ email: z.email('Email inválido'),
36
39
  password: z.string().min(1, 'Senha é obrigatória')
37
40
  });
38
41
 
39
42
  const signupSchema = z.object({
40
- email: z.string().email('Email inválido'),
43
+ email: z.email('Email inválido'),
41
44
  password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres'),
42
45
  name: z.string().min(1, 'Nome é obrigatório'),
43
46
  account_name: z.string().optional()
44
47
  });
45
48
 
46
49
  const forgotPasswordSchema = z.object({
47
- email: z.string().email('Email inválido')
50
+ email: z.email('Email inválido')
48
51
  });
49
52
 
50
53
  const resetPasswordSchema = z.object({
@@ -53,12 +56,13 @@ const resetPasswordSchema = z.object({
53
56
 
54
57
  const updateProfileSchema = z.object({
55
58
  name: z.string().min(1, 'Nome é obrigatório'),
56
- email: z.string().email('Email inválido')
59
+ email: z.email('Email inválido')
57
60
  });
58
61
 
59
62
  const changePasswordSchema = z.object({
60
63
  current_password: z.string().min(1, 'Senha atual é obrigatória'),
61
- new_password: z.string().min(6, 'Nova senha deve ter no mínimo 6 caracteres')
64
+ new_password: z.string()
65
+ .min(6, 'Nova senha deve ter no mínimo 6 caracteres')
62
66
  });
63
67
 
64
68
  const avatarUpload = createS3Upload({
@@ -67,18 +71,30 @@ const avatarUpload = createS3Upload({
67
71
  maxFileSize: 2 * 1024 * 1024, // 2MB
68
72
  });
69
73
 
70
- async function resolveAvatarUrl(avatarKey: string | null): Promise<string | null> {
71
- if (!avatarKey) return null;
74
+ async function resolveAvatarUrl(
75
+ avatarKey: string | null
76
+ ): Promise<string | null> {
77
+ if (!avatarKey) {
78
+ return null;
79
+ }
72
80
  return generateSignedUrl(avatarKey, 3600);
73
81
  }
74
82
 
75
- async function userWithAvatar(user: any) {
83
+ interface UserRow {
84
+ id: string;
85
+ email: string;
86
+ name: string;
87
+ role: string;
88
+ avatar_key?: string | null;
89
+ }
90
+
91
+ async function userWithAvatar(user: UserRow) {
76
92
  return {
77
93
  id: user.id,
78
94
  email: user.email,
79
95
  name: user.name,
80
96
  role: user.role,
81
- avatar_url: await resolveAvatarUrl(user.avatar_key)
97
+ avatar_url: await resolveAvatarUrl(user.avatar_key ?? null)
82
98
  };
83
99
  }
84
100
 
@@ -115,7 +131,8 @@ async function handleLogin(req: Request, res: Response): Promise<void> {
115
131
  }
116
132
 
117
133
  async function handleSignup(req: Request, res: Response): Promise<void> {
118
- const { email, password, name, account_name } = signupSchema.parse(req.body);
134
+ const { email, password, name, account_name } =
135
+ signupSchema.parse(req.body);
119
136
  const result = await req.context.auth.signup({
120
137
  email, password, name, accountName: account_name
121
138
  });
@@ -124,7 +141,8 @@ async function handleSignup(req: Request, res: Response): Promise<void> {
124
141
  res.json({
125
142
  ok: true,
126
143
  existingUser: true,
127
- message: 'Uma nova conta foi criada. Como seu email já estava cadastrado, faça login para acessá-la.'
144
+ message: 'Uma nova conta foi criada. Como seu email '
145
+ + 'já estava cadastrado, faça login para acessá-la.'
128
146
  });
129
147
  return;
130
148
  }
@@ -366,20 +384,35 @@ async function handleGoogleCallback(
366
384
  );
367
385
  }
368
386
 
369
- async function handleForgotPassword(req: Request, res: Response): Promise<void> {
387
+ async function handleForgotPassword(
388
+ req: Request,
389
+ res: Response
390
+ ): Promise<void> {
370
391
  const { email } = forgotPasswordSchema.parse(req.body);
371
392
  await req.context.auth.forgotPassword(email);
372
- res.json({ ok: true, message: 'Se o email existir, você receberá um link de recuperação' });
393
+ res.json({
394
+ ok: true,
395
+ message: 'Se o email existir, você receberá um link de '
396
+ + 'recuperação'
397
+ });
373
398
  }
374
399
 
375
- async function handleValidateResetToken(req: Request, res: Response): Promise<void> {
376
- const valid = await req.context.auth.validateResetToken((req.params.token as string));
400
+ async function handleValidateResetToken(
401
+ req: Request,
402
+ res: Response
403
+ ): Promise<void> {
404
+ const valid = await req.context.auth.validateResetToken(
405
+ req.params.token as string
406
+ );
377
407
  res.json({ ok: valid });
378
408
  }
379
409
 
380
410
  async function handleResetPassword(req: Request, res: Response): Promise<void> {
381
411
  const { password } = resetPasswordSchema.parse(req.body);
382
- await req.context.auth.resetPassword((req.params.token as string), password);
412
+ await req.context.auth.resetPassword(
413
+ req.params.token as string,
414
+ password
415
+ );
383
416
  res.json({ ok: true, message: 'Senha alterada com sucesso' });
384
417
  }
385
418
 
@@ -402,7 +435,12 @@ async function handleAuth(req: Request, res: Response): Promise<void> {
402
435
  throw new ForbiddenError('Acesso negado a esta conta');
403
436
  }
404
437
 
405
- res.json({ ok: true, user, account: userAccount.account, role: userAccount.role });
438
+ res.json({
439
+ ok: true,
440
+ user,
441
+ account: userAccount.account,
442
+ role: userAccount.role
443
+ });
406
444
  }
407
445
 
408
446
  async function handleGetAccounts(req: Request, res: Response): Promise<void> {
@@ -412,13 +450,24 @@ async function handleGetAccounts(req: Request, res: Response): Promise<void> {
412
450
 
413
451
  async function handleUpdateProfile(req: Request, res: Response): Promise<void> {
414
452
  const { name, email } = updateProfileSchema.parse(req.body);
415
- const user = await req.context.user.updateProfile(req.user!.id, { name, email });
453
+ const user = await req.context.user.updateProfile(
454
+ req.user!.id,
455
+ { name, email }
456
+ );
416
457
  res.json({ ok: true, user });
417
458
  }
418
459
 
419
- async function handleChangePassword(req: Request, res: Response): Promise<void> {
420
- const { current_password, new_password } = changePasswordSchema.parse(req.body);
421
- await req.context.auth.changePassword(req.user!.id, current_password, new_password);
460
+ async function handleChangePassword(
461
+ req: Request,
462
+ res: Response
463
+ ): Promise<void> {
464
+ const { current_password, new_password } =
465
+ changePasswordSchema.parse(req.body);
466
+ await req.context.auth.changePassword(
467
+ req.user!.id,
468
+ current_password,
469
+ new_password
470
+ );
422
471
  res.json({ ok: true, message: 'Senha alterada com sucesso' });
423
472
  }
424
473
 
@@ -470,12 +519,43 @@ router.post('/forgot-password', buildHandler(handleForgotPassword));
470
519
  router.get('/reset-password/:token', buildHandler(handleValidateResetToken));
471
520
  router.post('/reset-password/:token', buildHandler(handleResetPassword));
472
521
 
473
- router.get('/auth', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleAuth));
474
- router.get('/user/accounts', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleGetAccounts));
475
- router.post('/user/update-profile', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleUpdateProfile));
476
- router.post('/user/change-password', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleChangePassword));
477
- router.post('/user/avatar', trySetUserByTokenMiddleware, demandUserMiddleware, avatarUpload.single('avatar'), buildHandler(handleUploadAvatar));
478
- router.delete('/user/avatar', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleDeleteAvatar));
522
+ router.get(
523
+ '/auth',
524
+ trySetUserByTokenMiddleware,
525
+ demandUserMiddleware,
526
+ buildHandler(handleAuth)
527
+ );
528
+ router.get(
529
+ '/user/accounts',
530
+ trySetUserByTokenMiddleware,
531
+ demandUserMiddleware,
532
+ buildHandler(handleGetAccounts)
533
+ );
534
+ router.post(
535
+ '/user/update-profile',
536
+ trySetUserByTokenMiddleware,
537
+ demandUserMiddleware,
538
+ buildHandler(handleUpdateProfile)
539
+ );
540
+ router.post(
541
+ '/user/change-password',
542
+ trySetUserByTokenMiddleware,
543
+ demandUserMiddleware,
544
+ buildHandler(handleChangePassword)
545
+ );
546
+ router.post(
547
+ '/user/avatar',
548
+ trySetUserByTokenMiddleware,
549
+ demandUserMiddleware,
550
+ avatarUpload.single('avatar'),
551
+ buildHandler(handleUploadAvatar)
552
+ );
553
+ router.delete(
554
+ '/user/avatar',
555
+ trySetUserByTokenMiddleware,
556
+ demandUserMiddleware,
557
+ buildHandler(handleDeleteAvatar)
558
+ );
479
559
 
480
560
  export default function makeEndpoint(app: Express): void {
481
561
  app.use('/api', router);
@@ -1,9 +1,15 @@
1
1
  import type { Express, Request, Response } from 'express';
2
2
  import express from 'express';
3
- import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
3
+ import trySetUserByTokenMiddleware
4
+ from '#shared/middlewares/try-set-user-by-token.js';
4
5
  import demandUserMiddleware from '#shared/middlewares/demand-user.js';
5
6
  import { buildHandler } from '#shared/utils.js';
6
- import { createS3Upload, generateSignedUrl, extractS3Key, downloadFromS3 } from '#core/s3.js';
7
+ import {
8
+ createS3Upload,
9
+ generateSignedUrl,
10
+ extractS3Key,
11
+ downloadFromS3
12
+ } from '#core/s3.js';
7
13
  import { ValidationError, NotFoundError } from '#core/errors.js';
8
14
 
9
15
  const router = express.Router();
@@ -16,7 +22,8 @@ const upload = createS3Upload({
16
22
  'image/png',
17
23
  'image/jpg',
18
24
  'application/msword',
19
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
25
+ 'application/vnd.openxmlformats-officedocument'
26
+ + '.wordprocessingml.document',
20
27
  'application/vnd.ms-excel',
21
28
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
22
29
  ],
@@ -60,8 +67,10 @@ async function handleGetSignedUrl(req: Request, res: Response): Promise<void> {
60
67
  }
61
68
 
62
69
  async function handleDownload(req: Request, res: Response): Promise<void> {
70
+ // Express 5 wildcard params arrive as an array of path
71
+ // segments — rejoin them into the S3 key.
63
72
  const rawKey = req.params.key;
64
- const key = Array.isArray(rawKey) ? rawKey[0] : rawKey;
73
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : rawKey;
65
74
 
66
75
  if (!key) {
67
76
  throw new ValidationError('Chave do arquivo é obrigatória');
@@ -81,7 +90,7 @@ async function handleDownload(req: Request, res: Response): Promise<void> {
81
90
 
82
91
  router.post('/upload', upload.single('file'), buildHandler(handleUpload));
83
92
  router.get('/signed-url', buildHandler(handleGetSignedUrl));
84
- router.get('/download/:key(*)', buildHandler(handleDownload));
93
+ router.get('/download/*key', buildHandler(handleDownload));
85
94
 
86
95
  export default function makeEndpoint(app: Express): void {
87
96
  app.use(
@@ -15,9 +15,9 @@ const router = express.Router();
15
15
  // events at once.
16
16
  const eventSchema = z.object({
17
17
  kind: z.string().min(1).max(64),
18
- occurredAt: z.string().datetime(),
18
+ occurredAt: z.iso.datetime(),
19
19
  durationMs: z.number().int().nonnegative().optional(),
20
- payload: z.record(z.unknown()).optional()
20
+ payload: z.record(z.string(), z.unknown()).optional()
21
21
  });
22
22
 
23
23
  const syncSchema = z.object({
@@ -1,6 +1,7 @@
1
1
  import type { Express } from 'express';
2
2
  import express from 'express';
3
- import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
3
+ import trySetUserByTokenMiddleware
4
+ from '#shared/middlewares/try-set-user-by-token.js';
4
5
  import demandUserMiddleware from '#shared/middlewares/demand-user.js';
5
6
 
6
7
  const router = express.Router();