Files
chatapp/backend/src/api/auth.rs
T
zxq5 529d09aabc frontend v0.4.1
- fixed most of the bugs with the rewrite. should be ready to deploy now
2026-04-08 00:00:28 +01:00

194 lines
5.9 KiB
Rust

use crate::error::ApiResult;
use crate::model::auth::{AccessTokenForm, AuthResponse, LoginCredentials, SignupCredentials};
use crate::svc::access_token_svc::AccessTokenService;
use crate::svc::auth_svc::AuthService;
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket::serde::json::Json;
use rocket::serde::{Deserialize, Serialize};
use rocket::{Request, State};
use std::sync::LazyLock;
use std::time::{SystemTime, UNIX_EPOCH};
#[post("/signup", data = "<cred>")]
pub async fn signup(
cred: Json<SignupCredentials>,
svc: &State<AuthService>,
) -> ApiResult<Json<AuthResponse>> {
let response = svc
.signup(
&cred.email,
&cred.username,
&cred.password,
&cred.access_token,
)
.await?;
Ok(Json(response))
}
#[post("/login", data = "<cred>")]
pub async fn login(
cred: Json<LoginCredentials>,
svc: &State<AuthService>,
) -> ApiResult<Json<AuthResponse>> {
Ok(Json(svc.login(&cred.username, &cred.password).await?))
}
#[post("/invite", data = "<form>")]
pub async fn generate_invite(
session: AdminSession,
form: Json<AccessTokenForm>,
svc: &State<AccessTokenService>,
) -> ApiResult<String> {
svc.create(
session.uid,
&form.name,
form.max_uses,
form.start_date,
form.expiry_date,
)
.await
}
static JWT_SECRET: LazyLock<String> = LazyLock::new(|| std::env::var("JWT_SECRET").unwrap());
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum TokenScope {
Full,
TotpPending,
}
pub struct Session {
pub uid: i64,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Session {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match Claims::from_request(req).await {
Outcome::Success(user) if user.scope == TokenScope::Full => Outcome::Success(Session {
uid: user.sub as i64,
}),
Outcome::Success(_) => {
eprintln!("warning: user with scope other than Full attempted to access session");
Outcome::Error((Status::Forbidden, ()))
}
Outcome::Error(err) => {
eprintln!("Session request guard failed: {:?}", err);
Outcome::Error(err)
}
_ => unreachable!("forward should never be called"),
}
}
}
pub struct AdminSession {
pub uid: i64,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AdminSession {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
// First verify the session is valid
match Claims::from_request(req).await {
Outcome::Success(user) if user.scope == TokenScope::Full => {
let uid = user.sub as i64;
// Get AuthService from Rocket state
let auth_svc = match req.guard::<&State<AuthService>>().await {
Outcome::Success(svc) => svc,
Outcome::Error(err) => {
tracing::error!("AdminSession: Failed to get AuthService from state");
return Outcome::Error(err);
}
_ => unreachable!("forward should never be called"),
};
// Check if user is admin
match auth_svc.is_admin(uid).await {
Ok(true) => Outcome::Success(AdminSession { uid }),
Ok(false) => {
tracing::debug!("non-admin user attempted to access admin session");
Outcome::Error((Status::Forbidden, ()))
}
Err(err) => {
tracing::error!("AdminSession: is_admin check failed: {:?}", err);
Outcome::Error((Status::InternalServerError, ()))
}
}
}
Outcome::Success(_) => {
tracing::debug!("warning: user with scope other than Full attempted to access admin session");
Outcome::Error((Status::Forbidden, ()))
}
Outcome::Error(err) => {
tracing::debug!("AdminSession request guard failed: {:?}", err);
Outcome::Error(err)
}
_ => unreachable!("forward should never be called"),
}
}
}
#[derive(Serialize, Deserialize)]
pub struct Claims {
pub sub: i32,
pub exp: usize,
pub scope: TokenScope,
}
impl Claims {
pub fn new(user_id: usize, scope: TokenScope) -> Self {
Self {
sub: user_id as i32,
exp: (SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get time")
.as_secs()
+ 60 * 60 * 24 * 7) as usize,
scope,
}
}
pub fn encode(&self) -> String {
encode(
&Header::default(),
self,
&EncodingKey::from_secret(JWT_SECRET.as_bytes()),
)
.expect("unable to encode jwt")
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Claims {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let token = req
.headers()
.get_one("Authorization")
.and_then(|v| v.strip_prefix("Bearer "));
match token {
None => Outcome::Error((Status::Unauthorized, ())),
Some(t) => {
match decode::<Claims>(
t,
&DecodingKey::from_secret(JWT_SECRET.as_bytes()),
&Validation::default(),
) {
Ok(data) => Outcome::Success(data.claims),
Err(_) => Outcome::Error((Status::Unauthorized, ())),
}
}
}
}
}