529d09aabc
- fixed most of the bugs with the rewrite. should be ready to deploy now
194 lines
5.9 KiB
Rust
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, ())),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|