use std::{fs, str::FromStr}; use crate::database::models::ImageExif; use anyhow::{Context, anyhow}; use chrono::{DateTime, Utc}; use log::error; use actix_web::error::ErrorUnauthorized; use actix_web::{Error, FromRequest, HttpRequest, dev, http::header}; use futures::future::{Ready, err, ok}; use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; 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.strip_prefix("Bearer ").ok_or_else(|| { jsonwebtoken::errors::Error::from(jsonwebtoken::errors::ErrorKind::InvalidToken) })?; 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(Copy, Clone, Deserialize, PartialEq, Debug)] #[serde(rename_all = "lowercase")] pub enum SortType { Shuffle, NameAsc, NameDesc, TagCountAsc, TagCountDesc, DateTakenAsc, DateTakenDesc, } #[derive(Deserialize)] pub struct FilesRequest { pub path: String, // comma separated numbers pub tag_ids: Option, pub exclude_tag_ids: Option, pub tag_filter_mode: Option, pub recursive: Option, pub sort: Option, // EXIF-based search parameters pub camera_make: Option, pub camera_model: Option, pub lens_model: Option, // GPS location search pub gps_lat: Option, pub gps_lon: Option, pub gps_radius_km: Option, // Date range filtering (Unix timestamps) pub date_from: Option, pub date_to: Option, // Media type filtering pub media_type: Option, } #[derive(Copy, Clone, Deserialize, PartialEq, Debug)] pub enum FilterMode { Any, All, } #[derive(Copy, Clone, Deserialize, PartialEq, Debug)] #[serde(rename_all = "lowercase")] pub enum MediaType { Photo, Video, All, } #[derive(Copy, Clone, Deserialize, PartialEq, Debug)] #[serde(rename_all = "lowercase")] pub enum PhotoSize { Full, Thumb, } #[derive(Debug, Deserialize)] pub struct ThumbnailRequest { pub(crate) path: String, pub(crate) size: Option, #[serde(default)] pub(crate) format: Option, } #[derive(Debug, Deserialize, PartialEq)] pub enum ThumbnailFormat { #[serde(rename = "gif")] Gif, #[serde(rename = "image")] Image, } #[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, pub exif: Option, } 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(), exif: None, } } } #[derive(Debug, Serialize)] pub struct ExifMetadata { pub camera: Option, pub image_properties: Option, pub gps: Option, pub capture_settings: Option, pub date_taken: Option, } #[derive(Debug, Serialize)] pub struct CameraInfo { pub make: Option, pub model: Option, pub lens: Option, } #[derive(Debug, Serialize)] pub struct ImageProperties { pub width: Option, pub height: Option, pub orientation: Option, } #[derive(Debug, Serialize)] pub struct GpsCoordinates { pub latitude: Option, pub longitude: Option, pub altitude: Option, } #[derive(Debug, Serialize)] pub struct CaptureSettings { pub focal_length: Option, pub aperture: Option, pub shutter_speed: Option, pub iso: Option, } impl From for ExifMetadata { fn from(exif: ImageExif) -> Self { let has_camera_info = exif.camera_make.is_some() || exif.camera_model.is_some() || exif.lens_model.is_some(); let has_image_properties = exif.width.is_some() || exif.height.is_some() || exif.orientation.is_some(); let has_gps = exif.gps_latitude.is_some() || exif.gps_longitude.is_some() || exif.gps_altitude.is_some(); let has_capture_settings = exif.focal_length.is_some() || exif.aperture.is_some() || exif.shutter_speed.is_some() || exif.iso.is_some(); ExifMetadata { camera: if has_camera_info { Some(CameraInfo { make: exif.camera_make, model: exif.camera_model, lens: exif.lens_model, }) } else { None }, image_properties: if has_image_properties { Some(ImageProperties { width: exif.width, height: exif.height, orientation: exif.orientation, }) } else { None }, gps: if has_gps { Some(GpsCoordinates { latitude: exif.gps_latitude, longitude: exif.gps_longitude, altitude: exif.gps_altitude, }) } else { None }, capture_settings: if has_capture_settings { Some(CaptureSettings { focal_length: exif.focal_length, aperture: exif.aperture, shutter_speed: exif.shutter_speed, iso: exif.iso, }) } else { None }, date_taken: exif.date_taken, } } } #[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( "Bearer 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( "Bearer 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) } } } }