Skip to content

feat: foto de perfil com recorte, salva no R2 com copia local redundante - #20

Merged
janeiaraujo merged 1 commit into
mainfrom
feat/profile-avatar-crop
Aug 3, 2026
Merged

feat: foto de perfil com recorte, salva no R2 com copia local redundante#20
janeiaraujo merged 1 commit into
mainfrom
feat/profile-avatar-crop

Conversation

@janeiaraujo

Copy link
Copy Markdown
Owner

O que entrega

Foto de perfil com recorte, gravada no Cloudflare R2 e no disco local como redundância.

Frontend — AvatarUploader

Recorte quadrado com zoom e arraste (react-easy-crop, com máscara circular). O recorte é aplicado via canvas gerando um JPEG 512×512 antes do upload — o servidor recebe a imagem já pronta, sem precisar de biblioteca de processamento de imagem no backend.

O avatar aparece no Perfil e na sidebar. Se a URL principal (R2) falhar ao carregar, o componente cai para a cópia local via onError, e só depois para a inicial do nome — a redundância só tem valor se o frontend souber usá-la.

Backend — redundância de verdade

Criei utils/storage.js porque a lógica de R2 estava duplicada dentro de files.routes.js; agora existe um lugar só, e files.routes.js passa a importar de lá (sem mudança de comportamento).

A diferença entre os dois modos ficou explícita:

Função Comportamento Usado em
upload existente R2 ou disco local (fallback) arquivos de KB
storeFileWithRedundancy disco local sempre + R2 quando configurado avatar

Se o envio ao R2 falhar, degrada para somente-local em vez de derrubar a operação inteira.

Endpoints novos:

  • POST /api/users/me/avatar — valida tipo (JPEG/PNG/WebP) e tamanho (2MB), grava nas duas pontas, remove a imagem anterior. A antiga só é apagada depois da nova ser gravada, para nunca ficar sem avatar se a limpeza falhar.
  • DELETE /api/users/me/avatar

Avatar exposto em /auth/me, login e magic link, para aparecer já na autenticação.

Test plan

  • npm run build sem erros; node --check nos 4 arquivos de backend
  • Sem R2 configurado (padrão do dev): subir foto, recortar, salvar → conferir arquivo em backend/uploads/{tenant}/ e avatar na sidebar
  • Com R2 configurado: conferir que o arquivo aparece nos dois lugares (bucket + disco) e que storage volta como r2+local
  • Trocar a foto duas vezes e confirmar que a anterior é removida das duas cópias
  • Remover a foto e confirmar que volta para a inicial do nome
  • Testar rejeição: arquivo não-imagem e imagem acima de 8MB

Não consegui testar o fluxo completo aqui (o sandbox não alcança o Mongo, e o R2 exige credenciais reais). A validação foi build + revisão de código; o caminho do upload merece um teste manual atento.

Backend:
- utils/storage.js: helpers de R2/disco centralizados. A logica de R2
  estava duplicada dentro de files.routes.js; agora ha um lugar so, e
  files.routes.js passa a importar dali.
- storeFileWithRedundancy(): grava SEMPRE no disco local e, quando o R2
  esta configurado, tambem envia pra la. Diferente do upload de arquivos
  de KB, que e R2 *ou* local. Falha no R2 degrada para somente-local em
  vez de derrubar a operacao.
- POST/DELETE /api/users/me/avatar: valida tipo (JPEG/PNG/WebP) e
  tamanho (2MB), troca a imagem e limpa a anterior nas duas copias. A
  antiga so e removida depois da nova ser gravada.
- Avatar exposto em /auth/me, login e magic link.

Frontend:
- AvatarUploader: recorte quadrado com zoom e arraste (react-easy-crop),
  gerando um JPEG 512x512 via canvas antes do upload - o servidor recebe
  a imagem ja pronta, sem precisar processar.
- Avatar aparece no Perfil e na sidebar. Se a URL do R2 falhar ao
  carregar, o componente cai para a copia local (onError) e so depois
  para a inicial do nome - a redundancia so serve se o frontend usa.
Copilot AI review requested due to automatic review settings August 3, 2026 17:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implementa upload/edição de foto de perfil com recorte no frontend e persistência com redundância no backend (cópia local + Cloudflare R2 quando configurado), além de expor URLs do avatar nas respostas de autenticação e perfil para uso imediato na UI.

Changes:

  • Adiciona AvatarUploader com recorte (react-easy-crop) e upload do avatar via /users/me/avatar, incluindo remoção.
  • Centraliza utilitários de armazenamento (R2 + disco) em backend/src/utils/storage.js e refatora files.routes.js para reutilizar.
  • Propaga avatar_url e avatar_fallback_url no Perfil, Sidebar e rotas de auth (/auth/*).

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
frontend/src/services/api.js Expõe métodos uploadAvatar/deleteAvatar no userAPI.
frontend/src/pages/Profile.jsx Integra AvatarUploader ao perfil e sincroniza avatar no AuthContext/localStorage.
frontend/src/i18n/locales/pt.json Adiciona strings de UI para avatar e botão “Cancelar”.
frontend/src/i18n/locales/en.json Adiciona strings de UI para avatar e botão “Cancel”.
frontend/src/components/Sidebar.jsx Renderiza avatar na sidebar com fallback para URL local.
frontend/src/components/AvatarUploader.jsx Novo componente de recorte + upload/remoção do avatar.
frontend/package.json Adiciona dependência react-easy-crop.
frontend/package-lock.json Atualiza lockfile com react-easy-crop e dependências.
backend/src/utils/storage.js Novo util centralizado para cópia local e upload/remoção no R2.
backend/src/modules/users/users.routes.js Novos endpoints de upload/remoção de avatar e exposição no publicProfile.
backend/src/modules/files/files.routes.js Remove duplicação e importa helpers de utils/storage.js.
backend/src/modules/auth/auth.routes.js Inclui avatar_url e avatar_fallback_url nas respostas de auth.
Files not reviewed (1)
  • frontend/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +48 to +53
export const saveLocalCopy = (tenantId, fileName, buffer) => {
const dir = path.join(getUploadsDir(), String(tenantId));
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, fileName), buffer);
return buildLocalUrl(tenantId, fileName);
};
Comment on lines +244 to +248
<img
src={avatarSrc}
alt=""
className="w-100 h-100 rounded-circle"
style={{ objectFit: 'cover' }}
Comment on lines +95 to +99
// Avatar: URL principal (R2 quando configurado) -> copia local -> inicial do
// nome. `avatarFailed` cobre o caso da imagem principal nao carregar.
const avatarSrc = avatarFailed
? user?.avatar_fallback_url
: (user?.avatar_url || user?.avatar_fallback_url);
Comment on lines +129 to +138
{displayedUrl ? (
<img
src={displayedUrl}
alt={name || 'avatar'}
className="rounded-circle"
style={{ width: '88px', height: '88px', objectFit: 'cover' }}
onError={() => {
// URL principal indisponível (R2 fora do ar, bucket movido...)
if (!useFallback && fallbackUrl) setUseFallback(true);
}}
Comment on lines +176 to +182
for await (const chunk of data.file) {
total += chunk.length;
if (total > MAX_AVATAR_BYTES) {
return reply.status(413).send({ error: 'Imagem muito grande (máx. 2MB)' });
}
chunks.push(chunk);
}
@janeiaraujo
janeiaraujo merged commit e95bd6f into main Aug 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants