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
+67
View File
@@ -0,0 +1,67 @@
{
"_note": "Each icon is a flat grid of single-char keys. '.' = transparent. Colors are filled at render time from CSS tokens or props.",
"heart": {
"rows": 8,
"cols": 8,
"grid": [
"........",
".XX..XX.",
"XXXXXXXX",
"XXXXXXXX",
".XXXXXX.",
"..XXXX..",
"...XX...",
"........"
],
"defaultColor": "var(--fc-red, #ff3b6b)"
},
"coin": {
"rows": 8,
"cols": 8,
"grid": [
"..XXXX..",
".XYYYYX.",
"XYYSYYYX",
"XYYSYYYX",
"XYYSYYYX",
"XYYSYYYX",
".XYYYYX.",
"..XXXX.."
],
"colors": {
"X": "var(--fc-gold, #ffd84d)",
"Y": "var(--fc-gold, #ffd84d)",
"S": "#a16207"
}
},
"star": {
"rows": 8,
"cols": 8,
"grid": [
"...XX...",
"...XX...",
"XX.XX.XX",
".XXXXXX.",
"..XXXX..",
".XXXXXX.",
"XX.XX.XX",
"...XX..."
],
"defaultColor": "var(--fc-gold, #ffd84d)"
},
"anchor": {
"rows": 8,
"cols": 8,
"grid": [
"...XX...",
"..XXXX..",
"...XX...",
"XXXXXXXX",
"...XX...",
"X..XX..X",
"XXXXXXXX",
"..XXXX.."
],
"defaultColor": "var(--fc-accent-2, #00f0ff)"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+160
View File
@@ -0,0 +1,160 @@
{
"name": "manoel",
"rows": 24,
"cols": 17,
"_colorSlots": {
"L": "outline (immutable)",
"s": "skin highlight (immutable)",
"S": "skin shade (immutable)",
"e": "eye (immutable)",
"m": "mouth (immutable)",
"h": "hat main",
"w": "hat band",
"c": "shirt main",
"C": "shirt shade",
"p": "pants main",
"P": "pants shade",
"b": "boots"
},
"_legend": {
".": "transparent",
"L": "#10131c",
"s": "#f3c79b",
"S": "#c98863",
"e": "#10131c",
"m": "#80484b",
"h": "#ffd84d",
"w": "#fde68a",
"c": "#00f0ff",
"C": "#0369a1",
"p": "#3a1f6e",
"P": "#2d1857",
"b": "#0f172a"
},
"grid": [
"....LLLLLLLLL....",
"..LLhhhhhhhhhLL..",
".LhhhhhhhhhhhhhL.",
"LLhhhhhhhhhhhhhLL",
"LwwwwwwwwwwwwwwwL",
"LLLLLLLLLLLLLLLLL",
"....LssssssssL...",
"...LssssssssssL..",
"..LsseSssssSesL..",
"..LssssssssssL...",
"..LSsLmmmmmLSsL..",
"...LSssssssSSL...",
"....LLsssssLL....",
"...LccccccccL....",
"..LcccCwwCccccL..",
".LccccCwwCccccL..",
".LccccCwwCccccL..",
".LCCCCCCCCCCCCL..",
"..LpppLLLLpppL...",
"..LppppppppppL...",
"..LppppppppppL...",
"..LPPPPPPPPPPL...",
"..LLbbLL.LLbbLL..",
"...LLLL...LLLL..."
],
"cosmetics": [
{
"id": "default",
"name": "Look Equipado (atual)",
"unlockLv": 7,
"preview": "manoel-default",
"theme": {}
},
{
"id": "lv01-tripulante",
"name": "Tripulante Iniciante",
"unlockLv": 1,
"preview": "manoel-lv01",
"_notes": "sem chapéu — h/w viram cabelo castanho",
"theme": {
"h": "#5a3a1d",
"w": "#5a3a1d",
"c": "#0ea5e9",
"C": "#075985",
"p": "#a16207",
"P": "#7c4a08",
"b": "#1e293b"
}
},
{
"id": "lv03-bone-verolme",
"name": "Boné Verolme",
"unlockLv": 3,
"preview": "manoel-lv03-bone-verolme",
"theme": {
"h": "#1e40af",
"w": "#fde68a",
"c": "#0ea5e9",
"C": "#075985",
"p": "#a16207",
"P": "#7c4a08",
"b": "#1e293b"
}
},
{
"id": "lv05-wingspan",
"name": "Camiseta Wingspan",
"unlockLv": 5,
"preview": "manoel-lv05-wingspan",
"theme": {
"h": "#ffd84d",
"w": "#fde68a",
"c": "#10b981",
"C": "#065f46",
"p": "#3a1f6e",
"P": "#2d1857",
"b": "#0f172a"
}
},
{
"id": "lv07-chapeu-sol",
"name": "Chapéu de Sol",
"unlockLv": 7,
"preview": "manoel-lv07-chapeu-sol",
"theme": {
"h": "#ffd84d",
"w": "#fde68a",
"c": "#00f0ff",
"C": "#0369a1",
"p": "#3a1f6e",
"P": "#2d1857",
"b": "#0f172a"
}
},
{
"id": "lv10-anti-vento",
"name": "Casaco Anti-Vento",
"unlockLv": 10,
"preview": "manoel-lv10-anti-vento",
"theme": {
"h": "#1e40af",
"w": "#fde68a",
"c": "#a855f7",
"C": "#581c87",
"p": "#1e293b",
"P": "#0f172a",
"b": "#0f172a"
}
},
{
"id": "lv15-capitao",
"name": "Trajado de Capitão",
"unlockLv": 15,
"preview": "manoel-lv15-capitao",
"theme": {
"h": "#ffd84d",
"w": "#dc2626",
"c": "#dc2626",
"C": "#7a0e0e",
"p": "#1c1917",
"P": "#0c0a09",
"b": "#0c0a09"
}
}
]
}