Compare commits
3 Commits
e95bb7d291
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cef0b320d8 | |||
| c9dce27582 | |||
| d4ef24c99f |
@@ -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)
|
||||||
|
|||||||
115
src/users.rs
115
src/users.rs
@@ -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(®ister_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, ®ister_info.username, ®ister_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>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user