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.
- package/.claude/settings.local.json +14 -1
- package/create.mjs +3 -3
- package/package.json +5 -5
- package/template/.github/copilot-instructions.md +1 -1
- package/template/AGENTS.md +1 -1
- package/template/mobile/{{PROJECT_SLUG}}/README.md +17 -2
- package/template/mobile/{{PROJECT_SLUG}}/package.json +23 -25
- package/template/server/apps/api/app.ts +1 -1
- package/template/server/apps/api/routes/account.ts +32 -10
- package/template/server/apps/api/routes/admin.ts +13 -6
- package/template/server/apps/api/routes/auth.ts +109 -29
- package/template/server/apps/api/routes/files.ts +14 -5
- package/template/server/apps/api/routes/health.ts +2 -2
- package/template/server/apps/api/routes/user.ts +2 -1
- package/template/server/apps/shared/middlewares/request-logger.ts +3 -1
- package/template/server/apps/shared/utils.ts +2 -2
- package/template/server/apps/site-api/app.ts +1 -1
- package/template/server/console/index.ts +1 -1
- package/template/server/console/tasks/seed-admin.ts +8 -2
- package/template/server/core/context.ts +2 -2
- package/template/server/core/email.ts +9 -2
- package/template/server/core/knexfile.ts +1 -1
- package/template/server/core/models/account.ts +43 -13
- package/template/server/core/models/auth.ts +10 -3
- package/template/server/core/models/user.ts +61 -27
- package/template/server/core/s3.ts +29 -14
- package/template/server/eslint.config.js +9 -1
- package/template/server/migrations/20260208000000_initial-schema.ts +4 -2
- package/template/server/package.json +27 -28
- package/template/server/types/api.ts +3 -0
- package/template/ui/admin/CLAUDE.md +5 -4
- package/template/ui/admin/eslint.config.js +29 -1
- package/template/ui/admin/package.json +31 -39
- package/template/ui/admin/src/components/nav-user.tsx +4 -2
- package/template/ui/admin/src/components/theme-provider.tsx +7 -1
- package/template/ui/admin/src/components/ui/badge.tsx +1 -1
- package/template/ui/admin/src/components/ui/button.tsx +1 -1
- package/template/ui/admin/src/components/ui/card.tsx +1 -1
- package/template/ui/admin/src/components/ui/checkbox.tsx +1 -1
- package/template/ui/admin/src/components/ui/date-picker.tsx +3 -3
- package/template/ui/admin/src/components/ui/dialog.tsx +1 -1
- package/template/ui/admin/src/components/ui/dropdown-menu.tsx +6 -6
- package/template/ui/admin/src/components/ui/input.tsx +1 -1
- package/template/ui/admin/src/components/ui/popover.tsx +1 -1
- package/template/ui/admin/src/components/ui/select.tsx +4 -4
- package/template/ui/admin/src/components/ui/separator.tsx +1 -1
- package/template/ui/admin/src/components/ui/sheet.tsx +1 -1
- package/template/ui/admin/src/components/ui/sidebar.tsx +24 -24
- package/template/ui/admin/src/components/ui/sonner.tsx +4 -4
- package/template/ui/admin/src/components/ui/switch.tsx +1 -1
- package/template/ui/admin/src/components/ui/table.tsx +3 -3
- package/template/ui/admin/src/components/ui/tabs.tsx +2 -2
- package/template/ui/admin/src/components/ui/textarea.tsx +1 -1
- package/template/ui/admin/src/hooks/queries/use-accounts.ts +29 -9
- package/template/ui/admin/src/hooks/queries/use-auth.ts +20 -6
- package/template/ui/admin/src/hooks/queries/use-dashboard.ts +13 -5
- package/template/ui/admin/src/hooks/queries/use-members.ts +32 -10
- package/template/ui/admin/src/hooks/queries/use-users.ts +32 -10
- package/template/ui/admin/src/hooks/use-confirm.tsx +1 -1
- package/template/ui/admin/src/hooks/use-user.tsx +4 -1
- package/template/ui/admin/src/index.css +68 -6
- package/template/ui/admin/src/layouts/account.tsx +19 -6
- package/template/ui/admin/src/layouts/user.tsx +3 -1
- package/template/ui/admin/src/lib/fetch-api.ts +38 -14
- package/template/ui/admin/src/lib/query-keys.ts +2 -1
- package/template/ui/admin/src/pages/account/dashboard.tsx +10 -3
- package/template/ui/admin/src/pages/account/members.tsx +22 -12
- package/template/ui/admin/src/pages/account/settings.tsx +22 -11
- package/template/ui/admin/src/pages/admin/accounts.tsx +2 -3
- package/template/ui/admin/src/pages/admin/dashboard.tsx +10 -3
- package/template/ui/admin/src/pages/admin/users.tsx +19 -10
- package/template/ui/admin/src/pages/public/error.tsx +4 -1
- package/template/ui/admin/src/pages/public/forgot-password.tsx +2 -2
- package/template/ui/admin/src/pages/public/login.tsx +13 -3
- package/template/ui/admin/src/pages/public/reset-password.tsx +5 -3
- package/template/ui/admin/src/pages/public/signup.tsx +11 -3
- package/template/ui/admin/src/pages/user/profile.tsx +16 -11
- package/template/ui/admin/src/pages/user/select-account.tsx +3 -1
- package/template/ui/admin/src/vite-env.d.ts +1 -0
- package/template/ui/admin/tsconfig.json +0 -1
- package/template/ui/admin/vite.config.ts +2 -1
- package/template/ui/site/CLAUDE.md +1 -2
- package/template/ui/site/_variants/app/home.ejs +6 -6
- package/template/ui/site/app.ts +1 -1
- package/template/ui/site/eslint.config.js +9 -1
- package/template/ui/site/i18n/index.ts +0 -1
- package/template/ui/site/package.json +17 -17
- package/template/ui/site/styles/input.css +11 -3
- package/template/ui/site/views/pages/home.ejs +4 -4
- package/template/ui/site/views/partials/header.ejs +1 -1
- package/template-variables.mjs +1 -1
- package/template/ui/admin/postcss.config.js +0 -6
- package/template/ui/admin/tailwind.config.ts +0 -71
- package/template/ui/site/tailwind.config.js +0 -16
- /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: '
|
|
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: '
|
|
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: '
|
|
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.
|
|
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.
|
|
28
|
-
"figlet": "^1.
|
|
29
|
-
"inquirer": "^
|
|
30
|
-
"sharp": "^0.
|
|
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
|
|
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
|
package/template/AGENTS.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
24
|
-
"@react-navigation/native": "^7.
|
|
25
|
-
"@react-navigation/native-stack": "^7.
|
|
26
|
-
"expo": "~57.0.
|
|
27
|
-
"expo-apple-authentication": "~57.0.
|
|
28
|
-
"expo-
|
|
29
|
-
"expo-
|
|
30
|
-
"expo-
|
|
31
|
-
"expo-file-system": "~57.0.
|
|
32
|
-
"expo-
|
|
33
|
-
"expo-
|
|
34
|
-
"expo-
|
|
35
|
-
"expo-
|
|
36
|
-
"expo-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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.
|
|
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.
|
|
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": "^
|
|
51
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
54
52
|
"@types/react": "~19.2.2",
|
|
55
|
-
"babel-preset-expo": "~57.0.
|
|
56
|
-
"eslint": "^
|
|
53
|
+
"babel-preset-expo": "~57.0.3",
|
|
54
|
+
"eslint": "^10.7.0",
|
|
57
55
|
"typescript": "~5.9.3",
|
|
58
|
-
"typescript-eslint": "^8.
|
|
56
|
+
"typescript-eslint": "^8.64.0"
|
|
59
57
|
}
|
|
60
58
|
}
|
|
@@ -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
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
22
|
+
email: z.email('Email inválido'),
|
|
21
23
|
role: z.enum(['admin', 'user']).optional(),
|
|
22
|
-
password: z.string()
|
|
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(
|
|
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
|
|
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'
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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()
|
|
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(
|
|
71
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
|
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(
|
|
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({
|
|
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(
|
|
376
|
-
|
|
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(
|
|
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({
|
|
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(
|
|
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(
|
|
420
|
-
|
|
421
|
-
|
|
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(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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();
|