File size: 7,298 Bytes
eddc354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
 * Локальный сервисный слой аутентификации (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);
};