Files

86 lines
3.1 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../models/trackers/device_code.dart';
import '../../../utils/abortable_http_request.dart';
import '../../../utils/app_logger.dart';
import '../device_code_auth_service.dart';
import '../oauth_proxy_client.dart';
import '../tracker_constants.dart';
import '../tracker_session.dart';
import 'simkl_constants.dart';
/// Simkl OAuth PIN (device-code) flow.
///
/// `GET /oauth/pin` with Simkl's required app identity parameters and a
/// relay redirect returns a PIN the user enters at https://simkl.com/pin.
/// After entry Simkl redirects the browser to the relay's static "signed in"
/// page. The app polls `/oauth/pin/<user_code>` with the same identity.
class SimklAuthService extends DeviceCodeAuthServiceBase {
SimklAuthService({super.httpClient});
@override
Future<DeviceCode> createDeviceCode() async {
final uri = Uri.parse(
SimklConstants.pinUrl,
).replace(queryParameters: SimklConstants.queryParameters({'redirect': '${OAuthProxyClient.baseUrl}/auth/done'}));
final res = await sendAbortableHttpRequest(
httpClient,
'GET',
uri,
headers: SimklConstants.headers(),
timeout: TrackerConstants.authRequestTimeout,
operation: 'Simkl PIN request',
);
if (res.statusCode != 200) {
throw DeviceCodeAuthFlowException('Simkl PIN request failed: HTTP ${res.statusCode}');
}
final body = json.decode(res.body) as Map<String, dynamic>;
return DeviceCode(
deviceCode: body['device_code'] as String,
userCode: body['user_code'] as String,
verificationUrl: body['verification_url'] as String? ?? SimklConstants.verificationUrl,
// Simkl doesn't expose a prefilled URL; the user manually enters the code.
verificationUrlComplete: null,
expiresIn: (body['expires_in'] as num?)?.toInt() ?? 900,
interval: (body['interval'] as num?)?.toInt() ?? 5,
);
}
@override
Future<DevicePollEvent> probe(DeviceCode code) async {
final pollUri = Uri.parse(
SimklConstants.pinPollUrl(code.userCode),
).replace(queryParameters: SimklConstants.queryParameters());
final http.Response res;
try {
res = await sendAbortableHttpRequest(
httpClient,
'GET',
pollUri,
headers: SimklConstants.headers(),
timeout: TrackerConstants.authRequestTimeout,
operation: 'Simkl PIN poll',
);
} catch (e) {
appLogger.d('Simkl device-code poll error (transient)', error: e);
return const DevicePollPending();
}
// Simkl returns 200 for both pending and success; anything else is
// effectively expired/denied.
if (res.statusCode != 200) return const DevicePollExpired();
final body = json.decode(res.body) as Map<String, dynamic>;
if (body['result'] == 'OK' && body['access_token'] != null) {
return DevicePollSuccess(body);
}
return const DevicePollPending();
}
@override
TrackerSession buildSession(Map<String, dynamic> tokenResponse) =>
TrackerSession.fromTokenResponse(TrackerService.simkl, tokenResponse);
}