full backend rewrite.

calling this v0.4.0
This commit is contained in:
2026-04-06 00:57:23 +01:00
parent a2f7f5a505
commit bda1ef251a
55 changed files with 2945 additions and 1464 deletions
+134
View File
@@ -0,0 +1,134 @@
use crate::error::ApiResult;
use crate::model::auth::{AccessTokenForm, AuthResponse, LoginCredentials, SignupCredentials};
use crate::svc::auth_svc::AuthService;
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
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};
use crate::svc::access_token_svc::AccessTokenService;
#[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: Session,
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"),
}
}
}
#[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)
.unwrap()
.as_secs()
+ 3600) 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, ())),
}
}
}
}
}