Updating ui

This commit is contained in:
Benjamin Sherriff
2024-01-30 20:09:51 -05:00
parent ca9270f3a7
commit 57286bb0e7
32 changed files with 420 additions and 425 deletions

View File

@@ -11,11 +11,16 @@ echo "Generating public/private keys in: $DIR"
mkdir -p "$DIR" mkdir -p "$DIR"
# Remove any existing keys # Remove any existing keys
rm -f $DIR/private_key.pem rm -f $DIR/*_private_key.pem
rm -f $DIR/public_key.pem rm -f $DIR/*_public_key.pem
# Generate Keys # Generate Keys
openssl genrsa -out $DIR/private_key.pem 4096 openssl genrsa -out $DIR/access_private_key.pem 4096
openssl rsa -in $DIR/private_key.pem -pubout -outform PEM -out $DIR/public_key.pem openssl rsa -in $DIR/access_private_key.pem -pubout -outform PEM -out $DIR/access_public_key.pem
chmod 600 $DIR/private_key.pem chmod 600 $DIR/access_private_key.pem
chmod 644 $DIR/public_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

View File

@@ -7,8 +7,7 @@ DATABASE_HOST=localhost
DATABASE_PORT=5432 DATABASE_PORT=5432
KEYS_DIR_PATH= KEYS_DIR_PATH=
ACCESS_TOKEN_MAXAGE=5 SESSION_TTL=1440
REFRESH_TOKEN_MAXAGE=30
REDIS_HOST=localhost REDIS_HOST=localhost
REDIS_PORT=6379 REDIS_PORT=6379

View File

@@ -25,15 +25,12 @@ r2d2 = "0.8.10"
lazy_static = "1.4.0" lazy_static = "1.4.0"
uuid = { version = "1.4.1", features = ["serde", "v4"] } uuid = { version = "1.4.1", features = ["serde", "v4"] }
argon2 = "0.5.2" argon2 = "0.5.2"
jsonwebtoken = "9.0.0"
redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] } redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] }
base64 = "0.21.4"
rust-s3 = "0.33.0" rust-s3 = "0.33.0"
actix-multipart = "0.6.1" actix-multipart = "0.6.1"
openssl = "0.10.60" # Resolve `openssl` `X509StoreRef::objects` is unsound #10
rand = "0.8.5" rand = "0.8.5"
sha2 = "0.10.8"
rand_chacha = "0.3.1" rand_chacha = "0.3.1"
jsonwebtoken = "9.2.0"
[dependencies.tokio] [dependencies.tokio]
version = "1.32.0" version = "1.32.0"

View File

@@ -1,7 +1,7 @@
mod model; mod model;
mod routes; mod routes;
mod tokens; mod session;
pub use model::*; pub use model::*;
pub use tokens::*; pub use session::*;
pub use routes::init_routes; pub use routes::init_routes;

View File

@@ -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 actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http};
use diesel::prelude::*; use diesel::prelude::*;
use log::error;
use redis::Commands;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use siren::ServiceError; use siren::ServiceError;
use crate::storage::{schema::users, connection}; use crate::storage::{schema::users, connection};
use super::{hash, AccessToken}; use super::{hash, Session, SESSION_COOKIE_NAME};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct RegisterUser { pub struct RegisterUser {
@@ -58,7 +56,6 @@ impl QueryUser {
pub fn get_by_email(email: &str) -> Result<QueryUser, ServiceError> { pub fn get_by_email(email: &str) -> Result<QueryUser, ServiceError> {
let mut conn = connection()?; let mut conn = connection()?;
// Check if the user exists by email, case insensitive // Check if the user exists by email, case insensitive
let user = users::table let user = users::table
.filter(users::email.eq(email.to_lowercase())) .filter(users::email.eq(email.to_lowercase()))
.first(&mut conn)?; .first(&mut conn)?;
@@ -121,17 +118,17 @@ impl From<QueryUser> for ResponseUser {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct JwtAuth { pub struct Auth {
pub id: String, pub id: String,
pub user: ResponseUser pub user: ResponseUser
} }
impl FromRequest for JwtAuth { impl FromRequest for Auth {
type Error = ActixError; type Error = ActixError;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let access_token_string = match req let session_id = match req
.cookie("access_token") .cookie(SESSION_COOKIE_NAME)
.map(|c| c.value().to_string()) .map(|c| c.value().to_string())
.or_else(|| { .or_else(|| {
req.headers().get(http::header::AUTHORIZATION) req.headers().get(http::header::AUTHORIZATION)
@@ -143,54 +140,17 @@ impl FromRequest for JwtAuth {
message: "Unauthorized".to_string() 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() { let ip_address = req.peer_addr().unwrap().ip().to_string();
Ok(conn) => conn,
Err(err) => { match Session::verify(&session_id, &ip_address) {
error!("Failed to get redis connection: {}", err); Ok(v) => return ready(Ok(Auth { id: v.0.id, user: v.1.into() })),
return ready(Err(ActixError::from(ServiceError { Err(err) => return ready(Err(ActixError::from(err)))
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::<AccessToken>(&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")
})))
} }
} }
} }
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 { if auth.user.role == role {
Ok(()) Ok(())
} else { } else {

View File

@@ -3,10 +3,9 @@ use std::env;
use actix_web::{get, post, web, HttpResponse, ResponseError, cookie::{Cookie, time::Duration}, HttpRequest}; use actix_web::{get, post, web, HttpResponse, ResponseError, cookie::{Cookie, time::Duration}, HttpRequest};
use log::error; use log::error;
use redis::AsyncCommands; use redis::AsyncCommands;
use serde::{Serialize, Deserialize};
use siren::ServiceError; 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; use super::verify_hash;
@@ -47,14 +46,7 @@ async fn login(request: HttpRequest, login_request: web::Json<LoginRequest>) ->
}; };
// Verify password // Verify password
if verify_hash(&login_request.password, &query_user.hash) { if verify_hash(&login_request.password, &query_user.hash) {
let mut refresh_token = RefreshToken::new(&email, &ip_address); let session = Session::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 mut conn = match storage::redis_async_connection().await { let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn, Ok(conn) => conn,
@@ -64,51 +56,33 @@ async fn login(request: HttpRequest, login_request: web::Json<LoginRequest>) ->
} }
}; };
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE") let session_ttl = env::var("SESSION_TTL")
.expect("ACCESS_TOKEN_MAXAGE must be set") .expect("SESSION_TTL must be set")
.parse::<i64>() .parse::<i64>()
.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") let session_result: redis::RedisResult<()> = conn.set_ex(session.id.to_string(), &serde_json::to_string(&session).unwrap(), (session_ttl * 60) as usize).await;
.expect("REFRESH_TOKEN_MAXAGE must be set") if let Err(err) = session_result {
.parse::<i64>()
.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 {
error!("Failed to set access token in redis: {}", err); error!("Failed to set access token in redis: {}", err);
return ResponseError::error_response(&ServiceError::from(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; let session_cookie = Cookie::build(SESSION_COOKIE_NAME, session.id.clone())
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())
.path("/") .path("/")
.max_age(Duration::new(access_token_max_age * 60, 0)) .max_age(Duration::new(session_ttl * 60, 0))
.http_only(true) .http_only(true)
.secure(true) .secure(true)
.finish(); .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("/") .path("/")
.max_age(Duration::new(refresh_token_max_age * 60, 0)) .max_age(Duration::new(session_ttl * 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) .http_only(false)
.finish(); .finish();
HttpResponse::Ok() HttpResponse::Ok()
.cookie(access_cookie) .cookie(session_cookie)
.cookie(refresh_cookie) .cookie(user_id_cookie)
.cookie(logged_in_cookie) .json(Auth { id: session.id, user: query_user.into() })
.json(JwtAuth { id: access_token.id, user: query_user.into() })
} else { } else {
return ResponseError::error_response(&ServiceError { return ResponseError::error_response(&ServiceError {
status: 401, status: 401,
@@ -117,29 +91,9 @@ async fn login(request: HttpRequest, login_request: web::Json<LoginRequest>) ->
} }
} }
#[derive(Serialize, Deserialize)]
struct RefreshParams {
refresh_token_rotation: Option<bool>
}
#[get("/refresh")] #[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 ip_address = req.peer_addr().unwrap().ip().to_string();
let params = match web::Query::<RefreshParams>::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 { let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn, 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 { let session_ttl = env::var("SESSION_TTL")
Ok(result) => match serde_json::from_str::<RefreshToken>(&result) { .expect("SESSION_TTL must be set")
Ok(result) => { .parse::<i64>()
if verify_hash(&ip_address, &result.ip_address) { .expect("SESSION_TTL must be an integer");
result
} else { // Delete old session
return ResponseError::error_response(&ServiceError { let _: redis::RedisResult<()> = conn.del(auth.id).await;
status: 401,
message: "Refresh token is invalid".to_string() // 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 {
Err(err) => { error!("Failed to set session id in redis: {}", err);
error!("Failed to deserialize refresh token: {}", err); return ResponseError::error_response(&ServiceError::from(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 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) { HttpResponse::Ok()
Ok(query_user) => { .cookie(session_cookie)
// Revoke all old access tokens .cookie(user_id_cookie)
for id in refresh_token.tokens { .json(Auth { id: session.id, user: auth.user })
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::<i64>()
.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::<i64>()
.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)
}
} }
#[post("/logout")] #[post("/logout")]
async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { async fn logout(auth: Auth) -> 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()
})
};
let mut conn = match storage::redis_async_connection().await { let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => { Err(err) => {
@@ -280,45 +143,32 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
} }
}; };
let access_result: redis::RedisResult<()> = conn.del(&[ let session_result: redis::RedisResult<()> = conn.del(&auth.id.to_string()).await;
refresh_token.to_string(), if let Err(err) = session_result {
auth.id.to_string() error!("Failed to remove session id in redis: {}", err);
]).await; return ResponseError::error_response(&ServiceError::from(err))
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 access_cookie = Cookie::build("access_token", "") let session_cookie = Cookie::build(SESSION_COOKIE_NAME, "")
.path("/") .path("/")
.max_age(Duration::new(-1, 0)) .max_age(Duration::new(-1, 0))
.secure(true) .secure(true)
.http_only(true) .http_only(true)
.finish(); .finish();
let refresh_cookie = Cookie::build("refresh_token", "") let user_id_cookie = Cookie::build("user_id", "")
.path("/")
.max_age(Duration::new(-1, 0))
.secure(true)
.http_only(true)
.finish();
let logged_in_cookie = Cookie::build("logged_in", "")
.path("/") .path("/")
.max_age(Duration::new(-1, 0)) .max_age(Duration::new(-1, 0))
.http_only(true) .http_only(true)
.finish(); .finish();
HttpResponse::Ok() HttpResponse::Ok()
.cookie(access_cookie) .cookie(session_cookie)
.cookie(refresh_cookie) .cookie(user_id_cookie)
.cookie(logged_in_cookie)
.finish() .finish()
} }
#[get("/me")] #[get("/me")]
async fn me(auth: JwtAuth) -> HttpResponse { async fn me(auth: Auth) -> HttpResponse {
HttpResponse::Ok().json(auth) HttpResponse::Ok().json(auth)
} }

102
service/src/auth/session.rs Normal file
View File

@@ -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::<i64>()
.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::<i64>()
.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<String, ServiceError> {
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
}
}

View File

@@ -5,10 +5,10 @@ use serde::{Serialize, Deserialize};
use serenity::model::prelude::{GuildChannel, ChannelType}; use serenity::model::prelude::{GuildChannel, ChannelType};
use siren::{ServiceError, Response}; 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")] #[get("/guilds")]
async fn get_guilds(data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn get_guilds(data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -27,7 +27,7 @@ async fn get_guilds(data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpRespon
} }
#[get("/{id}/text")] #[get("/{id}/text")]
async fn get_text_channels(id: web::Path<String>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn get_text_channels(id: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -46,7 +46,7 @@ async fn get_text_channels(id: web::Path<String>, data: web::Data<Arc<AppState>>
} }
#[get("/{id}/voice")] #[get("/{id}/voice")]
async fn get_voice_channels(id: web::Path<String>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn get_voice_channels(id: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -70,7 +70,7 @@ struct ChannelMessage {
} }
#[post("/{guild_id}/text/{channel_id}/message")] #[post("/{guild_id}/text/{channel_id}/message")]
async fn send_message(path: web::Path<(String, String)>, text: web::Json<ChannelMessage>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn send_message(path: web::Path<(String, String)>, text: web::Json<ChannelMessage>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -130,7 +130,7 @@ struct PlayRequest {
} }
#[post("/{guild_id}/voice/{channel_id}/play")] #[post("/{guild_id}/voice/{channel_id}/play")]
async fn play(path: web::Path<(String, String)>, play_request: web::Json<PlayRequest>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn play(path: web::Path<(String, String)>, play_request: web::Json<PlayRequest>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -179,7 +179,7 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json<PlayReq
} }
#[post("/{guild_id}/voice/stop")] #[post("/{guild_id}/voice/stop")]
async fn stop(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn stop(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -203,7 +203,7 @@ async fn stop(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Jwt
} }
#[post("/{guild_id}/voice/resume")] #[post("/{guild_id}/voice/resume")]
async fn resume(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn resume(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -232,7 +232,7 @@ async fn resume(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: J
} }
#[post("/{guild_id}/voice/pause")] #[post("/{guild_id}/voice/pause")]
async fn pause(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn pause(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -266,7 +266,7 @@ struct SetVolume {
} }
#[get("/{guild_id}/voice/volume")] #[get("/{guild_id}/voice/volume")]
async fn get_volume(path: web::Path<String>, auth: JwtAuth) -> HttpResponse { async fn get_volume(path: web::Path<String>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -295,7 +295,7 @@ async fn get_volume(path: web::Path<String>, auth: JwtAuth) -> HttpResponse {
} }
#[post("/{guild_id}/voice/volume")] #[post("/{guild_id}/voice/volume")]
async fn set_volume(path: web::Path<String>, volume: web::Json::<SetVolume>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn set_volume(path: web::Path<String>, volume: web::Json::<SetVolume>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };
@@ -325,7 +325,7 @@ async fn set_volume(path: web::Path<String>, volume: web::Json::<SetVolume>, dat
} }
#[post("/{guild_id}/voice/skip")] #[post("/{guild_id}/voice/skip")]
async fn skip(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: JwtAuth) -> HttpResponse { async fn skip(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") { if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err) return ResponseError::error_response(&err)
}; };

View File

@@ -3,7 +3,7 @@ use log::error;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use siren::{Response, Metadata, ServiceError}; 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)] #[derive(Serialize, Deserialize)]
struct GetAllParams { struct GetAllParams {
@@ -21,7 +21,7 @@ struct GetAllParams {
} }
#[get("/messages")] #[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") { let _ = match verify_role(&auth, "admin") {
Ok(_) => {}, Ok(_) => {},
Err(err) => return ResponseError::error_response(&err) Err(err) => return ResponseError::error_response(&err)
@@ -68,7 +68,7 @@ async fn get_all(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
} }
#[post("/messages")] #[post("/messages")]
async fn create(message: web::Json<QueryMessage>, auth: JwtAuth) -> HttpResponse { async fn create(message: web::Json<QueryMessage>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") { let _ = match verify_role(&auth, "admin") {
Ok(_) => {}, Ok(_) => {},
Err(err) => return ResponseError::error_response(&err) Err(err) => return ResponseError::error_response(&err)

View File

@@ -3,7 +3,7 @@ use log::error;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use siren::{Response, Metadata, ServiceError}; 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}; use super::{Spell, InsertSpell};
@@ -134,7 +134,7 @@ async fn get_by_id(id: web::Path<String>) -> HttpResponse {
} }
#[post("/spells")] #[post("/spells")]
async fn create(spell: web::Json<Spell>, auth: JwtAuth) -> HttpResponse { async fn create(spell: web::Json<Spell>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") { let _ = match verify_role(&auth, "admin") {
Ok(_) => {}, Ok(_) => {},
Err(err) => return ResponseError::error_response(&err) Err(err) => return ResponseError::error_response(&err)
@@ -149,7 +149,7 @@ async fn create(spell: web::Json<Spell>, auth: JwtAuth) -> HttpResponse {
} }
#[put("/spells/{id}")] #[put("/spells/{id}")]
async fn update(id: web::Path<String>, spell: web::Json<Spell>, auth: JwtAuth) -> HttpResponse { async fn update(id: web::Path<String>, spell: web::Json<Spell>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") { let _ = match verify_role(&auth, "admin") {
Ok(_) => {}, Ok(_) => {},
Err(err) => return ResponseError::error_response(&err) Err(err) => return ResponseError::error_response(&err)
@@ -171,7 +171,7 @@ async fn update(id: web::Path<String>, spell: web::Json<Spell>, auth: JwtAuth) -
} }
#[delete("/spells/{id}")] #[delete("/spells/{id}")]
async fn delete(id: web::Path<String>, auth: JwtAuth) -> HttpResponse { async fn delete(id: web::Path<String>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") { let _ = match verify_role(&auth, "admin") {
Ok(_) => {}, Ok(_) => {},
Err(err) => return ResponseError::error_response(&err) Err(err) => return ResponseError::error_response(&err)

View File

@@ -112,12 +112,6 @@ impl From<argon2::password_hash::Error> for ServiceError {
} }
} }
impl From<jsonwebtoken::errors::Error> for ServiceError {
fn from(error: jsonwebtoken::errors::Error) -> ServiceError {
ServiceError::new(500, format!("Unknown jsonwebtoken error: {}", error))
}
}
impl From<redis::RedisError> for ServiceError { impl From<redis::RedisError> for ServiceError {
fn from(error: redis::RedisError) -> ServiceError { fn from(error: redis::RedisError) -> ServiceError {
ServiceError::new(500, format!("Unknown redis error: {}", error)) ServiceError::new(500, format!("Unknown redis error: {}", error))
@@ -153,6 +147,12 @@ impl From<std::env::VarError> for ServiceError {
} }
} }
impl From<jsonwebtoken::errors::Error> for ServiceError {
fn from(error: jsonwebtoken::errors::Error) -> ServiceError {
ServiceError::new(500, format!("Unknown jsonwebtoken error: {}", error))
}
}
impl ResponseError for ServiceError { impl ResponseError for ServiceError {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
let status_code = match StatusCode::from_u16(self.status) { let status_code = match StatusCode::from_u16(self.status) {

View File

@@ -4,10 +4,10 @@ use log::error;
use serenity::futures::StreamExt; use serenity::futures::StreamExt;
use siren::ServiceError; 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")] #[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 { while let Some(item) = payload.next().await {
let mut bytes = web::BytesMut::new(); let mut bytes = web::BytesMut::new();
let mut field = match item { let mut field = match item {
@@ -73,7 +73,7 @@ async fn set_picture(mut payload: Multipart, auth: JwtAuth) -> HttpResponse {
} }
#[get("/picture")] #[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) { let user = match QueryUser::get_by_email(&auth.user.email) {
Ok(user) => user, Ok(user) => user,
Err(err) => { Err(err) => {
@@ -95,7 +95,7 @@ async fn get_picture(auth: JwtAuth) -> HttpResponse {
} }
#[delete("/picture")] #[delete("/picture")]
async fn delete_picture(auth: JwtAuth) -> HttpResponse { async fn delete_picture(auth: Auth) -> HttpResponse {
match QueryUser::get_by_email(&auth.user.email) { match QueryUser::get_by_email(&auth.user.email) {
Ok(user) => { Ok(user) => {
match user.profile_picture { match user.profile_picture {

124
ui/package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@pixi/react": "^7.1.1", "@pixi/react": "^7.1.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"next": "^14.1.0", "next": "^14.1.0",
"next-auth": "^4.24.5",
"pixi.js": "^7.4.0", "pixi.js": "^7.4.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -579,6 +580,14 @@
"node": ">= 8" "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": { "node_modules/@pixi/app": {
"version": "7.3.2", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.3.2.tgz", "resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.3.2.tgz",
@@ -1646,6 +1655,14 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true "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": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3274,6 +3291,14 @@
"@pkgjs/parseargs": "^0.11.0" "@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": { "node_modules/js-cookie": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
@@ -3435,7 +3460,6 @@
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": { "dependencies": {
"yallist": "^4.0.0" "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": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -3611,6 +3662,11 @@
"node": ">=0.10.0" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -3619,6 +3675,14 @@
"node": ">=0.10.0" "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": { "node_modules/object-inspect": {
"version": "1.12.3", "version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
@@ -3727,6 +3791,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3736,6 +3808,20 @@
"wrappy": "1" "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": { "node_modules/optionator": {
"version": "0.9.3", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -4430,6 +4516,26 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -4466,6 +4572,11 @@
"node": ">=6.0.0" "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": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -5503,6 +5614,14 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5697,8 +5816,7 @@
"node_modules/yallist": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"dev": true
}, },
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",

View File

@@ -17,6 +17,7 @@
"@pixi/react": "^7.1.1", "@pixi/react": "^7.1.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"next": "^14.1.0", "next": "^14.1.0",
"next-auth": "^4.24.5",
"pixi.js": "^7.4.0", "pixi.js": "^7.4.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@@ -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<ResponseAuth | undefined> {
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<boolean> {
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<ResponseAuth | undefined> {
const response = await getRequest('auth/refresh', { refresh_token_rotation });
if (response?.status === 200) {
return response.json();
} else {
return undefined;
}
}
export async function me(): Promise<ResponseAuth | undefined> {
const response = await getRequest('auth/me');
if (response?.status === 200) {
return response.json();
} else {
return undefined;
}
}
export async function hasSession(): Promise<boolean> {
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;
}

View File

@@ -0,0 +1,32 @@
import { getRequest, postRequest } from '..';
import { BaseResponse, RegisterUser } from './types';
export async function login(email: string, password: string): Promise<BaseResponse> {
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<BaseResponse> {
const response = await postRequest('auth/register', user);
const data = await response.json();
return { data, status: response.status };
}
export async function logout(): Promise<BaseResponse> {
const response = await postRequest('auth/logout', {});
const data = await response.json();
return { data, status: response.status };
}
export async function refresh(): Promise<BaseResponse> {
const response = await getRequest('auth/refresh');
const data = await response.json();
return { data, status: response.status };
}
export async function me(): Promise<BaseResponse> {
const response = await getRequest('auth/me');
const data = await response.json();
return { data, status: response.status };
}

View File

@@ -1,8 +1,18 @@
export interface ResponseAuth { import { ErrorResponse } from "..";
token: string;
export interface AuthResponse {
id: string;
user: User; 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 { export interface RegisterUser {
email: string; email: string;
password: string; password: string;

View File

@@ -1,5 +1,5 @@
import { APIResponse, getRequest, postRequest } from '.'; import { APIResponse, getRequest, postRequest } from '..';
import { GuildChannel, GuildInfo } from './guilds.types'; import { GuildChannel, GuildInfo } from './types';
export async function getGuilds(): Promise<GuildInfo[]> { export async function getGuilds(): Promise<GuildInfo[]> {
const response = await getRequest('guilds'); const response = await getRequest('guilds');

View File

@@ -46,6 +46,11 @@ export interface APIResponse<T> {
metadata: Metadata; metadata: Metadata;
} }
export interface ErrorResponse {
status: string;
message: string;
}
export interface Metadata { export interface Metadata {
limit: number; limit: number;
page: number; page: number;

View File

@@ -1,5 +1,5 @@
import { getRequest } from '.'; import { getRequest } from '..';
import { GetSpellsResponse } from './spells.types'; import { GetSpellsResponse } from './types';
interface GetSpellsParams { interface GetSpellsParams {
name?: string; name?: string;

View File

@@ -1,4 +1,4 @@
import { Metadata } from '.'; import { Metadata } from '..';
export interface Spell { export interface Spell {
id: string; id: string;

View File

@@ -1,4 +1,4 @@
import { getRequest, postRequest } from '.'; import { getRequest, postRequest } from '..';
export async function getPicture(): Promise<Blob | undefined> { export async function getPicture(): Promise<Blob | undefined> {
const response = await getRequest('users/picture'); const response = await getRequest('users/picture');

View File

@@ -8,6 +8,7 @@ import { Notifications } from '@mantine/notifications';
import 'styles/globals.css'; import 'styles/globals.css';
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css'; import '@mantine/notifications/styles.css';
import Loader from '@/components/Loader';
export const metadata = { export const metadata = {
title: 'Siren', title: 'Siren',
@@ -27,8 +28,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<MantineProvider> <MantineProvider>
<Notifications /> <Notifications />
<ModalsProvider> <ModalsProvider>
<Header /> <Loader>
<Box>{children}</Box> <Header />
<Box>{children}</Box>
</Loader>
</ModalsProvider> </ModalsProvider>
</MantineProvider> </MantineProvider>
</RecoilRootWrapper> </RecoilRootWrapper>

View File

@@ -12,12 +12,10 @@ export default function Auth(Component: any, adminOnly = false) {
const isAdmin = useRecoilValue(isAdminState); const isAdmin = useRecoilValue(isAdminState);
function isAuthenticated() { function isAuthenticated() {
console.log('hasUser', hasUser, 'adminOnly', adminOnly, 'isAdmin', isAdmin)
return hasUser && (adminOnly ? isAdmin : true); return hasUser && (adminOnly ? isAdmin : true);
} }
useEffect(() => { useEffect(() => {
console.log('isAuthenticated', isAuthenticated());
if (!isAuthenticated) { if (!isAuthenticated) {
router.push('/'); router.push('/');
} }

View File

@@ -1,7 +1,8 @@
'use client'; 'use client';
import { ErrorResponse } from '@/api';
import { login, register } from '@/api/auth'; import { login, register } from '@/api/auth';
import { User } from '@/api/auth.types'; import { AuthResponse, User } from '@/api/auth.types';
import { import {
Modal, Modal,
Container, Container,
@@ -22,10 +23,9 @@ interface HeaderModalProps {
type?: string; type?: string;
toggle: any; toggle: any;
setUser: (user: User) => void; 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) { function passwordValidator(value: string) {
if (value.trim().length < 10) { if (value.trim().length < 10) {
return 'Password must be at least 10 characters'; return 'Password must be at least 10 characters';
@@ -144,22 +144,24 @@ export function HeaderModal({ type, toggle, setUser, setRefreshId }: HeaderModal
}); });
if (registerResponse) { if (registerResponse) {
const loginResponse = await login(values.email, values.password); const loginResponse = await login(values.email, values.password);
if (loginResponse) { if (loginResponse.status == 200) {
setUser(loginResponse.user); const user = (loginResponse.data as AuthResponse).user;
setUser(user);
onClose(); onClose();
notifications.update({ notifications.update({
id, id,
title: `Account created`, title: `Account created`,
message: `Welcome ${loginResponse.user.first_name}!`, message: `Welcome ${user.first_name}!`,
color: 'green', color: 'green',
autoClose: 2000, autoClose: 2000,
loading: false loading: false
}); });
} else { } else {
const error = loginResponse.data as ErrorResponse;
notifications.update({ notifications.update({
id, id,
title: `Unable to Login`, title: `Unable to Login`,
message: `Please try again.`, message: `${error.message}`,
color: 'red', color: 'red',
autoClose: 2000, autoClose: 2000,
loading: false loading: false
@@ -219,13 +221,14 @@ export function HeaderModal({ type, toggle, setUser, setRefreshId }: HeaderModal
<form <form
onSubmit={loginForm.onSubmit(async (values) => { onSubmit={loginForm.onSubmit(async (values) => {
const response = await login(values.email, values.password); const response = await login(values.email, values.password);
if (response) { if (response.status == 200) {
setUser(response.user); setUser((response.data as AuthResponse).user);
onClose(); onClose();
} else { } else {
const error = response.data as ErrorResponse;
notifications.show({ notifications.show({
title: `Unable to Login`, title: `Unable to Login`,
message: `Please try again.`, message: `${error.message}`,
color: 'red', color: 'red',
autoClose: 2000 autoClose: 2000
}); });

View File

@@ -6,7 +6,7 @@ import './header.css';
import { Avatar, Button, Card, Center, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core'; import { Avatar, Button, Card, Center, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { hasSession, logout, refresh, refreshLoggedIn } from '@/api/auth'; import { logout } from '@/api/auth';
import { useToggle } from '@mantine/hooks'; import { useToggle } from '@mantine/hooks';
import { HeaderModal } from './HeaderModal'; import { HeaderModal } from './HeaderModal';
import { HeaderItem, headerItems } from './headerItems'; import { HeaderItem, headerItems } from './headerItems';
@@ -21,7 +21,6 @@ export default function Header() {
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']); const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
const [headers] = useState<HeaderItem[]>(headerItems); const [headers] = useState<HeaderItem[]>(headerItems);
const [user, setUser] = useRecoilState(userState); const [user, setUser] = useRecoilState(userState);
const [refreshId, setRefreshId] = useState<NodeJS.Timeout | undefined>(undefined);
const [profilePicture, setProfilePicture] = useState<File | null>(null); const [profilePicture, setProfilePicture] = useState<File | null>(null);
const router = useRouter(); const router = useRouter();
@@ -32,9 +31,6 @@ export default function Header() {
}, [user]); }, [user]);
function updateUser(user?: User) { function updateUser(user?: User) {
if (!refreshId) {
setRefreshId(refreshLoggedIn());
}
if (user) { if (user) {
getPicture().then((response) => { getPicture().then((response) => {
if (response) { if (response) {
@@ -161,12 +157,7 @@ export default function Header() {
await logout(); await logout();
Cookies.remove('logged_in'); Cookies.remove('logged_in');
setUser(undefined); setUser(undefined);
clearInterval(refreshId);
setRefreshId(undefined);
setProfilePicture(null); setProfilePicture(null);
if (refreshId) {
clearInterval(refreshId);
}
}} }}
> >
Logout Logout
@@ -203,7 +194,6 @@ export default function Header() {
setUser(u); setUser(u);
updateUser(u); updateUser(u);
}} }}
setRefreshId={setRefreshId}
/> />
</> </>
); );

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { hasSession, refresh } from "@/api/auth"; import { refresh } from "@/api/auth";
import { userState } from "@/state/auth"; import { userState } from "@/state/auth";
import { Skeleton } from "@mantine/core"; import { Skeleton } from "@mantine/core";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -11,26 +11,20 @@ export default function Loading({ children }: { children: React.ReactNode }) {
const [user, setUser] = useRecoilState(userState); const [user, setUser] = useRecoilState(userState);
useEffect(() => { useEffect(() => {
if (!user) { checkUser();
hasSession().then((response) => {
if (response) {
refresh().then((response) => {
if (response) {
setUser(response.user);
setLoading(false);
} else {
setLoading(false);
}
});
} else {
setLoading(false);
}
});
} else {
setLoading(false);
}
}, []); }, []);
async function checkUser() {
setLoading(true);
if (!user) {
const response = await refresh();
if (response) {
setUser(response.user);
}
}
setLoading(false);
}
if (loading) { if (loading) {
return <Skeleton height={'100%'} />; return <Skeleton height={'100%'} />;
} else { } else {

View File

@@ -1,6 +1,6 @@
'use client'; import Cookies from "js-cookie";
import { NextRequest } from "next/server"; import { NextRequest } from "next/server";
export default function middleware(request: NextRequest) { export default function middleware(request: NextRequest) {
console.log(Cookies.get('user_id'))
} }

View File

@@ -30,7 +30,7 @@
"./src/*" "./src/*"
], ],
"@api/*": [ "@api/*": [
"src/api" "src/app/api"
], ],
"@app/*": [ "@app/*": [
"./src/app/*" "./src/app/*"