284299b946
Complete Flutter app (Android + iOS) mirroring the web frontend: - Core: Riverpod state, Dio networking with auth interceptor + auto-refresh, go_router navigation, flutter_secure_storage, light/dark theme with MedievalSharp/Crimson Pro fonts, German l10n - Market: search with text/GPS/radius/date/sort filters, list + map views (flutter_map + OSM), detail screen with opening hours, admission prices, single-marker map, pagination - Auth: login (password + magic link tabs), register, OAuth button placeholders, 2FA code prompt on 401, sealed auth state provider - User: profile view/edit/delete with confirm dialog, 2FA setup/disable on security screen - GPS: geolocator with IP-based fallback (geojs.io) matching web behavior - Platform: Android internet + location permissions, iOS NSLocation description - Tests: date/currency/distance formatter unit tests (13 passing) - Zero analysis issues, debug APK builds successfully
100 lines
2.8 KiB
Dart
100 lines
2.8 KiB
Dart
import 'package:dio/dio.dart';
|
|
import '../../../core/config/api_config.dart';
|
|
import '../../../core/storage/token_storage.dart';
|
|
import '../domain/models/auth_data.dart';
|
|
import '../domain/models/login_request.dart';
|
|
import '../domain/models/register_request.dart';
|
|
import '../domain/models/totp_setup_data.dart';
|
|
|
|
class AuthApi {
|
|
final Dio _dio;
|
|
final TokenStorage _tokenStorage;
|
|
|
|
AuthApi(this._dio, this._tokenStorage);
|
|
|
|
Future<AuthData> register(RegisterRequest request) async {
|
|
final response = await _dio.post(
|
|
'${ApiConfig.apiPrefix}/auth/register',
|
|
data: request.toJson(),
|
|
);
|
|
final data = AuthData.fromJson(
|
|
(response.data as Map<String, dynamic>)['data'] as Map<String, dynamic>,
|
|
);
|
|
await _tokenStorage.saveTokens(
|
|
accessToken: data.accessToken,
|
|
sessionToken: data.sessionToken,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
Future<AuthData> login(LoginRequest request) async {
|
|
final response = await _dio.post(
|
|
'${ApiConfig.apiPrefix}/auth/login',
|
|
data: request.toJson(),
|
|
);
|
|
final data = AuthData.fromJson(
|
|
(response.data as Map<String, dynamic>)['data'] as Map<String, dynamic>,
|
|
);
|
|
await _tokenStorage.saveTokens(
|
|
accessToken: data.accessToken,
|
|
sessionToken: data.sessionToken,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
Future<void> logout() async {
|
|
final sessionToken = await _tokenStorage.getSessionToken();
|
|
if (sessionToken != null) {
|
|
await _dio.post(
|
|
'${ApiConfig.apiPrefix}/auth/logout',
|
|
options: Options(headers: {'X-Session-Token': sessionToken}),
|
|
);
|
|
}
|
|
await _tokenStorage.clearTokens();
|
|
}
|
|
|
|
Future<AuthData> refresh() async {
|
|
final sessionToken = await _tokenStorage.getSessionToken();
|
|
final response = await _dio.post(
|
|
'${ApiConfig.apiPrefix}/auth/refresh',
|
|
options: Options(headers: {'X-Session-Token': sessionToken}),
|
|
);
|
|
final data = AuthData.fromJson(
|
|
(response.data as Map<String, dynamic>)['data'] as Map<String, dynamic>,
|
|
);
|
|
await _tokenStorage.saveTokens(
|
|
accessToken: data.accessToken,
|
|
sessionToken: data.sessionToken,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
Future<void> sendMagicLink(String email) async {
|
|
await _dio.post(
|
|
'${ApiConfig.apiPrefix}/auth/magic-link',
|
|
data: {'email': email},
|
|
);
|
|
}
|
|
|
|
Future<TotpSetupData> setupTotp() async {
|
|
final response = await _dio.post('${ApiConfig.apiPrefix}/auth/2fa/setup');
|
|
return TotpSetupData.fromJson(
|
|
(response.data as Map<String, dynamic>)['data'] as Map<String, dynamic>,
|
|
);
|
|
}
|
|
|
|
Future<void> verifyTotp(String code) async {
|
|
await _dio.post(
|
|
'${ApiConfig.apiPrefix}/auth/2fa/verify',
|
|
data: {'code': code},
|
|
);
|
|
}
|
|
|
|
Future<void> disableTotp(String code) async {
|
|
await _dio.delete(
|
|
'${ApiConfig.apiPrefix}/auth/2fa',
|
|
data: {'code': code},
|
|
);
|
|
}
|
|
}
|