/**
* Auth Module — 用户认证系统
* 邮箱注册/登录 + Apple Sign-In + JWT 管理 + 订阅状态
*/
import { aiChatSetAuth } from './ai-chat.js';
import { t, getLang } from './i18n.js';
// ============================================================================
// 配置
// ============================================================================
const API_BASE = ''; // 同域部署,留空;Capacitor 打包时改为服务器地址
const TOKEN_KEY = 'jyotish_auth_token';
const USER_KEY = 'jyotish_auth_user';
// ============================================================================
// 状态
// ============================================================================
let _token = null;
let _user = null;
let _modalEl = null;
let _onAuthChange = null; // 外部回调
// ============================================================================
// Token / 用户持久化
// ============================================================================
function loadSavedAuth() {
try {
_token = localStorage.getItem(TOKEN_KEY);
const raw = localStorage.getItem(USER_KEY);
_user = raw ? JSON.parse(raw) : null;
} catch { _token = null; _user = null; }
}
function saveAuth(token, user) {
_token = token;
_user = user;
if (token) {
localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(USER_KEY, JSON.stringify(user));
} else {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
}
// 同步给 ai-chat
aiChatSetAuth(token, user);
// 通知外部
if (_onAuthChange) _onAuthChange(user);
// 更新 UI
updateHeaderUI();
}
// ============================================================================
// 公共 API
// ============================================================================
export function getToken() { return _token; }
export function getUser() { return _user; }
export function isLoggedIn() { return !!_token && !!_user; }
export function getApiBase() {
// Capacitor 环境用服务器地址
if (window.Capacitor?.isNativePlatform?.()) {
return localStorage.getItem('jyotish_api_base') || 'https://your-server.com';
}
return API_BASE;
}
export function onAuthChange(cb) { _onAuthChange = cb; }
// ============================================================================
// 初始化
// ============================================================================
export function initAuth() {
loadSavedAuth();
createModal();
updateHeaderUI();
// 绑定 header 按钮
document.addEventListener('click', e => {
const btn = e.target.closest('[data-auth-action]');
if (!btn) return;
const action = btn.dataset.authAction;
if (action === 'open-login') openModal('login');
else if (action === 'open-register') openModal('register');
else if (action === 'logout') doLogout();
else if (action === 'open-profile') openModal('profile');
});
// 如果有 token,验证一下
if (_token) {
fetchUser().catch(() => {
// token 过期,清除
saveAuth(null, null);
});
}
// 同步给 ai-chat
aiChatSetAuth(_token, _user);
}
// ============================================================================
// Header UI 更新
// ============================================================================
function updateHeaderUI() {
const containers = document.querySelectorAll('[data-auth-header]');
containers.forEach(el => {
if (_user) {
const initial = (_user.email || 'U')[0].toUpperCase();
const sub = _user.subscription || {};
const planLabel = sub.plan === 'premium' ? '∞' : `${sub.todayUsage ?? 0}/3`;
el.innerHTML = `
${planLabel}
`;
} else {
el.innerHTML = `
`;
}
});
}
// ============================================================================
// 模态框
// ============================================================================
function createModal() {
_modalEl = document.createElement('div');
_modalEl.className = 'auth-modal-overlay';
_modalEl.id = 'auth-modal';
_modalEl.innerHTML = `
`;
document.body.appendChild(_modalEl);
_modalEl.querySelector('#auth-modal-close').addEventListener('click', closeModal);
_modalEl.addEventListener('click', e => {
if (e.target === _modalEl) closeModal();
});
}
function openModal(view = 'login') {
if (!_modalEl) return;
const body = _modalEl.querySelector('#auth-modal-body');
if (view === 'login') renderLoginView(body);
else if (view === 'register') renderRegisterView(body);
else if (view === 'profile') renderProfileView(body);
else if (view === 'forgot') renderForgotView(body);
_modalEl.classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeModal() {
if (!_modalEl) return;
_modalEl.classList.remove('open');
document.body.style.overflow = '';
}
// ============================================================================
// Login 视图
// ============================================================================
function renderLoginView(container) {
container.innerHTML = `
${t('auth.or')}
`;
bindLoginForm(container);
bindAppleLogin(container);
bindAuthLinks(container);
}
function bindLoginForm(container) {
const form = container.querySelector('#auth-login-form');
const errEl = container.querySelector('#auth-login-error');
const btnText = container.querySelector('#auth-login-btn .btn-text');
const btnLoading = container.querySelector('#auth-login-btn .btn-loading');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const email = container.querySelector('#auth-login-email').value.trim();
const password = container.querySelector('#auth-login-password').value;
if (!email || !password) return;
btnText.classList.add('hidden');
btnLoading.classList.remove('hidden');
errEl.classList.add('hidden');
try {
const res = await apiPost('/api/auth/login', { email, password });
saveAuth(res.token, res.user);
closeModal();
} catch (err) {
errEl.textContent = err.message || t('auth.login.failed');
errEl.classList.remove('hidden');
} finally {
btnText.classList.remove('hidden');
btnLoading.classList.add('hidden');
}
});
}
// ============================================================================
// Register 视图
// ============================================================================
function renderRegisterView(container) {
container.innerHTML = `
${t('auth.or')}
`;
bindRegisterForm(container);
bindAppleLogin(container);
bindAuthLinks(container);
}
function bindRegisterForm(container) {
const form = container.querySelector('#auth-register-form');
const errEl = container.querySelector('#auth-register-error');
const btnText = container.querySelector('#auth-register-btn .btn-text');
const btnLoading = container.querySelector('#auth-register-btn .btn-loading');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const email = container.querySelector('#auth-register-email').value.trim();
const password = container.querySelector('#auth-register-password').value;
const password2 = container.querySelector('#auth-register-password2').value;
if (password !== password2) {
errEl.textContent = t('auth.pw.mismatch');
errEl.classList.remove('hidden');
return;
}
if (password.length < 6) {
errEl.textContent = t('auth.pw.short');
errEl.classList.remove('hidden');
return;
}
btnText.classList.add('hidden');
btnLoading.classList.remove('hidden');
errEl.classList.add('hidden');
try {
const res = await apiPost('/api/auth/register', { email, password });
saveAuth(res.token, res.user);
closeModal();
} catch (err) {
errEl.textContent = err.message || t('auth.register.failed');
errEl.classList.remove('hidden');
} finally {
btnText.classList.remove('hidden');
btnLoading.classList.add('hidden');
}
});
}
// ============================================================================
// Apple Sign-In
// ============================================================================
function bindAppleLogin(container) {
container.querySelectorAll('[id^="auth-apple-"]').forEach(btn => {
btn.addEventListener('click', async () => {
try {
// Capacitor 原生环境
if (window.Capacitor?.Plugins?.SignInWithApple) {
const result = await window.Capacitor.Plugins.SignInWithApple.authorize({
clientId: localStorage.getItem('jyotish_apple_bundle_id') || 'com.jyotish.app',
redirectURI: window.location.origin,
scopes: 'email name',
});
await handleAppleToken(result.identityToken || result.idToken);
} else {
// Web 环境 - 使用 Apple JS SDK
if (!window.AppleID) {
alert(t('auth.apple.device'));
return;
}
// Web 方式由 Apple JS SDK 处理
const data = await window.AppleID.auth.signIn();
await handleAppleToken(data.authorization.id_token);
}
} catch (err) {
console.error('[Auth] Apple Sign-In error:', err);
if (err.message !== 'popup_closed_by_user') {
alert(t('auth.apple.failed') + (err.message || t('auth.error.unknown')));
}
}
});
});
}
async function handleAppleToken(idToken) {
try {
const res = await apiPost('/api/auth/apple', { idToken });
saveAuth(res.token, res.user);
closeModal();
} catch (err) {
alert(t('auth.apple.failed') + (err.message || t('auth.error.unknown')));
}
}
// ============================================================================
// Profile 视图
// ============================================================================
function renderProfileView(container) {
if (!_user) { closeModal(); return; }
const sub = _user.subscription || {};
const isPremium = sub.plan === 'premium';
const usage = sub.todayUsage ?? 0;
const limit = sub.limit ?? 3;
container.innerHTML = `
${!isPremium ? `
${t('auth.upgrade.title')}
- ${t('auth.upgrade.1')}
- ${t('auth.upgrade.2')}
- ${t('auth.upgrade.3')}
- ${t('auth.upgrade.4')}
` : ''}
`;
// 升级按钮
const upgradeBtn = container.querySelector('#auth-upgrade-btn');
if (upgradeBtn) {
upgradeBtn.addEventListener('click', () => {
// 触发 IAP(由 subscription.js 处理)
const event = new CustomEvent('jyotish:subscribe');
document.dispatchEvent(event);
});
}
bindAuthLinks(container);
}
// ============================================================================
// Forgot Password 视图(占位)
// ============================================================================
function renderForgotView(container) {
container.innerHTML = `
`;
bindAuthLinks(container);
}
// ============================================================================
// 视图间跳转链接
// ============================================================================
function bindAuthLinks(container) {
container.querySelectorAll('[data-auth-action]').forEach(el => {
// 避免重复绑定
if (el._authBound) return;
el._authBound = true;
el.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const action = el.dataset.authAction;
if (action === 'open-login') openModal('login');
else if (action === 'open-register') openModal('register');
else if (action === 'logout') { doLogout(); closeModal(); }
else if (action === 'open-profile') openModal('profile');
});
});
}
// ============================================================================
// Logout
// ============================================================================
function doLogout() {
saveAuth(null, null);
closeModal();
}
// ============================================================================
// API 调用
// ============================================================================
async function apiPost(path, body) {
const base = getApiBase();
const headers = { 'Content-Type': 'application/json' };
if (_token) headers['Authorization'] = `Bearer ${_token}`;
const resp = await fetch(`${base}${path}`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
const data = await resp.json();
if (!resp.ok) {
throw new Error(data.error || data.message || t('auth.request.failed') + ` (${resp.status})`);
}
return data;
}
async function fetchUser() {
const base = getApiBase();
const resp = await fetch(`${base}/api/auth/me`, {
headers: { 'Authorization': `Bearer ${_token}` },
});
if (!resp.ok) throw new Error(t('auth.token.expired'));
const data = await resp.json();
// 更新本地用户数据
_user = data.user || data;
localStorage.setItem(USER_KEY, JSON.stringify(_user));
updateHeaderUI();
aiChatSetAuth(_token, _user);
return _user;
}
// 暴露 apiPost 给其他模块使用
export { apiPost, fetchUser };