2026-02-14 21:29:10 -08:00
|
|
|
const CLIENT_ID = '{{.env.CLIENT_ID}}';
|
2026-02-14 21:11:25 -08:00
|
|
|
|
|
|
|
|
function getProfile() {
|
|
|
|
|
const data = localStorage.getItem('profile');
|
|
|
|
|
return data ? JSON.parse(data) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setProfile(profile) {
|
|
|
|
|
localStorage.setItem('profile', JSON.stringify(profile));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function logout() {
|
|
|
|
|
localStorage.removeItem('profile');
|
|
|
|
|
location.reload();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function api(method, path, body) {
|
|
|
|
|
const profile = getProfile();
|
|
|
|
|
const opts = {
|
|
|
|
|
method,
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if (profile?.token) {
|
|
|
|
|
opts.headers['Authorization'] = 'Bearer ' + profile.token;
|
|
|
|
|
}
|
|
|
|
|
if (body !== undefined) {
|
|
|
|
|
opts.body = JSON.stringify(body);
|
|
|
|
|
}
|
|
|
|
|
const res = await fetch(path, opts);
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(await res.text());
|
|
|
|
|
}
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bind(data) {
|
|
|
|
|
document.querySelectorAll('[data-bind]').forEach(el => {
|
|
|
|
|
const key = el.dataset.bind;
|
|
|
|
|
const value = key.split('.').reduce((o, k) => o?.[k], data);
|
|
|
|
|
if (el.tagName === 'IMG') {
|
|
|
|
|
el.src = value;
|
|
|
|
|
} else {
|
|
|
|
|
el.textContent = value;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const googleReady = new Promise((resolve) => {
|
|
|
|
|
const script = document.createElement('script');
|
|
|
|
|
script.src = 'https://accounts.google.com/gsi/client';
|
|
|
|
|
script.onload = resolve;
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export async function init() {
|
|
|
|
|
let profile = getProfile();
|
|
|
|
|
if (profile) {
|
|
|
|
|
bind(profile);
|
|
|
|
|
return profile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await googleReady;
|
|
|
|
|
|
|
|
|
|
const signin = document.getElementById('signin');
|
|
|
|
|
signin.style.display = 'flex';
|
|
|
|
|
|
|
|
|
|
profile = await new Promise((resolve) => {
|
|
|
|
|
google.accounts.id.initialize({
|
|
|
|
|
client_id: CLIENT_ID,
|
|
|
|
|
callback: async (response) => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/auth/google/callback', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
|
|
|
body: 'credential=' + encodeURIComponent(response.credential)
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`server returned ${res.status}: ${await res.text()}`);
|
|
|
|
|
}
|
|
|
|
|
const profile = await res.json();
|
|
|
|
|
setProfile(profile);
|
|
|
|
|
signin.style.display = 'none';
|
|
|
|
|
resolve(profile);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('sign-in callback error:', err);
|
|
|
|
|
alert('Sign-in failed: ' + err.message);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
error_callback: (err) => {
|
|
|
|
|
console.error('google sign-in error:', err);
|
|
|
|
|
alert('Google sign-in error: ' + (err.message || err.type || JSON.stringify(err)));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const buttonContainer = document.createElement('div');
|
|
|
|
|
signin.appendChild(buttonContainer);
|
|
|
|
|
|
|
|
|
|
google.accounts.id.renderButton(buttonContainer, {
|
|
|
|
|
type: 'standard',
|
|
|
|
|
theme: 'filled_black',
|
|
|
|
|
size: 'large',
|
|
|
|
|
text: 'sign_in_with',
|
|
|
|
|
shape: 'pill',
|
|
|
|
|
logo_alignment: 'left'
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
bind(profile);
|
|
|
|
|
return profile;
|
|
|
|
|
}
|