Spaces:
Sleeping
Sleeping
| /** | |
| * Локальный сервисный слой аутентификации (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 = <T,>(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<StoredUser[]>(() => { | |
| 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); | |
| }; | |