chat / public /js /auth.js
incognitolm
Fix JS Folder
deb39b7
Raw History Blame
8.67 kB
// auth.js - Authentication state and Supabase integration
import { send, on } from './ws.js';
const SUPABASE_URL = 'https://dpixehhdbtzsbckfektd.supabase.co';
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRwaXhlaGhkYnR6c2Jja2Zla3RkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjExNDI0MjcsImV4cCI6MjA3NjcxODQyN30.nR1KCSRQj1E_evQWnE2VaZzg7PgLp2kqt4eDKP2PkpE';
const AUTH_KEY = 'ipai_auth_v1';
const TEMP_ID_KEY = 'ipai_temp_id';
const CLIENT_ID_KEY= 'ipai_client_id';
export let currentUser = null;
export let userProfile = null;
export let userSettings = null;
export let subscriptionInfo = null;
const authListeners = new Set();
export function onAuthChange(fn) { authListeners.add(fn); return () => authListeners.delete(fn); }
export function isAuthenticated() { return !!currentUser; }
function notifyListeners() {
authListeners.forEach(fn => fn({ currentUser, userProfile, userSettings }));
}
export function getClientId() {
let id = localStorage.getItem(CLIENT_ID_KEY);
if (!id) { id = `web-${crypto.randomUUID()}`; localStorage.setItem(CLIENT_ID_KEY, id); }
return id;
}
export function getTempId() {
let id = localStorage.getItem(TEMP_ID_KEY);
if (!id) { id = crypto.randomUUID(); localStorage.setItem(TEMP_ID_KEY, id); }
return id;
}
export function saveAuth(data) { localStorage.setItem(AUTH_KEY, JSON.stringify(data)); }
export function loadAuth() { try { return JSON.parse(localStorage.getItem(AUTH_KEY) || 'null'); } catch { return null; } }
export function clearAuth() { localStorage.removeItem(AUTH_KEY); }
// ── Supabase REST helpers ─────────────────────────────────────────────────
async function supabaseFetch(path, options = {}) {
const token = options._useToken || SUPABASE_ANON_KEY;
delete options._useToken;
const res = await fetch(`${SUPABASE_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
...(options.headers || {}),
},
});
return res.json();
}
export async function loginWithEmail(email, password) {
const data = await supabaseFetch('/auth/v1/token?grant_type=password', {
method: 'POST', body: JSON.stringify({ email, password }),
});
if (data.error) throw new Error(data.error_description || data.error);
await handleSupabaseSession(data);
return data;
}
export async function signUpWithEmail(email, password) {
const data = await supabaseFetch('/auth/v1/signup', {
method: 'POST', body: JSON.stringify({ email, password }),
});
if (data.error) throw new Error(data.error_description || data.error);
if (data.access_token) await handleSupabaseSession(data);
return data;
}
export async function loginWithOAuth(provider) {
const redirectTo = encodeURIComponent(`${location.origin}/oauth-callback`);
const url = `${SUPABASE_URL}/auth/v1/authorize?provider=${provider}&redirect_to=${redirectTo}`;
window.open(url, '_blank', 'width=520,height=640,noopener');
}
export async function logout() {
const auth = loadAuth();
if (auth?.access_token) {
try {
await supabaseFetch('/auth/v1/logout', {
method: 'POST', _useToken: auth.access_token,
});
} catch {}
}
send({ type: 'auth:logout' });
clearAuth();
currentUser = null; userProfile = null; userSettings = null; subscriptionInfo = null;
notifyListeners();
updateSidebarProfile();
initAsGuest();
}
// ── Session handling ──────────────────────────────────────────────────────
async function handleSupabaseSession(data) {
if (!data.access_token) throw new Error('No access token');
saveAuth({ access_token: data.access_token, refresh_token: data.refresh_token, user: data.user });
return new Promise((resolve, reject) => {
const tempId = getTempId();
const clientId = getClientId();
send({ type: 'auth:login', accessToken: data.access_token, refreshToken: data.refresh_token,
tempId, clientId });
const unsubOk = on('auth:ok', (msg) => { unsubOk(); unsubErr(); applyAuthOk(msg); resolve(msg); });
const unsubErr = on('auth:error', (msg) => { unsubOk(); unsubErr(); reject(new Error(msg.message)); });
setTimeout(() => { unsubOk(); unsubErr(); reject(new Error('Auth timeout')); }, 12000);
});
}
function applyAuthOk(msg) {
currentUser = { id: msg.userId, email: msg.email };
userProfile = msg.profile;
userSettings = msg.settings;
const auth = loadAuth() || {};
saveAuth({ ...auth, userId: msg.userId, deviceToken: msg.deviceToken });
notifyListeners();
updateSidebarProfile();
// Apply saved theme
if (msg.settings?.theme) {
import('./settings.js').then(m => m.applyTheme(msg.settings.theme));
}
}
function initAsGuest() {
send({ type: 'auth:guest', tempId: getTempId() });
}
// ── WS events ─────────────────────────────────────────────────────────────
on('auth:newLogin', (msg) => {
import('./ui.js').then(({ showNotification }) => {
showNotification({
type: 'warning',
message: `New login detected from ${msg.ip || 'unknown location'}`,
action: { label: 'View', onClick: () => import('./settings.js').then(m => m.openSettings('account')) },
duration: 8000,
});
});
});
on('auth:forcedLogout', (msg) => {
import('./ui.js').then(({ showNotification }) => {
showNotification({ type: 'error', message: msg.reason || 'Session revoked', duration: 5000 });
});
setTimeout(() => logout(), 1500);
});
on('settings:updated', (msg) => {
if (msg.settings) {
userSettings = msg.settings;
import('./settings.js').then(m => m.applyTheme(msg.settings.theme));
}
});
// ── Reconnect on WS connect ────────────────────────────────────────────────
on('ws:connected', async () => {
const auth = loadAuth();
if (auth?.access_token) {
try { await handleSupabaseSession(auth); }
catch { clearAuth(); initAsGuest(); }
} else {
initAsGuest();
}
});
// ── OAuth popup message ───────────────────────────────────────────────────
window.addEventListener('message', async (e) => {
if (e.origin !== location.origin) return;
if (e.data?.type !== 'oauth:callback') return;
const { access_token, refresh_token } = e.data;
if (!access_token) return;
try { await handleSupabaseSession({ access_token, refresh_token }); }
catch (err) {
import('./ui.js').then(({ showNotification }) =>
showNotification({ type: 'error', message: `Sign-in failed: ${err.message}`, duration: 4000 }));
}
});
// Also handle if page was opened after OAuth redirect (no popup)
(function checkOAuthRedirect() {
const params = new URLSearchParams(location.search);
const t = params.get('t'), r = params.get('r');
if (params.get('oauth') === '1' && t) {
history.replaceState({}, '', '/');
handleSupabaseSession({ access_token: t, refresh_token: r || '' }).catch(() => {});
}
})();
// ── Sidebar profile ───────────────────────────────────────────────────────
export function updateSidebarProfile() {
const guestEl = document.getElementById('guest-section');
const userEl = document.getElementById('user-section');
const nameEl = document.getElementById('user-name-display');
const planEl = document.getElementById('user-plan-display');
const avatarEl= document.getElementById('user-avatar');
if (!currentUser) {
guestEl?.classList.remove('hidden');
userEl?.classList.add('hidden');
return;
}
guestEl?.classList.add('hidden');
userEl?.classList.remove('hidden');
const username = userProfile?.username || currentUser.email?.split('@')[0] || '?';
if (nameEl) nameEl.textContent = username;
if (avatarEl) avatarEl.textContent = username[0].toUpperCase();
const plan = subscriptionInfo?.planKey || 'free';
const planName = subscriptionInfo?.planName || 'Free';
if (planEl) { planEl.textContent = planName; planEl.setAttribute('data-plan', plan); }
}
on('auth:ok', updateSidebarProfile);
on('auth:loggedOut', updateSidebarProfile);