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.
- package/.claude/settings.local.json +16 -0
- package/build.mjs +409 -0
- package/cli.mjs +87 -109
- package/create.mjs +334 -0
- package/package.json +2 -7
- package/template/.claude/settings.local.json +20 -0
- package/template/.editorconfig +11 -0
- package/template/.github/copilot-instructions.md +186 -0
- package/template/.nvimrc +7 -0
- package/template/.vscode/extensions.json +9 -0
- package/template/.vscode/settings.json +28 -0
- package/template/AGENTS.md +305 -0
- package/template/CLAUDE.md +140 -0
- package/template/architecture.md +518 -0
- package/template/code.md +211 -0
- package/template/dev.sh +45 -0
- package/template/install.sh +47 -0
- package/template/misc/nginx.conf +151 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-api.service +31 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-site-api.service +31 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-site.service +30 -0
- package/template/seed.sh +23 -0
- package/template/server/.env.sample +40 -0
- package/template/server/.prettierrc +4 -0
- package/template/server/apps/api/app.ts +42 -0
- package/template/server/apps/api/routes/account.ts +98 -0
- package/template/server/apps/api/routes/admin.ts +80 -0
- package/template/server/apps/api/routes/auth.ts +222 -0
- package/template/server/apps/api/routes/files.ts +89 -0
- package/template/server/apps/api/routes/index.ts +19 -0
- package/template/server/apps/api/routes/user.ts +17 -0
- package/template/server/apps/shared/middlewares/demand-account-access.ts +52 -0
- package/template/server/apps/shared/middlewares/demand-admin-user.ts +25 -0
- package/template/server/apps/shared/middlewares/demand-user.ts +17 -0
- package/template/server/apps/shared/middlewares/error-handler.ts +28 -0
- package/template/server/apps/shared/middlewares/request-logger.ts +40 -0
- package/template/server/apps/shared/middlewares/try-set-user-by-token.ts +26 -0
- package/template/server/apps/shared/utils.ts +71 -0
- package/template/server/apps/site-api/app.ts +42 -0
- package/template/server/apps/site-api/routes/index.ts +10 -0
- package/template/server/apps/site-api/routes/public.ts +25 -0
- package/template/server/console/index.ts +26 -0
- package/template/server/console/runner.ts +67 -0
- package/template/server/console/tasks/example.ts +29 -0
- package/template/server/console/tasks/seed-admin.ts +71 -0
- package/template/server/core/context.ts +49 -0
- package/template/server/core/db.ts +12 -0
- package/template/server/core/email.ts +87 -0
- package/template/server/core/errors.ts +44 -0
- package/template/server/core/knexfile.ts +28 -0
- package/template/server/core/logger.ts +60 -0
- package/template/server/core/models/account.ts +319 -0
- package/template/server/core/models/auth.ts +317 -0
- package/template/server/core/models/base.ts +19 -0
- package/template/server/core/models/user.ts +343 -0
- package/template/server/core/s3.ts +183 -0
- package/template/server/emails/_styles.css +5 -0
- package/template/server/emails/_template.html +28 -0
- package/template/server/emails/accountWelcome.html +4 -0
- package/template/server/emails/forgetPassword.html +5 -0
- package/template/server/eslint.config.js +16 -0
- package/template/server/knex.sh +4 -0
- package/template/server/migrations/20260208000000_initial-schema.ts +56 -0
- package/template/server/migrations/20260208000001_seed-admin.ts +42 -0
- package/template/server/migrations/20260208000002_add-user-avatar.ts +13 -0
- package/template/server/package.json +58 -0
- package/template/server/tsconfig.json +27 -0
- package/template/server/types/api.ts +20 -0
- package/template/server/types/express.d.ts +35 -0
- package/template/ui/admin/.prettierrc +4 -0
- package/template/ui/admin/components.json +20 -0
- package/template/ui/admin/eslint.config.js +16 -0
- package/template/ui/admin/index.html +12 -0
- package/template/ui/admin/package.json +57 -0
- package/template/ui/admin/postcss.config.js +6 -0
- package/template/ui/admin/src/app.tsx +13 -0
- package/template/ui/admin/src/components/nav-user.tsx +95 -0
- package/template/ui/admin/src/components/theme-provider.tsx +65 -0
- package/template/ui/admin/src/components/theme-toggle.tsx +17 -0
- package/template/ui/admin/src/components/ui/badge.tsx +36 -0
- package/template/ui/admin/src/components/ui/button.tsx +56 -0
- package/template/ui/admin/src/components/ui/calendar.tsx +68 -0
- package/template/ui/admin/src/components/ui/card.tsx +79 -0
- package/template/ui/admin/src/components/ui/checkbox.tsx +28 -0
- package/template/ui/admin/src/components/ui/date-picker.tsx +78 -0
- package/template/ui/admin/src/components/ui/date-range-picker.tsx +208 -0
- package/template/ui/admin/src/components/ui/dialog.tsx +121 -0
- package/template/ui/admin/src/components/ui/dropdown-menu.tsx +200 -0
- package/template/ui/admin/src/components/ui/input.tsx +22 -0
- package/template/ui/admin/src/components/ui/label.tsx +24 -0
- package/template/ui/admin/src/components/ui/popover.tsx +29 -0
- package/template/ui/admin/src/components/ui/select.tsx +158 -0
- package/template/ui/admin/src/components/ui/separator.tsx +29 -0
- package/template/ui/admin/src/components/ui/sheet.tsx +136 -0
- package/template/ui/admin/src/components/ui/sidebar.tsx +759 -0
- package/template/ui/admin/src/components/ui/skeleton.tsx +15 -0
- package/template/ui/admin/src/components/ui/sonner.tsx +26 -0
- package/template/ui/admin/src/components/ui/switch.tsx +27 -0
- package/template/ui/admin/src/components/ui/table.tsx +117 -0
- package/template/ui/admin/src/components/ui/tabs.tsx +53 -0
- package/template/ui/admin/src/components/ui/textarea.tsx +22 -0
- package/template/ui/admin/src/components/ui/tooltip.tsx +28 -0
- package/template/ui/admin/src/hooks/queries/use-accounts.ts +68 -0
- package/template/ui/admin/src/hooks/queries/use-auth.ts +61 -0
- package/template/ui/admin/src/hooks/queries/use-dashboard.ts +70 -0
- package/template/ui/admin/src/hooks/queries/use-members.ts +90 -0
- package/template/ui/admin/src/hooks/queries/use-users.ts +99 -0
- package/template/ui/admin/src/hooks/use-confirm.tsx +92 -0
- package/template/ui/admin/src/hooks/use-mobile.tsx +22 -0
- package/template/ui/admin/src/hooks/use-sidebar.tsx +4 -0
- package/template/ui/admin/src/hooks/use-user.tsx +86 -0
- package/template/ui/admin/src/index.css +22 -0
- package/template/ui/admin/src/layouts/account.tsx +278 -0
- package/template/ui/admin/src/layouts/admin.tsx +182 -0
- package/template/ui/admin/src/layouts/public.tsx +9 -0
- package/template/ui/admin/src/layouts/user.tsx +107 -0
- package/template/ui/admin/src/lib/config.ts +9 -0
- package/template/ui/admin/src/lib/fetch-api.ts +101 -0
- package/template/ui/admin/src/lib/query-client.ts +12 -0
- package/template/ui/admin/src/lib/query-keys.ts +44 -0
- package/template/ui/admin/src/lib/utils.ts +24 -0
- package/template/ui/admin/src/main.tsx +17 -0
- package/template/ui/admin/src/pages/account/dashboard.tsx +128 -0
- package/template/ui/admin/src/pages/account/members.tsx +414 -0
- package/template/ui/admin/src/pages/account/settings.tsx +210 -0
- package/template/ui/admin/src/pages/admin/accounts.tsx +164 -0
- package/template/ui/admin/src/pages/admin/dashboard.tsx +243 -0
- package/template/ui/admin/src/pages/admin/users.tsx +395 -0
- package/template/ui/admin/src/pages/public/error.tsx +24 -0
- package/template/ui/admin/src/pages/public/forgot-password.tsx +74 -0
- package/template/ui/admin/src/pages/public/login.tsx +102 -0
- package/template/ui/admin/src/pages/public/reset-password.tsx +106 -0
- package/template/ui/admin/src/pages/public/signup.tsx +118 -0
- package/template/ui/admin/src/pages/user/profile.tsx +313 -0
- package/template/ui/admin/src/pages/user/select-account.tsx +114 -0
- package/template/ui/admin/src/router.tsx +71 -0
- package/template/ui/admin/tailwind.config.ts +71 -0
- package/template/ui/admin/tsconfig.json +24 -0
- package/template/ui/admin/vite.config.ts +15 -0
- package/template/ui/site/.env.sample +3 -0
- package/template/ui/site/.prettierrc +4 -0
- package/template/ui/site/app.ts +50 -0
- package/template/ui/site/eslint.config.js +16 -0
- package/template/ui/site/package.json +33 -0
- package/template/ui/site/styles/input.css +3 -0
- package/template/ui/site/tailwind.config.js +8 -0
- package/template/ui/site/tsconfig.json +15 -0
- package/template/ui/site/views/pages/home.ejs +305 -0
- package/template/ui/site/views/partials/footer.ejs +44 -0
- package/template/ui/site/views/partials/header.ejs +83 -0
- package/template-variables.mjs +186 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
|
|
5
|
+
import demandUserMiddleware from '#shared/middlewares/demand-user.js';
|
|
6
|
+
import demandAccountAccessMiddleware from '#shared/middlewares/demand-account-access.js';
|
|
7
|
+
import { buildHandler } from '#shared/utils.js';
|
|
8
|
+
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
|
|
11
|
+
const updateSettingsSchema = z.object({
|
|
12
|
+
name: z.string().min(1, 'Nome é obrigatório'),
|
|
13
|
+
settings: z.record(z.unknown()).optional()
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const addMemberSchema = z.object({
|
|
17
|
+
email: z.string().email('Email inválido'),
|
|
18
|
+
role: z.enum(['owner', 'manager', 'user']).default('user'),
|
|
19
|
+
name: z.string().min(1).optional(),
|
|
20
|
+
password: z.string().min(6).optional()
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const updateMemberRoleSchema = z.object({
|
|
24
|
+
role: z.enum(['owner', 'manager', 'user'])
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async function handleGetDashboard(req: Request, res: Response): Promise<void> {
|
|
28
|
+
const dashboard = await req.context.account.getAccountDashboard(
|
|
29
|
+
req.accountId!
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
res.json({
|
|
33
|
+
ok: true,
|
|
34
|
+
accountId: req.accountId,
|
|
35
|
+
...dashboard
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function handleGetSettings(req: Request, res: Response): Promise<void> {
|
|
40
|
+
const account = await req.context.account.findById(req.accountId!);
|
|
41
|
+
res.json({ ok: true, account });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function handleUpdateSettings(req: Request, res: Response): Promise<void> {
|
|
45
|
+
const data = updateSettingsSchema.parse(req.body);
|
|
46
|
+
const account = await req.context.account.update(req.accountId!, data);
|
|
47
|
+
res.json({ ok: true, account });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handleDeleteAccount(req: Request, res: Response): Promise<void> {
|
|
51
|
+
await req.context.account.delete(req.accountId!, req.user!.id, req.accountRole!);
|
|
52
|
+
res.json({ ok: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handleListMembers(req: Request, res: Response): Promise<void> {
|
|
56
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
57
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
58
|
+
const search = (req.query.search as string) || '';
|
|
59
|
+
|
|
60
|
+
const result = await req.context.account.listMembers(req.accountId!, { page, limit, search });
|
|
61
|
+
res.json({ ok: true, ...result });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleAddMember(req: Request, res: Response): Promise<void> {
|
|
65
|
+
const data = addMemberSchema.parse(req.body);
|
|
66
|
+
const member = await req.context.account.addMember(req.accountId!, data);
|
|
67
|
+
res.json({ ok: true, member });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function handleUpdateMemberRole(req: Request, res: Response): Promise<void> {
|
|
71
|
+
const { role } = updateMemberRoleSchema.parse(req.body);
|
|
72
|
+
await req.context.account.updateMemberRole(req.accountId!, req.params.userId, role);
|
|
73
|
+
res.json({ ok: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handleRemoveMember(req: Request, res: Response): Promise<void> {
|
|
77
|
+
await req.context.account.removeMember(req.accountId!, req.params.userId);
|
|
78
|
+
res.json({ ok: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
router.get('/dashboard', buildHandler(handleGetDashboard));
|
|
82
|
+
router.get('/settings', buildHandler(handleGetSettings));
|
|
83
|
+
router.post('/settings', buildHandler(handleUpdateSettings));
|
|
84
|
+
router.delete('/delete', buildHandler(handleDeleteAccount));
|
|
85
|
+
router.get('/members', buildHandler(handleListMembers));
|
|
86
|
+
router.post('/members', buildHandler(handleAddMember));
|
|
87
|
+
router.post('/members/:userId', buildHandler(handleUpdateMemberRole));
|
|
88
|
+
router.delete('/members/:userId', buildHandler(handleRemoveMember));
|
|
89
|
+
|
|
90
|
+
export default function makeEndpoint(app: Express): void {
|
|
91
|
+
app.use(
|
|
92
|
+
'/api/account/:accountId',
|
|
93
|
+
trySetUserByTokenMiddleware,
|
|
94
|
+
demandUserMiddleware,
|
|
95
|
+
demandAccountAccessMiddleware,
|
|
96
|
+
router
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
|
|
5
|
+
import demandUserMiddleware from '#shared/middlewares/demand-user.js';
|
|
6
|
+
import demandAdminUserMiddleware from '#shared/middlewares/demand-admin-user.js';
|
|
7
|
+
import { buildHandler } from '#shared/utils.js';
|
|
8
|
+
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
|
|
11
|
+
const createUserSchema = z.object({
|
|
12
|
+
name: z.string().min(1, 'Nome é obrigatório'),
|
|
13
|
+
email: z.string().email('Email inválido'),
|
|
14
|
+
password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres'),
|
|
15
|
+
role: z.enum(['admin', 'user']).default('user')
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const updateUserSchema = z.object({
|
|
19
|
+
name: z.string().min(1, 'Nome é obrigatório'),
|
|
20
|
+
email: z.string().email('Email inválido'),
|
|
21
|
+
role: z.enum(['admin', 'user']).optional(),
|
|
22
|
+
password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres').optional()
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function handleGetDashboard(req: Request, res: Response): Promise<void> {
|
|
26
|
+
const data = await req.context.account.getDashboardStats();
|
|
27
|
+
res.json({ ok: true, ...data });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function handleGetAccounts(req: Request, res: Response): Promise<void> {
|
|
31
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
32
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
33
|
+
const search = (req.query.search as string) || '';
|
|
34
|
+
|
|
35
|
+
const result = await req.context.account.list({ page, limit, search });
|
|
36
|
+
res.json({ ok: true, ...result });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function handleGetUsers(req: Request, res: Response): Promise<void> {
|
|
40
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
41
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
42
|
+
const search = (req.query.search as string) || '';
|
|
43
|
+
|
|
44
|
+
const result = await req.context.user.list({ page, limit, search });
|
|
45
|
+
res.json({ ok: true, ...result });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function handleCreateUser(req: Request, res: Response): Promise<void> {
|
|
49
|
+
const data = createUserSchema.parse(req.body);
|
|
50
|
+
const user = await req.context.user.create(data);
|
|
51
|
+
res.json({ ok: true, user });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function handleUpdateUser(req: Request, res: Response): Promise<void> {
|
|
55
|
+
const data = updateUserSchema.parse(req.body);
|
|
56
|
+
const user = await req.context.user.adminUpdate(req.params.id, data);
|
|
57
|
+
res.json({ ok: true, user });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleDeleteUser(req: Request, res: Response): Promise<void> {
|
|
61
|
+
await req.context.user.delete(req.params.id, req.user!.id);
|
|
62
|
+
res.json({ ok: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
router.get('/dashboard', buildHandler(handleGetDashboard));
|
|
66
|
+
router.get('/accounts', buildHandler(handleGetAccounts));
|
|
67
|
+
router.get('/users', buildHandler(handleGetUsers));
|
|
68
|
+
router.post('/users', buildHandler(handleCreateUser));
|
|
69
|
+
router.post('/users/:id', buildHandler(handleUpdateUser));
|
|
70
|
+
router.delete('/users/:id', buildHandler(handleDeleteUser));
|
|
71
|
+
|
|
72
|
+
export default function makeEndpoint(app: Express): void {
|
|
73
|
+
app.use(
|
|
74
|
+
'/api/admin',
|
|
75
|
+
trySetUserByTokenMiddleware,
|
|
76
|
+
demandUserMiddleware,
|
|
77
|
+
demandAdminUserMiddleware,
|
|
78
|
+
router
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
|
|
5
|
+
import demandUserMiddleware from '#shared/middlewares/demand-user.js';
|
|
6
|
+
import { buildHandler } from '#shared/utils.js';
|
|
7
|
+
import { ForbiddenError } from '#core/errors.js';
|
|
8
|
+
import { createS3Upload, generateSignedUrl, deleteFromS3 } from '#core/s3.js';
|
|
9
|
+
import logger from '#core/logger.js';
|
|
10
|
+
|
|
11
|
+
const router = express.Router();
|
|
12
|
+
|
|
13
|
+
const COOKIE_OPTIONS = {
|
|
14
|
+
httpOnly: true,
|
|
15
|
+
secure: process.env.NODE_ENV === 'production',
|
|
16
|
+
sameSite: (process.env.NODE_ENV === 'production' ? 'strict' : 'lax') as 'strict' | 'lax',
|
|
17
|
+
maxAge: 30 * 24 * 60 * 60 * 1000,
|
|
18
|
+
path: '/'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function setAuthCookie(res: Response, token: string): void {
|
|
22
|
+
res.cookie('auth_token', token, COOKIE_OPTIONS);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clearAuthCookie(res: Response): void {
|
|
26
|
+
res.clearCookie('auth_token', { path: '/' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const loginSchema = z.object({
|
|
30
|
+
email: z.string().email('Email inválido'),
|
|
31
|
+
password: z.string().min(1, 'Senha é obrigatória')
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const signupSchema = z.object({
|
|
35
|
+
email: z.string().email('Email inválido'),
|
|
36
|
+
password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres'),
|
|
37
|
+
name: z.string().min(1, 'Nome é obrigatório'),
|
|
38
|
+
account_name: z.string().optional()
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const forgotPasswordSchema = z.object({
|
|
42
|
+
email: z.string().email('Email inválido')
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const resetPasswordSchema = z.object({
|
|
46
|
+
password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres')
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const updateProfileSchema = z.object({
|
|
50
|
+
name: z.string().min(1, 'Nome é obrigatório'),
|
|
51
|
+
email: z.string().email('Email inválido')
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const changePasswordSchema = z.object({
|
|
55
|
+
current_password: z.string().min(1, 'Senha atual é obrigatória'),
|
|
56
|
+
new_password: z.string().min(6, 'Nova senha deve ter no mínimo 6 caracteres')
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const avatarUpload = createS3Upload({
|
|
60
|
+
folder: 'avatars',
|
|
61
|
+
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/jpg', 'image/webp'],
|
|
62
|
+
maxFileSize: 2 * 1024 * 1024, // 2MB
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
async function resolveAvatarUrl(avatarKey: string | null): Promise<string | null> {
|
|
66
|
+
if (!avatarKey) return null;
|
|
67
|
+
return generateSignedUrl(avatarKey, 3600);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function userWithAvatar(user: any) {
|
|
71
|
+
return {
|
|
72
|
+
id: user.id,
|
|
73
|
+
email: user.email,
|
|
74
|
+
name: user.name,
|
|
75
|
+
role: user.role,
|
|
76
|
+
avatar_url: await resolveAvatarUrl(user.avatar_key)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleLogin(req: Request, res: Response): Promise<void> {
|
|
81
|
+
const { email, password } = loginSchema.parse(req.body);
|
|
82
|
+
const result = await req.context.auth.login(email, password);
|
|
83
|
+
|
|
84
|
+
setAuthCookie(res, result.token);
|
|
85
|
+
const fullUser = await req.context.user.findById(result.user.id);
|
|
86
|
+
res.json({ ok: true, user: await userWithAvatar(fullUser) });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function handleSignup(req: Request, res: Response): Promise<void> {
|
|
90
|
+
const { email, password, name, account_name } = signupSchema.parse(req.body);
|
|
91
|
+
const result = await req.context.auth.signup({
|
|
92
|
+
email, password, name, accountName: account_name
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (result.existingUser) {
|
|
96
|
+
res.json({
|
|
97
|
+
ok: true,
|
|
98
|
+
existingUser: true,
|
|
99
|
+
message: 'Uma nova conta foi criada. Como seu email já estava cadastrado, faça login para acessá-la.'
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setAuthCookie(res, result.token);
|
|
105
|
+
res.json({ ok: true, user: result.user, account: result.account });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleLogout(_req: Request, res: Response): Promise<void> {
|
|
109
|
+
clearAuthCookie(res);
|
|
110
|
+
res.json({ ok: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function handleForgotPassword(req: Request, res: Response): Promise<void> {
|
|
114
|
+
const { email } = forgotPasswordSchema.parse(req.body);
|
|
115
|
+
await req.context.auth.forgotPassword(email);
|
|
116
|
+
res.json({ ok: true, message: 'Se o email existir, você receberá um link de recuperação' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleValidateResetToken(req: Request, res: Response): Promise<void> {
|
|
120
|
+
const valid = await req.context.auth.validateResetToken(req.params.token);
|
|
121
|
+
res.json({ ok: valid });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function handleResetPassword(req: Request, res: Response): Promise<void> {
|
|
125
|
+
const { password } = resetPasswordSchema.parse(req.body);
|
|
126
|
+
await req.context.auth.resetPassword(req.params.token, password);
|
|
127
|
+
res.json({ ok: true, message: 'Senha alterada com sucesso' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function handleAuth(req: Request, res: Response): Promise<void> {
|
|
131
|
+
const rawUser = req.user!;
|
|
132
|
+
const fullUser = await req.context.user.findById(rawUser.id);
|
|
133
|
+
const user = await userWithAvatar(fullUser);
|
|
134
|
+
const accountId = req.query.account as string | undefined;
|
|
135
|
+
|
|
136
|
+
if (!accountId) {
|
|
137
|
+
res.json({ ok: true, user });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const userAccount = await req.context.user.getAccount(
|
|
142
|
+
rawUser.id, accountId, rawUser.role
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (!userAccount) {
|
|
146
|
+
throw new ForbiddenError('Acesso negado a esta conta');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
res.json({ ok: true, user, account: userAccount.account, role: userAccount.role });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function handleGetAccounts(req: Request, res: Response): Promise<void> {
|
|
153
|
+
const accounts = await req.context.user.getAccounts(req.user!.id);
|
|
154
|
+
res.json({ ok: true, accounts });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function handleUpdateProfile(req: Request, res: Response): Promise<void> {
|
|
158
|
+
const { name, email } = updateProfileSchema.parse(req.body);
|
|
159
|
+
const user = await req.context.user.updateProfile(req.user!.id, { name, email });
|
|
160
|
+
res.json({ ok: true, user });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function handleChangePassword(req: Request, res: Response): Promise<void> {
|
|
164
|
+
const { current_password, new_password } = changePasswordSchema.parse(req.body);
|
|
165
|
+
await req.context.auth.changePassword(req.user!.id, current_password, new_password);
|
|
166
|
+
res.json({ ok: true, message: 'Senha alterada com sucesso' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function handleUploadAvatar(req: Request, res: Response): Promise<void> {
|
|
170
|
+
const file = req.file as Express.MulterS3.File | undefined;
|
|
171
|
+
|
|
172
|
+
if (!file) {
|
|
173
|
+
res.status(400).json({ ok: false, message: 'Nenhuma imagem enviada' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Delete old avatar from S3 if exists
|
|
178
|
+
const currentUser = await req.context.user.findById(req.user!.id);
|
|
179
|
+
if (currentUser.avatar_key) {
|
|
180
|
+
try {
|
|
181
|
+
await deleteFromS3(currentUser.avatar_key);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
logger.error('{auth} failed to delete old avatar', err);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const user = await req.context.user.updateAvatar(req.user!.id, file.key);
|
|
188
|
+
res.json({ ok: true, user: await userWithAvatar(user) });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function handleDeleteAvatar(req: Request, res: Response): Promise<void> {
|
|
192
|
+
const currentUser = await req.context.user.findById(req.user!.id);
|
|
193
|
+
|
|
194
|
+
if (currentUser.avatar_key) {
|
|
195
|
+
try {
|
|
196
|
+
await deleteFromS3(currentUser.avatar_key);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
logger.error('{auth} failed to delete avatar', err);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const user = await req.context.user.updateAvatar(req.user!.id, null);
|
|
203
|
+
res.json({ ok: true, user: await userWithAvatar(user) });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
router.post('/login', buildHandler(handleLogin));
|
|
207
|
+
router.post('/signup', buildHandler(handleSignup));
|
|
208
|
+
router.post('/logout', buildHandler(handleLogout));
|
|
209
|
+
router.post('/forgot-password', buildHandler(handleForgotPassword));
|
|
210
|
+
router.get('/reset-password/:token', buildHandler(handleValidateResetToken));
|
|
211
|
+
router.post('/reset-password/:token', buildHandler(handleResetPassword));
|
|
212
|
+
|
|
213
|
+
router.get('/auth', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleAuth));
|
|
214
|
+
router.get('/user/accounts', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleGetAccounts));
|
|
215
|
+
router.post('/user/update-profile', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleUpdateProfile));
|
|
216
|
+
router.post('/user/change-password', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleChangePassword));
|
|
217
|
+
router.post('/user/avatar', trySetUserByTokenMiddleware, demandUserMiddleware, avatarUpload.single('avatar'), buildHandler(handleUploadAvatar));
|
|
218
|
+
router.delete('/user/avatar', trySetUserByTokenMiddleware, demandUserMiddleware, buildHandler(handleDeleteAvatar));
|
|
219
|
+
|
|
220
|
+
export default function makeEndpoint(app: Express): void {
|
|
221
|
+
app.use('/api', router);
|
|
222
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
|
|
4
|
+
import demandUserMiddleware from '#shared/middlewares/demand-user.js';
|
|
5
|
+
import { buildHandler } from '#shared/utils.js';
|
|
6
|
+
import { createS3Upload, generateSignedUrl, extractS3Key, downloadFromS3 } from '#core/s3.js';
|
|
7
|
+
import { ValidationError, NotFoundError } from '#core/errors.js';
|
|
8
|
+
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
|
|
11
|
+
const upload = createS3Upload({
|
|
12
|
+
folder: 'uploads',
|
|
13
|
+
allowedMimeTypes: [
|
|
14
|
+
'application/pdf',
|
|
15
|
+
'image/jpeg',
|
|
16
|
+
'image/png',
|
|
17
|
+
'image/jpg',
|
|
18
|
+
'application/msword',
|
|
19
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
20
|
+
'application/vnd.ms-excel',
|
|
21
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
22
|
+
],
|
|
23
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function handleUpload(req: Request, res: Response): Promise<void> {
|
|
27
|
+
const file = req.file as Express.MulterS3.File | undefined;
|
|
28
|
+
|
|
29
|
+
if (!file) {
|
|
30
|
+
throw new ValidationError('Nenhum arquivo enviado');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
res.json({
|
|
34
|
+
ok: true,
|
|
35
|
+
file: {
|
|
36
|
+
url: file.location,
|
|
37
|
+
key: file.key,
|
|
38
|
+
originalName: file.originalname,
|
|
39
|
+
size: file.size,
|
|
40
|
+
mimetype: file.mimetype
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleGetSignedUrl(req: Request, res: Response): Promise<void> {
|
|
46
|
+
const { url } = req.query;
|
|
47
|
+
|
|
48
|
+
if (!url || typeof url !== 'string') {
|
|
49
|
+
throw new ValidationError('URL é obrigatória');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const key = extractS3Key(url);
|
|
53
|
+
|
|
54
|
+
if (!key) {
|
|
55
|
+
throw new ValidationError('URL S3 inválida');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const signedUrl = await generateSignedUrl(key, 3600);
|
|
59
|
+
res.json({ ok: true, signedUrl });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function handleDownload(req: Request, res: Response): Promise<void> {
|
|
63
|
+
const { key } = req.params;
|
|
64
|
+
|
|
65
|
+
if (!key) {
|
|
66
|
+
throw new ValidationError('Chave do arquivo é obrigatória');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const buffer = await downloadFromS3(key);
|
|
71
|
+
res.setHeader('Content-Disposition', `attachment; filename="${key.split('/').pop()}"`);
|
|
72
|
+
res.send(buffer);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new NotFoundError('Arquivo não encontrado');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
router.post('/upload', upload.single('file'), buildHandler(handleUpload));
|
|
79
|
+
router.get('/signed-url', buildHandler(handleGetSignedUrl));
|
|
80
|
+
router.get('/download/:key(*)', buildHandler(handleDownload));
|
|
81
|
+
|
|
82
|
+
export default function makeEndpoint(app: Express): void {
|
|
83
|
+
app.use(
|
|
84
|
+
'/api/files',
|
|
85
|
+
trySetUserByTokenMiddleware,
|
|
86
|
+
demandUserMiddleware,
|
|
87
|
+
router
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import makeAuthRoute from './auth.js';
|
|
3
|
+
import makeUserRoute from './user.js';
|
|
4
|
+
import makeAccountRoute from './account.js';
|
|
5
|
+
import makeAdminRoute from './admin.js';
|
|
6
|
+
import makeFilesRoute from './files.js';
|
|
7
|
+
|
|
8
|
+
export default function makeRoutes(app: Express): void {
|
|
9
|
+
makeAuthRoute(app);
|
|
10
|
+
makeUserRoute(app);
|
|
11
|
+
makeAccountRoute(app);
|
|
12
|
+
makeAdminRoute(app);
|
|
13
|
+
makeFilesRoute(app);
|
|
14
|
+
|
|
15
|
+
// Add new route modules here as the project grows:
|
|
16
|
+
// makeCompanyRoute(app);
|
|
17
|
+
// makeEmployeeRoute(app);
|
|
18
|
+
// makeDocumentRoute(app);
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
|
|
4
|
+
import demandUserMiddleware from '#shared/middlewares/demand-user.js';
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
// Add user-scoped routes here (profile, notifications, preferences, etc.)
|
|
9
|
+
|
|
10
|
+
export default function makeEndpoint(app: Express): void {
|
|
11
|
+
app.use(
|
|
12
|
+
'/api/user',
|
|
13
|
+
trySetUserByTokenMiddleware,
|
|
14
|
+
demandUserMiddleware,
|
|
15
|
+
router
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export default async function demandAccountAccess(
|
|
4
|
+
req: Request,
|
|
5
|
+
res: Response,
|
|
6
|
+
next: NextFunction
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
if (!req.user) {
|
|
9
|
+
res.status(401).json({
|
|
10
|
+
ok: false,
|
|
11
|
+
message: 'Authentication required'
|
|
12
|
+
});
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const accountId = req.params.accountId || (req.query.accountId as string);
|
|
17
|
+
|
|
18
|
+
if (!accountId) {
|
|
19
|
+
res.status(400).json({
|
|
20
|
+
ok: false,
|
|
21
|
+
message: 'Account ID is required'
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Admin users have access to all accounts
|
|
27
|
+
if (req.user.role === 'admin') {
|
|
28
|
+
req.accountId = accountId;
|
|
29
|
+
req.accountRole = 'admin';
|
|
30
|
+
next();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check user has access to this account
|
|
35
|
+
const userAccount = await req.context.user.getAccount(
|
|
36
|
+
req.user.id,
|
|
37
|
+
accountId,
|
|
38
|
+
req.user.role
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!userAccount) {
|
|
42
|
+
res.status(403).json({
|
|
43
|
+
ok: false,
|
|
44
|
+
message: 'Access to this account is forbidden'
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
req.accountId = accountId;
|
|
50
|
+
req.accountRole = userAccount.role;
|
|
51
|
+
next();
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export default function demandAdminUser(
|
|
4
|
+
req: Request,
|
|
5
|
+
res: Response,
|
|
6
|
+
next: NextFunction
|
|
7
|
+
): void {
|
|
8
|
+
if (!req.user) {
|
|
9
|
+
res.status(401).json({
|
|
10
|
+
ok: false,
|
|
11
|
+
message: 'Authentication required'
|
|
12
|
+
});
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (req.user.role !== 'admin') {
|
|
17
|
+
res.status(403).json({
|
|
18
|
+
ok: false,
|
|
19
|
+
message: 'Admin access required'
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
next();
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export default function demandUser(
|
|
4
|
+
req: Request,
|
|
5
|
+
res: Response,
|
|
6
|
+
next: NextFunction
|
|
7
|
+
): void {
|
|
8
|
+
if (!req.user) {
|
|
9
|
+
res.status(401).json({
|
|
10
|
+
ok: false,
|
|
11
|
+
message: 'Authentication required'
|
|
12
|
+
});
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
next();
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { AppError } from '#core/errors.js';
|
|
3
|
+
import logger from '#core/logger.js';
|
|
4
|
+
|
|
5
|
+
export default function errorHandler(
|
|
6
|
+
err: Error,
|
|
7
|
+
req: Request,
|
|
8
|
+
res: Response,
|
|
9
|
+
_next: NextFunction
|
|
10
|
+
): void {
|
|
11
|
+
logger.error(err.message, {
|
|
12
|
+
stack: err.stack,
|
|
13
|
+
method: req.method,
|
|
14
|
+
path: req.originalUrl,
|
|
15
|
+
ip: req.ip,
|
|
16
|
+
userId: req.user?.id
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const statusCode = err instanceof AppError ? err.statusCode : 500;
|
|
20
|
+
const message = statusCode === 500
|
|
21
|
+
? 'Erro interno do servidor'
|
|
22
|
+
: err.message;
|
|
23
|
+
|
|
24
|
+
res.status(statusCode).json({
|
|
25
|
+
ok: false,
|
|
26
|
+
message
|
|
27
|
+
});
|
|
28
|
+
}
|