feature(up-dow-avatar): tidy avatar upload, enforce BearerAuth, add apidog types

This commit is contained in:
pedrogomes5913 2025-10-29 16:49:28 -03:00
parent 3c52ec5e3a
commit 47ef207454
4 changed files with 1138 additions and 21 deletions

View File

@ -4,6 +4,7 @@
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@supabase/supabase-js": "^2.75.0", "@supabase/supabase-js": "^2.75.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"next": "^16.0.0",
"react-big-calendar": "^1.19.4", "react-big-calendar": "^1.19.4",
"react-signature-canvas": "^1.1.0-alpha.2" "react-signature-canvas": "^1.1.0-alpha.2"
} }

1107
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2771,30 +2771,27 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
}; };
const ext = extMap[_file.type] || 'jpg'; const ext = extMap[_file.type] || 'jpg';
// O bucket deve ser 'avatars' e o caminho do objeto será userId/avatar.ext
const bucket = 'avatars';
const objectPath = `${userId}/avatar.${ext}`; const objectPath = `${userId}/avatar.${ext}`;
const uploadUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/${bucket}/${encodeURIComponent(objectPath)}`; const uploadUrl = `https://mock.apidog.com/m1/1053378-0-default/storage/v1/object/avatars/${encodeURI(objectPath)}`;
// Build multipart form data
const form = new FormData(); const form = new FormData();
form.append('file', _file, `avatar.${ext}`); form.append('file', _file, `avatar.${ext}`);
const headers: Record<string, string> = { const headers: Record<string, string> = {
// Supabase requires the anon key in 'apikey' header for client-side uploads Accept: 'application/json'
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
// Accept json
Accept: 'application/json',
}; };
// if user is logged in, include Authorization header
const jwt = getAuthToken(); const jwt = getAuthToken();
if (jwt) headers.Authorization = `Bearer ${jwt}`; if (!jwt) {
throw new Error('Autenticação necessária: token JWT obrigatório para upload de avatar');
}
headers.Authorization = `Bearer ${jwt}`;
console.debug('[uploadFotoPaciente] Iniciando upload:', { console.debug('[uploadFotoPaciente] Iniciando upload:', {
url: uploadUrl, url: uploadUrl,
fileType: _file.type, fileType: _file.type,
fileSize: _file.size, fileSize: _file.size,
hasAuth: !!jwt hasAuth: true
}); });
const res = await fetch(uploadUrl, { const res = await fetch(uploadUrl, {
@ -2827,7 +2824,7 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
// The API may not return a structured body; return the Key we constructed // The API may not return a structured body; return the Key we constructed
const key = (json && (json.Key || json.key)) ?? objectPath; const key = (json && (json.Key || json.key)) ?? objectPath;
const publicUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/public/avatars/${encodeURIComponent(userId)}/avatar.${ext}`; const publicUrl = `https://mock.apidog.com/m1/1053378-0-default/storage/v1/object/avatars/${encodeURIComponent(userId)}/avatar.${ext}`;
return { foto_url: publicUrl, Key: key }; return { foto_url: publicUrl, Key: key };
} }
@ -2842,21 +2839,22 @@ export function getAvatarPublicUrl(userId: string | number): string {
// Example: https://<project>.supabase.co/storage/v1/object/public/avatars/{userId}/avatar // Example: https://<project>.supabase.co/storage/v1/object/public/avatars/{userId}/avatar
const id = String(userId || '').trim(); const id = String(userId || '').trim();
if (!id) throw new Error('userId é obrigatório para obter URL pública do avatar'); if (!id) throw new Error('userId é obrigatório para obter URL pública do avatar');
const base = String(ENV_CONFIG.SUPABASE_URL).replace(/\/$/, ''); return `https://mock.apidog.com/m1/1053378-0-default/storage/v1/object/avatars/${encodeURIComponent(id)}/avatar`;
// Note: Supabase public object path does not require an extension in some setups
return `${base}/storage/v1/object/public/${encodeURIComponent('avatars')}/${encodeURIComponent(id)}/avatar`;
} }
export async function removerFotoPaciente(_id: string | number): Promise<void> { export async function removerFotoPaciente(_id: string | number): Promise<void> {
const userId = String(_id || '').trim(); const userId = String(_id || '').trim();
if (!userId) throw new Error('ID do paciente é obrigatório para remover foto'); if (!userId) throw new Error('ID do paciente é obrigatório para remover foto');
const deleteUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/avatars/${encodeURIComponent(userId)}/avatar`; const objectPath = `${userId}/avatar`;
const deleteUrl = `https://mock.apidog.com/m1/1053378-0-default/storage/v1/object/avatars/${encodeURI(objectPath)}`;
const headers: Record<string,string> = { const headers: Record<string,string> = {
apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Accept: 'application/json'
Accept: 'application/json',
}; };
// Require auth for delete (follow security expectations)
const jwt = getAuthToken(); const jwt = getAuthToken();
if (jwt) headers.Authorization = `Bearer ${jwt}`; if (!jwt) throw new Error('Autenticação necessária: token JWT obrigatório para remover avatar');
headers.Authorization = `Bearer ${jwt}`;
try { try {
console.debug('[removerFotoPaciente] Deleting avatar for user:', userId, 'url:', deleteUrl); console.debug('[removerFotoPaciente] Deleting avatar for user:', userId, 'url:', deleteUrl);

View File

@ -0,0 +1,11 @@
// Types for APIdog/OpenAPI helper models
export interface ApidogModel {
/**
* Path parameter used by the mock/OpenAPI for storage endpoints.
* Example: "user-123/avatar.jpg"
*/
path: string;
[property: string]: any;
}
export default ApidogModel;