Skip to content

Commit 273298f

Browse files
authored
Wojciech mat/token persistance (#58)
* Move UserModel to models and add local storage for auth service * Abstract session storage to run on our tests
1 parent 14f7c8d commit 273298f

14 files changed

Lines changed: 248 additions & 45 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ tmp/
2929
tmp
3030

3131
concat.conf
32+
33+
notes.py

frontend/lib/app/config/app_router.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:resellio/presentation/main_page/main_layout.dart';
99
import 'package:resellio/presentation/common_widgets/adaptive_navigation.dart';
1010
import 'package:resellio/presentation/events/pages/event_details_page.dart';
1111
import 'package:resellio/presentation/cart/pages/cart_page.dart';
12+
import 'package:resellio/core/models/user_model.dart';
1213
import 'package:resellio/presentation/admin/pages/admin_dashboard_page.dart';
1314
import 'package:resellio/presentation/organizer/pages/create_event_page.dart';
1415
import 'package:resellio/presentation/organizer/pages/edit_event_page.dart';

frontend/lib/core/models/user_model.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
import 'package:equatable/equatable.dart';
22

3+
class UserModel {
4+
final int userId;
5+
final String email;
6+
final String name;
7+
final UserRole role;
8+
final int roleId;
9+
10+
UserModel({
11+
required this.userId,
12+
required this.email,
13+
required this.name,
14+
required this.role,
15+
required this.roleId,
16+
});
17+
18+
factory UserModel.fromJwt(Map<String, dynamic> jwtData) {
19+
UserRole role;
20+
switch (jwtData['role']) {
21+
case 'organizer':
22+
role = UserRole.organizer;
23+
break;
24+
case 'administrator':
25+
role = UserRole.admin;
26+
break;
27+
default:
28+
role = UserRole.customer;
29+
}
30+
31+
return UserModel(
32+
userId: jwtData['user_id'],
33+
email: jwtData['sub'],
34+
name: jwtData['name'] ?? 'User',
35+
role: role,
36+
roleId: jwtData['role_id'],
37+
);
38+
}
39+
}
40+
41+
enum UserRole { customer, organizer, admin }
42+
343
class UserProfile extends Equatable {
444
final int userId;
545
final String email;

frontend/lib/core/network/api_client.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
11
import 'package:dio/dio.dart';
2+
import 'package:flutter/foundation.dart';
23
import 'package:resellio/core/network/api_exception.dart';
4+
import 'package:resellio/core/services/storage_service.dart';
35

46
class ApiClient {
57
final Dio _dio;
68
String? _authToken;
9+
static const String _tokenKey = 'resellio_auth_token';
710

811
ApiClient(String baseUrl) : _dio = Dio() {
912
_dio.options.baseUrl = baseUrl;
1013
_dio.options.connectTimeout = const Duration(seconds: 15);
1114
_dio.options.receiveTimeout = const Duration(seconds: 15);
1215
_dio.interceptors.add(_createInterceptor());
16+
17+
if (kIsWeb) {
18+
_loadStoredToken();
19+
}
20+
}
21+
22+
void _loadStoredToken() {
23+
if (kIsWeb) {
24+
_authToken = StorageService.instance.getItem(_tokenKey);
25+
}
1326
}
1427

1528
void setAuthToken(String? token) {
1629
_authToken = token;
30+
if (kIsWeb) {
31+
if (token != null) {
32+
StorageService.instance.setItem(_tokenKey, token);
33+
} else {
34+
StorageService.instance.removeItem(_tokenKey);
35+
}
36+
}
1737
}
1838

1939
Future<dynamic> get(String endpoint, {Map<String, dynamic>? queryParams}) async {
@@ -59,6 +79,11 @@ class ApiClient {
5979
InterceptorsWrapper _createInterceptor() {
6080
return InterceptorsWrapper(
6181
onRequest: (options, handler) {
82+
// Always check localStorage for the latest token (web only)
83+
if (kIsWeb && _authToken == null) {
84+
_loadStoredToken();
85+
}
86+
6287
if (_authToken != null) {
6388
options.headers['Authorization'] = 'Bearer $_authToken';
6489
}
Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,11 @@
1+
import 'package:flutter/foundation.dart';
12
import 'package:flutter/material.dart';
23
import 'package:resellio/core/models/models.dart';
34
import 'package:resellio/core/repositories/repositories.dart';
45
import 'package:resellio/core/utils/jwt_decoder.dart';
6+
import 'package:resellio/core/services/storage_service.dart';
57
import 'package:resellio/presentation/common_widgets/adaptive_navigation.dart';
68

7-
class UserModel {
8-
final int userId;
9-
final String email;
10-
final String name;
11-
final UserRole role;
12-
final int roleId;
13-
14-
UserModel(
15-
{required this.userId,
16-
required this.email,
17-
required this.name,
18-
required this.role,
19-
required this.roleId});
20-
21-
factory UserModel.fromJwt(Map<String, dynamic> jwtData) {
22-
UserRole role;
23-
switch (jwtData['role']) {
24-
case 'organizer':
25-
role = UserRole.organizer;
26-
break;
27-
case 'administrator':
28-
role = UserRole.admin;
29-
break;
30-
default:
31-
role = UserRole.customer;
32-
}
33-
34-
return UserModel(
35-
userId: jwtData['user_id'],
36-
email: jwtData['sub'],
37-
name: jwtData['name'] ?? 'User',
38-
role: role,
39-
roleId: jwtData['role_id'],
40-
);
41-
}
42-
}
439

4410
class AuthService extends ChangeNotifier {
4511
final AuthRepository _authRepository;
@@ -48,15 +14,64 @@ class AuthService extends ChangeNotifier {
4814
UserModel? _user;
4915
UserProfile? _detailedProfile;
5016

51-
AuthService(this._authRepository, this._userRepository);
17+
static const String _tokenKey = 'resellio_auth_token';
18+
19+
AuthService(this._authRepository, this._userRepository) {
20+
_loadStoredToken();
21+
}
5222

5323
bool get isLoggedIn => _token != null;
5424
UserModel? get user => _user;
5525
UserProfile? get detailedProfile => _detailedProfile;
5626

27+
Future<void> _loadStoredToken() async {
28+
try {
29+
final storedToken = StorageService.instance.getItem(_tokenKey);
30+
if (storedToken != null && storedToken.isNotEmpty) {
31+
final jwtData = tryDecodeJwt(storedToken);
32+
if (jwtData != null) {
33+
final exp = jwtData['exp'];
34+
if (exp != null) {
35+
final expiryDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
36+
if (expiryDate.isAfter(DateTime.now())) {
37+
_token = storedToken;
38+
_user = UserModel.fromJwt(jwtData);
39+
40+
try {
41+
_detailedProfile = await _userRepository.getUserProfile();
42+
} catch (e) {
43+
debugPrint("Failed to fetch detailed profile on token restore: $e");
44+
}
45+
46+
notifyListeners();
47+
return;
48+
}
49+
}
50+
}
51+
await _clearStoredToken();
52+
}
53+
} catch (e) {
54+
debugPrint("Error loading stored token: $e");
55+
await _clearStoredToken();
56+
}
57+
}
58+
59+
void _storeToken(String token) {
60+
StorageService.instance.setItem(_tokenKey, token);
61+
}
62+
63+
Future<void> _clearStoredToken() async {
64+
StorageService.instance.removeItem(_tokenKey);
65+
_token = null;
66+
_user = null;
67+
_detailedProfile = null;
68+
}
69+
5770
Future<void> _setTokenAndUser(String token) async {
5871
_token = token;
59-
_detailedProfile = null; // Clear old profile data
72+
_storeToken(token);
73+
_detailedProfile = null;
74+
6075
final jwtData = tryDecodeJwt(token);
6176
if (jwtData != null) {
6277
_user = UserModel.fromJwt(jwtData);
@@ -76,7 +91,7 @@ class AuthService extends ChangeNotifier {
7691

7792
Future<String> registerCustomer(Map<String, dynamic> data) async {
7893
final message = await _authRepository.registerCustomer(data);
79-
return message;
94+
return message;
8095
}
8196

8297
Future<void> registerOrganizer(Map<String, dynamic> data) async {
@@ -93,9 +108,22 @@ class AuthService extends ChangeNotifier {
93108

94109
Future<void> logout() async {
95110
await _authRepository.logout();
96-
_token = null;
97-
_user = null;
98-
_detailedProfile = null;
111+
await _clearStoredToken();
99112
notifyListeners();
100113
}
114+
115+
Future<void> refreshUserData() async {
116+
if (_token != null && _user != null) {
117+
try {
118+
_detailedProfile = await _userRepository.getUserProfile();
119+
notifyListeners();
120+
} catch (e) {
121+
debugPrint("Failed to refresh user data: $e");
122+
// If refresh fails due to invalid token, logout
123+
if (e.toString().contains('401') || e.toString().contains('403')) {
124+
await logout();
125+
}
126+
}
127+
}
128+
}
101129
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'storage_service_stub.dart'
2+
if (dart.library.html) 'storage_service_web.dart'
3+
if (dart.library.io) 'storage_service_io.dart';
4+
5+
abstract class StorageService {
6+
static StorageService get instance => getStorageService();
7+
8+
String? getItem(String key);
9+
void setItem(String key, String value);
10+
void removeItem(String key);
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'storage_service.dart';
2+
3+
class IoStorageService implements StorageService {
4+
@override
5+
String? getItem(String key) => null;
6+
7+
@override
8+
void setItem(String key, String value) {}
9+
10+
@override
11+
void removeItem(String key) {}
12+
}
13+
14+
StorageService getStorageService() => IoStorageService();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'storage_service.dart';
2+
3+
class StubStorageService implements StorageService {
4+
@override
5+
String? getItem(String key) => null;
6+
7+
@override
8+
void setItem(String key, String value) {}
9+
10+
@override
11+
void removeItem(String key) {}
12+
}
13+
14+
StorageService getStorageService() => StubStorageService();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import 'dart:html' as html;
2+
import 'storage_service.dart';
3+
4+
class WebStorageService implements StorageService {
5+
@override
6+
String? getItem(String key) => html.window.localStorage[key];
7+
8+
@override
9+
void setItem(String key, String value) => html.window.localStorage[key] = value;
10+
11+
@override
12+
void removeItem(String key) => html.window.localStorage.remove(key);
13+
}
14+
15+
StorageService getStorageService() => WebStorageService();

frontend/lib/core/utils/jwt_decoder.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,24 @@ Map<String, dynamic>? tryDecodeJwt(String token) {
1414
return null;
1515
}
1616
}
17+
18+
bool isTokenExpired(String token) {
19+
final jwtData = tryDecodeJwt(token);
20+
if (jwtData == null) return true;
21+
22+
final exp = jwtData['exp'];
23+
if (exp == null) return true;
24+
25+
final expiryDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
26+
return expiryDate.isBefore(DateTime.now());
27+
}
28+
29+
DateTime? getTokenExpiry(String token) {
30+
final jwtData = tryDecodeJwt(token);
31+
if (jwtData == null) return null;
32+
33+
final exp = jwtData['exp'];
34+
if (exp == null) return null;
35+
36+
return DateTime.fromMillisecondsSinceEpoch(exp * 1000);
37+
}

0 commit comments

Comments
 (0)