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:
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="fc-bar" :class="[`fc-bar--${size}`, { 'fc-bar--success': success, 'fc-bar--danger': danger }]">
|
||||
<div class="fc-bar__fill" :style="{ width: clamped + '%' }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** Percentual 0–100 (ou maior, será clampado). */
|
||||
pct: number
|
||||
/** 'sm' (6px) | 'md' (8px) | 'lg' (14px) */
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
/** Pinta de verde quando meta foi atingida. */
|
||||
success?: boolean
|
||||
/** Pinta de vermelho. */
|
||||
danger?: boolean
|
||||
}>(), { size: 'md' })
|
||||
|
||||
const clamped = computed(() => Math.max(0, Math.min(100, props.pct)))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-bar {
|
||||
background: var(--fc-bg);
|
||||
border: 2px solid var(--fc-panel-edge);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.fc-bar--sm { height: 6px; }
|
||||
.fc-bar--md { height: 8px; }
|
||||
.fc-bar--lg { height: 14px; }
|
||||
|
||||
.fc-bar__fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--fc-accent-2), var(--fc-accent-3), var(--fc-accent));
|
||||
position: relative;
|
||||
transition: width .6s cubic-bezier(.2,.7,.3,1);
|
||||
}
|
||||
.fc-bar__fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: repeating-linear-gradient(90deg, rgba(0,0,0,.2) 0 2px, transparent 2px 6px);
|
||||
}
|
||||
|
||||
.fc-bar--success { border-color: var(--fc-green); }
|
||||
.fc-bar--success .fc-bar__fill {
|
||||
background: linear-gradient(90deg, var(--fc-green), var(--fc-accent-2));
|
||||
}
|
||||
|
||||
.fc-bar--danger { border-color: var(--fc-red); }
|
||||
.fc-bar--danger .fc-bar__fill {
|
||||
background: linear-gradient(90deg, var(--fc-red), var(--fc-accent));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user