umpordez 1.5.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.claude/settings.local.json +14 -1
  2. package/create.mjs +3 -3
  3. package/package.json +5 -5
  4. package/template/.github/copilot-instructions.md +1 -1
  5. package/template/AGENTS.md +1 -1
  6. package/template/mobile/{{PROJECT_SLUG}}/README.md +17 -2
  7. package/template/mobile/{{PROJECT_SLUG}}/package.json +23 -25
  8. package/template/server/apps/api/app.ts +1 -1
  9. package/template/server/apps/api/routes/account.ts +32 -10
  10. package/template/server/apps/api/routes/admin.ts +13 -6
  11. package/template/server/apps/api/routes/auth.ts +109 -29
  12. package/template/server/apps/api/routes/files.ts +14 -5
  13. package/template/server/apps/api/routes/health.ts +2 -2
  14. package/template/server/apps/api/routes/user.ts +2 -1
  15. package/template/server/apps/shared/middlewares/request-logger.ts +3 -1
  16. package/template/server/apps/shared/utils.ts +2 -2
  17. package/template/server/apps/site-api/app.ts +1 -1
  18. package/template/server/console/index.ts +1 -1
  19. package/template/server/console/tasks/seed-admin.ts +8 -2
  20. package/template/server/core/context.ts +2 -2
  21. package/template/server/core/email.ts +9 -2
  22. package/template/server/core/knexfile.ts +1 -1
  23. package/template/server/core/models/account.ts +43 -13
  24. package/template/server/core/models/auth.ts +10 -3
  25. package/template/server/core/models/user.ts +61 -27
  26. package/template/server/core/s3.ts +29 -14
  27. package/template/server/eslint.config.js +9 -1
  28. package/template/server/migrations/20260208000000_initial-schema.ts +4 -2
  29. package/template/server/package.json +27 -28
  30. package/template/server/types/api.ts +3 -0
  31. package/template/ui/admin/CLAUDE.md +5 -4
  32. package/template/ui/admin/eslint.config.js +29 -1
  33. package/template/ui/admin/package.json +31 -39
  34. package/template/ui/admin/src/components/nav-user.tsx +4 -2
  35. package/template/ui/admin/src/components/theme-provider.tsx +7 -1
  36. package/template/ui/admin/src/components/ui/badge.tsx +1 -1
  37. package/template/ui/admin/src/components/ui/button.tsx +1 -1
  38. package/template/ui/admin/src/components/ui/card.tsx +1 -1
  39. package/template/ui/admin/src/components/ui/checkbox.tsx +1 -1
  40. package/template/ui/admin/src/components/ui/date-picker.tsx +3 -3
  41. package/template/ui/admin/src/components/ui/dialog.tsx +1 -1
  42. package/template/ui/admin/src/components/ui/dropdown-menu.tsx +6 -6
  43. package/template/ui/admin/src/components/ui/input.tsx +1 -1
  44. package/template/ui/admin/src/components/ui/popover.tsx +1 -1
  45. package/template/ui/admin/src/components/ui/select.tsx +4 -4
  46. package/template/ui/admin/src/components/ui/separator.tsx +1 -1
  47. package/template/ui/admin/src/components/ui/sheet.tsx +1 -1
  48. package/template/ui/admin/src/components/ui/sidebar.tsx +24 -24
  49. package/template/ui/admin/src/components/ui/sonner.tsx +4 -4
  50. package/template/ui/admin/src/components/ui/switch.tsx +1 -1
  51. package/template/ui/admin/src/components/ui/table.tsx +3 -3
  52. package/template/ui/admin/src/components/ui/tabs.tsx +2 -2
  53. package/template/ui/admin/src/components/ui/textarea.tsx +1 -1
  54. package/template/ui/admin/src/hooks/queries/use-accounts.ts +29 -9
  55. package/template/ui/admin/src/hooks/queries/use-auth.ts +20 -6
  56. package/template/ui/admin/src/hooks/queries/use-dashboard.ts +13 -5
  57. package/template/ui/admin/src/hooks/queries/use-members.ts +32 -10
  58. package/template/ui/admin/src/hooks/queries/use-users.ts +32 -10
  59. package/template/ui/admin/src/hooks/use-confirm.tsx +1 -1
  60. package/template/ui/admin/src/hooks/use-user.tsx +4 -1
  61. package/template/ui/admin/src/index.css +68 -6
  62. package/template/ui/admin/src/layouts/account.tsx +19 -6
  63. package/template/ui/admin/src/layouts/user.tsx +3 -1
  64. package/template/ui/admin/src/lib/fetch-api.ts +38 -14
  65. package/template/ui/admin/src/lib/query-keys.ts +2 -1
  66. package/template/ui/admin/src/pages/account/dashboard.tsx +10 -3
  67. package/template/ui/admin/src/pages/account/members.tsx +22 -12
  68. package/template/ui/admin/src/pages/account/settings.tsx +22 -11
  69. package/template/ui/admin/src/pages/admin/accounts.tsx +2 -3
  70. package/template/ui/admin/src/pages/admin/dashboard.tsx +10 -3
  71. package/template/ui/admin/src/pages/admin/users.tsx +19 -10
  72. package/template/ui/admin/src/pages/public/error.tsx +4 -1
  73. package/template/ui/admin/src/pages/public/forgot-password.tsx +2 -2
  74. package/template/ui/admin/src/pages/public/login.tsx +13 -3
  75. package/template/ui/admin/src/pages/public/reset-password.tsx +5 -3
  76. package/template/ui/admin/src/pages/public/signup.tsx +11 -3
  77. package/template/ui/admin/src/pages/user/profile.tsx +16 -11
  78. package/template/ui/admin/src/pages/user/select-account.tsx +3 -1
  79. package/template/ui/admin/src/vite-env.d.ts +1 -0
  80. package/template/ui/admin/tsconfig.json +0 -1
  81. package/template/ui/admin/vite.config.ts +2 -1
  82. package/template/ui/site/CLAUDE.md +1 -2
  83. package/template/ui/site/_variants/app/home.ejs +6 -6
  84. package/template/ui/site/app.ts +1 -1
  85. package/template/ui/site/eslint.config.js +9 -1
  86. package/template/ui/site/i18n/index.ts +0 -1
  87. package/template/ui/site/package.json +17 -17
  88. package/template/ui/site/styles/input.css +11 -3
  89. package/template/ui/site/views/pages/home.ejs +4 -4
  90. package/template/ui/site/views/partials/header.ejs +1 -1
  91. package/template-variables.mjs +1 -1
  92. package/template/ui/admin/postcss.config.js +0 -6
  93. package/template/ui/admin/tailwind.config.ts +0 -71
  94. package/template/ui/site/tailwind.config.js +0 -16
  95. /package/template/mobile/{{PROJECT_SLUG}}/{eslint.config.js → eslint.config.mjs} +0 -0
@@ -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} : http=${res.statusCode} ${Date.now() - started}ms`;
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.errors.map((e) => e.message).join(', ');
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.errors.map((e) => ({
23
+ errors: error.issues.map((e) => ({
24
24
  field: e.path.join('.'),
25
25
  message: e.message
26
26
  }))
@@ -1,5 +1,5 @@
1
1
  import dotenv from 'dotenv';
2
- dotenv.config();
2
+ dotenv.config({ quiet: true });
3
3
 
4
4
  import express from 'express';
5
5
  import cors from 'cors';
@@ -1,5 +1,5 @@
1
1
  import dotenv from 'dotenv';
2
- dotenv.config();
2
+ dotenv.config({ quiet: true });
3
3
 
4
4
  import { parseArgs, runTask, listTasks, type ConsoleTask } from './runner.js';
5
5
 
@@ -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('Usage: npm run console -- --task=seed-admin --email=admin@example.com --password=yourpassword [--name=Admin]');
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 ? error.message : String(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 = (this as any)[key];
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 = (this as any)[key];
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(templateName: string, variables: Record<string, unknown>): string {
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 !!(process.env.EMAIL_HOST && process.env.EMAIL_USER && process.env.EMAIL_PASSWORD);
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 { NotFoundError, ForbiddenError, ConflictError, ValidationError } from '#core/errors.js';
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(id: string, data: { name?: string; settings?: Record<string, unknown> }) {
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) updateData.name = data.name;
83
- if (data.settings) updateData.settings = JSON.stringify(data.settings);
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('Apenas proprietários podem excluir contas');
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().count('ua.user_id as count');
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) para novos usuários'
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('Nome é obrigatório para novos usuários');
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(`{account} added member user_id=${user.id} to account_id=${accountId} role=${data.role}`);
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(`{account} updated member role user_id=${userId} account_id=${accountId} role=${role}`);
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('Não é possível remover o proprietário da conta');
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(`{account} removed member user_id=${userId} from account_id=${accountId}`);
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').count('id as count');
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(`{auth} signup existing user=${email}, new account=${account.name}`);
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 || 'http://localhost:{{ADMIN_UI_PORT}}';
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(currentPassword, user.password_hash);
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(`{user} created user=${user.email} role=${user.role} account_id=${account.id}`);
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').where({ id: userId }).update({ avatar_key: avatarKey });
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: any) => u.id);
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: Record<string, any[]> = {};
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((u: any) => ({
233
- ...u,
234
- accounts: accountsByUser[u.id] || []
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: { name?: string; email?: string; role?: string; password?: string }
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) updateData.name = data.name;
254
- if (data.role) updateData.role = data.role;
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) updateData.name = 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) return this.findById(userId);
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((account: any) => ({
350
- account,
351
- role: 'admin'
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((row: any) => ({
361
- account: {
362
- id: row.id,
363
- name: row.name,
364
- status: row.status
365
- },
366
- role: row.role
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) return null;
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) return null;
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 AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.'
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: (_req: any, file: any, cb: any) => {
76
+ metadata: (req, file, cb) => {
74
77
  cb(null, {
75
78
  fieldName: file.fieldname,
76
79
  originalName: file.originalname,
77
- uploadedBy: (_req as any).user?.id || 'unknown',
80
+ uploadedBy: req.user?.id || 'unknown',
78
81
  uploadedAt: new Date().toISOString()
79
82
  });
80
83
  },
81
- key: (_req: any, file: any, cb: any) => {
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
- (storageOptions as any).acl = 'public-read';
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(`Tipo de arquivo não permitido: ${file.mimetype}`));
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) return 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) return null;
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) return virtualHostedMatch[1];
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) return pathStyleMatch[1];
144
+ if (pathStyleMatch) {
145
+ return pathStyleMatch[1];
146
+ }
134
147
 
135
- if (!url.startsWith('http')) return url;
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: Record<string, unknown> = {
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 as any);
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 ON user_in_accounts(account_id);
45
- CREATE INDEX idx_password_reset_tokens_token ON password_reset_tokens(token);
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