@adonis-agora/authkit-server 0.44.0 → 0.45.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.
@@ -249,19 +249,36 @@ export declare function resolveDeviceFlow(input?: DeviceFlowConfigInput): Resolv
249
249
  */
250
250
  export interface UploadsConfigInput {
251
251
  avatars?: {
252
- /** Disk do `@adonisjs/drive` a usar. Default: o disk DEFAULT do app. */
252
+ /**
253
+ * Backend de storage do avatar. Default: `'auto'` (media se
254
+ * `@adonis-agora/media` estiver presente/configurado, senão o drive builtin).
255
+ * `'builtin'` força o `@adonisjs/drive`; `'media'` força o media (degrada para
256
+ * o input de URL se indisponível).
257
+ */
258
+ storage?: "auto" | "builtin" | "media";
259
+ /** Disk do `@adonisjs/drive` a usar (backend builtin). Default: o disk DEFAULT do app. */
253
260
  disk?: string;
254
- /** Diretório/prefixo das chaves. Default: 'authkit/avatars'. */
261
+ /** Diretório/prefixo das chaves (backend builtin). Default: 'authkit/avatars'. */
255
262
  directory?: string;
263
+ /** Collection single-file do media (backend media). Default: 'avatar'. */
264
+ collection?: string;
265
+ /** ownerType do media (backend media). Default: 'AuthAccount'. */
266
+ ownerType?: string;
256
267
  /** Tamanho máximo em MB. Default: 5. */
257
268
  maxSizeMb?: number;
258
269
  };
259
270
  }
260
271
  export interface ResolvedUploadsConfig {
261
272
  avatars: {
262
- /** Disk explícito; `undefined` = disk DEFAULT do app. */
273
+ /** Backend ativo (default 'auto'). */
274
+ storage: "auto" | "builtin" | "media";
275
+ /** Disk explícito; `undefined` = disk DEFAULT do app (backend builtin). */
263
276
  disk?: string;
264
277
  directory: string;
278
+ /** Collection single-file do media (backend media). */
279
+ collection: string;
280
+ /** ownerType do media (backend media). */
281
+ ownerType: string;
265
282
  maxSizeMb: number;
266
283
  };
267
284
  }
@@ -67,8 +67,11 @@ export function resolveDeviceFlow(input) {
67
67
  export function resolveUploads(input) {
68
68
  return {
69
69
  avatars: {
70
+ storage: input?.avatars?.storage ?? "auto",
70
71
  disk: input?.avatars?.disk,
71
72
  directory: input?.avatars?.directory ?? "authkit/avatars",
73
+ collection: input?.avatars?.collection ?? "avatar",
74
+ ownerType: input?.avatars?.ownerType ?? "AuthAccount",
72
75
  maxSizeMb: input?.avatars?.maxSizeMb ?? 5,
73
76
  },
74
77
  };
@@ -40,7 +40,7 @@ import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '.
40
40
  import { dispatchSecurityNotice } from '../security_notice_service.js';
41
41
  import { requireSudo, isSudoActive, SUDO_MODE_DEFAULTS, resolveEffectiveSudoMode } from '../sudo_mode.js';
42
42
  import { translate } from '../i18n.js';
43
- import { storeAvatar, isDriveAvailable, AvatarUploadError } from '../avatar_storage.js';
43
+ import { storeAvatar, isAvatarUploadSupported, AvatarUploadError } from '../avatar_storage.js';
44
44
  import { sendEmailChangeConfirmationEmail, sendEmailChangeNoticeEmail, } from '../default_mailer.js';
45
45
  import { ACTIVE_ORG_COOKIE } from '../active_org_cookie.js';
46
46
  // ---------------------------------------------------------------------------
@@ -138,7 +138,7 @@ export default class AccountApiController {
138
138
  passkeysSupported: supportsPasskeys(cfg.accountStore),
139
139
  orgsSupported: supportsOrganizations(cfg.accountStore),
140
140
  tokensSupported: !!cfg.patStore,
141
- avatarUploadSupported: await isDriveAvailable(),
141
+ avatarUploadSupported: await isAvatarUploadSupported(cfg.uploads),
142
142
  sessionsSupported: new AdminSessionsService(service).canList,
143
143
  },
144
144
  };
@@ -61,8 +61,8 @@ export declare function removeFromOrgs(cfg: ResolvedServerConfig, accountId: str
61
61
  orgMemberships: number;
62
62
  orgInvitations: number;
63
63
  }>;
64
- /** 7) Apaga o avatar no drive (best-effort, fail-safe). */
65
- export declare function deleteAccountAvatar(cfg: ResolvedServerConfig, avatarUrl: string | null): Promise<{
64
+ /** 7) Apaga o avatar no backend ativo (drive OU media; best-effort, fail-safe). */
65
+ export declare function deleteAccountAvatar(cfg: ResolvedServerConfig, accountId: string, avatarUrl: string | null): Promise<{
66
66
  avatarDeleted: boolean;
67
67
  }>;
68
68
  /** 8) Anonimiza o histórico de audit da conta (quando o sink suporta). */
@@ -92,9 +92,9 @@ export async function removeFromOrgs(cfg, accountId) {
92
92
  orgInvitations: orgResult.invitations,
93
93
  };
94
94
  }
95
- /** 7) Apaga o avatar no drive (best-effort, fail-safe). */
96
- export async function deleteAccountAvatar(cfg, avatarUrl) {
97
- const avatarDeleted = await deleteAvatar(cfg.uploads, avatarUrl);
95
+ /** 7) Apaga o avatar no backend ativo (drive OU media; best-effort, fail-safe). */
96
+ export async function deleteAccountAvatar(cfg, accountId, avatarUrl) {
97
+ const avatarDeleted = await deleteAvatar(cfg.uploads, accountId, avatarUrl);
98
98
  return { avatarDeleted };
99
99
  }
100
100
  /** 8) Anonimiza o histórico de audit da conta (quando o sink suporta). */
@@ -114,7 +114,7 @@ export class AccountDeletionService {
114
114
  }
115
115
  // 7) Avatar no drive (best-effort, fail-safe).
116
116
  try {
117
- result.avatarDeleted = (await deleteAccountAvatar(cfg, snapshot.avatarUrl)).avatarDeleted;
117
+ result.avatarDeleted = (await deleteAccountAvatar(cfg, accountId, snapshot.avatarUrl)).avatarDeleted;
118
118
  }
119
119
  catch {
120
120
  /* best-effort */
@@ -20,6 +20,18 @@ type DriveService = any;
20
20
  * @internal
21
21
  */
22
22
  export declare function __setDriveLoaderForTests(fn: (() => Promise<DriveService | null>) | undefined): void;
23
+ /**
24
+ * Módulo `@adonis-agora/media/single-file` resolvido de forma preguiçosa. Tipado
25
+ * como `any` de propósito: a lib NÃO depende do media em tempo de compilação
26
+ * (peer/opt-in). Expõe `storeSingleFile`/`removeSingleFile`/`isSingleFileStoreAvailable`.
27
+ */
28
+ type MediaModule = any;
29
+ /**
30
+ * Permite reapontar/limpar o loader do media (usado em testes). Espelha
31
+ * {@link __setDriveLoaderForTests}.
32
+ * @internal
33
+ */
34
+ export declare function __setMediaLoaderForTests(fn: (() => Promise<MediaModule | null>) | undefined): void;
23
35
  /** Erro de validação do upload (mensagem já localizada no controller). */
24
36
  export declare class AvatarUploadError extends Error {
25
37
  reason: 'extname' | 'size';
@@ -29,7 +41,9 @@ export declare class AvatarUploadError extends Error {
29
41
  export interface UploadedAvatar {
30
42
  extname?: string | null;
31
43
  size?: number;
32
- /** Caminho temporário (drive daqui para stream). */
44
+ /** MIME reportado pelo multipart (ex.: 'image/png'). Usado pelo backend media. */
45
+ type?: string | null;
46
+ /** Caminho temporário (drive lê daqui para stream; media lê os bytes daqui). */
33
47
  tmpPath?: string | null;
34
48
  /** Move o arquivo para um disk do drive (API v3+). */
35
49
  moveToDisk?: (key: string, options?: {
@@ -42,27 +56,39 @@ export interface UploadedAvatar {
42
56
  */
43
57
  export declare function isDriveAvailable(): Promise<boolean>;
44
58
  /**
45
- * Armazena o avatar no drive do host e retorna a URL pública.
59
+ * Indica se o upload de avatar está disponível para a config dada i.e. se ALGUM
60
+ * backend configurado consegue armazenar (mesma lógica de seleção do
61
+ * {@link resolveUploader}: `'builtin'` → drive; `'media'` → media; `'auto'` →
62
+ * media OU drive). Usado pelas views/controllers para decidir mostrar o input de
63
+ * arquivo. Best-effort: nunca lança.
64
+ *
65
+ * Substitui o antigo gate por {@link isDriveAvailable}, que escondia o input num
66
+ * host media-only (media presente, drive ausente) mesmo com o media capaz de armazenar.
67
+ */
68
+ export declare function isAvatarUploadSupported(cfg: ResolvedUploadsConfig): Promise<boolean>;
69
+ /**
70
+ * Armazena o avatar no backend ativo (drive OU media) e retorna a URL pública.
46
71
  *
47
- * - Valida extensão (jpg/jpeg/png/webp) e tamanho (≤ maxSizeMb) lança
72
+ * - Resolve o backend PRIMEIRO: se nenhum estiver disponível → retorna `null`
73
+ * (degrada para o input de URL, feature off) SEM validar/lançar. Isso preserva o
74
+ * comportamento histórico: um host sem backend nunca vê erro de validação.
75
+ * - Só quando há backend valida extensão (jpg/jpeg/png/webp) e tamanho
76
+ * (≤ maxSizeMb) — COMPARTILHADO, antes de entregar ao backend; lança
48
77
  * {@link AvatarUploadError} se inválido (o controller traduz/flasha).
49
- * - Usa o disk configurado em `uploads.avatars.disk` ou o disk DEFAULT do app.
50
- * - Se o drive estiver ausente/não-configurado → retorna `null` (degrada para URL).
78
+ * - Backend conforme `cfg.avatars.storage` (ver {@link resolveUploader}).
51
79
  *
52
- * Nunca lança por causa de drive ausente; só lança em validação.
80
+ * Nunca lança por causa de backend ausente; só lança em validação (com backend).
53
81
  */
54
- export declare function storeAvatar(_ctx: HttpContext, cfg: ResolvedUploadsConfig, file: UploadedAvatar, accountId: string, messages: {
82
+ export declare function storeAvatar(ctx: HttpContext, cfg: ResolvedUploadsConfig, file: UploadedAvatar, accountId: string, messages: {
55
83
  extname: string;
56
84
  size: string;
57
85
  }): Promise<string | null>;
58
86
  /**
59
- * Deleta (best-effort, fail-safe) o avatar de uma conta no drive do host, a partir
60
- * da URL/key armazenada. Usado pela deleção de conta (LGPD): apaga o arquivo se
61
- * conseguirmos derivar a key dentro do diretório configurado.
87
+ * Deleta (best-effort, fail-safe) o avatar de uma conta no backend ativo. Usado
88
+ * pela deleção de conta (LGPD). O `accountId` é o owner — necessário para o backend
89
+ * media apagar por owner; o backend builtin deriva a key da `storedUrlOrKey`.
62
90
  *
63
- * NUNCA lança: drive ausente, key não reconhecida ou erro de I/O → no-op. A key é
64
- * derivada da `storedUrlOrKey` pegando o trecho a partir de `directory/` (cobre
65
- * tanto uma key relativa quanto uma URL pública que contenha o caminho).
91
+ * NUNCA lança: backend ausente, key não reconhecida ou erro de I/O → no-op.
66
92
  */
67
- export declare function deleteAvatar(cfg: ResolvedUploadsConfig, storedUrlOrKey: string | null | undefined): Promise<boolean>;
93
+ export declare function deleteAvatar(cfg: ResolvedUploadsConfig, accountId: string | null | undefined, storedUrlOrKey: string | null | undefined): Promise<boolean>;
68
94
  export {};
@@ -34,8 +34,43 @@ export function __setDriveLoaderForTests(fn) {
34
34
  driveServicePromise = undefined;
35
35
  }
36
36
  }
37
+ let mediaModulePromise;
38
+ /**
39
+ * Importa o helper single-file do `@adonis-agora/media` de forma preguiçosa e
40
+ * fail-safe. Espelha {@link loadDrive}: se o pacote não estiver instalado, resolve
41
+ * `null` (o specifier é indireto para não ser resolvido em build-time — peer opcional).
42
+ */
43
+ async function loadMedia() {
44
+ if (!mediaModulePromise) {
45
+ const specifier = '@adonis-agora/media/single-file';
46
+ mediaModulePromise = import(__rewriteRelativeImportExtension(specifier))
47
+ .then((mod) => mod ?? null)
48
+ .catch(() => null);
49
+ }
50
+ return mediaModulePromise;
51
+ }
52
+ /**
53
+ * Permite reapontar/limpar o loader do media (usado em testes). Espelha
54
+ * {@link __setDriveLoaderForTests}.
55
+ * @internal
56
+ */
57
+ export function __setMediaLoaderForTests(fn) {
58
+ if (fn) {
59
+ mediaModulePromise = fn();
60
+ }
61
+ else {
62
+ mediaModulePromise = undefined;
63
+ }
64
+ }
37
65
  /** Extensões aceitas para o avatar (imagem raster comum). */
38
66
  const ALLOWED_EXTNAMES = ['jpg', 'jpeg', 'png', 'webp'];
67
+ /** MIME por extensão validada — fallback quando o file de multipart não traz `type`. */
68
+ const EXT_MIME = {
69
+ jpg: 'image/jpeg',
70
+ jpeg: 'image/jpeg',
71
+ png: 'image/png',
72
+ webp: 'image/webp',
73
+ };
39
74
  /** Erro de validação do upload (mensagem já localizada no controller). */
40
75
  export class AvatarUploadError extends Error {
41
76
  reason;
@@ -76,91 +111,205 @@ function buildKey(cfg, accountId, ext) {
76
111
  return `${cfg.avatars.directory}/${accountId}-${random}.${ext}`;
77
112
  }
78
113
  /**
79
- * Armazena o avatar no drive do host e retorna a URL pública.
114
+ * Backend builtin: o `@adonisjs/drive` configurado no app. Comportamento
115
+ * BYTE-IDÊNTICO ao histórico (moveToDisk/putStream + getUrl; delete por key
116
+ * derivada do diretório).
117
+ */
118
+ const builtinUploader = {
119
+ async store(_ctx, cfg, file, accountId, ext) {
120
+ const drive = await loadDrive();
121
+ if (!drive)
122
+ return null;
123
+ // Resolve o disk: o configurado, ou o DEFAULT do drive do app.
124
+ let disk;
125
+ try {
126
+ disk = cfg.avatars.disk ? drive.use(cfg.avatars.disk) : drive.use();
127
+ }
128
+ catch {
129
+ // disk inválido/não-configurado — degrada para URL.
130
+ return null;
131
+ }
132
+ if (!disk)
133
+ return null;
134
+ const key = buildKey(cfg, accountId || 'account', ext);
135
+ // API @adonisjs/drive v3+: o file de multipart move-se direto para o disk.
136
+ // `moveToDisk` lê do tmpPath e usa o disk informado (ou o default da config).
137
+ if (typeof file.moveToDisk === 'function') {
138
+ await file.moveToDisk(key, cfg.avatars.disk ? { disk: cfg.avatars.disk } : undefined);
139
+ }
140
+ else if (file.tmpPath) {
141
+ // Fallback: lê do tmpPath e escreve via putStream no disk resolvido.
142
+ const fs = await import('node:fs');
143
+ await disk.putStream(key, fs.createReadStream(file.tmpPath));
144
+ }
145
+ else {
146
+ return null;
147
+ }
148
+ try {
149
+ return await disk.getUrl(key);
150
+ }
151
+ catch {
152
+ // disk sem getUrl público — retorna a key como referência relativa.
153
+ return key;
154
+ }
155
+ },
156
+ async delete(cfg, _accountId, storedUrlOrKey) {
157
+ if (!storedUrlOrKey)
158
+ return false;
159
+ const drive = await loadDrive();
160
+ if (!drive)
161
+ return false;
162
+ // Deriva a key: trecho a partir de `<directory>/`. Se não bater, aborta (não
163
+ // arriscamos deletar algo fora do nosso diretório).
164
+ const dir = cfg.avatars.directory.replace(/\/+$/, '');
165
+ const marker = `${dir}/`;
166
+ const idx = storedUrlOrKey.indexOf(marker);
167
+ if (idx < 0)
168
+ return false;
169
+ // Remove querystring/fragment de uma URL pública.
170
+ const key = storedUrlOrKey.slice(idx).split(/[?#]/)[0];
171
+ let disk;
172
+ try {
173
+ disk = cfg.avatars.disk ? drive.use(cfg.avatars.disk) : drive.use();
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ if (!disk || typeof disk.delete !== 'function')
179
+ return false;
180
+ try {
181
+ await disk.delete(key);
182
+ return true;
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ },
188
+ };
189
+ /**
190
+ * Backend media: delega ao `@adonis-agora/media` (collection single-file). O
191
+ * lifecycle é keyed por owner (`ownerType`/`ownerId`) — o `single: true` da
192
+ * collection faz o replace de slot; a URL final é o que persistimos em `avatarUrl`.
193
+ */
194
+ function makeMediaUploader(media) {
195
+ return {
196
+ async store(_ctx, cfg, file, accountId, ext) {
197
+ if (!file.tmpPath)
198
+ return null;
199
+ const fs = await import('node:fs/promises');
200
+ const contents = await fs.readFile(file.tmpPath);
201
+ const fileName = `avatar.${ext}`;
202
+ const mimeType = file.type ?? EXT_MIME[ext] ?? 'application/octet-stream';
203
+ const result = await media.storeSingleFile({
204
+ ownerType: cfg.avatars.ownerType,
205
+ ownerId: accountId,
206
+ collection: cfg.avatars.collection,
207
+ fileName,
208
+ mimeType,
209
+ contents,
210
+ });
211
+ return result?.url ?? null;
212
+ },
213
+ async delete(cfg, accountId, _storedUrlOrKey) {
214
+ // media apaga por owner (a key/URL não é usada — o lifecycle é do owner).
215
+ if (!accountId)
216
+ return false;
217
+ try {
218
+ await media.removeSingleFile({
219
+ ownerType: cfg.avatars.ownerType,
220
+ ownerId: accountId,
221
+ collection: cfg.avatars.collection,
222
+ });
223
+ return true;
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ },
229
+ };
230
+ }
231
+ /**
232
+ * Resolve o backend de avatar conforme `cfg.avatars.storage`:
233
+ * - `'builtin'` → sempre o drive (ou `null` se ausente).
234
+ * - `'media'` → o media (ou `null` se ausente/indisponível — degrada gracioso).
235
+ * - `'auto'` → media se disponível, senão o drive.
80
236
  *
81
- * - Valida extensão (jpg/jpeg/png/webp) e tamanho (≤ maxSizeMb) lança
82
- * {@link AvatarUploadError} se inválido (o controller traduz/flasha).
83
- * - Usa o disk configurado em `uploads.avatars.disk` ou o disk DEFAULT do app.
84
- * - Se o drive estiver ausente/não-configurado → retorna `null` (degrada para URL).
237
+ * "media disponível" = pacote presente E `isSingleFileStoreAvailable()` true (o
238
+ * MediaManager está bindado no container do app). Nunca lança.
239
+ */
240
+ async function resolveUploader(cfg) {
241
+ const storage = cfg.avatars.storage;
242
+ if (storage === 'builtin') {
243
+ return (await loadDrive()) ? builtinUploader : null;
244
+ }
245
+ if (storage === 'media') {
246
+ const media = await loadMediaIfUsable();
247
+ return media ? makeMediaUploader(media) : null;
248
+ }
249
+ // 'auto' (default): media se disponível, senão builtin.
250
+ const media = await loadMediaIfUsable();
251
+ if (media)
252
+ return makeMediaUploader(media);
253
+ return (await loadDrive()) ? builtinUploader : null;
254
+ }
255
+ /**
256
+ * Indica se o upload de avatar está disponível para a config dada — i.e. se ALGUM
257
+ * backend configurado consegue armazenar (mesma lógica de seleção do
258
+ * {@link resolveUploader}: `'builtin'` → drive; `'media'` → media; `'auto'` →
259
+ * media OU drive). Usado pelas views/controllers para decidir mostrar o input de
260
+ * arquivo. Best-effort: nunca lança.
85
261
  *
86
- * Nunca lança por causa de drive ausente; lança em validação.
262
+ * Substitui o antigo gate por {@link isDriveAvailable}, que escondia o input num
263
+ * host media-only (media presente, drive ausente) mesmo com o media capaz de armazenar.
87
264
  */
88
- export async function storeAvatar(_ctx, cfg, file, accountId, messages) {
89
- const drive = await loadDrive();
90
- if (!drive)
265
+ export async function isAvatarUploadSupported(cfg) {
266
+ return (await resolveUploader(cfg)) !== null;
267
+ }
268
+ /**
269
+ * Carrega o módulo media só se ele estiver USÁVEL: pacote presente E
270
+ * `isSingleFileStoreAvailable()` resolve `true`. Best-effort — qualquer erro → null.
271
+ */
272
+ async function loadMediaIfUsable() {
273
+ const media = await loadMedia();
274
+ if (!media)
91
275
  return null;
92
- const ext = validate(file, cfg, messages);
93
- // Resolve o disk: o configurado, ou o DEFAULT do drive do app.
94
- let disk;
95
276
  try {
96
- disk = cfg.avatars.disk ? drive.use(cfg.avatars.disk) : drive.use();
277
+ return (await media.isSingleFileStoreAvailable()) ? media : null;
97
278
  }
98
279
  catch {
99
- // disk inválido/não-configurado — degrada para URL.
100
280
  return null;
101
281
  }
102
- if (!disk)
103
- return null;
104
- const key = buildKey(cfg, accountId || 'account', ext);
105
- // API @adonisjs/drive v3+: o file de multipart move-se direto para o disk.
106
- // `moveToDisk` do tmpPath e usa o disk informado (ou o default da config).
107
- if (typeof file.moveToDisk === 'function') {
108
- await file.moveToDisk(key, cfg.avatars.disk ? { disk: cfg.avatars.disk } : undefined);
109
- }
110
- else if (file.tmpPath) {
111
- // Fallback: do tmpPath e escreve via putStream no disk resolvido.
112
- const fs = await import('node:fs');
113
- await disk.putStream(key, fs.createReadStream(file.tmpPath));
114
- }
115
- else {
282
+ }
283
+ /**
284
+ * Armazena o avatar no backend ativo (drive OU media) e retorna a URL pública.
285
+ *
286
+ * - Resolve o backend PRIMEIRO: se nenhum estiver disponível retorna `null`
287
+ * (degrada para o input de URL, feature off) SEM validar/lançar. Isso preserva o
288
+ * comportamento histórico: um host sem backend nunca erro de validação.
289
+ * - Só quando há backend valida extensão (jpg/jpeg/png/webp) e tamanho
290
+ * (≤ maxSizeMb) — COMPARTILHADO, antes de entregar ao backend; lança
291
+ * {@link AvatarUploadError} se inválido (o controller traduz/flasha).
292
+ * - Backend conforme `cfg.avatars.storage` (ver {@link resolveUploader}).
293
+ *
294
+ * Nunca lança por causa de backend ausente; só lança em validação (com backend).
295
+ */
296
+ export async function storeAvatar(ctx, cfg, file, accountId, messages) {
297
+ const uploader = await resolveUploader(cfg);
298
+ if (!uploader)
116
299
  return null;
117
- }
118
- try {
119
- return await disk.getUrl(key);
120
- }
121
- catch {
122
- // disk sem getUrl público — retorna a key como referência relativa.
123
- return key;
124
- }
300
+ const ext = validate(file, cfg, messages);
301
+ return uploader.store(ctx, cfg, file, accountId, ext);
125
302
  }
126
303
  /**
127
- * Deleta (best-effort, fail-safe) o avatar de uma conta no drive do host, a partir
128
- * da URL/key armazenada. Usado pela deleção de conta (LGPD): apaga o arquivo se
129
- * conseguirmos derivar a key dentro do diretório configurado.
304
+ * Deleta (best-effort, fail-safe) o avatar de uma conta no backend ativo. Usado
305
+ * pela deleção de conta (LGPD). O `accountId` é o owner — necessário para o backend
306
+ * media apagar por owner; o backend builtin deriva a key da `storedUrlOrKey`.
130
307
  *
131
- * NUNCA lança: drive ausente, key não reconhecida ou erro de I/O → no-op. A key é
132
- * derivada da `storedUrlOrKey` pegando o trecho a partir de `directory/` (cobre
133
- * tanto uma key relativa quanto uma URL pública que contenha o caminho).
308
+ * NUNCA lança: backend ausente, key não reconhecida ou erro de I/O → no-op.
134
309
  */
135
- export async function deleteAvatar(cfg, storedUrlOrKey) {
136
- if (!storedUrlOrKey)
137
- return false;
138
- const drive = await loadDrive();
139
- if (!drive)
140
- return false;
141
- // Deriva a key: trecho a partir de `<directory>/`. Se não bater, aborta (não
142
- // arriscamos deletar algo fora do nosso diretório).
143
- const dir = cfg.avatars.directory.replace(/\/+$/, '');
144
- const marker = `${dir}/`;
145
- const idx = storedUrlOrKey.indexOf(marker);
146
- if (idx < 0)
147
- return false;
148
- // Remove querystring/fragment de uma URL pública.
149
- const key = storedUrlOrKey.slice(idx).split(/[?#]/)[0];
150
- let disk;
151
- try {
152
- disk = cfg.avatars.disk ? drive.use(cfg.avatars.disk) : drive.use();
153
- }
154
- catch {
310
+ export async function deleteAvatar(cfg, accountId, storedUrlOrKey) {
311
+ const uploader = await resolveUploader(cfg);
312
+ if (!uploader)
155
313
  return false;
156
- }
157
- if (!disk || typeof disk.delete !== 'function')
158
- return false;
159
- try {
160
- await disk.delete(key);
161
- return true;
162
- }
163
- catch {
164
- return false;
165
- }
314
+ return uploader.delete(cfg, accountId, storedUrlOrKey);
166
315
  }
@@ -3,7 +3,7 @@ import { ACCOUNT_SESSION_KEY } from "../middleware/account_auth.js";
3
3
  import { supportsAccountSecurity, supportsAccountDeletion, supportsProfile, supportsPasswordHistory, } from "../../accounts/account_store.js";
4
4
  import { changePasswordValidator, changeEmailValidator, deleteAccountValidator, updateProfileValidator, } from "../validators.js";
5
5
  import { sendEmailChangeConfirmationEmail, sendEmailChangeNoticeEmail, sendEmailChangedCompletedEmail, } from "../default_mailer.js";
6
- import { storeAvatar, isDriveAvailable, AvatarUploadError, } from "../avatar_storage.js";
6
+ import { storeAvatar, isAvatarUploadSupported, AvatarUploadError, } from "../avatar_storage.js";
7
7
  import { translate } from "../i18n.js";
8
8
  import { TRUSTED_DEVICE_COOKIE } from "../trusted_device.js";
9
9
  import { AccountDeletionService } from "../account_deletion_service.js";
@@ -67,8 +67,8 @@ export default class AccountSecurityController {
67
67
  csrfToken: ctx.request.csrfToken,
68
68
  supported: supportsAccountSecurity(cfg.accountStore),
69
69
  profileSupported: supportsProfile(cfg.accountStore),
70
- // Só mostramos o input de arquivo se o drive do app estiver disponível.
71
- avatarUploadSupported: await isDriveAvailable(),
70
+ // Só mostramos o input de arquivo se ALGUM backend (drive OU media) puder armazenar.
71
+ avatarUploadSupported: await isAvatarUploadSupported(cfg.uploads),
72
72
  email: account?.email ?? "",
73
73
  name: account?.name ?? "",
74
74
  avatarUrl: account?.avatarUrl ?? "",
@@ -75,7 +75,7 @@ export function defineAccountDeletionWorkflow(deps) {
75
75
  result.orgMemberships = orgResult.orgMemberships;
76
76
  result.orgInvitations = orgResult.orgInvitations;
77
77
  // 7) Avatar no drive.
78
- result.avatarDeleted = (await ctx.step("delete.avatar", async () => deleteAccountAvatar((await deps.oidc()).config, snapshot.avatarUrl))).avatarDeleted;
78
+ result.avatarDeleted = (await ctx.step("delete.avatar", async () => deleteAccountAvatar((await deps.oidc()).config, accountId, snapshot.avatarUrl))).avatarDeleted;
79
79
  // 8) Anonimiza o histórico de audit.
80
80
  result.auditAnonymized = (await ctx.step("anonymize.audit", async () => anonymizeAudit((await deps.oidc()).config, accountId))).auditAnonymized;
81
81
  // 9) Deleta a linha da conta (ÚLTIMA etapa, forward-only).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
@@ -56,6 +56,7 @@
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@adonis-agora/durable": "^0.2.0",
59
+ "@adonis-agora/media": "^0.1.0",
59
60
  "@adonis-agora/telescope": "^0.1.0",
60
61
  "@adonisjs/ally": "^6.3.0",
61
62
  "@adonisjs/auth": "^10.1.0",
@@ -73,6 +74,9 @@
73
74
  "@adonis-agora/durable": {
74
75
  "optional": true
75
76
  },
77
+ "@adonis-agora/media": {
78
+ "optional": true
79
+ },
76
80
  "@adonis-agora/telescope": {
77
81
  "optional": true
78
82
  },