From 57286bb0e7acc32c232db72499471c3882f16f53 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Tue, 30 Jan 2024 20:09:51 -0500 Subject: [PATCH] Updating ui --- generate_keys.sh | 17 +- service/.env.TEMPLATE | 3 +- service/Cargo.toml | 5 +- service/src/auth/mod.rs | 4 +- service/src/auth/model.rs | 64 +---- service/src/auth/routes.rs | 250 ++++-------------- service/src/auth/session.rs | 102 +++++++ service/src/auth/{tokens.rs => token.rs} | 0 service/src/bot/guilds/routes.rs | 24 +- service/src/bot/messages/routes.rs | 6 +- service/src/dnd/spells/routes.rs | 8 +- service/src/lib.rs | 12 +- service/src/users/routes.rs | 8 +- ui/package-lock.json | 124 ++++++++- ui/package.json | 1 + ui/src/api/auth.ts | 72 ----- .../api/auth/[...nextauth]/route.ts} | 0 ui/src/app/api/auth/index.ts | 32 +++ .../auth.types.ts => app/api/auth/types.ts} | 14 +- .../guilds.ts => app/api/guilds/index.ts} | 4 +- .../api/guilds/types.ts} | 0 ui/src/{ => app}/api/index.ts | 5 + .../spells.ts => app/api/spells/index.ts} | 4 +- .../api/spells/types.ts} | 2 +- .../{api/users.ts => app/api/users/index.ts} | 2 +- ui/src/app/layout.tsx | 7 +- ui/src/components/Auth.tsx | 2 - ui/src/components/Header/HeaderModal.tsx | 23 +- ui/src/components/Header/index.tsx | 12 +- ui/src/components/{Loading.tsx => Loader.tsx} | 32 +-- ui/src/middleware.ts | 4 +- ui/tsconfig.json | 2 +- 32 files changed, 420 insertions(+), 425 deletions(-) create mode 100644 service/src/auth/session.rs rename service/src/auth/{tokens.rs => token.rs} (100%) delete mode 100644 ui/src/api/auth.ts rename ui/src/{api/characters.types.ts => app/api/auth/[...nextauth]/route.ts} (100%) create mode 100644 ui/src/app/api/auth/index.ts rename ui/src/{api/auth.types.ts => app/api/auth/types.ts} (50%) rename ui/src/{api/guilds.ts => app/api/guilds/index.ts} (94%) rename ui/src/{api/guilds.types.ts => app/api/guilds/types.ts} (100%) rename ui/src/{ => app}/api/index.ts (95%) rename ui/src/{api/spells.ts => app/api/spells/index.ts} (92%) rename ui/src/{api/spells.types.ts => app/api/spells/types.ts} (97%) rename ui/src/{api/users.ts => app/api/users/index.ts} (92%) rename ui/src/components/{Loading.tsx => Loader.tsx} (54%) diff --git a/generate_keys.sh b/generate_keys.sh index cb3cd86..7653c32 100755 --- a/generate_keys.sh +++ b/generate_keys.sh @@ -11,11 +11,16 @@ echo "Generating public/private keys in: $DIR" mkdir -p "$DIR" # Remove any existing keys -rm -f $DIR/private_key.pem -rm -f $DIR/public_key.pem +rm -f $DIR/*_private_key.pem +rm -f $DIR/*_public_key.pem # Generate Keys -openssl genrsa -out $DIR/private_key.pem 4096 -openssl rsa -in $DIR/private_key.pem -pubout -outform PEM -out $DIR/public_key.pem -chmod 600 $DIR/private_key.pem -chmod 644 $DIR/public_key.pem +openssl genrsa -out $DIR/access_private_key.pem 4096 +openssl rsa -in $DIR/access_private_key.pem -pubout -outform PEM -out $DIR/access_public_key.pem +chmod 600 $DIR/access_private_key.pem +chmod 644 $DIR/access_public_key.pem + +openssl genrsa -out $DIR/refresh_private_key.pem 4096 +openssl rsa -in $DIR/refresh_private_key.pem -pubout -outform PEM -out $DIR/refresh_public_key.pem +chmod 600 $DIR/refresh_private_key.pem +chmod 644 $DIR/refresh_public_key.pem diff --git a/service/.env.TEMPLATE b/service/.env.TEMPLATE index 5a1a185..5e229dc 100644 --- a/service/.env.TEMPLATE +++ b/service/.env.TEMPLATE @@ -7,8 +7,7 @@ DATABASE_HOST=localhost DATABASE_PORT=5432 KEYS_DIR_PATH= -ACCESS_TOKEN_MAXAGE=5 -REFRESH_TOKEN_MAXAGE=30 +SESSION_TTL=1440 REDIS_HOST=localhost REDIS_PORT=6379 diff --git a/service/Cargo.toml b/service/Cargo.toml index b677d7f..30acc1d 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -25,15 +25,12 @@ r2d2 = "0.8.10" lazy_static = "1.4.0" uuid = { version = "1.4.1", features = ["serde", "v4"] } argon2 = "0.5.2" -jsonwebtoken = "9.0.0" redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] } -base64 = "0.21.4" rust-s3 = "0.33.0" actix-multipart = "0.6.1" -openssl = "0.10.60" # Resolve `openssl` `X509StoreRef::objects` is unsound #10 rand = "0.8.5" -sha2 = "0.10.8" rand_chacha = "0.3.1" +jsonwebtoken = "9.2.0" [dependencies.tokio] version = "1.32.0" diff --git a/service/src/auth/mod.rs b/service/src/auth/mod.rs index dbc5e5d..f0e12ee 100644 --- a/service/src/auth/mod.rs +++ b/service/src/auth/mod.rs @@ -1,7 +1,7 @@ mod model; mod routes; -mod tokens; +mod session; pub use model::*; -pub use tokens::*; +pub use session::*; pub use routes::init_routes; diff --git a/service/src/auth/model.rs b/service/src/auth/model.rs index 86a8ba0..17f78d0 100644 --- a/service/src/auth/model.rs +++ b/service/src/auth/model.rs @@ -1,14 +1,12 @@ -use std::{future::{ready, Ready}, env}; +use std::future::{ready, Ready}; use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http}; use diesel::prelude::*; -use log::error; -use redis::Commands; use serde::{Serialize, Deserialize}; use siren::ServiceError; use crate::storage::{schema::users, connection}; -use super::{hash, AccessToken}; +use super::{hash, Session, SESSION_COOKIE_NAME}; #[derive(Debug, Serialize, Deserialize)] pub struct RegisterUser { @@ -58,7 +56,6 @@ impl QueryUser { pub fn get_by_email(email: &str) -> Result { let mut conn = connection()?; // Check if the user exists by email, case insensitive - let user = users::table .filter(users::email.eq(email.to_lowercase())) .first(&mut conn)?; @@ -121,17 +118,17 @@ impl From for ResponseUser { } #[derive(Debug, Serialize, Deserialize)] -pub struct JwtAuth { +pub struct Auth { pub id: String, pub user: ResponseUser } -impl FromRequest for JwtAuth { +impl FromRequest for Auth { type Error = ActixError; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - let access_token_string = match req - .cookie("access_token") + let session_id = match req + .cookie(SESSION_COOKIE_NAME) .map(|c| c.value().to_string()) .or_else(|| { req.headers().get(http::header::AUTHORIZATION) @@ -143,54 +140,17 @@ impl FromRequest for JwtAuth { message: "Unauthorized".to_string() }))) }; - - let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); - let public_key = std::fs::read_to_string(format!("{}/public_key.pem", keys_dir)).expect("Failed to read access public key"); - - let access_token = match AccessToken::decode(&access_token_string, &public_key) { - Ok(token_details) => token_details, - Err(err) => { - error!("Failed to verify access token: {}", err); - return ready(Err(ActixError::from(ServiceError { - status: 401, - message: format!("Access token is invaid: {}", err) - }))) - } - }; - let mut conn = match crate::storage::redis_connection() { - Ok(conn) => conn, - Err(err) => { - error!("Failed to get redis connection: {}", err); - return ready(Err(ActixError::from(ServiceError { - status: 500, - message: format!("Failed to get redis connection: {}", err) - }))) - } - }; - let user_email = match conn.get::<_, String>(access_token.id.clone().to_string()) { - Ok(result) => serde_json::from_str::(&result).unwrap().email, - Err(_) => { - return ready(Err(ActixError::from(ServiceError { - status: 401, - message: format!("Access token is invalid") - }))) - } - }; - - match QueryUser::get_by_email(&user_email) { - Ok(user) => { - ready(Ok(JwtAuth { id: access_token.id, user: user.into() })) - } - Err(_) => return ready(Err(ActixError::from(ServiceError { - status: 401, - message: format!("User does not exist") - }))) + let ip_address = req.peer_addr().unwrap().ip().to_string(); + + match Session::verify(&session_id, &ip_address) { + Ok(v) => return ready(Ok(Auth { id: v.0.id, user: v.1.into() })), + Err(err) => return ready(Err(ActixError::from(err))) } } } -pub fn verify_role(auth: &JwtAuth, role: &str) -> Result<(), ServiceError> { +pub fn verify_role(auth: &Auth, role: &str) -> Result<(), ServiceError> { if auth.user.role == role { Ok(()) } else { diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index 36cdfbc..085c393 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -3,10 +3,9 @@ use std::env; use actix_web::{get, post, web, HttpResponse, ResponseError, cookie::{Cookie, time::Duration}, HttpRequest}; use log::error; use redis::AsyncCommands; -use serde::{Serialize, Deserialize}; use siren::ServiceError; -use crate::{auth::{InsertUser, JwtAuth, LoginRequest, QueryUser, RefreshToken, RegisterUser}, storage}; +use crate::{auth::{InsertUser, Auth, LoginRequest, QueryUser, RegisterUser, Session, SESSION_COOKIE_NAME}, storage::{self}}; use super::verify_hash; @@ -47,14 +46,7 @@ async fn login(request: HttpRequest, login_request: web::Json) -> }; // Verify password if verify_hash(&login_request.password, &query_user.hash) { - let mut refresh_token = RefreshToken::new(&email, &ip_address); - let access_token = match refresh_token.create_access_token() { - Ok(token) => token, - Err(err) => { - error!("Failed to generate access token: {}", err); - return ResponseError::error_response(&err) - } - }; + let session = Session::new(&email, &ip_address); let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, @@ -64,51 +56,33 @@ async fn login(request: HttpRequest, login_request: web::Json) -> } }; - let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE") - .expect("ACCESS_TOKEN_MAXAGE must be set") + let session_ttl = env::var("SESSION_TTL") + .expect("SESSION_TTL must be set") .parse::() - .expect("ACCESS_TOKEN_MAXAGE must be an integer"); + .expect("SESSION_TTL must be an integer"); - let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE") - .expect("REFRESH_TOKEN_MAXAGE must be set") - .parse::() - .expect("REFRESH_TOKEN_MAXAGE must be an integer"); - - let access_result: redis::RedisResult<()> = conn.set_ex(access_token.id.to_string(), &serde_json::to_string(&access_token).unwrap(), (access_token_max_age * 60) as usize).await; - if let Err(err) = access_result { + let session_result: redis::RedisResult<()> = conn.set_ex(session.id.to_string(), &serde_json::to_string(&session).unwrap(), (session_ttl * 60) as usize).await; + if let Err(err) = session_result { error!("Failed to set access token in redis: {}", err); return ResponseError::error_response(&ServiceError::from(err)) }; - let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.id.to_string(), &serde_json::to_string(&refresh_token).unwrap(), (refresh_token_max_age * 60) as usize).await; - if let Err(err) = refresh_result { - error!("Failed to set refresh token in redis: {}", err); - return ResponseError::error_response(&ServiceError::from(err)) - }; - - let access_cookie = Cookie::build("access_token", access_token.token.clone().unwrap()) + let session_cookie = Cookie::build(SESSION_COOKIE_NAME, session.id.clone()) .path("/") - .max_age(Duration::new(access_token_max_age * 60, 0)) + .max_age(Duration::new(session_ttl * 60, 0)) .http_only(true) .secure(true) .finish(); - let refresh_cookie = Cookie::build("refresh_token", refresh_token.id.clone()) + let user_id_cookie = Cookie::build("user_id", session.user_id.clone()) .path("/") - .max_age(Duration::new(refresh_token_max_age * 60, 0)) - .http_only(true) - .secure(true) - .finish(); - let logged_in_cookie = Cookie::build("logged_in", "true") - .path("/") - .max_age(Duration::new(access_token_max_age * 60, 0)) + .max_age(Duration::new(session_ttl * 60, 0)) .http_only(false) .finish(); HttpResponse::Ok() - .cookie(access_cookie) - .cookie(refresh_cookie) - .cookie(logged_in_cookie) - .json(JwtAuth { id: access_token.id, user: query_user.into() }) + .cookie(session_cookie) + .cookie(user_id_cookie) + .json(Auth { id: session.id, user: query_user.into() }) } else { return ResponseError::error_response(&ServiceError { status: 401, @@ -117,29 +91,9 @@ async fn login(request: HttpRequest, login_request: web::Json) -> } } -#[derive(Serialize, Deserialize)] -struct RefreshParams { - refresh_token_rotation: Option -} - #[get("/refresh")] -async fn refresh(req: HttpRequest) -> HttpResponse { +async fn refresh(req: HttpRequest, auth: Auth) -> HttpResponse { let ip_address = req.peer_addr().unwrap().ip().to_string(); - let params = match web::Query::::from_query(req.query_string()) { - Ok(params) => params, - Err(err) => return ResponseError::error_response(&ServiceError { - status: 422, - message: err.to_string() - }) - }; - - let refresh_token_string = match req.cookie("refresh_token") { - Some(cookie) => cookie.value().to_string(), - None => return ResponseError::error_response(&ServiceError { - status: 401, - message: "Refresh token not found".to_string() - }) - }; let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, @@ -149,129 +103,38 @@ async fn refresh(req: HttpRequest) -> HttpResponse { } }; - let mut refresh_token: RefreshToken = match conn.get::<_, String>(refresh_token_string.clone()).await { - Ok(result) => match serde_json::from_str::(&result) { - Ok(result) => { - if verify_hash(&ip_address, &result.ip_address) { - result - } else { - return ResponseError::error_response(&ServiceError { - status: 401, - message: "Refresh token is invalid".to_string() - }) - } - }, - Err(err) => { - error!("Failed to deserialize refresh token: {}", err); - return ResponseError::error_response(&ServiceError::from(err)) - } - }, - Err(err) => { - error!("Failed to get refresh token from redis: {}", err); - return ResponseError::error_response(&ServiceError::from(err)) - } + let session_ttl = env::var("SESSION_TTL") + .expect("SESSION_TTL must be set") + .parse::() + .expect("SESSION_TTL must be an integer"); + + // Delete old session + let _: redis::RedisResult<()> = conn.del(auth.id).await; + + // Create new session + let session = Session::new(&auth.user.email, &ip_address); + let session_result: redis::RedisResult<()> = conn.set_ex(session.id.to_string(), &serde_json::to_string(&session).unwrap(), (session_ttl * 60) as usize).await; + if let Err(err) = session_result { + error!("Failed to set session id in redis: {}", err); + return ResponseError::error_response(&ServiceError::from(err)) }; - let email = refresh_token.email.clone(); + // Create cookies + let session_cookie = session.create_cookie(); + let user_id_cookie = Cookie::build("user_id", session.user_id.clone()) + .path("/") + .max_age(Duration::new(session_ttl * 60, 0)) + .http_only(false) + .finish(); - match QueryUser::get_by_email(&email) { - Ok(query_user) => { - // Revoke all old access tokens - for id in refresh_token.tokens { - let _: redis::RedisResult<()> = conn.del(id).await; - } - refresh_token.tokens = vec![]; - - // Create new access token - let access_token = match refresh_token.create_access_token() { - Ok(token) => token, - Err(err) => { - error!("Failed to generate access token: {}", err); - return ResponseError::error_response(&err) - } - }; - - let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE") - .expect("ACCESS_TOKEN_MAXAGE must be set") - .parse::() - .expect("ACCESS_TOKEN_MAXAGE must be an integer"); - - let access_result: redis::RedisResult<()> = conn.set_ex(access_token.id.to_string(), &serde_json::to_string(&access_token).unwrap(), (access_token_max_age * 60) as usize).await; - if let Err(err) = access_result { - error!("Failed to set access token in redis: {}", err); - return ResponseError::error_response(&ServiceError::from(err)) - }; - - let access_cookie = Cookie::build("access_token", access_token.id.clone()) - .path("/") - .max_age(Duration::new(access_token_max_age * 60, 0)) - .http_only(true) - .secure(true) - .finish(); - let logged_in_cookie = Cookie::build("logged_in", "true") - .path("/") - .max_age(Duration::new(access_token_max_age * 60, 0)) - .http_only(false) - .finish(); - - // Refresh the refresh token if requested - let refresh_token_rotation = match params.refresh_token_rotation { - Some(refresh_token_rotation) => refresh_token_rotation, - None => false - }; - if refresh_token_rotation { - // Delete the old refresh token from redis - let _: redis::RedisResult<()> = conn.del(refresh_token.id.to_string()).await; - - let refresh_token = RefreshToken::new(&refresh_token.email, &ip_address); - let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE") - .expect("REFRESH_TOKEN_MAXAGE must be set") - .parse::() - .expect("REFRESH_TOKEN_MAXAGE must be an integer"); - - // Add the new refresh token to redis - let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.id.to_string(), &serde_json::to_string(&refresh_token).unwrap(), (refresh_token_max_age * 60) as usize).await; - if let Err(err) = refresh_result { - error!("Failed to set refresh token in redis: {}", err); - return ResponseError::error_response(&ServiceError { - status: 500, - message: format!("Failed to set refresh token in redis: {}", err) - }) - }; - - let refresh_cookie = Cookie::build("refresh_token", refresh_token.id.clone()) - .path("/") - .max_age(Duration::new(refresh_token_max_age * 60, 0)) - .http_only(true) - .secure(true) - .finish(); - - HttpResponse::Ok() - .cookie(refresh_cookie) - .cookie(access_cookie) - .cookie(logged_in_cookie) - .json(JwtAuth { id: access_token.id, user: query_user.into() }) - } else { - HttpResponse::Ok() - .cookie(access_cookie) - .cookie(logged_in_cookie) - .json(JwtAuth { id: access_token.id, user: query_user.into() }) - } - }, - Err(err) => return ResponseError::error_response(&err) - } + HttpResponse::Ok() + .cookie(session_cookie) + .cookie(user_id_cookie) + .json(Auth { id: session.id, user: auth.user }) } #[post("/logout")] -async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { - let refresh_token = match req.cookie("refresh_token") { - Some(cookie) => cookie.value().to_string(), - None => return ResponseError::error_response(&ServiceError { - status: 401, - message: "Refresh token not found".to_string() - }) - }; - +async fn logout(auth: Auth) -> HttpResponse { let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { @@ -280,45 +143,32 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { } }; - let access_result: redis::RedisResult<()> = conn.del(&[ - refresh_token.to_string(), - auth.id.to_string() - ]).await; - if let Err(err) = access_result { - error!("Failed to set access token in redis: {}", err); - return ResponseError::error_response(&ServiceError { - status: 500, - message: format!("Failed to set access token in redis: {}", err) - }) + let session_result: redis::RedisResult<()> = conn.del(&auth.id.to_string()).await; + if let Err(err) = session_result { + error!("Failed to remove session id in redis: {}", err); + return ResponseError::error_response(&ServiceError::from(err)) }; - let access_cookie = Cookie::build("access_token", "") + let session_cookie = Cookie::build(SESSION_COOKIE_NAME, "") .path("/") .max_age(Duration::new(-1, 0)) .secure(true) .http_only(true) .finish(); - let refresh_cookie = Cookie::build("refresh_token", "") - .path("/") - .max_age(Duration::new(-1, 0)) - .secure(true) - .http_only(true) - .finish(); - let logged_in_cookie = Cookie::build("logged_in", "") + let user_id_cookie = Cookie::build("user_id", "") .path("/") .max_age(Duration::new(-1, 0)) .http_only(true) .finish(); HttpResponse::Ok() - .cookie(access_cookie) - .cookie(refresh_cookie) - .cookie(logged_in_cookie) + .cookie(session_cookie) + .cookie(user_id_cookie) .finish() } #[get("/me")] -async fn me(auth: JwtAuth) -> HttpResponse { +async fn me(auth: Auth) -> HttpResponse { HttpResponse::Ok().json(auth) } diff --git a/service/src/auth/session.rs b/service/src/auth/session.rs new file mode 100644 index 0000000..50bfa8c --- /dev/null +++ b/service/src/auth/session.rs @@ -0,0 +1,102 @@ +use std::env; + +use actix_web::cookie::{time::Duration, Cookie}; +use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use rand::prelude::*; +use rand_chacha::ChaCha20Rng; +use redis::Commands; +use serde::{Deserialize, Serialize}; +use siren::ServiceError; + +use super::QueryUser; + +pub const SESSION_COOKIE_NAME: &str = "session"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Session { + pub id: String, + pub user_id: String, + pub ip_address: String, + pub expiration: i64, +} + +impl Session { + pub fn new(user_id: &str, ip_address: &str) -> Self { + let ttl = env::var("SESSION_TTL") + .expect("SESSION_TTL must be set") + .parse::() + .expect("SESSION_TTL must be an integer"); + let now = chrono::Utc::now(); + Self { + id: csprng_128bit(), + user_id: user_id.to_string(), + ip_address: hash(&ip_address).unwrap(), + expiration: (now + chrono::Duration::minutes(ttl)).timestamp() + } + } + + pub fn verify(session_id: &str, ip_address: &str) -> Result<(Self, QueryUser), ServiceError> { + let mut conn = crate::storage::redis_connection()?; + // Check if the session exists + let session = match conn.get::<_, String>(session_id) { + Ok(session) => session, + Err(_) => return Err(ServiceError::new(401, "Unauthorized".to_string())) + }; + let session: Self = serde_json::from_str(&session)?; + // Check if the IP address matches + let session_ip_address = session.ip_address.clone(); + let session_user_id = session.user_id.clone(); + if verify_hash(ip_address, &session_ip_address) { + let email = session_user_id; + // Check if the user exists + let user = match crate::auth::model::QueryUser::get_by_email(&email) { + Ok(user) => user, + Err(_) => return Err(ServiceError::new(401, "Unauthorized".to_string())) + }; + // Check if the session has expired + let now = chrono::Utc::now().timestamp(); + if now < session.expiration { + return Ok((session, user)) + } + } + Err(ServiceError::new(401, "Unauthorized".to_string())) + } + + pub fn create_cookie(&self) -> Cookie { + let ttl = env::var("SESSION_TTL") + .expect("SESSION_TTL must be set") + .parse::() + .expect("SESSION_TTL must be an integer"); + Cookie::build(SESSION_COOKIE_NAME, self.id.clone()) + .path("/") + .max_age(Duration::new(ttl * 60, 0)) + .secure(true) + .http_only(true) + .finish() + } +} + +fn csprng_128bit() -> String { + // Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9) + let rng = ChaCha20Rng::from_entropy(); + rng.sample_iter(rand::distributions::Alphanumeric).take(16).map(char::from).collect() +} + +pub fn hash(str: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let bytes = str.as_bytes(); + let hash = Argon2::default().hash_password(bytes, &salt)?.to_string(); + Ok(hash) +} + +pub fn verify_hash(str: &str, hash: &str) -> bool { + let bytes = str.as_bytes(); + let parsed_hash = match PasswordHash::new(hash) { + Ok(h) => h, + Err(_) => return false + }; + match Argon2::default().verify_password(bytes, &parsed_hash) { + Ok(_) => true, + Err(_) => false + } +} \ No newline at end of file diff --git a/service/src/auth/tokens.rs b/service/src/auth/token.rs similarity index 100% rename from service/src/auth/tokens.rs rename to service/src/auth/token.rs diff --git a/service/src/bot/guilds/routes.rs b/service/src/bot/guilds/routes.rs index eaf8f10..7b17aef 100644 --- a/service/src/bot/guilds/routes.rs +++ b/service/src/bot/guilds/routes.rs @@ -5,10 +5,10 @@ use serde::{Serialize, Deserialize}; use serenity::model::prelude::{GuildChannel, ChannelType}; use siren::{ServiceError, Response}; -use crate::{AppState, bot::commands::audio::{play::play_track, join}, bot::guilds::QueryGuild, auth::{JwtAuth, verify_role}}; +use crate::{AppState, bot::commands::audio::{play::play_track, join}, bot::guilds::QueryGuild, auth::{Auth, verify_role}}; #[get("/guilds")] -async fn get_guilds(data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn get_guilds(data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -27,7 +27,7 @@ async fn get_guilds(data: web::Data>, auth: JwtAuth) -> HttpRespon } #[get("/{id}/text")] -async fn get_text_channels(id: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn get_text_channels(id: web::Path, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -46,7 +46,7 @@ async fn get_text_channels(id: web::Path, data: web::Data> } #[get("/{id}/voice")] -async fn get_voice_channels(id: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn get_voice_channels(id: web::Path, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -70,7 +70,7 @@ struct ChannelMessage { } #[post("/{guild_id}/text/{channel_id}/message")] -async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -130,7 +130,7 @@ struct PlayRequest { } #[post("/{guild_id}/voice/{channel_id}/play")] -async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -179,7 +179,7 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn stop(path: web::Path, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -203,7 +203,7 @@ async fn stop(path: web::Path, data: web::Data>, auth: Jwt } #[post("/{guild_id}/voice/resume")] -async fn resume(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn resume(path: web::Path, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -232,7 +232,7 @@ async fn resume(path: web::Path, data: web::Data>, auth: J } #[post("/{guild_id}/voice/pause")] -async fn pause(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn pause(path: web::Path, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -266,7 +266,7 @@ struct SetVolume { } #[get("/{guild_id}/voice/volume")] -async fn get_volume(path: web::Path, auth: JwtAuth) -> HttpResponse { +async fn get_volume(path: web::Path, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -295,7 +295,7 @@ async fn get_volume(path: web::Path, auth: JwtAuth) -> HttpResponse { } #[post("/{guild_id}/voice/volume")] -async fn set_volume(path: web::Path, volume: web::Json::, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn set_volume(path: web::Path, volume: web::Json::, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; @@ -325,7 +325,7 @@ async fn set_volume(path: web::Path, volume: web::Json::, dat } #[post("/{guild_id}/voice/skip")] -async fn skip(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { +async fn skip(path: web::Path, data: web::Data>, auth: Auth) -> HttpResponse { if let Err(err) = verify_role(&auth, "admin") { return ResponseError::error_response(&err) }; diff --git a/service/src/bot/messages/routes.rs b/service/src/bot/messages/routes.rs index 584bdb7..e1d9e42 100644 --- a/service/src/bot/messages/routes.rs +++ b/service/src/bot/messages/routes.rs @@ -3,7 +3,7 @@ use log::error; use serde::{Serialize, Deserialize}; use siren::{Response, Metadata, ServiceError}; -use crate::{bot::messages::{QueryMessage, QueryFilters}, auth::{JwtAuth, verify_role}}; +use crate::{bot::messages::{QueryMessage, QueryFilters}, auth::{Auth, verify_role}}; #[derive(Serialize, Deserialize)] struct GetAllParams { @@ -21,7 +21,7 @@ struct GetAllParams { } #[get("/messages")] -async fn get_all(req: HttpRequest, auth: JwtAuth) -> HttpResponse { +async fn get_all(req: HttpRequest, auth: Auth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) @@ -68,7 +68,7 @@ async fn get_all(req: HttpRequest, auth: JwtAuth) -> HttpResponse { } #[post("/messages")] -async fn create(message: web::Json, auth: JwtAuth) -> HttpResponse { +async fn create(message: web::Json, auth: Auth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) diff --git a/service/src/dnd/spells/routes.rs b/service/src/dnd/spells/routes.rs index 095028f..5ed0e7b 100644 --- a/service/src/dnd/spells/routes.rs +++ b/service/src/dnd/spells/routes.rs @@ -3,7 +3,7 @@ use log::error; use serde::{Serialize, Deserialize}; use siren::{Response, Metadata, ServiceError}; -use crate::{dnd::spells::{QuerySpell, QueryFilters}, auth::{JwtAuth, verify_role}}; +use crate::{dnd::spells::{QuerySpell, QueryFilters}, auth::{Auth, verify_role}}; use super::{Spell, InsertSpell}; @@ -134,7 +134,7 @@ async fn get_by_id(id: web::Path) -> HttpResponse { } #[post("/spells")] -async fn create(spell: web::Json, auth: JwtAuth) -> HttpResponse { +async fn create(spell: web::Json, auth: Auth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) @@ -149,7 +149,7 @@ async fn create(spell: web::Json, auth: JwtAuth) -> HttpResponse { } #[put("/spells/{id}")] -async fn update(id: web::Path, spell: web::Json, auth: JwtAuth) -> HttpResponse { +async fn update(id: web::Path, spell: web::Json, auth: Auth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) @@ -171,7 +171,7 @@ async fn update(id: web::Path, spell: web::Json, auth: JwtAuth) - } #[delete("/spells/{id}")] -async fn delete(id: web::Path, auth: JwtAuth) -> HttpResponse { +async fn delete(id: web::Path, auth: Auth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) diff --git a/service/src/lib.rs b/service/src/lib.rs index 27994f7..f5870b9 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -112,12 +112,6 @@ impl From for ServiceError { } } -impl From for ServiceError { - fn from(error: jsonwebtoken::errors::Error) -> ServiceError { - ServiceError::new(500, format!("Unknown jsonwebtoken error: {}", error)) - } -} - impl From for ServiceError { fn from(error: redis::RedisError) -> ServiceError { ServiceError::new(500, format!("Unknown redis error: {}", error)) @@ -153,6 +147,12 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(error: jsonwebtoken::errors::Error) -> ServiceError { + ServiceError::new(500, format!("Unknown jsonwebtoken error: {}", error)) + } +} + impl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { let status_code = match StatusCode::from_u16(self.status) { diff --git a/service/src/users/routes.rs b/service/src/users/routes.rs index 0e4babd..a878bda 100644 --- a/service/src/users/routes.rs +++ b/service/src/users/routes.rs @@ -4,10 +4,10 @@ use log::error; use serenity::futures::StreamExt; use siren::ServiceError; -use crate::{auth::{JwtAuth, InsertUser, QueryUser}, storage::{upload_file, get_file, delete_file}}; +use crate::{auth::{Auth, InsertUser, QueryUser}, storage::{upload_file, get_file, delete_file}}; #[post("/picture")] -async fn set_picture(mut payload: Multipart, auth: JwtAuth) -> HttpResponse { +async fn set_picture(mut payload: Multipart, auth: Auth) -> HttpResponse { while let Some(item) = payload.next().await { let mut bytes = web::BytesMut::new(); let mut field = match item { @@ -73,7 +73,7 @@ async fn set_picture(mut payload: Multipart, auth: JwtAuth) -> HttpResponse { } #[get("/picture")] -async fn get_picture(auth: JwtAuth) -> HttpResponse { +async fn get_picture(auth: Auth) -> HttpResponse { let user = match QueryUser::get_by_email(&auth.user.email) { Ok(user) => user, Err(err) => { @@ -95,7 +95,7 @@ async fn get_picture(auth: JwtAuth) -> HttpResponse { } #[delete("/picture")] -async fn delete_picture(auth: JwtAuth) -> HttpResponse { +async fn delete_picture(auth: Auth) -> HttpResponse { match QueryUser::get_by_email(&auth.user.email) { Ok(user) => { match user.profile_picture { diff --git a/ui/package-lock.json b/ui/package-lock.json index d542478..b59df67 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -16,6 +16,7 @@ "@pixi/react": "^7.1.1", "js-cookie": "^3.0.5", "next": "^14.1.0", + "next-auth": "^4.24.5", "pixi.js": "^7.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -579,6 +580,14 @@ "node": ">= 8" } }, + "node_modules/@panva/hkdf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz", + "integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pixi/app": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.3.2.tgz", @@ -1646,6 +1655,14 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3274,6 +3291,14 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", @@ -3435,7 +3460,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3569,6 +3593,33 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.5.tgz", + "integrity": "sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.5.0", + "jose": "^4.11.4", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "next": "^12.2.5 || ^13 || ^14", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18", + "react-dom": "^17.0.2 || ^18" + }, + "peerDependenciesMeta": { + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -3611,6 +3662,11 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3619,6 +3675,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -3727,6 +3791,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3736,6 +3808,20 @@ "wrappy": "1" } }, + "node_modules/openid-client": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.4.tgz", + "integrity": "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==", + "dependencies": { + "jose": "^4.15.4", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -4430,6 +4516,26 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/preact": { + "version": "10.19.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", + "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4466,6 +4572,11 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5503,6 +5614,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5697,8 +5816,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yocto-queue": { "version": "0.1.0", diff --git a/ui/package.json b/ui/package.json index 8a826fd..4ef98d8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,6 +17,7 @@ "@pixi/react": "^7.1.1", "js-cookie": "^3.0.5", "next": "^14.1.0", + "next-auth": "^4.24.5", "pixi.js": "^7.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/ui/src/api/auth.ts b/ui/src/api/auth.ts deleted file mode 100644 index 319a36b..0000000 --- a/ui/src/api/auth.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Cookies from 'js-cookie'; -import { getRequest, postRequest } from '.'; -import { RegisterUser, ResponseAuth } from './auth.types'; - -export async function login(email: string, password: string): Promise { - const response = await postRequest('auth/login', { email, password }); - if (response?.status === 200) { - return response.json(); - } else { - return undefined; - } -} - -export async function register(user: RegisterUser): Promise { - const response = await postRequest('auth/register', user); - if (response?.status === 201) { - return true; - } else { - return false; - } -} - -export async function logout() { - return await postRequest('auth/logout', {}); -} - -export async function refresh(refresh_token_rotation?: boolean): Promise { - const response = await getRequest('auth/refresh', { refresh_token_rotation }); - if (response?.status === 200) { - return response.json(); - } else { - return undefined; - } -} - -export async function me(): Promise { - const response = await getRequest('auth/me'); - if (response?.status === 200) { - return response.json(); - } else { - return undefined; - } -} - -export async function hasSession(): Promise { - const response = await getRequest('auth/session'); - if (response?.status === 200) { - return response?.json(); - } else { - return false; - } -} - -/** - * Refreshes the logged_in cookie every interval. By default, the interval is 14 minutes. - * @param interval - * @returns interval id - */ -export function refreshLoggedIn(interval = 840000) { - let loggedIn = Cookies.get('logged_in'); - const id = setInterval(async () => { - const cookie = Cookies.get('logged_in'); - if (cookie != loggedIn) { - loggedIn = cookie; - const response = await refresh(true); - if (!response) { - Cookies.remove('logged_in'); - } - } - }, interval); - return id; -} diff --git a/ui/src/api/characters.types.ts b/ui/src/app/api/auth/[...nextauth]/route.ts similarity index 100% rename from ui/src/api/characters.types.ts rename to ui/src/app/api/auth/[...nextauth]/route.ts diff --git a/ui/src/app/api/auth/index.ts b/ui/src/app/api/auth/index.ts new file mode 100644 index 0000000..537207b --- /dev/null +++ b/ui/src/app/api/auth/index.ts @@ -0,0 +1,32 @@ +import { getRequest, postRequest } from '..'; +import { BaseResponse, RegisterUser } from './types'; + +export async function login(email: string, password: string): Promise { + const response = await postRequest('auth/login', { email, password }); + const data = await response.json(); + return { data, status: response.status }; +} + +export async function register(user: RegisterUser): Promise { + const response = await postRequest('auth/register', user); + const data = await response.json(); + return { data, status: response.status }; +} + +export async function logout(): Promise { + const response = await postRequest('auth/logout', {}); + const data = await response.json(); + return { data, status: response.status }; +} + +export async function refresh(): Promise { + const response = await getRequest('auth/refresh'); + const data = await response.json(); + return { data, status: response.status }; +} + +export async function me(): Promise { + const response = await getRequest('auth/me'); + const data = await response.json(); + return { data, status: response.status }; +} diff --git a/ui/src/api/auth.types.ts b/ui/src/app/api/auth/types.ts similarity index 50% rename from ui/src/api/auth.types.ts rename to ui/src/app/api/auth/types.ts index 76ac70a..91a2767 100644 --- a/ui/src/api/auth.types.ts +++ b/ui/src/app/api/auth/types.ts @@ -1,8 +1,18 @@ -export interface ResponseAuth { - token: string; +import { ErrorResponse } from ".."; + +export interface AuthResponse { + id: string; user: User; } +// AuthResponse can be either a success or an error +export type AuthType = AuthResponse | ErrorResponse; + +export interface BaseResponse { + data: AuthType; + status: number; +} + export interface RegisterUser { email: string; password: string; diff --git a/ui/src/api/guilds.ts b/ui/src/app/api/guilds/index.ts similarity index 94% rename from ui/src/api/guilds.ts rename to ui/src/app/api/guilds/index.ts index c8984f1..fea7877 100644 --- a/ui/src/api/guilds.ts +++ b/ui/src/app/api/guilds/index.ts @@ -1,5 +1,5 @@ -import { APIResponse, getRequest, postRequest } from '.'; -import { GuildChannel, GuildInfo } from './guilds.types'; +import { APIResponse, getRequest, postRequest } from '..'; +import { GuildChannel, GuildInfo } from './types'; export async function getGuilds(): Promise { const response = await getRequest('guilds'); diff --git a/ui/src/api/guilds.types.ts b/ui/src/app/api/guilds/types.ts similarity index 100% rename from ui/src/api/guilds.types.ts rename to ui/src/app/api/guilds/types.ts diff --git a/ui/src/api/index.ts b/ui/src/app/api/index.ts similarity index 95% rename from ui/src/api/index.ts rename to ui/src/app/api/index.ts index 076de6b..fc00e31 100644 --- a/ui/src/api/index.ts +++ b/ui/src/app/api/index.ts @@ -46,6 +46,11 @@ export interface APIResponse { metadata: Metadata; } +export interface ErrorResponse { + status: string; + message: string; +} + export interface Metadata { limit: number; page: number; diff --git a/ui/src/api/spells.ts b/ui/src/app/api/spells/index.ts similarity index 92% rename from ui/src/api/spells.ts rename to ui/src/app/api/spells/index.ts index e3fc0d1..7a44fb9 100644 --- a/ui/src/api/spells.ts +++ b/ui/src/app/api/spells/index.ts @@ -1,5 +1,5 @@ -import { getRequest } from '.'; -import { GetSpellsResponse } from './spells.types'; +import { getRequest } from '..'; +import { GetSpellsResponse } from './types'; interface GetSpellsParams { name?: string; diff --git a/ui/src/api/spells.types.ts b/ui/src/app/api/spells/types.ts similarity index 97% rename from ui/src/api/spells.types.ts rename to ui/src/app/api/spells/types.ts index 4af3ee0..b09068c 100644 --- a/ui/src/api/spells.types.ts +++ b/ui/src/app/api/spells/types.ts @@ -1,4 +1,4 @@ -import { Metadata } from '.'; +import { Metadata } from '..'; export interface Spell { id: string; diff --git a/ui/src/api/users.ts b/ui/src/app/api/users/index.ts similarity index 92% rename from ui/src/api/users.ts rename to ui/src/app/api/users/index.ts index e14ea66..84b4a00 100644 --- a/ui/src/api/users.ts +++ b/ui/src/app/api/users/index.ts @@ -1,4 +1,4 @@ -import { getRequest, postRequest } from '.'; +import { getRequest, postRequest } from '..'; export async function getPicture(): Promise { const response = await getRequest('users/picture'); diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index 765fa60..e9d29ce 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -8,6 +8,7 @@ import { Notifications } from '@mantine/notifications'; import 'styles/globals.css'; import '@mantine/core/styles.css'; import '@mantine/notifications/styles.css'; +import Loader from '@/components/Loader'; export const metadata = { title: 'Siren', @@ -27,8 +28,10 @@ export default function RootLayout({ children }: { children: React.ReactNode }) -
- {children} + +
+ {children} + diff --git a/ui/src/components/Auth.tsx b/ui/src/components/Auth.tsx index b17002b..aa26675 100644 --- a/ui/src/components/Auth.tsx +++ b/ui/src/components/Auth.tsx @@ -12,12 +12,10 @@ export default function Auth(Component: any, adminOnly = false) { const isAdmin = useRecoilValue(isAdminState); function isAuthenticated() { - console.log('hasUser', hasUser, 'adminOnly', adminOnly, 'isAdmin', isAdmin) return hasUser && (adminOnly ? isAdmin : true); } useEffect(() => { - console.log('isAuthenticated', isAuthenticated()); if (!isAuthenticated) { router.push('/'); } diff --git a/ui/src/components/Header/HeaderModal.tsx b/ui/src/components/Header/HeaderModal.tsx index 4510353..f1e1d66 100644 --- a/ui/src/components/Header/HeaderModal.tsx +++ b/ui/src/components/Header/HeaderModal.tsx @@ -1,7 +1,8 @@ 'use client'; +import { ErrorResponse } from '@/api'; import { login, register } from '@/api/auth'; -import { User } from '@/api/auth.types'; +import { AuthResponse, User } from '@/api/auth.types'; import { Modal, Container, @@ -22,10 +23,9 @@ interface HeaderModalProps { type?: string; toggle: any; setUser: (user: User) => void; - setRefreshId: (id: NodeJS.Timeout) => void; } -export function HeaderModal({ type, toggle, setUser, setRefreshId }: HeaderModalProps) { +export function HeaderModal({ type, toggle, setUser }: HeaderModalProps) { function passwordValidator(value: string) { if (value.trim().length < 10) { return 'Password must be at least 10 characters'; @@ -144,22 +144,24 @@ export function HeaderModal({ type, toggle, setUser, setRefreshId }: HeaderModal }); if (registerResponse) { const loginResponse = await login(values.email, values.password); - if (loginResponse) { - setUser(loginResponse.user); + if (loginResponse.status == 200) { + const user = (loginResponse.data as AuthResponse).user; + setUser(user); onClose(); notifications.update({ id, title: `Account created`, - message: `Welcome ${loginResponse.user.first_name}!`, + message: `Welcome ${user.first_name}!`, color: 'green', autoClose: 2000, loading: false }); } else { + const error = loginResponse.data as ErrorResponse; notifications.update({ id, title: `Unable to Login`, - message: `Please try again.`, + message: `${error.message}`, color: 'red', autoClose: 2000, loading: false @@ -219,13 +221,14 @@ export function HeaderModal({ type, toggle, setUser, setRefreshId }: HeaderModal
{ const response = await login(values.email, values.password); - if (response) { - setUser(response.user); + if (response.status == 200) { + setUser((response.data as AuthResponse).user); onClose(); } else { + const error = response.data as ErrorResponse; notifications.show({ title: `Unable to Login`, - message: `Please try again.`, + message: `${error.message}`, color: 'red', autoClose: 2000 }); diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 51ca566..647b304 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -6,7 +6,7 @@ import './header.css'; import { Avatar, Button, Card, Center, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core'; import Cookies from 'js-cookie'; import { useEffect, useState } from 'react'; -import { hasSession, logout, refresh, refreshLoggedIn } from '@/api/auth'; +import { logout } from '@/api/auth'; import { useToggle } from '@mantine/hooks'; import { HeaderModal } from './HeaderModal'; import { HeaderItem, headerItems } from './headerItems'; @@ -21,7 +21,6 @@ export default function Header() { const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']); const [headers] = useState(headerItems); const [user, setUser] = useRecoilState(userState); - const [refreshId, setRefreshId] = useState(undefined); const [profilePicture, setProfilePicture] = useState(null); const router = useRouter(); @@ -32,9 +31,6 @@ export default function Header() { }, [user]); function updateUser(user?: User) { - if (!refreshId) { - setRefreshId(refreshLoggedIn()); - } if (user) { getPicture().then((response) => { if (response) { @@ -161,12 +157,7 @@ export default function Header() { await logout(); Cookies.remove('logged_in'); setUser(undefined); - clearInterval(refreshId); - setRefreshId(undefined); setProfilePicture(null); - if (refreshId) { - clearInterval(refreshId); - } }} > Logout @@ -203,7 +194,6 @@ export default function Header() { setUser(u); updateUser(u); }} - setRefreshId={setRefreshId} /> ); diff --git a/ui/src/components/Loading.tsx b/ui/src/components/Loader.tsx similarity index 54% rename from ui/src/components/Loading.tsx rename to ui/src/components/Loader.tsx index 2c7ce8a..03a2934 100644 --- a/ui/src/components/Loading.tsx +++ b/ui/src/components/Loader.tsx @@ -1,6 +1,6 @@ 'use client'; -import { hasSession, refresh } from "@/api/auth"; +import { refresh } from "@/api/auth"; import { userState } from "@/state/auth"; import { Skeleton } from "@mantine/core"; import { useEffect, useState } from "react"; @@ -11,26 +11,20 @@ export default function Loading({ children }: { children: React.ReactNode }) { const [user, setUser] = useRecoilState(userState); useEffect(() => { - if (!user) { - hasSession().then((response) => { - if (response) { - refresh().then((response) => { - if (response) { - setUser(response.user); - setLoading(false); - } else { - setLoading(false); - } - }); - } else { - setLoading(false); - } - }); - } else { - setLoading(false); - } + checkUser(); }, []); + async function checkUser() { + setLoading(true); + if (!user) { + const response = await refresh(); + if (response) { + setUser(response.user); + } + } + setLoading(false); + } + if (loading) { return ; } else { diff --git a/ui/src/middleware.ts b/ui/src/middleware.ts index 643627c..dae5c9e 100644 --- a/ui/src/middleware.ts +++ b/ui/src/middleware.ts @@ -1,6 +1,6 @@ -'use client'; - +import Cookies from "js-cookie"; import { NextRequest } from "next/server"; export default function middleware(request: NextRequest) { + console.log(Cookies.get('user_id')) } \ No newline at end of file diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 597235c..0edcf9d 100755 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -30,7 +30,7 @@ "./src/*" ], "@api/*": [ - "src/api" + "src/app/api" ], "@app/*": [ "./src/app/*"