import 'package:dio/dio.dart'; import '../../../errors/auth_exception.dart'; import '../../auth/token_storage.dart'; import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_endpoint.dart'; /// Probes that the stored bearer token still maps to the given credentials. /// Server returns 200 only when the credentials belong to the user that the /// token was issued for — a password rotation on that user's account flips /// it to 401 even if the token itself would still be accepted. /// /// Bypasses the shared dio singleton so the auth interceptor doesn't kick in /// and obscure a real 401 with a silent re-login. class AuthVerify { static const Duration _connectTimeout = Duration(seconds: 10); static const Duration _receiveTimeout = Duration(seconds: 15); final MarianumConnectTokenStorage _tokenStorage; final Dio _dio; AuthVerify({ MarianumConnectTokenStorage tokenStorage = const MarianumConnectTokenStorage(), Dio? dio, }) : _tokenStorage = tokenStorage, _dio = dio ?? Dio( BaseOptions( connectTimeout: _connectTimeout, sendTimeout: _connectTimeout, receiveTimeout: _receiveTimeout, responseType: ResponseType.json, contentType: 'application/json', ), ); /// Throws [AuthException] on 401 (credentials no longer match the token's /// user, token missing, or token rejected), other [AppException]s on /// network/server errors. Completes silently on success. Future run({ required String username, required String password, }) async { final token = await _tokenStorage.readToken(); if (token == null || token.isEmpty) { throw AuthException.unauthorized( technicalDetails: 'AuthVerify: no bearer token in storage', ); } try { await _dio.post( MarianumConnectEndpoint.resolve('auth/verify'), data: {'username': username, 'password': password}, options: Options(headers: {'Authorization': 'Bearer $token'}), ); } on DioException catch (e) { throw mapMarianumConnectError(e); } } }