Updated auth to use generated keys
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
.env
|
.env
|
||||||
target/
|
target/
|
||||||
.idea/
|
.idea/
|
||||||
|
keys/
|
||||||
**/Cargo.lock
|
**/Cargo.lock
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
@@ -69,3 +69,8 @@ The application can also be tested from within a Docker container:
|
|||||||
docker build -t siren:latest .
|
docker build -t siren:latest .
|
||||||
docker run --env-file .env -it --rm --name siren siren:latest
|
docker run --env-file .env -it --rm --name siren siren:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
The Siren service uses a JWT/session based authentication system, in that JWT tokens are issued and used, but a state is also kept server-side. This is to allow for the ability to revoke and expire tokens, as well as to allow for the ability to have multiple tokens per user.
|
||||||
|
|
||||||
|
Public/Private keys can be generated with `make generate`. These keys should be located within a `/keys` directory in the root of the project.
|
||||||
@@ -6,12 +6,8 @@ DATABASE_NAME=siren
|
|||||||
DATABASE_HOST=localhost
|
DATABASE_HOST=localhost
|
||||||
DATABASE_PORT=5432
|
DATABASE_PORT=5432
|
||||||
|
|
||||||
ACCESS_TOKEN_PRIVATE_KEY=
|
KEYS_DIR_PATH=
|
||||||
ACCESS_TOKEN_PUBLIC_KEY=
|
|
||||||
ACCESS_TOKEN_MAXAGE=5
|
ACCESS_TOKEN_MAXAGE=5
|
||||||
|
|
||||||
REFRESH_TOKEN_PRIVATE_KEY=
|
|
||||||
REFRESH_TOKEN_PUBLIC_KEY=
|
|
||||||
REFRESH_TOKEN_MAXAGE=30
|
REFRESH_TOKEN_MAXAGE=30
|
||||||
|
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ COPY Cargo.toml ./
|
|||||||
RUN apt-get update && apt-get install -y cmake
|
RUN apt-get update && apt-get install -y cmake
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
|
# ======
|
||||||
|
# Keys
|
||||||
|
# ======
|
||||||
|
FROM debian:bookworm-slim as keys
|
||||||
|
WORKDIR /keys
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y openssl libpq-dev
|
||||||
|
RUN openssl genrsa -out access.pem 4096
|
||||||
|
RUN openssl rsa -in access.pem -pubout -outform PEM -out access.pem.pub
|
||||||
|
RUN openssl genrsa -out refresh.pem 4096
|
||||||
|
RUN openssl rsa -in refresh.pem -pubout -outform PEM -out refresh.pem.pub
|
||||||
|
|
||||||
# ==========
|
# ==========
|
||||||
# Packages
|
# Packages
|
||||||
# ==========
|
# ==========
|
||||||
@@ -51,6 +63,7 @@ USER root
|
|||||||
|
|
||||||
COPY --from=builder /builder/target/release/service /usr/local/bin/service
|
COPY --from=builder /builder/target/release/service /usr/local/bin/service
|
||||||
COPY --from=packages /packages /usr/bin
|
COPY --from=packages /packages /usr/bin
|
||||||
|
COPY --from=keys /keys /keys
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y libc6 libc6-dev libopus-dev libpq5 libpq-dev python3-pip ffmpeg
|
RUN apt-get update && apt-get install -y libc6 libc6-dev libopus-dev libpq5 libpq-dev python3-pip ffmpeg
|
||||||
|
|
||||||
|
|||||||
@@ -29,3 +29,10 @@ clean:
|
|||||||
docker image rm siren-service || \
|
docker image rm siren-service || \
|
||||||
docker network rm siren_frontend || \
|
docker network rm siren_frontend || \
|
||||||
docker network rm siren-backend
|
docker network rm siren-backend
|
||||||
|
|
||||||
|
generate: ## Generate RSA keys
|
||||||
|
mkdir keys
|
||||||
|
openssl genrsa -out keys/access_private_key.pem 4096
|
||||||
|
openssl rsa -in keys/access_private_key.pem -pubout -outform PEM -out keys/access_public_key.pem
|
||||||
|
openssl genrsa -out keys/refresh_private_key.pem 4096
|
||||||
|
openssl rsa -in keys/refresh_private_key.pem -pubout -outform PEM -out keys/refresh_public_key.pem
|
||||||
@@ -24,6 +24,7 @@ services:
|
|||||||
SERVICE_HOST: service
|
SERVICE_HOST: service
|
||||||
SERVICE_PORT: 5000
|
SERVICE_PORT: 5000
|
||||||
DATA_DIR_PATH: /data
|
DATA_DIR_PATH: /data
|
||||||
|
KEYS_DIR_PATH: /keys
|
||||||
volumes:
|
volumes:
|
||||||
- ${DATA_DIR_PATH}:/data
|
- ${DATA_DIR_PATH}:/data
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
|
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -14,34 +13,84 @@ use siren::ServiceError;
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct TokenClaims {
|
struct TokenClaims {
|
||||||
sub: String, // Subject
|
sub: String, // Subject (User)
|
||||||
token_uuid: String, // Token UUID
|
token_uuid: String, // Token UUID
|
||||||
iss: String, // Issuer
|
iss: String, // Issuer (Service)
|
||||||
exp: i64, // Expiration time
|
exp: i64, // Expiration time
|
||||||
iat: i64, // Issued At
|
iat: i64, // Issued At
|
||||||
nbf: i64 // Not Before
|
nbf: i64 // Not Before
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum TokenType {
|
||||||
|
#[serde(rename = "access")]
|
||||||
|
Access,
|
||||||
|
#[serde(rename = "refresh")]
|
||||||
|
Refresh,
|
||||||
|
#[serde(rename = "none")]
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TokenType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
TokenType::Access => write!(f, "access"),
|
||||||
|
TokenType::Refresh => write!(f, "refresh"),
|
||||||
|
TokenType::None => write!(f, "none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for TokenType {
|
||||||
|
type Err = ServiceError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"access" => Ok(TokenType::Access),
|
||||||
|
"refresh" => Ok(TokenType::Refresh),
|
||||||
|
"none" => Ok(TokenType::None),
|
||||||
|
_ => Err(ServiceError::new(400, "Invalid token type".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct TokenDetails {
|
pub struct TokenDetails {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
pub token_uuid: uuid::Uuid,
|
pub token_uuid: uuid::Uuid,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
pub token_type: TokenType,
|
||||||
pub expires_in: Option<i64>
|
pub expires_in: Option<i64>
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://codevoweb.com/rust-actix-web-jwt-access-and-refresh-tokens/
|
impl std::fmt::Display for TokenDetails {
|
||||||
// https://github.com/wpcodevo/rust-jwt-rs256/blob/master/src/main.rs
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}:{}:{}", self.token_type.to_string(), self.email, self.token_uuid.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for TokenDetails {
|
||||||
|
type Err = ServiceError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let parts: Vec<&str> = s.split(":").collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(ServiceError::new(400, "Invalid token".to_string()));
|
||||||
|
}
|
||||||
|
let token_type = parts[0].parse::<TokenType>()?;
|
||||||
|
let email = parts[1].to_string();
|
||||||
|
let uuid = uuid::Uuid::parse_str(parts[2])?;
|
||||||
|
Ok(TokenDetails { token: None, token_uuid: uuid, email, token_type, expires_in: None })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn verify_token(token: &str, public_key: &str) -> Result<TokenDetails, ServiceError> {
|
pub fn verify_token(token: &str, public_key: &str) -> Result<TokenDetails, ServiceError> {
|
||||||
let bytes_public_key = general_purpose::STANDARD.decode(public_key).unwrap();
|
let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?;
|
||||||
let decoded_public_key = String::from_utf8(bytes_public_key).unwrap();
|
|
||||||
let key = DecodingKey::from_rsa_pem(decoded_public_key.as_bytes())?;
|
|
||||||
let validation = Validation::new(Algorithm::RS256);
|
let validation = Validation::new(Algorithm::RS256);
|
||||||
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
|
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
|
||||||
let email = decoded.claims.sub;
|
let email = decoded.claims.sub;
|
||||||
let token_uuid = uuid::Uuid::parse_str(decoded.claims.token_uuid.as_str()).unwrap();
|
let token_uuid = uuid::Uuid::parse_str(decoded.claims.token_uuid.as_str()).unwrap();
|
||||||
Ok(TokenDetails { token: None, token_uuid, email, expires_in: None })
|
Ok(TokenDetails { token: None, token_uuid, email, token_type: TokenType::None, expires_in: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_access_token(email: &str) -> Result<TokenDetails, ServiceError> {
|
pub fn generate_access_token(email: &str) -> Result<TokenDetails, ServiceError> {
|
||||||
@@ -49,9 +98,9 @@ pub fn generate_access_token(email: &str) -> Result<TokenDetails, ServiceError>
|
|||||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||||
.parse::<i64>()
|
.parse::<i64>()
|
||||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||||
let access_private_key = env::var("ACCESS_TOKEN_PRIVATE_KEY")
|
let keys_dir = env::var("KEYS_DIR_PATH")?;
|
||||||
.expect("ACCESS_TOKEN_PRIVATE_KEY must be set");
|
let access_private_key = std::fs::read_to_string(format!("{}/access_private_key.pem", keys_dir))?;
|
||||||
generate_token(&email, access_token_max_age, &access_private_key)
|
generate_token(&email, TokenType::Refresh, access_token_max_age, &access_private_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_refresh_token(email: &str) -> Result<TokenDetails, ServiceError> {
|
pub fn generate_refresh_token(email: &str) -> Result<TokenDetails, ServiceError> {
|
||||||
@@ -59,17 +108,18 @@ pub fn generate_refresh_token(email: &str) -> Result<TokenDetails, ServiceError>
|
|||||||
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
||||||
.parse::<i64>()
|
.parse::<i64>()
|
||||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||||
let refresh_private_key = env::var("REFRESH_TOKEN_PRIVATE_KEY")
|
let keys_dir = env::var("KEYS_DIR_PATH")?;
|
||||||
.expect("REFRESH_TOKEN_PRIVATE_KEY must be set");
|
let refresh_private_key = std::fs::read_to_string(format!("{}/refresh_private_key.pem", keys_dir))?;
|
||||||
generate_token(&email, refresh_token_max_age, &refresh_private_key)
|
generate_token(&email, TokenType::Access, refresh_token_max_age, &refresh_private_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result<TokenDetails, ServiceError> {
|
fn generate_token(email: &str, token_type: TokenType, ttl: i64, private_key: &str) -> Result<TokenDetails, ServiceError> {
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let mut token_details = TokenDetails {
|
let mut token_details = TokenDetails {
|
||||||
token: None,
|
token: None,
|
||||||
token_uuid: uuid::Uuid::new_v4(),
|
token_uuid: uuid::Uuid::new_v4(),
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
|
token_type,
|
||||||
expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp())
|
expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp())
|
||||||
};
|
};
|
||||||
let claims = TokenClaims {
|
let claims = TokenClaims {
|
||||||
@@ -81,9 +131,7 @@ pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result<TokenD
|
|||||||
nbf: now.timestamp()
|
nbf: now.timestamp()
|
||||||
};
|
};
|
||||||
let header = Header::new(Algorithm::RS256);
|
let header = Header::new(Algorithm::RS256);
|
||||||
let bytes_private_key = general_purpose::STANDARD.decode(private_key).unwrap();
|
let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
|
||||||
let decoded_private_key = String::from_utf8(bytes_private_key).unwrap();
|
|
||||||
let key = EncodingKey::from_rsa_pem(decoded_private_key.as_bytes())?;
|
|
||||||
let token = encode(&header, &claims, &key)?;
|
let token = encode(&header, &claims, &key)?;
|
||||||
token_details.token = Some(token);
|
token_details.token = Some(token);
|
||||||
Ok(token_details)
|
Ok(token_details)
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ impl FromRequest for JwtAuth {
|
|||||||
})))
|
})))
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_key = env::var("ACCESS_TOKEN_PUBLIC_KEY")
|
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||||
.expect("ACCESS_TOKEN_PUBLIC_KEY must be set");
|
let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir)).expect("Failed to read access public key");
|
||||||
|
|
||||||
let access_token_details = match verify_token(&access_token, &public_key) {
|
let access_token_details = match verify_token(&access_token, &public_key) {
|
||||||
Ok(token_details) => token_details,
|
Ok(token_details) => token_details,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
|||||||
.parse::<i64>()
|
.parse::<i64>()
|
||||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||||
|
|
||||||
let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &email, (access_token_max_age * 60) as usize).await;
|
let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &access_token_details.to_string(), (access_token_max_age * 60) as usize).await;
|
||||||
if let Err(err) = access_result {
|
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 {
|
return ResponseError::error_response(&ServiceError {
|
||||||
@@ -85,7 +85,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &email, (refresh_token_max_age * 60) as usize).await;
|
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &refresh_token_details.to_string(), (refresh_token_max_age * 60) as usize).await;
|
||||||
if let Err(err) = refresh_result {
|
if let Err(err) = refresh_result {
|
||||||
error!("Failed to set refresh token in redis: {}", err);
|
error!("Failed to set refresh token in redis: {}", err);
|
||||||
return ResponseError::error_response(&ServiceError {
|
return ResponseError::error_response(&ServiceError {
|
||||||
@@ -150,8 +150,9 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY")
|
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||||
.expect("REFRESH_TOKEN_PUBLIC_KEY must be set");
|
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||||
|
.expect("Unable to read refresh public key");
|
||||||
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
||||||
Ok(token_details) => token_details,
|
Ok(token_details) => token_details,
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
@@ -177,12 +178,12 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete old auth token if it exists
|
// Delete old access token if it exists
|
||||||
match req.cookie("access_token") {
|
match req.cookie("access_token") {
|
||||||
Some(cookie) => {
|
Some(cookie) => {
|
||||||
let access_token = cookie.value().to_string();
|
let access_token = cookie.value().to_string();
|
||||||
let public_key = env::var("ACCESS_TOKEN_PUBLIC_KEY")
|
let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir))
|
||||||
.expect("ACCESS_TOKEN_PUBLIC_KEY must be set");
|
.expect("Unable to read access public key");
|
||||||
match verify_token(&access_token, &public_key) {
|
match verify_token(&access_token, &public_key) {
|
||||||
Ok(token_details) => {
|
Ok(token_details) => {
|
||||||
let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await;
|
let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await;
|
||||||
@@ -199,7 +200,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
.parse::<i64>()
|
.parse::<i64>()
|
||||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||||
|
|
||||||
let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &email, (access_token_max_age * 60) as usize).await;
|
let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &access_token_details.to_string(), (access_token_max_age * 60) as usize).await;
|
||||||
if let Err(err) = access_result {
|
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 {
|
return ResponseError::error_response(&ServiceError {
|
||||||
@@ -244,7 +245,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
.parse::<i64>()
|
.parse::<i64>()
|
||||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||||
|
|
||||||
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &refresh_token_details.email, (refresh_token_max_age * 60) as usize).await;
|
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &refresh_token_details.to_string(), (refresh_token_max_age * 60) as usize).await;
|
||||||
if let Err(err) = refresh_result {
|
if let Err(err) = refresh_result {
|
||||||
error!("Failed to set refresh token in redis: {}", err);
|
error!("Failed to set refresh token in redis: {}", err);
|
||||||
return ResponseError::error_response(&ServiceError {
|
return ResponseError::error_response(&ServiceError {
|
||||||
@@ -278,6 +279,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
|
|
||||||
#[post("/logout")]
|
#[post("/logout")]
|
||||||
async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
||||||
|
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||||
let refresh_token = match req.cookie("refresh_token") {
|
let refresh_token = match req.cookie("refresh_token") {
|
||||||
Some(cookie) => cookie.value().to_string(),
|
Some(cookie) => cookie.value().to_string(),
|
||||||
None => return ResponseError::error_response(&ServiceError {
|
None => return ResponseError::error_response(&ServiceError {
|
||||||
@@ -285,8 +287,8 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
|||||||
message: "Refresh token not found".to_string()
|
message: "Refresh token not found".to_string()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY")
|
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||||
.expect("REFRESH_TOKEN_PUBLIC_KEY must be set");
|
.expect("Unable to read refresh public key");
|
||||||
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
||||||
Ok(token_details) => token_details,
|
Ok(token_details) => token_details,
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
@@ -342,12 +344,13 @@ async fn me(auth: JwtAuth) -> HttpResponse {
|
|||||||
|
|
||||||
#[get("/check-session")]
|
#[get("/check-session")]
|
||||||
async fn check_session(req: HttpRequest) -> HttpResponse {
|
async fn check_session(req: HttpRequest) -> HttpResponse {
|
||||||
|
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||||
// If there is a access_token cookie, check if it is valid
|
// If there is a access_token cookie, check if it is valid
|
||||||
let has_session = match req.cookie("access_token") {
|
let has_session = match req.cookie("access_token") {
|
||||||
Some(cookie) => {
|
Some(cookie) => {
|
||||||
let access_token = cookie.value().to_string();
|
let access_token = cookie.value().to_string();
|
||||||
let public_key = env::var("ACCESS_TOKEN_PUBLIC_KEY")
|
let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir))
|
||||||
.expect("ACCESS_TOKEN_PUBLIC_KEY must be set");
|
.expect("Unable to read access public key");
|
||||||
match verify_token(&access_token, &public_key) {
|
match verify_token(&access_token, &public_key) {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(_) => false
|
Err(_) => false
|
||||||
@@ -360,8 +363,8 @@ async fn check_session(req: HttpRequest) -> HttpResponse {
|
|||||||
match req.cookie("refresh_token") {
|
match req.cookie("refresh_token") {
|
||||||
Some(cookie) => {
|
Some(cookie) => {
|
||||||
let refresh_token = cookie.value().to_string();
|
let refresh_token = cookie.value().to_string();
|
||||||
let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY")
|
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||||
.expect("REFRESH_TOKEN_PUBLIC_KEY must be set");
|
.expect("Unable to read refresh public key");
|
||||||
match verify_token(&refresh_token, &public_key) {
|
match verify_token(&refresh_token, &public_key) {
|
||||||
Ok(_) => return HttpResponse::Ok().json(true),
|
Ok(_) => return HttpResponse::Ok().json(true),
|
||||||
Err(_) => return HttpResponse::Ok().json(false)
|
Err(_) => return HttpResponse::Ok().json(false)
|
||||||
@@ -380,6 +383,7 @@ async fn roles() -> HttpResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||||
|
// TODO: Remove this when deploying
|
||||||
let r = RegisterUser {
|
let r = RegisterUser {
|
||||||
email: "admin".to_string(),
|
email: "admin".to_string(),
|
||||||
password: "admin".to_string(),
|
password: "admin".to_string(),
|
||||||
|
|||||||
@@ -141,6 +141,18 @@ impl From<s3::creds::error::CredentialsError> for ServiceError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<uuid::Error> for ServiceError {
|
||||||
|
fn from(error: uuid::Error) -> ServiceError {
|
||||||
|
ServiceError::new(500, format!("Unknown uuid error: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::env::VarError> for ServiceError {
|
||||||
|
fn from(error: std::env::VarError) -> ServiceError {
|
||||||
|
ServiceError::new(500, format!("Unknown env 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) {
|
||||||
|
|||||||
@@ -5,13 +5,6 @@ const nextConfig = {
|
|||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true
|
ignoreDuringBuilds: true
|
||||||
},
|
},
|
||||||
webpackDevMiddleware: (config) => {
|
|
||||||
config.watchOptions = {
|
|
||||||
poll: 1000,
|
|
||||||
aggregateTimeout: 300
|
|
||||||
};
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
publicRuntimeConfig: {
|
publicRuntimeConfig: {
|
||||||
// remove private variables from processEnv
|
// remove private variables from processEnv
|
||||||
processEnv: Object.fromEntries(Object.entries(process.env).filter(([key]) => key.includes('NEXT_PUBLIC_')))
|
processEnv: Object.fromEntries(Object.entries(process.env).filter(([key]) => key.includes('NEXT_PUBLIC_')))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
stopTrack
|
stopTrack
|
||||||
} from '@/api/guilds';
|
} from '@/api/guilds';
|
||||||
import { GuildChannel, GuildInfo } from '@/api/guilds.types';
|
import { GuildChannel, GuildInfo } from '@/api/guilds.types';
|
||||||
|
import Auth from '@/components/Auth';
|
||||||
import { userState } from '@/state/auth';
|
import { userState } from '@/state/auth';
|
||||||
import { Button, Card, Grid, Select, Slider, Tabs, TextInput, Textarea } from '@mantine/core';
|
import { Button, Card, Grid, Select, Slider, Tabs, TextInput, Textarea } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
@@ -21,7 +22,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
export default function Page() {
|
function Page() {
|
||||||
const user = useRecoilValue(userState);
|
const user = useRecoilValue(userState);
|
||||||
const [guilds, setGuilds] = useState<GuildInfo[]>([]);
|
const [guilds, setGuilds] = useState<GuildInfo[]>([]);
|
||||||
const [activeGuild, setActiveGuild] = useState<GuildInfo | null>(null);
|
const [activeGuild, setActiveGuild] = useState<GuildInfo | null>(null);
|
||||||
@@ -68,6 +69,8 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Auth(Page);
|
||||||
|
|
||||||
function TextChannelCard({ guild }: { guild: GuildInfo | null }) {
|
function TextChannelCard({ guild }: { guild: GuildInfo | null }) {
|
||||||
const [textChannels, setTextChannels] = useState<GuildChannel[]>([]);
|
const [textChannels, setTextChannels] = useState<GuildChannel[]>([]);
|
||||||
const [activeChannel, setActiveChannel] = useState<GuildChannel | null>(null);
|
const [activeChannel, setActiveChannel] = useState<GuildChannel | null>(null);
|
||||||
|
|||||||
@@ -3,15 +3,8 @@
|
|||||||
import { ActionIcon, Tooltip } from '@mantine/core';
|
import { ActionIcon, Tooltip } from '@mantine/core';
|
||||||
import { FaPlus } from "react-icons/fa";
|
import { FaPlus } from "react-icons/fa";
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { getCampigns } from '@/api/campaigns';
|
|
||||||
import { Campaign } from '@/api/campaigns.types';
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [campaigns, setCampaigns] = React.useState<Campaign[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCampigns().then((data) => setCampaigns(data));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -21,9 +14,6 @@ export default function Page() {
|
|||||||
<FaPlus />
|
<FaPlus />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{campaigns && campaigns.map((campaign) => (
|
|
||||||
<div key={campaign.id}>{campaign.name}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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 Loading from '@/components/Loading';
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Siren',
|
title: 'Siren',
|
||||||
@@ -28,10 +27,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
<Loading>
|
|
||||||
<Header />
|
<Header />
|
||||||
<Box>{children}</Box>
|
<Box>{children}</Box>
|
||||||
</Loading>
|
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</RecoilRootWrapper>
|
</RecoilRootWrapper>
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import React from 'react';
|
|||||||
// Home page for siren
|
// Home page for siren
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
// <div>
|
<div>
|
||||||
// <p>Siren is a Dungeon Master's best friend.</p>
|
<p>Siren is a Dungeon Master's best friend.</p>
|
||||||
// <h2>Features:</h2>
|
<h2>Features:</h2>
|
||||||
// <ul>
|
<ul>
|
||||||
// <li>Manage your campaign and players</li>
|
<li>Manage your campaign and players</li>
|
||||||
// <li>Create battlemaps on the fly and track initiative</li>
|
<li>Create battlemaps on the fly and track initiative</li>
|
||||||
// <li>Connect the Discord Bot to play online with friends</li>
|
<li>Connect the Discord Bot to play online with friends</li>
|
||||||
// <li>Reference Races, Classes, Items, Spells, and more</li>
|
<li>Reference Races, Classes, Items, Spells, and more</li>
|
||||||
// </ul>
|
</ul>
|
||||||
// </div>
|
|
||||||
<div style={{ overflow: 'hidden' }}>
|
|
||||||
<TileGrid />
|
|
||||||
</div>
|
</div>
|
||||||
|
// <div style={{ overflow: 'hidden' }}>
|
||||||
|
// <TileGrid />
|
||||||
|
// </div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
32
ui/src/components/Auth.tsx
Normal file
32
ui/src/components/Auth.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { hasUserState, isAdminState } from "@/state/auth";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useRecoilValue } from "recoil";
|
||||||
|
|
||||||
|
export default function Auth(Component: any, adminOnly = false) {
|
||||||
|
return function AuthWrapper(props: any) {
|
||||||
|
const router = useRouter();
|
||||||
|
const hasUser = useRecoilValue(hasUserState);
|
||||||
|
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('/');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component {...props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -199,6 +199,7 @@ export default function Header() {
|
|||||||
type={modalType}
|
type={modalType}
|
||||||
toggle={toggle}
|
toggle={toggle}
|
||||||
setUser={(u) => {
|
setUser={(u) => {
|
||||||
|
console.log(u);
|
||||||
setUser(u);
|
setUser(u);
|
||||||
updateUser(u);
|
updateUser(u);
|
||||||
}}
|
}}
|
||||||
|
|||||||
6
ui/src/middleware.ts
Normal file
6
ui/src/middleware.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export default function middleware(request: NextRequest) {
|
||||||
|
}
|
||||||
@@ -1,7 +1,24 @@
|
|||||||
import { User } from '@/api/auth.types';
|
import { User } from '@/api/auth.types';
|
||||||
import { atom } from 'recoil';
|
import { atom, selector } from 'recoil';
|
||||||
|
|
||||||
export const userState = atom({
|
export const userState = atom({
|
||||||
key: 'userState',
|
key: 'userState',
|
||||||
default: undefined as User | undefined
|
default: undefined as User | undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const hasUserState = selector({
|
||||||
|
key: 'hasUserState',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const user = get(userState);
|
||||||
|
return user !== undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const isAdminState = selector({
|
||||||
|
key: 'isAdminState',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const user = get(userState);
|
||||||
|
return user?.role === 'admin';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user