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
|
@@ -25,7 +25,9 @@ export default function requestLogger(
|
|
|
25
25
|
idsMsg = ` u=${usr}`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const msg = `{req} [${ip}] ${method} ${url}${idsMsg}
|
|
28
|
+
const msg = `{req} [${ip}] ${method} ${url}${idsMsg}`
|
|
29
|
+
+ ` : http=${res.statusCode}`
|
|
30
|
+
+ ` ${Date.now() - started}ms`;
|
|
29
31
|
|
|
30
32
|
if (res.statusCode >= 500) {
|
|
31
33
|
logger.error(msg);
|
|
@@ -11,7 +11,7 @@ export function buildHandler(handler: AsyncHandler) {
|
|
|
11
11
|
await handler(req, res);
|
|
12
12
|
} catch (error) {
|
|
13
13
|
if (error instanceof ZodError) {
|
|
14
|
-
const messages = error.
|
|
14
|
+
const messages = error.issues.map((e) => e.message).join(', ');
|
|
15
15
|
logger.warn(`{route} ValidationError: ${messages}`, {
|
|
16
16
|
path: req.originalUrl,
|
|
17
17
|
method: req.method,
|
|
@@ -20,7 +20,7 @@ export function buildHandler(handler: AsyncHandler) {
|
|
|
20
20
|
res.status(400).json({
|
|
21
21
|
ok: false,
|
|
22
22
|
message: messages,
|
|
23
|
-
errors: error.
|
|
23
|
+
errors: error.issues.map((e) => ({
|
|
24
24
|
field: e.path.join('.'),
|
|
25
25
|
message: e.message
|
|
26
26
|
}))
|
|
@@ -8,7 +8,11 @@ const task: ConsoleTask = {
|
|
|
8
8
|
|
|
9
9
|
async run(context, args) {
|
|
10
10
|
if (!args.email || !args.password) {
|
|
11
|
-
logger.error(
|
|
11
|
+
logger.error(
|
|
12
|
+
'Usage: npm run console -- --task=seed-admin '
|
|
13
|
+
+ '--email=admin@example.com '
|
|
14
|
+
+ '--password=yourpassword [--name=Admin]'
|
|
15
|
+
);
|
|
12
16
|
process.exit(1);
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -59,7 +63,9 @@ const task: ConsoleTask = {
|
|
|
59
63
|
logger.info('Welcome email sent', { email });
|
|
60
64
|
} catch (error) {
|
|
61
65
|
logger.error('Failed to send welcome email', {
|
|
62
|
-
error: error instanceof Error
|
|
66
|
+
error: error instanceof Error
|
|
67
|
+
? error.message
|
|
68
|
+
: String(error)
|
|
63
69
|
});
|
|
64
70
|
}
|
|
65
71
|
} else {
|
|
@@ -27,7 +27,7 @@ class Context {
|
|
|
27
27
|
try {
|
|
28
28
|
await knex.transaction(async (trx) => {
|
|
29
29
|
for (const key in this) {
|
|
30
|
-
const model =
|
|
30
|
+
const model: unknown = this[key];
|
|
31
31
|
if (model instanceof BaseModel) {
|
|
32
32
|
model.knex = trx;
|
|
33
33
|
}
|
|
@@ -37,7 +37,7 @@ class Context {
|
|
|
37
37
|
});
|
|
38
38
|
} finally {
|
|
39
39
|
for (const key in this) {
|
|
40
|
-
const model =
|
|
40
|
+
const model: unknown = this[key];
|
|
41
41
|
if (model instanceof BaseModel) {
|
|
42
42
|
model.knex = knex;
|
|
43
43
|
}
|
|
@@ -23,7 +23,10 @@ function getTransporter(): nodemailer.Transporter {
|
|
|
23
23
|
return _transporter;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function loadTemplate(
|
|
26
|
+
function loadTemplate(
|
|
27
|
+
templateName: string,
|
|
28
|
+
variables: Record<string, unknown>
|
|
29
|
+
): string {
|
|
27
30
|
const emailsDir = path.resolve(dirname, 'emails');
|
|
28
31
|
|
|
29
32
|
const stylesPath = path.join(emailsDir, '_styles.css');
|
|
@@ -52,7 +55,11 @@ function loadTemplate(templateName: string, variables: Record<string, unknown>):
|
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export function isEmailConfigured(): boolean {
|
|
55
|
-
return !!(
|
|
58
|
+
return !!(
|
|
59
|
+
process.env.EMAIL_HOST
|
|
60
|
+
&& process.env.EMAIL_USER
|
|
61
|
+
&& process.env.EMAIL_PASSWORD
|
|
62
|
+
);
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
export async function sendEmail(
|
|
@@ -4,7 +4,7 @@ import dirname from '#root/dirname.js';
|
|
|
4
4
|
|
|
5
5
|
// dirname.ts resolves to server/ root regardless of where node was
|
|
6
6
|
// invoked from — see its file header for the why.
|
|
7
|
-
dotenv.config({ path: path.resolve(dirname, '.env') });
|
|
7
|
+
dotenv.config({ path: path.resolve(dirname, '.env'), quiet: true });
|
|
8
8
|
|
|
9
9
|
const config = {
|
|
10
10
|
client: 'pg',
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import BaseModel from './base.js';
|
|
2
2
|
import logger from '#core/logger.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
NotFoundError,
|
|
5
|
+
ForbiddenError,
|
|
6
|
+
ConflictError,
|
|
7
|
+
ValidationError
|
|
8
|
+
} from '#core/errors.js';
|
|
4
9
|
|
|
5
10
|
interface PaginatedResult<T> {
|
|
6
11
|
rows: T[];
|
|
@@ -75,12 +80,19 @@ class AccountModel extends BaseModel {
|
|
|
75
80
|
};
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
async update(
|
|
83
|
+
async update(
|
|
84
|
+
id: string,
|
|
85
|
+
data: { name?: string; settings?: Record<string, unknown> }
|
|
86
|
+
) {
|
|
79
87
|
await this.findById(id);
|
|
80
88
|
|
|
81
89
|
const updateData: Record<string, unknown> = {};
|
|
82
|
-
if (data.name)
|
|
83
|
-
|
|
90
|
+
if (data.name) {
|
|
91
|
+
updateData.name = data.name;
|
|
92
|
+
}
|
|
93
|
+
if (data.settings) {
|
|
94
|
+
updateData.settings = JSON.stringify(data.settings);
|
|
95
|
+
}
|
|
84
96
|
|
|
85
97
|
if (Object.keys(updateData).length > 0) {
|
|
86
98
|
await this.knex('accounts').where({ id }).update(updateData);
|
|
@@ -93,7 +105,9 @@ class AccountModel extends BaseModel {
|
|
|
93
105
|
|
|
94
106
|
async delete(id: string, userId: string, userRole: string) {
|
|
95
107
|
if (userRole !== 'owner' && userRole !== 'admin') {
|
|
96
|
-
throw new ForbiddenError(
|
|
108
|
+
throw new ForbiddenError(
|
|
109
|
+
'Apenas proprietários podem excluir contas'
|
|
110
|
+
);
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
await this.findById(id);
|
|
@@ -134,7 +148,8 @@ class AccountModel extends BaseModel {
|
|
|
134
148
|
});
|
|
135
149
|
}
|
|
136
150
|
|
|
137
|
-
const [{ count }] = await baseQuery.clone()
|
|
151
|
+
const [{ count }] = await baseQuery.clone()
|
|
152
|
+
.count('ua.user_id as count');
|
|
138
153
|
const total = parseInt(String(count), 10);
|
|
139
154
|
|
|
140
155
|
const rows = await baseQuery.clone()
|
|
@@ -170,11 +185,14 @@ class AccountModel extends BaseModel {
|
|
|
170
185
|
if (!user) {
|
|
171
186
|
if (!data.password || data.password.length < 6) {
|
|
172
187
|
throw new ValidationError(
|
|
173
|
-
'Senha é obrigatória (mínimo 6 caracteres)
|
|
188
|
+
'Senha é obrigatória (mínimo 6 caracteres) '
|
|
189
|
+
+ 'para novos usuários'
|
|
174
190
|
);
|
|
175
191
|
}
|
|
176
192
|
if (!data.name) {
|
|
177
|
-
throw new ValidationError(
|
|
193
|
+
throw new ValidationError(
|
|
194
|
+
'Nome é obrigatório para novos usuários'
|
|
195
|
+
);
|
|
178
196
|
}
|
|
179
197
|
|
|
180
198
|
user = await this.context.user.create({
|
|
@@ -200,7 +218,10 @@ class AccountModel extends BaseModel {
|
|
|
200
218
|
role: data.role || 'user'
|
|
201
219
|
});
|
|
202
220
|
|
|
203
|
-
logger.info(
|
|
221
|
+
logger.info(
|
|
222
|
+
`{account} added member user_id=${user.id}`
|
|
223
|
+
+ ` to account_id=${accountId} role=${data.role}`
|
|
224
|
+
);
|
|
204
225
|
|
|
205
226
|
return {
|
|
206
227
|
id: user.id,
|
|
@@ -223,7 +244,10 @@ class AccountModel extends BaseModel {
|
|
|
223
244
|
.where({ user_id: userId, account_id: accountId })
|
|
224
245
|
.update({ role });
|
|
225
246
|
|
|
226
|
-
logger.info(
|
|
247
|
+
logger.info(
|
|
248
|
+
`{account} updated member role user_id=${userId}`
|
|
249
|
+
+ ` account_id=${accountId} role=${role}`
|
|
250
|
+
);
|
|
227
251
|
}
|
|
228
252
|
|
|
229
253
|
async removeMember(accountId: string, userId: string) {
|
|
@@ -236,14 +260,19 @@ class AccountModel extends BaseModel {
|
|
|
236
260
|
}
|
|
237
261
|
|
|
238
262
|
if (member.role === 'owner') {
|
|
239
|
-
throw new ForbiddenError(
|
|
263
|
+
throw new ForbiddenError(
|
|
264
|
+
'Não é possível remover o proprietário da conta'
|
|
265
|
+
);
|
|
240
266
|
}
|
|
241
267
|
|
|
242
268
|
await this.knex('user_in_accounts')
|
|
243
269
|
.where({ user_id: userId, account_id: accountId })
|
|
244
270
|
.delete();
|
|
245
271
|
|
|
246
|
-
logger.info(
|
|
272
|
+
logger.info(
|
|
273
|
+
`{account} removed member user_id=${userId}`
|
|
274
|
+
+ ` from account_id=${accountId}`
|
|
275
|
+
);
|
|
247
276
|
}
|
|
248
277
|
|
|
249
278
|
async getAccountDashboard(accountId: string) {
|
|
@@ -277,7 +306,8 @@ class AccountModel extends BaseModel {
|
|
|
277
306
|
|
|
278
307
|
async getDashboardStats() {
|
|
279
308
|
const [usersCount] = await this.knex('users').count('id as count');
|
|
280
|
-
const [accountsCount] = await this.knex('accounts')
|
|
309
|
+
const [accountsCount] = await this.knex('accounts')
|
|
310
|
+
.count('id as count');
|
|
281
311
|
|
|
282
312
|
const recentUsers = await this.knex('users')
|
|
283
313
|
.select('id', 'name', 'email', 'role', 'utc_created_on')
|
|
@@ -271,7 +271,10 @@ class AuthModel extends BaseModel {
|
|
|
271
271
|
role: 'owner'
|
|
272
272
|
});
|
|
273
273
|
|
|
274
|
-
logger.info(
|
|
274
|
+
logger.info(
|
|
275
|
+
`{auth} signup existing user=${email},`
|
|
276
|
+
+ ` new account=${account.name}`
|
|
277
|
+
);
|
|
275
278
|
|
|
276
279
|
return {
|
|
277
280
|
existingUser: true,
|
|
@@ -354,7 +357,8 @@ class AuthModel extends BaseModel {
|
|
|
354
357
|
expires_at: expiresAt
|
|
355
358
|
});
|
|
356
359
|
|
|
357
|
-
const baseUiUrl = process.env.BASE_UI_URL
|
|
360
|
+
const baseUiUrl = process.env.BASE_UI_URL
|
|
361
|
+
|| 'http://localhost:{{ADMIN_UI_PORT}}';
|
|
358
362
|
const recoverPasswordUrl = `${baseUiUrl}/recover/${token}`;
|
|
359
363
|
|
|
360
364
|
if (!isEmailConfigured()) {
|
|
@@ -436,7 +440,10 @@ class AuthModel extends BaseModel {
|
|
|
436
440
|
throw new NotFoundError('Usuário não encontrado');
|
|
437
441
|
}
|
|
438
442
|
|
|
439
|
-
const isValid = await bcrypt.compare(
|
|
443
|
+
const isValid = await bcrypt.compare(
|
|
444
|
+
currentPassword,
|
|
445
|
+
user.password_hash
|
|
446
|
+
);
|
|
440
447
|
if (!isValid) {
|
|
441
448
|
throw new ValidationError('Senha atual incorreta');
|
|
442
449
|
}
|
|
@@ -90,7 +90,10 @@ class UserModel extends BaseModel {
|
|
|
90
90
|
role: 'owner'
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
logger.info(
|
|
93
|
+
logger.info(
|
|
94
|
+
`{user} created user=${user.email}`
|
|
95
|
+
+ ` role=${user.role} account_id=${account.id}`
|
|
96
|
+
);
|
|
94
97
|
} else {
|
|
95
98
|
logger.info(`{user} created user=${user.email} role=${user.role}`);
|
|
96
99
|
}
|
|
@@ -171,7 +174,9 @@ class UserModel extends BaseModel {
|
|
|
171
174
|
|
|
172
175
|
async updateAvatar(userId: string, avatarKey: string | null) {
|
|
173
176
|
await this.findById(userId);
|
|
174
|
-
await this.knex('users')
|
|
177
|
+
await this.knex('users')
|
|
178
|
+
.where({ id: userId })
|
|
179
|
+
.update({ avatar_key: avatarKey });
|
|
175
180
|
logger.info(`{user} avatar-updated user_id=${userId}`);
|
|
176
181
|
return this.findById(userId);
|
|
177
182
|
}
|
|
@@ -203,7 +208,7 @@ class UserModel extends BaseModel {
|
|
|
203
208
|
.limit(limit)
|
|
204
209
|
.offset(offset);
|
|
205
210
|
|
|
206
|
-
const userIds = users.map((u:
|
|
211
|
+
const userIds = users.map((u: { id: string }) => u.id);
|
|
207
212
|
|
|
208
213
|
const accountLinks = userIds.length > 0
|
|
209
214
|
? await this.knex('user_in_accounts as ua')
|
|
@@ -217,7 +222,8 @@ class UserModel extends BaseModel {
|
|
|
217
222
|
)
|
|
218
223
|
: [];
|
|
219
224
|
|
|
220
|
-
const accountsByUser:
|
|
225
|
+
const accountsByUser:
|
|
226
|
+
Record<string, UserWithAccounts['accounts']> = {};
|
|
221
227
|
for (const link of accountLinks) {
|
|
222
228
|
if (!accountsByUser[link.user_id]) {
|
|
223
229
|
accountsByUser[link.user_id] = [];
|
|
@@ -229,10 +235,12 @@ class UserModel extends BaseModel {
|
|
|
229
235
|
});
|
|
230
236
|
}
|
|
231
237
|
|
|
232
|
-
const rows = users.map(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
const rows = users.map(
|
|
239
|
+
(u: Omit<UserWithAccounts, 'accounts'>) => ({
|
|
240
|
+
...u,
|
|
241
|
+
accounts: accountsByUser[u.id] || []
|
|
242
|
+
})
|
|
243
|
+
);
|
|
236
244
|
|
|
237
245
|
return {
|
|
238
246
|
rows,
|
|
@@ -245,13 +253,22 @@ class UserModel extends BaseModel {
|
|
|
245
253
|
|
|
246
254
|
async adminUpdate(
|
|
247
255
|
id: string,
|
|
248
|
-
data: {
|
|
256
|
+
data: {
|
|
257
|
+
name?: string;
|
|
258
|
+
email?: string;
|
|
259
|
+
role?: string;
|
|
260
|
+
password?: string;
|
|
261
|
+
}
|
|
249
262
|
) {
|
|
250
263
|
await this.findById(id);
|
|
251
264
|
|
|
252
265
|
const updateData: Record<string, string> = {};
|
|
253
|
-
if (data.name)
|
|
254
|
-
|
|
266
|
+
if (data.name) {
|
|
267
|
+
updateData.name = data.name;
|
|
268
|
+
}
|
|
269
|
+
if (data.role) {
|
|
270
|
+
updateData.role = data.role;
|
|
271
|
+
}
|
|
255
272
|
|
|
256
273
|
if (data.email) {
|
|
257
274
|
const email = data.email.toLowerCase().trim();
|
|
@@ -311,7 +328,9 @@ class UserModel extends BaseModel {
|
|
|
311
328
|
}
|
|
312
329
|
|
|
313
330
|
const updateData: Record<string, string> = {};
|
|
314
|
-
if (data.name)
|
|
331
|
+
if (data.name) {
|
|
332
|
+
updateData.name = data.name;
|
|
333
|
+
}
|
|
315
334
|
|
|
316
335
|
if (data.email) {
|
|
317
336
|
const email = data.email.toLowerCase().trim();
|
|
@@ -326,7 +345,9 @@ class UserModel extends BaseModel {
|
|
|
326
345
|
updateData.email = email;
|
|
327
346
|
}
|
|
328
347
|
|
|
329
|
-
if (Object.keys(updateData).length === 0)
|
|
348
|
+
if (Object.keys(updateData).length === 0) {
|
|
349
|
+
return this.findById(userId);
|
|
350
|
+
}
|
|
330
351
|
|
|
331
352
|
await this.knex('users').where({ id: userId }).update(updateData);
|
|
332
353
|
|
|
@@ -346,10 +367,12 @@ class UserModel extends BaseModel {
|
|
|
346
367
|
.select('id', 'name', 'status')
|
|
347
368
|
.orderBy('name');
|
|
348
369
|
|
|
349
|
-
return accounts.map(
|
|
350
|
-
account
|
|
351
|
-
|
|
352
|
-
|
|
370
|
+
return accounts.map(
|
|
371
|
+
(account: UserAccount['account']) => ({
|
|
372
|
+
account,
|
|
373
|
+
role: 'admin'
|
|
374
|
+
})
|
|
375
|
+
);
|
|
353
376
|
}
|
|
354
377
|
|
|
355
378
|
const rows = await this.knex('user_in_accounts as ua')
|
|
@@ -357,14 +380,21 @@ class UserModel extends BaseModel {
|
|
|
357
380
|
.where('ua.user_id', userId)
|
|
358
381
|
.select('a.id', 'a.name', 'a.status', 'ua.role');
|
|
359
382
|
|
|
360
|
-
return rows.map(
|
|
361
|
-
|
|
362
|
-
id:
|
|
363
|
-
name:
|
|
364
|
-
status:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
383
|
+
return rows.map(
|
|
384
|
+
(row: {
|
|
385
|
+
id: string;
|
|
386
|
+
name: string;
|
|
387
|
+
status: string;
|
|
388
|
+
role: string;
|
|
389
|
+
}) => ({
|
|
390
|
+
account: {
|
|
391
|
+
id: row.id,
|
|
392
|
+
name: row.name,
|
|
393
|
+
status: row.status
|
|
394
|
+
},
|
|
395
|
+
role: row.role
|
|
396
|
+
})
|
|
397
|
+
);
|
|
368
398
|
}
|
|
369
399
|
|
|
370
400
|
async getAccount(
|
|
@@ -377,7 +407,9 @@ class UserModel extends BaseModel {
|
|
|
377
407
|
.where({ id: accountId })
|
|
378
408
|
.first();
|
|
379
409
|
|
|
380
|
-
if (!account)
|
|
410
|
+
if (!account) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
381
413
|
|
|
382
414
|
return {
|
|
383
415
|
account: {
|
|
@@ -395,7 +427,9 @@ class UserModel extends BaseModel {
|
|
|
395
427
|
.select('a.id', 'a.name', 'a.status', 'ua.role')
|
|
396
428
|
.first();
|
|
397
429
|
|
|
398
|
-
if (!row)
|
|
430
|
+
if (!row) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
399
433
|
|
|
400
434
|
return {
|
|
401
435
|
account: {
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
PutObjectCommand,
|
|
5
5
|
DeleteObjectCommand,
|
|
6
6
|
CopyObjectCommand,
|
|
7
|
-
HeadObjectCommand
|
|
7
|
+
HeadObjectCommand,
|
|
8
|
+
type GetObjectCommandInput
|
|
8
9
|
} from '@aws-sdk/client-s3';
|
|
9
10
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
10
11
|
import multer from 'multer';
|
|
@@ -22,7 +23,9 @@ export function getS3Client(): S3Client {
|
|
|
22
23
|
|
|
23
24
|
if (!accessKeyId || !secretAccessKey) {
|
|
24
25
|
throw new Error(
|
|
25
|
-
'AWS credentials not configured. Set
|
|
26
|
+
'AWS credentials not configured. Set '
|
|
27
|
+
+ 'AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY '
|
|
28
|
+
+ 'environment variables.'
|
|
26
29
|
);
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -70,15 +73,15 @@ export function createS3Upload(options: CreateS3UploadOptions = {}) {
|
|
|
70
73
|
s3: getS3Client(),
|
|
71
74
|
bucket: getBucketName(),
|
|
72
75
|
contentType: multerS3.AUTO_CONTENT_TYPE,
|
|
73
|
-
metadata: (
|
|
76
|
+
metadata: (req, file, cb) => {
|
|
74
77
|
cb(null, {
|
|
75
78
|
fieldName: file.fieldname,
|
|
76
79
|
originalName: file.originalname,
|
|
77
|
-
uploadedBy:
|
|
80
|
+
uploadedBy: req.user?.id || 'unknown',
|
|
78
81
|
uploadedAt: new Date().toISOString()
|
|
79
82
|
});
|
|
80
83
|
},
|
|
81
|
-
key: (_req
|
|
84
|
+
key: (_req, file, cb) => {
|
|
82
85
|
const timestamp = Date.now();
|
|
83
86
|
const randomString = crypto.randomBytes(8).toString('hex');
|
|
84
87
|
const ext = path.extname(file.originalname);
|
|
@@ -88,7 +91,7 @@ export function createS3Upload(options: CreateS3UploadOptions = {}) {
|
|
|
88
91
|
};
|
|
89
92
|
|
|
90
93
|
if (acl === 'public-read') {
|
|
91
|
-
|
|
94
|
+
storageOptions.acl = 'public-read';
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
return multer({
|
|
@@ -98,14 +101,18 @@ export function createS3Upload(options: CreateS3UploadOptions = {}) {
|
|
|
98
101
|
if (allowedMimeTypes.includes(file.mimetype)) {
|
|
99
102
|
cb(null, true);
|
|
100
103
|
} else {
|
|
101
|
-
cb(new Error(
|
|
104
|
+
cb(new Error(
|
|
105
|
+
`Tipo de arquivo não permitido: ${file.mimetype}`
|
|
106
|
+
));
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
});
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
export function normalizeS3Url(url: string): string {
|
|
108
|
-
if (!url)
|
|
113
|
+
if (!url) {
|
|
114
|
+
return url;
|
|
115
|
+
}
|
|
109
116
|
|
|
110
117
|
const virtualHostedMatch = url.match(
|
|
111
118
|
/https?:\/\/([^.]+)\.s3\.([^.]+)\.amazonaws\.com\/(.+)/
|
|
@@ -120,19 +127,27 @@ export function normalizeS3Url(url: string): string {
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
export function extractS3Key(url: string): string | null {
|
|
123
|
-
if (!url)
|
|
130
|
+
if (!url) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
124
133
|
|
|
125
134
|
const virtualHostedMatch = url.match(
|
|
126
135
|
/https?:\/\/[^.]+\.s3\.[^.]+\.amazonaws\.com\/(.+)/
|
|
127
136
|
);
|
|
128
|
-
if (virtualHostedMatch)
|
|
137
|
+
if (virtualHostedMatch) {
|
|
138
|
+
return virtualHostedMatch[1];
|
|
139
|
+
}
|
|
129
140
|
|
|
130
141
|
const pathStyleMatch = url.match(
|
|
131
142
|
/https?:\/\/s3\.[^.]+\.amazonaws\.com\/[^/]+\/(.+)/
|
|
132
143
|
);
|
|
133
|
-
if (pathStyleMatch)
|
|
144
|
+
if (pathStyleMatch) {
|
|
145
|
+
return pathStyleMatch[1];
|
|
146
|
+
}
|
|
134
147
|
|
|
135
|
-
if (!url.startsWith('http'))
|
|
148
|
+
if (!url.startsWith('http')) {
|
|
149
|
+
return url;
|
|
150
|
+
}
|
|
136
151
|
|
|
137
152
|
return null;
|
|
138
153
|
}
|
|
@@ -142,7 +157,7 @@ export async function generateSignedUrl(
|
|
|
142
157
|
expiresIn: number = 3600,
|
|
143
158
|
options?: { filename?: string; contentType?: string }
|
|
144
159
|
): Promise<string> {
|
|
145
|
-
const commandParams:
|
|
160
|
+
const commandParams: GetObjectCommandInput = {
|
|
146
161
|
Bucket: getBucketName(),
|
|
147
162
|
Key: key
|
|
148
163
|
};
|
|
@@ -156,7 +171,7 @@ export async function generateSignedUrl(
|
|
|
156
171
|
commandParams.ResponseContentType = options.contentType;
|
|
157
172
|
}
|
|
158
173
|
|
|
159
|
-
const command = new GetObjectCommand(commandParams
|
|
174
|
+
const command = new GetObjectCommand(commandParams);
|
|
160
175
|
return getSignedUrl(getS3Client(), command, { expiresIn });
|
|
161
176
|
}
|
|
162
177
|
|
|
@@ -10,7 +10,15 @@ export default [
|
|
|
10
10
|
},
|
|
11
11
|
rules: {
|
|
12
12
|
"@stylistic/indent": ["error", 4],
|
|
13
|
-
"@stylistic/semi": ["error", "always"]
|
|
13
|
+
"@stylistic/semi": ["error", "always"],
|
|
14
|
+
"@stylistic/max-len": ["error", { "code": 80 }],
|
|
15
|
+
// Underscore prefix = intentionally unused (e.g. the
|
|
16
|
+
// mandatory 4th arg of an Express error handler).
|
|
17
|
+
"@typescript-eslint/no-unused-vars": ["error", {
|
|
18
|
+
"argsIgnorePattern": "^_",
|
|
19
|
+
"varsIgnorePattern": "^_"
|
|
20
|
+
}],
|
|
21
|
+
"curly": ["error", "all"]
|
|
14
22
|
}
|
|
15
23
|
}
|
|
16
24
|
];
|
|
@@ -41,8 +41,10 @@ export async function up(knex: Knex): Promise<void> {
|
|
|
41
41
|
|
|
42
42
|
CREATE INDEX idx_users_email ON users(email);
|
|
43
43
|
CREATE INDEX idx_user_in_accounts_user ON user_in_accounts(user_id);
|
|
44
|
-
CREATE INDEX idx_user_in_accounts_account
|
|
45
|
-
|
|
44
|
+
CREATE INDEX idx_user_in_accounts_account
|
|
45
|
+
ON user_in_accounts(account_id);
|
|
46
|
+
CREATE INDEX idx_password_reset_tokens_token
|
|
47
|
+
ON password_reset_tokens(token);
|
|
46
48
|
`);
|
|
47
49
|
}
|
|
48
50
|
|