330 lines
8.6 KiB
TypeScript
330 lines
8.6 KiB
TypeScript
/**
|
||
* Nakama认证服务
|
||
* 处理用户登录、注册、会话管理等功能
|
||
*/
|
||
|
||
import { Client, Session } from '@heroiclabs/nakama-js';
|
||
|
||
export interface LoginCredentials {
|
||
email?: string;
|
||
username?: string;
|
||
password: string;
|
||
}
|
||
|
||
export interface RegisterCredentials {
|
||
email: string;
|
||
username: string;
|
||
password: string;
|
||
displayName?: string;
|
||
}
|
||
|
||
export interface UserProfile {
|
||
id: string;
|
||
username: string;
|
||
displayName: string;
|
||
email?: string;
|
||
avatarUrl?: string;
|
||
createTime: string;
|
||
updateTime: string;
|
||
}
|
||
|
||
export interface AuthState {
|
||
isAuthenticated: boolean;
|
||
user: UserProfile | null;
|
||
session: Session | null;
|
||
loading: boolean;
|
||
error: string | null;
|
||
}
|
||
|
||
class NakamaAuthService {
|
||
private client: Client;
|
||
private session: Session | null = null;
|
||
private readonly SERVER_KEY = 'defaultkey'; // 替换为实际的服务器密钥
|
||
private readonly HOST = '43.143.58.201'; // 替换为实际的Nakama服务器地址
|
||
private readonly PORT = '7350'; // 替换为实际的端口
|
||
private readonly USE_SSL = true; // 根据实际情况设置
|
||
|
||
constructor() {
|
||
this.client = new Client(this.SERVER_KEY, this.HOST, this.PORT, this.USE_SSL);
|
||
this.loadStoredSession();
|
||
}
|
||
|
||
/**
|
||
* 从本地存储加载会话
|
||
*/
|
||
private loadStoredSession(): void {
|
||
try {
|
||
const storedSession = localStorage.getItem('nakama_session');
|
||
if (storedSession) {
|
||
const sessionData = JSON.parse(storedSession);
|
||
this.session = Session.restore(
|
||
sessionData.token,
|
||
sessionData.refresh_token,
|
||
sessionData.username,
|
||
sessionData.user_id,
|
||
sessionData.created,
|
||
sessionData.expires_at,
|
||
sessionData.vars
|
||
);
|
||
|
||
// 检查会话是否过期
|
||
if (this.session.isexpired(Date.now() / 1000)) {
|
||
this.refreshSession();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load stored session:', error);
|
||
this.clearStoredSession();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存会话到本地存储
|
||
*/
|
||
private saveSession(session: Session): void {
|
||
try {
|
||
const sessionData = {
|
||
token: session.token,
|
||
refresh_token: session.refresh_token,
|
||
username: session.username,
|
||
user_id: session.user_id,
|
||
created: session.created,
|
||
expires_at: session.expires_at,
|
||
vars: session.vars
|
||
};
|
||
localStorage.setItem('nakama_session', JSON.stringify(sessionData));
|
||
this.session = session;
|
||
} catch (error) {
|
||
console.error('Failed to save session:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除本地存储的会话
|
||
*/
|
||
private clearStoredSession(): void {
|
||
localStorage.removeItem('nakama_session');
|
||
this.session = null;
|
||
}
|
||
|
||
/**
|
||
* 刷新会话
|
||
*/
|
||
private async refreshSession(): Promise<void> {
|
||
if (!this.session?.refresh_token) {
|
||
throw new Error('No refresh token available');
|
||
}
|
||
|
||
try {
|
||
const newSession = await this.client.sessionRefresh(this.session);
|
||
this.saveSession(newSession);
|
||
} catch (error) {
|
||
console.error('Failed to refresh session:', error);
|
||
this.clearStoredSession();
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户登录
|
||
*/
|
||
async login(credentials: LoginCredentials): Promise<UserProfile> {
|
||
try {
|
||
let session: Session;
|
||
|
||
if (credentials.email) {
|
||
// 邮箱登录
|
||
session = await this.client.authenticateEmail(credentials.email, credentials.password);
|
||
} else if (credentials.username) {
|
||
// 用户名登录
|
||
session = await this.client.authenticateUsername(credentials.username, credentials.password);
|
||
} else {
|
||
throw new Error('Email or username is required');
|
||
}
|
||
|
||
this.saveSession(session);
|
||
|
||
// 获取用户信息
|
||
const account = await this.client.getAccount(session);
|
||
|
||
return {
|
||
id: account.user?.id || '',
|
||
username: account.user?.username || '',
|
||
displayName: account.user?.display_name || account.user?.username || '',
|
||
email: account.email,
|
||
avatarUrl: account.user?.avatar_url,
|
||
createTime: account.user?.create_time || '',
|
||
updateTime: account.user?.update_time || ''
|
||
};
|
||
} catch (error) {
|
||
console.error('Login failed:', error);
|
||
throw new Error(this.getErrorMessage(error));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户注册
|
||
*/
|
||
async register(credentials: RegisterCredentials): Promise<UserProfile> {
|
||
try {
|
||
const session = await this.client.authenticateEmail(
|
||
credentials.email,
|
||
credentials.password,
|
||
true, // create account if not exists
|
||
credentials.username,
|
||
credentials.displayName
|
||
);
|
||
|
||
this.saveSession(session);
|
||
|
||
// 获取用户信息
|
||
const account = await this.client.getAccount(session);
|
||
|
||
return {
|
||
id: account.user?.id || '',
|
||
username: account.user?.username || '',
|
||
displayName: account.user?.display_name || credentials.displayName || credentials.username,
|
||
email: account.email,
|
||
avatarUrl: account.user?.avatar_url,
|
||
createTime: account.user?.create_time || '',
|
||
updateTime: account.user?.update_time || ''
|
||
};
|
||
} catch (error) {
|
||
console.error('Registration failed:', error);
|
||
throw new Error(this.getErrorMessage(error));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户登出
|
||
*/
|
||
async logout(): Promise<void> {
|
||
try {
|
||
if (this.session) {
|
||
await this.client.sessionLogout(this.session);
|
||
}
|
||
} catch (error) {
|
||
console.error('Logout error:', error);
|
||
} finally {
|
||
this.clearStoredSession();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前用户信息
|
||
*/
|
||
async getCurrentUser(): Promise<UserProfile | null> {
|
||
if (!this.session) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
// 检查会话是否过期
|
||
if (this.session.isexpired(Date.now() / 1000)) {
|
||
await this.refreshSession();
|
||
}
|
||
|
||
const account = await this.client.getAccount(this.session);
|
||
|
||
return {
|
||
id: account.user?.id || '',
|
||
username: account.user?.username || '',
|
||
displayName: account.user?.display_name || account.user?.username || '',
|
||
email: account.email,
|
||
avatarUrl: account.user?.avatar_url,
|
||
createTime: account.user?.create_time || '',
|
||
updateTime: account.user?.update_time || ''
|
||
};
|
||
} catch (error) {
|
||
console.error('Failed to get current user:', error);
|
||
this.clearStoredSession();
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查是否已登录
|
||
*/
|
||
isAuthenticated(): boolean {
|
||
return this.session !== null && !this.session.isexpired(Date.now() / 1000);
|
||
}
|
||
|
||
/**
|
||
* 获取当前会话
|
||
*/
|
||
getSession(): Session | null {
|
||
return this.session;
|
||
}
|
||
|
||
/**
|
||
* 更新用户资料
|
||
*/
|
||
async updateProfile(updates: Partial<Pick<UserProfile, 'displayName' | 'avatarUrl'>>): Promise<void> {
|
||
if (!this.session) {
|
||
throw new Error('Not authenticated');
|
||
}
|
||
|
||
try {
|
||
await this.client.updateAccount(this.session, {
|
||
display_name: updates.displayName,
|
||
avatar_url: updates.avatarUrl
|
||
});
|
||
} catch (error) {
|
||
console.error('Failed to update profile:', error);
|
||
throw new Error(this.getErrorMessage(error));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 修改密码
|
||
*/
|
||
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
||
if (!this.session) {
|
||
throw new Error('Not authenticated');
|
||
}
|
||
|
||
try {
|
||
// 首先验证当前密码
|
||
const account = await this.client.getAccount(this.session);
|
||
if (account.email) {
|
||
await this.client.authenticateEmail(account.email, currentPassword);
|
||
}
|
||
|
||
// 更新密码(这里需要根据Nakama的实际API调整)
|
||
// Nakama可能需要通过其他方式更新密码
|
||
console.warn('Password change not implemented - requires server-side function');
|
||
throw new Error('Password change requires server-side implementation');
|
||
} catch (error) {
|
||
console.error('Failed to change password:', error);
|
||
throw new Error(this.getErrorMessage(error));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取错误消息
|
||
*/
|
||
private getErrorMessage(error: any): string {
|
||
if (error?.message) {
|
||
return error.message;
|
||
}
|
||
|
||
// 根据Nakama错误码返回友好的错误消息
|
||
switch (error?.code) {
|
||
case 3: // INVALID_ARGUMENT
|
||
return '输入参数无效';
|
||
case 5: // NOT_FOUND
|
||
return '用户不存在';
|
||
case 6: // ALREADY_EXISTS
|
||
return '用户已存在';
|
||
case 16: // UNAUTHENTICATED
|
||
return '用户名或密码错误';
|
||
default:
|
||
return '操作失败,请稍后重试';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出单例实例
|
||
export const nakamaAuth = new NakamaAuthService();
|
||
export default nakamaAuth;
|