From a471a110159a8e0f4ce16b1158eb65e44ceda904 Mon Sep 17 00:00:00 2001 From: Sandertp Date: Thu, 29 Aug 2024 13:25:02 +0200 Subject: [PATCH 1/5] Implement Refresh Token on startup Co-authored-by: Reimar --- API/Application/Users/Commands/CreateUser.cs | 2 ++ API/Application/Users/Commands/LoginUser.cs | 2 +- API/Controllers/UsersController.cs | 10 ++++------ API/Models/User.cs | 5 +++++ API/Persistence/Repositories/IUserRepository.cs | 1 + API/Persistence/Repositories/UserRepository.cs | 11 +++++++---- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/API/Application/Users/Commands/CreateUser.cs b/API/Application/Users/Commands/CreateUser.cs index 419ee62..e271b8d 100644 --- a/API/Application/Users/Commands/CreateUser.cs +++ b/API/Application/Users/Commands/CreateUser.cs @@ -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), }; } } diff --git a/API/Application/Users/Commands/LoginUser.cs b/API/Application/Users/Commands/LoginUser.cs index 2e01ca5..de90f3d 100644 --- a/API/Application/Users/Commands/LoginUser.cs +++ b/API/Application/Users/Commands/LoginUser.cs @@ -34,7 +34,7 @@ namespace API.Application.Users.Commands } var jwtToken = _tokenHelper.GenerateJwtToken(user); - return new OkObjectResult(new { token = jwtToken, id = user.Id}); + return new OkObjectResult(new { token = jwtToken, id = user.Id, refreshToken = user.RefreshToken}); } } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index d58d0f1..07c3c2d 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -87,14 +87,12 @@ namespace API.Controllers public async Task DeleteUser(string id) { return await _deleteUser.Handle(id); - } - - [Authorize] + } + [HttpPost("/RefreshToken")] - public async Task RefreshToken() + public async Task 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)); } diff --git a/API/Models/User.cs b/API/Models/User.cs index fcaaa30..8bdab07 100644 --- a/API/Models/User.cs +++ b/API/Models/User.cs @@ -39,3 +39,8 @@ public class UpdateUserDTO public string Password { get; set; } } +public class RefreshTokenDTO +{ + public string RefreshToken { get; set; } +} + diff --git a/API/Persistence/Repositories/IUserRepository.cs b/API/Persistence/Repositories/IUserRepository.cs index 690210f..3678a6c 100644 --- a/API/Persistence/Repositories/IUserRepository.cs +++ b/API/Persistence/Repositories/IUserRepository.cs @@ -10,5 +10,6 @@ namespace API.Persistence.Repositories Task QueryUserByIdAsync(string id); Task QueryUserByEmailAsync(string email); Task UpdateUserAsync(User user); + Task QueryUserByRefreshTokenAsync(string refreshToken); } } \ No newline at end of file diff --git a/API/Persistence/Repositories/UserRepository.cs b/API/Persistence/Repositories/UserRepository.cs index da8f833..3509bd9 100644 --- a/API/Persistence/Repositories/UserRepository.cs +++ b/API/Persistence/Repositories/UserRepository.cs @@ -7,6 +7,7 @@ namespace API.Persistence.Repositories public class UserRepository(AppDBContext context) : IUserRepository { private readonly AppDBContext _context = context; + public async Task> 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 CreateUserAsync(User user) @@ -73,7 +72,11 @@ namespace API.Persistence.Repositories public async Task QueryUserByEmailAsync(string email) { return await _context.Users.SingleOrDefaultAsync(u => u.Email == email); - + } + + public async Task QueryUserByRefreshTokenAsync(string refreshToken) + { + return await _context.Users.SingleOrDefaultAsync(u => u.RefreshToken == refreshToken); } } } From aaf35986500c7a4a70dfe466e7f780acc7d6fb44 Mon Sep 17 00:00:00 2001 From: Sandertp Date: Mon, 2 Sep 2024 11:39:03 +0200 Subject: [PATCH 2/5] Add Api changes and Dart code changes from last week Co-authored-by: Reimar --- Mobile/lib/api.dart | 17 ++++++++++------- Mobile/lib/login.dart | 1 + Mobile/lib/main.dart | 10 +++++++++- Mobile/lib/models.dart | 4 +++- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Mobile/lib/api.dart b/Mobile/lib/api.dart index 25b6bdc..5bf781f 100644 --- a/Mobile/lib/api.dart +++ b/Mobile/lib/api.dart @@ -8,8 +8,8 @@ enum ApiService { app, } -Future request(BuildContext context, ApiService service, String method, String path, Object? body) async { - final messenger = ScaffoldMessenger.of(context); +Future 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 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']))); + debugPrint('API error: ' + json['message']); + } catch (e) { + debugPrint(e.toString()); + messenger?.showSnackBar(SnackBar(content: Text('Something went wrong (HTTP ${response.statusCode})'))); } return null; } diff --git a/Mobile/lib/login.dart b/Mobile/lib/login.dart index 8c36021..b08790b 100644 --- a/Mobile/lib/login.dart +++ b/Mobile/lib/login.dart @@ -33,6 +33,7 @@ class _LoginPageState extends State { 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'))); diff --git a/Mobile/lib/main.dart b/Mobile/lib/main.dart index d3f8bd9..32ba677 100644 --- a/Mobile/lib/main.dart +++ b/Mobile/lib/main.dart @@ -5,6 +5,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'; @@ -12,7 +13,14 @@ import 'api.dart' as api; import 'models.dart'; import 'package:http/http.dart' as http; -void main() { +void main() async { + final prefs = await SharedPreferences.getInstance(); + debugPrint('token: ' + (prefs.getString('token') ?? '') + ' reffresshh: ' + (prefs.getString("refresh-token") ?? '')); + if (prefs.getString("token") != null && prefs.getString("refresh-token") != null) { + final token = await api.request(null, api.ApiService.auth, "POST", "/RefreshToken", {'refresh-token': prefs.getString("refresh-token")}); + debugPrint(token); + } + runApp(const MyApp()); } diff --git a/Mobile/lib/models.dart b/Mobile/lib/models.dart index 78fb075..e9d4ba1 100644 --- a/Mobile/lib/models.dart +++ b/Mobile/lib/models.dart @@ -23,13 +23,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 json) { return Login( json['token'], json['id'], + json['refreshToken'], ); } } From 9a0757922df4d946e8478db95a69beab2eb55bb5 Mon Sep 17 00:00:00 2001 From: Reimar Date: Mon, 2 Sep 2024 13:14:13 +0200 Subject: [PATCH 3/5] Fix refresh token API call not working --- API/Application/Users/Commands/LoginUser.cs | 5 ++++- API/Controllers/UsersController.cs | 9 +++------ API/Models/User.cs | 2 +- .../Repositories/IUserRepository.cs | 3 ++- API/Persistence/Repositories/UserRepository.cs | 5 +++++ Mobile/lib/api.dart | 18 ++++++++++++++---- Mobile/lib/base/sidemenu.dart | 5 +---- Mobile/lib/login.dart | 6 +++--- Mobile/lib/main.dart | 6 +++--- 9 files changed, 36 insertions(+), 23 deletions(-) diff --git a/API/Application/Users/Commands/LoginUser.cs b/API/Application/Users/Commands/LoginUser.cs index de90f3d..7576a50 100644 --- a/API/Application/Users/Commands/LoginUser.cs +++ b/API/Application/Users/Commands/LoginUser.cs @@ -34,7 +34,10 @@ namespace API.Application.Users.Commands } var jwtToken = _tokenHelper.GenerateJwtToken(user); - return new OkObjectResult(new { token = jwtToken, id = user.Id, refreshToken = user.RefreshToken}); + user.RefreshToken = System.Guid.NewGuid().ToString(); + _repository.Save(); + + return new OkObjectResult(new { token = jwtToken, id = user.Id, refreshToken = user.RefreshToken }); } } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 07c3c2d..8c83fa0 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -58,15 +58,13 @@ namespace API.Controllers [HttpGet] public async Task>> GetUsers() { - return await _queryAllUsers.Handle(); + return await _queryAllUsers.Handle(); } - [HttpGet("{id}")] public async Task> GetUser(string id) { return await _queryUserById.Handle(id); - } [Authorize] @@ -87,14 +85,13 @@ namespace API.Controllers public async Task DeleteUser(string id) { return await _deleteUser.Handle(id); - } - + } + [HttpPost("/RefreshToken")] public async Task RefreshToken(RefreshTokenDTO refreshTokenDTO) { User user = await _repository.QueryUserByRefreshTokenAsync(refreshTokenDTO.RefreshToken); return new OkObjectResult(_tokenHelper.GenerateJwtToken(user)); } - } } diff --git a/API/Models/User.cs b/API/Models/User.cs index 8bdab07..bfb9b09 100644 --- a/API/Models/User.cs +++ b/API/Models/User.cs @@ -39,7 +39,7 @@ public class UpdateUserDTO public string Password { get; set; } } -public class RefreshTokenDTO +public class RefreshTokenDTO { public string RefreshToken { get; set; } } diff --git a/API/Persistence/Repositories/IUserRepository.cs b/API/Persistence/Repositories/IUserRepository.cs index 3678a6c..9f2584d 100644 --- a/API/Persistence/Repositories/IUserRepository.cs +++ b/API/Persistence/Repositories/IUserRepository.cs @@ -11,5 +11,6 @@ namespace API.Persistence.Repositories Task QueryUserByEmailAsync(string email); Task UpdateUserAsync(User user); Task QueryUserByRefreshTokenAsync(string refreshToken); + void Save(); } -} \ No newline at end of file +} diff --git a/API/Persistence/Repositories/UserRepository.cs b/API/Persistence/Repositories/UserRepository.cs index 3509bd9..e007973 100644 --- a/API/Persistence/Repositories/UserRepository.cs +++ b/API/Persistence/Repositories/UserRepository.cs @@ -78,5 +78,10 @@ namespace API.Persistence.Repositories { return await _context.Users.SingleOrDefaultAsync(u => u.RefreshToken == refreshToken); } + + public void Save() + { + _context.SaveChanges(); + } } } diff --git a/Mobile/lib/api.dart b/Mobile/lib/api.dart index 5bf781f..ed7734f 100644 --- a/Mobile/lib/api.dart +++ b/Mobile/lib/api.dart @@ -51,7 +51,7 @@ Future request(BuildContext? context, ApiService service, String method if (response.statusCode < 200 || response.statusCode >= 300) { try { final json = jsonDecode(response.body); - messenger?.showSnackBar(SnackBar(content: Text(json['message']))); + messenger?.showSnackBar(SnackBar(content: Text(json['message'] ?? json['title']))); debugPrint('API error: ' + json['message']); } catch (e) { debugPrint(e.toString()); @@ -69,7 +69,7 @@ Future isLoggedIn(BuildContext context) async { final token = prefs.getString('token'); if (token == null) { - prefs.remove('id'); + logout(); return false; } @@ -81,15 +81,25 @@ Future 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'); +} diff --git a/Mobile/lib/base/sidemenu.dart b/Mobile/lib/base/sidemenu.dart index 411db7c..0208d7d 100644 --- a/Mobile/lib/base/sidemenu.dart +++ b/Mobile/lib/base/sidemenu.dart @@ -25,10 +25,7 @@ class _SideMenuState extends State { } void _logout() async { - final prefs = await SharedPreferences.getInstance(); - - prefs.remove('token'); - prefs.remove('id'); + api.logout(); setState(() { user = null; diff --git a/Mobile/lib/login.dart b/Mobile/lib/login.dart index b08790b..87e71f2 100644 --- a/Mobile/lib/login.dart +++ b/Mobile/lib/login.dart @@ -19,15 +19,15 @@ class _LoginPageState extends State { final passwordInput = TextEditingController(); Future _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 json = jsonDecode(token); + Map json = jsonDecode(response); Login jsonUser = Login.fromJson(json); final prefs = await SharedPreferences.getInstance(); diff --git a/Mobile/lib/main.dart b/Mobile/lib/main.dart index 32ba677..d4b720a 100644 --- a/Mobile/lib/main.dart +++ b/Mobile/lib/main.dart @@ -14,11 +14,11 @@ import 'models.dart'; import 'package:http/http.dart' as http; void main() async { + // Refresh JWT on startup final prefs = await SharedPreferences.getInstance(); - debugPrint('token: ' + (prefs.getString('token') ?? '') + ' reffresshh: ' + (prefs.getString("refresh-token") ?? '')); if (prefs.getString("token") != null && prefs.getString("refresh-token") != null) { - final token = await api.request(null, api.ApiService.auth, "POST", "/RefreshToken", {'refresh-token': prefs.getString("refresh-token")}); - debugPrint(token); + 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()); From f8b3c3e1fc249c513553fb44cc10c4fa05eda6ee Mon Sep 17 00:00:00 2001 From: Sandertp Date: Tue, 3 Sep 2024 10:38:55 +0200 Subject: [PATCH 4/5] Add Review Table to Rust Backend and migrated Co-authored-by: Reimar --- .../migrations/V3__create_reviews_table.sql | 12 ++++++++ rust-backend/src/models.rs | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 rust-backend/migrations/V3__create_reviews_table.sql diff --git a/rust-backend/migrations/V3__create_reviews_table.sql b/rust-backend/migrations/V3__create_reviews_table.sql new file mode 100644 index 0000000..6ab31b4 --- /dev/null +++ b/rust-backend/migrations/V3__create_reviews_table.sql @@ -0,0 +1,12 @@ +CREATE TABLE reviews ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + lat REAL NOT NULL, + lng REAL NOT NULL, + place_name TEXT NOT NULL, + place_description TEXT NOT NULL, + title TEXT NOT NULL, + content TEXT NOT NULL, + rating REAL NOT NULL +); + diff --git a/rust-backend/src/models.rs b/rust-backend/src/models.rs index 4892d0b..c4933ac 100644 --- a/rust-backend/src/models.rs +++ b/rust-backend/src/models.rs @@ -25,3 +25,32 @@ impl Favorite { } } +#[derive(Serialize)] +pub struct Review { + pub id: i64, + pub user_id: String, + pub lat: f64, + pub lng: f64, + pub place_name: String, + pub place_description: String, + pub title: String, + pub content: String, + pub rating: i64, +} + +impl Review { + pub fn from_row(row: &Row) -> Result { + Ok(Review { + id: row.get("id")?, + user_id: row.get("user_id")?, + lat: row.get("lat")?, + lng: row.get("lng")?, + place_name: row.get("place_name")?, + place_description: row.get("place_description")?, + title: row.get("title")?, + content: row.get("content")?, + rating: row.get("rating")?, + }) + } +} + From 06feea265054e14d43b1a5f6ec1bf561f6283280 Mon Sep 17 00:00:00 2001 From: Sandertp Date: Tue, 3 Sep 2024 11:28:48 +0200 Subject: [PATCH 5/5] Implement CRUD for Reviews Co-authored-by: Reimar --- rust-backend/src/main.rs | 93 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/rust-backend/src/main.rs b/rust-backend/src/main.rs index f059c3a..5da3fc6 100644 --- a/rust-backend/src/main.rs +++ b/rust-backend/src/main.rs @@ -4,7 +4,7 @@ mod models; use actix_web::{get, post, delete, Responder, HttpResponse, HttpServer, App, web}; use std::sync::{Mutex, MutexGuard, Arc}; use auth::AuthorizedUser; -use models::Favorite; +use models::{Favorite, Review}; use serde::Deserialize; mod embedded { @@ -110,6 +110,94 @@ async fn delete_favorite(auth: AuthorizedUser, data:web::Data, path: we } } +#[get("/reviews")] +async fn reviews(data: web::Data) -> impl Responder { + let db = data.database.lock().unwrap(); + + match get_reviews(db) { + Some(reviews) => HttpResponse::Ok().insert_header(("Content-Type", "application/json; charset=utf-8")).json(reviews), + None => HttpResponse::InternalServerError().finish(), + } +} + +fn get_reviews(db: MutexGuard<'_, rusqlite::Connection>) -> Option> { + Some( + db.prepare("SELECT * FROM reviews").ok()? + .query_map([], |row| Review::from_row(row)) + .ok()? + .map(|rev| rev.unwrap()) + .collect() + ) +} + +#[derive(Deserialize)] +struct CreateReviewRequest { + lat: f64, + lng: f64, + place_name: String, + place_description: String, + title: String, + content: String, + rating: i64, +} + +#[post("/reviews")] +async fn create_review(auth: AuthorizedUser, data: web::Data, input: web::Json) -> impl Responder { + let db = data.database.lock().unwrap(); + + match db.execute( + "INSERT INTO reviews (user_id, lat, lng, place_name, place_description, title, content, rating) VALUES (:user_id, :lat, :lng, :place_name, :place_description, :title, :content, :rating)", + &[ + (":user_id", &auth.user_id), + (":lat", &input.lat.to_string()), + (":lng", &input.lng.to_string()), + (":place_name", &input.place_name), + (":place_description", &input.place_description), + (":title", &input.title), + (":content", &input.content), + (":rating", &input.rating.to_string()), + ], + ) { + Ok(_) => HttpResponse::Created().json(Review { + id: db.last_insert_rowid(), + user_id: auth.user_id, + lat: input.lat, + lng: input.lng, + place_name: input.place_name.clone(), + place_description: input.place_description.clone(), + title: input.title.clone(), + content: input.content.clone(), + rating: input.rating.clone(), + }), + Err(_) => HttpResponse::InternalServerError().finish(), + } +} + + +#[delete("/reviews/{review}")] +async fn delete_review(auth: AuthorizedUser, data:web::Data, path: web::Path) -> impl Responder { + let db = data.database.lock().unwrap(); + let review_id = path.into_inner(); + let params = &[(":id", &review_id.to_string())]; + + let result = db.query_row("SELECT * FROM reviews WHERE id = :id LIMIT 1", params, |row| Review::from_row(row)); + + if result.is_err() { + return HttpResponse::InternalServerError().finish(); + } + + let review = result.unwrap(); + + if review.user_id != auth.user_id { + return HttpResponse::Forbidden().body("Cannot remove review that you did not create"); + } + + match db.execute("DELETE FROM reviews WHERE id = :id", params) { + Ok(_) => HttpResponse::NoContent().finish(), + Err(_) => HttpResponse::InternalServerError().finish(), + } +} + #[actix_web::main] async fn main() -> std::io::Result<()> { let _ = dotenvy::dotenv(); @@ -142,6 +230,9 @@ async fn main() -> std::io::Result<()> { .service(favorites) .service(create_favorite) .service(delete_favorite) + .service(reviews) + .service(create_review) + .service(delete_review) }) .bind(("0.0.0.0", port))? .run()