Страница /club (уровни, форма заявки, статус, админ-модерация), /club/gallery (закрытая галерея), /login (кнопка «Войти через Яндекс» + email-вход, разбор #token). Хелперы lib/auth.ts, lib/club.ts, компонент AdminRequests. Ссылки «Клуб»/«Войти» в Nav. Эстетика по существующим токенам (paper/ink/accent, Cormorant/Inter). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
// Клиентский помощник авторизации (браузер).
|
|
// Токен храним в localStorage; редирект Яндекса приносит его в hash: /club#token=...&refresh=...
|
|
'use client';
|
|
|
|
import { API_BASE } from './api';
|
|
|
|
const TOKEN_KEY = 'nika_token';
|
|
const REFRESH_KEY = 'nika_refresh';
|
|
|
|
export interface AuthUser {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
role: 'owner' | 'admin' | 'buyer' | 'subscriber';
|
|
telegram_id?: number | null;
|
|
club_tier?: string | null;
|
|
avatar_url?: string | null;
|
|
created_at?: string;
|
|
}
|
|
|
|
export function getToken(): string | null {
|
|
if (typeof window === 'undefined') return null;
|
|
return window.localStorage.getItem(TOKEN_KEY);
|
|
}
|
|
|
|
export function setTokens(token: string, refresh?: string | null) {
|
|
if (typeof window === 'undefined') return;
|
|
window.localStorage.setItem(TOKEN_KEY, token);
|
|
if (refresh) window.localStorage.setItem(REFRESH_KEY, refresh);
|
|
}
|
|
|
|
export function clearTokens() {
|
|
if (typeof window === 'undefined') return;
|
|
window.localStorage.removeItem(TOKEN_KEY);
|
|
window.localStorage.removeItem(REFRESH_KEY);
|
|
}
|
|
|
|
// Разбирает токен из hash после редиректа OAuth. Возвращает true, если токен найден и сохранён.
|
|
export function consumeHashToken(): boolean {
|
|
if (typeof window === 'undefined') return false;
|
|
const hash = window.location.hash.replace(/^#/, '');
|
|
if (!hash) return false;
|
|
const params = new URLSearchParams(hash);
|
|
const token = params.get('token');
|
|
if (!token) return false;
|
|
setTokens(token, params.get('refresh'));
|
|
// Чистим hash, чтобы токен не «висел» в адресной строке.
|
|
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
return true;
|
|
}
|
|
|
|
// Авторизованный fetch к нашему API. Кидает объект { status, message } при ошибке.
|
|
export async function authFetch<T = unknown>(
|
|
path: string,
|
|
init: RequestInit = {},
|
|
): Promise<T> {
|
|
const token = getToken();
|
|
const res = await fetch(`${API_BASE}${path.startsWith('/') ? path : `/${path}`}`, {
|
|
...init,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
...(init.body ? { 'Content-Type': 'application/json' } : {}),
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...(init.headers || {}),
|
|
},
|
|
});
|
|
let data: unknown = null;
|
|
try {
|
|
data = await res.json();
|
|
} catch {
|
|
/* пустое тело */
|
|
}
|
|
if (!res.ok) {
|
|
const message =
|
|
(data && typeof data === 'object' && 'error' in data && (data as { error: string }).error) ||
|
|
`${res.status} ${res.statusText}`;
|
|
throw { status: res.status, message } as { status: number; message: string };
|
|
}
|
|
return data as T;
|
|
}
|
|
|
|
// Текущий пользователь (или null, если не авторизован / токен протух).
|
|
export async function fetchMe(): Promise<AuthUser | null> {
|
|
if (!getToken()) return null;
|
|
try {
|
|
const { user } = await authFetch<{ user: AuthUser }>('/auth/me');
|
|
return user;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Ссылка на старт входа через Яндекс (полный путь до backend).
|
|
export function yandexLoginUrl(): string {
|
|
return `${API_BASE}/auth/yandex`;
|
|
}
|
|
|
|
export function isAdmin(user: AuthUser | null): boolean {
|
|
return !!user && (user.role === 'admin' || user.role === 'owner');
|
|
}
|