Security fix for path-traversal attack (#48)
Additionally: - Make expense and budget modal "reset" once closed, saved, or deleted. - Make manifest icons dark - Budgets in small screens should be full-screen - Minor code cleanup Fixes #48
This commit is contained in:
@@ -9,26 +9,33 @@ interface BudgetModalProps {
|
|||||||
onClickSave: (newBudgetName: string, newBudgetMonth: string, newBudgetValue: number) => Promise<void>;
|
onClickSave: (newBudgetName: string, newBudgetMonth: string, newBudgetValue: number) => Promise<void>;
|
||||||
onClickDelete: () => Promise<void>;
|
onClickDelete: () => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
shouldResetForm: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BudgetModal(
|
export default function BudgetModal(
|
||||||
{ isOpen, budget, onClickSave, onClickDelete, onClose }: BudgetModalProps,
|
{ isOpen, budget, onClickSave, onClickDelete, onClose, shouldResetForm }: BudgetModalProps,
|
||||||
) {
|
) {
|
||||||
const newBudgetName = useSignal<string>(budget?.name ?? '');
|
const newBudgetName = useSignal<string>(budget?.name ?? '');
|
||||||
const newBudgetMonth = useSignal<string>(budget?.month ?? new Date().toISOString().substring(0, 10));
|
const newBudgetMonth = useSignal<string>(budget?.month ?? new Date().toISOString().substring(0, 10));
|
||||||
const newBudgetValue = useSignal<number>(budget?.value ?? 100);
|
const newBudgetValue = useSignal<number>(budget?.value ?? 100);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
newBudgetName.value = '';
|
||||||
|
newBudgetMonth.value = new Date().toISOString().substring(0, 10);
|
||||||
|
newBudgetValue.value = 100;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (budget) {
|
if (budget) {
|
||||||
newBudgetName.value = budget.name;
|
newBudgetName.value = budget.name;
|
||||||
newBudgetMonth.value = `${budget.month}-15`;
|
newBudgetMonth.value = `${budget.month}-15`;
|
||||||
newBudgetValue.value = budget.value;
|
newBudgetValue.value = budget.value;
|
||||||
} else {
|
|
||||||
newBudgetName.value = '';
|
|
||||||
newBudgetMonth.value = new Date().toISOString().substring(0, 10);
|
|
||||||
newBudgetValue.value = 100;
|
|
||||||
}
|
}
|
||||||
}, [budget]);
|
|
||||||
|
if (shouldResetForm) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}, [budget, shouldResetForm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ interface ExpenseModalProps {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
onClickDelete: () => Promise<void>;
|
onClickDelete: () => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
shouldResetForm: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExpenseModal(
|
export default function ExpenseModal(
|
||||||
{ isOpen, expense, budgets, onClickSave, onClickDelete, onClose }: ExpenseModalProps,
|
{ isOpen, expense, budgets, onClickSave, onClickDelete, onClose, shouldResetForm }: ExpenseModalProps,
|
||||||
) {
|
) {
|
||||||
const newExpenseCost = useSignal<number | ''>(expense?.cost ?? '');
|
const newExpenseCost = useSignal<number | ''>(expense?.cost ?? '');
|
||||||
const newExpenseDescription = useSignal<string>(expense?.description ?? '');
|
const newExpenseDescription = useSignal<string>(expense?.description ?? '');
|
||||||
@@ -34,6 +35,14 @@ export default function ExpenseModal(
|
|||||||
const suggestions = useSignal<string[]>([]);
|
const suggestions = useSignal<string[]>([]);
|
||||||
const showSuggestions = useSignal<boolean>(false);
|
const showSuggestions = useSignal<boolean>(false);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
newExpenseCost.value = '';
|
||||||
|
newExpenseDescription.value = '';
|
||||||
|
newExpenseBudget.value = 'Misc';
|
||||||
|
newExpenseDate.value = '';
|
||||||
|
newExpenseIsRecurring.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (expense) {
|
if (expense) {
|
||||||
newExpenseCost.value = expense.cost;
|
newExpenseCost.value = expense.cost;
|
||||||
@@ -42,15 +51,12 @@ export default function ExpenseModal(
|
|||||||
newExpenseDate.value = expense.date;
|
newExpenseDate.value = expense.date;
|
||||||
newExpenseIsRecurring.value = expense.is_recurring;
|
newExpenseIsRecurring.value = expense.is_recurring;
|
||||||
showSuggestions.value = false;
|
showSuggestions.value = false;
|
||||||
} else {
|
|
||||||
newExpenseCost.value = '';
|
|
||||||
newExpenseDescription.value = '';
|
|
||||||
newExpenseBudget.value = 'Misc';
|
|
||||||
newExpenseDate.value = '';
|
|
||||||
newExpenseIsRecurring.value = false;
|
|
||||||
showSuggestions.value = false;
|
|
||||||
}
|
}
|
||||||
}, [expense]);
|
|
||||||
|
if (shouldResetForm) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}, [expense, shouldResetForm]);
|
||||||
|
|
||||||
const sortedBudgetNames = budgets.map((budget) => budget.name).sort();
|
const sortedBudgetNames = budgets.map((budget) => budget.name).sort();
|
||||||
|
|
||||||
@@ -225,14 +231,15 @@ export default function ExpenseModal(
|
|||||||
: null}
|
: null}
|
||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md mr-2'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md mr-2'
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
onClickSave(
|
onClickSave(
|
||||||
newExpenseCost.value as number,
|
newExpenseCost.value as number,
|
||||||
newExpenseDescription.value,
|
newExpenseDescription.value,
|
||||||
newExpenseBudget.value,
|
newExpenseBudget.value,
|
||||||
newExpenseDate.value,
|
newExpenseDate.value,
|
||||||
newExpenseIsRecurring.value,
|
newExpenseIsRecurring.value,
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{expense ? 'Update' : 'Create'}
|
{expense ? 'Update' : 'Create'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default function ListBudgets(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => budget.id === 'total' ? swapView('chart') : onClickEditBudget(budget.id)}
|
onClick={() => budget.id === 'total' ? swapView('chart') : onClickEditBudget(budget.id)}
|
||||||
class='flex max-w-sm gap-y-4 gap-x-4 rounded shadow-md bg-slate-700 relative cursor-pointer py-4 px-6 hover:opacity-80'
|
class='flex w-full md:w-auto max-w-sm gap-y-4 gap-x-4 rounded shadow-md bg-slate-700 relative cursor-pointer py-4 px-6 hover:opacity-80'
|
||||||
>
|
>
|
||||||
<article class='order-first tracking-tight flex flex-col text-base mr-4'>
|
<article class='order-first tracking-tight flex flex-col text-base mr-4'>
|
||||||
<span class='font-bold text-lg' title='Amount used from budgeted amount'>
|
<span class='font-bold text-lg' title='Amount used from budgeted amount'>
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
const editingExpense = useSignal<Expense | null>(null);
|
const editingExpense = useSignal<Expense | null>(null);
|
||||||
const isBudgetModalOpen = useSignal<boolean>(false);
|
const isBudgetModalOpen = useSignal<boolean>(false);
|
||||||
const editingBudget = useSignal<Budget | null>(null);
|
const editingBudget = useSignal<Budget | null>(null);
|
||||||
|
const shouldResetExpenseModal = useSignal<boolean>(false);
|
||||||
|
const shouldResetBudgetModal = useSignal<boolean>(false);
|
||||||
const searchTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
|
const searchTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
|
||||||
|
|
||||||
const dateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long' });
|
const dateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long' });
|
||||||
@@ -197,6 +199,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldResetExpenseModal.value = false;
|
||||||
editingExpense.value = null;
|
editingExpense.value = null;
|
||||||
isExpenseModalOpen.value = true;
|
isExpenseModalOpen.value = true;
|
||||||
}
|
}
|
||||||
@@ -209,6 +212,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldResetBudgetModal.value = false;
|
||||||
editingBudget.value = null;
|
editingBudget.value = null;
|
||||||
isBudgetModalOpen.value = true;
|
isBudgetModalOpen.value = true;
|
||||||
}
|
}
|
||||||
@@ -221,6 +225,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldResetExpenseModal.value = false;
|
||||||
editingExpense.value = expenses.value.find((expense) => expense.id === expenseId)!;
|
editingExpense.value = expenses.value.find((expense) => expense.id === expenseId)!;
|
||||||
isExpenseModalOpen.value = true;
|
isExpenseModalOpen.value = true;
|
||||||
}
|
}
|
||||||
@@ -238,6 +243,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldResetBudgetModal.value = false;
|
||||||
editingBudget.value = budgets.value.find((budget) => budget.id === budgetId)!;
|
editingBudget.value = budgets.value.find((budget) => budget.id === budgetId)!;
|
||||||
isBudgetModalOpen.value = true;
|
isBudgetModalOpen.value = true;
|
||||||
}
|
}
|
||||||
@@ -292,6 +298,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
|
|
||||||
isExpenseModalOpen.value = false;
|
isExpenseModalOpen.value = false;
|
||||||
editingExpense.value = null;
|
editingExpense.value = null;
|
||||||
|
shouldResetExpenseModal.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert(error);
|
alert(error);
|
||||||
@@ -326,6 +333,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
budgets.value = [...result.newBudgets];
|
budgets.value = [...result.newBudgets];
|
||||||
|
|
||||||
isExpenseModalOpen.value = false;
|
isExpenseModalOpen.value = false;
|
||||||
|
shouldResetExpenseModal.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert(error);
|
alert(error);
|
||||||
@@ -379,6 +387,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
|
|
||||||
isBudgetModalOpen.value = false;
|
isBudgetModalOpen.value = false;
|
||||||
editingBudget.value = null;
|
editingBudget.value = null;
|
||||||
|
shouldResetBudgetModal.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert(error);
|
alert(error);
|
||||||
@@ -410,6 +419,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
budgets.value = [...result.newBudgets];
|
budgets.value = [...result.newBudgets];
|
||||||
|
|
||||||
isBudgetModalOpen.value = false;
|
isBudgetModalOpen.value = false;
|
||||||
|
shouldResetBudgetModal.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert(error);
|
alert(error);
|
||||||
@@ -460,6 +470,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
|
|
||||||
isExpenseModalOpen.value = false;
|
isExpenseModalOpen.value = false;
|
||||||
editingExpense.value = null;
|
editingExpense.value = null;
|
||||||
|
shouldResetExpenseModal.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert(error);
|
alert(error);
|
||||||
@@ -508,6 +519,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
|
|
||||||
isBudgetModalOpen.value = false;
|
isBudgetModalOpen.value = false;
|
||||||
editingBudget.value = null;
|
editingBudget.value = null;
|
||||||
|
shouldResetBudgetModal.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert(error);
|
alert(error);
|
||||||
@@ -519,11 +531,13 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
function onCloseExpense() {
|
function onCloseExpense() {
|
||||||
isExpenseModalOpen.value = false;
|
isExpenseModalOpen.value = false;
|
||||||
editingExpense.value = null;
|
editingExpense.value = null;
|
||||||
|
shouldResetExpenseModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCloseBudget() {
|
function onCloseBudget() {
|
||||||
isBudgetModalOpen.value = false;
|
isBudgetModalOpen.value = false;
|
||||||
editingBudget.value = null;
|
editingBudget.value = null;
|
||||||
|
shouldResetBudgetModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNewOptionsDropdown() {
|
function toggleNewOptionsDropdown() {
|
||||||
@@ -780,6 +794,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
onClickSave={onClickSaveExpense}
|
onClickSave={onClickSaveExpense}
|
||||||
onClickDelete={onClickDeleteExpense}
|
onClickDelete={onClickDeleteExpense}
|
||||||
onClose={onCloseExpense}
|
onClose={onCloseExpense}
|
||||||
|
shouldResetForm={shouldResetExpenseModal.value}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BudgetModal
|
<BudgetModal
|
||||||
@@ -788,6 +803,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
onClickSave={onClickSaveBudget}
|
onClickSave={onClickSaveBudget}
|
||||||
onClickDelete={onClickDeleteBudget}
|
onClickDelete={onClickDeleteBudget}
|
||||||
onClose={onCloseBudget}
|
onClose={onCloseBudget}
|
||||||
|
shouldResetForm={shouldResetBudgetModal.value}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -195,13 +195,12 @@ export default function Feeds({ initialFeeds }: FeedsProps) {
|
|||||||
|
|
||||||
isExporting.value = true;
|
isExporting.value = true;
|
||||||
|
|
||||||
const fileName = ['feeds-', new Date().toISOString().substring(0, 19).replace(/:/g, '-'), '.opml']
|
const fileName = `feeds-${new Date().toISOString().substring(0, 19).replace(/:/g, '-')}.opml`;
|
||||||
.join('');
|
|
||||||
|
|
||||||
const exportContents = formatNewsFeedsToOpml([...feeds.peek()]);
|
const exportContents = formatNewsFeedsToOpml([...feeds.peek()]);
|
||||||
|
|
||||||
// Add content-type
|
// Add content-type
|
||||||
const xmlContent = ['data:application/xml; charset=utf-8,', exportContents].join('');
|
const xmlContent = `data:application/xml; charset=utf-8,${exportContents}`;
|
||||||
|
|
||||||
// Download the file
|
// Download the file
|
||||||
const data = encodeURI(xmlContent);
|
const data = encodeURI(xmlContent);
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
import { join } from 'std/path/join.ts';
|
import { join } from 'std/path/join.ts';
|
||||||
|
import { resolve } from 'std/path/resolve.ts';
|
||||||
import { lookup } from 'mrmime';
|
import { lookup } from 'mrmime';
|
||||||
|
|
||||||
import { getFilesRootPath } from '/lib/config.ts';
|
import { getFilesRootPath } from '/lib/config.ts';
|
||||||
import { Directory, DirectoryFile } from '/lib/types.ts';
|
import { Directory, DirectoryFile } from '/lib/types.ts';
|
||||||
import { sortDirectoriesByName, sortEntriesByName, sortFilesByName, TRASH_PATH } from '/lib/utils/files.ts';
|
import { sortDirectoriesByName, sortEntriesByName, sortFilesByName, TRASH_PATH } from '/lib/utils/files.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the user path is valid and securely accessible (meaning it's not trying to access files outside of the user's root directory).
|
||||||
|
* Does not check if the path exists.
|
||||||
|
*
|
||||||
|
* @param userId - The user ID
|
||||||
|
* @param path - The relative path (user-provided) to check
|
||||||
|
*/
|
||||||
|
export function ensureUserPathIsValidAndSecurelyAccessible(userId: string, path: string): void {
|
||||||
|
const userRootPath = join(getFilesRootPath(), userId, '/');
|
||||||
|
|
||||||
|
const fullPath = join(userRootPath, path);
|
||||||
|
|
||||||
|
const resolvedFullPath = `${resolve(fullPath)}/`;
|
||||||
|
|
||||||
|
if (!resolvedFullPath.startsWith(userRootPath)) {
|
||||||
|
throw new Error('Invalid file path');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDirectories(userId: string, path: string): Promise<Directory[]> {
|
export async function getDirectories(userId: string, path: string): Promise<Directory[]> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
const directories: Directory[] = [];
|
const directories: Directory[] = [];
|
||||||
@@ -34,6 +56,8 @@ export async function getDirectories(userId: string, path: string): Promise<Dire
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getFiles(userId: string, path: string): Promise<DirectoryFile[]> {
|
export async function getFiles(userId: string, path: string): Promise<DirectoryFile[]> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
const files: DirectoryFile[] = [];
|
const files: DirectoryFile[] = [];
|
||||||
@@ -62,6 +86,8 @@ export async function getFiles(userId: string, path: string): Promise<DirectoryF
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getPathEntries(userId: string, path: string): Promise<Deno.DirEntry[]> {
|
async function getPathEntries(userId: string, path: string): Promise<Deno.DirEntry[]> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
// Ensure the user directory exists
|
// Ensure the user directory exists
|
||||||
@@ -98,6 +124,8 @@ async function getPathEntries(userId: string, path: string): Promise<Deno.DirEnt
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createDirectory(userId: string, path: string, name: string): Promise<boolean> {
|
export async function createDirectory(userId: string, path: string, name: string): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -117,6 +145,9 @@ export async function renameDirectoryOrFile(
|
|||||||
oldName: string,
|
oldName: string,
|
||||||
newName: string,
|
newName: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(oldPath, oldName));
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(newPath, newName));
|
||||||
|
|
||||||
const oldRootPath = join(getFilesRootPath(), userId, oldPath);
|
const oldRootPath = join(getFilesRootPath(), userId, oldPath);
|
||||||
const newRootPath = join(getFilesRootPath(), userId, newPath);
|
const newRootPath = join(getFilesRootPath(), userId, newPath);
|
||||||
|
|
||||||
@@ -131,6 +162,8 @@ export async function renameDirectoryOrFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteDirectoryOrFile(userId: string, path: string, name: string): Promise<boolean> {
|
export async function deleteDirectoryOrFile(userId: string, path: string, name: string): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -154,6 +187,8 @@ export async function createFile(
|
|||||||
name: string,
|
name: string,
|
||||||
contents: string | ArrayBuffer,
|
contents: string | ArrayBuffer,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -176,6 +211,8 @@ export async function updateFile(
|
|||||||
name: string,
|
name: string,
|
||||||
contents: string,
|
contents: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -193,6 +230,8 @@ export async function getFile(
|
|||||||
path: string,
|
path: string,
|
||||||
name?: string,
|
name?: string,
|
||||||
): Promise<{ success: boolean; contents?: Uint8Array; contentType?: string; byteSize?: number }> {
|
): Promise<{ success: boolean; contents?: Uint8Array; contentType?: string; byteSize?: number }> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name || ''));
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
getProperDestinationPath,
|
getProperDestinationPath,
|
||||||
getPropertyNames,
|
getPropertyNames,
|
||||||
} from '/lib/utils/webdav.ts';
|
} from '/lib/utils/webdav.ts';
|
||||||
import { getFile } from '/lib/data/files.ts';
|
import { ensureUserPathIsValidAndSecurelyAccessible, getFile } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -35,7 +35,9 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
|
|
||||||
filePath = decodeURIComponent(filePath);
|
filePath = decodeURIComponent(filePath);
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), context.state.user.id);
|
const userId = context.state.user.id;
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId);
|
||||||
|
|
||||||
if (request.method === 'OPTIONS') {
|
if (request.method === 'OPTIONS') {
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
@@ -51,7 +53,7 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
|
|
||||||
if (request.method === 'GET') {
|
if (request.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
const fileResult = await getFile(context.state.user.id, filePath);
|
const fileResult = await getFile(userId, filePath);
|
||||||
|
|
||||||
if (!fileResult.success) {
|
if (!fileResult.success) {
|
||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
@@ -74,6 +76,8 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
|
|
||||||
if (request.method === 'DELETE') {
|
if (request.method === 'DELETE') {
|
||||||
try {
|
try {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, filePath);
|
||||||
|
|
||||||
await Deno.remove(join(rootPath, filePath));
|
await Deno.remove(join(rootPath, filePath));
|
||||||
|
|
||||||
return new Response(null, { status: 204 });
|
return new Response(null, { status: 204 });
|
||||||
@@ -90,6 +94,8 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
const body = contentLength === 0 ? new Blob([new Uint8Array([0])]).stream() : request.clone().body;
|
const body = contentLength === 0 ? new Blob([new Uint8Array([0])]).stream() : request.clone().body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, filePath);
|
||||||
|
|
||||||
const newFile = await Deno.open(join(rootPath, filePath), {
|
const newFile = await Deno.open(join(rootPath, filePath), {
|
||||||
create: true,
|
create: true,
|
||||||
write: true,
|
write: true,
|
||||||
@@ -110,6 +116,9 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
const newFilePath = request.headers.get('destination');
|
const newFilePath = request.headers.get('destination');
|
||||||
if (newFilePath) {
|
if (newFilePath) {
|
||||||
try {
|
try {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, filePath);
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, getProperDestinationPath(newFilePath));
|
||||||
|
|
||||||
await Deno.copyFile(join(rootPath, filePath), join(rootPath, getProperDestinationPath(newFilePath)));
|
await Deno.copyFile(join(rootPath, filePath), join(rootPath, getProperDestinationPath(newFilePath)));
|
||||||
return new Response('Created', { status: 201 });
|
return new Response('Created', { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -124,6 +133,9 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
const newFilePath = request.headers.get('destination');
|
const newFilePath = request.headers.get('destination');
|
||||||
if (newFilePath) {
|
if (newFilePath) {
|
||||||
try {
|
try {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, filePath);
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, getProperDestinationPath(newFilePath));
|
||||||
|
|
||||||
await Deno.rename(join(rootPath, filePath), join(rootPath, getProperDestinationPath(newFilePath)));
|
await Deno.rename(join(rootPath, filePath), join(rootPath, getProperDestinationPath(newFilePath)));
|
||||||
return new Response('Created', { status: 201 });
|
return new Response('Created', { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -134,6 +146,7 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
|
|
||||||
if (request.method === 'MKCOL') {
|
if (request.method === 'MKCOL') {
|
||||||
try {
|
try {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, filePath);
|
||||||
await Deno.mkdir(join(rootPath, filePath), { recursive: true });
|
await Deno.mkdir(join(rootPath, filePath), { recursive: true });
|
||||||
return new Response('Created', { status: 201 });
|
return new Response('Created', { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -209,6 +222,9 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
const parsedXml = parse(xml);
|
const parsedXml = parse(xml);
|
||||||
|
|
||||||
const properties = getPropertyNames(parsedXml);
|
const properties = getPropertyNames(parsedXml);
|
||||||
|
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, filePath);
|
||||||
|
|
||||||
const responseXml = await buildPropFindResponse(properties, rootPath, filePath, depth);
|
const responseXml = await buildPropFindResponse(properties, rootPath, filePath, depth);
|
||||||
|
|
||||||
return responseXml['D:multistatus']['D:response'].length === 0
|
return responseXml['D:multistatus']['D:response'].length === 0
|
||||||
|
|||||||
BIN
static/images/favicon-dark.png
Normal file
BIN
static/images/favicon-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
121
static/images/favicon-dark.svg
Normal file
121
static/images/favicon-dark.svg
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
viewBox="0 0 80 80"
|
||||||
|
class="iconLeft"
|
||||||
|
version="1.1"
|
||||||
|
id="svg3"
|
||||||
|
sodipodi:docname="favicon-dark.svg"
|
||||||
|
inkscape:export-filename="favicon-dark.png"
|
||||||
|
inkscape:export-xdpi="1228.8"
|
||||||
|
inkscape:export-ydpi="1228.8"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview3"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.92"
|
||||||
|
inkscape:cx="67.96875"
|
||||||
|
inkscape:cy="15.104167"
|
||||||
|
inkscape:window-width="1312"
|
||||||
|
inkscape:window-height="449"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="38"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="g5" />
|
||||||
|
<!---->
|
||||||
|
<defs
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
id="defs1">
|
||||||
|
<!---->
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#d27698e6-eb24-4a3f-96af-256d84d5b3ea"
|
||||||
|
id="linearGradient4"
|
||||||
|
gradientTransform="scale(1.3465159,0.74265739)"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="74.265739"
|
||||||
|
y2="0"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#d27698e6-eb24-4a3f-96af-256d84d5b3ea"
|
||||||
|
id="linearGradient5"
|
||||||
|
gradientTransform="scale(1.3465159,0.74265739)"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="74.265739"
|
||||||
|
y2="0"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
</defs>
|
||||||
|
<defs
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
id="defs2">
|
||||||
|
<!---->
|
||||||
|
</defs>
|
||||||
|
<defs
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
id="defs3">
|
||||||
|
<linearGradient
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
gradientTransform="rotate(25)"
|
||||||
|
id="d27698e6-eb24-4a3f-96af-256d84d5b3ea"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="1"
|
||||||
|
y2="0">
|
||||||
|
<stop
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
offset="0%"
|
||||||
|
stop-color="#51A4FB"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop2" />
|
||||||
|
<stop
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
offset="100%"
|
||||||
|
stop-color="#51A4FB"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop3" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
id="g5"
|
||||||
|
transform="translate(-24.999993,-125)">
|
||||||
|
<rect
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
fill="#e6f2fe"
|
||||||
|
x="24.999992"
|
||||||
|
y="125"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
class="logo-background-square"
|
||||||
|
id="rect1"
|
||||||
|
style="stroke-width:0.266666;fill:#02203a;fill-opacity:1" />
|
||||||
|
<g
|
||||||
|
data-v-fde0c5aa=""
|
||||||
|
id="f64f14cc-5390-468e-9cd6-ee0163dcbf09"
|
||||||
|
stroke="none"
|
||||||
|
fill="url(#d27698e6-eb24-4a3f-96af-256d84d5b3ea)"
|
||||||
|
transform="matrix(0.60062915,0,0,0.60062915,34.968536,148.43645)"
|
||||||
|
style="fill:url(#linearGradient5)">
|
||||||
|
<path
|
||||||
|
d="M 77.959,55.154 H 11.282 C 5.061,55.154 0,50.093 0,43.873 0,38.595 3.643,34.152 8.546,32.927 10.734,27.458 15.794,23.541 21.695,22.838 22.317,10.14 32.843,0 45.693,0 55.11,0 63.515,5.536 67.416,13.756 a 21.944,21.944 0 0 1 10.543,-2.682 c 12.153,0 22.041,9.887 22.041,22.04 0,12.153 -9.887,22.04 -22.041,22.04 z M 11.209,40.372 a 3.507,3.507 0 0 0 -3.429,3.501 3.504,3.504 0 0 0 3.501,3.501 h 66.678 c 7.863,0 14.26,-6.397 14.26,-14.26 0,-7.863 -6.396,-14.259 -14.26,-14.259 -3.683,0 -7.18,1.404 -9.847,3.952 L 63.016,27.678 61.612,20.77 C 60.083,13.243 53.388,7.78 45.693,7.78 c -8.958,0 -16.247,7.288 -16.247,16.246 0,0.728 0.054,1.483 0.159,2.246 l 0.73,5.287 -5.256,-0.923 a 8.59,8.59 0 0 0 -1.474,-0.132 c -4.002,0 -7.479,2.842 -8.267,6.757 l -0.65,3.227 -3.29,-0.106 z"
|
||||||
|
id="path3"
|
||||||
|
style="fill:url(#linearGradient4)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<!---->
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -3,20 +3,31 @@
|
|||||||
"short_name": "bewCloud",
|
"short_name": "bewCloud",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/images/logomark.svg",
|
"src": "/images/favicon-dark.svg",
|
||||||
"sizes": "1024x1024 any",
|
"sizes": "1024x1024 any",
|
||||||
"type": "image/svg+xml",
|
"type": "image/svg+xml",
|
||||||
"purpose": "monochrome maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/images/favicon.png",
|
"src": "/images/favicon-dark.png",
|
||||||
"sizes": "1024x1024",
|
"sizes": "1024x1024",
|
||||||
"type": "image/png"
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"theme_color": "#51a4fb",
|
"theme_color": "#1e293b",
|
||||||
"background_color": "#1e293b",
|
"background_color": "#1e293b",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "portrait"
|
"orientation": "portrait",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "News",
|
||||||
|
"url": "/news"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Expenses",
|
||||||
|
"url": "/expenses"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user