feat: multi-usuário com autenticação JWT

- Tabela `profiles` + coluna `profile_id` em todas as entidades
  (categories, transactions, recurring_expenses, accounts, player_profile,
   xp_events, player_quests, player_achievements, player_cosmetics)
- Dados existentes migrados para profile_id = 1 (Manoel)
- CLI `./api create-user --name <n> --password <p>` cria perfil com
  seed de categorias e player_profile; faz upsert de senha se já existir
- Auth substituída: cookie+APP_PASSWORD → JWT Bearer 24h (HS256)
- Middleware RequireAuth injeta profile_id no context de todas as rotas
- Todos os repositórios filtram por profile_id do context
- Endpoints: POST /api/auth/login, GET /api/auth/me,
  POST /api/auth/change-password, POST /api/logout
- Frontend: auth store usa localStorage (fc_token/fc_profile),
  api.ts envia Authorization header, LoginView usa campo name
- SettingsView reescrita com troca de senha e logout
- docker-compose.yml: remove APP_USERNAME/APP_PASSWORD, adiciona JWT_SECRET

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-27 20:04:44 -03:00
co-authored by Claude Sonnet 4.6
parent 50637bd590
commit acbce4edc0
49 changed files with 2662 additions and 382 deletions
+95
View File
@@ -0,0 +1,95 @@
<template>
<div
class="fc-panel"
:class="{ 'fc-panel--glow': glow, [`fc-panel--${variant}`]: !!variant }"
>
<template v-if="corners">
<span class="fc-corner fc-corner--tl" />
<span class="fc-corner fc-corner--tr" />
<span class="fc-corner fc-corner--bl" />
<span class="fc-corner fc-corner--br" />
</template>
<header v-if="title || $slots.header" class="fc-panel__header">
<div v-if="title" class="fc-panel__title">:: {{ title }}</div>
<slot name="header" />
</header>
<slot />
</div>
</template>
<script setup lang="ts">
defineProps<{
/** Painel "principal" da rota: borda roxa + halo. Use com parcimônia. */
glow?: boolean
/** Cantos em L decorativos. Reserve pro card de destaque. */
corners?: boolean
/** 'danger' (recorrência ausente, meta falhou) | 'success' */
variant?: 'danger' | 'success'
/** Título estilo HUD; renderizado automaticamente como `:: NOME`. */
title?: string
}>()
</script>
<style scoped>
.fc-panel {
position: relative;
background: linear-gradient(180deg, var(--fc-bg-raised), var(--fc-bg-panel));
border: 2px solid var(--fc-panel-edge);
border-radius: var(--fc-radius);
padding: var(--fc-space-4);
box-shadow: var(--fc-shadow-panel);
}
.fc-panel--glow {
border-color: var(--fc-accent-3);
box-shadow: var(--fc-shadow-panel-glow);
}
.fc-panel--danger {
border-color: var(--fc-red);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px var(--fc-red),
0 0 16px rgba(255, 59, 107, .3),
0 8px 24px rgba(0,0,0,.4);
}
.fc-panel--success {
border-color: var(--fc-green);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px var(--fc-green),
0 0 16px rgba(57, 255, 122, .25),
0 8px 24px rgba(0,0,0,.4);
}
.fc-panel__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--fc-space-3);
gap: var(--fc-space-2);
}
.fc-panel__title {
font-family: var(--fc-font-pixel);
font-size: 9px;
color: var(--fc-accent-2);
letter-spacing: .04em;
text-transform: uppercase;
}
.fc-corner {
position: absolute;
width: 8px;
height: 8px;
border: 2px solid var(--fc-accent-2);
pointer-events: none;
}
.fc-corner--tl { top: -2px; left: -2px; border-right: none; border-bottom: none; }
.fc-corner--tr { top: -2px; right: -2px; border-left: none; border-bottom: none; }
.fc-corner--bl { bottom: -2px; left: -2px; border-right: none; border-top: none; }
.fc-corner--br { bottom: -2px; right: -2px; border-left: none; border-top: none; }
</style>