use std::{fs, str::FromStr}; use anyhow::{anyhow, Context}; use chrono::{DateTime, Utc}; use log::error; use actix_web::error::ErrorUnauthorized; use actix_web::{dev, http::header, Error, FromRequest, HttpRequest}; use futures::future::{err, ok, Ready}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use serde::{Deserialize, Serialize}; #[derive(Serialize)] pub struct Token<'a> { pub token: &'a str, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Claims { pub sub: String, pub exp: i64, } #[cfg(test)] pub mod helper { use super::Claims; use chrono::{Duration, Utc}; impl Claims { pub fn valid_user(user_id: String) -> Self { Claims { sub: user_id, exp: (Utc::now() + Duration::minutes(1)).timestamp(), } } } } pub fn secret_key() -> String { if cfg!(test) { String::from("test_key") } else { dotenv::var("SECRET_KEY").expect("SECRET_KEY env not set!") } } impl FromStr for Claims { type Err = jsonwebtoken::errors::Error; fn from_str(s: &str) -> Result { let token = *(s.split("Bearer ").collect::>().last().unwrap_or(&"")); match decode::( token, &DecodingKey::from_secret(secret_key().as_bytes()), &Validation::new(Algorithm::HS256), ) { Ok(data) => Ok(data.claims), Err(other) => { error!("DecodeError: {}", other); Err(other) } } } } impl FromRequest for Claims { type Error = Error; type Future = Ready>; fn from_request(req: &HttpRequest, _payload: &mut dev::Payload) -> Self::Future { req.headers() .get(header::AUTHORIZATION) .map_or_else( || Err(anyhow!("No authorization header")), |header| { header .to_str() .context("Unable to read Authorization header to string") }, ) .and_then(|header| { Claims::from_str(header) .with_context(|| format!("Unable to decode token from: {}", header)) }) .map_or_else( |e| { error!("{}", e); err(ErrorUnauthorized("Bad token")) }, ok, ) } } #[derive(Serialize, Deserialize, Debug)] pub struct PhotosResponse { pub photos: Vec, pub dirs: Vec, } #[derive(Deserialize)] pub struct FilesRequest { pub path: String, pub tag_ids: Option, // comma separated numbers pub tag_filter_mode: Option, } #[derive(Copy, Clone, Deserialize, PartialEq)] pub enum FilterMode { Any, All, } #[derive(Deserialize)] pub struct ThumbnailRequest { pub path: String, pub size: Option, } #[derive(Deserialize)] pub struct LoginRequest { pub username: String, pub password: String, } #[derive(Deserialize)] pub struct CreateAccountRequest { pub username: String, pub password: String, pub confirmation: String, } #[derive(Deserialize)] pub struct AddFavoriteRequest { pub path: String, } #[derive(Debug, Serialize)] pub struct MetadataResponse { pub created: Option, pub modified: Option, pub size: u64, } impl From for MetadataResponse { fn from(metadata: fs::Metadata) -> Self { MetadataResponse { created: metadata.created().ok().map(|created| { let utc: DateTime = created.into(); utc.timestamp() }), modified: metadata.modified().ok().map(|modified| { let utc: DateTime = modified.into(); utc.timestamp() }), size: metadata.len(), } } } #[derive(Debug, Deserialize)] pub struct AddTagRequest { pub file_name: String, pub tag_name: String, } #[derive(Deserialize)] pub struct GetTagsRequest { pub path: Option, } #[cfg(test)] mod tests { use super::Claims; use jsonwebtoken::errors::ErrorKind; use std::str::FromStr; #[test] fn test_token_from_claims() { let claims = Claims { exp: 16136164790, // 2481-ish sub: String::from("9"), }; let c = Claims::from_str( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5IiwiZXhwIjoxNjEzNjE2NDc5MH0.9wwK4l8vhvq55YoueEljMbN_5uVTaAsGLLRPr0AuymE") .unwrap(); assert_eq!(claims.sub, c.sub); assert_eq!(claims.exp, c.exp); } #[test] fn test_expired_token() { let err = Claims::from_str( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5IiwiZXhwIjoxNn0.eZnfaNfiD54VMbphIqeBICeG9SzAtwNXntLwtTBihjY"); match err.unwrap_err().into_kind() { ErrorKind::ExpiredSignature => assert!(true), kind => { println!("Unexpected error: {:?}", kind); assert!(false) } } } #[test] fn test_junk_token_is_invalid() { let err = Claims::from_str("uni-֍ՓՓՓՓՓՓՓՓՓՓՓՓՓՓՓ"); match err.unwrap_err().into_kind() { ErrorKind::InvalidToken => assert!(true), kind => { println!("Unexpected error: {:?}", kind); assert!(false) } } } }