Grain flutter app
1import 'dart:convert';
2
3import 'package:flutter_secure_storage/flutter_secure_storage.dart';
4import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
5import 'package:grain/api.dart';
6import 'package:grain/app_logger.dart';
7import 'package:grain/main.dart';
8import 'package:grain/models/session.dart';
9
10class Auth {
11 static const _storage = FlutterSecureStorage();
12 Auth();
13
14 Future<bool> hasToken() async {
15 final session = await _loadSession();
16 return session != null && session.token.isNotEmpty && !isSessionExpired(session);
17 }
18
19 Future<void> login(String handle) async {
20 final apiUrl = AppConfig.apiUrl;
21 String? token;
22 String? refreshToken;
23 String? expiresAtStr;
24 String? did;
25
26 try {
27 final redirectUrl = await FlutterWebAuth2.authenticate(
28 url: '$apiUrl/oauth/login?client=native&handle=${Uri.encodeComponent(handle)}',
29 callbackUrlScheme: 'grainflutter',
30 );
31
32 appLogger.i('Redirected URL: $redirectUrl');
33
34 final uri = Uri.parse(redirectUrl);
35 token = uri.queryParameters['token'];
36 refreshToken = uri.queryParameters['refreshToken'];
37 expiresAtStr = uri.queryParameters['expiresAt'];
38 did = uri.queryParameters['did'];
39 } catch (e) {
40 appLogger.e('Error during authentication: $e');
41 throw Exception('Authentication failed');
42 }
43
44 appLogger.i('User signed in with handle: $handle');
45
46 if (token == null || token.isEmpty) {
47 throw Exception('No token found in redirect URL');
48 }
49 if (refreshToken == null || refreshToken.isEmpty) {
50 throw Exception('No refreshToken found in redirect URL');
51 }
52 if (expiresAtStr == null || expiresAtStr.isEmpty) {
53 throw Exception('No expiresAt found in redirect URL');
54 }
55 if (did == null || did.isEmpty) {
56 throw Exception('No did found in redirect URL');
57 }
58
59 DateTime expiresAt;
60 try {
61 expiresAt = DateTime.parse(expiresAtStr);
62 } catch (e) {
63 throw Exception('Invalid expiresAt format');
64 }
65
66 final session = Session(
67 token: token,
68 refreshToken: refreshToken,
69 expiresAt: expiresAt,
70 did: did,
71 );
72 await _saveSession(session);
73 }
74
75 Future<void> _saveSession(Session session) async {
76 final sessionJson = jsonEncode(session.toJson());
77 await _storage.write(key: 'session', value: sessionJson);
78 }
79
80 Future<Session?> _loadSession() async {
81 final sessionJsonString = await _storage.read(key: 'session');
82 if (sessionJsonString == null) return null;
83
84 try {
85 final sessionJson = jsonDecode(sessionJsonString);
86 return Session.fromJson(sessionJson);
87 } catch (e) {
88 // Optionally log or clear storage if corrupted
89 return null;
90 }
91 }
92
93 bool isSessionExpired(Session session, {Duration tolerance = const Duration(seconds: 30)}) {
94 final now = DateTime.now().toUtc();
95 return session.expiresAt.subtract(tolerance).isBefore(now);
96 }
97
98 Future<Session?> getValidSession() async {
99 final session = await _loadSession();
100 if (session == null) {
101 // No session at all, do not attempt refresh
102 return null;
103 }
104 if (isSessionExpired(session)) {
105 appLogger.w('Session is expired, attempting refresh');
106 try {
107 final refreshed = await apiService.refreshSession(session);
108 if (refreshed != null && !isSessionExpired(refreshed)) {
109 await _saveSession(refreshed);
110 appLogger.i('Session refreshed and saved');
111 return refreshed;
112 } else {
113 appLogger.w('Session refresh failed or still expired, clearing session');
114 await clearSession();
115 return null;
116 }
117 } catch (e) {
118 appLogger.e('Error refreshing session: $e');
119 await clearSession();
120 return null;
121 }
122 }
123 return session;
124 }
125
126 Future<void> clearSession() async {
127 // Remove session from secure storage
128 await _storage.delete(key: 'session');
129 }
130
131 Future<void> logout() async {
132 final session = await _loadSession();
133
134 appLogger.i('Logging out user with session: $session');
135
136 // Clear any in-memory session/user data
137 apiService.currentUser = null;
138
139 if (session == null) {
140 appLogger.w('No session to revoke');
141 return;
142 }
143
144 await apiService.revokeSession(session);
145
146 await clearSession();
147
148 appLogger.i('User logged out and session cleared');
149 }
150}
151
152final auth = Auth();