app/src/api/authorization.rs

139 lines
3.7 KiB
Rust

use std::ops::Add;
use jsonwebtoken::{encode, EncodingKey, Header};
use tracing::instrument;
use crate::api::api_prelude::*;
use crate::auth::{generate_secret, get_secret, set_password, verify_password};
use crate::error::{Error, Result};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthRequest {
password: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthResponse {
token: String,
}
#[instrument]
#[post("authorize")]
pub async fn authorize(
state: web::Data<AppState>,
req: web::Json<AuthRequest>,
) -> Result<HttpResponse> {
if verify_password(&state.db, &req.password).await {
let secret = get_secret(&state.db).await?;
let exp = chrono::Utc::now()
.add(chrono::Duration::days(30))
.timestamp() as usize;
let token = encode(
&Header::default(),
&crate::auth::AuthClaims { exp },
&EncodingKey::from_secret(&secret),
)
.map_err(|_| Error::unauthorized())?;
Ok(HttpResponse::Ok().json(AuthResponse { token }))
} else {
Err(Error::unauthorized())
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePasswordRequest {
password: String,
}
#[instrument]
#[post("password")]
pub async fn update_password(
state: web::Data<AppState>,
req: web::Json<UpdatePasswordRequest>,
) -> Result<HttpResponse> {
set_password(&state.db, &req.password).await?;
Ok(HttpResponse::Ok().body(""))
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetupRequest {
password: String,
}
#[instrument]
#[post("setup")]
pub async fn initial_setup(
state: web::Data<AppState>,
req: web::Json<SetupRequest>,
) -> Result<HttpResponse> {
let count = setting::Entity::find()
.filter(setting::Column::SettingName.eq("password_hash".to_string()))
.count(&state.db)
.await?;
if count == 0 {
set_password(&state.db, &req.password).await?;
generate_secret(&state.db).await?;
Ok(HttpResponse::Ok().body(""))
} else {
Err(Error::new(
crate::error::ErrorCode::UnAuthorized,
"Setup has already been run",
))
}
}
#[cfg(test)]
mod tests {
use crate::{
api::test_prelude::*,
auth::{get_secret, AuthClaims},
};
use actix_web::http::Method;
#[actix_rt::test]
async fn test_authorize() -> Result<()> {
let state = setup_state().await?;
let test_password = "sshh a secret".to_string();
let test_hash = bcrypt::hash(test_password.clone(), bcrypt::DEFAULT_COST).unwrap();
setting::ActiveModel {
id: NotSet,
setting_name: Set("password_hash".into()),
setting_value: Set(test_hash),
}
.insert(&state.db)
.await?;
let mut req = actix_web::test::TestRequest::with_uri("/api/authorize")
.method(Method::POST)
.set_json(super::AuthRequest {
password: test_password,
})
.to_request();
let resp = call_endpoint!(req, state);
let jwt_secret = get_secret(&state.db).await?;
let status = resp.status();
assert_eq!(status, 200);
let data = get_response!(resp, super::AuthResponse);
assert_eq!(status, 200);
let decoded = jsonwebtoken::decode::<AuthClaims>(
&data.token,
&jsonwebtoken::DecodingKey::from_secret(&jwt_secret),
&jsonwebtoken::Validation::default(),
);
decoded.expect("Decode failure");
Ok(())
}
}