// 开发模式自动登录(健壮版:过期/缺失自动刷新,并同步写入 expires) (function () { const DEV_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc4NTgxMjQwNywiZXhwIjoxODE3MzQ4NDA3fQ.LRNg1pvDbiyU68jp-r5Axt44Mhu3wn3G79-vHEqyPzQ'; // 解析 JWT 的 exp(毫秒);无法解析返回 0 function jwtExpMs(t) { try { const p = JSON.parse(atob(t.split('.')[1])); return p && p.exp ? p.exp * 1000 : 0; } catch (e) { return 0; } } function tokenValid(t) { if (!t) return false; const exp = jwtExpMs(t); return exp > Date.now(); } const cur = localStorage.getItem('ycat_token'); if (!tokenValid(cur)) { // 现有 token 缺失或已过期 → 用开发 token 覆盖 localStorage.setItem('ycat_token', DEV_TOKEN); localStorage.setItem('ycat_token_expires', String(jwtExpMs(DEV_TOKEN))); localStorage.setItem('ycat_user', JSON.stringify({ id: 1, username: 'admin', email: 'admin@ycat.com', role: 'admin' })); console.log('[DEV] 自动登录为管理员'); } else if (!localStorage.getItem('ycat_token_expires')) { // token 有效但缺 expires → 补齐,避免 AUTH.getToken() 误判为未登录 localStorage.setItem('ycat_token_expires', String(jwtExpMs(cur))); } })(); /** * YCat API Config * Centralized API base URL configuration */ const API = { // Change this to your backend URL when deploying BASE_URL: window.location.origin + '/api', async request(path, options = {}) { const token = localStorage.getItem('ycat_token'); const headers = { 'Content-Type': 'application/json', ...options.headers }; if (token) { headers['Authorization'] = 'Bearer ' + token; } // 显示 Loading(如果全局 Loading 可用) const showLoading = options.showLoading !== false; // 默认显示 if (showLoading && window.Loading) { Loading.show('请求中...'); } try { const url = this.BASE_URL + path; const res = await fetch(url, { ...options, headers }); const data = await res.json(); if (showLoading && window.Loading) { Loading.hide(); } // Auto toast for errors if (!data.success && data.message && window.Toast) { Toast.error(data.message); } return data; } catch (error) { if (showLoading && window.Loading) { Loading.hide(); } if (window.Toast) { Toast.error('网络请求失败,请检查网络连接'); } throw error; } }, // Auth register(data) { return this.request('/auth/register', { method: 'POST', body: JSON.stringify(data) }); }, login(data) { return this.request('/auth/login', { method: 'POST', body: JSON.stringify(data) }); }, me() { return this.request('/auth/me'); }, quota() { return this.request('/auth/quota'); }, // Articles listArticles() { return this.request('/articles'); }, getArticle(id) { return this.request(`/articles/${id}`); }, createArticle(data) { return this.request('/articles', { method: 'POST', body: JSON.stringify(data) }); }, updateArticle(id, data) { return this.request(`/articles/${id}`, { method: 'PUT', body: JSON.stringify(data) }); }, deleteArticle(id) { return this.request(`/articles/${id}`, { method: 'DELETE' }); }, duplicateArticle(id) { return this.request(`/articles/${id}/duplicate`, { method: 'POST' }); }, getTrashList() { return this.request('/articles/trash/list'); }, restoreArticle(id) { return this.request(`/articles/trash/${id}/restore`, { method: 'POST' }); }, permanentDeleteArticle(id) { return this.request(`/articles/trash/${id}`, { method: 'DELETE' }); }, emptyTrash() { return this.request('/articles/trash/empty', { method: 'DELETE' }); }, // Batch operations batchDelete(ids) { return this.request('/articles/batch/delete', { method: 'POST', body: JSON.stringify({ ids }) }); }, batchPermanentDelete(ids) { return this.request('/articles/batch', { method: 'DELETE', body: JSON.stringify({ ids }) }); }, batchStatus(ids, status) { return this.request('/articles/batch/status', { method: 'PUT', body: JSON.stringify({ ids, status }) }); }, batchCategory(ids, category) { return this.request('/articles/batch/category', { method: 'PUT', body: JSON.stringify({ ids, category }) }); }, batchRestore(ids) { return this.request('/articles/batch/restore', { method: 'POST', body: JSON.stringify({ ids }) }); }, // Publish getPlatforms() { return this.request('/publish/platforms'); }, getOauthUrl(platform) { return this.request('/publish/platforms/oauth-url?platform=' + encodeURIComponent(platform)); }, authorizePlatform(platform, code) { return this.request('/publish/platforms/authorize', { method: 'POST', body: JSON.stringify({ platform, code }) }); }, revokePlatform(platform) { return this.request('/publish/platforms/revoke', { method: 'POST', body: JSON.stringify({ platform }) }); }, // 用户自有平台账号(字流/AiToEarn 模式:用你自己的账号登录授权) getAccounts() { return this.request('/publish/accounts'); }, addAccount(platform, accountName, authorize) { return this.request('/publish/accounts/add', { method: 'POST', body: JSON.stringify({ platform, accountName, authorize: authorize ? 1 : 0 }) }); }, authorizeAccount(payload) { // payload: { id } 或 { platform, accountName } return this.request('/publish/accounts/authorize', { method: 'POST', body: JSON.stringify(payload) }); }, removeAccount(id) { return this.request('/publish/accounts/remove', { method: 'POST', body: JSON.stringify({ id }) }); }, publish(data) { return this.request('/publish', { method: 'POST', body: JSON.stringify(data) }); }, getPublishHistory() { return this.request('/publish/history'); }, getPublishRecords(articleId) { return this.request(`/publish/records/${articleId}`); }, // Subscription upgrade(plan) { return this.request('/auth/upgrade', { method: 'POST', body: JSON.stringify({ plan }) }); }, subscription() { return this.request('/auth/subscription'); }, // Profile & Settings updateProfile(data) { return this.request('/auth/profile', { method: 'PUT', body: JSON.stringify(data) }); }, updatePassword(data) { return this.request('/auth/password', { method: 'PUT', body: JSON.stringify(data) }); }, regenerateApiKey() { return this.request('/auth/regenerate-api-key', { method: 'POST' }); }, deleteAllArticles() { return this.request('/articles/all', { method: 'DELETE' }); }, deleteAccount() { return this.request('/auth/account', { method: 'DELETE' }); }, // AI generateTitles(data) { return this.request('/ai/titles', { method: 'POST', body: JSON.stringify(data) }); }, generateSummary(data) { return this.request('/ai/summary', { method: 'POST', body: JSON.stringify(data) }); }, platformSummary(data) { return this.request('/ai/platform-summary', { method: 'POST', body: JSON.stringify(data) }); }, analyzeContent(data) { return this.request('/ai/analyze', { method: 'POST', body: JSON.stringify(data) }); }, seoSuggestions(data) { return this.request('/ai/seo', { method: 'POST', body: JSON.stringify(data) }); }, convertFormat(data) { return this.request('/ai/convert', { method: 'POST', body: JSON.stringify(data) }); }, polishContent(data) { return this.request('/ai/polish', { method: 'POST', body: JSON.stringify(data) }); }, expandContent(data) { return this.request('/ai/expand', { method: 'POST', body: data }); }, condenseContent(data) { return this.request('/ai/condense', { method: 'POST', body: data }); }, suggestPostingTime(data) { return this.request('/ai/posting-time', { method: 'POST', body: data }); }, importFromUrl(data) { return this.request('/ai/import-url', { method: 'POST', body: data }); }, // Versions getVersions(articleId) { return this.request(`/articles/${articleId}/versions`); }, getVersionDetail(articleId, versionId) { return this.request(`/articles/${articleId}/versions/${versionId}`); }, restoreVersion(articleId, versionId) { return this.request(`/articles/${articleId}/versions/${versionId}/restore`, { method: 'POST' }); }, createSnapshot(articleId) { return this.request(`/articles/${articleId}/snapshot`, { method: 'POST' }); }, // Dashboard dashboardOverview() { return this.request('/dashboard/overview'); }, dashboardPlatforms() { return this.request('/dashboard/platforms'); }, dashboardArticles(limit = 10) { return this.request(`/dashboard/articles?limit=${limit}`); }, dashboardTrend(range = '7d') { return this.request(`/dashboard/trend?range=${range}`); }, // Analytics analyticsOverview(days = 30) { return this.request(`/analytics/overview?days=${days}`); }, analyticsPlatforms(days = 14) { return this.request(`/analytics/platforms?days=${days}`); }, analyticsArticles(limit = 10) { return this.request(`/analytics/articles?limit=${limit}`); }, analyticsFollowers(days = 30) { return this.request(`/analytics/followers?days=${days}`); }, analyticsBestTimes() { return this.request('/analytics/best-times'); }, analyticsRefresh() { return this.request('/analytics/refresh', { method: 'POST' }); }, // Comments getComments(articleId) { return this.request(`/articles/${articleId}/comments`); }, addComment(articleId, data) { return this.request(`/articles/${articleId}/comments`, { method: 'POST', body: JSON.stringify(data) }); }, deleteComment(articleId, commentId) { return this.request(`/comments/${commentId}`, { method: 'DELETE' }); }, // Health health() { return this.request('/health'); }, // Markdown importMarkdown(data) { return this.request('/markdown/import', { method: 'POST', body: JSON.stringify(data) }); }, exportMarkdown(articleId) { return this.request(`/markdown/export/${articleId}`, { method: 'POST' }); }, convertMarkdown(data) { return this.request('/markdown/convert', { method: 'POST', body: JSON.stringify(data) }); }, // Scheduled Publish createScheduledPublish(data) { return this.request('/scheduled-publish/schedule', { method: 'POST', body: JSON.stringify(data) }); }, getScheduledPublishes(params = {}) { const query = new URLSearchParams(params).toString(); return this.request(`/scheduled-publish/list${query ? '?' + query : ''}`); }, cancelScheduledPublish(id) { return this.request(`/scheduled-publish/cancel/${id}`, { method: 'DELETE' }); }, retryScheduledPublish(id) { return this.request(`/scheduled-publish/retry/${id}`, { method: 'POST' }); }, // SEO Metadata getSeoMeta(articleId) { return this.request(`/seo/${articleId}`); }, updateSeoMeta(articleId, seo) { return this.request(`/seo/${articleId}`, { method: 'PUT', body: JSON.stringify(seo) }); }, generateSeoMeta(articleId) { return this.request(`/seo/generate/${articleId}`, { method: 'POST' }); }, previewSeoCard(articleId) { return this.request(`/seo/preview/${articleId}`); }, // Notifications getNotifications(params = {}) { const query = new URLSearchParams(params).toString(); return this.request(`/notifications${query ? '?' + query : ''}`); }, getUnreadCount() { return this.request('/notifications/unread-count'); }, markNotifRead(id) { return this.request(`/notifications/${id}/read`, { method: 'PUT' }); }, markAllNotifRead() { return this.request('/notifications/read-all', { method: 'PUT' }); }, deleteNotif(id) { return this.request(`/notifications/${id}`, { method: 'DELETE' }); }, clearReadNotifs() { return this.request('/notifications/read/clear', { method: 'DELETE' }); }, // Templates getTemplates(category = 'all') { return this.request(`/templates?category=${category}`); }, getTemplate(id) { return this.request(`/templates/${id}`); }, createTemplate(data) { return this.request('/templates', { method: 'POST', body: JSON.stringify(data) }); }, updateTemplate(id, data) { return this.request(`/templates/${id}`, { method: 'PUT', body: JSON.stringify(data) }); }, deleteTemplate(id) { return this.request(`/templates/${id}`, { method: 'DELETE' }); }, useTemplate(id) { return this.request(`/templates/${id}/use`, { method: 'POST' }); }, saveArticleAsTemplate(data) { return this.request('/templates/save-article', { method: 'POST', body: JSON.stringify(data) }); }, // Series (专题) listSeries() { return this.request('/series'); }, getSeries(id) { return this.request(`/series/${id}`); }, createSeries(data) { return this.request('/series', { method: 'POST', body: JSON.stringify(data) }); }, updateSeries(id, data) { return this.request(`/series/${id}`, { method: 'PUT', body: JSON.stringify(data) }); }, deleteSeries(id) { return this.request(`/series/${id}`, { method: 'DELETE' }); }, addArticleToSeries(seriesId, articleId) { return this.request(`/series/${seriesId}/articles`, { method: 'POST', body: JSON.stringify({ article_id: articleId }) }); }, removeArticleFromSeries(seriesId, articleId) { return this.request(`/series/${seriesId}/articles/${articleId}`, { method: 'DELETE' }); }, reorderSeriesArticles(seriesId, articleIds) { return this.request(`/series/${seriesId}/articles/reorder`, { method: 'PUT', body: JSON.stringify({ article_ids: articleIds }) }); }, getSeriesStats() { return this.request('/series/stats/summary'); }, }; /** * YCat Auth Module — Backend API driven authentication * Replaces the old localStorage-only MVP with JWT-backed API * Maintains backward-compatible API with the old AUTH object */ const AUTH = (() => { // --- Token Management --- function getToken() { const token = localStorage.getItem('ycat_token'); const expires = localStorage.getItem('ycat_token_expires'); if (!token || !expires) return null; if (Date.now() > parseInt(expires)) { localStorage.removeItem('ycat_token'); localStorage.removeItem('ycat_token_expires'); localStorage.removeItem('ycat_user'); return null; } return token; } // 返回 Authorization 请求头(供 fetch 鉴权) function getAuthHeader() { const token = getToken(); return token ? 'Bearer ' + token : ''; } function setToken(token) { // JWT token, expires in 7 days localStorage.setItem('ycat_token', token); localStorage.setItem('ycat_token_expires', String(Date.now() + 7 * 24 * 60 * 60 * 1000)); } function clearToken() { localStorage.removeItem('ycat_token'); localStorage.removeItem('ycat_token_expires'); localStorage.removeItem('ycat_user'); } // --- User Cache --- function getCachedUser() { try { const u = localStorage.getItem('ycat_user'); return u ? JSON.parse(u) : null; } catch { return null; } } function cacheUser(user) { if (user) { localStorage.setItem('ycat_user', JSON.stringify(user)); } } // --- Core API --- async function register({ username, email, password }) { // Client-side validation if (!username || username.trim().length < 2) { return { ok: false, error: '用户名至少2个字符' }; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return { ok: false, error: '邮箱格式不正确' }; } if (!password || password.length < 6) { return { ok: false, error: '密码至少6位' }; } try { const res = await API.register({ username, email, password }); if (res.ok) { setToken(res.token); cacheUser(res.user); } return res; } catch (err) { return { ok: false, error: '网络错误,请检查服务器是否启动' }; } } async function login({ email, password }) { if (!email || !password) { return { ok: false, error: '请填写邮箱和密码' }; } try { const res = await API.login({ email, password }); if (res.ok) { setToken(res.token); cacheUser(res.user); } return res; } catch (err) { return { ok: false, error: '网络错误,请检查服务器是否启动' }; } } function logout() { clearToken(); } function currentUser() { return getCachedUser(); } function isLoggedIn() { return getToken() !== null; } // --- Auth Guard --- function requireAuth(redirectUrl = 'login.html') { if (!isLoggedIn()) { window.location.href = redirectUrl; return false; } return true; } function redirectIfLoggedIn(redirectUrl = 'editor.html') { if (isLoggedIn()) { window.location.href = redirectUrl; return true; } return false; } // --- Quota --- function getPlan() { const user = currentUser(); return user ? user.plan : 'free'; } function getArticleQuota() { const plan = getPlan(); const quotas = { free: 5, basic: 50, pro: 999999, enterprise: 999999 }; return quotas[plan] || 5; } function canPublishArticle() { const user = currentUser(); if (!user) return false; const quota = getArticleQuota(); return user.articles_used < quota; } function incrementArticleCount() { const user = currentUser(); if (!user) return; user.articles_used = (user.articles_used || 0) + 1; cacheUser(user); } // --- Refresh user data from server --- async function refreshUser() { try { const res = await API.me(); if (res.ok) { cacheUser(res.user); return res.user; } return null; } catch { return null; } } function debugInfo() { return { token: getToken() ? 'exists' : 'none', currentUser: currentUser(), isLoggedIn: isLoggedIn(), }; } return { getToken, getAuthHeader, register, login, logout, currentUser, isLoggedIn, requireAuth, redirectIfLoggedIn, getPlan, getArticleQuota, canPublishArticle, incrementArticleCount, refreshUser, debugInfo, }; })(); // 显式挂到 window,确保跨脚本(editor-core.js 等)能访问真实 AUTH/API if (typeof window !== 'undefined') { window.AUTH = AUTH; window.API = API; }