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, req: web::Json, ) -> Result { 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] #[put("password")] pub async fn update_password( state: web::Data, req: web::Json, ) -> Result { 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, req: web::Json, ) -> Result { 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::( &data.token, &jsonwebtoken::DecodingKey::from_secret(&jwt_secret), &jsonwebtoken::Validation::default(), ); decoded.expect("Decode failure"); Ok(()) } }