Autentizace
Magic Link, Google OAuth, email/password, JWT tokeny v HTTP-only cookies a autorizační guardy
Turtor podporuje tři metody autentizace: Magic Link (OTP přes e-mail), Google OAuth (přes WorkOS) a email/password. JWT tokeny jsou uložené v HTTP-only cookies.
Metody přihlášení
Magic Link (e-mailový OTP)
Primární metoda přihlášení pro veřejný web. Odešle 6místný kód na e-mail.
POST /api/v1/auth/magic-link/send { email }
POST /api/v1/auth/magic-link/verify { email, code }| Parametr | Hodnota | Popis |
|---|---|---|
| Délka kódu | 6 číslic | Numerický kód |
| Platnost | 10 minut | Doba platnosti kódu |
| Max pokusů | 3 | Ověřovací pokusy na kód |
| Rate limit | 5/hod | Odeslání kódu na e-mail |
| Uložení kódu | bcrypt | Hashovaný s 10 koly |
Pokud e-mail neexistuje, automaticky se vytvoří nový účet s rolí company_admin.
Google OAuth (WorkOS SSO)
GET /api/v1/auth/login/workos -> Přesměrování na WorkOS AuthKit
GET /api/v1/auth/callback -> Zpracování OAuth callbacku
POST /api/v1/auth/callback -> API varianta (body: { code })Nový OAuth uživatel je vytvořen s rolí company_admin. Pokud e-mail již existuje, WorkOS ID se propojí s existujícím účtem.
Email a heslo
POST /api/v1/auth/register { email, password, firstName, lastName, role }
POST /api/v1/auth/login { email, password }Heslo je hashováno přes bcrypt (12 kol). Registrace odesílá ověřovací e-mail.
JWT tokeny
Uživatel se přihlásí (libovolná metoda)
│
▼
Backend vygeneruje tokeny
│
▼
Tokeny nastaveny jako HTTP-only cookies
│
├── access_token: 15 min, sameSite: lax
└── refresh_token: 7 dní, path: /api/v1/auth/refreshPayload tokenu
interface TokenPayload {
sub: string; // user.id
email: string;
roles: UserRole[];
activeRole: UserRole;
}Obnovení tokenu
POST /api/v1/auth/refreshRefresh token je omezený na cestu /api/v1/auth/refresh -- frontend jej nikdy nevidí. Klient automaticky obnovuje token každých 12 minut a při návratu na záložku.
API endpointy
| Metoda | Endpoint | Autentizace | Rate limit | Popis |
|---|---|---|---|---|
| POST | /auth/register | Veřejný | 3/min | Registrace |
| POST | /auth/login | Veřejný | 5/min | Přihlášení |
| POST | /auth/magic-link/send | Veřejný | 3/min | Odeslání OTP |
| POST | /auth/magic-link/verify | Veřejný | 5/min | Ověření OTP |
| GET | /auth/login/workos | Veřejný | -- | Google OAuth |
| POST | /auth/callback | Veřejný | -- | OAuth callback |
| POST | /auth/refresh | Cookie | -- | Obnovení tokenu |
| POST | /auth/logout | Veřejný | -- | Odhlášení |
| GET | /auth/me | JWT | -- | Profil uživatele |
| PATCH | /auth/me | JWT | -- | Aktualizace profilu |
| POST | /auth/switch-role | JWT | -- | Přepnutí role |
| POST | /auth/forgot-password | Veřejný | 3/min | Reset hesla |
| POST | /auth/reset-password | Veřejný | 5/min | Nové heslo |
| POST | /auth/change-password | JWT | -- | Změna hesla |
Unified profil (GET /auth/me)
Vrací kompletní profil kombinující data uživatele, instruktora, firmy a garanta:
interface UnifiedProfileDto {
id: string;
email: string;
firstName: string;
lastName: string;
phone: string | null;
phoneVerified: boolean;
roles: UserRole[];
activeRole: UserRole;
activeCompanyId: string | null;
emailVerified: boolean;
// Vnořené profily (null pokud neexistují)
instructor: InstructorProfileDto | null;
company: CompanyProfileDto | null;
garantRegions: GarantRegionDto[] | null;
preferences: UserPreferencesDto;
}Guardy na backendu
// Pouze autentizovaný uživatel
@UseGuards(JwtAuthGuard)
// Autentizovaný + konkrétní role
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
// Získání aktuálního uživatele
async endpoint(@CurrentUser() user: User) { }Ochrana routes na frontendu
Admin aplikace
Root layout vynucuje autentizaci přes beforeLoad:
export const Route = createRootRouteWithContext<RouterContext>()({
beforeLoad: async ({ location, context }) => {
if (location.pathname.startsWith('/auth')) return;
const user = await ensureAuth(context);
if (!user) {
throw redirect({
to: '/auth/login',
search: { returnUrl: location.pathname },
});
}
// Kontrola oprávnění role
const allowedRoles = getAllowedRoles(location.pathname);
if (allowedRoles && !hasRoutePermission(user.activeRole, allowedRoles)) {
throw redirect({ to: '/dashboard' });
}
},
});Web aplikace
Veřejný web nevyžaduje autentizaci. Přihlášení je vyžadováno jen pro checkout a profilové stránky.
Synchronizace mezi záložkami
Web aplikace synchronizuje stav přihlášení mezi záložkami pomocí BroadcastChannel (s localStorage fallbackem):
import { broadcastLogin, broadcastLogout, subscribeToAuthEvents } from '~/lib/auth';
// Po přihlášení
broadcastLogin();
// Po odhlášení
broadcastLogout();
// Ostatní záložky reagují automaticky
subscribeToAuthEvents(onLogout, onLogin);Uživatelské role
| Role | Hodnota | Popis |
|---|---|---|
| Administrátor | admin | Plný přístup k systému |
| Garant | garant | Schvaluje instruktory, spravuje region |
| Instruktor | instructor | Vyučuje kurzy |
| Administrátor firmy | company_admin | Rezervuje kurzy, spravuje firmu |
| Zaměstnanec firmy | company_employee | Prohlíží firemní rezervace |
| Obchodní zástupce | sales_rep | Spravuje leady a sales pipeline |
Uživatelé mohou mít více rolí a přepínat mezi nimi přes POST /auth/switch-role.