fix share password issues

This commit is contained in:
2025-09-06 19:19:54 +02:00
parent 5c09120c23
commit a0c90f57d5
11 changed files with 256 additions and 54 deletions

View File

@@ -60,6 +60,9 @@ pub trait WarrenMetrics: Clone + Send + Sync + 'static {
fn record_warren_share_cat_success(&self) -> impl Future<Output = ()> + Send; fn record_warren_share_cat_success(&self) -> impl Future<Output = ()> + Send;
fn record_warren_share_cat_failure(&self) -> impl Future<Output = ()> + Send; fn record_warren_share_cat_failure(&self) -> impl Future<Output = ()> + Send;
fn record_warren_share_password_verification_success(&self) -> impl Future<Output = ()> + Send;
fn record_warren_share_password_verification_failure(&self) -> impl Future<Output = ()> + Send;
} }
pub trait FileSystemMetrics: Clone + Send + Sync + 'static { pub trait FileSystemMetrics: Clone + Send + Sync + 'static {

View File

@@ -25,6 +25,7 @@ use super::models::{
DeleteShareError, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest, DeleteShareError, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, ShareCatError, GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, ShareCatError,
ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest, ShareLsResponse, ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest, ShareLsResponse,
VerifySharePasswordError, VerifySharePasswordRequest, VerifySharePasswordResponse,
}, },
user::{ user::{
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
@@ -138,6 +139,10 @@ pub trait WarrenService: Clone + Send + Sync + 'static {
&self, &self,
request: ShareCatRequest, request: ShareCatRequest,
) -> impl Future<Output = Result<ShareCatResponse, ShareCatError>> + Send; ) -> impl Future<Output = Result<ShareCatResponse, ShareCatError>> + Send;
fn verify_warren_share_password(
&self,
request: VerifySharePasswordRequest,
) -> impl Future<Output = Result<VerifySharePasswordResponse, VerifySharePasswordError>> + Send;
} }
pub trait FileSystemService: Clone + Send + Sync + 'static { pub trait FileSystemService: Clone + Send + Sync + 'static {

View File

@@ -5,7 +5,7 @@ use crate::domain::warren::models::{
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse}, file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
share::{ share::{
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse, CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
ShareCatResponse, ShareLsResponse, ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
}, },
user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User}, user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User},
user_warren::UserWarren, user_warren::UserWarren,
@@ -64,6 +64,10 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
) -> impl Future<Output = ()> + Send; ) -> impl Future<Output = ()> + Send;
fn warren_share_ls(&self, response: &ShareLsResponse) -> impl Future<Output = ()> + Send; fn warren_share_ls(&self, response: &ShareLsResponse) -> impl Future<Output = ()> + Send;
fn warren_share_cat(&self, response: &ShareCatResponse) -> impl Future<Output = ()> + Send; fn warren_share_cat(&self, response: &ShareCatResponse) -> impl Future<Output = ()> + Send;
fn warren_share_password_verified(
&self,
response: &VerifySharePasswordResponse,
) -> impl Future<Output = ()> + Send;
} }
pub trait FileSystemNotifier: Clone + Send + Sync + 'static { pub trait FileSystemNotifier: Clone + Send + Sync + 'static {

View File

@@ -6,7 +6,8 @@ use crate::domain::warren::{
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, Share, GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, Share,
ShareCatError, ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest, ShareCatError, ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest,
ShareLsResponse, ShareLsResponse, VerifySharePasswordError, VerifySharePasswordRequest,
VerifySharePasswordResponse,
}, },
warren::{ warren::{
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest, CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
@@ -528,4 +529,24 @@ where
Ok(response) Ok(response)
} }
async fn verify_warren_share_password(
&self,
request: VerifySharePasswordRequest,
) -> Result<VerifySharePasswordResponse, VerifySharePasswordError> {
let result = self.repository.verify_warren_share_password(request).await;
if let Ok(response) = result.as_ref() {
self.metrics
.record_warren_share_password_verification_success()
.await;
self.notifier.warren_share_password_verified(response).await;
} else {
self.metrics
.record_warren_share_password_verification_failure()
.await;
}
result
}
} }

View File

@@ -13,6 +13,7 @@ use crate::{
}, },
inbound::http::{ inbound::http::{
AppState, AppState,
handlers::extractors::SharePasswordHeader,
responses::{ApiError, ApiSuccess}, responses::{ApiError, ApiSuccess},
}, },
}; };
@@ -59,17 +60,14 @@ impl From<ParseShareLsHttpRequestError> for ApiError {
pub(super) struct LsShareHttpRequestBody { pub(super) struct LsShareHttpRequestBody {
share_id: Uuid, share_id: Uuid,
path: String, path: String,
password: Option<String>,
} }
impl LsShareHttpRequestBody { impl LsShareHttpRequestBody {
fn try_into_domain(self) -> Result<ShareLsRequest, ParseShareLsHttpRequestError> { fn try_into_domain(
self,
password: Option<SharePassword>,
) -> Result<ShareLsRequest, ParseShareLsHttpRequestError> {
let path = FilePath::new(&self.path)?.try_into()?; let path = FilePath::new(&self.path)?.try_into()?;
let password = if let Some(password) = self.password.as_ref() {
Some(SharePassword::new(password)?)
} else {
None
};
Ok(ShareLsRequest::new(self.share_id, path, password)) Ok(ShareLsRequest::new(self.share_id, path, password))
} }
@@ -98,9 +96,10 @@ impl From<ShareLsResponse> for ShareLsResponseData {
pub async fn ls_share<WS: WarrenService, AS: AuthService>( pub async fn ls_share<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>, State(state): State<AppState<WS, AS>>,
SharePasswordHeader(password): SharePasswordHeader,
Json(request): Json<LsShareHttpRequestBody>, Json(request): Json<LsShareHttpRequestBody>,
) -> Result<ApiSuccess<ShareLsResponseData>, ApiError> { ) -> Result<ApiSuccess<ShareLsResponseData>, ApiError> {
let domain_request = request.try_into_domain()?; let domain_request = request.try_into_domain(password)?;
state state
.warren_service .warren_service

View File

@@ -7,6 +7,7 @@ mod list_shares;
mod list_warrens; mod list_warrens;
mod ls_share; mod ls_share;
mod upload_warren_files; mod upload_warren_files;
mod verify_share_password;
mod warren_cat; mod warren_cat;
mod warren_cp; mod warren_cp;
mod warren_ls; mod warren_ls;
@@ -37,6 +38,7 @@ use get_share::get_share;
use list_shares::list_shares; use list_shares::list_shares;
use ls_share::ls_share; use ls_share::ls_share;
use upload_warren_files::warren_save; use upload_warren_files::warren_save;
use verify_share_password::verify_share_password;
use warren_cat::fetch_file; use warren_cat::fetch_file;
use warren_cp::warren_cp; use warren_cp::warren_cp;
use warren_mv::warren_mv; use warren_mv::warren_mv;
@@ -116,4 +118,5 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
.route("/files/get_share", post(get_share)) .route("/files/get_share", post(get_share))
.route("/files/ls_share", post(ls_share)) .route("/files/ls_share", post(ls_share))
.route("/files/cat_share", get(cat_share)) .route("/files/cat_share", get(cat_share))
.route("/files/verify_share_password", post(verify_share_password))
} }

View File

@@ -0,0 +1,72 @@
use axum::{Json, extract::State, http::StatusCode};
use serde::Deserialize;
use thiserror::Error;
use uuid::Uuid;
use crate::{
domain::warren::{
models::share::{SharePassword, SharePasswordError, VerifySharePasswordRequest},
ports::{AuthService, WarrenService},
},
inbound::http::{
AppState,
responses::{ApiError, ApiSuccess},
},
};
#[derive(Debug, Error)]
enum ParseVerifySharePasswordHttpRequestError {
#[error(transparent)]
SharePassword(#[from] SharePasswordError),
}
impl From<ParseVerifySharePasswordHttpRequestError> for ApiError {
fn from(value: ParseVerifySharePasswordHttpRequestError) -> Self {
match value {
ParseVerifySharePasswordHttpRequestError::SharePassword(err) => Self::BadRequest(
match err {
SharePasswordError::Empty => "The provided password is empty",
SharePasswordError::LeadingWhitespace
| SharePasswordError::TrailingWhitespace
| SharePasswordError::TooShort
| SharePasswordError::TooLong => "",
}
.to_string(),
),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct VerifySharePasswordHttpRequestBody {
share_id: Uuid,
password: String,
}
impl VerifySharePasswordHttpRequestBody {
fn try_into_domain(
self,
) -> Result<VerifySharePasswordRequest, ParseVerifySharePasswordHttpRequestError> {
let password = SharePassword::new(&self.password)?;
Ok(VerifySharePasswordRequest::new(
self.share_id,
Some(password),
))
}
}
pub async fn verify_share_password<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>,
Json(request): Json<VerifySharePasswordHttpRequestBody>,
) -> Result<ApiSuccess<()>, ApiError> {
let domain_request = request.try_into_domain()?;
state
.warren_service
.verify_warren_share_password(domain_request)
.await
.map(|_| ApiSuccess::new(StatusCode::OK, ()))
.map_err(ApiError::from)
}

View File

@@ -151,6 +151,13 @@ impl WarrenMetrics for MetricsDebugLogger {
async fn record_warren_share_cat_failure(&self) { async fn record_warren_share_cat_failure(&self) {
tracing::debug!("[Metrics] Warren share cat failed"); tracing::debug!("[Metrics] Warren share cat failed");
} }
async fn record_warren_share_password_verification_success(&self) {
tracing::debug!("[Metrics] Warren share password verification succeeded");
}
async fn record_warren_share_password_verification_failure(&self) {
tracing::debug!("[Metrics] Warren share password verification failed");
}
} }
impl FileSystemMetrics for MetricsDebugLogger { impl FileSystemMetrics for MetricsDebugLogger {

View File

@@ -11,7 +11,7 @@ use crate::domain::{
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse}, file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
share::{ share::{
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse, CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
ShareCatResponse, ShareLsResponse, ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
}, },
user::{ user::{
ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User, ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User,
@@ -201,6 +201,13 @@ impl WarrenNotifier for NotifierDebugLogger {
response.share().id(), response.share().id(),
); );
} }
async fn warren_share_password_verified(&self, response: &VerifySharePasswordResponse) {
tracing::debug!(
"[Notifier] Verified password for share {}",
response.share().id()
);
}
} }
impl FileSystemNotifier for NotifierDebugLogger { impl FileSystemNotifier for NotifierDebugLogger {

View File

@@ -120,19 +120,24 @@ export async function listShareFiles(
| { success: true; files: DirectoryEntry[]; parent: DirectoryEntry | null } | { success: true; files: DirectoryEntry[]; parent: DirectoryEntry | null }
| { success: false } | { success: false }
> { > {
const { data } = await useFetch< const { data, error } = await useFetch<
ApiResponse<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }> ApiResponse<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }>
>(getApiUrl('warrens/files/ls_share'), { >(getApiUrl('warrens/files/ls_share'), {
method: 'POST', method: 'POST',
headers: getApiHeaders(), // This is only required for development
headers:
password != null
? { ...getApiHeaders(), 'X-Share-Password': password }
: getApiHeaders(),
body: JSON.stringify({ body: JSON.stringify({
shareId: shareId, shareId: shareId,
path: path, path: path,
password: password,
}), }),
}); });
if (data.value == null) { if (data.value == null) {
const errorMessage = await error.value?.data;
console.log(errorMessage);
return { return {
success: false, success: false,
}; };
@@ -174,3 +179,32 @@ export async function fetchShareFile(
data: data.value, data: data.value,
}; };
} }
export async function verifySharePassword(
shareId: string,
password: string
): Promise<{ success: boolean }> {
const { data } = await useFetch<ApiResponse<null>>(
getApiUrl(`warrens/files/verify_share_password`),
{
method: 'POST',
headers: getApiHeaders(),
body: JSON.stringify({
shareId: shareId,
password: password,
}),
responseType: 'json',
cache: 'default',
}
);
if (data.value == null || data.value.status !== 200) {
return {
success: false,
};
}
return {
success: true,
};
}

View File

@@ -1,5 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { fetchShareFile, getShare, listShareFiles } from '~/lib/api/shares'; import byteSize from 'byte-size';
import { toast } from 'vue-sonner';
import {
fetchShareFile,
getShare,
listShareFiles,
verifySharePassword,
} from '~/lib/api/shares';
import type { DirectoryEntry } from '~/shared/types'; import type { DirectoryEntry } from '~/shared/types';
import type { Share } from '~/shared/types/shares'; import type { Share } from '~/shared/types/shares';
import { useImageViewer, useTextEditor } from '~/stores/viewers'; import { useImageViewer, useTextEditor } from '~/stores/viewers';
@@ -60,7 +67,39 @@ async function getShareFromQuery(): Promise<{
} }
async function submitPassword() { async function submitPassword() {
loadFiles(); if (share == null || loading.value) {
return;
}
if (!passwordValid.value) {
loading.value = true;
const result = await verifySharePassword(share.data.id, password.value);
loading.value = false;
if (result.success) {
passwordValid.value = true;
let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`;
if (share.data.expiresAt != null) {
const dayjs = useDayjs();
const diff = dayjs(share.data.expiresAt).diff(dayjs()) / 1000;
cookie += `Max-Age=${diff};`;
}
document.cookie = cookie;
} else {
toast.error('Share', {
id: 'SHARE_PASSWORD_TOAST',
description: 'Invalid password',
});
return;
}
}
if (share.file.fileType === 'directory') {
loadFiles();
}
} }
async function loadFiles() { async function loadFiles() {
@@ -68,10 +107,6 @@ async function loadFiles() {
return; return;
} }
if (share.file.fileType !== 'directory') {
return;
}
loading.value = true; loading.value = true;
const result = await listShareFiles( const result = await listShareFiles(
@@ -81,18 +116,6 @@ async function loadFiles() {
); );
if (result.success) { if (result.success) {
passwordValid.value = true;
let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`;
if (share.data.expiresAt != null) {
const dayjs = useDayjs();
const diff = dayjs(share.data.expiresAt).diff(dayjs()) / 1000;
cookie += `Max-Age=${diff};`;
}
document.cookie = cookie;
warrenStore.setCurrentWarrenEntries(result.files, result.parent); warrenStore.setCurrentWarrenEntries(result.files, result.parent);
} }
@@ -102,7 +125,11 @@ async function loadFiles() {
async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) { async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
if (warrenStore.current == null) { if (
warrenStore.current == null ||
share == null ||
(share.data.password && !passwordValid.value)
) {
return; return;
} }
@@ -110,7 +137,10 @@ async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
return; return;
} }
const entryPath = joinPaths(warrenStore.current.path, entry.name); const entryPath =
entry !== share.file
? joinPaths(warrenStore.current.path, entry.name)
: warrenStore.current.path;
if (entry.fileType === 'directory') { if (entry.fileType === 'directory') {
warrenStore.setCurrentWarrenPath(entryPath); warrenStore.setCurrentWarrenPath(entryPath);
@@ -217,17 +247,35 @@ function onEntryDownload(entry: DirectoryEntry) {
> >
<div <div
:class="[ :class="[
'h-[min(98vh,600px)] w-full max-w-screen-xl rounded-lg border transition-all', 'w-full rounded-lg border transition-all',
passwordValid ? 'max-w-screen-xl' : 'max-w-lg', passwordValid && share.file.fileType === 'directory'
? 'h-[min(98vh,600px)] max-w-screen-xl'
: 'max-w-2xl',
]" ]"
> >
<div <div class="flex flex-row items-center justify-between gap-4 p-6">
class="flex flex-row items-center justify-between gap-4 px-6 pt-6" <button
> :disabled="share.data.password && !passwordValid"
<div class="flex w-full flex-row"> :class="[
<div class="flex grow flex-col gap-1.5"> 'flex min-w-0 grow flex-row items-center gap-2 text-left',
<h3 class="leading-none font-semibold">Share</h3> (!share.data.password || passwordValid) &&
<p class="text-muted-foreground text-sm"> 'cursor-pointer',
]"
@click="(e) => onEntryClicked(share!.file, e)"
>
<DirectoryEntryIcon :entry="share.file" />
<div class="flex flex-col overflow-hidden">
<h3
:title="share.file.name"
class="truncate leading-none font-semibold"
>
{{ share.file.name }}
</h3>
<p class="text-muted-foreground text-sm text-nowrap">
{{ byteSize(share.file.size) }}
</p>
<p class="text-muted-foreground text-sm text-nowrap">
Created Created
{{ {{
$dayjs(share.data.createdAt).format( $dayjs(share.data.createdAt).format(
@@ -236,19 +284,12 @@ function onEntryDownload(entry: DirectoryEntry) {
}} }}
</p> </p>
</div> </div>
<div class="flex flex-row items-center justify-end gap-4"> </button>
<p>{{ share.file.name }}</p>
<DirectoryEntryIcon
:entry="{ ...share.file, name: '/' }"
/>
</div>
</div>
<div class="flex flex-row items-end"> <div class="flex flex-row items-end">
<Button <Button
:class=" :class="
share.file.fileType !== 'file' && share.data.password && !passwordValid && 'hidden'
entries == null &&
'hidden'
" "
size="icon" size="icon"
variant="outline" variant="outline"
@@ -258,7 +299,13 @@ function onEntryDownload(entry: DirectoryEntry) {
</div> </div>
</div> </div>
<div class="flex w-full flex-col p-6"> <div
v-if="
share.file.fileType === 'directory' ||
(share.data.password && !passwordValid)
"
class="flex w-full flex-col px-6 pb-6"
>
<DirectoryList <DirectoryList
v-if="entries != null" v-if="entries != null"
:entries :entries