← Назад/**
* Auth UI — Login/Register/Google, JWT token management, user state
*/
const authUI = (() => {
// --- State ---
let currentUser = null
let token = localStorage.getItem('fs_token') || null
// --- DOM refs ---
const authBtn = document.getElementById('authBtn')
const authModal = document.getElementById('authModal')
const authModalClose = document.getElementById('authModalClose')
const authOverlay = authModal.querySelector('.auth-modal-overlay')
const authError = document.getElementById('authError')
const loginForm = document.getElementById('loginForm')
const registerForm = document.getElementById('registerForm')
const authTabs = authModal.querySelectorAll('.auth-tab')
const googleAuthBtn = document.getElementById('googleAuthBtn')
// --- API helper ---
async function api(path, opts = {}) {
const headers = { 'Content-Type': 'application/json', ...opts.headers }
if (token) headers['Authorization'] = `Bearer ${token}`
const res = await fetch(path, { ...opts, headers })
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Request failed')
return data
}
// Expose for other modules
function getToken() { return token }
function getUser() { return currentUser }
function isLoggedIn() { return !!currentUser }
function isPro() { return currentUser && (currentUser.tier === 'pro' || currentUser.tier === 'admin') }
// --- Auth-aware fetch wrapper (used by other modules) ---
function authFetch(url, opts = {}) {
const headers = { ...opts.headers }
if (token) headers['Authorization'] = `Bearer ${token}`
return fetch(url, { ...opts, headers })
}
// --- UI Updates ---
function updateAuthButton() {
if (currentUser) {
const initial = (currentUser.name || currentUser.email)[0].toUpperCase()
const tierBadge = currentUser.tier === 'pro' ? ' PRO' : currentUser.tier === 'admin' ? ' ADM' : ''
authBtn.innerHTML = `<span class="auth-avatar">${initial}</span>${tierBadge ? `<span class="auth-tier-badge ${currentUser.tier}">${tierBadge}</span>` : ''}`
authBtn.title = `${currentUser.name || currentUser.email} (${currentUser.tier})`
authBtn.classList.add('logged-in')
} else {
authBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`
authBtn.title = 'Login / Register'
authBtn.classList.remove('logged-in')
}
}
function showError(msg) {
authError.textContent = msg
authError.classList.remove('hidden')
setTimeout(() => authError.classList.add('hidden'), 5000)
}
function openModal() {
authModal.classList.remove('hidden')
if (currentUser) {
// Show profile/logout instead of login
showProfile()
} else {
loginForm.classList.remove('hidden')
registerForm.classList.add('hidden')
authTabs[0].classList.add('active')
authTabs[1].classList.remove('active')
}
}
function closeModal() {
authModal.classList.add('hidden')
authError.classList.add('hidden')
}
function showProfile() {
// Replace forms with profile view
const box = authModal.querySelector('.auth-modal-box')
const existingProfile = box.querySelector('.auth-profile')
if (existingProfile) existingProfile.remove()
const profileHtml = document.createElement('div')
profileHtml.className = 'auth-profile'
profileHtml.innerHTML = `
<div class="auth-profile-info">
<div class="auth-profile-avatar">${(currentUser.name || currentUser.email)[0].toUpperCase()}</div>
<div class="auth-profile-details">
<div class="auth-profile-name">${currentUser.name || 'User'}</div>
<div class="auth-profile-email">${currentUser.email}</div>
<div class="auth-profile-tier">Plan: <span class="${currentUser.tier}">${currentUser.tier.toUpperCase()}</span></div>
</div>
</div>
<button class="auth-logout-btn" id="logoutBtn">Logout</button>
`
// Hide forms, tabs, divider, google
loginForm.classList.add('hidden')
registerForm.classList.add('hidden')
authModal.querySelector('.auth-modal-tabs').classList.add('hidden')
authModal.querySelector('.auth-divider').classList.add('hidden')
googleAuthBtn.classList.add('hidden')
box.insertBefore(profileHtml, authModal.querySelector('.auth-divider'))
profileHtml.querySelector('#logoutBtn').addEventListener('click', () => {
logout()
closeModal()
})
}
// --- Auth Actions ---
async function tryAutoLogin() {
if (!token) return
try {
const data = await api('/api/auth/me')
currentUser = data.user
updateAuthButton()
console.log('[Auth] Auto-login:', currentUser.email, currentUser.tier)
} catch (e) {
// Token expired/invalid
token = null
localStorage.removeItem('fs_token')
console.log('[Auth] Token expired, cleared')
}
}
function saveToken(t) {
token = t
lsSet('fs_token', t)
}
function logout() {
token = null
currentUser = null
localStorage.removeItem('fs_token')
updateAuthButton()
// Restore modal to login state
const profile = authModal.querySelector('.auth-profile')
if (profile) profile.remove()
authModal.querySelector('.auth-modal-tabs').classList.remove('hidden')
authModal.querySelector('.auth-divider').classList.remove('hidden')
googleAuthBtn.classList.remove('hidden')
console.log('[Auth] Logged out')
}
// --- Event Listeners ---
authBtn.addEventListener('click', openModal)
authModalClose.addEventListener('click', closeModal)
authOverlay.addEventListener('click', closeModal)
// Tab switching
authTabs.forEach(tab => {
tab.addEventListener('click', () => {
const target = tab.dataset.authTab
authTabs.forEach(t => t.classList.remove('active'))
tab.classList.add('active')
loginForm.classList.toggle('hidden', target !== 'login')
registerForm.classList.toggle('hidden', target !== 'register')
authError.classList.add('hidden')
})
})
// Login
loginForm.addEventListener('submit', async (e) => {
e.preventDefault()
const email = document.getElementById('loginEmail').value.trim()
const password = document.getElementById('loginPassword').value
try {
const data = await api('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
saveToken(data.token)
currentUser = data.user
updateAuthButton()
closeModal()
loginForm.reset()
} catch (err) {
showError(err.message)
}
})
// Register
registerForm.addEventListener('submit', async (e) => {
e.preventDefault()
const name = document.getElementById('regName').value.trim()
const email = document.getElementById('regEmail').value.trim()
const password = document.getElementById('regPassword').value
const confirm = document.getElementById('regConfirm').value
if (password !== confirm) {
showError('Passwords do not match')
return
}
try {
const data = await api('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, name })
})
saveToken(data.token)
currentUser = data.user
updateAuthButton()
closeModal()
registerForm.reset()
} catch (err) {
showError(err.message)
}
})
// Google OAuth
googleAuthBtn.addEventListener('click', async () => {
try {
const data = await api('/api/auth/google/url')
if (data.url) {
window.location.href = data.url
} else {
showError('Google OAuth not configured yet')
}
} catch (err) {
showError('Google OAuth not available')
}
})
// Handle Google OAuth callback (if redirected back with ?code=)
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has('code')) {
const code = urlParams.get('code')
api('/api/auth/google/callback', {
method: 'POST',
body: JSON.stringify({ code })
}).then(data => {
saveToken(data.token)
currentUser = data.user
updateAuthButton()
// Clean URL
window.history.replaceState({}, '', window.location.pathname)
}).catch(err => {
console.error('[Auth] Google callback error:', err)
})
}
// Keyboard: Escape closes modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !authModal.classList.contains('hidden')) {
closeModal()
}
})
// --- Init ---
tryAutoLogin()
// --- Tier Gating ---
// Shows PRO badge on locked features. In dev mode everything is unlocked.
// Set authUI.prodMode = true before launch to enforce gates.
let prodMode = false
/**
* Check if feature is available. If not, shows login or upgrade prompt.
* @param {string} feature - feature name for logging
* @param {boolean} requiresPro - true if PRO only
* @returns {boolean} true if allowed
*/
function gateCheck(feature, requiresPro = false) {
// Dev mode: everything unlocked
if (!prodMode) return true
if (requiresPro && !isPro()) {
if (!isLoggedIn()) {
openModal()
} else {
// Show upgrade prompt
showUpgradeHint(feature)
}
return false
}
return true
}
function showUpgradeHint(feature) {
const existing = document.querySelector('.pro-upgrade-toast')
if (existing) existing.remove()
const toast = document.createElement('div')
toast.className = 'pro-upgrade-toast'
toast.innerHTML = `🔒 <b>${feature}</b> requires PRO plan`
document.body.appendChild(toast)
setTimeout(() => toast.remove(), 3000)
}
// --- Public API ---
return {
getToken,
getUser,
isLoggedIn,
isPro,
authFetch,
openModal,
logout,
gateCheck,
get prodMode() { return prodMode },
set prodMode(v) { prodMode = v },
}
})()