Files
jizhi/frontend/lib/shared/api/auth_api.dart
T

285 lines
8.7 KiB
Dart

import 'package:dio/dio.dart';
import 'package:miaoji_zhang/shared/api/api_client.dart';
import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
import 'package:miaoji_zhang/shared/services/local_export_service.dart';
import 'package:miaoji_zhang/shared/services/session_store.dart';
import 'package:miaoji_zhang/shared/services/push_service.dart';
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
class AiCompanion {
final String avatarKey;
final String personaKey;
final String? customName;
final int roastLevel, stickerFrequency, proactiveLevel;
AiCompanion.fromJson(Map<String, dynamic> json)
: avatarKey = json['avatarKey'] as String,
personaKey = json['personaKey'] as String,
customName = json['customName'] as String?,
roastLevel = json['roastLevel'] as int,
stickerFrequency = json['stickerFrequency'] as int,
proactiveLevel = json['proactiveLevel'] as int;
}
class UserProfile {
final int userId;
final String username;
final String? nickname;
final String appMode;
final AiCompanion? aiCompanion;
final bool onboardingDone;
final bool aiEnabled;
UserProfile.fromJson(Map<String, dynamic> json)
: userId = (json['userId'] as num).toInt(),
username = json['username'] as String,
nickname = json['nickname'] as String?,
appMode = json['appMode'] as String,
aiCompanion = json['aiCompanion'] == null
? null
: AiCompanion.fromJson(json['aiCompanion'] as Map<String, dynamic>),
onboardingDone = json['onboardingDone'] as bool,
aiEnabled =
((json['permissions'] as Map<String, dynamic>?)?['ai'] as bool?) ??
true;
const UserProfile.local({
required this.userId,
required this.username,
required this.nickname,
required this.appMode,
required this.onboardingDone,
this.aiEnabled = false,
}) : aiCompanion = null;
}
class AuthLoginResult {
final bool accountClosureCancelled;
final UserProfile profile;
const AuthLoginResult({
required this.accountClosureCancelled,
required this.profile,
});
}
class AuthApi {
static final _dio = ApiClient.instance.dio;
static Future<UserProfile> register(
String username,
String password, {
required bool agreedToTerms,
}) async {
final response = await _dio.post(
'/api/auth/register',
data: {
'username': username,
'password': password,
'agreedToTerms': agreedToTerms,
},
);
await ApiClient.instance.saveToken(response.data['token'] as String);
return me(forceRemote: true);
}
static Future<AuthLoginResult> login(String username, String password) async {
final response = await _dio.post(
'/api/auth/login',
data: {'username': username, 'password': password},
);
await ApiClient.instance.saveToken(response.data['token'] as String);
final profile = await me(forceRemote: true);
return AuthLoginResult(
accountClosureCancelled:
response.data['accountClosureCancelled'] as bool? ?? false,
profile: profile,
);
}
static Future<UserProfile> me({bool forceRemote = false}) async {
final session = SessionStore.instance;
if (!forceRemote &&
(session.isGuest ||
(session.isAccount && session.shouldUseLocalOnly))) {
return _localProfile();
}
try {
final response = await _dio.get('/api/users/me');
final profile = UserProfile.fromJson(
response.data as Map<String, dynamic>,
);
await _cacheProfile(profile);
return profile;
} catch (error) {
if (session.isAccount &&
(isConnectivityError(error) || session.needsReauth)) {
return _localProfile();
}
rethrow;
}
}
static Future<UserProfile> completeOnboarding({
required String appMode,
required String avatarKey,
required String personaKey,
String? customName,
}) async {
SessionStore.instance.requireOnline('完成账号初始化需要连接网络');
final response = await _dio.post(
'/api/users/me/onboarding',
data: {
'appMode': appMode,
'avatarKey': avatarKey,
'personaKey': personaKey,
'customName': customName,
},
);
final profile = UserProfile.fromJson(response.data as Map<String, dynamic>);
await _cacheProfile(profile);
return profile;
}
static Future<UserProfile> switchMode(String appMode) async {
SessionStore.instance.requireOnline('切换 AI 模式需要登录并连接网络');
final response = await _dio.put(
'/api/users/me/mode',
data: {'appMode': appMode},
);
final profile = UserProfile.fromJson(response.data as Map<String, dynamic>);
await _cacheProfile(profile);
return profile;
}
static Future<UserProfile> updateCompanion({
String? avatarKey,
String? personaKey,
String? customName,
int? roastLevel,
int? stickerFrequency,
int? proactiveLevel,
}) async {
SessionStore.instance.requireOnline('AI 伙伴设置需要登录并连接网络');
final response = await _dio.put(
'/api/users/me/companion',
data: {
'avatarKey': avatarKey,
'personaKey': personaKey,
'customName': customName,
'roastLevel': roastLevel,
'stickerFrequency': stickerFrequency,
'proactiveLevel': proactiveLevel,
},
);
final profile = UserProfile.fromJson(response.data as Map<String, dynamic>);
await _cacheProfile(profile);
return profile;
}
static Future<UserProfile> updateProfile(String nickname) async {
final session = SessionStore.instance;
final normalized = nickname.trim();
if (session.isGuest) {
await session.updateNickname(normalized);
return _localProfile();
}
session.requireOnline('修改云端资料需要连接网络');
final previous = session.nickname;
await session.updateNickname(normalized);
try {
final response = await _dio.put(
'/api/users/me/profile',
data: {'nickname': normalized},
);
final profile = UserProfile.fromJson(
response.data as Map<String, dynamic>,
);
await _cacheProfile(profile);
return profile;
} catch (_) {
await session.updateNickname(previous);
rethrow;
}
}
static Future<void> changePassword(
String currentPassword,
String newPassword,
) async {
SessionStore.instance.requireOnline('修改密码需要连接网络');
final response = await _dio.put(
'/api/users/me/password',
data: {'currentPassword': currentPassword, 'newPassword': newPassword},
);
await ApiClient.instance.saveToken(response.data['token'] as String);
}
static Future<List<int>> exportData() async {
if (SessionStore.instance.shouldUseLocalOnly) {
return LocalExportService.buildZip();
}
try {
final response = await _dio.get<List<int>>(
'/api/users/me/export',
options: Options(responseType: ResponseType.bytes),
);
return response.data ?? const [];
} catch (error) {
if (isConnectivityError(error)) return LocalExportService.buildZip();
rethrow;
}
}
static Future<DateTime> requestAccountClosure(
String password,
String confirmationText,
) async {
SessionStore.instance.requireOnline('注销账号需要连接网络');
final response = await _dio.post(
'/api/users/me/closure',
data: {'password': password, 'confirmationText': confirmationText},
);
final scheduledAt = ShanghaiTime.parseCivil(
response.data['scheduledAt'] as String,
);
await logout();
return scheduledAt;
}
static Future<void> deleteAccount(String password) async {
SessionStore.instance.requireOnline('注销账号需要连接网络');
await _dio.delete('/api/users/me', data: {'password': password});
await logout();
}
static Future<void> logout() async {
CurrentLedgerStore.instance.clear();
await PushService.instance.logout();
await ApiClient.instance.clearToken();
await SessionStore.instance.clearActiveSession();
}
static UserProfile _localProfile() {
final session = SessionStore.instance;
return UserProfile.local(
userId: session.userId ?? 0,
username: session.username ?? '游客',
nickname: session.nickname,
appMode: session.isGuest ? 'normal' : session.appMode,
onboardingDone: session.isGuest || session.onboardingDone,
aiEnabled: session.aiEnabled,
);
}
static Future<void> _cacheProfile(UserProfile profile) =>
SessionStore.instance.activateAccount(
userId: profile.userId,
username: profile.username,
nickname: profile.nickname,
appMode: profile.appMode,
onboardingDone: profile.onboardingDone,
aiEnabled: profile.aiEnabled,
);
}