/** * Локальный сервисный слой аутентификации (RealKnot Studio). * * Сейчас все данные хранятся в localStorage браузера. * Точки интеграции с Supabase помечены комментариями SUPABASE: * при подключении Supabase JS клиента достаточно заменить тела функций, * не меняя вызовы в UI. */ export type UserRole = "admin" | "user"; export type UserStatus = "active" | "pending"; export type StoredUser = { id: string; username: string; role: UserRole; /** Хэш пароля. В Supabase хранением занимается auth.users. */ secret: string; /** true — пароль временный, требуется смена при входе. */ temporary: boolean; status: UserStatus; createdAt: number; lastLoginAt: number | null; /** Профиль. SUPABASE: таблица `profiles` (display_name, avatar_url, about). */ displayName?: string; avatar?: string; about?: string; }; export type Session = { userId: string; startedAt: number }; export const USERS_KEY = "realknot-users-v1"; export const SESSION_KEY = "realknot-session-v1"; const safe = (fn: () => T, fallback: T): T => { try { return fn(); } catch { return fallback; } }; export const newId = () => Math.random().toString(36).slice(2, 10); /** Демонстрационный хэш. SUPABASE: не нужен — пароли хранит Supabase Auth. */ const hash = (value: string) => { let h = 0; for (let i = 0; i < value.length; i += 1) h = (h * 31 + value.charCodeAt(i)) | 0; return `h${(h >>> 0).toString(36)}`; }; export const generateTempPassword = () => { const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; let out = ""; for (let i = 0; i < 8; i += 1) out += alphabet[Math.floor(Math.random() * alphabet.length)]; return out; }; const writeUsers = (list: StoredUser[]) => safe(() => localStorage.setItem(USERS_KEY, JSON.stringify(list)), undefined); /** SUPABASE: заменить на `supabase.from("profiles").select("*")`. */ export const listUsers = (): StoredUser[] => { const list = safe(() => { const raw = localStorage.getItem(USERS_KEY); return raw ? (JSON.parse(raw) as StoredUser[]) : []; }, []); if (list.length === 0) { const admin: StoredUser = { id: "admin", username: "admin", role: "admin", secret: hash("1234"), temporary: false, status: "active", createdAt: Date.now(), lastLoginAt: null, }; writeUsers([admin]); return [admin]; } return list.sort((a, b) => b.createdAt - a.createdAt); }; export const getUser = (id: string): StoredUser | null => listUsers().find((u) => u.id === id) ?? null; export const findByUsername = (username: string): StoredUser | null => listUsers().find((u) => u.username.toLowerCase() === username.trim().toLowerCase()) ?? null; export type LoginResult = | { ok: true; user: StoredUser; mustChangePassword: boolean } | { ok: false; error: string }; /** SUPABASE: заменить на `supabase.auth.signInWithPassword({ email, password })`. */ export const login = (username: string, password: string, remember: boolean): LoginResult => { const user = findByUsername(username); if (!user || user.secret !== hash(password)) { return { ok: false, error: "Неверный логин или пароль." }; } const list = listUsers().map((u) => (u.id === user.id ? { ...u, lastLoginAt: Date.now() } : u)); writeUsers(list); saveSession(user.id, remember); return { ok: true, user, mustChangePassword: user.temporary }; }; /** SUPABASE: сессию хранит `supabase.auth` — этот слой станет не нужен. */ export const saveSession = (userId: string, remember: boolean) => safe(() => { const payload = JSON.stringify({ userId, startedAt: Date.now() } satisfies Session); (remember ? localStorage : sessionStorage).setItem(SESSION_KEY, payload); }, undefined); export const readSession = (): Session | null => safe(() => { const raw = localStorage.getItem(SESSION_KEY) ?? sessionStorage.getItem(SESSION_KEY); return raw ? (JSON.parse(raw) as Session) : null; }, null); /** SUPABASE: `supabase.auth.signOut()`. */ export const logout = () => safe(() => { localStorage.removeItem(SESSION_KEY); sessionStorage.removeItem(SESSION_KEY); }, undefined); export const currentUser = (): StoredUser | null => { const session = readSession(); return session ? getUser(session.userId) : null; }; /** SUPABASE: `supabase.auth.admin.createUser({ ... })`. */ export const createUser = ( username: string, role: UserRole = "user", ): { ok: true; user: StoredUser; tempPassword: string } | { ok: false; error: string } => { const name = username.trim(); if (name.length < 3) return { ok: false, error: "Логин должен содержать минимум 3 символа." }; if (findByUsername(name)) return { ok: false, error: "Пользователь с таким логином уже существует." }; const tempPassword = generateTempPassword(); const user: StoredUser = { id: newId(), username: name, role, secret: hash(tempPassword), temporary: true, status: "pending", createdAt: Date.now(), lastLoginAt: null, }; writeUsers([user, ...listUsers()]); return { ok: true, user, tempPassword }; }; /** SUPABASE: `supabase.auth.admin.updateUserById(id, { password })`. */ export const resetPassword = (id: string): string => { const tempPassword = generateTempPassword(); writeUsers( listUsers().map((u) => u.id === id ? { ...u, secret: hash(tempPassword), temporary: true, status: "pending" } : u, ), ); return tempPassword; }; /** Смена временного пароля на постоянный. SUPABASE: `supabase.auth.updateUser({ password })`. */ export const setPermanentPassword = (id: string, password: string): { ok: boolean; error?: string } => { if (password.trim().length < 4) return { ok: false, error: "Пароль должен содержать минимум 4 символа." }; writeUsers( listUsers().map((u) => u.id === id ? { ...u, secret: hash(password.trim()), temporary: false, status: "active" } : u, ), ); return { ok: true }; }; /** SUPABASE: `supabase.auth.admin.deleteUser(id)` + каскадное удаление проектов. */ export const deleteUser = (id: string) => { writeUsers(listUsers().filter((u) => u.id !== id)); }; export const roleLabel = (role: UserRole) => (role === "admin" ? "Администратор" : "Пользователь"); export const statusLabel = (status: UserStatus) => status === "active" ? "Активен" : "Ожидает смены пароля"; /** Обновление профиля (имя, аватар, о себе). SUPABASE: `supabase.from("profiles").update(...)`. */ export const updateProfile = ( id: string, patch: { displayName?: string; avatar?: string; about?: string }, ): StoredUser | null => { writeUsers(listUsers().map((u) => (u.id === id ? { ...u, ...patch } : u))); return getUser(id); };