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
+140
View File
@@ -0,0 +1,140 @@
/* Financeiro Carvalho · Arcade Neon
* Drop this into src/styles/tokens.css and import once in main.ts:
* import './styles/tokens.css'
*
* All other components MUST consume var(--fc-*) instead of literal hex.
*/
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600;700&display=swap');
:root {
/* ─────────────── color tokens ─────────────── */
--fc-bg: #0a0418;
--fc-bg-raised: #150827;
--fc-bg-panel: #1c0d33;
--fc-panel-edge: #2d1857;
--fc-line: #3a1f6e;
--fc-text: #f4eaff;
--fc-text-dim: #8b75b8;
--fc-accent: #ff2d95; /* magenta — alerts, "today", primary CTA accent */
--fc-accent-2: #00f0ff; /* cyan — links, nav, "weekly" */
--fc-accent-3: #a855f7; /* purple — structure, glow, default button */
--fc-gold: #ffd84d; /* XP, achievements, "monthly" */
--fc-green: #39ff7a; /* goal hit, positive value */
--fc-red: #ff3b6b; /* goal missed, critical alert */
/* rgb fragments for shadows / alphas (no color-mix support? use these) */
--fc-accent-rgb: 255 45 149;
--fc-accent-2-rgb: 0 240 255;
--fc-accent-3-rgb: 168 85 247;
--fc-gold-rgb: 255 216 77;
--fc-green-rgb: 57 255 122;
--fc-red-rgb: 255 59 107;
/* ─────────────── sprite color slots ─────────────── */
--fc-sprite-outline: #10131c;
--fc-sprite-skin: #f3c79b;
--fc-sprite-skin-shade: #c98863;
--fc-sprite-eye: #10131c;
--fc-sprite-mouth: #80484b;
--fc-sprite-hat: #ffd84d;
--fc-sprite-band: #fde68a;
--fc-sprite-shirt: #00f0ff;
--fc-sprite-shirt-shade: #0369a1;
--fc-sprite-pants: #3a1f6e;
--fc-sprite-pants-shade: #2d1857;
--fc-sprite-boots: #0f172a;
/* ─────────────── sizing ─────────────── */
--fc-radius-sm: 2px;
--fc-radius: 4px;
--fc-radius-lg: 6px;
--fc-space-1: 4px;
--fc-space-2: 8px;
--fc-space-3: 12px;
--fc-space-4: 16px;
--fc-space-5: 24px;
/* ─────────────── shadows ─────────────── */
--fc-shadow-panel:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px rgba(0,0,0,.4),
0 8px 24px rgba(0,0,0,.4);
--fc-shadow-panel-glow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px var(--fc-accent-3),
0 0 24px rgba(168,85,247,.25),
0 8px 24px rgba(0,0,0,.5);
/* ─────────────── fonts ─────────────── */
--fc-font-pixel: 'Press Start 2P', monospace;
--fc-font-mono: 'JetBrains Mono', ui-monospace, monospace;
--fc-font-body: 'Inter', system-ui, sans-serif;
}
/* Global reset for the app root */
html, body {
margin: 0;
padding: 0;
background: var(--fc-bg);
color: var(--fc-text);
font-family: var(--fc-font-body);
}
* { box-sizing: border-box; }
/* Util classes */
.fc-pixel { font-family: var(--fc-font-pixel); letter-spacing: .02em; }
.fc-mono { font-family: var(--fc-font-mono); }
.fc-body { font-family: var(--fc-font-body); }
.fc-text-dim { color: var(--fc-text-dim); }
.fc-text-accent { color: var(--fc-accent); }
.fc-text-accent2 { color: var(--fc-accent-2); }
.fc-text-accent3 { color: var(--fc-accent-3); }
.fc-text-gold { color: var(--fc-gold); }
.fc-text-green { color: var(--fc-green); }
.fc-text-red { color: var(--fc-red); }
.fc-glow-cyan { text-shadow: 0 0 8px rgba(0,240,255,.6); }
.fc-glow-magenta { text-shadow: 0 0 8px rgba(255,45,149,.6); }
.fc-glow-purple { text-shadow: 0 0 8px rgba(168,85,247,.6); }
.fc-glow-gold { text-shadow: 0 0 8px rgba(255,216,77,.6); }
.fc-glow-green { text-shadow: 0 0 8px rgba(57,255,122,.6); }
.fc-glow-red { text-shadow: 0 0 8px rgba(255,59,107,.6); }
/* App shell — wrap your <router-view> in <div class="fc-app"> */
.fc-app {
background:
radial-gradient(ellipse 80% 50% at 50% 0%, rgba(168,85,247,.18), transparent 70%),
radial-gradient(ellipse 60% 40% at 80% 100%, rgba(255,45,149,.12), transparent 60%),
var(--fc-bg);
position: relative;
min-height: 100vh;
overflow-x: hidden;
}
.fc-app::before {
content: '';
position: absolute; inset: 0;
background-image: repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 3px);
pointer-events: none;
z-index: 2;
}
.fc-app::after {
content: '';
position: absolute; inset: 0;
background-image:
linear-gradient(var(--fc-line) 1px, transparent 1px),
linear-gradient(90deg, var(--fc-line) 1px, transparent 1px);
background-size: 32px 32px;
opacity: .12;
pointer-events: none;
}
.fc-app > * { position: relative; z-index: 1; }
/* Blinking utility for critical alerts (recurrence missing, etc.) */
@keyframes fc-blink { 0%,49% { opacity: 1 } 50%,100% { opacity: 0.2 } }
.fc-blink { animation: fc-blink 1.2s steps(1) infinite; }