2
									
								
								backend/.idea/runConfigurations/clippy.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								backend/.idea/runConfigurations/clippy.xml
									
									
									
										generated
									
									
									
								
							@@ -2,7 +2,7 @@
 | 
				
			|||||||
  <configuration default="false" name="clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
 | 
					  <configuration default="false" name="clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
 | 
				
			||||||
    <option name="command" value="clippy" />
 | 
					    <option name="command" value="clippy" />
 | 
				
			||||||
    <option name="workingDirectory" value="file://$PROJECT_DIR$" />
 | 
					    <option name="workingDirectory" value="file://$PROJECT_DIR$" />
 | 
				
			||||||
    <option name="channel" value="DEFAULT" />
 | 
					    <option name="channel" value="STABLE" />
 | 
				
			||||||
    <option name="requiredFeatures" value="true" />
 | 
					    <option name="requiredFeatures" value="true" />
 | 
				
			||||||
    <option name="allFeatures" value="false" />
 | 
					    <option name="allFeatures" value="false" />
 | 
				
			||||||
    <option name="emulateTerminal" value="false" />
 | 
					    <option name="emulateTerminal" value="false" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -152,6 +152,17 @@ pub mod requests {
 | 
				
			|||||||
        pub otp: Option<String>
 | 
					        pub otp: Option<String>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[derive(Debug, Deserialize, Serialize, Clone)]
 | 
				
			||||||
 | 
					    pub struct SendRecoveryKey {
 | 
				
			||||||
 | 
					        pub username: String
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[derive(Debug, Deserialize, Serialize, Clone)]
 | 
				
			||||||
 | 
					    pub struct ResetPassword {
 | 
				
			||||||
 | 
					        pub key: String,
 | 
				
			||||||
 | 
					        pub password: String
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[derive(Debug, Deserialize, Serialize, Clone)]
 | 
					    #[derive(Debug, Deserialize, Serialize, Clone)]
 | 
				
			||||||
    pub struct TfaSetup {
 | 
					    pub struct TfaSetup {
 | 
				
			||||||
        pub mail: bool
 | 
					        pub mail: bool
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,7 @@ use tiny_http::{Method, Request, Response, ResponseBox, Server, StatusCode};
 | 
				
			|||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    db::DBConnection,
 | 
					    db::DBConnection,
 | 
				
			||||||
    metrics::TRACER,
 | 
					    metrics::TRACER,
 | 
				
			||||||
    routes::{get_reply, header, AppError}
 | 
					    routes::{header, AppError}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static THREAD_COUNT: usize = 10;
 | 
					static THREAD_COUNT: usize = 10;
 | 
				
			||||||
@@ -117,6 +117,8 @@ fn handle_api_request(span: &mut Span, req: &mut Request, pool: db::DBPool) -> R
 | 
				
			|||||||
        ("/api/metrics",                Method::Get)  => metrics::get_metrics(),
 | 
					        ("/api/metrics",                Method::Get)  => metrics::get_metrics(),
 | 
				
			||||||
        ("/api/auth/login",             Method::Post) => parse_body(span, req).and_then(|v|routes::auth::basic::login(span, req, db, v)),
 | 
					        ("/api/auth/login",             Method::Post) => parse_body(span, req).and_then(|v|routes::auth::basic::login(span, req, db, v)),
 | 
				
			||||||
        ("/api/auth/signup",            Method::Post) => parse_body(span, req).and_then(|v| routes::auth::basic::signup(span, req, db, v)),
 | 
					        ("/api/auth/signup",            Method::Post) => parse_body(span, req).and_then(|v| routes::auth::basic::signup(span, req, db, v)),
 | 
				
			||||||
 | 
					        ("/api/auth/send_key",          Method::Post) => parse_body(span, req).and_then(|v| routes::auth::basic::send_key(span, req, db, v)),
 | 
				
			||||||
 | 
					        ("/api/auth/reset",             Method::Post) => parse_body(span, req).and_then(|v| routes::auth::basic::reset(span, req, db, v)),
 | 
				
			||||||
        ("/api/auth/gitlab",            Method::Get)  => routes::auth::gitlab::gitlab(span, req, db),
 | 
					        ("/api/auth/gitlab",            Method::Get)  => routes::auth::gitlab::gitlab(span, req, db),
 | 
				
			||||||
        ("/api/auth/gitlab_callback",   Method::Get)  => routes::auth::gitlab::gitlab_callback(span, req, db, &query),
 | 
					        ("/api/auth/gitlab_callback",   Method::Get)  => routes::auth::gitlab::gitlab_callback(span, req, db, &query),
 | 
				
			||||||
        ("/api/fs/download",            Method::Post) => routes::fs::routes::download(span, req, db),
 | 
					        ("/api/fs/download",            Method::Post) => routes::fs::routes::download(span, req, db),
 | 
				
			||||||
@@ -143,7 +145,7 @@ fn handle_api_request(span: &mut Span, req: &mut Request, pool: db::DBPool) -> R
 | 
				
			|||||||
                ("/api/admin/logout",           Method::Post, None)         => parse_body(span, req).and_then(|v| routes::admin::logout(span, req, db, info, v)),
 | 
					                ("/api/admin/logout",           Method::Post, None)         => parse_body(span, req).and_then(|v| routes::admin::logout(span, req, db, info, v)),
 | 
				
			||||||
                ("/api/admin/delete",           Method::Post, None)         => parse_body(span, req).and_then(|v| routes::admin::delete_user(span, req, db, info, v)),
 | 
					                ("/api/admin/delete",           Method::Post, None)         => parse_body(span, req).and_then(|v| routes::admin::delete_user(span, req, db, info, v)),
 | 
				
			||||||
                ("/api/admin/disable_2fa",      Method::Post, None)         => parse_body(span, req).and_then(|v| routes::admin::disable_2fa(span, req, db, info, v)),
 | 
					                ("/api/admin/disable_2fa",      Method::Post, None)         => parse_body(span, req).and_then(|v| routes::admin::disable_2fa(span, req, db, info, v)),
 | 
				
			||||||
                ("/api/admin/is_admin",         Method::Post, None)         => get_reply(&dto::responses::Success { statusCode: 200 }),
 | 
					                ("/api/admin/is_admin",         Method::Get, None)          => routes::admin::is_admin(span, req, db, info),
 | 
				
			||||||
                ("/api/admin/get_token",        Method::Get, Some(v))   => routes::admin::get_token(span, req, db, info, v),
 | 
					                ("/api/admin/get_token",        Method::Get, Some(v))   => routes::admin::get_token(span, req, db, info, v),
 | 
				
			||||||
                ("/api/auth/refresh",           Method::Post, None)         => routes::auth::basic::refresh(span, req, db, info),
 | 
					                ("/api/auth/refresh",           Method::Post, None)         => routes::auth::basic::refresh(span, req, db, info),
 | 
				
			||||||
                ("/api/auth/logout_all",        Method::Post, None)         => routes::auth::basic::logout_all(span, req, db, info),
 | 
					                ("/api/auth/logout_all",        Method::Post, None)         => routes::auth::basic::logout_all(span, req, db, info),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -110,6 +110,14 @@ pub fn disable_2fa(
 | 
				
			|||||||
    get_reply(&dto::responses::Success { statusCode: 200 })
 | 
					    get_reply(&dto::responses::Success { statusCode: 200 })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn is_admin(_: &Span, _: &mut Request, _: &mut DBConnection, info: UserInfo) -> Result<ResponseBox, AppError> {
 | 
				
			||||||
 | 
					    if info.0.role != UserRole::Admin {
 | 
				
			||||||
 | 
					        return AppError::Forbidden("Forbidden").err();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get_reply(&dto::responses::Success { statusCode: 200 })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn get_token(
 | 
					pub fn get_token(
 | 
				
			||||||
    span: &Span,
 | 
					    span: &Span,
 | 
				
			||||||
    _: &mut Request,
 | 
					    _: &mut Request,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,23 @@
 | 
				
			|||||||
 | 
					use std::{collections::HashMap, ops::Add};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use lettre::Transport;
 | 
				
			||||||
 | 
					use once_cell::sync::Lazy;
 | 
				
			||||||
 | 
					use parking_lot::Mutex;
 | 
				
			||||||
 | 
					use ring::rand::SecureRandom;
 | 
				
			||||||
use rustracing_jaeger::Span;
 | 
					use rustracing_jaeger::Span;
 | 
				
			||||||
use tiny_http::{Request, ResponseBox};
 | 
					use tiny_http::{Request, ResponseBox};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    db::{DBConnection, TfaTypes, UserRole},
 | 
					    db::{DBConnection, TfaTypes, UserRole},
 | 
				
			||||||
    dto,
 | 
					    dto,
 | 
				
			||||||
    routes::{filters::UserInfo, get_reply, AppError}
 | 
					    metrics,
 | 
				
			||||||
 | 
					    routes::{auth::tfa::MAIL_SENDER, filters::UserInfo, get_reply, AppError}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RecoveryKeyMap = HashMap<String, (i32, chrono::DateTime<chrono::Utc>)>;
 | 
				
			||||||
 | 
					static RECOVERY_KEYS: Lazy<Mutex<RecoveryKeyMap>> = Lazy::new(|| Mutex::new(RecoveryKeyMap::new()));
 | 
				
			||||||
 | 
					const KEY_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn login(
 | 
					pub fn login(
 | 
				
			||||||
    span: &Span,
 | 
					    span: &Span,
 | 
				
			||||||
    _: &mut Request,
 | 
					    _: &mut Request,
 | 
				
			||||||
@@ -60,6 +71,70 @@ pub fn signup(
 | 
				
			|||||||
    get_reply(&dto::responses::Success { statusCode: 200 })
 | 
					    get_reply(&dto::responses::Success { statusCode: 200 })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn send_key(
 | 
				
			||||||
 | 
					    span: &Span,
 | 
				
			||||||
 | 
					    _: &mut Request,
 | 
				
			||||||
 | 
					    db: &mut DBConnection,
 | 
				
			||||||
 | 
					    data: dto::requests::SendRecoveryKey
 | 
				
			||||||
 | 
					) -> Result<ResponseBox, AppError> {
 | 
				
			||||||
 | 
					    // Silently fail to prevent information leak about usernames
 | 
				
			||||||
 | 
					    if let Some(user) = db.find_user(span, &data.username, false) {
 | 
				
			||||||
 | 
					        let _span = metrics::span("send_recovery_key", span);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut bin = [0_u8; 10];
 | 
				
			||||||
 | 
					        crate::routes::auth::SEC_RANDOM.fill(&mut bin).expect("Failed to generate recovery key");
 | 
				
			||||||
 | 
					        let code_arr = bin.map(|v| KEY_CHARSET[v as usize % KEY_CHARSET.len()]);
 | 
				
			||||||
 | 
					        let code = String::from_utf8_lossy(&code_arr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RECOVERY_KEYS.lock().insert(
 | 
				
			||||||
 | 
					                code.to_string(),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    user.id,
 | 
				
			||||||
 | 
					                    chrono::Utc::now().add(chrono::Duration::minutes(5))
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mail = lettre::Message::builder()
 | 
				
			||||||
 | 
					            .from("fileserver@mattv.de".parse().unwrap())
 | 
				
			||||||
 | 
					            .to(user.name.parse().unwrap())
 | 
				
			||||||
 | 
					            .subject("MFileserver - Password reset")
 | 
				
			||||||
 | 
					            .body(format!(
 | 
				
			||||||
 | 
					                "Your recovery key is: {}\r\nIt is valid for 5 minutes",
 | 
				
			||||||
 | 
					                code
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        MAIL_SENDER.send(&mail).expect("Failed to send mail");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get_reply(&dto::responses::Success { statusCode: 200 })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn reset(
 | 
				
			||||||
 | 
					    span: &Span,
 | 
				
			||||||
 | 
					    _: &mut Request,
 | 
				
			||||||
 | 
					    db: &mut DBConnection,
 | 
				
			||||||
 | 
					    data: dto::requests::ResetPassword
 | 
				
			||||||
 | 
					) -> Result<ResponseBox, AppError> {
 | 
				
			||||||
 | 
					    if let Some(entry) = { RECOVERY_KEYS.lock().get(&data.key) } {
 | 
				
			||||||
 | 
					        if entry.1 < chrono::Utc::now() {
 | 
				
			||||||
 | 
					            return AppError::BadRequest("Invalid key").err();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(mut user) = db.get_user(span, entry.0) {
 | 
				
			||||||
 | 
					            user.password = super::hash_password(&data.password);
 | 
				
			||||||
 | 
					            db.save_user(span, &user);
 | 
				
			||||||
 | 
					            db.delete_all_tokens(span, user.id);
 | 
				
			||||||
 | 
					            get_reply(&dto::responses::Success { statusCode: 200 })
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            AppError::BadRequest("Invalid user").err()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        AppError::BadRequest("Invalid key").err()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn refresh(span: &Span, _: &mut Request, db: &mut DBConnection, info: UserInfo) -> Result<ResponseBox, AppError> {
 | 
					pub fn refresh(span: &Span, _: &mut Request, db: &mut DBConnection, info: UserInfo) -> Result<ResponseBox, AppError> {
 | 
				
			||||||
    db.delete_token(span, info.1.id);
 | 
					    db.delete_token(span, info.1.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@ fn build_mail_sender() -> lettre::SmtpTransport {
 | 
				
			|||||||
        .build()
 | 
					        .build()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static MAIL_SENDER: Lazy<lettre::SmtpTransport> = Lazy::new(build_mail_sender);
 | 
					pub static MAIL_SENDER: Lazy<lettre::SmtpTransport> = Lazy::new(build_mail_sender);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn get_totp(user: &db::User) -> totp_rs::TOTP {
 | 
					fn get_totp(user: &db::User) -> totp_rs::TOTP {
 | 
				
			||||||
    totp_rs::TOTP::from_rfc6238(
 | 
					    totp_rs::TOTP::from_rfc6238(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,10 +6,9 @@ use std::{
 | 
				
			|||||||
    ops::DerefMut,
 | 
					    ops::DerefMut,
 | 
				
			||||||
    sync::atomic::Ordering
 | 
					    sync::atomic::Ordering
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use image::DynamicImage;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use fast_image_resize as fir;
 | 
					use fast_image_resize as fir;
 | 
				
			||||||
use fast_image_resize::PixelType;
 | 
					use image::DynamicImage;
 | 
				
			||||||
use rustracing_jaeger::Span;
 | 
					use rustracing_jaeger::Span;
 | 
				
			||||||
use tiny_http::{Request, Response, ResponseBox, StatusCode};
 | 
					use tiny_http::{Request, Response, ResponseBox, StatusCode};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -212,18 +211,54 @@ pub fn upload(
 | 
				
			|||||||
                let width = NonZeroU32::try_from(img.width()).unwrap();
 | 
					                let width = NonZeroU32::try_from(img.width()).unwrap();
 | 
				
			||||||
                let height = NonZeroU32::try_from(img.height()).unwrap();
 | 
					                let height = NonZeroU32::try_from(img.height()).unwrap();
 | 
				
			||||||
                match img {
 | 
					                match img {
 | 
				
			||||||
                    DynamicImage::ImageLuma8(v) => fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8),
 | 
					                    DynamicImage::ImageLuma8(v) =>
 | 
				
			||||||
                    DynamicImage::ImageLumaA8(v) => fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x2),
 | 
					                        fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8),
 | 
				
			||||||
                    DynamicImage::ImageRgb8(v) => fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x3),
 | 
					                    DynamicImage::ImageLumaA8(v) =>
 | 
				
			||||||
                    DynamicImage::ImageRgba8(v) => fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x4),
 | 
					                        fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x2),
 | 
				
			||||||
                    DynamicImage::ImageLuma16(_) => fir::Image::from_vec_u8(width, height, img.to_luma8().into_raw(), fir::PixelType::U8),
 | 
					                    DynamicImage::ImageRgb8(v) =>
 | 
				
			||||||
                    DynamicImage::ImageLumaA16(_) => fir::Image::from_vec_u8(width, height, img.to_luma_alpha8().into_raw(), fir::PixelType::U8x2),
 | 
					                        fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x3),
 | 
				
			||||||
                    DynamicImage::ImageRgb16(_) => fir::Image::from_vec_u8(width, height, img.to_rgb8().into_raw(), fir::PixelType::U8x3),
 | 
					                    DynamicImage::ImageRgba8(v) =>
 | 
				
			||||||
                    DynamicImage::ImageRgba16(_) => fir::Image::from_vec_u8(width, height, img.to_rgba8().into_raw(), fir::PixelType::U8x4),
 | 
					                        fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x4),
 | 
				
			||||||
                    DynamicImage::ImageRgb32F(_) => fir::Image::from_vec_u8(width, height, img.to_rgb8().into_raw(), fir::PixelType::U8x3),
 | 
					                    DynamicImage::ImageLuma16(_) =>
 | 
				
			||||||
                    DynamicImage::ImageRgba32F(_) => fir::Image::from_vec_u8(width, height, img.to_rgba8().into_raw(), fir::PixelType::U8x4),
 | 
					                        fir::Image::from_vec_u8(width, height, img.to_luma8().into_raw(), fir::PixelType::U8),
 | 
				
			||||||
                    _ => fir::Image::from_vec_u8(width, height, img.to_rgba8().into_raw(), fir::PixelType::U8x4)
 | 
					                    DynamicImage::ImageLumaA16(_) => fir::Image::from_vec_u8(
 | 
				
			||||||
                }.expect("Failed to convert preview image")
 | 
					                        width,
 | 
				
			||||||
 | 
					                        height,
 | 
				
			||||||
 | 
					                        img.to_luma_alpha8().into_raw(),
 | 
				
			||||||
 | 
					                        fir::PixelType::U8x2
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    DynamicImage::ImageRgb16(_) => fir::Image::from_vec_u8(
 | 
				
			||||||
 | 
					                        width,
 | 
				
			||||||
 | 
					                        height,
 | 
				
			||||||
 | 
					                        img.to_rgb8().into_raw(),
 | 
				
			||||||
 | 
					                        fir::PixelType::U8x3
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    DynamicImage::ImageRgba16(_) => fir::Image::from_vec_u8(
 | 
				
			||||||
 | 
					                        width,
 | 
				
			||||||
 | 
					                        height,
 | 
				
			||||||
 | 
					                        img.to_rgba8().into_raw(),
 | 
				
			||||||
 | 
					                        fir::PixelType::U8x4
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    DynamicImage::ImageRgb32F(_) => fir::Image::from_vec_u8(
 | 
				
			||||||
 | 
					                        width,
 | 
				
			||||||
 | 
					                        height,
 | 
				
			||||||
 | 
					                        img.to_rgb8().into_raw(),
 | 
				
			||||||
 | 
					                        fir::PixelType::U8x3
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    DynamicImage::ImageRgba32F(_) => fir::Image::from_vec_u8(
 | 
				
			||||||
 | 
					                        width,
 | 
				
			||||||
 | 
					                        height,
 | 
				
			||||||
 | 
					                        img.to_rgba8().into_raw(),
 | 
				
			||||||
 | 
					                        fir::PixelType::U8x4
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    _ => fir::Image::from_vec_u8(
 | 
				
			||||||
 | 
					                        width,
 | 
				
			||||||
 | 
					                        height,
 | 
				
			||||||
 | 
					                        img.to_rgba8().into_raw(),
 | 
				
			||||||
 | 
					                        fir::PixelType::U8x4
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                .expect("Failed to convert preview image")
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            let img = {
 | 
					            let img = {
 | 
				
			||||||
                let _span = metrics::span("generate_preview_resize", &prev_span);
 | 
					                let _span = metrics::span("generate_preview_resize", &prev_span);
 | 
				
			||||||
@@ -233,12 +268,9 @@ pub fn upload(
 | 
				
			|||||||
                    NonZeroU32::try_from(new_dim.1).unwrap(),
 | 
					                    NonZeroU32::try_from(new_dim.1).unwrap(),
 | 
				
			||||||
                    img.pixel_type()
 | 
					                    img.pixel_type()
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
                fir::Resizer::new(fir::ResizeAlg::SuperSampling(
 | 
					                fir::Resizer::new(fir::ResizeAlg::SuperSampling(fir::FilterType::Hamming, 2))
 | 
				
			||||||
                    fir::FilterType::Hamming,
 | 
					                    .resize(&img.view(), &mut dst.view_mut())
 | 
				
			||||||
                    2
 | 
					                    .expect("Failed to resize preview image");
 | 
				
			||||||
                ))
 | 
					 | 
				
			||||||
                .resize(&img.view(), &mut dst.view_mut())
 | 
					 | 
				
			||||||
                .expect("Failed to resize preview image");
 | 
					 | 
				
			||||||
                dst
 | 
					                dst
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            let _span = metrics::span("generate_preview_save", &prev_span);
 | 
					            let _span = metrics::span("generate_preview_save", &prev_span);
 | 
				
			||||||
@@ -252,10 +284,10 @@ pub fn upload(
 | 
				
			|||||||
                    img.width().get(),
 | 
					                    img.width().get(),
 | 
				
			||||||
                    img.height().get(),
 | 
					                    img.height().get(),
 | 
				
			||||||
                    match img.pixel_type() {
 | 
					                    match img.pixel_type() {
 | 
				
			||||||
                        PixelType::U8 => image::ColorType::L8,
 | 
					                        fir::PixelType::U8 => image::ColorType::L8,
 | 
				
			||||||
                        PixelType::U8x2 => image::ColorType::La8,
 | 
					                        fir::PixelType::U8x2 => image::ColorType::La8,
 | 
				
			||||||
                        PixelType::U8x3 => image::ColorType::Rgb8,
 | 
					                        fir::PixelType::U8x3 => image::ColorType::Rgb8,
 | 
				
			||||||
                        PixelType::U8x4 => image::ColorType::Rgba8,
 | 
					                        fir::PixelType::U8x4 => image::ColorType::Rgba8,
 | 
				
			||||||
                        _ => unreachable!()
 | 
					                        _ => unreachable!()
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,22 @@ export const auth_signup = (
 | 
				
			|||||||
		password: password
 | 
							password: password
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const send_recovery_key = (
 | 
				
			||||||
 | 
						username: string
 | 
				
			||||||
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
 | 
						post<Requests.SendRecoveryKey>('/api/auth/send_key', {
 | 
				
			||||||
 | 
							username: username
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const reset_password = (
 | 
				
			||||||
 | 
						key: string,
 | 
				
			||||||
 | 
						password: string
 | 
				
			||||||
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
 | 
						post<Requests.ResetPassword>('/api/auth/reset', {
 | 
				
			||||||
 | 
							key: key,
 | 
				
			||||||
 | 
							password: password
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const refresh_token = (
 | 
					export const refresh_token = (
 | 
				
			||||||
	token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Login | Responses.Error> =>
 | 
					): Promise<Responses.Login | Responses.Error> =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,5 @@
 | 
				
			|||||||
import type { Requests, Responses, UploadFile } from '@/dto';
 | 
					import type { Requests, Responses, UploadFile } from '@/dto';
 | 
				
			||||||
import {
 | 
					import { get_token, post_token, isErrorResponse } from './base';
 | 
				
			||||||
	get_token,
 | 
					 | 
				
			||||||
	post_token,
 | 
					 | 
				
			||||||
	post_token_form,
 | 
					 | 
				
			||||||
	isErrorResponse
 | 
					 | 
				
			||||||
} from './base';
 | 
					 | 
				
			||||||
import axios from 'axios';
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const get_root = (
 | 
					export const get_root = (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,15 @@ export namespace Requests {
 | 
				
			|||||||
		otp?: string;
 | 
							otp?: string;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface SendRecoveryKey extends Base {
 | 
				
			||||||
 | 
							username: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface ResetPassword extends Base {
 | 
				
			||||||
 | 
							key: string;
 | 
				
			||||||
 | 
							password: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export interface TfaSetup extends Base {
 | 
						export interface TfaSetup extends Base {
 | 
				
			||||||
		mail: boolean;
 | 
							mail: boolean;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										119
									
								
								frontend/src/views/ForgotPasswordView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								frontend/src/views/ForgotPasswordView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from 'vue';
 | 
				
			||||||
 | 
					import { Auth, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					import { useMessage, NInput, NGrid, NGi, NButton, NCard } from 'naive-ui';
 | 
				
			||||||
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = useRouter();
 | 
				
			||||||
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const email = ref('');
 | 
				
			||||||
 | 
					const code = ref('');
 | 
				
			||||||
 | 
					const password = ref('');
 | 
				
			||||||
 | 
					const password2 = ref('');
 | 
				
			||||||
 | 
					const enterCode = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const reset = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
 | 
						if (password.value !== password2.value) {
 | 
				
			||||||
 | 
							message.error("Passwords don't match");
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const res = await Auth.reset_password(code.value, password.value);
 | 
				
			||||||
 | 
						if (isErrorResponse(res)) {
 | 
				
			||||||
 | 
							message.error(`Failed to reset password: ${res.message}`);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							message.success('Password reset', {
 | 
				
			||||||
 | 
								duration: 2000
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							login();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sendKey = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
 | 
						const res = await Auth.send_recovery_key(email.value);
 | 
				
			||||||
 | 
						if (isErrorResponse(res)) {
 | 
				
			||||||
 | 
							message.error(`Failed to send recovery key: ${res.message}`);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							message.success('If the email is registered a mail has been sent', {
 | 
				
			||||||
 | 
								duration: 10000
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							enterCode.value = true;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function login() {
 | 
				
			||||||
 | 
						router.replace('login');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onKey(event: KeyboardEvent) {
 | 
				
			||||||
 | 
						if (event.key == 'Enter') enterCode.value ? reset() : sendKey();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<n-card>
 | 
				
			||||||
 | 
							<n-grid cols="2" x-gap="16" y-gap="16">
 | 
				
			||||||
 | 
								<template v-if="enterCode">
 | 
				
			||||||
 | 
									<n-gi span="2">
 | 
				
			||||||
 | 
										<n-input
 | 
				
			||||||
 | 
											:key="1"
 | 
				
			||||||
 | 
											type="text"
 | 
				
			||||||
 | 
											placeholder="Recovery key"
 | 
				
			||||||
 | 
											v-model:value="code"
 | 
				
			||||||
 | 
											maxlength="10"
 | 
				
			||||||
 | 
											autofocus
 | 
				
			||||||
 | 
											@keyup="onKey"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi span="2">
 | 
				
			||||||
 | 
										<n-input
 | 
				
			||||||
 | 
											type="password"
 | 
				
			||||||
 | 
											placeholder="Password"
 | 
				
			||||||
 | 
											v-model:value="password"
 | 
				
			||||||
 | 
											@keyup="onKey"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi span="2">
 | 
				
			||||||
 | 
										<n-input
 | 
				
			||||||
 | 
											type="password"
 | 
				
			||||||
 | 
											placeholder="Repeat password"
 | 
				
			||||||
 | 
											v-model:value="password2"
 | 
				
			||||||
 | 
											@keyup="onKey"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi>
 | 
				
			||||||
 | 
										<n-button type="info" @click="reset">
 | 
				
			||||||
 | 
											Reset password
 | 
				
			||||||
 | 
										</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi style="text-align: right">
 | 
				
			||||||
 | 
										<n-button ghost @click="login">Go to login</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
 | 
								<template v-else>
 | 
				
			||||||
 | 
									<n-gi span="2">
 | 
				
			||||||
 | 
										<n-input
 | 
				
			||||||
 | 
											:key="2"
 | 
				
			||||||
 | 
											type="text"
 | 
				
			||||||
 | 
											placeholder="Email"
 | 
				
			||||||
 | 
											v-model:value="email"
 | 
				
			||||||
 | 
											autofocus
 | 
				
			||||||
 | 
											:input-props="{ type: 'email' }"
 | 
				
			||||||
 | 
											@keyup="onKey"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi>
 | 
				
			||||||
 | 
										<n-button type="info" @click="sendKey">
 | 
				
			||||||
 | 
											Send recovery key
 | 
				
			||||||
 | 
										</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi style="text-align: right">
 | 
				
			||||||
 | 
										<n-button ghost @click="login">Go to login</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
 | 
							</n-grid>
 | 
				
			||||||
 | 
						</n-card>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped></style>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user