Compare commits
7 Commits
2ffe9c0c2b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cef0b320d8 | |||
| c9dce27582 | |||
| d4ef24c99f | |||
| e95bb7d291 | |||
| 972a280229 | |||
| 8d494c6756 | |||
| 190543cd6a |
50
.sqlx/query-1e9045f52c002a19b815351fee1c7ee7520478ff4f5886b4523fb0dc4df0e204.json
generated
Normal file
50
.sqlx/query-1e9045f52c002a19b815351fee1c7ee7520478ff4f5886b4523fb0dc4df0e204.json
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM items WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "rarity",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1e9045f52c002a19b815351fee1c7ee7520478ff4f5886b4523fb0dc4df0e204"
|
||||
}
|
||||
44
.sqlx/query-21875450b713d126c5fe3074b2f6ed2766bc92b6c047eb014106b0c82b905dd6.json
generated
Normal file
44
.sqlx/query-21875450b713d126c5fe3074b2f6ed2766bc92b6c047eb014106b0c82b905dd6.json
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM cases WHERE uuid = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"ordinal": 4,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "21875450b713d126c5fe3074b2f6ed2766bc92b6c047eb014106b0c82b905dd6"
|
||||
}
|
||||
44
.sqlx/query-26ed513b4f9720a7cec47a9bcd3023cedb6888111199fd3c7d9a2bdcc6dd3398.json
generated
Normal file
44
.sqlx/query-26ed513b4f9720a7cec47a9bcd3023cedb6888111199fd3c7d9a2bdcc6dd3398.json
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM cases WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"ordinal": 4,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "26ed513b4f9720a7cec47a9bcd3023cedb6888111199fd3c7d9a2bdcc6dd3398"
|
||||
}
|
||||
26
.sqlx/query-3367d7df853d161f82b517e6601de33b7655c98bdade65ab8b7d985ee77b0779.json
generated
Normal file
26
.sqlx/query-3367d7df853d161f82b517e6601de33b7655c98bdade65ab8b7d985ee77b0779.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM items_cases WHERE \"item\" = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "item",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "case",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3367d7df853d161f82b517e6601de33b7655c98bdade65ab8b7d985ee77b0779"
|
||||
}
|
||||
44
.sqlx/query-3d549f4fa2e86c6eab7dfb482cf0ef6a80cfcff18c1504a81757c26920c03f55.json
generated
Normal file
44
.sqlx/query-3d549f4fa2e86c6eab7dfb482cf0ef6a80cfcff18c1504a81757c26920c03f55.json
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM cases",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"ordinal": 4,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3d549f4fa2e86c6eab7dfb482cf0ef6a80cfcff18c1504a81757c26920c03f55"
|
||||
}
|
||||
50
.sqlx/query-467dcd7fd379750cbf30a190d06dc1b3e61b1bd7ef288fbf6fbedec52c276434.json
generated
Normal file
50
.sqlx/query-467dcd7fd379750cbf30a190d06dc1b3e61b1bd7ef288fbf6fbedec52c276434.json
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM items",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "rarity",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "467dcd7fd379750cbf30a190d06dc1b3e61b1bd7ef288fbf6fbedec52c276434"
|
||||
}
|
||||
26
.sqlx/query-4bc709613cface1482452d8bcebf70e822fd3854e4e441e01426fb79c3dfb489.json
generated
Normal file
26
.sqlx/query-4bc709613cface1482452d8bcebf70e822fd3854e4e441e01426fb79c3dfb489.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM items_cases WHERE \"case\" = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "item",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "case",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4bc709613cface1482452d8bcebf70e822fd3854e4e441e01426fb79c3dfb489"
|
||||
}
|
||||
12
.sqlx/query-597ffc913008b9c86d1370a1d6ce3019b17c20e67400211000c395da08b21708.json
generated
Normal file
12
.sqlx/query-597ffc913008b9c86d1370a1d6ce3019b17c20e67400211000c395da08b21708.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO refresh_tokens ('token', 'previous', 'user', 'expiry') VALUES (?1, ?2, ?3, ?4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "597ffc913008b9c86d1370a1d6ce3019b17c20e67400211000c395da08b21708"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM users WHERE username = $1",
|
||||
"query": "SELECT * FROM users WHERE 'id' = ?1 OR 'username' = ?2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
@@ -40,5 +40,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "606364c79e0990deb07dfbe6c32b3d302d083ec5333f3a5ce04113c38a041100"
|
||||
"hash": "6826871e47d86547cc501c6bb14db889beb0dce0fce4ecd22826a482b7558a2f"
|
||||
}
|
||||
50
.sqlx/query-9c9232680b6d9701e9646f546673e54a2ea3a9b6f53a32c79661e6f93c57dcd6.json
generated
Normal file
50
.sqlx/query-9c9232680b6d9701e9646f546673e54a2ea3a9b6f53a32c79661e6f93c57dcd6.json
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM items WHERE uuid = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "rarity",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9c9232680b6d9701e9646f546673e54a2ea3a9b6f53a32c79661e6f93c57dcd6"
|
||||
}
|
||||
@@ -55,6 +55,5 @@ CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
'previous' TEXT,
|
||||
'user' INTEGER,
|
||||
'expiry' INTEGER,
|
||||
'revoked' BOOLEAN,
|
||||
FOREIGN KEY ('user') REFERENCES users ('id')
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
mod cases;
|
||||
mod inventories;
|
||||
mod items;
|
||||
mod types;
|
||||
mod users;
|
||||
@@ -17,7 +16,8 @@ use sqlx::sqlite::SqlitePool;
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
database: SqlitePool,
|
||||
token_expiration: u64,
|
||||
jwt_token_expiration: u64,
|
||||
refresh_token_expiration: u64,
|
||||
allow_origins: Vec<&'static str>,
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ async fn main() -> std::io::Result<()> {
|
||||
.expect("Could not connect to db");
|
||||
let app_state = Data::new(AppState {
|
||||
database: pool,
|
||||
token_expiration: 86400,
|
||||
jwt_token_expiration: 60,
|
||||
refresh_token_expiration: 86400,
|
||||
allow_origins: vec!["http://localhost:5173"],
|
||||
});
|
||||
HttpServer::new(move || {
|
||||
@@ -42,8 +43,8 @@ async fn main() -> std::io::Result<()> {
|
||||
.add(("Access-Control-Allow-Credentials", "true")),
|
||||
)
|
||||
.service(login)
|
||||
.service(register)
|
||||
.service(logout)
|
||||
.service(register)
|
||||
.service(get_case)
|
||||
.service(get_cases)
|
||||
.service(get_item)
|
||||
|
||||
435
src/users.rs
435
src/users.rs
@@ -20,186 +20,319 @@
|
||||
//! Once authenticated, the short-lived token can be renewed
|
||||
//! Here's how it works:
|
||||
//! - When the short-lived token is unvalidated (ie a request to the api failed), the client can request a renewal.
|
||||
//! - The renewal request contains the now unvalid token with the refresh-token cookie.
|
||||
//! - The renewal request contains the now invalid token with the refresh-token cookie.
|
||||
//! - The server checks that the refresh-token is valid (good user, not expired, ...)
|
||||
//! - If checks pass, the server generates a new short-lived token.
|
||||
//! - If it fails, the client redirects the user to login.
|
||||
use crate::AppState;
|
||||
use crate::types::{User, UserLogin, UserRegister, UserTokenClaims};
|
||||
use actix_web::cookie::Cookie;
|
||||
use actix_web::cookie::time::Duration;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, post};
|
||||
use argon2::Argon2;
|
||||
use argon2::password_hash::{
|
||||
PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
|
||||
};
|
||||
use jsonwebtoken::{
|
||||
Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, get_current_timestamp,
|
||||
};
|
||||
use actix_web::{App, HttpRequest, HttpResponse, Responder, post};
|
||||
use argon2::{Argon2, PasswordHasher};
|
||||
use argon2::password_hash::{PasswordHash, PasswordVerifier, SaltString};
|
||||
use argon2::password_hash::rand_core::OsRng;
|
||||
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
|
||||
use rand::Rng;
|
||||
use rand::distr::Alphanumeric;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde_json::{json, to_string};
|
||||
use sqlx::{query, query_as};
|
||||
use sqlx::sqlite::SqliteQueryResult;
|
||||
use sqlx::{Pool, Sqlite, query, query_as};
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::File;
|
||||
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)]
|
||||
struct Password {
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Username {
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct User {
|
||||
id: i64,
|
||||
uuid: String,
|
||||
username: String,
|
||||
email: String,
|
||||
hash: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
async fn fetch_optional(
|
||||
database: &Pool<Sqlite>,
|
||||
id: Option<i64>,
|
||||
uuid: Option<&str>,
|
||||
username: Option<&Username>,
|
||||
) -> Result<Option<User>, sqlx::Error> {
|
||||
match username {
|
||||
Some(username) => {
|
||||
query_as!(
|
||||
User,
|
||||
"SELECT * FROM users WHERE 'id' = ?1 OR 'username' = ?2 or 'uuid' = ?3",
|
||||
id,
|
||||
uuid,
|
||||
username.value
|
||||
)
|
||||
.fetch_optional(database)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
query_as!(
|
||||
User,
|
||||
"SELECT * FROM users WHERE 'id' = ?1 OR 'username' = ?2 OR 'uuid' = ?3",
|
||||
id,
|
||||
uuid,
|
||||
None::<String>
|
||||
)
|
||||
.fetch_optional(database)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 UsernameError;
|
||||
|
||||
impl Display for PasswordError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Invalid password.")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Password {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Password::try_from(value).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Username {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Username::try_from(value).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UsernameError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Invalid username.")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Password {
|
||||
type Error = PasswordError;
|
||||
|
||||
// From String trait that automatically validates the password
|
||||
// and fails if the password is not valid
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
if validate_password(&value) {
|
||||
Ok(Self { value })
|
||||
} else {
|
||||
Err(PasswordError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Username {
|
||||
type Error = UsernameError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
if validate_username(&value) {
|
||||
Ok(Self { value })
|
||||
} else {
|
||||
Err(UsernameError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_password(password: &str) -> bool {
|
||||
let chars: Vec<char> = password.chars().collect();
|
||||
chars.len() < 8 && !chars.iter().any(|x| !x.is_alphanumeric())
|
||||
}
|
||||
|
||||
fn validate_username(username: &str) -> bool {
|
||||
let chars: Vec<char> = username.chars().collect();
|
||||
chars.len() >= 3
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct LoginInfo {
|
||||
username: Username,
|
||||
password: Password,
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
async fn login(
|
||||
user_login: Json<UserLogin>,
|
||||
app_state: Data<AppState>,
|
||||
) -> Result<impl Responder, Box<(dyn Error + 'static)>> {
|
||||
// Verify that the password is correct
|
||||
let argon2 = Argon2::default();
|
||||
Ok(
|
||||
match query_as!(
|
||||
User,
|
||||
"SELECT * FROM users WHERE username = $1",
|
||||
user_login.username
|
||||
)
|
||||
.fetch_optional(&app_state.database)
|
||||
.await?
|
||||
{
|
||||
Some(user) => {
|
||||
let hash = PasswordHash::new(&user.hash)?;
|
||||
if argon2
|
||||
.verify_password(user_login.password.as_bytes(), &hash)
|
||||
.is_err()
|
||||
{
|
||||
return Ok(HttpResponse::BadRequest().finish());
|
||||
}
|
||||
// Create the JWT
|
||||
let header = Header::new(Algorithm::ES256);
|
||||
// Put a random KeyId
|
||||
let mut rng = rand::rng();
|
||||
let key_id: i64 = rng.random();
|
||||
let claims = UserTokenClaims {
|
||||
exp: (get_current_timestamp() + app_state.token_expiration) as usize,
|
||||
kid: key_id,
|
||||
uid: user.uuid.clone(),
|
||||
};
|
||||
let mut key = File::open("priv.pem")?;
|
||||
let mut buf = vec![];
|
||||
key.read_to_end(&mut buf)?;
|
||||
let token = encode(&header, &claims, &EncodingKey::from_ec_pem(&buf)?)?;
|
||||
let user = json!({
|
||||
"uuid": user.uuid,
|
||||
"username": user.username,
|
||||
"token": token,
|
||||
});
|
||||
let user_string = to_string(&user)?;
|
||||
// Send the JWT as cookie
|
||||
HttpResponse::Ok().body(user_string)
|
||||
}
|
||||
None => HttpResponse::BadRequest().finish(),
|
||||
},
|
||||
)
|
||||
login_info: Json<LoginInfo>,
|
||||
) -> Result<impl Responder, Box<dyn Error>> {
|
||||
if let Ok(Some(user)) =
|
||||
User::fetch_optional(&app_state.database, None, None, Some(&login_info.username)).await
|
||||
{
|
||||
if validate_authentication(&user, &login_info.password)? {
|
||||
let jwt_token = generate_jwt(&user, app_state.clone())?;
|
||||
let refresh_token =
|
||||
generate_refresh_token(&user, &jwt_token, app_state.clone()).await?;
|
||||
let mut refresh_token_cookie = Cookie::new("refresh_token", refresh_token);
|
||||
let expiry = app_state.refresh_token_expiration as i64;
|
||||
refresh_token_cookie.set_max_age(Duration::new(expiry, 0));
|
||||
let frontend_data = json!({
|
||||
"uuid": user.uuid,
|
||||
"username": user.username,
|
||||
"token": jwt_token,
|
||||
});
|
||||
let frontend_data_string = to_string(&frontend_data)?;
|
||||
Ok(HttpResponse::Ok()
|
||||
.cookie(refresh_token_cookie)
|
||||
.body(frontend_data_string))
|
||||
} else {
|
||||
Ok(HttpResponse::Unauthorized().finish())
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::Unauthorized().finish())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct LogoutInfo {
|
||||
uuid: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[post("/logout")]
|
||||
async fn logout(
|
||||
req: HttpRequest,
|
||||
app_state: Data<AppState>,
|
||||
) -> Result<impl Responder, Box<(dyn Error + 'static)>> {
|
||||
todo!();
|
||||
// Put the (KeyId, User) pair in the revoked table
|
||||
// And remove data from client
|
||||
// match req.headers().get("Authorization") {
|
||||
// Some(token) => {
|
||||
// let token = token.to_str()?;
|
||||
// let token = match token.split_once(" ") {
|
||||
// Some((_, token)) => token,
|
||||
// None => return Ok(HttpResponse::BadRequest().finish()),
|
||||
// };
|
||||
// let mut key = File::open("pub.pem")?;
|
||||
// let mut buf = vec![];
|
||||
// key.read_to_end(&mut buf)?;
|
||||
// let token = decode::<UserTokenClaims>(
|
||||
// token,
|
||||
// &DecodingKey::from_ec_pem(&buf).unwrap(),
|
||||
// &Validation::new(Algorithm::ES256),
|
||||
// )?;
|
||||
// let exp = token.claims.exp as i64;
|
||||
// query!(
|
||||
// "INSERT INTO revoked ( token_id, user_id, expires ) VALUES ( $1, $2, $3 )",
|
||||
// token.claims.kid,
|
||||
// token.claims.uid,
|
||||
// exp
|
||||
// )
|
||||
// .execute(&app_state.database)
|
||||
// .await?;
|
||||
// Ok(HttpResponse::Ok().finish())
|
||||
// }
|
||||
// None => Ok(HttpResponse::BadRequest().finish()),
|
||||
// }
|
||||
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(
|
||||
user_register: Json<UserRegister>,
|
||||
app_state: Data<AppState>,
|
||||
) -> Result<impl Responder, Box<(dyn Error + 'static)>> {
|
||||
let mut uuid = Uuid::new_v4().to_string();
|
||||
while query!("SELECT (uuid) FROM users WHERE uuid = $1", uuid)
|
||||
.fetch_optional(&app_state.database)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
uuid = Uuid::new_v4().to_string();
|
||||
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(user_register.password.as_bytes(), &salt)?
|
||||
.to_string();
|
||||
query!(
|
||||
"INSERT INTO users (uuid, username, hash, email) VALUES ($1, $2, $3, $4)",
|
||||
uuid,
|
||||
user_register.username,
|
||||
hash,
|
||||
user_register.email
|
||||
)
|
||||
.execute(&app_state.database)
|
||||
.await?;
|
||||
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())
|
||||
}
|
||||
|
||||
async fn verify_token(
|
||||
app_state: Data<AppState>,
|
||||
token: &str,
|
||||
) -> Result<bool, Box<(dyn Error + 'static)>> {
|
||||
todo!();
|
||||
// let mut key = File::open("pub.pem")?;
|
||||
// let mut buf = vec![];
|
||||
// key.read_to_end(&mut buf)?;
|
||||
// let token = decode::<UserTokenClaims>(
|
||||
// token,
|
||||
// &DecodingKey::from_ec_pem(&buf).unwrap(),
|
||||
// &Validation::new(Algorithm::ES256),
|
||||
// )?;
|
||||
// let exp = token.claims.exp as u64;
|
||||
// let now = get_current_timestamp();
|
||||
// if exp > now {
|
||||
// return Ok(false);
|
||||
// }
|
||||
// let kid = token.claims.kid;
|
||||
// let uid = token.claims.uid;
|
||||
// if query!(
|
||||
// "SELECT token_id FROM revoked WHERE token_id = $1 AND user_id = $2",
|
||||
// kid,
|
||||
// uid
|
||||
// )
|
||||
// .fetch_optional(&app_state.database)
|
||||
// .await?
|
||||
// .is_some()
|
||||
// {
|
||||
// return Ok(false);
|
||||
// }
|
||||
// Ok(true)
|
||||
fn validate_authentication(user: &User, password: &Password) -> Result<bool, Box<dyn Error>> {
|
||||
let argon2 = Argon2::default();
|
||||
let hash = PasswordHash::new(&user.hash)?;
|
||||
if argon2
|
||||
.verify_password(password.value.as_bytes(), &hash)
|
||||
.is_err()
|
||||
{
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_jwt(user: &User, app_state: Data<AppState>) -> Result<String, Box<dyn Error>> {
|
||||
let header = Header::new(Algorithm::ES256);
|
||||
let mut rng = rand::rng();
|
||||
let key_id: i64 = rng.random();
|
||||
let claims = json!({
|
||||
"exp": (get_current_timestamp() + app_state.jwt_token_expiration) as usize,
|
||||
"kid": key_id,
|
||||
"uid": user.uuid.clone(),
|
||||
});
|
||||
let mut key = File::open("priv.pem")?;
|
||||
let mut buf = vec![];
|
||||
key.read_to_end(&mut buf)?;
|
||||
Ok(encode(&header, &claims, &EncodingKey::from_ec_pem(&buf)?)?)
|
||||
}
|
||||
|
||||
async fn generate_refresh_token(
|
||||
user: &User,
|
||||
jwt: &str,
|
||||
app_state: Data<AppState>,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
let rng = rand::rng();
|
||||
let token: String = rng
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(256)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let expiry = (get_current_timestamp() + app_state.refresh_token_expiration) as i64;
|
||||
query!(
|
||||
"INSERT INTO refresh_tokens ('token', 'previous', 'user', 'expiry') VALUES (?1, ?2, ?3, ?4)",
|
||||
token,
|
||||
jwt,
|
||||
user.id,
|
||||
expiry
|
||||
)
|
||||
.execute(&app_state.database)
|
||||
.await?;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user