| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { backendReachable, getToken, setToken, request, localFallbackAllowed } from './Api.js'; |
|
|
| const SERVER_UNREACHABLE = "Can't reach the server right now — please check your connection and try again in a moment."; |
|
|
| const USERS_KEY = 'echo_users_v1'; |
| const SESSION_KEY = 'echo_session_v1'; |
| const CACHED_ME_KEY = 'echo_me_v1'; |
| const ADMIN_EMAILS = new Set(['admin@echo.app', 'superadmin@echo.app']); |
|
|
| |
| |
| |
| function hash(s) { |
| let h = 5381; |
| for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0; |
| return (h >>> 0).toString(16); |
| } |
| function readUsers() { try { return JSON.parse(localStorage.getItem(USERS_KEY) || '{}'); } catch { return {}; } } |
| function writeUsers(u) { localStorage.setItem(USERS_KEY, JSON.stringify(u)); } |
|
|
| const SEED_ADMIN = { email: 'admin@echo.app', name: 'Administrator', pwd: 'EchoAdm-2n5y59032058' }; |
| (function seedLocalAdmin() { |
| const users = readUsers(); |
| if (users[SEED_ADMIN.email]) return; |
| users[SEED_ADMIN.email] = { |
| email: SEED_ADMIN.email, name: SEED_ADMIN.name, pwd: hash(SEED_ADMIN.pwd), |
| created: Date.now(), stars: 0, mastered: 0, grade: 1, isAdmin: true, |
| }; |
| writeUsers(users); |
| })(); |
|
|
| function cachedMe() { |
| try { return JSON.parse(localStorage.getItem(CACHED_ME_KEY) || 'null'); } catch { return null; } |
| } |
| function setCachedMe(u) { |
| if (u) localStorage.setItem(CACHED_ME_KEY, JSON.stringify(u)); |
| else localStorage.removeItem(CACHED_ME_KEY); |
| } |
|
|
| |
| |
| |
| export const Auth = { |
| |
| session() { return getToken() || localStorage.getItem(SESSION_KEY); }, |
|
|
| |
| current() { |
| const cached = cachedMe(); |
| if (cached) return cached; |
| |
| const e = localStorage.getItem(SESSION_KEY); |
| if (!e) return null; |
| return readUsers()[e] || null; |
| }, |
|
|
| |
| isAdmin() { |
| const me = this.current(); |
| return !!(me && me.isAdmin); |
| }, |
|
|
| async register(email, pwd, name) { |
| if (await backendReachable()) { |
| const { token, user } = await request('POST', '/api/register', { email, pwd, name }); |
| setToken(token); setCachedMe(user); |
| return user; |
| } |
| |
| |
| |
| if (!localFallbackAllowed()) throw new Error(SERVER_UNREACHABLE); |
| |
| email = String(email).trim().toLowerCase(); |
| if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) throw new Error('Enter a valid email'); |
| if (!pwd || pwd.length < 6) throw new Error('Password must be 6+ characters'); |
| const u = readUsers(); |
| if (u[email]) throw new Error('Email already registered — try logging in'); |
| u[email] = { email, name: name || email.split('@')[0], pwd: hash(pwd), |
| created: Date.now(), stars: 0, mastered: 0, grade: 1, |
| isAdmin: ADMIN_EMAILS.has(email) }; |
| writeUsers(u); |
| localStorage.setItem(SESSION_KEY, email); |
| setCachedMe(u[email]); |
| return u[email]; |
| }, |
|
|
| async login(email, pwd) { |
| if (await backendReachable()) { |
| const { token, user } = await request('POST', '/api/login', { email, pwd }); |
| setToken(token); setCachedMe(user); |
| return user; |
| } |
| |
| |
| if (!localFallbackAllowed()) throw new Error(SERVER_UNREACHABLE); |
| email = String(email).trim().toLowerCase(); |
| const u = readUsers(); |
| if (!u[email]) throw new Error('No account with that email'); |
| if (u[email].pwd !== hash(pwd)) throw new Error('Wrong password'); |
| if (ADMIN_EMAILS.has(email) && !u[email].isAdmin) { u[email].isAdmin = true; writeUsers(u); } |
| localStorage.setItem(SESSION_KEY, email); |
| setCachedMe(u[email]); |
| return u[email]; |
| }, |
|
|
| async logout() { |
| try { if (await backendReachable()) await request('POST', '/api/logout'); } catch {} |
| setToken(null); |
| setCachedMe(null); |
| localStorage.removeItem(SESSION_KEY); |
| }, |
|
|
| |
| async refreshMe() { |
| if (!this.session()) return null; |
| if (await backendReachable()) { |
| try { |
| const me = await request('GET', '/api/me'); |
| setCachedMe(me); |
| return me; |
| } catch { return this.current(); } |
| } |
| return this.current(); |
| }, |
|
|
| async saveProgress({ stars, mastered, grade }) { |
| if (await backendReachable()) { |
| try { await request('POST', '/api/me/progress', { stars, mastered, grade }); } catch {} |
| } |
| const me = cachedMe(); |
| if (me) { setCachedMe({ ...me, stars, mastered, grade }); } |
| const e = localStorage.getItem(SESSION_KEY); |
| if (e) { const u = readUsers(); if (u[e]) { u[e] = { ...u[e], stars, mastered, grade }; writeUsers(u); } } |
| }, |
|
|
| |
| |
| async fetchWords() { |
| if (!this.session()) return []; |
| if (!(await backendReachable())) return []; |
| try { |
| const { words } = await request('GET', '/api/me/words'); |
| return Array.isArray(words) ? words : []; |
| } catch { return []; } |
| }, |
|
|
| |
| async saveWords(words) { |
| if (!Array.isArray(words) || !words.length) return; |
| if (!this.session()) return; |
| if (!(await backendReachable())) return; |
| try { await request('POST', '/api/me/words', { words }); } catch {} |
| }, |
|
|
| |
| allUsers() { return Object.values(readUsers()); }, |
| update(patch) { |
| const e = localStorage.getItem(SESSION_KEY); |
| if (!e) return; |
| const u = readUsers(); |
| if (!u[e]) return; |
| u[e] = { ...u[e], ...patch }; |
| writeUsers(u); |
| setCachedMe(u[e]); |
| }, |
| resetPwd(email, newPwd) { |
| email = String(email).trim().toLowerCase(); |
| const u = readUsers(); |
| if (!u[email]) throw new Error('No account with that email'); |
| if (!newPwd || newPwd.length < 6) throw new Error('New password must be 6+ chars'); |
| u[email].pwd = hash(newPwd); |
| writeUsers(u); |
| return true; |
| }, |
|
|
| |
| async adminListUsers() { |
| if (await backendReachable()) return request('GET', '/api/admin/users'); |
| return Object.values(readUsers()).map(u => ({ ...u, online: false })); |
| }, |
| async adminDeleteUser(email) { |
| if (await backendReachable()) return request('DELETE', `/api/admin/users/${encodeURIComponent(email)}`); |
| email = String(email).toLowerCase(); |
| const u = readUsers(); |
| if (email === localStorage.getItem(SESSION_KEY)) throw new Error('Cannot delete the currently logged-in admin'); |
| if (!u[email]) throw new Error('No such user'); |
| delete u[email]; writeUsers(u); |
| }, |
| async adminSetAdmin(email, on) { |
| if (await backendReachable()) return request('POST', `/api/admin/users/${encodeURIComponent(email)}/admin`, { isAdmin: !!on }); |
| email = String(email).toLowerCase(); |
| const u = readUsers(); |
| if (!u[email]) throw new Error('No such user'); |
| u[email].isAdmin = !!on; writeUsers(u); |
| }, |
| async adminResetProgress(email) { |
| if (await backendReachable()) return request('POST', `/api/admin/users/${encodeURIComponent(email)}/reset`); |
| email = String(email).toLowerCase(); |
| const u = readUsers(); |
| if (!u[email]) throw new Error('No such user'); |
| u[email].stars = 0; u[email].mastered = 0; u[email].grade = 1; writeUsers(u); |
| }, |
| async adminCreateUser({ email, pwd, name, isAdmin }) { |
| if (await backendReachable()) return request('POST', '/api/admin/users', { email, pwd, name, isAdmin }); |
| email = String(email).trim().toLowerCase(); |
| if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) throw new Error('Enter a valid email'); |
| if (!pwd || pwd.length < 6) throw new Error('Password must be 6+ characters'); |
| const u = readUsers(); |
| if (u[email]) throw new Error('Email already registered'); |
| u[email] = { email, name: name || email.split('@')[0], pwd: hash(pwd), |
| created: Date.now(), stars: 0, mastered: 0, grade: 1, isAdmin: !!isAdmin }; |
| writeUsers(u); |
| }, |
| async adminExport() { |
| if (await backendReachable()) return JSON.stringify(await request('GET', '/api/admin/export'), null, 2); |
| return JSON.stringify({ users: readUsers(), exportedAt: Date.now() }, null, 2); |
| }, |
| async adminImport(json) { |
| let data; |
| try { data = JSON.parse(json); } catch { throw new Error('Invalid JSON'); } |
| if (await backendReachable()) { |
| |
| const arr = Array.isArray(data.users) ? data.users : |
| data.users && typeof data.users === 'object' ? Object.values(data.users) : null; |
| if (!arr) throw new Error('Missing users array'); |
| const r = await request('POST', '/api/admin/import', { users: arr }); |
| return r.count; |
| } |
| if (!data || typeof data.users !== 'object') throw new Error('Missing "users" object'); |
| writeUsers(data.users); |
| return Object.keys(data.users).length; |
| }, |
| }; |
|
|