Rewrote backend in Rust
This commit is contained in:
129
backend/src/routes/admin.rs
Normal file
129
backend/src/routes/admin.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use warp::{Filter, Reply};
|
||||
use crate::db::{DBConnection, DBPool, with_db};
|
||||
use crate::dto;
|
||||
use crate::routes::{AppError, get_reply};
|
||||
use crate::routes::filters::{admin, UserInfo};
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
|
||||
let users = warp::path!("admin" / "users")
|
||||
.and(warp::get())
|
||||
.and(admin(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(users);
|
||||
let set_role = warp::path!("admin" / "set_role")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(admin(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(set_role);
|
||||
let logout = warp::path!("admin" / "logout")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(admin(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(logout);
|
||||
let delete_user = warp::path!("admin" / "delete")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(admin(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(delete_user);
|
||||
let disable_2fa = warp::path!("admin" / "disable_2fa")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(admin(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(disable_2fa);
|
||||
let is_admin = warp::path!("admin" / "is_admin")
|
||||
.and(warp::get())
|
||||
.and(admin(db.clone()))
|
||||
.and_then(|_| async { get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
}) });
|
||||
let get_token = warp::path!("admin" / "get_token" / i32)
|
||||
.and(warp::get())
|
||||
.and(admin(db.clone()))
|
||||
.and(with_db(db))
|
||||
.and_then(get_token);
|
||||
|
||||
users.or(set_role).or(logout).or(delete_user).or(disable_2fa).or(is_admin).or(get_token)
|
||||
}
|
||||
|
||||
async fn users(_: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let users = db.get_users();
|
||||
|
||||
let mut res = dto::responses::AdminUsers {
|
||||
statusCode: 200,
|
||||
users: Vec::new()
|
||||
};
|
||||
|
||||
for user in users {
|
||||
res.users.push(dto::responses::AdminUsersEntry {
|
||||
id: user.id,
|
||||
gitlab: user.gitlab,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
tfaEnabled: user.tfa_type != crate::db::TfaTypes::None
|
||||
});
|
||||
}
|
||||
|
||||
get_reply(&res)
|
||||
}
|
||||
|
||||
async fn set_role(data: dto::requests::AdminSetRole, _: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let mut user = db.get_user(data.user)
|
||||
.ok_or(AppError::Forbidden("Invalid user"))?;
|
||||
user.role = data.role;
|
||||
db.save_user(&user);
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
|
||||
async fn logout(data: dto::requests::Admin, _: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
db.delete_all_tokens(data.user);
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_user(data: dto::requests::Admin, _: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let user = db.get_user(data.user)
|
||||
.ok_or(AppError::Forbidden("Invalid user"))?;
|
||||
|
||||
db.delete_all_tokens(data.user);
|
||||
|
||||
let root_node = super::fs::get_node_and_validate(&user, user.root_id, &mut db).expect("Failed to get root node for deleting");
|
||||
|
||||
super::fs::delete_node_root(&root_node, &mut db);
|
||||
|
||||
db.delete_user(&user);
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
|
||||
async fn disable_2fa(data: dto::requests::Admin, _: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let mut user = db.get_user(data.user)
|
||||
.ok_or(AppError::Forbidden("Invalid user"))?;
|
||||
|
||||
user.tfa_type = crate::db::TfaTypes::None;
|
||||
db.save_user(&user);
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_token(user: i32, _: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let user = db.get_user(user)
|
||||
.ok_or(AppError::Forbidden("Invalid user"))?;
|
||||
|
||||
get_reply(&dto::responses::Login {
|
||||
statusCode: 200,
|
||||
jwt: super::auth::get_token(&user, &mut db)
|
||||
})
|
||||
}
|
||||
115
backend/src/routes/auth/basic.rs
Normal file
115
backend/src/routes/auth/basic.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use warp::Filter;
|
||||
use crate::db::{DBConnection, DBPool, with_db};
|
||||
use crate::db::{TfaTypes, UserRole};
|
||||
use crate::dto;
|
||||
use crate::dto::requests::ChangePassword;
|
||||
use crate::routes::{AppError, get_reply};
|
||||
use crate::routes::filters::{authenticated, UserInfo};
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
let login = warp::path!("auth" / "login")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(login);
|
||||
let signup = warp::path!("auth" / "signup")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(signup);
|
||||
let refresh = warp::path!("auth" / "refresh")
|
||||
.and(warp::post())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(refresh);
|
||||
let logout_all = warp::path!("auth" / "logout_all")
|
||||
.and(warp::post())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(logout_all);
|
||||
let change_password = warp::path!("auth" / "change_password")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db))
|
||||
.and_then(change_password);
|
||||
|
||||
login.or(signup).or(refresh).or(logout_all).or(change_password)
|
||||
}
|
||||
|
||||
async fn login(data: dto::requests::Login, mut db: DBConnection)
|
||||
-> Result<impl warp::Reply, warp::Rejection> {
|
||||
let user = db.find_user(&data.username, false)
|
||||
.ok_or(AppError::Unauthorized("Invalid username or password"))?;
|
||||
|
||||
if !argon2::verify_encoded(user.password.as_str(), data.password.as_bytes()).unwrap_or(false) {
|
||||
return AppError::Unauthorized("Invalid username or password").err();
|
||||
}
|
||||
|
||||
if user.role == UserRole::Disabled {
|
||||
return AppError::Unauthorized("Account is disabled").err();
|
||||
}
|
||||
|
||||
if user.tfa_type != TfaTypes::None {
|
||||
if let Some(otp) = data.otp {
|
||||
if !super::tfa::verify2fa(&user, otp) {
|
||||
return AppError::Unauthorized("Incorrect 2fa").err();
|
||||
}
|
||||
} else {
|
||||
if user.tfa_type == TfaTypes::Email { super::tfa::send_2fa_mail(&user); }
|
||||
|
||||
return get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_reply(&dto::responses::Login {
|
||||
statusCode: 200,
|
||||
jwt: super::get_token(&user, &mut db)
|
||||
})
|
||||
}
|
||||
|
||||
async fn signup(data: dto::requests::SignUp, mut db: DBConnection)
|
||||
-> Result<impl warp::Reply, warp::Rejection> {
|
||||
if db.find_user(&data.username, false).is_some() {
|
||||
return AppError::BadRequest("Username is already taken").err();
|
||||
}
|
||||
|
||||
db.create_user_password(data.username, super::hash_password(&data.password));
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
|
||||
async fn refresh(info: UserInfo, mut db: DBConnection) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
db.delete_token(info.1.id);
|
||||
|
||||
get_reply(&dto::responses::Login {
|
||||
statusCode: 200,
|
||||
jwt: super::get_token(&info.0, &mut db)
|
||||
})
|
||||
}
|
||||
|
||||
async fn logout_all(info: UserInfo, mut db: DBConnection) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
db.delete_all_tokens(info.0.id);
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
|
||||
async fn change_password(data: ChangePassword, mut info: UserInfo, mut db: DBConnection) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
if !argon2::verify_encoded(info.0.password.as_str(), data.oldPassword.as_bytes()).unwrap_or(false) {
|
||||
return AppError::Unauthorized("Old password is wrong").err();
|
||||
}
|
||||
|
||||
info.0.password = super::hash_password(&data.newPassword);
|
||||
db.save_user(&info.0);
|
||||
db.delete_all_tokens(info.0.id);
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
108
backend/src/routes/auth/gitlab.rs
Normal file
108
backend/src/routes/auth/gitlab.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use cached::proc_macro::cached;
|
||||
use lazy_static::lazy_static;
|
||||
use warp::{Filter, Reply};
|
||||
use crate::config::CONFIG;
|
||||
use crate::db::{DBConnection, DBPool, with_db};
|
||||
use crate::routes::AppError;
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct GitlabTokens {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct GitlabUser {
|
||||
pub username: String,
|
||||
pub is_admin: bool
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct GitlabCallbackQuery {
|
||||
pub code: String
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref REDIRECT_URL: String = CONFIG.gitlab_redirect_url.clone() + "/api/auth/gitlab_callback";
|
||||
static ref TOKEN_URL: String = format!("{}/oauth/token", CONFIG.gitlab_api_url.clone());
|
||||
static ref USER_URL: String = format!("{}/api/v4/user", CONFIG.gitlab_api_url.clone());
|
||||
static ref AUTHORIZE_URL: String = format!("{}/oauth/authorize", CONFIG.gitlab_url.clone());
|
||||
}
|
||||
|
||||
pub fn get_gitlab_token(code_or_token: String, token: bool) -> Option<GitlabTokens> {
|
||||
let mut req = ureq::post(&TOKEN_URL)
|
||||
.query("redirect_uri", &REDIRECT_URL)
|
||||
.query("client_id", &CONFIG.gitlab_id)
|
||||
.query("client_secret", &CONFIG.gitlab_secret);
|
||||
if token {
|
||||
req = req
|
||||
.query("refresh_token", &code_or_token)
|
||||
.query("grant_type", "refresh_token");
|
||||
} else {
|
||||
req = req
|
||||
.query("code", &code_or_token)
|
||||
.query("grant_type", "authorization_code");
|
||||
}
|
||||
req.call().ok()?.into_json().ok()
|
||||
}
|
||||
|
||||
#[cached(time=300, time_refresh=false, option=true)]
|
||||
pub fn get_gitlab_user(token: String) -> Option<GitlabUser> {
|
||||
ureq::get(&USER_URL)
|
||||
.set("Authorization", &format!("Bearer {}", token))
|
||||
.call()
|
||||
.ok()?
|
||||
.into_json().ok()
|
||||
}
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
|
||||
let gitlab = warp::path!("auth" / "gitlab")
|
||||
.and(warp::get())
|
||||
.and_then(gitlab);
|
||||
let gitlab_callback = warp::path!("auth" / "gitlab_callback")
|
||||
.and(warp::get())
|
||||
.and(warp::query::query::<GitlabCallbackQuery>())
|
||||
.and(with_db(db))
|
||||
.and_then(gitlab_callback);
|
||||
|
||||
gitlab.or(gitlab_callback)
|
||||
}
|
||||
|
||||
async fn gitlab() -> Result<impl Reply, warp::Rejection> {
|
||||
let uri = format!("{}?redirect_uri={}&client_id={}&scope=read_user&response_type=code", AUTHORIZE_URL.as_str(), REDIRECT_URL.as_str(), CONFIG.gitlab_id);
|
||||
Ok(warp::redirect::found(uri.parse::<warp::http::Uri>().expect("Failed to parse gitlab auth uri")))
|
||||
}
|
||||
|
||||
async fn gitlab_callback(code: GitlabCallbackQuery, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
use crate::db::UserRole;
|
||||
|
||||
let tokens = get_gitlab_token(code.code, false).ok_or(AppError::Unauthorized("Invalid code"))?;
|
||||
let gitlab_user = get_gitlab_user(tokens.access_token.clone()).ok_or(AppError::Unauthorized("Invalid code"))?;
|
||||
|
||||
let user = db.find_user(&gitlab_user.username, true);
|
||||
|
||||
let user = match user {
|
||||
Some(mut v) => {
|
||||
v.gitlab_at = Some(tokens.access_token);
|
||||
v.gitlab_rt = Some(tokens.refresh_token);
|
||||
db.save_user(&v);
|
||||
v
|
||||
},
|
||||
None => {
|
||||
db.create_user_gitlab(
|
||||
gitlab_user.username,
|
||||
if gitlab_user.is_admin { UserRole::Admin } else { UserRole::Disabled },
|
||||
tokens.access_token,
|
||||
tokens.refresh_token
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if user.role == UserRole::Disabled {
|
||||
Ok(warp::reply::html("<!DOCTYPE html><html><h2>Your account is disabled, please contact an admin.<br/><a href=\"/login\">Go to login page</a></h2></html>").into_response())
|
||||
} else {
|
||||
let uri = format!("/set_token?token={}", super::get_token(&user, &mut db));
|
||||
Ok(warp::redirect::found(uri.parse::<warp::http::Uri>().expect("Failed to parse set_token uri")).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
75
backend/src/routes/auth/mod.rs
Normal file
75
backend/src/routes/auth/mod.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
mod basic;
|
||||
mod tfa;
|
||||
pub mod gitlab;
|
||||
|
||||
use std::ops::Add;
|
||||
use lazy_static::lazy_static;
|
||||
use ring::rand;
|
||||
use ring::rand::SecureRandom;
|
||||
use warp::Filter;
|
||||
use crate::db::DBPool;
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
SEC_RANDOM.fill(&mut [0; 1]).expect("Failed to init secure random");
|
||||
basic::build_routes(db.clone())
|
||||
.or(tfa::build_routes(db.clone()))
|
||||
.or(gitlab::build_routes(db))
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct JWTClaims {
|
||||
pub exp: i64,
|
||||
pub iat: i64,
|
||||
pub jti: i32,
|
||||
pub sub: i32
|
||||
}
|
||||
|
||||
pub static JWT_ALGORITHM: jsonwebtoken::Algorithm = jsonwebtoken::Algorithm::HS512;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SEC_RANDOM: rand::SystemRandom = rand::SystemRandom::new();
|
||||
pub static ref JWT_SECRET: Vec<u8> = get_jwt_secret();
|
||||
pub static ref JWT_DECODE_KEY: jsonwebtoken::DecodingKey = jsonwebtoken::DecodingKey::from_secret(JWT_SECRET.as_slice());
|
||||
pub static ref JWT_ENCODE_KEY: jsonwebtoken::EncodingKey = jsonwebtoken::EncodingKey::from_secret(JWT_SECRET.as_slice());
|
||||
}
|
||||
|
||||
fn get_jwt_secret() -> Vec<u8> {
|
||||
let secret = std::fs::read("jwt.secret");
|
||||
if let Ok(secret) = secret {
|
||||
secret
|
||||
} else {
|
||||
let mut secret: [u8; 128] = [0; 128];
|
||||
SEC_RANDOM.fill(&mut secret).expect("Failed to generate jwt secret");
|
||||
std::fs::write("jwt.secret", secret).expect("Failed to write jwt secret");
|
||||
Vec::from(secret)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_token(user: &crate::db::User, db: &mut crate::db::DBConnection) -> String {
|
||||
let iat = chrono::Utc::now();
|
||||
let exp = iat.add(chrono::Duration::hours(24)).timestamp();
|
||||
let iat = iat.timestamp();
|
||||
|
||||
let token = db.create_token(user.id, exp);
|
||||
|
||||
let claims = JWTClaims {
|
||||
exp,
|
||||
iat,
|
||||
jti: token.id,
|
||||
sub: user.id
|
||||
};
|
||||
|
||||
jsonwebtoken::encode(&jsonwebtoken::Header::new(JWT_ALGORITHM), &claims, &JWT_ENCODE_KEY)
|
||||
.expect("Failed to create JWT token")
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &String) -> String {
|
||||
let mut salt = [0_u8; 16];
|
||||
SEC_RANDOM.fill(&mut salt).expect("Failed to generate salt");
|
||||
let config = argon2::Config {
|
||||
mem_cost: 64 * 1024,
|
||||
variant: argon2::Variant::Argon2id,
|
||||
..Default::default()
|
||||
};
|
||||
argon2::hash_encoded(password.as_bytes(), &salt, &config).expect("Failed to hash password")
|
||||
}
|
||||
136
backend/src/routes/auth/tfa.rs
Normal file
136
backend/src/routes/auth/tfa.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use lazy_static::lazy_static;
|
||||
use lettre::Transport;
|
||||
use ring::rand::SecureRandom;
|
||||
use warp::Filter;
|
||||
use crate::config::CONFIG;
|
||||
use crate::db::{DBConnection, DBPool, with_db, TfaTypes};
|
||||
use crate::dto;
|
||||
use crate::routes::{AppError, get_reply};
|
||||
use crate::routes::filters::{authenticated, UserInfo};
|
||||
|
||||
fn build_mail_sender() -> lettre::SmtpTransport {
|
||||
lettre::SmtpTransport::builder_dangerous(CONFIG.smtp_server.clone())
|
||||
.port(CONFIG.smtp_port)
|
||||
.tls(
|
||||
lettre::transport::smtp::client::Tls::Required(
|
||||
lettre::transport::smtp::client::TlsParameters::new(
|
||||
CONFIG.smtp_server.clone()
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.credentials(lettre::transport::smtp::authentication::Credentials::new(CONFIG.smtp_user.clone(), CONFIG.smtp_password.clone()))
|
||||
.build()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref MAIL_SENDER: lettre::SmtpTransport = build_mail_sender();
|
||||
}
|
||||
|
||||
fn get_totp(user: &crate::db::User) -> totp_rs::TOTP {
|
||||
totp_rs::TOTP::from_rfc6238(
|
||||
totp_rs::Rfc6238::new(
|
||||
6,
|
||||
user.tfa_secret.clone().unwrap(),
|
||||
Some("MFileserver".to_owned()),
|
||||
user.name.clone()
|
||||
).unwrap()
|
||||
).unwrap()
|
||||
}
|
||||
|
||||
pub fn verify2fa(user: &crate::db::User, code: String) -> bool {
|
||||
let allowed_skew = if user.tfa_type == TfaTypes::Totp {0} else {10};
|
||||
let totp = get_totp(user);
|
||||
let time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let base_step = time / totp.step - allowed_skew;
|
||||
for i in 0..allowed_skew + 1 {
|
||||
let step = (base_step + i) * totp.step;
|
||||
if totp.generate(step).eq(&code) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn send_2fa_mail(user: &crate::db::User) {
|
||||
let totp = get_totp(user);
|
||||
let code = totp.generate_current().unwrap();
|
||||
let mail = lettre::Message::builder()
|
||||
.from("fileserver@mattv.de".parse().unwrap())
|
||||
.to(user.name.parse().unwrap())
|
||||
.subject("MFileserver - Email 2fa code")
|
||||
.body(format!("Your code is: {}\r\nIt is valid for 5 minutes", code))
|
||||
.unwrap();
|
||||
|
||||
MAIL_SENDER.send(&mail).expect("Failed to send mail");
|
||||
}
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
let tfa_setup = warp::path!("auth" / "2fa" / "setup")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(tfa_setup);
|
||||
let tfa_complete = warp::path!("auth" / "2fa" / "complete")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(tfa_complete);
|
||||
let tfa_disable = warp::path!("auth" / "2fa" / "disable")
|
||||
.and(warp::post())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db))
|
||||
.and_then(tfa_disable);
|
||||
|
||||
tfa_setup.or(tfa_complete).or(tfa_disable)
|
||||
}
|
||||
|
||||
async fn tfa_setup(data: dto::requests::TfaSetup, mut info: UserInfo, mut db: DBConnection)
|
||||
-> Result<impl warp::Reply, warp::Rejection> {
|
||||
let mut secret: [u8; 32] = [0; 32];
|
||||
super::SEC_RANDOM.fill(&mut secret).expect("Failed to generate secret");
|
||||
let secret = Vec::from(secret);
|
||||
info.0.tfa_secret = Some(secret);
|
||||
db.save_user(&info.0);
|
||||
|
||||
if data.mail {
|
||||
send_2fa_mail(&info.0);
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
} else {
|
||||
let totp = get_totp(&info.0);
|
||||
get_reply(&dto::responses::TfaSetup {
|
||||
statusCode: 200,
|
||||
secret: totp.get_secret_base32(),
|
||||
qrCode: "data:image/png;base64,".to_owned() + &totp.get_qr().expect("Failed to generate qr code")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn tfa_complete(data: dto::requests::TfaComplete, mut info: UserInfo, mut db: DBConnection)
|
||||
-> Result<impl warp::Reply, warp::Rejection> {
|
||||
info.0.tfa_type = if data.mail { TfaTypes::Email } else { TfaTypes::Totp };
|
||||
|
||||
if verify2fa(&info.0, data.code) {
|
||||
db.save_user(&info.0);
|
||||
db.delete_all_tokens(info.0.id);
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
} else {
|
||||
AppError::BadRequest("Incorrect 2fa code").err()
|
||||
}
|
||||
}
|
||||
|
||||
async fn tfa_disable(mut info: UserInfo, mut db: DBConnection)
|
||||
-> Result<impl warp::Reply, warp::Rejection> {
|
||||
info.0.tfa_secret = None;
|
||||
info.0.tfa_type = TfaTypes::None;
|
||||
db.save_user(&info.0);
|
||||
db.delete_all_tokens(info.0.id);
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
90
backend/src/routes/filters.rs
Normal file
90
backend/src/routes/filters.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use warp::Filter;
|
||||
use warp::http::{HeaderMap, HeaderValue};
|
||||
use crate::db::UserRole;
|
||||
use crate::db::{DBConnection, DBPool, with_db};
|
||||
use crate::routes::AppError;
|
||||
use crate::routes::auth;
|
||||
|
||||
pub type UserInfo = (crate::db::User, crate::db::Token);
|
||||
|
||||
pub fn authenticated(db: DBPool) -> impl Filter<Extract=(UserInfo,), Error=warp::reject::Rejection> + Clone {
|
||||
warp::header::headers_cloned()
|
||||
.map(move |_headers: HeaderMap<HeaderValue>| _headers)
|
||||
.and(with_db(db))
|
||||
.and_then(authorize)
|
||||
}
|
||||
|
||||
pub fn admin(db: DBPool) -> impl Filter<Extract=(UserInfo, ), Error=warp::reject::Rejection> + Clone {
|
||||
warp::header::headers_cloned()
|
||||
.map(move |_headers: HeaderMap<HeaderValue>| _headers)
|
||||
.and(with_db(db))
|
||||
.and_then(|_headers, db| async {
|
||||
let info = authorize(_headers, db).await?;
|
||||
if info.0.role == UserRole::Admin {
|
||||
Ok(info)
|
||||
} else {
|
||||
AppError::Forbidden("Forbidden").err()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn authorize(_headers: HeaderMap<HeaderValue>, mut db: DBConnection) -> Result<UserInfo, warp::reject::Rejection> {
|
||||
authorize_jwt(extract_jwt(&_headers).map_err(|e| e.reject())?, &mut db).await
|
||||
}
|
||||
|
||||
pub async fn authorize_jwt(jwt: String, db: &mut DBConnection) -> Result<UserInfo, warp::reject::Rejection> {
|
||||
let decoded = jsonwebtoken::decode::<auth::JWTClaims>(
|
||||
&jwt,
|
||||
&crate::routes::auth::JWT_DECODE_KEY,
|
||||
&jsonwebtoken::Validation::new(auth::JWT_ALGORITHM)
|
||||
).map_err(|_| AppError::Forbidden("Invalid token"))?;
|
||||
|
||||
db.cleanup_tokens();
|
||||
|
||||
let mut user = db.get_user(decoded.claims.sub)
|
||||
.ok_or(AppError::Forbidden("Invalid token"))?;
|
||||
let token = db.get_token(decoded.claims.jti)
|
||||
.ok_or(AppError::Forbidden("Invalid token"))?;
|
||||
|
||||
if user.id != token.owner_id {
|
||||
return AppError::Forbidden("Invalid token").err();
|
||||
}
|
||||
if user.role == UserRole::Disabled {
|
||||
return AppError::Forbidden("Account disabled").err();
|
||||
}
|
||||
if user.gitlab {
|
||||
let info = auth::gitlab::get_gitlab_user(user.gitlab_at.clone().unwrap());
|
||||
let info = match info {
|
||||
Some(v) => Some(v),
|
||||
None => {
|
||||
let tokens = auth::gitlab::get_gitlab_token(user.gitlab_rt.clone().unwrap(), true);
|
||||
if let Some(tokens) = tokens {
|
||||
user.gitlab_at = Some(tokens.access_token.clone());
|
||||
user.gitlab_rt = Some(tokens.refresh_token);
|
||||
db.save_user(&user);
|
||||
auth::gitlab::get_gitlab_user(tokens.access_token)
|
||||
} else { None }
|
||||
}
|
||||
};
|
||||
if info.is_none() || info.unwrap().username != user.name {
|
||||
db.delete_all_tokens(token.owner_id);
|
||||
db.delete_all_tokens(user.id);
|
||||
return AppError::Forbidden("Invalid gitlab user").err();
|
||||
}
|
||||
}
|
||||
|
||||
Ok((user, token))
|
||||
}
|
||||
|
||||
fn extract_jwt(_headers: &HeaderMap<HeaderValue>) -> Result<String, AppError> {
|
||||
let header = match _headers.get(warp::http::header::AUTHORIZATION) {
|
||||
Some(v) => v,
|
||||
None => return Err(AppError::Unauthorized("Missing token"))
|
||||
};
|
||||
let header = header.to_str().map_err(|_| AppError::Unauthorized("Missing token"))?;
|
||||
if !header.starts_with("Bearer ") {
|
||||
Err(AppError::Unauthorized("Missing token"))
|
||||
} else {
|
||||
Ok(header.trim_start_matches("Bearer ").to_owned())
|
||||
}
|
||||
}
|
||||
199
backend/src/routes/fs/mod.rs
Normal file
199
backend/src/routes/fs/mod.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::iter::Iterator;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
|
||||
use lazy_static::lazy_static;
|
||||
use warp::Filter;
|
||||
use futures::TryFutureExt;
|
||||
use futures::TryStreamExt;
|
||||
use crate::db::DBPool;
|
||||
|
||||
mod routes;
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
{
|
||||
if !std::path::Path::new("temp").is_dir() {
|
||||
std::fs::create_dir("temp").expect("Failed to create temp dir");
|
||||
}
|
||||
std::fs::read_dir("temp")
|
||||
.expect("Failed to iter temp dir")
|
||||
.for_each(|dir| {
|
||||
std::fs::remove_file(dir.expect("Failed to retrieve temp dir entry").path()).expect("Failed to delete file in temp dir");
|
||||
});
|
||||
DELETE_RT.spawn(async {});
|
||||
ZIP_RT.spawn(async {});
|
||||
}
|
||||
routes::build_routes(db)
|
||||
}
|
||||
|
||||
pub static WINDOWS_INVALID_CHARS: &str = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F<>:\"/\\|";
|
||||
|
||||
pub struct ZipProgressEntry {
|
||||
temp_id: u64,
|
||||
done: AtomicBool,
|
||||
progress: AtomicU64,
|
||||
total: AtomicU64,
|
||||
delete_after: AtomicI64
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CreateNodeResult {
|
||||
InvalidName,
|
||||
InvalidParent,
|
||||
Exists(bool, i32)
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref DELETE_RT: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(1).enable_time().build().expect("Failed to create delete runtime");
|
||||
static ref ZIP_RT: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(3).enable_time().build().expect("Failed to create zip runtime");
|
||||
pub static ref ZIP_TO_PROGRESS: tokio::sync::RwLock<std::collections::HashMap<std::collections::BTreeSet<i32>, Arc<ZipProgressEntry>>> = tokio::sync::RwLock::new(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
async fn cleanup_temp_zips() {
|
||||
let mut existing = ZIP_TO_PROGRESS.write().await;
|
||||
existing.retain(|_, v| {
|
||||
if Arc::strong_count(v) == 1 && v.done.load(Ordering::Relaxed) && v.delete_after.load(Ordering::Relaxed) <= chrono::Utc::now().timestamp() {
|
||||
std::fs::remove_file(std::path::Path::new(&format!("./temp/{}", v.temp_id))).expect("Failed to delete temp file");
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn get_nodes_recursive(root: crate::db::Inode, db: &mut crate::db::DBConnection) -> VecDeque<crate::db::Inode> {
|
||||
let mut nodes = VecDeque::from(vec![root.clone()]);
|
||||
if root.is_file { return nodes; }
|
||||
let mut nodes_to_check = VecDeque::from(vec![root]);
|
||||
while !nodes_to_check.is_empty() {
|
||||
let node = nodes_to_check.pop_front().unwrap();
|
||||
db.get_children(node.id).iter().for_each(|node| {
|
||||
nodes.push_back(node.clone());
|
||||
if !node.is_file { nodes_to_check.push_front(node.clone()); }
|
||||
});
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
fn get_node_path(node: crate::db::Inode, db: &mut crate::db::DBConnection) -> VecDeque<crate::db::Inode> {
|
||||
let mut path = VecDeque::from(vec![node.clone()]);
|
||||
let mut node = node;
|
||||
while let Some(parent) = node.parent_id {
|
||||
node = db.get_node(parent).expect("Failed to get node parent");
|
||||
path.push_front(node.clone());
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
fn get_total_size(node: crate::db::Inode, db: &mut crate::db::DBConnection) -> u64 {
|
||||
let nodes = get_nodes_recursive(node, db);
|
||||
nodes.iter().fold(0_u64, |acc, node| acc + node.size.unwrap_or(0) as u64)
|
||||
}
|
||||
|
||||
pub fn get_node_and_validate(user: &crate::db::User, node: i32, db: &mut crate::db::DBConnection) -> Option<crate::db::Inode> {
|
||||
let node = db.get_node(node)?;
|
||||
if node.owner_id != user.id {
|
||||
None
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_node(name: String, owner: &crate::db::User, file: bool, parent: Option<i32>, force: bool, db: &mut crate::db::DBConnection)
|
||||
-> Result<crate::db::Inode, CreateNodeResult> {
|
||||
if !force && (name.is_empty() || name.starts_with(' ') || name.contains(|c| {
|
||||
WINDOWS_INVALID_CHARS.contains(c)
|
||||
} || name.ends_with(' ') || name.ends_with('.') || name == "." || name == "..")) {
|
||||
return Err(CreateNodeResult::InvalidName);
|
||||
}
|
||||
|
||||
if let Some(parent) = parent {
|
||||
let parent = match get_node_and_validate(owner, parent, db) {
|
||||
None => { return Err(CreateNodeResult::InvalidParent); }
|
||||
Some(v) => v
|
||||
};
|
||||
if parent.is_file { return Err(CreateNodeResult::InvalidParent); }
|
||||
let children = db.get_children(parent.id);
|
||||
for child in children {
|
||||
if child.name == name {
|
||||
return Err(CreateNodeResult::Exists(child.is_file, child.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(db.create_node(file, name, parent, owner.id))
|
||||
}
|
||||
|
||||
pub fn delete_node_root(node: &crate::db::Inode, db: &mut crate::db::DBConnection) {
|
||||
get_nodes_recursive(node.clone(), db).iter().rev().for_each(|node| {
|
||||
db.delete_node(node);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn delete_node(node: &crate::db::Inode, sender: &mut warp::hyper::body::Sender, db: &mut crate::db::DBConnection) {
|
||||
if node.parent_id.is_none() { return; }
|
||||
|
||||
for node in get_nodes_recursive(node.clone(), db).iter().rev() {
|
||||
sender.send_data(warp::hyper::body::Bytes::from(format!("Deleting {}...", generate_path(node, db)))).await.unwrap();
|
||||
db.delete_node(node);
|
||||
sender.send_data(warp::hyper::body::Bytes::from(" Done \n")).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_path(node: &crate::db::Inode, db: &mut crate::db::DBConnection) -> String {
|
||||
let mut path = String::new();
|
||||
|
||||
get_node_path(node.clone(), db).iter().for_each(|node| {
|
||||
if node.parent_id.is_none() {
|
||||
path += "/";
|
||||
} else {
|
||||
path += &node.name;
|
||||
if !node.is_file {
|
||||
path += "/";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub fn generate_path_dto(node: &crate::db::Inode, db: &mut crate::db::DBConnection) -> crate::dto::responses::GetPath {
|
||||
let mut get_path = crate::dto::responses::GetPath {
|
||||
segments: Vec::new()
|
||||
};
|
||||
|
||||
get_node_path(node.clone(), db).iter().for_each(|node| {
|
||||
if node.parent_id.is_none() {
|
||||
get_path.segments.push(crate::dto::responses::GetPathSegment {
|
||||
path: "/".to_owned(),
|
||||
node: Some(node.id)
|
||||
});
|
||||
} else {
|
||||
get_path.segments.push(crate::dto::responses::GetPathSegment {
|
||||
path: node.name.clone(),
|
||||
node: Some(node.id)
|
||||
});
|
||||
if !node.is_file {
|
||||
get_path.segments.push(crate::dto::responses::GetPathSegment {
|
||||
path: "/".to_owned(),
|
||||
node: None
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
get_path
|
||||
}
|
||||
|
||||
pub fn get_file_stream_body(path: String) -> warp::hyper::Body {
|
||||
warp::hyper::Body::wrap_stream(
|
||||
tokio::fs::File::open(path)
|
||||
.map_ok(|file|
|
||||
tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
|
||||
.map_ok(bytes::BytesMut::freeze)
|
||||
)
|
||||
.try_flatten_stream()
|
||||
)
|
||||
}
|
||||
444
backend/src/routes/fs/routes.rs
Normal file
444
backend/src/routes/fs/routes.rs
Normal file
@@ -0,0 +1,444 @@
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::Ordering;
|
||||
use futures::{Stream, StreamExt};
|
||||
use headers::HeaderMapExt;
|
||||
use warp::{Filter, Reply};
|
||||
use crate::db::{DBConnection, DBPool, with_db};
|
||||
use crate::dto;
|
||||
use crate::routes::{AppError, get_reply};
|
||||
use crate::routes::filters::{authenticated, UserInfo};
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
|
||||
let root = warp::path!("fs" / "root")
|
||||
.and(warp::get())
|
||||
.and(authenticated(db.clone()))
|
||||
.and_then(root);
|
||||
let node = warp::path!("fs" / "node" / i32)
|
||||
.and(warp::get())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(node)
|
||||
.with(warp::compression::brotli());
|
||||
let path = warp::path!("fs" / "path" / i32)
|
||||
.and(warp::get())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(path);
|
||||
let create_folder = warp::path!("fs" / "create_folder")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(|data, info, db| create_node(data, info, db, false));
|
||||
let create_file = warp::path!("fs" / "create_file")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(|data, info, db| create_node(data, info, db, true));
|
||||
let delete_node = warp::path!("fs" / "delete" / i32)
|
||||
.and(warp::post())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(delete_node);
|
||||
let upload = warp::path!("fs" / "upload" / i32)
|
||||
.and(warp::post())
|
||||
.and(warp::body::stream())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(upload);
|
||||
let create_zip = warp::path!("fs" / "create_zip")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(create_zip);
|
||||
let download = warp::path!("fs" / "download")
|
||||
.and(warp::post())
|
||||
.and(warp::body::form())
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(download);
|
||||
let download_multi = warp::path!("fs" / "download_multi")
|
||||
.and(warp::post())
|
||||
.and(warp::body::form())
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(download_multi);
|
||||
let download_preview = warp::path!("fs" / "download_preview" / i32)
|
||||
.and(warp::get())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(download_preview);
|
||||
let get_type = warp::path!("fs" / "get_type" / i32)
|
||||
.and(warp::get())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db))
|
||||
.and_then(get_type);
|
||||
|
||||
root.or(node).or(path).or(create_folder).or(create_file).or(delete_node).or(upload).or(create_zip).or(download).or(download_multi).or(download_preview).or(get_type)
|
||||
}
|
||||
|
||||
async fn root(info: UserInfo) -> Result<impl Reply, warp::Rejection> {
|
||||
get_reply(&dto::responses::Root {
|
||||
statusCode: 200,
|
||||
rootId: info.0.root_id
|
||||
})
|
||||
}
|
||||
|
||||
async fn node(node: i32, info: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let _guard = guard_lock.read().await;
|
||||
let node = super::get_node_and_validate(&info.0, node, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?;
|
||||
|
||||
get_reply(&dto::responses::GetNode {
|
||||
statusCode: 200,
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
isFile: node.is_file,
|
||||
preview: node.has_preview,
|
||||
parent: node.parent_id,
|
||||
size: node.size,
|
||||
children: (!node.is_file).then(|| {
|
||||
db.get_children(node.id).iter().map(|child| dto::responses::GetNodeEntry {
|
||||
id: child.id,
|
||||
name: child.name.clone(),
|
||||
isFile: child.is_file,
|
||||
preview: child.has_preview,
|
||||
parent: child.parent_id,
|
||||
size: child.size
|
||||
}).collect()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn path(node: i32, info: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let _guard = guard_lock.read().await;
|
||||
let node = super::get_node_and_validate(&info.0, node, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?;
|
||||
|
||||
get_reply(&super::generate_path_dto(&node, &mut db))
|
||||
}
|
||||
|
||||
async fn create_node(data: dto::requests::CreateNode, info: UserInfo, mut db: DBConnection, file: bool) -> Result<impl Reply, warp::Rejection> {
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let _guard = guard_lock.read().await;
|
||||
let node = super::create_node(data.name, &info.0, file, Some(data.parent), false, &mut db);
|
||||
|
||||
match node {
|
||||
Ok(v) => get_reply(&dto::responses::NewNode {
|
||||
statusCode: 200,
|
||||
id: v.id
|
||||
}),
|
||||
Err(v) => {
|
||||
match v {
|
||||
super::CreateNodeResult::InvalidName => AppError::BadRequest("Invalid name").err(),
|
||||
super::CreateNodeResult::InvalidParent => AppError::BadRequest("Invalid parent").err(),
|
||||
super::CreateNodeResult::Exists(file, id) => get_reply(&dto::responses::NodeExists {
|
||||
statusCode: 200,
|
||||
id,
|
||||
exists: true,
|
||||
isFile: file
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_node(node: i32, info: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let inner_guard_lock = guard_lock.clone();
|
||||
let _guard = guard_lock.read().await;
|
||||
let node = super::get_node_and_validate(&info.0, node, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?;
|
||||
|
||||
if node.parent_id.is_none() {
|
||||
return AppError::BadRequest("Can't delete root").err();
|
||||
}
|
||||
|
||||
let (mut sender, body) = warp::hyper::Body::channel();
|
||||
|
||||
sender.send_data(warp::hyper::body::Bytes::from("Waiting in queue\n")).await.unwrap();
|
||||
super::DELETE_RT.spawn(async move {
|
||||
let guard_lock = inner_guard_lock.clone();
|
||||
let _guard = guard_lock.write().await;
|
||||
super::delete_node(&node, &mut sender, &mut db).await;
|
||||
});
|
||||
|
||||
let mut resp = warp::reply::Response::new(body);
|
||||
*resp.status_mut() = warp::http::StatusCode::OK;
|
||||
resp.headers_mut().typed_insert(
|
||||
headers::ContentType::text_utf8()
|
||||
);
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
async fn upload<S, B>(node: i32, stream: S, info: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection>
|
||||
where
|
||||
S: Stream<Item = Result<B, warp::Error>>,
|
||||
S: StreamExt,
|
||||
B: warp::Buf
|
||||
{
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let _guard = guard_lock.read().await;
|
||||
let mut node = super::get_node_and_validate(&info.0, node, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?;
|
||||
|
||||
if !node.is_file {
|
||||
return AppError::BadRequest("Can't upload to a directory").err();
|
||||
}
|
||||
|
||||
let mut file_size = 0_i64;
|
||||
let file_name = format!("./files/{}", node.id);
|
||||
{
|
||||
let mut file = std::fs::File::create(file_name.clone()).unwrap();
|
||||
|
||||
stream.for_each(|f| {
|
||||
let mut buffer = f.unwrap();
|
||||
file_size += buffer.remaining() as i64;
|
||||
while buffer.remaining() != 0 {
|
||||
let chunk = buffer.chunk();
|
||||
buffer.advance(file.write(chunk).expect("Failed to write file"));
|
||||
}
|
||||
futures::future::ready(())
|
||||
}).await;
|
||||
}
|
||||
|
||||
let generate_preview = || -> Option<()> {
|
||||
if file_size > 20 * 1024 * 1024 { return None; }
|
||||
let mime = mime_guess::from_path(std::path::Path::new(&node.name)).first()?.to_string();
|
||||
let img = image::load(
|
||||
std::io::BufReader::new(std::fs::File::open(file_name.clone()).unwrap()),
|
||||
image::ImageFormat::from_mime_type(mime)?
|
||||
).ok()?;
|
||||
let img = img.resize(300, 300, image::imageops::FilterType::Triangle);
|
||||
img.save(std::path::Path::new(&(file_name + "_preview.jpg"))).expect("Failed to save preview image");
|
||||
Some(())
|
||||
};
|
||||
|
||||
node.has_preview = generate_preview().is_some();
|
||||
node.size = Some(file_size);
|
||||
db.save_node(&node);
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_zip(data: dto::requests::CreateZip, info: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let inner_guard_lock = guard_lock.clone();
|
||||
let _guard = guard_lock.read().await;
|
||||
let mut nodes: Vec<crate::db::Inode> = Vec::new();
|
||||
for node in data.nodes.clone() {
|
||||
nodes.push(
|
||||
super::get_node_and_validate(&info.0, node, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?
|
||||
);
|
||||
}
|
||||
let zip_nodes = BTreeSet::from_iter(data.nodes.iter().copied());
|
||||
|
||||
{
|
||||
let guard = super::ZIP_TO_PROGRESS.read().await;
|
||||
if let Some(entry) = guard.get(&zip_nodes) {
|
||||
return get_reply(&dto::responses::CreateZipDone {
|
||||
statusCode: 200,
|
||||
done: entry.done.load(Ordering::Relaxed),
|
||||
progress: Some(entry.progress.load(Ordering::Relaxed)),
|
||||
total: Some(entry.total.load(Ordering::Relaxed))
|
||||
})
|
||||
}
|
||||
}
|
||||
let entry = {
|
||||
let mut guard = super::ZIP_TO_PROGRESS.write().await;
|
||||
guard.insert(zip_nodes.clone(), std::sync::Arc::from(super::ZipProgressEntry {
|
||||
temp_id: super::NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed),
|
||||
done: std::sync::atomic::AtomicBool::new(false),
|
||||
progress: std::sync::atomic::AtomicU64::new(0),
|
||||
total: std::sync::atomic::AtomicU64::new(1),
|
||||
delete_after: std::sync::atomic::AtomicI64::new(0)
|
||||
}));
|
||||
guard.get(&zip_nodes).unwrap().clone()
|
||||
};
|
||||
super::ZIP_RT.spawn(async move {
|
||||
type NodeMap = HashMap<i32, crate::db::Inode>;
|
||||
|
||||
super::cleanup_temp_zips().await;
|
||||
|
||||
let _guard = inner_guard_lock.read().await;
|
||||
|
||||
fn get_path(node: &crate::db::Inode, dirs: &NodeMap) -> String {
|
||||
let mut path = node.name.clone();
|
||||
let mut _node = dirs.get(&node.parent_id.unwrap_or(-1));
|
||||
while let Some(node) = _node {
|
||||
path.insert_str(0, &(node.name.clone() + "/"));
|
||||
_node = dirs.get(&node.parent_id.unwrap_or(-1));
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
nodes.iter().for_each(|node| {
|
||||
entry.total.fetch_add(super::get_total_size(node.clone(), &mut db), Ordering::Relaxed);
|
||||
});
|
||||
entry.total.fetch_sub(1, Ordering::Relaxed);
|
||||
{
|
||||
let mut buf = vec![0_u8; 1024 * 1024 * 4];
|
||||
let file = std::fs::File::create(format!("./temp/{}", entry.temp_id)).expect("Failed to create temp file");
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
let zip_options = zip::write::FileOptions::default().large_file(true);
|
||||
let (files, dirs): (NodeMap, NodeMap) =
|
||||
nodes.iter()
|
||||
.flat_map(|node| super::get_nodes_recursive(node.clone(), &mut db))
|
||||
.map(|node| (node.id, node))
|
||||
.partition(|v| v.1.is_file);
|
||||
|
||||
dirs.values().for_each(|dir| {
|
||||
zip.add_directory(get_path(dir, &dirs), zip_options).expect("Failed to add dir to zip");
|
||||
});
|
||||
files.values().for_each(|node| {
|
||||
zip.start_file(get_path(node, &dirs), zip_options).expect("Failed to start zip file");
|
||||
let mut file = std::fs::File::open(format!("./files/{}", node.id)).expect("Failed to open file for zip");
|
||||
loop {
|
||||
let count = file.read(&mut buf).expect("Failed to read file for zip");
|
||||
if count == 0 { break; }
|
||||
zip.write_all(&buf[..count]).expect("Failed to write zip");
|
||||
entry.progress.fetch_add(count as u64, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
zip.finish().expect("Failed to finish zip");
|
||||
}
|
||||
entry.done.store(true, Ordering::Relaxed);
|
||||
entry.delete_after.store(chrono::Utc::now().timestamp() + 10 * 60, Ordering::Relaxed);
|
||||
});
|
||||
get_reply(&dto::responses::CreateZipDone {
|
||||
statusCode: 200,
|
||||
done: false,
|
||||
progress: Some(0),
|
||||
total: Some(1)
|
||||
})
|
||||
}
|
||||
|
||||
async fn download(data: dto::requests::Download, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let info = crate::routes::filters::authorize_jwt(data.jwtToken, &mut db).await?;
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let _guard = guard_lock.read().await;
|
||||
|
||||
let node: crate::db::Inode = super::get_node_and_validate(&info.0, data.id, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?;
|
||||
|
||||
if node.is_file {
|
||||
let mut resp = warp::reply::Response::new(super::get_file_stream_body(
|
||||
format!("./files/{}", node.id)
|
||||
));
|
||||
*resp.status_mut() = warp::http::StatusCode::OK;
|
||||
resp.headers_mut().typed_insert(
|
||||
headers::ContentLength(node.size.unwrap() as u64)
|
||||
);
|
||||
resp.headers_mut().typed_insert(
|
||||
headers::ContentType::from(
|
||||
mime_guess::from_path(std::path::Path::new(&node.name)).first_or_octet_stream()
|
||||
)
|
||||
);
|
||||
resp.headers_mut().insert(
|
||||
"Content-Disposition",
|
||||
("attachment; filename=".to_owned() + &node.name).parse().unwrap()
|
||||
);
|
||||
Ok(resp)
|
||||
} else {
|
||||
let nodes_key = BTreeSet::from([node.id]);
|
||||
let guard = super::ZIP_TO_PROGRESS.read().await;
|
||||
let entry = guard.get(&nodes_key)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?;
|
||||
if !entry.done.load(Ordering::Relaxed) {
|
||||
AppError::BadRequest("Unknown node").err()
|
||||
} else {
|
||||
let file = format!("./temp/{}", entry.temp_id);
|
||||
let mut resp = warp::reply::Response::new(super::get_file_stream_body(file.clone()));
|
||||
*resp.status_mut() = warp::http::StatusCode::OK;
|
||||
resp.headers_mut().typed_insert(
|
||||
headers::ContentLength(std::fs::metadata(std::path::Path::new(&file)).unwrap().len())
|
||||
);
|
||||
resp.headers_mut().typed_insert(
|
||||
headers::ContentType::from(
|
||||
mime_guess::from_ext("zip").first().unwrap()
|
||||
)
|
||||
);
|
||||
resp.headers_mut().insert(
|
||||
"Content-Disposition",
|
||||
("attachment; filename=".to_owned() + &node.name + ".zip").parse().unwrap()
|
||||
);
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_multi(data: dto::requests::DownloadMulti, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let info = crate::routes::filters::authorize_jwt(data.jwtToken, &mut db).await?;
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let _guard = guard_lock.read().await;
|
||||
|
||||
let mut nodes: Vec<crate::db::Inode> = Vec::new();
|
||||
for node in data.id.split(',').map(|v| v.parse::<i32>()
|
||||
.map_err(|_| AppError::BadRequest("Failed to parse").reject())
|
||||
) {
|
||||
nodes.push(
|
||||
super::get_node_and_validate(&info.0, node?, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?
|
||||
);
|
||||
}
|
||||
|
||||
let nodes_key = BTreeSet::from_iter(nodes.iter().map(|node| node.id));
|
||||
let guard = super::ZIP_TO_PROGRESS.read().await;
|
||||
let entry = guard.get(&nodes_key)
|
||||
.ok_or(AppError::BadRequest("Unknown zip"))?;
|
||||
if !entry.done.load(Ordering::Relaxed) {
|
||||
AppError::BadRequest("Unfinished zip").err()
|
||||
} else {
|
||||
let file = format!("./temp/{}", entry.temp_id);
|
||||
let mut resp = warp::reply::Response::new(super::get_file_stream_body(file.clone()));
|
||||
*resp.status_mut() = warp::http::StatusCode::OK;
|
||||
resp.headers_mut().typed_insert(
|
||||
headers::ContentLength(std::fs::metadata(std::path::Path::new(&file)).unwrap().len())
|
||||
);
|
||||
resp.headers_mut().typed_insert(
|
||||
headers::ContentType::from(
|
||||
mime_guess::from_ext("zip").first().unwrap()
|
||||
)
|
||||
);
|
||||
resp.headers_mut().insert(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=files.zip".parse().unwrap()
|
||||
);
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_preview(node: i32, info: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let guard_lock = DBConnection::get_lock(info.0.id).await;
|
||||
let _guard = guard_lock.read().await;
|
||||
let node: crate::db::Inode = super::get_node_and_validate(&info.0, node, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?;
|
||||
|
||||
if node.has_preview {
|
||||
let file = format!("./files/{}_preview.jpg", node.id);
|
||||
get_reply(&dto::responses::DownloadBase64 {
|
||||
statusCode: 200,
|
||||
data: "data:image/png;base64,".to_owned() + &base64::encode(std::fs::read(std::path::Path::new(&file)).unwrap())
|
||||
})
|
||||
} else {
|
||||
AppError::BadRequest("No preview").err()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_type(node: i32, info: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
let node: crate::db::Inode = super::get_node_and_validate(&info.0, node, &mut db)
|
||||
.ok_or(AppError::BadRequest("Unknown node"))?;
|
||||
|
||||
get_reply(&dto::responses::Type {
|
||||
statusCode: 200,
|
||||
_type: mime_guess::from_path(std::path::Path::new(&node.name)).first_or_octet_stream().to_string()
|
||||
})
|
||||
}
|
||||
114
backend/src/routes/mod.rs
Normal file
114
backend/src/routes/mod.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
mod filters;
|
||||
mod auth;
|
||||
mod admin;
|
||||
mod user;
|
||||
pub mod fs;
|
||||
|
||||
use warp::{Filter, Reply};
|
||||
use crate::db::DBPool;
|
||||
use crate::dto;
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path::path("api")
|
||||
.and(
|
||||
auth::build_routes(db.clone())
|
||||
.or(admin::build_routes(db.clone()))
|
||||
.or(user::build_routes(db.clone()))
|
||||
.or(fs::build_routes(db))
|
||||
.recover(error_handler)
|
||||
)
|
||||
.or(warp::fs::dir("./static/"))
|
||||
.or(warp::fs::file("./static/index.html"))
|
||||
}
|
||||
|
||||
pub fn get_reply<T>(data: &T) -> Result<warp::reply::Response, warp::Rejection> where T: serde::Serialize {
|
||||
Ok(warp::reply::with_status(warp::reply::json(data), warp::http::StatusCode::OK).into_response())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone)]
|
||||
pub enum AppError {
|
||||
#[error("unauthorized")]
|
||||
Unauthorized(&'static str),
|
||||
#[error("forbidden")]
|
||||
Forbidden(&'static str),
|
||||
#[error("bad request")]
|
||||
BadRequest(&'static str),
|
||||
#[error("internal error")]
|
||||
InternalError(&'static str)
|
||||
}
|
||||
impl warp::reject::Reject for AppError {}
|
||||
|
||||
impl AppError {
|
||||
pub fn reject(&self) -> warp::reject::Rejection {
|
||||
warp::reject::custom(self.clone())
|
||||
}
|
||||
|
||||
pub fn err<T>(&self) -> Result<T, warp::reject::Rejection> {
|
||||
Err(self.reject())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn error_handler(err: warp::reject::Rejection) -> Result<impl Reply, std::convert::Infallible> {
|
||||
if err.is_not_found() {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&dto::responses::Error {
|
||||
statusCode: 404,
|
||||
message: "bruh".to_owned()
|
||||
}),
|
||||
warp::http::StatusCode::NOT_FOUND
|
||||
));
|
||||
}
|
||||
if let Some(e) = err.find::<AppError>() {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&dto::responses::Error {
|
||||
statusCode: match e {
|
||||
AppError::BadRequest(_) => 400,
|
||||
AppError::Unauthorized(_) => 401,
|
||||
AppError::Forbidden(_) => 403,
|
||||
AppError::InternalError(_) => 500
|
||||
},
|
||||
message: match e {
|
||||
AppError::BadRequest(v) => v.to_string(),
|
||||
AppError::Unauthorized(v) => v.to_string(),
|
||||
AppError::Forbidden(v) => v.to_string(),
|
||||
AppError::InternalError(v) => v.to_string()
|
||||
},
|
||||
}),
|
||||
match e {
|
||||
AppError::BadRequest(_) => warp::http::StatusCode::BAD_REQUEST,
|
||||
AppError::Unauthorized(_) => warp::http::StatusCode::UNAUTHORIZED,
|
||||
AppError::Forbidden(_) => warp::http::StatusCode::FORBIDDEN,
|
||||
AppError::InternalError(_) => warp::http::StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
));
|
||||
}
|
||||
if let Some(e) = err.find::<warp::body::BodyDeserializeError>() {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&dto::responses::Error {
|
||||
statusCode: 400,
|
||||
message: e.to_string(),
|
||||
}),
|
||||
warp::http::StatusCode::BAD_REQUEST
|
||||
))
|
||||
}
|
||||
if let Some(e) = err.find::<warp::reject::InvalidQuery>() {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&dto::responses::Error {
|
||||
statusCode: 400,
|
||||
message: e.to_string(),
|
||||
}),
|
||||
warp::http::StatusCode::BAD_REQUEST
|
||||
))
|
||||
}
|
||||
if let Some(e) = err.find::<warp::reject::MethodNotAllowed>() {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&dto::responses::Error {
|
||||
statusCode: 405,
|
||||
message: e.to_string(),
|
||||
}),
|
||||
warp::http::StatusCode::METHOD_NOT_ALLOWED
|
||||
))
|
||||
}
|
||||
|
||||
Err(err).expect("Can't handle error")
|
||||
}
|
||||
42
backend/src/routes/user.rs
Normal file
42
backend/src/routes/user.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use warp::{Filter, Reply};
|
||||
use crate::db::{DBConnection, DBPool, with_db};
|
||||
use crate::dto;
|
||||
use crate::routes::get_reply;
|
||||
use crate::routes::filters::{authenticated, UserInfo};
|
||||
|
||||
pub fn build_routes(db: DBPool) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
|
||||
let info = warp::path!("user" / "info")
|
||||
.and(warp::get())
|
||||
.and(authenticated(db.clone()))
|
||||
.and_then(info);
|
||||
let delete_user = warp::path!("user" / "delete")
|
||||
.and(warp::post())
|
||||
.and(authenticated(db.clone()))
|
||||
.and(with_db(db))
|
||||
.and_then(delete_user);
|
||||
|
||||
info.or(delete_user)
|
||||
}
|
||||
|
||||
async fn info(info: UserInfo) -> Result<impl Reply, warp::Rejection> {
|
||||
get_reply(&dto::responses::UserInfo {
|
||||
statusCode: info.0.id,
|
||||
name: info.0.name,
|
||||
gitlab: info.0.gitlab,
|
||||
tfaEnabled: info.0.tfa_type != crate::db::TfaTypes::None
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_user(info: UserInfo, mut db: DBConnection) -> Result<impl Reply, warp::Rejection> {
|
||||
db.delete_all_tokens(info.0.id);
|
||||
|
||||
let root_node = super::fs::get_node_and_validate(&info.0, info.0.root_id, &mut db).expect("Failed to get root node for deleting");
|
||||
|
||||
super::fs::delete_node_root(&root_node, &mut db);
|
||||
|
||||
db.delete_user(&info.0);
|
||||
|
||||
get_reply(&dto::responses::Success {
|
||||
statusCode: 200
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user