@adonis-agora/authkit-server 0.44.0 → 0.46.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/build/commands/ui_preset.js +15 -1
- package/build/host/views/account/confirm.edge +118 -52
- package/build/host/views/account/mfa.edge +1 -1
- package/build/host/views/login.edge +2 -4
- package/build/host/views/mfa-challenge.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +5 -1
- package/build/index.js +15 -1
- package/build/src/define_config.d.ts +71 -3
- package/build/src/define_config.js +11 -0
- package/build/src/host/account_api/account_api_controller.js +2 -2
- package/build/src/host/account_deletion_ops.d.ts +2 -2
- package/build/src/host/account_deletion_ops.js +3 -3
- package/build/src/host/account_deletion_service.js +1 -1
- package/build/src/host/assets/webauthn.js +2 -0
- package/build/src/host/avatar_storage.d.ts +40 -14
- package/build/src/host/avatar_storage.js +221 -72
- package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
- package/build/src/host/controllers/account_confirm_controller.js +42 -130
- package/build/src/host/controllers/account_security_controller.js +3 -3
- package/build/src/host/controllers/account_session_controller.js +20 -4
- package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
- package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
- package/build/src/host/durable/account_deletion_workflow.js +1 -1
- package/build/src/host/i18n.d.ts +14 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/impersonation_session.js +15 -0
- package/build/src/host/rate_limit.d.ts +10 -0
- package/build/src/host/rate_limit.js +6 -0
- package/build/src/host/register_auth_host.d.ts +19 -0
- package/build/src/host/register_auth_host.js +72 -4
- package/build/src/host/sudo/index.d.ts +47 -0
- package/build/src/host/sudo/index.js +41 -0
- package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
- package/build/src/host/sudo/methods/magic_link.js +174 -0
- package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
- package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
- package/build/src/host/sudo/methods/passkey.d.ts +28 -0
- package/build/src/host/sudo/methods/passkey.js +139 -0
- package/build/src/host/sudo/methods/password.d.ts +19 -0
- package/build/src/host/sudo/methods/password.js +93 -0
- package/build/src/host/sudo/runtime.d.ts +141 -0
- package/build/src/host/sudo/runtime.js +327 -0
- package/build/src/host/sudo/types.d.ts +93 -0
- package/build/src/host/sudo/types.js +1 -0
- package/build/src/host/sudo_mode.d.ts +116 -6
- package/build/src/host/sudo_mode.js +133 -7
- package/package.json +10 -2
- package/build/stubs/ui/edge/views/consent.edge +0 -13
- package/build/stubs/ui/edge/views/login.edge +0 -19
- package/stubs/ui/edge/views/consent.edge +0 -13
- package/stubs/ui/edge/views/login.edge +0 -19
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function l(e){let t=new Uint8Array(e),r="";for(let n of t)r+=String.fromCharCode(n);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function h(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=(4-t.length%4)%4,i=t.padEnd(t.length+r,"="),n=atob(i),c=new ArrayBuffer(n.length),f=new Uint8Array(c);for(let a=0;a<n.length;a++)f[a]=n.charCodeAt(a);return c}function p(){return C.stubThis(globalThis?.PublicKeyCredential!==void 0&&typeof globalThis.PublicKeyCredential=="function")}var C={stubThis:e=>e};function R(e){let{id:t}=e;return{...e,id:h(t),transports:e.transports}}function w(e){return e==="localhost"||/^((xn--[a-z0-9-]+|[a-z0-9]+(-[a-z0-9]+)*)\.)+([a-z]{2,}|xn--[a-z0-9-]+)$/i.test(e)}var o=class extends Error{constructor({message:t,code:r,cause:i,name:n}){super(t,{cause:i}),Object.defineProperty(this,"code",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.name=n??i.name,this.code=r}};function I({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new o({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else if(e.name==="ConstraintError"){if(r.authenticatorSelection?.requireResidentKey===!0)return new o({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:e});if(t.mediation==="conditional"&&r.authenticatorSelection?.userVerification==="required")return new o({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:e});if(r.authenticatorSelection?.userVerification==="required")return new o({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:e})}else{if(e.name==="InvalidStateError")return new o({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:e});if(e.name==="NotAllowedError")return new o({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="NotSupportedError")return r.pubKeyCredParams.filter(n=>n.type==="public-key").length===0?new o({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:e}):new o({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:e});if(e.name==="SecurityError"){let i=globalThis.location.hostname;if(w(i)){if(r.rp.id!==i)return new o({message:`The RP ID "${r.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new o({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="TypeError"){if(r.user.id.byteLength<1||r.user.id.byteLength>64)return new o({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:e})}else if(e.name==="UnknownError")return new o({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}var S=class{constructor(){Object.defineProperty(this,"controller",{enumerable:!0,configurable:!0,writable:!0,value:void 0})}createNewAbortSignal(){if(this.controller){let r=new Error("Cancelling existing WebAuthn API call for new one");r.name="AbortError",this.controller.abort(r)}let t=new AbortController;return this.controller=t,t.signal}cancelCeremony(){if(this.controller){let t=new Error("Manually cancelling existing WebAuthn API call");t.name="AbortError",this.controller.abort(t),this.controller=void 0}}},A=new S;var x=["cross-platform","platform"];function E(e){if(e&&!(x.indexOf(e)<0))return e}async function N(e){!e.optionsJSON&&e.challenge&&(console.warn("startRegistration() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useAutoRegister:r=!1}=e;if(!p())throw new Error("WebAuthn is not supported in this browser");let i={...t,challenge:h(t.challenge),user:{...t.user,id:h(t.user.id)},excludeCredentials:t.excludeCredentials?.map(R)},n={};r&&(n.mediation="conditional"),n.publicKey=i,n.signal=A.createNewAbortSignal();let c;try{c=await navigator.credentials.create(n)}catch(u){throw I({error:u,options:n})}if(!c)throw new Error("Registration was not completed");let{id:f,rawId:a,response:s,type:y}=c,d;typeof s.getTransports=="function"&&(d=s.getTransports());let g;if(typeof s.getPublicKeyAlgorithm=="function")try{g=s.getPublicKeyAlgorithm()}catch(u){O("getPublicKeyAlgorithm()",u)}let b;if(typeof s.getPublicKey=="function")try{let u=s.getPublicKey();u!==null&&(b=l(u))}catch(u){O("getPublicKey()",u)}let m;if(typeof s.getAuthenticatorData=="function")try{m=l(s.getAuthenticatorData())}catch(u){O("getAuthenticatorData()",u)}return{id:f,rawId:l(a),response:{attestationObject:l(s.attestationObject),clientDataJSON:l(s.clientDataJSON),transports:d,publicKeyAlgorithm:g,publicKey:b,authenticatorData:m},type:y,clientExtensionResults:c.getClientExtensionResults(),authenticatorAttachment:E(c.authenticatorAttachment)}}function O(e,t){console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them.
|
|
2
|
+
`,t)}function _(){if(!p())return T.stubThis(new Promise(t=>t(!1)));let e=globalThis.PublicKeyCredential;return e?.isConditionalMediationAvailable===void 0?T.stubThis(new Promise(t=>t(!1))):T.stubThis(e.isConditionalMediationAvailable())}var T={stubThis:e=>e};function P({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new o({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new o({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){let i=globalThis.location.hostname;if(w(i)){if(r.rpId!==i)return new o({message:`The RP ID "${r.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new o({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new o({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function D(e){!e.optionsJSON&&e.challenge&&(console.warn("startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useBrowserAutofill:r=!1,verifyBrowserAutofillInput:i=!0}=e;if(!p())throw new Error("WebAuthn is not supported in this browser");let n;t.allowCredentials?.length!==0&&(n=t.allowCredentials?.map(R));let c={...t,challenge:h(t.challenge),allowCredentials:n},f={};if(r){if(!await _())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&i)throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');f.mediation="conditional",c.allowCredentials=[]}f.publicKey=c,f.signal=A.createNewAbortSignal();let a;try{a=await navigator.credentials.get(f)}catch(m){throw P({error:m,options:f})}if(!a)throw new Error("Authentication was not completed");let{id:s,rawId:y,response:d,type:g}=a,b;return d.userHandle&&(b=l(d.userHandle)),{id:s,rawId:l(y),response:{authenticatorData:l(d.authenticatorData),clientDataJSON:l(d.clientDataJSON),signature:l(d.signature),userHandle:b},type:g,clientExtensionResults:a.getClientExtensionResults(),authenticatorAttachment:E(a.authenticatorAttachment)}}function v(){return p()?PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable():new Promise(e=>e(!1))}export{A as WebAuthnAbortService,o as WebAuthnError,p as browserSupportsWebAuthn,_ as browserSupportsWebAuthnAutofill,v as platformAuthenticatorIsAvailable,D as startAuthentication,N as startRegistration};
|
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
* -
|
|
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
|
-
* -
|
|
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
|
|
80
|
+
* Nunca lança por causa de backend ausente; só lança em validação (com backend).
|
|
53
81
|
*/
|
|
54
|
-
export declare function storeAvatar(
|
|
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
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
114
|
+
* Backend builtin: o `@adonisjs/drive` JÁ 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
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
*
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 vê 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
|
-
|
|
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
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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:
|
|
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
|
-
|
|
137
|
-
|
|
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
|
}
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sudo mode — tela de confirmação de identidade (/account/confirm).
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* POST /account/confirm/passkey → verifica a resposta da passkey e marca o sudo.
|
|
4
|
+
* O GET lista os métodos DISPONÍVEIS para a conta (SPI `SudoMethod`); a
|
|
5
|
+
* verificação de cada um vive no próprio método, nas rotas que ele registra.
|
|
6
|
+
* Este controller não verifica credencial nem chama `markSudo`.
|
|
8
7
|
*
|
|
9
8
|
* A tela está atrás do `accountGuard` (requer sessão de conta ativa).
|
|
10
|
-
* Após confirmação, redireciona para `return_to` (validado) ou para `/account/tokens`.
|
|
11
9
|
*/
|
|
12
10
|
import '../augmentations.js';
|
|
13
11
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
12
|
+
import { sudoContextFrom } from '../sudo/runtime.js';
|
|
13
|
+
/**
|
|
14
|
+
* Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
|
|
15
|
+
* `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
|
|
16
|
+
* antigo segue valendo para quem já o importava.
|
|
17
|
+
*/
|
|
18
|
+
export { sudoContextFrom };
|
|
14
19
|
export default class AccountConfirmController {
|
|
15
|
-
show(ctx: HttpContext): Promise<
|
|
16
|
-
confirm(ctx: HttpContext): Promise<void>;
|
|
17
|
-
passkeyOptions(ctx: HttpContext): Promise<any>;
|
|
18
|
-
passkeyConfirm(ctx: HttpContext): Promise<void>;
|
|
19
|
-
/**
|
|
20
|
-
* Verifica se a conta é "passwordless" (sem hash de senha definido).
|
|
21
|
-
* Fail-safe: retorna `false` quando não é possível determinar.
|
|
22
|
-
*/
|
|
23
|
-
private isPasswordless;
|
|
20
|
+
show(ctx: HttpContext): Promise<unknown>;
|
|
24
21
|
}
|