feat: PKCE auth + CI/CD deploy
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 1m2s

- Frontend: PKCE flow (oauth.ts, AuthCallback code exchange, 401 interceptor)
- Backend: token introspection via SuperOAuth (no more JWT secret)
- User model: superOauthId (unified) replaces oauthId+provider
- Cookies httpOnly session + refresh token
- POST /auth/refresh endpoint
- Gitea CI workflow (vps-runner pattern)
- DB_SYNC env var for initial schema creation
This commit is contained in:
2026-03-24 13:01:14 +01:00
parent c1bf793234
commit 8c6777c980
61 changed files with 5850 additions and 66 deletions

View File

@@ -0,0 +1,24 @@
interface BarProps {
value: number;
max: number;
type: 'hp' | 'end' | 'xp';
label?: string;
showValues?: boolean;
}
export function Bar({ value, max, type, label, showValues = true }: BarProps) {
const pct = Math.min(100, Math.round((value / Math.max(max, 1)) * 100));
return (
<div>
{(label || showValues) && (
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4, fontSize: 12, color: '#6b7a99' }}>
{label && <span>{label}</span>}
{showValues && <span>{value} / {max}</span>}
</div>
)}
<div className="bar-track">
<div className={`bar-fill-${type}`} style={{ width: `${pct}%` }} />
</div>
</div>
);
}

View File

@@ -0,0 +1,90 @@
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { Swords, Package, Hammer, User, LogOut, Shield } from 'lucide-react';
const NAV = [
{ to: '/dashboard', icon: User, label: 'Personnage' },
{ to: '/combat', icon: Swords, label: 'Combat' },
{ to: '/inventory', icon: Package, label: 'Inventaire' },
{ to: '/craft', icon: Hammer, label: 'Artisanat' },
{ to: '/forge', icon: Shield, label: 'Forge' },
];
export function Layout({ children }: { children: React.ReactNode }) {
const { user, logout } = useAuth();
const loc = useLocation();
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<header style={{
background: '#161b25',
borderBottom: '1px solid #2a3448',
padding: '0 1.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
height: 52,
position: 'sticky',
top: 0,
zIndex: 10,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 20 }}>🐸</span>
<span style={{ fontWeight: 800, color: '#f4c94e', letterSpacing: '-0.5px' }}>TetaRdPG</span>
</div>
{user && (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 13, color: '#6b7a99' }}>{user.username}</span>
<button className="btn btn-ghost" style={{ padding: '0.3rem 0.6rem' }} onClick={logout} title="Déconnexion">
<LogOut size={14} />
</button>
</div>
)}
</header>
<div style={{ display: 'flex', flex: 1 }}>
{/* Sidebar nav */}
<nav style={{
width: 56,
background: '#161b25',
borderRight: '1px solid #2a3448',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '1rem 0',
gap: 4,
position: 'sticky',
top: 52,
height: 'calc(100vh - 52px)',
}}>
{NAV.map(({ to, icon: Icon, label }) => {
const active = loc.pathname.startsWith(to);
return (
<Link key={to} to={to} title={label} style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
borderRadius: 8,
color: active ? '#f4c94e' : '#6b7a99',
background: active ? '#1e2535' : 'transparent',
border: active ? '1px solid #c49c2e' : '1px solid transparent',
textDecoration: 'none',
transition: 'all 0.15s',
}}>
<Icon size={18} />
</Link>
);
})}
</nav>
{/* Main content */}
<main style={{ flex: 1, padding: '1.5rem', maxWidth: 900, margin: '0 auto', width: '100%' }}>
{children}
</main>
</div>
</div>
);
}