222 lines
5.4 KiB
Rust
222 lines
5.4 KiB
Rust
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<Self, Self::Err> {
|
|
let token = *(s.split("Bearer ").collect::<Vec<_>>().last().unwrap_or(&""));
|
|
|
|
match decode::<Claims>(
|
|
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<Result<Self, Self::Error>>;
|
|
|
|
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<String>,
|
|
pub dirs: Vec<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct FilesRequest {
|
|
pub path: String,
|
|
pub tag_ids: Option<String>, // comma separated numbers
|
|
pub tag_filter_mode: Option<FilterMode>,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Deserialize, PartialEq)]
|
|
pub enum FilterMode {
|
|
Any,
|
|
All,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct ThumbnailRequest {
|
|
pub path: String,
|
|
pub size: Option<String>,
|
|
}
|
|
|
|
#[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<i64>,
|
|
pub modified: Option<i64>,
|
|
pub size: u64,
|
|
}
|
|
|
|
impl From<fs::Metadata> for MetadataResponse {
|
|
fn from(metadata: fs::Metadata) -> Self {
|
|
MetadataResponse {
|
|
created: metadata.created().ok().map(|created| {
|
|
let utc: DateTime<Utc> = created.into();
|
|
utc.timestamp()
|
|
}),
|
|
modified: metadata.modified().ok().map(|modified| {
|
|
let utc: DateTime<Utc> = 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<String>,
|
|
}
|
|
|
|
#[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)
|
|
}
|
|
}
|
|
}
|
|
}
|