Initial commit
ci / docker (backend, backend/Dockerfile, backend) (push) Successful in 21s
ci / docker (frontend, frontend/Dockerfile, frontend) (push) Successful in 23s

This commit is contained in:
2025-10-17 20:19:47 +02:00
commit 2af2af3460
132 changed files with 35408 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
use anyhow::Result;
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::config::AppConfig;
#[derive(Clone)]
pub struct JwtService {
encoding: EncodingKey,
decoding: DecodingKey,
issuer: String,
audience: String,
expiry: Duration,
download_audience: String,
download_expiry: Duration,
}
impl JwtService {
pub fn from_config(config: &AppConfig) -> Result<Self> {
Ok(Self {
encoding: EncodingKey::from_secret(config.jwt_secret.as_bytes()),
decoding: DecodingKey::from_secret(config.jwt_secret.as_bytes()),
issuer: config.jwt_issuer.clone(),
audience: config.jwt_audience.clone(),
expiry: Duration::minutes(config.jwt_expiry_minutes),
download_audience: config.download_token_audience.clone(),
download_expiry: Duration::minutes(config.download_token_expiry_minutes),
})
}
pub fn generate_token(&self, user_id: Uuid, username: &str, role: &str) -> Result<String> {
let now = Utc::now();
let exp = now + self.expiry;
let claims = Claims {
sub: user_id,
username: username.to_owned(),
role: role.to_owned(),
iss: self.issuer.clone(),
aud: self.audience.clone(),
iat: now.timestamp() as usize,
exp: exp.timestamp() as usize,
};
Ok(encode(&Header::default(), &claims, &self.encoding)?)
}
pub fn verify_token(&self, token: &str) -> Result<Claims> {
let mut validation = Validation::default();
validation.set_audience(&[self.audience.clone()]);
validation.set_issuer(&[self.issuer.clone()]);
let data = decode::<Claims>(token, &self.decoding, &validation)?;
Ok(data.claims)
}
pub fn generate_download_token(&self, document_id: Uuid, user_id: Uuid) -> Result<String> {
let now = Utc::now();
let exp = now + self.download_expiry;
let claims = DownloadClaims {
doc_id: document_id,
user_id,
iss: self.issuer.clone(),
aud: self.download_audience.clone(),
iat: now.timestamp() as usize,
exp: exp.timestamp() as usize,
};
Ok(encode(&Header::default(), &claims, &self.encoding)?)
}
pub fn verify_download_token(&self, token: &str) -> Result<DownloadClaims> {
let mut validation = Validation::default();
validation.set_audience(&[self.download_audience.clone()]);
validation.set_issuer(&[self.issuer.clone()]);
let data = decode::<DownloadClaims>(token, &self.decoding, &validation)?;
Ok(data.claims)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: Uuid,
pub username: String,
pub role: String,
pub iss: String,
pub aud: String,
pub iat: usize,
pub exp: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DownloadClaims {
pub doc_id: Uuid,
pub user_id: Uuid,
pub iss: String,
pub aud: String,
pub iat: usize,
pub exp: usize,
}
+42
View File
@@ -0,0 +1,42 @@
pub mod jwt;
pub mod password;
use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
use axum_extra::headers::{authorization::Bearer, Authorization};
use axum_extra::TypedHeader;
use serde::{Deserialize, Serialize};
use crate::{error::AppError, state::AppState};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthenticatedUser {
pub user_id: uuid::Uuid,
pub username: String,
pub role: String,
}
#[async_trait]
impl FromRequestParts<AppState> for AuthenticatedUser {
type Rejection = AppError;
async fn from_request_parts(
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
let TypedHeader(Authorization(bearer)) =
TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
.await
.map_err(|_| AppError::unauthorized())?;
let claims = state
.jwt
.verify_token(bearer.token())
.map_err(|_| AppError::unauthorized())?;
Ok(AuthenticatedUser {
user_id: claims.sub,
username: claims.username,
role: claims.role,
})
}
}
+12
View File
@@ -0,0 +1,12 @@
use anyhow::{anyhow, Result};
use argon2::{
password_hash::{PasswordHash, PasswordVerifier},
Argon2,
};
pub fn verify_password(password: &str, password_hash: &str) -> Result<bool> {
let parsed_hash = PasswordHash::new(password_hash).map_err(|err| anyhow!(err))?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}