Initial clean project import
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* 前端认证服务
|
||||
* 负责与后端 IPC 通信,处理用户注册、登录、Token 管理等
|
||||
*/
|
||||
|
||||
const AUTH_TOKEN_KEY = 'auth_token';
|
||||
const AUTH_USER_KEY = 'auth_user';
|
||||
const AUTH_SUBSCRIPTION_KEY = 'auth_subscription';
|
||||
const AUTH_SESSION_FLAG = 'auth_session_active';
|
||||
const AUTH_REMEMBER_ME_KEY = 'auth_remember_me';
|
||||
|
||||
// 🔧 应用启动时清除登录状态(确保每次启动都需要重新登录)
|
||||
// 使用 sessionStorage 标记当前会话,如果没有标记说明是新启动
|
||||
// 💡 新增:如果用户选择了"记住我",则不清除登录状态
|
||||
if (typeof window !== 'undefined' && typeof sessionStorage !== 'undefined') {
|
||||
const isSessionActive = sessionStorage.getItem(AUTH_SESSION_FLAG);
|
||||
const rememberMe = localStorage.getItem(AUTH_REMEMBER_ME_KEY);
|
||||
|
||||
if (!isSessionActive) {
|
||||
// 检查是否勾选了"记住我"
|
||||
if (rememberMe !== 'true') {
|
||||
// 新会话,且用户未选择记住我,清除之前的登录状态
|
||||
console.log('[Auth] 新会话启动,清除之前的登录状态');
|
||||
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||
localStorage.removeItem(AUTH_USER_KEY);
|
||||
localStorage.removeItem(AUTH_SUBSCRIPTION_KEY);
|
||||
} else {
|
||||
console.log('[Auth] 新会话启动,但用户选择了记住我,保留登录状态');
|
||||
}
|
||||
// 标记当前会话
|
||||
sessionStorage.setItem(AUTH_SESSION_FLAG, 'true');
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthResult {
|
||||
success: boolean;
|
||||
token?: string;
|
||||
user?: any;
|
||||
subscription?: any;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface VerifyResult {
|
||||
valid: boolean;
|
||||
payload?: any;
|
||||
kicked?: boolean;
|
||||
message?: string;
|
||||
apiUnavailable?: boolean;
|
||||
}
|
||||
|
||||
interface RegisterData {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
invite_code?: string;
|
||||
}
|
||||
|
||||
interface LoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 window.$mapi 是否可用
|
||||
*/
|
||||
const ensureApiAvailable = async (): Promise<boolean> => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (typeof window !== 'undefined' && window['$mapi'] && window['$mapi'].auth) {
|
||||
return true;
|
||||
}
|
||||
if (i === 0) {
|
||||
const keys = typeof window !== 'undefined' ? Object.keys(window).filter(k => k.startsWith('$') || k.startsWith('__')) : [];
|
||||
console.warn(`[Auth.ensureApiAvailable] window.$mapi 不存在, 等待重试 (${i + 1}/10), window特殊键:`, keys);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
console.error('[Auth.ensureApiAvailable] 等待超时,window.$mapi 仍未就绪');
|
||||
return false;
|
||||
};
|
||||
|
||||
const clearAuthStorage = () => {
|
||||
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||
localStorage.removeItem(AUTH_USER_KEY);
|
||||
localStorage.removeItem(AUTH_SUBSCRIPTION_KEY);
|
||||
};
|
||||
|
||||
const refreshRemoteConfigAfterAuth = async () => {
|
||||
try {
|
||||
const systemConfig = await import('./systemConfig');
|
||||
systemConfig.clearConfigCache();
|
||||
const config = await systemConfig.fetchSystemConfig();
|
||||
(window as any).__systemConfig = config;
|
||||
} catch (error) {
|
||||
console.warn('[Auth] 刷新渲染进程系统配置失败:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window['$mapi']?.runninghub?.refreshConfig?.();
|
||||
if (result && !result.success) {
|
||||
console.warn('[Auth] 刷新主进程 RunningHub 配置失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Auth] 刷新主进程 RunningHub 配置异常:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
const register = async (data: RegisterData): Promise<AuthResult> => {
|
||||
try {
|
||||
if (!await ensureApiAvailable()) {
|
||||
return {
|
||||
success: false,
|
||||
message: '认证 API 不可用'
|
||||
};
|
||||
}
|
||||
|
||||
const result = await window['$mapi'].auth.register(data);
|
||||
|
||||
if (result.success && result.token) {
|
||||
// 保存 Token 和用户信息
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, result.token);
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(result.user));
|
||||
localStorage.setItem(AUTH_SUBSCRIPTION_KEY, JSON.stringify(result.subscription));
|
||||
await refreshRemoteConfigAfterAuth();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '注册失败'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
const login = async (data: LoginData): Promise<AuthResult> => {
|
||||
try {
|
||||
if (!await ensureApiAvailable()) {
|
||||
return {
|
||||
success: false,
|
||||
message: '认证 API 不可用'
|
||||
};
|
||||
}
|
||||
|
||||
const result = await window['$mapi'].auth.login(data);
|
||||
|
||||
if (result.success && result.token) {
|
||||
// 保存 Token 和用户信息
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, result.token);
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(result.user));
|
||||
localStorage.setItem(AUTH_SUBSCRIPTION_KEY, JSON.stringify(result.subscription));
|
||||
|
||||
// 💡 保存"记住我"的选择
|
||||
if (data.rememberMe) {
|
||||
localStorage.setItem(AUTH_REMEMBER_ME_KEY, 'true');
|
||||
localStorage.setItem('auth_saved_email', data.email);
|
||||
localStorage.setItem('auth_saved_password', data.password);
|
||||
console.log('[Auth] 用户选择了记住我,已保存登录状态');
|
||||
} else {
|
||||
localStorage.removeItem(AUTH_REMEMBER_ME_KEY);
|
||||
localStorage.removeItem('auth_saved_email');
|
||||
localStorage.removeItem('auth_saved_password');
|
||||
console.log('[Auth] 用户未选择记住我,登录状态仅保留到应用关闭');
|
||||
}
|
||||
await refreshRemoteConfigAfterAuth();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '登录失败'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
const logout = async () => {
|
||||
try {
|
||||
clearAuthStorage();
|
||||
localStorage.removeItem(AUTH_REMEMBER_ME_KEY);
|
||||
localStorage.removeItem('auth_saved_email');
|
||||
localStorage.removeItem('auth_saved_password');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '已登出'
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '登出失败'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取存储的 Token
|
||||
*/
|
||||
const getToken = (): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取存储的用户信息
|
||||
*/
|
||||
const getUser = (): any | null => {
|
||||
try {
|
||||
const userStr = localStorage.getItem(AUTH_USER_KEY);
|
||||
return userStr ? JSON.parse(userStr) : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取存储的订阅信息
|
||||
*/
|
||||
const getSubscription = (): any | null => {
|
||||
try {
|
||||
const subStr = localStorage.getItem(AUTH_SUBSCRIPTION_KEY);
|
||||
return subStr ? JSON.parse(subStr) : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证 Token 有效性
|
||||
*/
|
||||
const verifyToken = async (token?: string): Promise<VerifyResult> => {
|
||||
try {
|
||||
if (!await ensureApiAvailable()) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '认证 API 不可用',
|
||||
apiUnavailable: true,
|
||||
};
|
||||
}
|
||||
|
||||
const tokenToVerify = token || getToken();
|
||||
if (!tokenToVerify) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Token 不存在'
|
||||
};
|
||||
}
|
||||
|
||||
const result = await window['$mapi'].auth.verifyToken(tokenToVerify);
|
||||
|
||||
if (!result.valid) {
|
||||
clearAuthStorage();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
valid: false,
|
||||
message: error.message || 'Token 验证失败'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前用户信息(从后端)
|
||||
*/
|
||||
const getCurrentUser = async (token?: string) => {
|
||||
try {
|
||||
if (!await ensureApiAvailable()) {
|
||||
return {
|
||||
success: false,
|
||||
message: '认证 API 不可用'
|
||||
};
|
||||
}
|
||||
|
||||
const tokenToUse = token || getToken();
|
||||
if (!tokenToUse) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Token 不存在'
|
||||
};
|
||||
}
|
||||
|
||||
const result = await window['$mapi'].auth.getCurrentUser(tokenToUse);
|
||||
|
||||
if (result.success) {
|
||||
// 更新本地缓存
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(result.user));
|
||||
localStorage.setItem(AUTH_SUBSCRIPTION_KEY, JSON.stringify(result.subscription));
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '获取用户信息失败'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查订阅状态
|
||||
*/
|
||||
const checkSubscriptionStatus = async (userId?: number) => {
|
||||
try {
|
||||
if (!await ensureApiAvailable()) {
|
||||
return {
|
||||
hasAccess: false,
|
||||
isExpired: true
|
||||
};
|
||||
}
|
||||
|
||||
// 如果没有提供 userId,从本地用户信息获取
|
||||
const user = userId ? { id: userId } : getUser();
|
||||
if (!user || !user.id) {
|
||||
return {
|
||||
hasAccess: false,
|
||||
isExpired: true,
|
||||
message: '用户不存在'
|
||||
};
|
||||
}
|
||||
|
||||
const result = await window['$mapi'].auth.checkSubscriptionStatus(user.id);
|
||||
|
||||
if (result.subscription) {
|
||||
localStorage.setItem(AUTH_SUBSCRIPTION_KEY, JSON.stringify(result.subscription));
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
hasAccess: false,
|
||||
isExpired: true,
|
||||
message: error.message || '检查订阅状态失败'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户是否已登录
|
||||
*/
|
||||
const isAuthenticated = (): boolean => {
|
||||
return !!getToken() && !!getUser();
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户是否为管理员
|
||||
*/
|
||||
const isAdmin = (): boolean => {
|
||||
const user = getUser();
|
||||
return user?.role === 'admin';
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否可以访问应用(已登录 + 订阅未过期)
|
||||
*/
|
||||
const canAccess = async (): Promise<boolean> => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const verifyResult = await verifyToken(token);
|
||||
if (!verifyResult.valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await getCurrentUser(token);
|
||||
if (!result.success || !result.subscription) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expiresAt = new Date(result.subscription.expires_at);
|
||||
const now = new Date();
|
||||
return now <= expiresAt;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取剩余天数
|
||||
*/
|
||||
const getRemainingDays = (): number => {
|
||||
const subscription = getSubscription();
|
||||
if (!subscription) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const expiresAt = new Date(subscription.expires_at);
|
||||
const now = new Date();
|
||||
const diffTime = expiresAt.getTime() - now.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays > 0 ? diffDays : 0;
|
||||
};
|
||||
|
||||
export default {
|
||||
register,
|
||||
login,
|
||||
logout,
|
||||
getToken,
|
||||
getUser,
|
||||
getSubscription,
|
||||
verifyToken,
|
||||
getCurrentUser,
|
||||
checkSubscriptionStatus,
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
canAccess,
|
||||
getRemainingDays
|
||||
};
|
||||
Reference in New Issue
Block a user