Test / lib /auth-service.ts
TrikozikGames's picture
Add source files
eddc354
Raw
History Blame Contribute Delete
7.3 kB
/**
* Локальный сервисный слой аутентификации (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);
};