Compare commits

...

3 Commits

Author SHA1 Message Date
61586ef169
Fix refresh token API call not working 2024-09-02 15:45:21 +02:00
4b5c74d824
Add Api changes and Dart code changes from last week
Co-authored-by: Reimar <mail@reim.ar>
2024-09-02 15:45:21 +02:00
0792862f25
Implement Refresh Token on startup
Co-authored-by: Reimar <mail@reim.ar>
2024-09-02 15:45:21 +02:00
11 changed files with 69 additions and 33 deletions

View File

@ -82,6 +82,8 @@ namespace API.Application.Users.Commands
CreatedAt = DateTime.UtcNow.AddHours(2),
UpdatedAt = DateTime.UtcNow.AddHours(2),
HashedPassword = hashedPassword,
RefreshToken = System.Guid.NewGuid().ToString(),
RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(7),
};
}
}

View File

@ -34,7 +34,10 @@ namespace API.Application.Users.Commands
}
var jwtToken = _tokenHelper.GenerateJwtToken(user);
return new OkObjectResult(new { token = jwtToken, id = user.Id});
user.RefreshToken = System.Guid.NewGuid().ToString();
_repository.Save();
return new OkObjectResult(new { token = jwtToken, id = user.Id, refreshToken = user.RefreshToken });
}
}

View File

@ -58,15 +58,13 @@ namespace API.Controllers
[HttpGet]
public async Task<ActionResult<List<UserDTO>>> GetUsers()
{
return await _queryAllUsers.Handle();
return await _queryAllUsers.Handle();
}
[HttpGet("{id}")]
public async Task<ActionResult<UserDTO>> GetUser(string id)
{
return await _queryUserById.Handle(id);
}
[Authorize]
@ -89,14 +87,11 @@ namespace API.Controllers
return await _deleteUser.Handle(id);
}
[Authorize]
[HttpPost("/RefreshToken")]
public async Task<IActionResult> RefreshToken()
public async Task<IActionResult> RefreshToken(RefreshTokenDTO refreshTokenDTO)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _repository.QueryUserByIdAsync(userId);
User user = await _repository.QueryUserByRefreshTokenAsync(refreshTokenDTO.RefreshToken);
return new OkObjectResult(_tokenHelper.GenerateJwtToken(user));
}
}
}

View File

@ -39,3 +39,8 @@ public class UpdateUserDTO
public string Password { get; set; }
}
public class RefreshTokenDTO
{
public string RefreshToken { get; set; }
}

View File

@ -10,5 +10,7 @@ namespace API.Persistence.Repositories
Task<User> QueryUserByIdAsync(string id);
Task<User> QueryUserByEmailAsync(string email);
Task<bool> UpdateUserAsync(User user);
Task<User> QueryUserByRefreshTokenAsync(string refreshToken);
void Save();
}
}
}

View File

@ -7,6 +7,7 @@ namespace API.Persistence.Repositories
public class UserRepository(AppDBContext context) : IUserRepository
{
private readonly AppDBContext _context = context;
public async Task<List<User>> QueryAllUsersAsync()
{
return await _context.Users.ToListAsync();
@ -16,14 +17,12 @@ namespace API.Persistence.Repositories
{
try
{
return await _context.Users
.FirstOrDefaultAsync(user => user.Id == id);
return await _context.Users.FirstOrDefaultAsync(user => user.Id == id);
}
catch (Exception)
{
return new User();
}
}
public async Task<string> CreateUserAsync(User user)
@ -73,7 +72,16 @@ namespace API.Persistence.Repositories
public async Task<User> QueryUserByEmailAsync(string email)
{
return await _context.Users.SingleOrDefaultAsync(u => u.Email == email);
}
public async Task<User> QueryUserByRefreshTokenAsync(string refreshToken)
{
return await _context.Users.SingleOrDefaultAsync(u => u.RefreshToken == refreshToken);
}
public void Save()
{
_context.SaveChanges();
}
}
}

View File

@ -8,8 +8,8 @@ enum ApiService {
app,
}
Future<String?> request(BuildContext context, ApiService service, String method, String path, Object? body) async {
final messenger = ScaffoldMessenger.of(context);
Future<String?> request(BuildContext? context, ApiService service, String method, String path, Object? body) async {
final messenger = context != null ? ScaffoldMessenger.of(context) : null;
final prefs = await SharedPreferences.getInstance();
final host = switch (service) {
@ -42,17 +42,20 @@ Future<String?> request(BuildContext context, ApiService service, String method,
body: body != null ? jsonEncode(body) : null,
);
}
} catch (_) {
messenger.showSnackBar(const SnackBar(content: Text('Unable to connect to server')));
} catch (e) {
debugPrint(e.toString());
messenger?.showSnackBar(const SnackBar(content: Text('Unable to connect to server')));
return null;
}
if (response.statusCode < 200 || response.statusCode >= 300) {
try {
final json = jsonDecode(response.body);
messenger.showSnackBar(SnackBar(content: Text(json['message'])));
} catch (_) {
messenger.showSnackBar(SnackBar(content: Text('Something went wrong (HTTP ${response.statusCode})')));
messenger?.showSnackBar(SnackBar(content: Text(json['message'] ?? json['title'])));
debugPrint('API error: ' + json['message']);
} catch (e) {
debugPrint(e.toString());
messenger?.showSnackBar(SnackBar(content: Text('Something went wrong (HTTP ${response.statusCode})')));
}
return null;
}
@ -66,7 +69,7 @@ Future<bool> isLoggedIn(BuildContext context) async {
final token = prefs.getString('token');
if (token == null) {
prefs.remove('id');
logout();
return false;
}
@ -78,15 +81,25 @@ Future<bool> isLoggedIn(BuildContext context) async {
if (payload['exp'] < DateTime.now().millisecondsSinceEpoch / 1000) {
messenger.showSnackBar(const SnackBar(content: Text('Token expired, please sign in again')));
prefs.remove('token');
logout();
return false;
}
} catch (e) {
messenger.showSnackBar(const SnackBar(content: Text('Invalid token, please sign in again')));
prefs.remove('token');
debugPrint(e.toString());
logout();
return false;
}
return true;
}
void logout() async {
final prefs = await SharedPreferences.getInstance();
prefs.remove('token');
prefs.remove('refresh-token');
prefs.remove('id');
}

View File

@ -25,10 +25,7 @@ class _SideMenuState extends State<SideMenu> {
}
void _logout() async {
final prefs = await SharedPreferences.getInstance();
prefs.remove('token');
prefs.remove('id');
api.logout();
setState(() {
user = null;

View File

@ -19,20 +19,21 @@ class _LoginPageState extends State<LoginPage> {
final passwordInput = TextEditingController();
Future<void> _login() async {
final token = await api.request(context, api.ApiService.auth, 'POST', '/api/Users/login', {
final response = await api.request(context, api.ApiService.auth, 'POST', '/api/Users/login', {
'email': emailInput.text,
'password': passwordInput.text,
});
if (token == null) return;
if (response == null) return;
// Assuming token is a JSON string
Map<String, dynamic> json = jsonDecode(token);
Map<String, dynamic> json = jsonDecode(response);
Login jsonUser = Login.fromJson(json);
final prefs = await SharedPreferences.getInstance();
prefs.setString('token', jsonUser.token);
prefs.setString('id', jsonUser.id);
prefs.setString('refresh-token', jsonUser.refreshToken);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Successfully logged in')));

View File

@ -6,6 +6,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:mobile/favorites.dart';
import 'package:mobile/register.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'login.dart';
import 'base/sidemenu.dart';
import 'profile.dart';
@ -15,7 +16,14 @@ import 'api.dart' as api;
import 'package:http/http.dart' as http;
import 'dart:developer';
void main() {
void main() async {
// Refresh JWT on startup
final prefs = await SharedPreferences.getInstance();
if (prefs.getString("token") != null && prefs.getString("refresh-token") != null) {
final token = await api.request(null, api.ApiService.auth, "POST", "/RefreshToken", {'refreshToken': prefs.getString("refresh-token")});
if (token != null) prefs.setString("token", token);
}
runApp(const MyApp());
}

View File

@ -25,13 +25,15 @@ class Favorite {
class Login {
String token;
String id;
String refreshToken;
Login(this.token, this.id);
Login(this.token, this.id, this.refreshToken);
factory Login.fromJson(Map<String, dynamic> json) {
return Login(
json['token'],
json['id'],
json['refreshToken'],
);
}
}