Files
vikingowl 284299b946 feat: implement full Flutter MVP app
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
2026-02-21 07:10:30 +01:00

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},
);
}
}