Compare commits

...

3 Commits

Author SHA1 Message Date
cef0b320d8 Added user register 2025-04-16 17:28:21 +02:00
c9dce27582 Added logout api route 2025-04-13 22:25:41 +02:00
d4ef24c99f Small formating 2025-04-11 23:50:16 +02:00
2 changed files with 96 additions and 22 deletions

View File

@@ -1,5 +1,4 @@
mod cases; mod cases;
mod inventories;
mod items; mod items;
mod types; mod types;
mod users; mod users;
@@ -44,6 +43,8 @@ async fn main() -> std::io::Result<()> {
.add(("Access-Control-Allow-Credentials", "true")), .add(("Access-Control-Allow-Credentials", "true")),
) )
.service(login) .service(login)
.service(logout)
.service(register)
.service(get_case) .service(get_case)
.service(get_cases) .service(get_cases)
.service(get_item) .service(get_item)

View File

@@ -28,31 +28,21 @@ use crate::AppState;
use actix_web::cookie::Cookie; use actix_web::cookie::Cookie;
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use actix_web::{HttpRequest, HttpResponse, Responder, post}; use actix_web::{App, HttpRequest, HttpResponse, Responder, post};
use argon2::Argon2; use argon2::{Argon2, PasswordHasher};
use argon2::password_hash::{ use argon2::password_hash::{PasswordHash, PasswordVerifier, SaltString};
PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng, use argon2::password_hash::rand_core::OsRng;
}; use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
use jsonwebtoken::{
Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, get_current_timestamp,
};
use rand::Rng; use rand::Rng;
use rand::distr::Alphanumeric; use rand::distr::Alphanumeric;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use serde_json::{json, to_string}; use serde_json::{json, to_string};
use sqlx::sqlite::SqliteQueryResult;
use sqlx::{Pool, Sqlite, query, query_as}; use sqlx::{Pool, Sqlite, query, query_as};
use std::error::Error; use std::error::Error;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use uuid::Uuid;
/*
Will use cookies in final version of app
In the meanwhile, we'll just save the token in the userStore
use actix_web::cookie::{Cookie, SameSite};
use actix_web::cookie::time::{Duration, OffsetDateTime, UtcDateTime};
*/
#[derive(Debug)] #[derive(Debug)]
struct Password { struct Password {
@@ -77,14 +67,16 @@ impl User {
async fn fetch_optional( async fn fetch_optional(
database: &Pool<Sqlite>, database: &Pool<Sqlite>,
id: Option<i64>, id: Option<i64>,
uuid: Option<&str>,
username: Option<&Username>, username: Option<&Username>,
) -> Result<Option<User>, sqlx::Error> { ) -> Result<Option<User>, sqlx::Error> {
match username { match username {
Some(username) => { Some(username) => {
query_as!( query_as!(
User, User,
"SELECT * FROM users WHERE 'id' = ?1 OR 'username' = ?2", "SELECT * FROM users WHERE 'id' = ?1 OR 'username' = ?2 or 'uuid' = ?3",
id, id,
uuid,
username.value username.value
) )
.fetch_optional(database) .fetch_optional(database)
@@ -93,8 +85,9 @@ impl User {
None => { None => {
query_as!( query_as!(
User, User,
"SELECT * FROM users WHERE 'id' = ?1 OR 'username' = ?2", "SELECT * FROM users WHERE 'id' = ?1 OR 'username' = ?2 OR 'uuid' = ?3",
id, id,
uuid,
None::<String> None::<String>
) )
.fetch_optional(database) .fetch_optional(database)
@@ -102,6 +95,23 @@ impl User {
} }
} }
} }
async fn create(database: &Pool<Sqlite>, hash: PasswordHash<'_>, username: &Username, email: &String) -> Result<User, sqlx::Error> {
let mut uuid = uuid::Uuid::new_v4().to_string();
while let Ok(Some(_)) = query!("SELECT id FROM users WHERE 'uuid' = ?1", uuid).fetch_optional(database).await {
uuid = uuid::Uuid::new_v4().to_string();
}
let hash_string = hash.to_string();
query_as!(User,
"INSERT INTO users ('uuid', 'username', 'email', 'hash') VALUES (?1, ?2, ?3, ?4) RETURNING *",
uuid,
username.value,
email,
hash_string
)
.fetch_one(database)
.await
}
} }
struct PasswordError; struct PasswordError;
@@ -187,7 +197,7 @@ async fn login(
login_info: Json<LoginInfo>, login_info: Json<LoginInfo>,
) -> Result<impl Responder, Box<dyn Error>> { ) -> Result<impl Responder, Box<dyn Error>> {
if let Ok(Some(user)) = if let Ok(Some(user)) =
User::fetch_optional(&app_state.database, None, Some(&login_info.username)).await User::fetch_optional(&app_state.database, None, None, Some(&login_info.username)).await
{ {
if validate_authentication(&user, &login_info.password)? { if validate_authentication(&user, &login_info.password)? {
let jwt_token = generate_jwt(&user, app_state.clone())?; let jwt_token = generate_jwt(&user, app_state.clone())?;
@@ -213,6 +223,68 @@ async fn login(
} }
} }
#[derive(Deserialize, Debug)]
struct LogoutInfo {
uuid: String,
token: String,
}
#[post("/logout")]
async fn logout(
app_state: Data<AppState>,
logout_info: Json<LogoutInfo>,
req: HttpRequest,
) -> Result<impl Responder, Box<dyn Error>> {
let refresh_cookie = match req.cookie("refresh_token") {
Some(cookie) => cookie,
None => return Ok(HttpResponse::Unauthorized().finish()),
};
let refresh_token = refresh_cookie.value();
match User::fetch_optional(&app_state.database, None, Some(&logout_info.uuid), None).await? {
Some(user) => {
let now = get_current_timestamp() as i64;
let result: SqliteQueryResult = query!(
"UPDATE refresh_tokens SET expiry = ?1 WHERE token = ?2 AND user = ?3 AND previous = ?4",
now,
refresh_token,
user.id,
logout_info.token
)
.execute(&app_state.database)
.await?;
if result.rows_affected() == 0 {
return Ok(HttpResponse::Unauthorized().finish());
}
let mut refresh_token_cookie = Cookie::new("refresh_token", "");
refresh_token_cookie.make_removal();
Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish())
}
None => Ok(HttpResponse::Unauthorized().finish()),
}
}
#[derive(Deserialize, Debug)]
struct RegisterInfo {
username: Username,
password: Password,
email: String,
}
#[post("/register")]
async fn register(
app_state: Data<AppState>,
register_info: Json<RegisterInfo>,
) -> Result<impl Responder, Box<dyn Error>> {
if let Ok(Some(_)) = User::fetch_optional(&app_state.database, None, None, Some(&register_info.username)).await {
return Ok(HttpResponse::InternalServerError().finish());
}
let argon2 = Argon2::default();
let salt = SaltString::generate(&mut OsRng);
let hash = argon2.hash_password(register_info.password.value.as_bytes(), &salt)?;
User::create(&app_state.database, hash, &register_info.username, &register_info.email).await?;
Ok(HttpResponse::Ok().finish())
}
fn validate_authentication(user: &User, password: &Password) -> Result<bool, Box<dyn Error>> { fn validate_authentication(user: &User, password: &Password) -> Result<bool, Box<dyn Error>> {
let argon2 = Argon2::default(); let argon2 = Argon2::default();
let hash = PasswordHash::new(&user.hash)?; let hash = PasswordHash::new(&user.hash)?;
@@ -220,9 +292,10 @@ fn validate_authentication(user: &User, password: &Password) -> Result<bool, Box
.verify_password(password.value.as_bytes(), &hash) .verify_password(password.value.as_bytes(), &hash)
.is_err() .is_err()
{ {
return Ok(false); Ok(false)
} else {
Ok(true)
} }
Ok(true)
} }
fn generate_jwt(user: &User, app_state: Data<AppState>) -> Result<String, Box<dyn Error>> { fn generate_jwt(user: &User, app_state: Data<AppState>) -> Result<String, Box<dyn Error>> {