Refactor data handlers + misc fixes
This refactors the data handlers into a more standard/understood model-like architecture, to prepare for a new, more robust config system. It also fixes a problem with creating new Notes and uploading new Photos via the web interface (related to #58). Finally, it speeds up docker builds by sending in less files, which aren't necessary or will be built anyway. This is all in preparation to allow building #13 more robustly.
This commit is contained in:
@@ -8,3 +8,6 @@ render.yaml
|
|||||||
LICENSE
|
LICENSE
|
||||||
README.md
|
README.md
|
||||||
.env.sample
|
.env.sample
|
||||||
|
node_modules
|
||||||
|
_fresh
|
||||||
|
data-files
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM denoland/deno:ubuntu-2.3.1
|
FROM denoland/deno:ubuntu-2.3.3
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default function MainNotes({ initialDirectories, initialFiles, initialPat
|
|||||||
|
|
||||||
const requestBody = new FormData();
|
const requestBody = new FormData();
|
||||||
requestBody.set('parent_path', path.value);
|
requestBody.set('parent_path', path.value);
|
||||||
|
requestBody.set('path_in_view', path.value);
|
||||||
requestBody.set('name', `${newNoteName}.md`);
|
requestBody.set('name', `${newNoteName}.md`);
|
||||||
requestBody.set('contents', `# ${newNoteName}\n\nStart your new note!\n`);
|
requestBody.set('contents', `# ${newNoteName}\n\nStart your new note!\n`);
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default function MainPhotos({ initialDirectories, initialFiles, initialPa
|
|||||||
|
|
||||||
const requestBody = new FormData();
|
const requestBody = new FormData();
|
||||||
requestBody.set('parent_path', path.value);
|
requestBody.set('parent_path', path.value);
|
||||||
|
requestBody.set('path_in_view', path.value);
|
||||||
requestBody.set('name', chosenFile.name);
|
requestBody.set('name', chosenFile.name);
|
||||||
requestBody.set('contents', chosenFile);
|
requestBody.set('contents', chosenFile);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Database, { sql } from '/lib/interfaces/database.ts';
|
import Database, { sql } from '/lib/interfaces/database.ts';
|
||||||
import { NewsFeed } from '/lib/types.ts';
|
import { NewsFeed } from '/lib/types.ts';
|
||||||
import { concurrentPromises } from '/lib/utils/misc.ts';
|
import { concurrentPromises } from '/lib/utils/misc.ts';
|
||||||
import { crawlNewsFeed } from '/lib/data/news.ts';
|
import { FeedModel } from '/lib/models/news.ts';
|
||||||
|
|
||||||
const db = new Database();
|
const db = new Database();
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export async function fetchNewArticles(forceFetch = false) {
|
|||||||
|
|
||||||
console.info('Will crawl', feedsToCrawl.length, 'news feeds');
|
console.info('Will crawl', feedsToCrawl.length, 'news feeds');
|
||||||
|
|
||||||
await concurrentPromises(feedsToCrawl.map((newsFeed) => () => crawlNewsFeed(newsFeed)), 3);
|
await concurrentPromises(feedsToCrawl.map((newsFeed) => () => FeedModel.crawl(newsFeed)), 3);
|
||||||
|
|
||||||
console.info('Crawled', feedsToCrawl.length, 'news feeds');
|
console.info('Crawled', feedsToCrawl.length, 'news feeds');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'std/dotenv/load.ts';
|
|||||||
|
|
||||||
import { baseUrl, generateHash, isRunningLocally } from './utils/misc.ts';
|
import { baseUrl, generateHash, isRunningLocally } from './utils/misc.ts';
|
||||||
import { User, UserSession } from './types.ts';
|
import { User, UserSession } from './types.ts';
|
||||||
import { createUserSession, deleteUserSession, getUserByEmail, validateUserAndSession } from './data/user.ts';
|
import { UserModel, UserSessionModel, validateUserAndSession } from './models/user.ts';
|
||||||
import { isCookieDomainAllowed, isCookieDomainSecurityDisabled } from './config.ts';
|
import { isCookieDomainAllowed, isCookieDomainSecurityDisabled } from './config.ts';
|
||||||
|
|
||||||
const JWT_SECRET = Deno.env.get('JWT_SECRET') || '';
|
const JWT_SECRET = Deno.env.get('JWT_SECRET') || '';
|
||||||
@@ -102,7 +102,7 @@ async function getDataFromAuthorizationHeader(authorizationHeader: string) {
|
|||||||
|
|
||||||
const hashedPassword = await generateHash(`${basicAuthPassword}:${PASSWORD_SALT}`, 'SHA-256');
|
const hashedPassword = await generateHash(`${basicAuthPassword}:${PASSWORD_SALT}`, 'SHA-256');
|
||||||
|
|
||||||
const user = await getUserByEmail(basicAuthUsername);
|
const user = await UserModel.getByEmail(basicAuthUsername);
|
||||||
|
|
||||||
if (!user || (user.hashed_password !== hashedPassword && user.extra.dav_hashed_password !== hashedPassword)) {
|
if (!user || (user.hashed_password !== hashedPassword && user.extra.dav_hashed_password !== hashedPassword)) {
|
||||||
throw new Error('Email not found or invalid password.');
|
throw new Error('Email not found or invalid password.');
|
||||||
@@ -159,7 +159,7 @@ export async function logoutUser(request: Request) {
|
|||||||
const { session_id } = tokenData;
|
const { session_id } = tokenData;
|
||||||
|
|
||||||
// Delete user session
|
// Delete user session
|
||||||
await deleteUserSession(session_id);
|
await UserSessionModel.delete(session_id);
|
||||||
|
|
||||||
// Generate response with empty and expiring cookie
|
// Generate response with empty and expiring cookie
|
||||||
const cookie: Cookie = {
|
const cookie: Cookie = {
|
||||||
@@ -210,7 +210,7 @@ export async function createSessionCookie(
|
|||||||
response: Response,
|
response: Response,
|
||||||
isShortLived = false,
|
isShortLived = false,
|
||||||
) {
|
) {
|
||||||
const newSession = await createUserSession(user, isShortLived);
|
const newSession = await UserSessionModel.create(user, isShortLived);
|
||||||
|
|
||||||
// Generate response with session cookie
|
// Generate response with session cookie
|
||||||
const token = await generateToken({ user_id: user.id, session_id: newSession.id });
|
const token = await generateToken({ user_id: user.id, session_id: newSession.id });
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'std/dotenv/load.ts';
|
import 'std/dotenv/load.ts';
|
||||||
|
|
||||||
import { isThereAnAdmin } from './data/user.ts';
|
import { UserModel } from './models/user.ts';
|
||||||
|
|
||||||
export async function isSignupAllowed() {
|
export async function isSignupAllowed() {
|
||||||
const areSignupsAllowed = Deno.env.get('CONFIG_ALLOW_SIGNUPS') === 'true';
|
const areSignupsAllowed = Deno.env.get('CONFIG_ALLOW_SIGNUPS') === 'true';
|
||||||
|
|
||||||
const areThereAdmins = await isThereAnAdmin();
|
const areThereAdmins = await UserModel.isThereAnAdmin();
|
||||||
|
|
||||||
if (areSignupsAllowed || !areThereAdmins) {
|
if (areSignupsAllowed || !areThereAdmins) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import Database, { sql } from '/lib/interfaces/database.ts';
|
|
||||||
import { Dashboard } from '/lib/types.ts';
|
|
||||||
|
|
||||||
const db = new Database();
|
|
||||||
|
|
||||||
export async function getDashboardByUserId(userId: string) {
|
|
||||||
const dashboard = (await db.query<Dashboard>(sql`SELECT * FROM "bewcloud_dashboards" WHERE "user_id" = $1 LIMIT 1`, [
|
|
||||||
userId,
|
|
||||||
]))[0];
|
|
||||||
|
|
||||||
return dashboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createDashboard(userId: string) {
|
|
||||||
const data: Dashboard['data'] = { links: [], notes: '' };
|
|
||||||
|
|
||||||
const newDashboard = (await db.query<Dashboard>(
|
|
||||||
sql`INSERT INTO "bewcloud_dashboards" (
|
|
||||||
"user_id",
|
|
||||||
"data"
|
|
||||||
) VALUES ($1, $2)
|
|
||||||
RETURNING *`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
JSON.stringify(data),
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
return newDashboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateDashboard(dashboard: Dashboard) {
|
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_dashboards" SET
|
|
||||||
"data" = $2
|
|
||||||
WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
dashboard.id,
|
|
||||||
JSON.stringify(dashboard.data),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,469 +0,0 @@
|
|||||||
import Database, { sql } from '/lib/interfaces/database.ts';
|
|
||||||
import Locker from '/lib/interfaces/locker.ts';
|
|
||||||
import { Budget, Expense } from '/lib/types.ts';
|
|
||||||
|
|
||||||
const db = new Database();
|
|
||||||
|
|
||||||
export async function getBudgets(
|
|
||||||
userId: string,
|
|
||||||
month: string,
|
|
||||||
{ skipRecalculation = false }: { skipRecalculation?: boolean } = {},
|
|
||||||
) {
|
|
||||||
if (!skipRecalculation) {
|
|
||||||
await recalculateMonthBudgets(userId, month);
|
|
||||||
}
|
|
||||||
|
|
||||||
const budgets = await db.query<Budget>(
|
|
||||||
sql`SELECT * FROM "bewcloud_budgets" WHERE "user_id" = $1 AND "month" = $2 ORDER BY cast("extra"->>'availableValue' as numeric) DESC, "value" DESC, "name" ASC`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
month,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
return budgets.map((budget) => ({
|
|
||||||
...budget,
|
|
||||||
value: Number(budget.value),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBudgetByName(userId: string, month: string, name: string) {
|
|
||||||
const budget = (await db.query<Budget>(
|
|
||||||
sql`SELECT * FROM "bewcloud_budgets" WHERE "user_id" = $1 AND "month" = $2 AND LOWER("name") = LOWER($3)`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
month,
|
|
||||||
name,
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
if (!budget) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
return {
|
|
||||||
...budget,
|
|
||||||
value: Number(budget.value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBudgetById(userId: string, id: string) {
|
|
||||||
const budget = (await db.query<Budget>(
|
|
||||||
sql`SELECT * FROM "bewcloud_budgets" WHERE "user_id" = $1 AND "id" = $2`,
|
|
||||||
[userId, id],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
if (!budget) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
return {
|
|
||||||
...budget,
|
|
||||||
value: Number(budget.value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllBudgetsForExport(
|
|
||||||
userId: string,
|
|
||||||
): Promise<(Omit<Budget, 'id' | 'user_id' | 'created_at' | 'extra'> & { extra: Record<never, never> })[]> {
|
|
||||||
const budgets = await db.query<Budget>(
|
|
||||||
sql`SELECT * FROM "bewcloud_budgets" WHERE "user_id" = $1 ORDER BY "month" DESC, "name" ASC`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return budgets.map((budget) => ({
|
|
||||||
name: budget.name,
|
|
||||||
month: budget.month,
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
value: Number(budget.value),
|
|
||||||
extra: {},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getExpenses(userId: string, month: string) {
|
|
||||||
const expenses = await db.query<Expense>(
|
|
||||||
sql`SELECT * FROM "bewcloud_expenses" WHERE "user_id" = $1 AND "date" >= $2 AND "date" <= $3 ORDER BY "date" DESC, "created_at" DESC`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
`${month}-01`,
|
|
||||||
`${month}-31`,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
return expenses.map((expense) => ({
|
|
||||||
...expense,
|
|
||||||
cost: Number(expense.cost),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getExpenseByName(userId: string, name: string) {
|
|
||||||
const expense = (await db.query<Expense>(
|
|
||||||
sql`SELECT * FROM "bewcloud_expenses" WHERE "user_id" = $1 AND LOWER("description") = LOWER($2) ORDER BY "date" DESC, "created_at" DESC`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
name,
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
if (!expense) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
return {
|
|
||||||
...expense,
|
|
||||||
cost: Number(expense.cost),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getExpenseSuggestions(userId: string, name: string) {
|
|
||||||
const expenses = await db.query<Pick<Expense, 'description'>>(
|
|
||||||
sql`SELECT "description" FROM "bewcloud_expenses" WHERE "user_id" = $1 AND LOWER("description") ILIKE LOWER($2) GROUP BY "description" ORDER BY LENGTH("description") ASC, "description" ASC`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
`%${name}%`,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return expenses.map((expense) => expense.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllExpensesForExport(
|
|
||||||
userId: string,
|
|
||||||
): Promise<(Omit<Expense, 'id' | 'user_id' | 'created_at' | 'extra'> & { extra: Record<never, never> })[]> {
|
|
||||||
const expenses = await db.query<Expense>(
|
|
||||||
sql`SELECT * FROM "bewcloud_expenses" WHERE "user_id" = $1 ORDER BY "date" DESC, "created_at" DESC`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return expenses.map((expense) => ({
|
|
||||||
description: expense.description,
|
|
||||||
budget: expense.budget,
|
|
||||||
date: expense.date,
|
|
||||||
is_recurring: expense.is_recurring,
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
cost: Number(expense.cost),
|
|
||||||
extra: {},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getExpenseById(userId: string, id: string) {
|
|
||||||
const expense = (await db.query<Expense>(
|
|
||||||
sql`SELECT * FROM "bewcloud_expenses" WHERE "user_id" = $1 AND "id" = $2`,
|
|
||||||
[userId, id],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
if (!expense) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
return {
|
|
||||||
...expense,
|
|
||||||
cost: Number(expense.cost),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createBudget(userId: string, name: string, month: string, value: number) {
|
|
||||||
const extra: Budget['extra'] = {
|
|
||||||
usedValue: 0,
|
|
||||||
availableValue: value,
|
|
||||||
};
|
|
||||||
|
|
||||||
const newBudget = (await db.query<Budget>(
|
|
||||||
sql`INSERT INTO "bewcloud_budgets" (
|
|
||||||
"user_id",
|
|
||||||
"name",
|
|
||||||
"month",
|
|
||||||
"value",
|
|
||||||
"extra"
|
|
||||||
) VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING *`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
name,
|
|
||||||
month,
|
|
||||||
value,
|
|
||||||
JSON.stringify(extra),
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
return {
|
|
||||||
...newBudget,
|
|
||||||
value: Number(newBudget.value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateBudget(
|
|
||||||
budget: Budget,
|
|
||||||
{ skipRecalculation = false }: { skipRecalculation?: boolean } = {},
|
|
||||||
) {
|
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_budgets" SET
|
|
||||||
"name" = $2,
|
|
||||||
"month" = $3,
|
|
||||||
"value" = $4,
|
|
||||||
"extra" = $5
|
|
||||||
WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
budget.id,
|
|
||||||
budget.name,
|
|
||||||
budget.month,
|
|
||||||
budget.value,
|
|
||||||
JSON.stringify(budget.extra),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!skipRecalculation) {
|
|
||||||
await recalculateMonthBudgets(budget.user_id, budget.month);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteBudget(userId: string, id: string) {
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_budgets" WHERE "id" = $1 AND "user_id" = $2`,
|
|
||||||
[
|
|
||||||
id,
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteAllBudgetsAndExpenses(userId: string) {
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_expenses" WHERE "user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_budgets" WHERE "user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createExpense(
|
|
||||||
userId: string,
|
|
||||||
cost: number,
|
|
||||||
description: string,
|
|
||||||
budget: string,
|
|
||||||
date: string,
|
|
||||||
is_recurring: boolean,
|
|
||||||
{ skipRecalculation = false, skipBudgetMatching = false, skipBudgetCreation = false }: {
|
|
||||||
skipRecalculation?: boolean;
|
|
||||||
skipBudgetMatching?: boolean;
|
|
||||||
skipBudgetCreation?: boolean;
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
const extra: Expense['extra'] = {};
|
|
||||||
|
|
||||||
if (!budget.trim()) {
|
|
||||||
budget = 'Misc';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match budget to an existing expense "by default"
|
|
||||||
if (!skipBudgetMatching && budget === 'Misc') {
|
|
||||||
const existingExpense = await getExpenseByName(userId, description);
|
|
||||||
|
|
||||||
if (existingExpense) {
|
|
||||||
budget = existingExpense.budget;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipBudgetCreation) {
|
|
||||||
const existingBudgetInMonth = await getBudgetByName(userId, date.substring(0, 7), budget);
|
|
||||||
|
|
||||||
if (!existingBudgetInMonth) {
|
|
||||||
await createBudget(userId, budget, date.substring(0, 7), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newExpense = (await db.query<Expense>(
|
|
||||||
sql`INSERT INTO "bewcloud_expenses" (
|
|
||||||
"user_id",
|
|
||||||
"cost",
|
|
||||||
"description",
|
|
||||||
"budget",
|
|
||||||
"date",
|
|
||||||
"is_recurring",
|
|
||||||
"extra"
|
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
||||||
RETURNING *`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
cost,
|
|
||||||
description,
|
|
||||||
budget,
|
|
||||||
date,
|
|
||||||
is_recurring,
|
|
||||||
JSON.stringify(extra),
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
if (!skipRecalculation) {
|
|
||||||
await recalculateMonthBudgets(userId, date.substring(0, 7));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numeric values come as strings, so we need to convert them to numbers
|
|
||||||
return {
|
|
||||||
...newExpense,
|
|
||||||
cost: Number(newExpense.cost),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateExpense(expense: Expense) {
|
|
||||||
const existingBudgetInMonth = await getBudgetByName(expense.user_id, expense.date.substring(0, 7), expense.budget);
|
|
||||||
|
|
||||||
if (!existingBudgetInMonth) {
|
|
||||||
await createBudget(expense.user_id, expense.budget, expense.date.substring(0, 7), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_expenses" SET
|
|
||||||
"cost" = $2,
|
|
||||||
"description" = $3,
|
|
||||||
"budget" = $4,
|
|
||||||
"date" = $5,
|
|
||||||
"is_recurring" = $6,
|
|
||||||
"extra" = $7
|
|
||||||
WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
expense.id,
|
|
||||||
expense.cost,
|
|
||||||
expense.description,
|
|
||||||
expense.budget,
|
|
||||||
expense.date,
|
|
||||||
expense.is_recurring,
|
|
||||||
JSON.stringify(expense.extra),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await recalculateMonthBudgets(expense.user_id, expense.date.substring(0, 7));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteExpense(userId: string, id: string) {
|
|
||||||
const expense = await getExpenseById(userId, id);
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_expenses" WHERE "id" = $1 AND "user_id" = $2`,
|
|
||||||
[
|
|
||||||
id,
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await recalculateMonthBudgets(userId, expense!.date.substring(0, 7));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMonthlyBudgetsAndExpenses(userId: string, month: string) {
|
|
||||||
const lock = new Locker(`expenses:${userId}:${month}`);
|
|
||||||
|
|
||||||
await lock.acquire();
|
|
||||||
|
|
||||||
let addedBudgetsCount = 0;
|
|
||||||
let addedExpensesCount = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Confirm there are no budgets or expenses for this month
|
|
||||||
const monthBudgets = await getBudgets(userId, month, { skipRecalculation: true });
|
|
||||||
const monthExpenses = await getExpenses(userId, month);
|
|
||||||
|
|
||||||
if (monthBudgets.length > 0 || monthExpenses.length > 0) {
|
|
||||||
throw new Error('Budgets and expenses already exist for this month!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the previous month's budgets, to copy over
|
|
||||||
const previousMonthDate = new Date(month);
|
|
||||||
previousMonthDate.setMonth(previousMonthDate.getMonth() - 1);
|
|
||||||
|
|
||||||
const previousMonth = previousMonthDate.toISOString().substring(0, 7);
|
|
||||||
|
|
||||||
const budgets = await getBudgets(userId, previousMonth, { skipRecalculation: true });
|
|
||||||
|
|
||||||
for (const budget of budgets) {
|
|
||||||
await createBudget(userId, budget.name, month, budget.value);
|
|
||||||
|
|
||||||
addedBudgetsCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the recurring expenses for the previous month, to copy over
|
|
||||||
const recurringExpenses = (await getExpenses(userId, previousMonth)).filter((expense) => expense.is_recurring);
|
|
||||||
|
|
||||||
for (const expense of recurringExpenses) {
|
|
||||||
await createExpense(
|
|
||||||
userId,
|
|
||||||
expense.cost,
|
|
||||||
expense.description,
|
|
||||||
expense.budget,
|
|
||||||
expense.date.replace(previousMonth, month),
|
|
||||||
expense.is_recurring,
|
|
||||||
{ skipRecalculation: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
addedExpensesCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info(`Added ${addedBudgetsCount} new budgets and ${addedExpensesCount} new expenses for ${month}`);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
} catch (error) {
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function recalculateMonthBudgets(userId: string, month: string) {
|
|
||||||
const lock = new Locker(`expenses:${userId}:${month}`);
|
|
||||||
|
|
||||||
await lock.acquire();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const budgets = await getBudgets(userId, month, { skipRecalculation: true });
|
|
||||||
const expenses = await getExpenses(userId, month);
|
|
||||||
|
|
||||||
// Calculate total expenses for each budget
|
|
||||||
const budgetExpenses = new Map<string, number>();
|
|
||||||
for (const expense of expenses) {
|
|
||||||
const currentTotal = Number(budgetExpenses.get(expense.budget) || 0);
|
|
||||||
budgetExpenses.set(expense.budget, Number(currentTotal + expense.cost));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update each budget with new calculations
|
|
||||||
for (const budget of budgets) {
|
|
||||||
const usedValue = Number(budgetExpenses.get(budget.name) || 0);
|
|
||||||
const availableValue = Number(budget.value - usedValue);
|
|
||||||
|
|
||||||
const updatedBudget: Budget = {
|
|
||||||
...budget,
|
|
||||||
extra: {
|
|
||||||
...budget.extra,
|
|
||||||
usedValue,
|
|
||||||
availableValue,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (budget.extra.usedValue !== usedValue || budget.extra.availableValue !== availableValue) {
|
|
||||||
await updateBudget(updatedBudget, { skipRecalculation: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
} catch (error) {
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,504 +0,0 @@
|
|||||||
import { join } from 'std/path/join.ts';
|
|
||||||
import { resolve } from 'std/path/resolve.ts';
|
|
||||||
import { lookup } from 'mrmime';
|
|
||||||
|
|
||||||
import { getFilesRootPath } from '/lib/config.ts';
|
|
||||||
import { Directory, DirectoryFile } from '/lib/types.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[]> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
|
||||||
|
|
||||||
const directories: Directory[] = [];
|
|
||||||
|
|
||||||
const directoryEntries = (await getPathEntries(userId, path)).filter((entry) => entry.isDirectory || entry.isSymlink);
|
|
||||||
|
|
||||||
for (const entry of directoryEntries) {
|
|
||||||
const stat = await Deno.stat(join(rootPath, entry.name));
|
|
||||||
|
|
||||||
const directory: Directory = {
|
|
||||||
user_id: userId,
|
|
||||||
parent_path: path,
|
|
||||||
directory_name: entry.name,
|
|
||||||
has_write_access: true,
|
|
||||||
size_in_bytes: stat.size,
|
|
||||||
updated_at: stat.mtime || new Date(),
|
|
||||||
created_at: stat.birthtime || new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
directories.push(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
directories.sort(sortDirectoriesByName);
|
|
||||||
|
|
||||||
return directories;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFiles(userId: string, path: string): Promise<DirectoryFile[]> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
|
||||||
|
|
||||||
const files: DirectoryFile[] = [];
|
|
||||||
|
|
||||||
const fileEntries = (await getPathEntries(userId, path)).filter((entry) => entry.isFile);
|
|
||||||
|
|
||||||
for (const entry of fileEntries) {
|
|
||||||
const stat = await Deno.stat(join(rootPath, entry.name));
|
|
||||||
|
|
||||||
const file: DirectoryFile = {
|
|
||||||
user_id: userId,
|
|
||||||
parent_path: path,
|
|
||||||
file_name: entry.name,
|
|
||||||
has_write_access: true,
|
|
||||||
size_in_bytes: stat.size,
|
|
||||||
updated_at: stat.mtime || new Date(),
|
|
||||||
created_at: stat.birthtime || new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
files.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
files.sort(sortFilesByName);
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPathEntries(userId: string, path: string): Promise<Deno.DirEntry[]> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
|
||||||
|
|
||||||
// Ensure the user directory exists
|
|
||||||
if (path === '/') {
|
|
||||||
try {
|
|
||||||
await Deno.stat(rootPath);
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as Error).toString().includes('NotFound')) {
|
|
||||||
await Deno.mkdir(join(rootPath, TRASH_PATH), { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the Notes or Photos directories exist, if being requested
|
|
||||||
if (path === '/Notes/' || path === '/Photos/') {
|
|
||||||
try {
|
|
||||||
await Deno.stat(rootPath);
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as Error).toString().includes('NotFound')) {
|
|
||||||
await Deno.mkdir(rootPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries: Deno.DirEntry[] = [];
|
|
||||||
|
|
||||||
for await (const dirEntry of Deno.readDir(rootPath)) {
|
|
||||||
entries.push(dirEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.sort(sortEntriesByName);
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createDirectory(userId: string, path: string, name: string): Promise<boolean> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Deno.mkdir(join(rootPath, name), { recursive: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renameDirectoryOrFile(
|
|
||||||
userId: string,
|
|
||||||
oldPath: string,
|
|
||||||
newPath: string,
|
|
||||||
oldName: string,
|
|
||||||
newName: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(oldPath, oldName));
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(newPath, newName));
|
|
||||||
|
|
||||||
const oldRootPath = join(getFilesRootPath(), userId, oldPath);
|
|
||||||
const newRootPath = join(getFilesRootPath(), userId, newPath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Deno.rename(join(oldRootPath, oldName), join(newRootPath, newName));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteDirectoryOrFile(userId: string, path: string, name: string): Promise<boolean> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (path.startsWith(TRASH_PATH)) {
|
|
||||||
await Deno.remove(join(rootPath, name), { recursive: true });
|
|
||||||
} else {
|
|
||||||
const trashPath = join(getFilesRootPath(), userId, TRASH_PATH);
|
|
||||||
await Deno.rename(join(rootPath, name), join(trashPath, name));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createFile(
|
|
||||||
userId: string,
|
|
||||||
path: string,
|
|
||||||
name: string,
|
|
||||||
contents: string | ArrayBuffer,
|
|
||||||
): Promise<boolean> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ensure the directory exist, if being requested
|
|
||||||
try {
|
|
||||||
await Deno.stat(rootPath);
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as Error).toString().includes('NotFound')) {
|
|
||||||
await Deno.mkdir(rootPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof contents === 'string') {
|
|
||||||
await Deno.writeTextFile(join(rootPath, name), contents, { append: false, createNew: true });
|
|
||||||
} else {
|
|
||||||
await Deno.writeFile(join(rootPath, name), new Uint8Array(contents), { append: false, createNew: true });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateFile(
|
|
||||||
userId: string,
|
|
||||||
path: string,
|
|
||||||
name: string,
|
|
||||||
contents: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Deno.writeTextFile(join(rootPath, name), contents, { append: false, createNew: false });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFile(
|
|
||||||
userId: string,
|
|
||||||
path: string,
|
|
||||||
name?: string,
|
|
||||||
): Promise<{ success: boolean; contents?: Uint8Array; contentType?: string; byteSize?: number }> {
|
|
||||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name || ''));
|
|
||||||
|
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stat = await Deno.stat(join(rootPath, name || ''));
|
|
||||||
|
|
||||||
if (stat) {
|
|
||||||
const contents = await Deno.readFile(join(rootPath, name || ''));
|
|
||||||
|
|
||||||
const extension = (name || path).split('.').slice(-1).join('').toLowerCase();
|
|
||||||
|
|
||||||
const contentType = lookup(extension) || 'application/octet-stream';
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
contents,
|
|
||||||
contentType,
|
|
||||||
byteSize: stat.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function searchFilesAndDirectories(
|
|
||||||
userId: string,
|
|
||||||
searchTerm: string,
|
|
||||||
): Promise<{ success: boolean; directories: Directory[]; files: DirectoryFile[] }> {
|
|
||||||
const directoryNamesResult = await searchDirectoryNames(userId, searchTerm);
|
|
||||||
const fileNamesResult = await searchFileNames(userId, searchTerm);
|
|
||||||
const fileContentsResult = await searchFileContents(userId, searchTerm);
|
|
||||||
|
|
||||||
const success = directoryNamesResult.success && fileNamesResult.success && fileContentsResult.success;
|
|
||||||
|
|
||||||
const directories = [...directoryNamesResult.directories];
|
|
||||||
directories.sort(sortDirectoriesByName);
|
|
||||||
|
|
||||||
const files = [...fileNamesResult.files, ...fileContentsResult.files];
|
|
||||||
files.sort(sortFilesByName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success,
|
|
||||||
directories,
|
|
||||||
files,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchDirectoryNames(
|
|
||||||
userId: string,
|
|
||||||
searchTerm: string,
|
|
||||||
): Promise<{ success: boolean; directories: Directory[] }> {
|
|
||||||
const rootPath = join(getFilesRootPath(), userId);
|
|
||||||
|
|
||||||
const directories: Directory[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
|
||||||
|
|
||||||
const command = new Deno.Command(`find`, {
|
|
||||||
args: [
|
|
||||||
`.`, // proper cwd is sent below
|
|
||||||
`-type`,
|
|
||||||
`d,l`, // directories and symbolic links
|
|
||||||
`-iname`,
|
|
||||||
`*${searchTerm}*`,
|
|
||||||
],
|
|
||||||
cwd: rootPath,
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { code, stdout, stderr } = await command.output();
|
|
||||||
|
|
||||||
if (commandTimeout) {
|
|
||||||
clearTimeout(commandTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code !== 0) {
|
|
||||||
if (stderr) {
|
|
||||||
throw new Error(new TextDecoder().decode(stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown error running "find"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = new TextDecoder().decode(stdout);
|
|
||||||
const matchingDirectories = output.split('\n').map((directoryPath) => directoryPath.trim()).filter(Boolean);
|
|
||||||
|
|
||||||
for (const relativeDirectoryPath of matchingDirectories) {
|
|
||||||
const stat = await Deno.stat(join(rootPath, relativeDirectoryPath));
|
|
||||||
let parentPath = `/${relativeDirectoryPath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
|
||||||
const directoryName = relativeDirectoryPath.split('/').pop()!;
|
|
||||||
|
|
||||||
if (parentPath === '//') {
|
|
||||||
parentPath = '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
const directory: Directory = {
|
|
||||||
user_id: userId,
|
|
||||||
parent_path: parentPath,
|
|
||||||
directory_name: directoryName,
|
|
||||||
has_write_access: true,
|
|
||||||
size_in_bytes: stat.size,
|
|
||||||
updated_at: stat.mtime || new Date(),
|
|
||||||
created_at: stat.birthtime || new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
directories.push(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, directories };
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, directories };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchFileNames(
|
|
||||||
userId: string,
|
|
||||||
searchTerm: string,
|
|
||||||
): Promise<{ success: boolean; files: DirectoryFile[] }> {
|
|
||||||
const rootPath = join(getFilesRootPath(), userId);
|
|
||||||
|
|
||||||
const files: DirectoryFile[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
|
||||||
|
|
||||||
const command = new Deno.Command(`find`, {
|
|
||||||
args: [
|
|
||||||
`.`, // proper cwd is sent below
|
|
||||||
`-type`,
|
|
||||||
`f`,
|
|
||||||
`-iname`,
|
|
||||||
`*${searchTerm}*`,
|
|
||||||
],
|
|
||||||
cwd: rootPath,
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { code, stdout, stderr } = await command.output();
|
|
||||||
|
|
||||||
if (commandTimeout) {
|
|
||||||
clearTimeout(commandTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code !== 0) {
|
|
||||||
if (stderr) {
|
|
||||||
throw new Error(new TextDecoder().decode(stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown error running "find"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = new TextDecoder().decode(stdout);
|
|
||||||
const matchingFiles = output.split('\n').map((filePath) => filePath.trim()).filter(Boolean);
|
|
||||||
|
|
||||||
for (const relativeFilePath of matchingFiles) {
|
|
||||||
const stat = await Deno.stat(join(rootPath, relativeFilePath));
|
|
||||||
let parentPath = `/${relativeFilePath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
|
||||||
const fileName = relativeFilePath.split('/').pop()!;
|
|
||||||
|
|
||||||
if (parentPath === '//') {
|
|
||||||
parentPath = '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
const file: DirectoryFile = {
|
|
||||||
user_id: userId,
|
|
||||||
parent_path: parentPath,
|
|
||||||
file_name: fileName,
|
|
||||||
has_write_access: true,
|
|
||||||
size_in_bytes: stat.size,
|
|
||||||
updated_at: stat.mtime || new Date(),
|
|
||||||
created_at: stat.birthtime || new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
files.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, files };
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, files };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchFileContents(
|
|
||||||
userId: string,
|
|
||||||
searchTerm: string,
|
|
||||||
): Promise<{ success: boolean; files: DirectoryFile[] }> {
|
|
||||||
const rootPath = join(getFilesRootPath(), userId);
|
|
||||||
|
|
||||||
const files: DirectoryFile[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
|
||||||
|
|
||||||
const command = new Deno.Command(`grep`, {
|
|
||||||
args: [
|
|
||||||
`-rHisl`,
|
|
||||||
`${searchTerm}`,
|
|
||||||
`.`, // proper cwd is sent below
|
|
||||||
],
|
|
||||||
cwd: rootPath,
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { code, stdout, stderr } = await command.output();
|
|
||||||
|
|
||||||
if (commandTimeout) {
|
|
||||||
clearTimeout(commandTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code > 1) {
|
|
||||||
if (stderr) {
|
|
||||||
throw new Error(new TextDecoder().decode(stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown error running "grep"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = new TextDecoder().decode(stdout);
|
|
||||||
const matchingFiles = output.split('\n').map((filePath) => filePath.trim()).filter(Boolean);
|
|
||||||
|
|
||||||
for (const relativeFilePath of matchingFiles) {
|
|
||||||
const stat = await Deno.stat(join(rootPath, relativeFilePath));
|
|
||||||
let parentPath = `/${relativeFilePath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
|
||||||
const fileName = relativeFilePath.split('/').pop()!;
|
|
||||||
|
|
||||||
if (parentPath === '//') {
|
|
||||||
parentPath = '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
const file: DirectoryFile = {
|
|
||||||
user_id: userId,
|
|
||||||
parent_path: parentPath,
|
|
||||||
file_name: fileName,
|
|
||||||
has_write_access: true,
|
|
||||||
size_in_bytes: stat.size,
|
|
||||||
updated_at: stat.mtime || new Date(),
|
|
||||||
created_at: stat.birthtime || new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
files.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, files };
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, files };
|
|
||||||
}
|
|
||||||
311
lib/data/news.ts
311
lib/data/news.ts
@@ -1,311 +0,0 @@
|
|||||||
import { Feed } from 'https://deno.land/x/rss@1.0.0/mod.ts';
|
|
||||||
|
|
||||||
import Database, { sql } from '/lib/interfaces/database.ts';
|
|
||||||
import Locker from '/lib/interfaces/locker.ts';
|
|
||||||
import { NewsFeed, NewsFeedArticle } from '/lib/types.ts';
|
|
||||||
import {
|
|
||||||
findFeedInUrl,
|
|
||||||
getArticleUrl,
|
|
||||||
getFeedInfo,
|
|
||||||
JsonFeed,
|
|
||||||
parseTextFromHtml,
|
|
||||||
parseUrl,
|
|
||||||
parseUrlAsGooglebot,
|
|
||||||
parseUrlWithProxy,
|
|
||||||
} from '/lib/feed.ts';
|
|
||||||
|
|
||||||
const db = new Database();
|
|
||||||
|
|
||||||
export async function getNewsFeeds(userId: string) {
|
|
||||||
const newsFeeds = await db.query<NewsFeed>(sql`SELECT * FROM "bewcloud_news_feeds" WHERE "user_id" = $1`, [
|
|
||||||
userId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return newsFeeds;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNewsFeed(id: string, userId: string) {
|
|
||||||
const newsFeeds = await db.query<NewsFeed>(
|
|
||||||
sql`SELECT * FROM "bewcloud_news_feeds" WHERE "id" = $1 AND "user_id" = $2 LIMIT 1`,
|
|
||||||
[
|
|
||||||
id,
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return newsFeeds[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNewsArticles(userId: string) {
|
|
||||||
const articles = await db.query<NewsFeedArticle>(
|
|
||||||
sql`SELECT * FROM "bewcloud_news_feed_articles" WHERE "user_id" = $1 ORDER BY "article_date" DESC`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return articles;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNewsArticlesByFeedId(feedId: string) {
|
|
||||||
const articles = await db.query<NewsFeedArticle>(
|
|
||||||
sql`SELECT * FROM "bewcloud_news_feed_articles" WHERE "feed_id" = $1 ORDER BY "article_date" DESC`,
|
|
||||||
[
|
|
||||||
feedId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return articles;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNewsArticle(id: string, userId: string) {
|
|
||||||
const articles = await db.query<NewsFeedArticle>(
|
|
||||||
sql`SELECT * FROM "bewcloud_news_feed_articles" WHERE "id" = $1 AND "user_id" = $2 LIMIT 1`,
|
|
||||||
[
|
|
||||||
id,
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return articles[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createNewsFeed(userId: string, feedUrl: string) {
|
|
||||||
const extra: NewsFeed['extra'] = {};
|
|
||||||
|
|
||||||
const newNewsFeed = (await db.query<NewsFeed>(
|
|
||||||
sql`INSERT INTO "bewcloud_news_feeds" (
|
|
||||||
"user_id",
|
|
||||||
"feed_url",
|
|
||||||
"extra"
|
|
||||||
) VALUES ($1, $2, $3)
|
|
||||||
RETURNING *`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
feedUrl,
|
|
||||||
JSON.stringify(extra),
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
return newNewsFeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateNewsFeed(newsFeed: NewsFeed) {
|
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_news_feeds" SET
|
|
||||||
"feed_url" = $2,
|
|
||||||
"last_crawled_at" = $3,
|
|
||||||
"extra" = $4
|
|
||||||
WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
newsFeed.id,
|
|
||||||
newsFeed.feed_url,
|
|
||||||
newsFeed.last_crawled_at,
|
|
||||||
JSON.stringify(newsFeed.extra),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteNewsFeed(id: string, userId: string) {
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_news_feed_articles" WHERE "feed_id" = $1 AND "user_id" = $2`,
|
|
||||||
[
|
|
||||||
id,
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_news_feeds" WHERE "id" = $1 AND "user_id" = $2`,
|
|
||||||
[
|
|
||||||
id,
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createsNewsArticle(
|
|
||||||
userId: string,
|
|
||||||
feedId: string,
|
|
||||||
article: Omit<NewsFeedArticle, 'id' | 'user_id' | 'feed_id' | 'extra' | 'is_read' | 'created_at'>,
|
|
||||||
) {
|
|
||||||
const extra: NewsFeedArticle['extra'] = {};
|
|
||||||
|
|
||||||
const newNewsArticle = (await db.query<NewsFeedArticle>(
|
|
||||||
sql`INSERT INTO "bewcloud_news_feed_articles" (
|
|
||||||
"user_id",
|
|
||||||
"feed_id",
|
|
||||||
"article_url",
|
|
||||||
"article_title",
|
|
||||||
"article_summary",
|
|
||||||
"article_date",
|
|
||||||
"extra"
|
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
||||||
RETURNING *`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
feedId,
|
|
||||||
article.article_url,
|
|
||||||
article.article_title,
|
|
||||||
article.article_summary,
|
|
||||||
article.article_date,
|
|
||||||
JSON.stringify(extra),
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
return newNewsArticle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateNewsArticle(article: NewsFeedArticle) {
|
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_news_feed_articles" SET
|
|
||||||
"is_read" = $2,
|
|
||||||
"extra" = $3
|
|
||||||
WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
article.id,
|
|
||||||
article.is_read,
|
|
||||||
JSON.stringify(article.extra),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function markAllArticlesRead(userId: string) {
|
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_news_feed_articles" SET
|
|
||||||
"is_read" = TRUE
|
|
||||||
WHERE "user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchNewsArticles(newsFeed: NewsFeed): Promise<Feed['entries'] | JsonFeed['items']> {
|
|
||||||
try {
|
|
||||||
if (!newsFeed.extra.title || !newsFeed.extra.feed_type || !newsFeed.extra.crawl_type) {
|
|
||||||
throw new Error('Invalid News Feed!');
|
|
||||||
}
|
|
||||||
|
|
||||||
let feed: JsonFeed | Feed | null = null;
|
|
||||||
|
|
||||||
if (newsFeed.extra.crawl_type === 'direct') {
|
|
||||||
feed = await parseUrl(newsFeed.feed_url);
|
|
||||||
} else if (newsFeed.extra.crawl_type === 'googlebot') {
|
|
||||||
feed = await parseUrlAsGooglebot(newsFeed.feed_url);
|
|
||||||
} else if (newsFeed.extra.crawl_type === 'proxy') {
|
|
||||||
feed = await parseUrlWithProxy(newsFeed.feed_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (feed as Feed)?.entries || (feed as JsonFeed)?.items || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed parsing feed to get articles', newsFeed.feed_url);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
type FeedArticle = Feed['entries'][number];
|
|
||||||
type JsonFeedArticle = JsonFeed['items'][number];
|
|
||||||
|
|
||||||
const MAX_ARTICLES_CRAWLED_PER_RUN = 10;
|
|
||||||
|
|
||||||
export async function crawlNewsFeed(newsFeed: NewsFeed) {
|
|
||||||
const lock = new Locker(`feeds:${newsFeed.id}`);
|
|
||||||
|
|
||||||
await lock.acquire();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!newsFeed.extra.title || !newsFeed.extra.feed_type || !newsFeed.extra.crawl_type) {
|
|
||||||
const feedUrl = await findFeedInUrl(newsFeed.feed_url);
|
|
||||||
|
|
||||||
if (!feedUrl) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid URL for feed: "${feedUrl}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (feedUrl !== newsFeed.feed_url) {
|
|
||||||
newsFeed.feed_url = feedUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
const feedInfo = await getFeedInfo(newsFeed.feed_url);
|
|
||||||
|
|
||||||
newsFeed.extra.title = feedInfo.title;
|
|
||||||
newsFeed.extra.feed_type = feedInfo.feed_type;
|
|
||||||
newsFeed.extra.crawl_type = feedInfo.crawl_type;
|
|
||||||
}
|
|
||||||
|
|
||||||
const feedArticles = await fetchNewsArticles(newsFeed);
|
|
||||||
|
|
||||||
const articles: Omit<NewsFeedArticle, 'id' | 'user_id' | 'feed_id' | 'extra' | 'is_read' | 'created_at'>[] = [];
|
|
||||||
|
|
||||||
for (const feedArticle of feedArticles) {
|
|
||||||
// Don't add too many articles per run
|
|
||||||
if (articles.length >= MAX_ARTICLES_CRAWLED_PER_RUN) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = (feedArticle as JsonFeedArticle).url || getArticleUrl((feedArticle as FeedArticle).links) ||
|
|
||||||
feedArticle.id;
|
|
||||||
|
|
||||||
const articleIsoDate = (feedArticle as JsonFeedArticle).date_published ||
|
|
||||||
(feedArticle as FeedArticle).published?.toISOString() || (feedArticle as JsonFeedArticle).date_modified ||
|
|
||||||
(feedArticle as FeedArticle).updated?.toISOString();
|
|
||||||
|
|
||||||
const articleDate = articleIsoDate ? new Date(articleIsoDate) : new Date();
|
|
||||||
|
|
||||||
const summary = await parseTextFromHtml(
|
|
||||||
(feedArticle as FeedArticle).description?.value || (feedArticle as FeedArticle).content?.value ||
|
|
||||||
(feedArticle as JsonFeedArticle).content_text || (feedArticle as JsonFeedArticle).content_html ||
|
|
||||||
(feedArticle as JsonFeedArticle).summary || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
articles.push({
|
|
||||||
article_title: (feedArticle as FeedArticle).title?.value || (feedArticle as JsonFeedArticle).title ||
|
|
||||||
url.replace('http://', '').replace('https://', ''),
|
|
||||||
article_url: url,
|
|
||||||
article_summary: summary,
|
|
||||||
article_date: articleDate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingArticles = await getNewsArticlesByFeedId(newsFeed.id);
|
|
||||||
const existingArticleUrls = new Set<string>(existingArticles.map((article) => article.article_url));
|
|
||||||
const previousLatestArticleUrl = existingArticles[0]?.article_url;
|
|
||||||
let seenPreviousLatestArticleUrl = false;
|
|
||||||
let addedArticlesCount = 0;
|
|
||||||
|
|
||||||
for (const article of articles) {
|
|
||||||
// Stop looking after seeing the previous latest article
|
|
||||||
if (article.article_url === previousLatestArticleUrl) {
|
|
||||||
seenPreviousLatestArticleUrl = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!seenPreviousLatestArticleUrl && !existingArticleUrls.has(article.article_url)) {
|
|
||||||
try {
|
|
||||||
await createsNewsArticle(newsFeed.user_id, newsFeed.id, article);
|
|
||||||
++addedArticlesCount;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
console.error(`Failed to add new article: "${article.article_url}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info('Added', addedArticlesCount, 'new articles');
|
|
||||||
|
|
||||||
newsFeed.last_crawled_at = new Date();
|
|
||||||
|
|
||||||
await updateNewsFeed(newsFeed);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
} catch (error) {
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
286
lib/data/user.ts
286
lib/data/user.ts
@@ -1,286 +0,0 @@
|
|||||||
import Database, { sql } from '/lib/interfaces/database.ts';
|
|
||||||
import { User, UserSession, VerificationCode } from '/lib/types.ts';
|
|
||||||
import { generateRandomCode } from '/lib/utils/misc.ts';
|
|
||||||
import { isEmailEnabled, isForeverSignupEnabled } from '/lib/config.ts';
|
|
||||||
|
|
||||||
const db = new Database();
|
|
||||||
|
|
||||||
export async function isThereAnAdmin() {
|
|
||||||
const user =
|
|
||||||
(await db.query<User>(sql`SELECT * FROM "bewcloud_users" WHERE ("extra" ->> 'is_admin')::boolean IS TRUE LIMIT 1`))[
|
|
||||||
0
|
|
||||||
];
|
|
||||||
|
|
||||||
return Boolean(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserByEmail(email: string) {
|
|
||||||
const lowercaseEmail = email.toLowerCase().trim();
|
|
||||||
|
|
||||||
const user = (await db.query<User>(sql`SELECT * FROM "bewcloud_users" WHERE "email" = $1 LIMIT 1`, [
|
|
||||||
lowercaseEmail,
|
|
||||||
]))[0];
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserById(id: string) {
|
|
||||||
const user = (await db.query<User>(sql`SELECT * FROM "bewcloud_users" WHERE "id" = $1 LIMIT 1`, [
|
|
||||||
id,
|
|
||||||
]))[0];
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createUser(email: User['email'], hashedPassword: User['hashed_password']) {
|
|
||||||
const trialDays = isForeverSignupEnabled() ? 36_525 : 30;
|
|
||||||
const now = new Date();
|
|
||||||
const trialEndDate = new Date(new Date().setUTCDate(new Date().getUTCDate() + trialDays));
|
|
||||||
|
|
||||||
const subscription: User['subscription'] = {
|
|
||||||
external: {},
|
|
||||||
expires_at: trialEndDate.toISOString(),
|
|
||||||
updated_at: now.toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const extra: User['extra'] = { is_email_verified: isEmailEnabled() ? false : true };
|
|
||||||
|
|
||||||
// First signup will be an admin "forever"
|
|
||||||
if (!(await isThereAnAdmin())) {
|
|
||||||
extra.is_admin = true;
|
|
||||||
subscription.expires_at = new Date('2100-12-31').toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = (await db.query<User>(
|
|
||||||
sql`INSERT INTO "bewcloud_users" (
|
|
||||||
"email",
|
|
||||||
"subscription",
|
|
||||||
"status",
|
|
||||||
"hashed_password",
|
|
||||||
"extra"
|
|
||||||
) VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING *`,
|
|
||||||
[
|
|
||||||
email,
|
|
||||||
JSON.stringify(subscription),
|
|
||||||
extra.is_admin || isForeverSignupEnabled() ? 'active' : 'trial',
|
|
||||||
hashedPassword,
|
|
||||||
JSON.stringify(extra),
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
return newUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateUser(user: User) {
|
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_users" SET
|
|
||||||
"email" = $2,
|
|
||||||
"subscription" = $3,
|
|
||||||
"status" = $4,
|
|
||||||
"hashed_password" = $5,
|
|
||||||
"extra" = $6
|
|
||||||
WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
user.id,
|
|
||||||
user.email,
|
|
||||||
JSON.stringify(user.subscription),
|
|
||||||
user.status,
|
|
||||||
user.hashed_password,
|
|
||||||
JSON.stringify(user.extra),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteUser(userId: string) {
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_user_sessions" WHERE "user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_verification_codes" WHERE "user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_news_feed_articles" WHERE "user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_news_feeds" WHERE "user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_users" WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSessionById(id: string) {
|
|
||||||
const session = (await db.query<UserSession>(
|
|
||||||
sql`SELECT * FROM "bewcloud_user_sessions" WHERE "id" = $1 AND "expires_at" > now() LIMIT 1`,
|
|
||||||
[
|
|
||||||
id,
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createUserSession(user: User, isShortLived = false) {
|
|
||||||
const oneMonthFromToday = new Date(new Date().setUTCMonth(new Date().getUTCMonth() + 1));
|
|
||||||
const oneWeekFromToday = new Date(new Date().setUTCDate(new Date().getUTCDate() + 7));
|
|
||||||
|
|
||||||
const newSession: Omit<UserSession, 'id' | 'created_at'> = {
|
|
||||||
user_id: user.id,
|
|
||||||
expires_at: isShortLived ? oneWeekFromToday : oneMonthFromToday,
|
|
||||||
last_seen_at: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const newUserSessionResult = (await db.query<UserSession>(
|
|
||||||
sql`INSERT INTO "bewcloud_user_sessions" (
|
|
||||||
"user_id",
|
|
||||||
"expires_at",
|
|
||||||
"last_seen_at"
|
|
||||||
) VALUES ($1, $2, $3)
|
|
||||||
RETURNING *`,
|
|
||||||
[
|
|
||||||
newSession.user_id,
|
|
||||||
newSession.expires_at,
|
|
||||||
newSession.last_seen_at,
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
return newUserSessionResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateSession(session: UserSession) {
|
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_user_sessions" SET
|
|
||||||
"expires_at" = $2,
|
|
||||||
"last_seen_at" = $3
|
|
||||||
WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
session.id,
|
|
||||||
session.expires_at,
|
|
||||||
session.last_seen_at,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteUserSession(sessionId: string) {
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_user_sessions" WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
sessionId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateUserAndSession(userId: string, sessionId: string) {
|
|
||||||
const user = await getUserById(userId);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error('Not Found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await getSessionById(sessionId);
|
|
||||||
|
|
||||||
if (!session || session.user_id !== user.id) {
|
|
||||||
throw new Error('Not Found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const oneMonthFromToday = new Date(new Date().setUTCMonth(new Date().getUTCMonth() + 1));
|
|
||||||
|
|
||||||
session.last_seen_at = new Date();
|
|
||||||
session.expires_at = oneMonthFromToday;
|
|
||||||
|
|
||||||
await updateSession(session);
|
|
||||||
|
|
||||||
return { user, session };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createVerificationCode(
|
|
||||||
user: User,
|
|
||||||
verificationId: string,
|
|
||||||
type: VerificationCode['verification']['type'],
|
|
||||||
) {
|
|
||||||
const inThirtyMinutes = new Date(new Date().setUTCMinutes(new Date().getUTCMinutes() + 30));
|
|
||||||
|
|
||||||
const code = generateRandomCode();
|
|
||||||
|
|
||||||
const newVerificationCode: Omit<VerificationCode, 'id' | 'created_at'> = {
|
|
||||||
user_id: user.id,
|
|
||||||
code,
|
|
||||||
expires_at: inThirtyMinutes,
|
|
||||||
verification: {
|
|
||||||
id: verificationId,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
sql`INSERT INTO "bewcloud_verification_codes" (
|
|
||||||
"user_id",
|
|
||||||
"code",
|
|
||||||
"expires_at",
|
|
||||||
"verification"
|
|
||||||
) VALUES ($1, $2, $3, $4)
|
|
||||||
RETURNING "id"`,
|
|
||||||
[
|
|
||||||
newVerificationCode.user_id,
|
|
||||||
newVerificationCode.code,
|
|
||||||
newVerificationCode.expires_at,
|
|
||||||
JSON.stringify(newVerificationCode.verification),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateVerificationCode(
|
|
||||||
user: User,
|
|
||||||
verificationId: string,
|
|
||||||
code: string,
|
|
||||||
type: VerificationCode['verification']['type'],
|
|
||||||
) {
|
|
||||||
const verificationCode = (await db.query<VerificationCode>(
|
|
||||||
sql`SELECT * FROM "bewcloud_verification_codes"
|
|
||||||
WHERE "user_id" = $1 AND
|
|
||||||
"code" = $2 AND
|
|
||||||
"verification" ->> 'type' = $3 AND
|
|
||||||
"verification" ->> 'id' = $4 AND
|
|
||||||
"expires_at" > now()
|
|
||||||
LIMIT 1`,
|
|
||||||
[
|
|
||||||
user.id,
|
|
||||||
code,
|
|
||||||
type,
|
|
||||||
verificationId,
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
if (verificationCode) {
|
|
||||||
await db.query(
|
|
||||||
sql`DELETE FROM "bewcloud_verification_codes" WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
verificationCode.id,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error('Not Found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Client } from 'https://deno.land/x/postgres@v0.19.2/mod.ts';
|
import { Client } from 'https://deno.land/x/postgres@v0.19.3/mod.ts';
|
||||||
import 'std/dotenv/load.ts';
|
import 'std/dotenv/load.ts';
|
||||||
|
|
||||||
const POSTGRESQL_HOST = Deno.env.get('POSTGRESQL_HOST') || '';
|
const POSTGRESQL_HOST = Deno.env.get('POSTGRESQL_HOST') || '';
|
||||||
|
|||||||
45
lib/models/dashboard.ts
Normal file
45
lib/models/dashboard.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Database, { sql } from '/lib/interfaces/database.ts';
|
||||||
|
import { Dashboard } from '/lib/types.ts';
|
||||||
|
|
||||||
|
const db = new Database();
|
||||||
|
|
||||||
|
export class DashboardModel {
|
||||||
|
static async getByUserId(userId: string) {
|
||||||
|
const dashboard =
|
||||||
|
(await db.query<Dashboard>(sql`SELECT * FROM "bewcloud_dashboards" WHERE "user_id" = $1 LIMIT 1`, [
|
||||||
|
userId,
|
||||||
|
]))[0];
|
||||||
|
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(userId: string) {
|
||||||
|
const data: Dashboard['data'] = { links: [], notes: '' };
|
||||||
|
|
||||||
|
const newDashboard = (await db.query<Dashboard>(
|
||||||
|
sql`INSERT INTO "bewcloud_dashboards" (
|
||||||
|
"user_id",
|
||||||
|
"data"
|
||||||
|
) VALUES ($1, $2)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
JSON.stringify(data),
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
return newDashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(dashboard: Dashboard) {
|
||||||
|
await db.query(
|
||||||
|
sql`UPDATE "bewcloud_dashboards" SET
|
||||||
|
"data" = $2
|
||||||
|
WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
dashboard.id,
|
||||||
|
JSON.stringify(dashboard.data),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
479
lib/models/expenses.ts
Normal file
479
lib/models/expenses.ts
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
import Database, { sql } from '/lib/interfaces/database.ts';
|
||||||
|
import Locker from '/lib/interfaces/locker.ts';
|
||||||
|
import { Budget, Expense } from '/lib/types.ts';
|
||||||
|
|
||||||
|
const db = new Database();
|
||||||
|
|
||||||
|
export class BudgetModel {
|
||||||
|
static async list(
|
||||||
|
userId: string,
|
||||||
|
month: string,
|
||||||
|
{ skipRecalculation = false }: { skipRecalculation?: boolean } = {},
|
||||||
|
) {
|
||||||
|
if (!skipRecalculation) {
|
||||||
|
await recalculateMonthBudgets(userId, month);
|
||||||
|
}
|
||||||
|
|
||||||
|
const budgets = await db.query<Budget>(
|
||||||
|
sql`SELECT * FROM "bewcloud_budgets" WHERE "user_id" = $1 AND "month" = $2 ORDER BY cast("extra"->>'availableValue' as numeric) DESC, "value" DESC, "name" ASC`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
month,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
return budgets.map((budget) => ({
|
||||||
|
...budget,
|
||||||
|
value: Number(budget.value),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getByName(userId: string, month: string, name: string) {
|
||||||
|
const budget = (await db.query<Budget>(
|
||||||
|
sql`SELECT * FROM "bewcloud_budgets" WHERE "user_id" = $1 AND "month" = $2 AND LOWER("name") = LOWER($3)`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
month,
|
||||||
|
name,
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
if (!budget) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
return {
|
||||||
|
...budget,
|
||||||
|
value: Number(budget.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getById(userId: string, id: string) {
|
||||||
|
const budget = (await db.query<Budget>(
|
||||||
|
sql`SELECT * FROM "bewcloud_budgets" WHERE "user_id" = $1 AND "id" = $2`,
|
||||||
|
[userId, id],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
if (!budget) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
return {
|
||||||
|
...budget,
|
||||||
|
value: Number(budget.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getAllForExport(
|
||||||
|
userId: string,
|
||||||
|
): Promise<(Omit<Budget, 'id' | 'user_id' | 'created_at' | 'extra'> & { extra: Record<never, never> })[]> {
|
||||||
|
const budgets = await db.query<Budget>(
|
||||||
|
sql`SELECT * FROM "bewcloud_budgets" WHERE "user_id" = $1 ORDER BY "month" DESC, "name" ASC`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return budgets.map((budget) => ({
|
||||||
|
name: budget.name,
|
||||||
|
month: budget.month,
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
value: Number(budget.value),
|
||||||
|
extra: {},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(userId: string, name: string, month: string, value: number) {
|
||||||
|
const extra: Budget['extra'] = {
|
||||||
|
usedValue: 0,
|
||||||
|
availableValue: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newBudget = (await db.query<Budget>(
|
||||||
|
sql`INSERT INTO "bewcloud_budgets" (
|
||||||
|
"user_id",
|
||||||
|
"name",
|
||||||
|
"month",
|
||||||
|
"value",
|
||||||
|
"extra"
|
||||||
|
) VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
month,
|
||||||
|
value,
|
||||||
|
JSON.stringify(extra),
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
return {
|
||||||
|
...newBudget,
|
||||||
|
value: Number(newBudget.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(
|
||||||
|
budget: Budget,
|
||||||
|
{ skipRecalculation = false }: { skipRecalculation?: boolean } = {},
|
||||||
|
) {
|
||||||
|
await db.query(
|
||||||
|
sql`UPDATE "bewcloud_budgets" SET
|
||||||
|
"name" = $2,
|
||||||
|
"month" = $3,
|
||||||
|
"value" = $4,
|
||||||
|
"extra" = $5
|
||||||
|
WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
budget.id,
|
||||||
|
budget.name,
|
||||||
|
budget.month,
|
||||||
|
budget.value,
|
||||||
|
JSON.stringify(budget.extra),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!skipRecalculation) {
|
||||||
|
await recalculateMonthBudgets(budget.user_id, budget.month);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(userId: string, id: string) {
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_budgets" WHERE "id" = $1 AND "user_id" = $2`,
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpenseModel {
|
||||||
|
static async list(userId: string, month: string) {
|
||||||
|
const expenses = await db.query<Expense>(
|
||||||
|
sql`SELECT * FROM "bewcloud_expenses" WHERE "user_id" = $1 AND "date" >= $2 AND "date" <= $3 ORDER BY "date" DESC, "created_at" DESC`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
`${month}-01`,
|
||||||
|
`${month}-31`,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
return expenses.map((expense) => ({
|
||||||
|
...expense,
|
||||||
|
cost: Number(expense.cost),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getByName(userId: string, name: string) {
|
||||||
|
const expense = (await db.query<Expense>(
|
||||||
|
sql`SELECT * FROM "bewcloud_expenses" WHERE "user_id" = $1 AND LOWER("description") = LOWER($2) ORDER BY "date" DESC, "created_at" DESC`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
if (!expense) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
return {
|
||||||
|
...expense,
|
||||||
|
cost: Number(expense.cost),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async listSuggestions(userId: string, name: string) {
|
||||||
|
const expenses = await db.query<Pick<Expense, 'description'>>(
|
||||||
|
sql`SELECT "description" FROM "bewcloud_expenses" WHERE "user_id" = $1 AND LOWER("description") ILIKE LOWER($2) GROUP BY "description" ORDER BY LENGTH("description") ASC, "description" ASC`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
`%${name}%`,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return expenses.map((expense) => expense.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getAllForExport(
|
||||||
|
userId: string,
|
||||||
|
): Promise<(Omit<Expense, 'id' | 'user_id' | 'created_at' | 'extra'> & { extra: Record<never, never> })[]> {
|
||||||
|
const expenses = await db.query<Expense>(
|
||||||
|
sql`SELECT * FROM "bewcloud_expenses" WHERE "user_id" = $1 ORDER BY "date" DESC, "created_at" DESC`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return expenses.map((expense) => ({
|
||||||
|
description: expense.description,
|
||||||
|
budget: expense.budget,
|
||||||
|
date: expense.date,
|
||||||
|
is_recurring: expense.is_recurring,
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
cost: Number(expense.cost),
|
||||||
|
extra: {},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getById(userId: string, id: string) {
|
||||||
|
const expense = (await db.query<Expense>(
|
||||||
|
sql`SELECT * FROM "bewcloud_expenses" WHERE "user_id" = $1 AND "id" = $2`,
|
||||||
|
[userId, id],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
if (!expense) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
return {
|
||||||
|
...expense,
|
||||||
|
cost: Number(expense.cost),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(
|
||||||
|
userId: string,
|
||||||
|
cost: number,
|
||||||
|
description: string,
|
||||||
|
budget: string,
|
||||||
|
date: string,
|
||||||
|
is_recurring: boolean,
|
||||||
|
{ skipRecalculation = false, skipBudgetMatching = false, skipBudgetCreation = false }: {
|
||||||
|
skipRecalculation?: boolean;
|
||||||
|
skipBudgetMatching?: boolean;
|
||||||
|
skipBudgetCreation?: boolean;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const extra: Expense['extra'] = {};
|
||||||
|
|
||||||
|
if (!budget.trim()) {
|
||||||
|
budget = 'Misc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match budget to an existing expense "by default"
|
||||||
|
if (!skipBudgetMatching && budget === 'Misc') {
|
||||||
|
const existingExpense = await this.getByName(userId, description);
|
||||||
|
|
||||||
|
if (existingExpense) {
|
||||||
|
budget = existingExpense.budget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipBudgetCreation) {
|
||||||
|
const existingBudgetInMonth = await BudgetModel.getByName(userId, date.substring(0, 7), budget);
|
||||||
|
|
||||||
|
if (!existingBudgetInMonth) {
|
||||||
|
await BudgetModel.create(userId, budget, date.substring(0, 7), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newExpense = (await db.query<Expense>(
|
||||||
|
sql`INSERT INTO "bewcloud_expenses" (
|
||||||
|
"user_id",
|
||||||
|
"cost",
|
||||||
|
"description",
|
||||||
|
"budget",
|
||||||
|
"date",
|
||||||
|
"is_recurring",
|
||||||
|
"extra"
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
cost,
|
||||||
|
description,
|
||||||
|
budget,
|
||||||
|
date,
|
||||||
|
is_recurring,
|
||||||
|
JSON.stringify(extra),
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
if (!skipRecalculation) {
|
||||||
|
await recalculateMonthBudgets(userId, date.substring(0, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric values come as strings, so we need to convert them to numbers
|
||||||
|
return {
|
||||||
|
...newExpense,
|
||||||
|
cost: Number(newExpense.cost),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(expense: Expense) {
|
||||||
|
const existingBudgetInMonth = await BudgetModel.getByName(
|
||||||
|
expense.user_id,
|
||||||
|
expense.date.substring(0, 7),
|
||||||
|
expense.budget,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingBudgetInMonth) {
|
||||||
|
await BudgetModel.create(expense.user_id, expense.budget, expense.date.substring(0, 7), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`UPDATE "bewcloud_expenses" SET
|
||||||
|
"cost" = $2,
|
||||||
|
"description" = $3,
|
||||||
|
"budget" = $4,
|
||||||
|
"date" = $5,
|
||||||
|
"is_recurring" = $6,
|
||||||
|
"extra" = $7
|
||||||
|
WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
expense.id,
|
||||||
|
expense.cost,
|
||||||
|
expense.description,
|
||||||
|
expense.budget,
|
||||||
|
expense.date,
|
||||||
|
expense.is_recurring,
|
||||||
|
JSON.stringify(expense.extra),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await recalculateMonthBudgets(expense.user_id, expense.date.substring(0, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(userId: string, id: string) {
|
||||||
|
const expense = await this.getById(userId, id);
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_expenses" WHERE "id" = $1 AND "user_id" = $2`,
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await recalculateMonthBudgets(userId, expense!.date.substring(0, 7));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAllBudgetsAndExpenses(userId: string) {
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_expenses" WHERE "user_id" = $1`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_budgets" WHERE "user_id" = $1`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMonthlyBudgetsAndExpenses(userId: string, month: string) {
|
||||||
|
const lock = new Locker(`expenses:${userId}:${month}`);
|
||||||
|
|
||||||
|
await lock.acquire();
|
||||||
|
|
||||||
|
let addedBudgetsCount = 0;
|
||||||
|
let addedExpensesCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Confirm there are no budgets or expenses for this month
|
||||||
|
const monthBudgets = await BudgetModel.list(userId, month, { skipRecalculation: true });
|
||||||
|
const monthExpenses = await ExpenseModel.list(userId, month);
|
||||||
|
|
||||||
|
if (monthBudgets.length > 0 || monthExpenses.length > 0) {
|
||||||
|
throw new Error('Budgets and expenses already exist for this month!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the previous month's budgets, to copy over
|
||||||
|
const previousMonthDate = new Date(month);
|
||||||
|
previousMonthDate.setMonth(previousMonthDate.getMonth() - 1);
|
||||||
|
|
||||||
|
const previousMonth = previousMonthDate.toISOString().substring(0, 7);
|
||||||
|
|
||||||
|
const budgets = await BudgetModel.list(userId, previousMonth, { skipRecalculation: true });
|
||||||
|
|
||||||
|
for (const budget of budgets) {
|
||||||
|
await BudgetModel.create(userId, budget.name, month, budget.value);
|
||||||
|
|
||||||
|
addedBudgetsCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the recurring expenses for the previous month, to copy over
|
||||||
|
const recurringExpenses = (await ExpenseModel.list(userId, previousMonth)).filter((expense) =>
|
||||||
|
expense.is_recurring
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const expense of recurringExpenses) {
|
||||||
|
await ExpenseModel.create(
|
||||||
|
userId,
|
||||||
|
expense.cost,
|
||||||
|
expense.description,
|
||||||
|
expense.budget,
|
||||||
|
expense.date.replace(previousMonth, month),
|
||||||
|
expense.is_recurring,
|
||||||
|
{ skipRecalculation: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
addedExpensesCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`Added ${addedBudgetsCount} new budgets and ${addedExpensesCount} new expenses for ${month}`);
|
||||||
|
|
||||||
|
lock.release();
|
||||||
|
} catch (error) {
|
||||||
|
lock.release();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recalculateMonthBudgets(userId: string, month: string) {
|
||||||
|
const lock = new Locker(`expenses:${userId}:${month}`);
|
||||||
|
|
||||||
|
await lock.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const budgets = await BudgetModel.list(userId, month, { skipRecalculation: true });
|
||||||
|
const expenses = await ExpenseModel.list(userId, month);
|
||||||
|
|
||||||
|
// Calculate total expenses for each budget
|
||||||
|
const budgetExpenses = new Map<string, number>();
|
||||||
|
for (const expense of expenses) {
|
||||||
|
const currentTotal = Number(budgetExpenses.get(expense.budget) || 0);
|
||||||
|
budgetExpenses.set(expense.budget, Number(currentTotal + expense.cost));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update each budget with new calculations
|
||||||
|
for (const budget of budgets) {
|
||||||
|
const usedValue = Number(budgetExpenses.get(budget.name) || 0);
|
||||||
|
const availableValue = Number(budget.value - usedValue);
|
||||||
|
|
||||||
|
const updatedBudget: Budget = {
|
||||||
|
...budget,
|
||||||
|
extra: {
|
||||||
|
...budget.extra,
|
||||||
|
usedValue,
|
||||||
|
availableValue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (budget.extra.usedValue !== usedValue || budget.extra.availableValue !== availableValue) {
|
||||||
|
await BudgetModel.update(updatedBudget, { skipRecalculation: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.release();
|
||||||
|
} catch (error) {
|
||||||
|
lock.release();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
538
lib/models/files.ts
Normal file
538
lib/models/files.ts
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
import { join } from 'std/path/join.ts';
|
||||||
|
import { resolve } from 'std/path/resolve.ts';
|
||||||
|
import { lookup } from 'mrmime';
|
||||||
|
|
||||||
|
import { getFilesRootPath } from '/lib/config.ts';
|
||||||
|
import { Directory, DirectoryFile } from '/lib/types.ts';
|
||||||
|
import { sortDirectoriesByName, sortEntriesByName, sortFilesByName, TRASH_PATH } from '/lib/utils/files.ts';
|
||||||
|
|
||||||
|
export class DirectoryModel {
|
||||||
|
static async list(userId: string, path: string): Promise<Directory[]> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
|
const directories: Directory[] = [];
|
||||||
|
|
||||||
|
const directoryEntries = (await getPathEntries(userId, path)).filter((entry) =>
|
||||||
|
entry.isDirectory || entry.isSymlink
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const entry of directoryEntries) {
|
||||||
|
const stat = await Deno.stat(join(rootPath, entry.name));
|
||||||
|
|
||||||
|
const directory: Directory = {
|
||||||
|
user_id: userId,
|
||||||
|
parent_path: path,
|
||||||
|
directory_name: entry.name,
|
||||||
|
has_write_access: true,
|
||||||
|
size_in_bytes: stat.size,
|
||||||
|
updated_at: stat.mtime || new Date(),
|
||||||
|
created_at: stat.birthtime || new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
directories.push(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
directories.sort(sortDirectoriesByName);
|
||||||
|
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(userId: string, path: string, name: string): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Deno.mkdir(join(rootPath, name), { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async rename(
|
||||||
|
userId: string,
|
||||||
|
oldPath: string,
|
||||||
|
newPath: string,
|
||||||
|
oldName: string,
|
||||||
|
newName: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return await renameDirectoryOrFile(userId, oldPath, newPath, oldName, newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(userId: string, path: string, name: string): Promise<boolean> {
|
||||||
|
return await deleteDirectoryOrFile(userId, path, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async searchNames(
|
||||||
|
userId: string,
|
||||||
|
searchTerm: string,
|
||||||
|
): Promise<{ success: boolean; directories: Directory[] }> {
|
||||||
|
const rootPath = join(getFilesRootPath(), userId);
|
||||||
|
|
||||||
|
const directories: Directory[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
||||||
|
|
||||||
|
const command = new Deno.Command(`find`, {
|
||||||
|
args: [
|
||||||
|
`.`, // proper cwd is sent below
|
||||||
|
`-type`,
|
||||||
|
`d,l`, // directories and symbolic links
|
||||||
|
`-iname`,
|
||||||
|
`*${searchTerm}*`,
|
||||||
|
],
|
||||||
|
cwd: rootPath,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, stdout, stderr } = await command.output();
|
||||||
|
|
||||||
|
if (commandTimeout) {
|
||||||
|
clearTimeout(commandTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
if (stderr) {
|
||||||
|
throw new Error(new TextDecoder().decode(stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown error running "find"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = new TextDecoder().decode(stdout);
|
||||||
|
const matchingDirectories = output.split('\n').map((directoryPath) => directoryPath.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
for (const relativeDirectoryPath of matchingDirectories) {
|
||||||
|
const stat = await Deno.stat(join(rootPath, relativeDirectoryPath));
|
||||||
|
let parentPath = `/${relativeDirectoryPath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
||||||
|
const directoryName = relativeDirectoryPath.split('/').pop()!;
|
||||||
|
|
||||||
|
if (parentPath === '//') {
|
||||||
|
parentPath = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const directory: Directory = {
|
||||||
|
user_id: userId,
|
||||||
|
parent_path: parentPath,
|
||||||
|
directory_name: directoryName,
|
||||||
|
has_write_access: true,
|
||||||
|
size_in_bytes: stat.size,
|
||||||
|
updated_at: stat.mtime || new Date(),
|
||||||
|
created_at: stat.birthtime || new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
directories.push(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, directories };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, directories };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileModel {
|
||||||
|
static async list(userId: string, path: string): Promise<DirectoryFile[]> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
|
const files: DirectoryFile[] = [];
|
||||||
|
|
||||||
|
const fileEntries = (await getPathEntries(userId, path)).filter((entry) => entry.isFile);
|
||||||
|
|
||||||
|
for (const entry of fileEntries) {
|
||||||
|
const stat = await Deno.stat(join(rootPath, entry.name));
|
||||||
|
|
||||||
|
const file: DirectoryFile = {
|
||||||
|
user_id: userId,
|
||||||
|
parent_path: path,
|
||||||
|
file_name: entry.name,
|
||||||
|
has_write_access: true,
|
||||||
|
size_in_bytes: stat.size,
|
||||||
|
updated_at: stat.mtime || new Date(),
|
||||||
|
created_at: stat.birthtime || new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort(sortFilesByName);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(
|
||||||
|
userId: string,
|
||||||
|
path: string,
|
||||||
|
name: string,
|
||||||
|
contents: string | ArrayBuffer,
|
||||||
|
): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure the directory exist, if being requested
|
||||||
|
try {
|
||||||
|
await Deno.stat(rootPath);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).toString().includes('NotFound')) {
|
||||||
|
await Deno.mkdir(rootPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof contents === 'string') {
|
||||||
|
await Deno.writeTextFile(join(rootPath, name), contents, { append: false, createNew: true });
|
||||||
|
} else {
|
||||||
|
await Deno.writeFile(join(rootPath, name), new Uint8Array(contents), { append: false, createNew: true });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(
|
||||||
|
userId: string,
|
||||||
|
path: string,
|
||||||
|
name: string,
|
||||||
|
contents: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Deno.writeTextFile(join(rootPath, name), contents, { append: false, createNew: false });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get(
|
||||||
|
userId: string,
|
||||||
|
path: string,
|
||||||
|
name?: string,
|
||||||
|
): Promise<{ success: boolean; contents?: Uint8Array; contentType?: string; byteSize?: number }> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name || ''));
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = await Deno.stat(join(rootPath, name || ''));
|
||||||
|
|
||||||
|
if (stat) {
|
||||||
|
const contents = await Deno.readFile(join(rootPath, name || ''));
|
||||||
|
|
||||||
|
const extension = (name || path).split('.').slice(-1).join('').toLowerCase();
|
||||||
|
|
||||||
|
const contentType = lookup(extension) || 'application/octet-stream';
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
contents,
|
||||||
|
contentType,
|
||||||
|
byteSize: stat.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async rename(
|
||||||
|
userId: string,
|
||||||
|
oldPath: string,
|
||||||
|
newPath: string,
|
||||||
|
oldName: string,
|
||||||
|
newName: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return await renameDirectoryOrFile(userId, oldPath, newPath, oldName, newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(userId: string, path: string, name: string): Promise<boolean> {
|
||||||
|
return await deleteDirectoryOrFile(userId, path, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async searchNames(
|
||||||
|
userId: string,
|
||||||
|
searchTerm: string,
|
||||||
|
): Promise<{ success: boolean; files: DirectoryFile[] }> {
|
||||||
|
const rootPath = join(getFilesRootPath(), userId);
|
||||||
|
|
||||||
|
const files: DirectoryFile[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
||||||
|
|
||||||
|
const command = new Deno.Command(`find`, {
|
||||||
|
args: [
|
||||||
|
`.`, // proper cwd is sent below
|
||||||
|
`-type`,
|
||||||
|
`f`,
|
||||||
|
`-iname`,
|
||||||
|
`*${searchTerm}*`,
|
||||||
|
],
|
||||||
|
cwd: rootPath,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, stdout, stderr } = await command.output();
|
||||||
|
|
||||||
|
if (commandTimeout) {
|
||||||
|
clearTimeout(commandTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
if (stderr) {
|
||||||
|
throw new Error(new TextDecoder().decode(stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown error running "find"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = new TextDecoder().decode(stdout);
|
||||||
|
const matchingFiles = output.split('\n').map((filePath) => filePath.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
for (const relativeFilePath of matchingFiles) {
|
||||||
|
const stat = await Deno.stat(join(rootPath, relativeFilePath));
|
||||||
|
let parentPath = `/${relativeFilePath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
||||||
|
const fileName = relativeFilePath.split('/').pop()!;
|
||||||
|
|
||||||
|
if (parentPath === '//') {
|
||||||
|
parentPath = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const file: DirectoryFile = {
|
||||||
|
user_id: userId,
|
||||||
|
parent_path: parentPath,
|
||||||
|
file_name: fileName,
|
||||||
|
has_write_access: true,
|
||||||
|
size_in_bytes: stat.size,
|
||||||
|
updated_at: stat.mtime || new Date(),
|
||||||
|
created_at: stat.birthtime || new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, files };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, files };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async searchContents(
|
||||||
|
userId: string,
|
||||||
|
searchTerm: string,
|
||||||
|
): Promise<{ success: boolean; files: DirectoryFile[] }> {
|
||||||
|
const rootPath = join(getFilesRootPath(), userId);
|
||||||
|
|
||||||
|
const files: DirectoryFile[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
||||||
|
|
||||||
|
const command = new Deno.Command(`grep`, {
|
||||||
|
args: [
|
||||||
|
`-rHisl`,
|
||||||
|
`${searchTerm}`,
|
||||||
|
`.`, // proper cwd is sent below
|
||||||
|
],
|
||||||
|
cwd: rootPath,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, stdout, stderr } = await command.output();
|
||||||
|
|
||||||
|
if (commandTimeout) {
|
||||||
|
clearTimeout(commandTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code > 1) {
|
||||||
|
if (stderr) {
|
||||||
|
throw new Error(new TextDecoder().decode(stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown error running "grep"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = new TextDecoder().decode(stdout);
|
||||||
|
const matchingFiles = output.split('\n').map((filePath) => filePath.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
for (const relativeFilePath of matchingFiles) {
|
||||||
|
const stat = await Deno.stat(join(rootPath, relativeFilePath));
|
||||||
|
let parentPath = `/${relativeFilePath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
||||||
|
const fileName = relativeFilePath.split('/').pop()!;
|
||||||
|
|
||||||
|
if (parentPath === '//') {
|
||||||
|
parentPath = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const file: DirectoryFile = {
|
||||||
|
user_id: userId,
|
||||||
|
parent_path: parentPath,
|
||||||
|
file_name: fileName,
|
||||||
|
has_write_access: true,
|
||||||
|
size_in_bytes: stat.size,
|
||||||
|
updated_at: stat.mtime || new Date(),
|
||||||
|
created_at: stat.birthtime || new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, files };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, files };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPathEntries(userId: string, path: string): Promise<Deno.DirEntry[]> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
|
// Ensure the user directory exists
|
||||||
|
if (path === '/') {
|
||||||
|
try {
|
||||||
|
await Deno.stat(rootPath);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).toString().includes('NotFound')) {
|
||||||
|
await Deno.mkdir(join(rootPath, TRASH_PATH), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the Notes or Photos directories exist, if being requested
|
||||||
|
if (path === '/Notes/' || path === '/Photos/') {
|
||||||
|
try {
|
||||||
|
await Deno.stat(rootPath);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).toString().includes('NotFound')) {
|
||||||
|
await Deno.mkdir(rootPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: Deno.DirEntry[] = [];
|
||||||
|
|
||||||
|
for await (const dirEntry of Deno.readDir(rootPath)) {
|
||||||
|
entries.push(dirEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort(sortEntriesByName);
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renameDirectoryOrFile(
|
||||||
|
userId: string,
|
||||||
|
oldPath: string,
|
||||||
|
newPath: string,
|
||||||
|
oldName: string,
|
||||||
|
newName: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(oldPath, oldName));
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(newPath, newName));
|
||||||
|
|
||||||
|
const oldRootPath = join(getFilesRootPath(), userId, oldPath);
|
||||||
|
const newRootPath = join(getFilesRootPath(), userId, newPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Deno.rename(join(oldRootPath, oldName), join(newRootPath, newName));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDirectoryOrFile(userId: string, path: string, name: string): Promise<boolean> {
|
||||||
|
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||||
|
|
||||||
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (path.startsWith(TRASH_PATH)) {
|
||||||
|
await Deno.remove(join(rootPath, name), { recursive: true });
|
||||||
|
} else {
|
||||||
|
const trashPath = join(getFilesRootPath(), userId, TRASH_PATH);
|
||||||
|
await Deno.rename(join(rootPath, name), join(trashPath, name));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchFilesAndDirectories(
|
||||||
|
userId: string,
|
||||||
|
searchTerm: string,
|
||||||
|
): Promise<{ success: boolean; directories: Directory[]; files: DirectoryFile[] }> {
|
||||||
|
const directoryNamesResult = await DirectoryModel.searchNames(userId, searchTerm);
|
||||||
|
const fileNamesResult = await FileModel.searchNames(userId, searchTerm);
|
||||||
|
const fileContentsResult = await FileModel.searchContents(userId, searchTerm);
|
||||||
|
|
||||||
|
const success = directoryNamesResult.success && fileNamesResult.success && fileContentsResult.success;
|
||||||
|
|
||||||
|
const directories = [...directoryNamesResult.directories];
|
||||||
|
directories.sort(sortDirectoriesByName);
|
||||||
|
|
||||||
|
const files = [...fileNamesResult.files, ...fileContentsResult.files];
|
||||||
|
files.sort(sortFilesByName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
directories,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
}
|
||||||
315
lib/models/news.ts
Normal file
315
lib/models/news.ts
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import { Feed } from 'https://deno.land/x/rss@1.0.0/mod.ts';
|
||||||
|
|
||||||
|
import Database, { sql } from '/lib/interfaces/database.ts';
|
||||||
|
import Locker from '/lib/interfaces/locker.ts';
|
||||||
|
import { NewsFeed, NewsFeedArticle } from '/lib/types.ts';
|
||||||
|
import {
|
||||||
|
findFeedInUrl,
|
||||||
|
getArticleUrl,
|
||||||
|
getFeedInfo,
|
||||||
|
JsonFeed,
|
||||||
|
parseTextFromHtml,
|
||||||
|
parseUrl,
|
||||||
|
parseUrlAsGooglebot,
|
||||||
|
parseUrlWithProxy,
|
||||||
|
} from '/lib/feed.ts';
|
||||||
|
|
||||||
|
const db = new Database();
|
||||||
|
|
||||||
|
export class FeedModel {
|
||||||
|
static async list(userId: string) {
|
||||||
|
const newsFeeds = await db.query<NewsFeed>(sql`SELECT * FROM "bewcloud_news_feeds" WHERE "user_id" = $1`, [
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return newsFeeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get(id: string, userId: string) {
|
||||||
|
const newsFeeds = await db.query<NewsFeed>(
|
||||||
|
sql`SELECT * FROM "bewcloud_news_feeds" WHERE "id" = $1 AND "user_id" = $2 LIMIT 1`,
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return newsFeeds[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(userId: string, feedUrl: string) {
|
||||||
|
const extra: NewsFeed['extra'] = {};
|
||||||
|
|
||||||
|
const newNewsFeed = (await db.query<NewsFeed>(
|
||||||
|
sql`INSERT INTO "bewcloud_news_feeds" (
|
||||||
|
"user_id",
|
||||||
|
"feed_url",
|
||||||
|
"extra"
|
||||||
|
) VALUES ($1, $2, $3)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
feedUrl,
|
||||||
|
JSON.stringify(extra),
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
return newNewsFeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(newsFeed: NewsFeed) {
|
||||||
|
await db.query(
|
||||||
|
sql`UPDATE "bewcloud_news_feeds" SET
|
||||||
|
"feed_url" = $2,
|
||||||
|
"last_crawled_at" = $3,
|
||||||
|
"extra" = $4
|
||||||
|
WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
newsFeed.id,
|
||||||
|
newsFeed.feed_url,
|
||||||
|
newsFeed.last_crawled_at,
|
||||||
|
JSON.stringify(newsFeed.extra),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(id: string, userId: string) {
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_news_feed_articles" WHERE "feed_id" = $1 AND "user_id" = $2`,
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_news_feeds" WHERE "id" = $1 AND "user_id" = $2`,
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async crawl(newsFeed: NewsFeed) {
|
||||||
|
type FeedArticle = Feed['entries'][number];
|
||||||
|
type JsonFeedArticle = JsonFeed['items'][number];
|
||||||
|
|
||||||
|
const MAX_ARTICLES_CRAWLED_PER_RUN = 10;
|
||||||
|
|
||||||
|
const lock = new Locker(`feeds:${newsFeed.id}`);
|
||||||
|
|
||||||
|
await lock.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!newsFeed.extra.title || !newsFeed.extra.feed_type || !newsFeed.extra.crawl_type) {
|
||||||
|
const feedUrl = await findFeedInUrl(newsFeed.feed_url);
|
||||||
|
|
||||||
|
if (!feedUrl) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid URL for feed: "${feedUrl}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedUrl !== newsFeed.feed_url) {
|
||||||
|
newsFeed.feed_url = feedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedInfo = await getFeedInfo(newsFeed.feed_url);
|
||||||
|
|
||||||
|
newsFeed.extra.title = feedInfo.title;
|
||||||
|
newsFeed.extra.feed_type = feedInfo.feed_type;
|
||||||
|
newsFeed.extra.crawl_type = feedInfo.crawl_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedArticles = await fetchNewsArticles(newsFeed);
|
||||||
|
|
||||||
|
const articles: Omit<NewsFeedArticle, 'id' | 'user_id' | 'feed_id' | 'extra' | 'is_read' | 'created_at'>[] = [];
|
||||||
|
|
||||||
|
for (const feedArticle of feedArticles) {
|
||||||
|
// Don't add too many articles per run
|
||||||
|
if (articles.length >= MAX_ARTICLES_CRAWLED_PER_RUN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = (feedArticle as JsonFeedArticle).url || getArticleUrl((feedArticle as FeedArticle).links) ||
|
||||||
|
feedArticle.id;
|
||||||
|
|
||||||
|
const articleIsoDate = (feedArticle as JsonFeedArticle).date_published ||
|
||||||
|
(feedArticle as FeedArticle).published?.toISOString() || (feedArticle as JsonFeedArticle).date_modified ||
|
||||||
|
(feedArticle as FeedArticle).updated?.toISOString();
|
||||||
|
|
||||||
|
const articleDate = articleIsoDate ? new Date(articleIsoDate) : new Date();
|
||||||
|
|
||||||
|
const summary = await parseTextFromHtml(
|
||||||
|
(feedArticle as FeedArticle).description?.value || (feedArticle as FeedArticle).content?.value ||
|
||||||
|
(feedArticle as JsonFeedArticle).content_text || (feedArticle as JsonFeedArticle).content_html ||
|
||||||
|
(feedArticle as JsonFeedArticle).summary || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
articles.push({
|
||||||
|
article_title: (feedArticle as FeedArticle).title?.value || (feedArticle as JsonFeedArticle).title ||
|
||||||
|
url.replace('http://', '').replace('https://', ''),
|
||||||
|
article_url: url,
|
||||||
|
article_summary: summary,
|
||||||
|
article_date: articleDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingArticles = await ArticleModel.listByFeedId(newsFeed.id);
|
||||||
|
const existingArticleUrls = new Set<string>(existingArticles.map((article) => article.article_url));
|
||||||
|
const previousLatestArticleUrl = existingArticles[0]?.article_url;
|
||||||
|
let seenPreviousLatestArticleUrl = false;
|
||||||
|
let addedArticlesCount = 0;
|
||||||
|
|
||||||
|
for (const article of articles) {
|
||||||
|
// Stop looking after seeing the previous latest article
|
||||||
|
if (article.article_url === previousLatestArticleUrl) {
|
||||||
|
seenPreviousLatestArticleUrl = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!seenPreviousLatestArticleUrl && !existingArticleUrls.has(article.article_url)) {
|
||||||
|
try {
|
||||||
|
await ArticleModel.create(newsFeed.user_id, newsFeed.id, article);
|
||||||
|
++addedArticlesCount;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.error(`Failed to add new article: "${article.article_url}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('Added', addedArticlesCount, 'new articles');
|
||||||
|
|
||||||
|
newsFeed.last_crawled_at = new Date();
|
||||||
|
|
||||||
|
await this.update(newsFeed);
|
||||||
|
|
||||||
|
lock.release();
|
||||||
|
} catch (error) {
|
||||||
|
lock.release();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArticleModel {
|
||||||
|
static async list(userId: string) {
|
||||||
|
const articles = await db.query<NewsFeedArticle>(
|
||||||
|
sql`SELECT * FROM "bewcloud_news_feed_articles" WHERE "user_id" = $1 ORDER BY "article_date" DESC`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async listByFeedId(feedId: string) {
|
||||||
|
const articles = await db.query<NewsFeedArticle>(
|
||||||
|
sql`SELECT * FROM "bewcloud_news_feed_articles" WHERE "feed_id" = $1 ORDER BY "article_date" DESC`,
|
||||||
|
[
|
||||||
|
feedId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get(id: string, userId: string) {
|
||||||
|
const articles = await db.query<NewsFeedArticle>(
|
||||||
|
sql`SELECT * FROM "bewcloud_news_feed_articles" WHERE "id" = $1 AND "user_id" = $2 LIMIT 1`,
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return articles[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(
|
||||||
|
userId: string,
|
||||||
|
feedId: string,
|
||||||
|
article: Omit<NewsFeedArticle, 'id' | 'user_id' | 'feed_id' | 'extra' | 'is_read' | 'created_at'>,
|
||||||
|
) {
|
||||||
|
const extra: NewsFeedArticle['extra'] = {};
|
||||||
|
|
||||||
|
const newNewsArticle = (await db.query<NewsFeedArticle>(
|
||||||
|
sql`INSERT INTO "bewcloud_news_feed_articles" (
|
||||||
|
"user_id",
|
||||||
|
"feed_id",
|
||||||
|
"article_url",
|
||||||
|
"article_title",
|
||||||
|
"article_summary",
|
||||||
|
"article_date",
|
||||||
|
"extra"
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
feedId,
|
||||||
|
article.article_url,
|
||||||
|
article.article_title,
|
||||||
|
article.article_summary,
|
||||||
|
article.article_date,
|
||||||
|
JSON.stringify(extra),
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
return newNewsArticle;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(article: NewsFeedArticle) {
|
||||||
|
await db.query(
|
||||||
|
sql`UPDATE "bewcloud_news_feed_articles" SET
|
||||||
|
"is_read" = $2,
|
||||||
|
"extra" = $3
|
||||||
|
WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
article.id,
|
||||||
|
article.is_read,
|
||||||
|
JSON.stringify(article.extra),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async markAllRead(userId: string) {
|
||||||
|
await db.query(
|
||||||
|
sql`UPDATE "bewcloud_news_feed_articles" SET
|
||||||
|
"is_read" = TRUE
|
||||||
|
WHERE "user_id" = $1`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchNewsArticles(newsFeed: NewsFeed): Promise<Feed['entries'] | JsonFeed['items']> {
|
||||||
|
try {
|
||||||
|
if (!newsFeed.extra.title || !newsFeed.extra.feed_type || !newsFeed.extra.crawl_type) {
|
||||||
|
throw new Error('Invalid News Feed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
let feed: JsonFeed | Feed | null = null;
|
||||||
|
|
||||||
|
if (newsFeed.extra.crawl_type === 'direct') {
|
||||||
|
feed = await parseUrl(newsFeed.feed_url);
|
||||||
|
} else if (newsFeed.extra.crawl_type === 'googlebot') {
|
||||||
|
feed = await parseUrlAsGooglebot(newsFeed.feed_url);
|
||||||
|
} else if (newsFeed.extra.crawl_type === 'proxy') {
|
||||||
|
feed = await parseUrlWithProxy(newsFeed.feed_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (feed as Feed)?.entries || (feed as JsonFeed)?.items || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed parsing feed to get articles', newsFeed.feed_url);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
290
lib/models/user.ts
Normal file
290
lib/models/user.ts
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import Database, { sql } from '/lib/interfaces/database.ts';
|
||||||
|
import { User, UserSession, VerificationCode } from '/lib/types.ts';
|
||||||
|
import { generateRandomCode } from '/lib/utils/misc.ts';
|
||||||
|
import { isEmailEnabled, isForeverSignupEnabled } from '/lib/config.ts';
|
||||||
|
|
||||||
|
const db = new Database();
|
||||||
|
|
||||||
|
export class UserModel {
|
||||||
|
static async isThereAnAdmin() {
|
||||||
|
const user = (await db.query<User>(
|
||||||
|
sql`SELECT * FROM "bewcloud_users" WHERE ("extra" ->> 'is_admin')::boolean IS TRUE LIMIT 1`,
|
||||||
|
))[
|
||||||
|
0
|
||||||
|
];
|
||||||
|
|
||||||
|
return Boolean(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getByEmail(email: string) {
|
||||||
|
const lowercaseEmail = email.toLowerCase().trim();
|
||||||
|
|
||||||
|
const user = (await db.query<User>(sql`SELECT * FROM "bewcloud_users" WHERE "email" = $1 LIMIT 1`, [
|
||||||
|
lowercaseEmail,
|
||||||
|
]))[0];
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getById(id: string) {
|
||||||
|
const user = (await db.query<User>(sql`SELECT * FROM "bewcloud_users" WHERE "id" = $1 LIMIT 1`, [
|
||||||
|
id,
|
||||||
|
]))[0];
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(email: User['email'], hashedPassword: User['hashed_password']) {
|
||||||
|
const trialDays = isForeverSignupEnabled() ? 36_525 : 30;
|
||||||
|
const now = new Date();
|
||||||
|
const trialEndDate = new Date(new Date().setUTCDate(new Date().getUTCDate() + trialDays));
|
||||||
|
|
||||||
|
const subscription: User['subscription'] = {
|
||||||
|
external: {},
|
||||||
|
expires_at: trialEndDate.toISOString(),
|
||||||
|
updated_at: now.toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const extra: User['extra'] = { is_email_verified: isEmailEnabled() ? false : true };
|
||||||
|
|
||||||
|
// First signup will be an admin "forever"
|
||||||
|
if (!(await this.isThereAnAdmin())) {
|
||||||
|
extra.is_admin = true;
|
||||||
|
subscription.expires_at = new Date('2100-12-31').toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = (await db.query<User>(
|
||||||
|
sql`INSERT INTO "bewcloud_users" (
|
||||||
|
"email",
|
||||||
|
"subscription",
|
||||||
|
"status",
|
||||||
|
"hashed_password",
|
||||||
|
"extra"
|
||||||
|
) VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
email,
|
||||||
|
JSON.stringify(subscription),
|
||||||
|
extra.is_admin || isForeverSignupEnabled() ? 'active' : 'trial',
|
||||||
|
hashedPassword,
|
||||||
|
JSON.stringify(extra),
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(user: User) {
|
||||||
|
await db.query(
|
||||||
|
sql`UPDATE "bewcloud_users" SET
|
||||||
|
"email" = $2,
|
||||||
|
"subscription" = $3,
|
||||||
|
"status" = $4,
|
||||||
|
"hashed_password" = $5,
|
||||||
|
"extra" = $6
|
||||||
|
WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
user.email,
|
||||||
|
JSON.stringify(user.subscription),
|
||||||
|
user.status,
|
||||||
|
user.hashed_password,
|
||||||
|
JSON.stringify(user.extra),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(userId: string) {
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_user_sessions" WHERE "user_id" = $1`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_verification_codes" WHERE "user_id" = $1`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_news_feed_articles" WHERE "user_id" = $1`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_news_feeds" WHERE "user_id" = $1`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_users" WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserSessionModel {
|
||||||
|
static async getById(id: string) {
|
||||||
|
const session = (await db.query<UserSession>(
|
||||||
|
sql`SELECT * FROM "bewcloud_user_sessions" WHERE "id" = $1 AND "expires_at" > now() LIMIT 1`,
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(user: User, isShortLived = false) {
|
||||||
|
const oneMonthFromToday = new Date(new Date().setUTCMonth(new Date().getUTCMonth() + 1));
|
||||||
|
const oneWeekFromToday = new Date(new Date().setUTCDate(new Date().getUTCDate() + 7));
|
||||||
|
|
||||||
|
const newSession: Omit<UserSession, 'id' | 'created_at'> = {
|
||||||
|
user_id: user.id,
|
||||||
|
expires_at: isShortLived ? oneWeekFromToday : oneMonthFromToday,
|
||||||
|
last_seen_at: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const newUserSessionResult = (await db.query<UserSession>(
|
||||||
|
sql`INSERT INTO "bewcloud_user_sessions" (
|
||||||
|
"user_id",
|
||||||
|
"expires_at",
|
||||||
|
"last_seen_at"
|
||||||
|
) VALUES ($1, $2, $3)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
newSession.user_id,
|
||||||
|
newSession.expires_at,
|
||||||
|
newSession.last_seen_at,
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
return newUserSessionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(session: UserSession) {
|
||||||
|
await db.query(
|
||||||
|
sql`UPDATE "bewcloud_user_sessions" SET
|
||||||
|
"expires_at" = $2,
|
||||||
|
"last_seen_at" = $3
|
||||||
|
WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
session.id,
|
||||||
|
session.expires_at,
|
||||||
|
session.last_seen_at,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(sessionId: string) {
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_user_sessions" WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
sessionId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateUserAndSession(userId: string, sessionId: string) {
|
||||||
|
const user = await UserModel.getById(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Not Found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await UserSessionModel.getById(sessionId);
|
||||||
|
|
||||||
|
if (!session || session.user_id !== user.id) {
|
||||||
|
throw new Error('Not Found');
|
||||||
|
}
|
||||||
|
|
||||||
|
session.last_seen_at = new Date();
|
||||||
|
|
||||||
|
await UserSessionModel.update(session);
|
||||||
|
|
||||||
|
return { user, session };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VerificationCodeModel {
|
||||||
|
static async create(
|
||||||
|
user: User,
|
||||||
|
verificationId: string,
|
||||||
|
type: VerificationCode['verification']['type'],
|
||||||
|
) {
|
||||||
|
const inThirtyMinutes = new Date(new Date().setUTCMinutes(new Date().getUTCMinutes() + 30));
|
||||||
|
|
||||||
|
const code = generateRandomCode();
|
||||||
|
|
||||||
|
const newVerificationCode: Omit<VerificationCode, 'id' | 'created_at'> = {
|
||||||
|
user_id: user.id,
|
||||||
|
code,
|
||||||
|
expires_at: inThirtyMinutes,
|
||||||
|
verification: {
|
||||||
|
id: verificationId,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
sql`INSERT INTO "bewcloud_verification_codes" (
|
||||||
|
"user_id",
|
||||||
|
"code",
|
||||||
|
"expires_at",
|
||||||
|
"verification"
|
||||||
|
) VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING "id"`,
|
||||||
|
[
|
||||||
|
newVerificationCode.user_id,
|
||||||
|
newVerificationCode.code,
|
||||||
|
newVerificationCode.expires_at,
|
||||||
|
JSON.stringify(newVerificationCode.verification),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async validate(
|
||||||
|
user: User,
|
||||||
|
verificationId: string,
|
||||||
|
code: string,
|
||||||
|
type: VerificationCode['verification']['type'],
|
||||||
|
) {
|
||||||
|
const verificationCode = (await db.query<VerificationCode>(
|
||||||
|
sql`SELECT * FROM "bewcloud_verification_codes"
|
||||||
|
WHERE "user_id" = $1 AND
|
||||||
|
"code" = $2 AND
|
||||||
|
"verification" ->> 'type' = $3 AND
|
||||||
|
"verification" ->> 'id' = $4 AND
|
||||||
|
"expires_at" > now()
|
||||||
|
LIMIT 1`,
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
code,
|
||||||
|
type,
|
||||||
|
verificationId,
|
||||||
|
],
|
||||||
|
))[0];
|
||||||
|
|
||||||
|
if (verificationCode) {
|
||||||
|
await db.query(
|
||||||
|
sql`DELETE FROM "bewcloud_verification_codes" WHERE "id" = $1`,
|
||||||
|
[
|
||||||
|
verificationCode.id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('Not Found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DashboardLink, FreshContextState } from '/lib/types.ts';
|
import { DashboardLink, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDashboardByUserId, updateDashboard } from '/lib/data/dashboard.ts';
|
import { DashboardModel } from '/lib/models/dashboard.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDashboard = await getDashboardByUserId(context.state.user.id);
|
const userDashboard = await DashboardModel.getByUserId(context.state.user.id);
|
||||||
|
|
||||||
if (!userDashboard) {
|
if (!userDashboard) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
@@ -30,7 +30,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
if (typeof requestBody.links !== 'undefined') {
|
if (typeof requestBody.links !== 'undefined') {
|
||||||
userDashboard.data.links = requestBody.links;
|
userDashboard.data.links = requestBody.links;
|
||||||
|
|
||||||
await updateDashboard(userDashboard);
|
await DashboardModel.update(userDashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true };
|
const responseBody: ResponseBody = { success: true };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { getDashboardByUserId, updateDashboard } from '/lib/data/dashboard.ts';
|
import { DashboardModel } from '/lib/models/dashboard.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDashboard = await getDashboardByUserId(context.state.user.id);
|
const userDashboard = await DashboardModel.getByUserId(context.state.user.id);
|
||||||
|
|
||||||
if (!userDashboard) {
|
if (!userDashboard) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
@@ -30,7 +30,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
if (typeof requestBody.notes !== 'undefined' && userDashboard.data.notes !== requestBody.notes) {
|
if (typeof requestBody.notes !== 'undefined' && userDashboard.data.notes !== requestBody.notes) {
|
||||||
userDashboard.data.notes = requestBody.notes;
|
userDashboard.data.notes = requestBody.notes;
|
||||||
|
|
||||||
await updateDashboard(userDashboard);
|
await DashboardModel.update(userDashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true };
|
const responseBody: ResponseBody = { success: true };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Budget, FreshContextState } from '/lib/types.ts';
|
import { Budget, FreshContextState } from '/lib/types.ts';
|
||||||
import { createBudget, getBudgets } from '/lib/data/expenses.ts';
|
import { BudgetModel } from '/lib/models/expenses.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newBudget = await createBudget(
|
const newBudget = await BudgetModel.create(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.name,
|
requestBody.name,
|
||||||
requestBody.month,
|
requestBody.month,
|
||||||
@@ -48,7 +48,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response(`${error}`, { status: 500 });
|
return new Response(`${error}`, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBudgets = await getBudgets(context.state.user.id, requestBody.currentMonth);
|
const newBudgets = await BudgetModel.list(context.state.user.id, requestBody.currentMonth);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newBudgets };
|
const responseBody: ResponseBody = { success: true, newBudgets };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
||||||
import { createExpense, getBudgets, getExpenses } from '/lib/data/expenses.ts';
|
import { BudgetModel, ExpenseModel } from '/lib/models/expenses.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newExpense = await createExpense(
|
const newExpense = await ExpenseModel.create(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.cost,
|
requestBody.cost,
|
||||||
requestBody.description,
|
requestBody.description,
|
||||||
@@ -69,9 +69,9 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response(`${error}`, { status: 500 });
|
return new Response(`${error}`, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newExpenses = await getExpenses(context.state.user.id, requestBody.month);
|
const newExpenses = await ExpenseModel.list(context.state.user.id, requestBody.month);
|
||||||
|
|
||||||
const newBudgets = await getBudgets(context.state.user.id, requestBody.month);
|
const newBudgets = await BudgetModel.list(context.state.user.id, requestBody.month);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newExpenses, newBudgets };
|
const responseBody: ResponseBody = { success: true, newExpenses, newBudgets };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { getExpenseSuggestions } from '/lib/data/expenses.ts';
|
import { ExpenseModel } from '/lib/models/expenses.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad request', { status: 400 });
|
return new Response('Bad request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestions = await getExpenseSuggestions(
|
const suggestions = await ExpenseModel.listSuggestions(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.name,
|
requestBody.name,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Budget, FreshContextState } from '/lib/types.ts';
|
import { Budget, FreshContextState } from '/lib/types.ts';
|
||||||
import { deleteBudget, getBudgetById, getBudgets } from '/lib/data/expenses.ts';
|
import { BudgetModel } from '/lib/models/expenses.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -29,20 +29,20 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad request', { status: 400 });
|
return new Response('Bad request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const budget = await getBudgetById(context.state.user.id, requestBody.id);
|
const budget = await BudgetModel.getById(context.state.user.id, requestBody.id);
|
||||||
|
|
||||||
if (!budget) {
|
if (!budget) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteBudget(context.state.user.id, requestBody.id);
|
await BudgetModel.delete(context.state.user.id, requestBody.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return new Response(`${error}`, { status: 500 });
|
return new Response(`${error}`, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBudgets = await getBudgets(context.state.user.id, requestBody.currentMonth);
|
const newBudgets = await BudgetModel.list(context.state.user.id, requestBody.currentMonth);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newBudgets };
|
const responseBody: ResponseBody = { success: true, newBudgets };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
||||||
import { deleteExpense, getBudgets, getExpenseById, getExpenses } from '/lib/data/expenses.ts';
|
import { BudgetModel, ExpenseModel } from '/lib/models/expenses.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -30,22 +30,22 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad request', { status: 400 });
|
return new Response('Bad request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const expense = await getExpenseById(context.state.user.id, requestBody.id);
|
const expense = await ExpenseModel.getById(context.state.user.id, requestBody.id);
|
||||||
|
|
||||||
if (!expense) {
|
if (!expense) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteExpense(context.state.user.id, requestBody.id);
|
await ExpenseModel.delete(context.state.user.id, requestBody.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return new Response(`${error}`, { status: 500 });
|
return new Response(`${error}`, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newExpenses = await getExpenses(context.state.user.id, requestBody.month);
|
const newExpenses = await ExpenseModel.list(context.state.user.id, requestBody.month);
|
||||||
|
|
||||||
const newBudgets = await getBudgets(context.state.user.id, requestBody.month);
|
const newBudgets = await BudgetModel.list(context.state.user.id, requestBody.month);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newExpenses, newBudgets };
|
const responseBody: ResponseBody = { success: true, newExpenses, newBudgets };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
||||||
import { getAllBudgetsForExport, getAllExpensesForExport } from '/lib/data/expenses.ts';
|
import { BudgetModel, ExpenseModel } from '/lib/models/expenses.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newExpenses = await getAllExpensesForExport(context.state.user.id);
|
const newExpenses = await ExpenseModel.getAllForExport(context.state.user.id);
|
||||||
|
|
||||||
const newBudgets = await getAllBudgetsForExport(context.state.user.id);
|
const newBudgets = await BudgetModel.getAllForExport(context.state.user.id);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, jsonContents: { expenses: newExpenses, budgets: newBudgets } };
|
const responseBody: ResponseBody = { success: true, jsonContents: { expenses: newExpenses, budgets: newBudgets } };
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import { Handlers } from 'fresh/server.ts';
|
|||||||
|
|
||||||
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
||||||
import { concurrentPromises } from '/lib/utils/misc.ts';
|
import { concurrentPromises } from '/lib/utils/misc.ts';
|
||||||
import {
|
import { BudgetModel, deleteAllBudgetsAndExpenses, ExpenseModel } from '/lib/models/expenses.ts';
|
||||||
createBudget,
|
|
||||||
createExpense,
|
|
||||||
deleteAllBudgetsAndExpenses,
|
|
||||||
getBudgets,
|
|
||||||
getExpenses,
|
|
||||||
} from '/lib/data/expenses.ts';
|
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -47,14 +41,14 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
try {
|
try {
|
||||||
await concurrentPromises(
|
await concurrentPromises(
|
||||||
requestBody.budgets.map((budget) => () =>
|
requestBody.budgets.map((budget) => () =>
|
||||||
createBudget(context.state.user!.id, budget.name, budget.month, budget.value)
|
BudgetModel.create(context.state.user!.id, budget.name, budget.month, budget.value)
|
||||||
),
|
),
|
||||||
5,
|
5,
|
||||||
);
|
);
|
||||||
|
|
||||||
await concurrentPromises(
|
await concurrentPromises(
|
||||||
requestBody.expenses.map((expense) => () =>
|
requestBody.expenses.map((expense) => () =>
|
||||||
createExpense(
|
ExpenseModel.create(
|
||||||
context.state.user!.id,
|
context.state.user!.id,
|
||||||
expense.cost,
|
expense.cost,
|
||||||
expense.description,
|
expense.description,
|
||||||
@@ -71,9 +65,9 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response(`${error}`, { status: 500 });
|
return new Response(`${error}`, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newExpenses = await getExpenses(context.state.user.id, requestBody.month);
|
const newExpenses = await ExpenseModel.list(context.state.user.id, requestBody.month);
|
||||||
|
|
||||||
const newBudgets = await getBudgets(context.state.user.id, requestBody.month);
|
const newBudgets = await BudgetModel.list(context.state.user.id, requestBody.month);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newExpenses, newBudgets };
|
const responseBody: ResponseBody = { success: true, newExpenses, newBudgets };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Budget, FreshContextState } from '/lib/types.ts';
|
import { Budget, FreshContextState } from '/lib/types.ts';
|
||||||
import { getBudgetById, getBudgets, updateBudget } from '/lib/data/expenses.ts';
|
import { BudgetModel } from '/lib/models/expenses.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad request', { status: 400 });
|
return new Response('Bad request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const budget = await getBudgetById(context.state.user.id, requestBody.id);
|
const budget = await BudgetModel.getById(context.state.user.id, requestBody.id);
|
||||||
|
|
||||||
if (!budget) {
|
if (!budget) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
@@ -45,13 +45,13 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
budget.value = requestBody.value;
|
budget.value = requestBody.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateBudget(budget);
|
await BudgetModel.update(budget);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return new Response(`${error}`, { status: 500 });
|
return new Response(`${error}`, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBudgets = await getBudgets(context.state.user.id, requestBody.currentMonth);
|
const newBudgets = await BudgetModel.list(context.state.user.id, requestBody.currentMonth);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newBudgets };
|
const responseBody: ResponseBody = { success: true, newBudgets };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
|
||||||
import { getBudgets, getExpenseById, getExpenses, updateExpense } from '/lib/data/expenses.ts';
|
import { BudgetModel, ExpenseModel } from '/lib/models/expenses.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad request', { status: 400 });
|
return new Response('Bad request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const expense = await getExpenseById(context.state.user.id, requestBody.id);
|
const expense = await ExpenseModel.getById(context.state.user.id, requestBody.id);
|
||||||
|
|
||||||
if (!expense) {
|
if (!expense) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
@@ -65,15 +65,15 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
expense.is_recurring = requestBody.is_recurring;
|
expense.is_recurring = requestBody.is_recurring;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateExpense(expense);
|
await ExpenseModel.update(expense);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return new Response(`${error}`, { status: 500 });
|
return new Response(`${error}`, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newExpenses = await getExpenses(context.state.user.id, requestBody.month);
|
const newExpenses = await ExpenseModel.list(context.state.user.id, requestBody.month);
|
||||||
|
|
||||||
const newBudgets = await getBudgets(context.state.user.id, requestBody.month);
|
const newBudgets = await BudgetModel.list(context.state.user.id, requestBody.month);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newExpenses, newBudgets };
|
const responseBody: ResponseBody = { success: true, newExpenses, newBudgets };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { createDirectory, getDirectories } from '/lib/data/files.ts';
|
import { DirectoryModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdDirectory = await createDirectory(
|
const createdDirectory = await DirectoryModel.create(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
const newDirectories = await DirectoryModel.list(context.state.user.id, requestBody.parentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: createdDirectory, newDirectories };
|
const responseBody: ResponseBody = { success: createdDirectory, newDirectories };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { deleteDirectoryOrFile, getDirectories } from '/lib/data/files.ts';
|
import { DirectoryModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedDirectory = await deleteDirectoryOrFile(
|
const deletedDirectory = await DirectoryModel.delete(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
const newDirectories = await DirectoryModel.list(context.state.user.id, requestBody.parentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: deletedDirectory, newDirectories };
|
const responseBody: ResponseBody = { success: deletedDirectory, newDirectories };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { deleteDirectoryOrFile, getFiles } from '/lib/data/files.ts';
|
import { FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedFile = await deleteDirectoryOrFile(
|
const deletedFile = await FileModel.delete(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newFiles = await getFiles(context.state.user.id, requestBody.parentPath);
|
const newFiles = await FileModel.list(context.state.user.id, requestBody.parentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: deletedFile, newFiles };
|
const responseBody: ResponseBody = { success: deletedFile, newFiles };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectories } from '/lib/data/files.ts';
|
import { DirectoryModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const directories = await getDirectories(
|
const directories = await DirectoryModel.list(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { getFiles } from '/lib/data/files.ts';
|
import { FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = await getFiles(
|
const files = await FileModel.list(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectories, renameDirectoryOrFile } from '/lib/data/files.ts';
|
import { DirectoryModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const movedDirectory = await renameDirectoryOrFile(
|
const movedDirectory = await DirectoryModel.rename(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.oldParentPath,
|
requestBody.oldParentPath,
|
||||||
requestBody.newParentPath,
|
requestBody.newParentPath,
|
||||||
@@ -41,7 +41,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.oldParentPath);
|
const newDirectories = await DirectoryModel.list(context.state.user.id, requestBody.oldParentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: movedDirectory, newDirectories };
|
const responseBody: ResponseBody = { success: movedDirectory, newDirectories };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { getFiles, renameDirectoryOrFile } from '/lib/data/files.ts';
|
import { FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const movedFile = await renameDirectoryOrFile(
|
const movedFile = await FileModel.rename(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.oldParentPath,
|
requestBody.oldParentPath,
|
||||||
requestBody.newParentPath,
|
requestBody.newParentPath,
|
||||||
@@ -41,7 +41,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newFiles = await getFiles(context.state.user.id, requestBody.oldParentPath);
|
const newFiles = await FileModel.list(context.state.user.id, requestBody.oldParentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: movedFile, newFiles };
|
const responseBody: ResponseBody = { success: movedFile, newFiles };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectories, renameDirectoryOrFile } from '/lib/data/files.ts';
|
import { DirectoryModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const movedDirectory = await renameDirectoryOrFile(
|
const movedDirectory = await DirectoryModel.rename(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
@@ -40,7 +40,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
requestBody.newName.trim(),
|
requestBody.newName.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
const newDirectories = await DirectoryModel.list(context.state.user.id, requestBody.parentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: movedDirectory, newDirectories };
|
const responseBody: ResponseBody = { success: movedDirectory, newDirectories };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { getFiles, renameDirectoryOrFile } from '/lib/data/files.ts';
|
import { FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const movedFile = await renameDirectoryOrFile(
|
const movedFile = await FileModel.rename(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
@@ -40,7 +40,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
requestBody.newName.trim(),
|
requestBody.newName.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newFiles = await getFiles(context.state.user.id, requestBody.parentPath);
|
const newFiles = await FileModel.list(context.state.user.id, requestBody.parentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: movedFile, newFiles };
|
const responseBody: ResponseBody = { success: movedFile, newFiles };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { searchFilesAndDirectories } from '/lib/data/files.ts';
|
import { searchFilesAndDirectories } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { createFile, getDirectories, getFiles } from '/lib/data/files.ts';
|
import { DirectoryModel, FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -33,10 +33,10 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
const fileContents = typeof contents === 'string' ? contents : await contents.arrayBuffer();
|
const fileContents = typeof contents === 'string' ? contents : await contents.arrayBuffer();
|
||||||
|
|
||||||
const createdFile = await createFile(context.state.user.id, parentPath, name.trim(), fileContents);
|
const createdFile = await FileModel.create(context.state.user.id, parentPath, name.trim(), fileContents);
|
||||||
|
|
||||||
const newFiles = await getFiles(context.state.user.id, pathInView);
|
const newFiles = await FileModel.list(context.state.user.id, pathInView);
|
||||||
const newDirectories = await getDirectories(context.state.user.id, pathInView);
|
const newDirectories = await DirectoryModel.list(context.state.user.id, pathInView);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: createdFile, newFiles, newDirectories };
|
const responseBody: ResponseBody = { success: createdFile, newFiles, newDirectories };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState, NewsFeed } from '/lib/types.ts';
|
import { FreshContextState, NewsFeed } from '/lib/types.ts';
|
||||||
import { createNewsFeed, getNewsFeeds } from '/lib/data/news.ts';
|
import { FeedModel } from '/lib/models/news.ts';
|
||||||
import { fetchNewArticles } from '/crons/news.ts';
|
import { fetchNewArticles } from '/crons/news.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
@@ -24,7 +24,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
const requestBody = await request.clone().json() as RequestBody;
|
const requestBody = await request.clone().json() as RequestBody;
|
||||||
|
|
||||||
if (requestBody.feedUrl) {
|
if (requestBody.feedUrl) {
|
||||||
const newFeed = await createNewsFeed(context.state.user.id, requestBody.feedUrl);
|
const newFeed = await FeedModel.create(context.state.user.id, requestBody.feedUrl);
|
||||||
|
|
||||||
if (!newFeed) {
|
if (!newFeed) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
@@ -33,7 +33,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
await fetchNewArticles();
|
await fetchNewArticles();
|
||||||
|
|
||||||
const newFeeds = await getNewsFeeds(context.state.user.id);
|
const newFeeds = await FeedModel.list(context.state.user.id);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newFeeds };
|
const responseBody: ResponseBody = { success: true, newFeeds };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState, NewsFeed } from '/lib/types.ts';
|
import { FreshContextState, NewsFeed } from '/lib/types.ts';
|
||||||
import { deleteNewsFeed, getNewsFeed, getNewsFeeds } from '/lib/data/news.ts';
|
import { FeedModel } from '/lib/models/news.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -23,16 +23,16 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
const requestBody = await request.clone().json() as RequestBody;
|
const requestBody = await request.clone().json() as RequestBody;
|
||||||
|
|
||||||
if (requestBody.feedId) {
|
if (requestBody.feedId) {
|
||||||
const newsFeed = await getNewsFeed(requestBody.feedId, context.state.user.id);
|
const newsFeed = await FeedModel.get(requestBody.feedId, context.state.user.id);
|
||||||
|
|
||||||
if (!newsFeed) {
|
if (!newsFeed) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteNewsFeed(requestBody.feedId, context.state.user.id);
|
await FeedModel.delete(requestBody.feedId, context.state.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFeeds = await getNewsFeeds(context.state.user.id);
|
const newFeeds = await FeedModel.list(context.state.user.id);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newFeeds };
|
const responseBody: ResponseBody = { success: true, newFeeds };
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Handlers } from 'fresh/server.ts';
|
|||||||
|
|
||||||
import { FreshContextState, NewsFeed } from '/lib/types.ts';
|
import { FreshContextState, NewsFeed } from '/lib/types.ts';
|
||||||
import { concurrentPromises } from '/lib/utils/misc.ts';
|
import { concurrentPromises } from '/lib/utils/misc.ts';
|
||||||
import { createNewsFeed, getNewsFeeds } from '/lib/data/news.ts';
|
import { FeedModel } from '/lib/models/news.ts';
|
||||||
import { fetchNewArticles } from '/crons/news.ts';
|
import { fetchNewArticles } from '/crons/news.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
@@ -30,14 +30,14 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await concurrentPromises(
|
await concurrentPromises(
|
||||||
requestBody.feedUrls.map((feedUrl) => () => createNewsFeed(context.state.user!.id, feedUrl)),
|
requestBody.feedUrls.map((feedUrl) => () => FeedModel.create(context.state.user!.id, feedUrl)),
|
||||||
5,
|
5,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchNewArticles();
|
await fetchNewArticles();
|
||||||
|
|
||||||
const newFeeds = await getNewsFeeds(context.state.user.id);
|
const newFeeds = await FeedModel.list(context.state.user.id);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newFeeds };
|
const responseBody: ResponseBody = { success: true, newFeeds };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { getNewsArticle, markAllArticlesRead, updateNewsArticle } from '/lib/data/news.ts';
|
import { ArticleModel } from '/lib/models/news.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -23,9 +23,9 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
if (requestBody.articleId) {
|
if (requestBody.articleId) {
|
||||||
if (requestBody.articleId === 'all') {
|
if (requestBody.articleId === 'all') {
|
||||||
await markAllArticlesRead(context.state.user.id);
|
await ArticleModel.markAllRead(context.state.user.id);
|
||||||
} else {
|
} else {
|
||||||
const article = await getNewsArticle(requestBody.articleId, context.state.user.id);
|
const article = await ArticleModel.get(requestBody.articleId, context.state.user.id);
|
||||||
|
|
||||||
if (!article) {
|
if (!article) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
@@ -33,7 +33,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
article.is_read = true;
|
article.is_read = true;
|
||||||
|
|
||||||
await updateNewsArticle(article);
|
await ArticleModel.update(article);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState, NewsFeedArticle } from '/lib/types.ts';
|
import { FreshContextState, NewsFeedArticle } from '/lib/types.ts';
|
||||||
import { getNewsArticles, getNewsFeeds } from '/lib/data/news.ts';
|
import { ArticleModel, FeedModel } from '/lib/models/news.ts';
|
||||||
import { fetchNewArticles } from '/crons/news.ts';
|
import { fetchNewArticles } from '/crons/news.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
@@ -19,7 +19,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newsFeeds = await getNewsFeeds(context.state.user.id);
|
const newsFeeds = await FeedModel.list(context.state.user.id);
|
||||||
|
|
||||||
if (!newsFeeds.length) {
|
if (!newsFeeds.length) {
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
@@ -27,7 +27,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
await fetchNewArticles(true);
|
await fetchNewArticles(true);
|
||||||
|
|
||||||
const newArticles = await getNewsArticles(context.state.user.id);
|
const newArticles = await ArticleModel.list(context.state.user.id);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: true, newArticles };
|
const responseBody: ResponseBody = { success: true, newArticles };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { getFile, updateFile } from '/lib/data/files.ts';
|
import { FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileResult = await getFile(
|
const fileResult = await FileModel.get(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.currentPath,
|
requestBody.currentPath,
|
||||||
decodeURIComponent(requestBody.fileName),
|
decodeURIComponent(requestBody.fileName),
|
||||||
@@ -52,7 +52,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedFile = await updateFile(
|
const updatedFile = await FileModel.update(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.currentPath,
|
requestBody.currentPath,
|
||||||
decodeURIComponent(requestBody.fileName),
|
decodeURIComponent(requestBody.fileName),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Dashboard, FreshContextState } from '/lib/types.ts';
|
import { Dashboard, FreshContextState } from '/lib/types.ts';
|
||||||
import { createDashboard, getDashboardByUserId } from '/lib/data/dashboard.ts';
|
import { DashboardModel } from '/lib/models/dashboard.ts';
|
||||||
import Notes from '/islands/dashboard/Notes.tsx';
|
import Notes from '/islands/dashboard/Notes.tsx';
|
||||||
import Links from '/islands/dashboard/Links.tsx';
|
import Links from '/islands/dashboard/Links.tsx';
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||||
}
|
}
|
||||||
|
|
||||||
let userDashboard = await getDashboardByUserId(context.state.user.id);
|
let userDashboard = await DashboardModel.getByUserId(context.state.user.id);
|
||||||
|
|
||||||
if (!userDashboard) {
|
if (!userDashboard) {
|
||||||
userDashboard = await createDashboard(context.state.user.id);
|
userDashboard = await DashboardModel.create(context.state.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await context.render({ userDashboard });
|
return await context.render({ userDashboard });
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
getProperDestinationPath,
|
getProperDestinationPath,
|
||||||
getPropertyNames,
|
getPropertyNames,
|
||||||
} from '/lib/utils/webdav.ts';
|
} from '/lib/utils/webdav.ts';
|
||||||
import { ensureUserPathIsValidAndSecurelyAccessible, getFile } from '/lib/data/files.ts';
|
import { ensureUserPathIsValidAndSecurelyAccessible, FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -53,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(userId, filePath);
|
const fileResult = await FileModel.get(userId, filePath);
|
||||||
|
|
||||||
if (!fileResult.success) {
|
if (!fileResult.success) {
|
||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
|
|||||||
|
|
||||||
import { Budget, Expense, FreshContextState, SupportedCurrencySymbol } from '/lib/types.ts';
|
import { Budget, Expense, FreshContextState, SupportedCurrencySymbol } from '/lib/types.ts';
|
||||||
import { isAppEnabled } from '/lib/config.ts';
|
import { isAppEnabled } from '/lib/config.ts';
|
||||||
import { generateMonthlyBudgetsAndExpenses, getBudgets, getExpenses } from '/lib/data/expenses.ts';
|
import { BudgetModel, ExpenseModel, generateMonthlyBudgetsAndExpenses } from '/lib/models/expenses.ts';
|
||||||
import ExpensesWrapper from '/islands/expenses/ExpensesWrapper.tsx';
|
import ExpensesWrapper from '/islands/expenses/ExpensesWrapper.tsx';
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
@@ -41,16 +41,16 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
initialMonth = nextMonth;
|
initialMonth = nextMonth;
|
||||||
}
|
}
|
||||||
|
|
||||||
let userBudgets = await getBudgets(context.state.user.id, initialMonth);
|
let userBudgets = await BudgetModel.list(context.state.user.id, initialMonth);
|
||||||
|
|
||||||
let userExpenses = await getExpenses(context.state.user.id, initialMonth);
|
let userExpenses = await ExpenseModel.list(context.state.user.id, initialMonth);
|
||||||
|
|
||||||
// If there are no budgets or expenses, and the selected month is in the future, generate the month's budgets and expenses
|
// If there are no budgets or expenses, and the selected month is in the future, generate the month's budgets and expenses
|
||||||
if (userBudgets.length === 0 && userExpenses.length === 0 && initialMonth >= currentMonth) {
|
if (userBudgets.length === 0 && userExpenses.length === 0 && initialMonth >= currentMonth) {
|
||||||
await generateMonthlyBudgetsAndExpenses(context.state.user.id, initialMonth);
|
await generateMonthlyBudgetsAndExpenses(context.state.user.id, initialMonth);
|
||||||
|
|
||||||
userBudgets = await getBudgets(context.state.user.id, initialMonth);
|
userBudgets = await BudgetModel.list(context.state.user.id, initialMonth);
|
||||||
userExpenses = await getExpenses(context.state.user.id, initialMonth);
|
userExpenses = await ExpenseModel.list(context.state.user.id, initialMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currency = context.state.user.extra.expenses_currency || '$';
|
const currency = context.state.user.extra.expenses_currency || '$';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectories, getFiles } from '/lib/data/files.ts';
|
import { DirectoryModel, FileModel } from '/lib/models/files.ts';
|
||||||
import FilesWrapper from '/islands/files/FilesWrapper.tsx';
|
import FilesWrapper from '/islands/files/FilesWrapper.tsx';
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
@@ -30,9 +30,9 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
currentPath = `${currentPath}/`;
|
currentPath = `${currentPath}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDirectories = await getDirectories(context.state.user.id, currentPath);
|
const userDirectories = await DirectoryModel.list(context.state.user.id, currentPath);
|
||||||
|
|
||||||
const userFiles = await getFiles(context.state.user.id, currentPath);
|
const userFiles = await FileModel.list(context.state.user.id, currentPath);
|
||||||
|
|
||||||
return await context.render({ userDirectories, userFiles, currentPath });
|
return await context.render({ userDirectories, userFiles, currentPath });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { getFile } from '/lib/data/files.ts';
|
import { FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
currentPath = `${currentPath}/`;
|
currentPath = `${currentPath}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileResult = await getFile(context.state.user.id, currentPath, decodeURIComponent(fileName));
|
const fileResult = await FileModel.get(context.state.user.id, currentPath, decodeURIComponent(fileName));
|
||||||
|
|
||||||
if (!fileResult.success) {
|
if (!fileResult.success) {
|
||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
|
|||||||
import { generateHash, helpEmail, validateEmail } from '/lib/utils/misc.ts';
|
import { generateHash, helpEmail, validateEmail } from '/lib/utils/misc.ts';
|
||||||
import { createSessionResponse, PASSWORD_SALT } from '/lib/auth.ts';
|
import { createSessionResponse, PASSWORD_SALT } from '/lib/auth.ts';
|
||||||
import { FormField, generateFieldHtml, getFormDataField } from '/lib/form-utils.tsx';
|
import { FormField, generateFieldHtml, getFormDataField } from '/lib/form-utils.tsx';
|
||||||
import { createVerificationCode, getUserByEmail, updateUser, validateVerificationCode } from '/lib/data/user.ts';
|
import { UserModel, VerificationCodeModel } from '/lib/models/user.ts';
|
||||||
import { sendVerifyEmailEmail } from '/lib/providers/brevo.ts';
|
import { sendVerifyEmailEmail } from '/lib/providers/brevo.ts';
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { isEmailEnabled } from '/lib/config.ts';
|
import { isEmailEnabled } from '/lib/config.ts';
|
||||||
@@ -61,7 +61,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
const hashedPassword = await generateHash(`${password}:${PASSWORD_SALT}`, 'SHA-256');
|
const hashedPassword = await generateHash(`${password}:${PASSWORD_SALT}`, 'SHA-256');
|
||||||
|
|
||||||
const user = await getUserByEmail(email);
|
const user = await UserModel.getByEmail(email);
|
||||||
|
|
||||||
if (!user || user.hashed_password !== hashedPassword) {
|
if (!user || user.hashed_password !== hashedPassword) {
|
||||||
throw new Error('Email not found or invalid password.');
|
throw new Error('Email not found or invalid password.');
|
||||||
@@ -70,24 +70,24 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
if (!isEmailEnabled() && !user.extra.is_email_verified) {
|
if (!isEmailEnabled() && !user.extra.is_email_verified) {
|
||||||
user.extra.is_email_verified = true;
|
user.extra.is_email_verified = true;
|
||||||
|
|
||||||
await updateUser(user);
|
await UserModel.update(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.extra.is_email_verified) {
|
if (!user.extra.is_email_verified) {
|
||||||
const code = getFormDataField(formData, 'verification-code');
|
const code = getFormDataField(formData, 'verification-code');
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
const verificationCode = await createVerificationCode(user, user.email, 'email');
|
const verificationCode = await VerificationCodeModel.create(user, user.email, 'email');
|
||||||
|
|
||||||
await sendVerifyEmailEmail(user.email, verificationCode);
|
await sendVerifyEmailEmail(user.email, verificationCode);
|
||||||
|
|
||||||
throw new Error('Email not verified. New code sent to verify your email.');
|
throw new Error('Email not verified. New code sent to verify your email.');
|
||||||
} else {
|
} else {
|
||||||
await validateVerificationCode(user, user.email, code, 'email');
|
await VerificationCodeModel.validate(user, user.email, code, 'email');
|
||||||
|
|
||||||
user.extra.is_email_verified = true;
|
user.extra.is_email_verified = true;
|
||||||
|
|
||||||
await updateUser(user);
|
await UserModel.update(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
|
|||||||
|
|
||||||
import { FreshContextState, NewsFeedArticle } from '/lib/types.ts';
|
import { FreshContextState, NewsFeedArticle } from '/lib/types.ts';
|
||||||
import { isAppEnabled } from '/lib/config.ts';
|
import { isAppEnabled } from '/lib/config.ts';
|
||||||
import { getNewsArticles } from '/lib/data/news.ts';
|
import { ArticleModel } from '/lib/models/news.ts';
|
||||||
import Articles from '/islands/news/Articles.tsx';
|
import Articles from '/islands/news/Articles.tsx';
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
@@ -19,7 +19,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/dashboard` } });
|
return new Response('Redirect', { status: 303, headers: { 'Location': `/dashboard` } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userArticles = await getNewsArticles(context.state.user.id);
|
const userArticles = await ArticleModel.list(context.state.user.id);
|
||||||
|
|
||||||
return await context.render({ userArticles });
|
return await context.render({ userArticles });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState, NewsFeed } from '/lib/types.ts';
|
import { FreshContextState, NewsFeed } from '/lib/types.ts';
|
||||||
import { getNewsFeeds } from '/lib/data/news.ts';
|
import { FeedModel } from '/lib/models/news.ts';
|
||||||
import Feeds from '/islands/news/Feeds.tsx';
|
import Feeds from '/islands/news/Feeds.tsx';
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
@@ -14,7 +14,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userFeeds = await getNewsFeeds(context.state.user.id);
|
const userFeeds = await FeedModel.list(context.state.user.id);
|
||||||
|
|
||||||
return await context.render({ userFeeds });
|
return await context.render({ userFeeds });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
|
|||||||
|
|
||||||
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { isAppEnabled } from '/lib/config.ts';
|
import { isAppEnabled } from '/lib/config.ts';
|
||||||
import { getDirectories, getFiles } from '/lib/data/files.ts';
|
import { DirectoryModel, FileModel } from '/lib/models/files.ts';
|
||||||
import NotesWrapper from '/islands/notes/NotesWrapper.tsx';
|
import NotesWrapper from '/islands/notes/NotesWrapper.tsx';
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
@@ -35,9 +35,9 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
currentPath = `${currentPath}/`;
|
currentPath = `${currentPath}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDirectories = await getDirectories(context.state.user.id, currentPath);
|
const userDirectories = await DirectoryModel.list(context.state.user.id, currentPath);
|
||||||
|
|
||||||
const userFiles = await getFiles(context.state.user.id, currentPath);
|
const userFiles = await FileModel.list(context.state.user.id, currentPath);
|
||||||
|
|
||||||
const userNotes = userFiles.filter((file) => file.file_name.endsWith('.md'));
|
const userNotes = userFiles.filter((file) => file.file_name.endsWith('.md'));
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { getFile } from '/lib/data/files.ts';
|
import { FileModel } from '/lib/models/files.ts';
|
||||||
import Note from '/islands/notes/Note.tsx';
|
import Note from '/islands/notes/Note.tsx';
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
@@ -41,7 +41,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileResult = await getFile(context.state.user.id, currentPath, decodeURIComponent(fileName));
|
const fileResult = await FileModel.get(context.state.user.id, currentPath, decodeURIComponent(fileName));
|
||||||
|
|
||||||
if (!fileResult.success) {
|
if (!fileResult.success) {
|
||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
|
|||||||
|
|
||||||
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { isAppEnabled } from '/lib/config.ts';
|
import { isAppEnabled } from '/lib/config.ts';
|
||||||
import { getDirectories, getFiles } from '/lib/data/files.ts';
|
import { DirectoryModel, FileModel } from '/lib/models/files.ts';
|
||||||
import { PHOTO_EXTENSIONS } from '/lib/utils/photos.ts';
|
import { PHOTO_EXTENSIONS } from '/lib/utils/photos.ts';
|
||||||
import PhotosWrapper from '/islands/photos/PhotosWrapper.tsx';
|
import PhotosWrapper from '/islands/photos/PhotosWrapper.tsx';
|
||||||
|
|
||||||
@@ -36,9 +36,9 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
currentPath = `${currentPath}/`;
|
currentPath = `${currentPath}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDirectories = await getDirectories(context.state.user.id, currentPath);
|
const userDirectories = await DirectoryModel.list(context.state.user.id, currentPath);
|
||||||
|
|
||||||
const userFiles = await getFiles(context.state.user.id, currentPath);
|
const userFiles = await FileModel.list(context.state.user.id, currentPath);
|
||||||
|
|
||||||
const userPhotos = userFiles.filter((file) => {
|
const userPhotos = userFiles.filter((file) => {
|
||||||
const lowercaseFileName = file.file_name.toLowerCase();
|
const lowercaseFileName = file.file_name.toLowerCase();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Handlers } from 'fresh/server.ts';
|
|||||||
import { resize } from 'https://deno.land/x/deno_image@0.0.4/mod.ts';
|
import { resize } from 'https://deno.land/x/deno_image@0.0.4/mod.ts';
|
||||||
|
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { getFile } from '/lib/data/files.ts';
|
import { FileModel } from '/lib/models/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
currentPath = `${currentPath}/`;
|
currentPath = `${currentPath}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileResult = await getFile(context.state.user.id, currentPath, decodeURIComponent(fileName));
|
const fileResult = await FileModel.get(context.state.user.id, currentPath, decodeURIComponent(fileName));
|
||||||
|
|
||||||
if (!fileResult.success) {
|
if (!fileResult.success) {
|
||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
|
|||||||
|
|
||||||
import { currencyMap, FreshContextState, SupportedCurrencySymbol } from '/lib/types.ts';
|
import { currencyMap, FreshContextState, SupportedCurrencySymbol } from '/lib/types.ts';
|
||||||
import { PASSWORD_SALT } from '/lib/auth.ts';
|
import { PASSWORD_SALT } from '/lib/auth.ts';
|
||||||
import {
|
import { UserModel, VerificationCodeModel } from '/lib/models/user.ts';
|
||||||
createVerificationCode,
|
|
||||||
deleteUser,
|
|
||||||
getUserByEmail,
|
|
||||||
updateUser,
|
|
||||||
validateVerificationCode,
|
|
||||||
} from '/lib/data/user.ts';
|
|
||||||
import { convertFormDataToObject, generateHash, validateEmail } from '/lib/utils/misc.ts';
|
import { convertFormDataToObject, generateHash, validateEmail } from '/lib/utils/misc.ts';
|
||||||
import { getFormDataField } from '/lib/form-utils.tsx';
|
import { getFormDataField } from '/lib/form-utils.tsx';
|
||||||
import { sendVerifyEmailEmail } from '/lib/providers/brevo.ts';
|
import { sendVerifyEmailEmail } from '/lib/providers/brevo.ts';
|
||||||
@@ -72,14 +66,14 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
throw new Error(`New email is the same as the current email.`);
|
throw new Error(`New email is the same as the current email.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchingUser = await getUserByEmail(email);
|
const matchingUser = await UserModel.getByEmail(email);
|
||||||
|
|
||||||
if (matchingUser) {
|
if (matchingUser) {
|
||||||
throw new Error('Email is already in use.');
|
throw new Error('Email is already in use.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'change-email' && isEmailEnabled()) {
|
if (action === 'change-email' && isEmailEnabled()) {
|
||||||
const verificationCode = await createVerificationCode(user, email, 'email');
|
const verificationCode = await VerificationCodeModel.create(user, email, 'email');
|
||||||
|
|
||||||
await sendVerifyEmailEmail(email, verificationCode);
|
await sendVerifyEmailEmail(email, verificationCode);
|
||||||
|
|
||||||
@@ -89,12 +83,12 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
if (isEmailEnabled()) {
|
if (isEmailEnabled()) {
|
||||||
const code = getFormDataField(formData, 'verification-code');
|
const code = getFormDataField(formData, 'verification-code');
|
||||||
|
|
||||||
await validateVerificationCode(user, email, code, 'email');
|
await VerificationCodeModel.validate(user, email, code, 'email');
|
||||||
}
|
}
|
||||||
|
|
||||||
user.email = email;
|
user.email = email;
|
||||||
|
|
||||||
await updateUser(user);
|
await UserModel.update(user);
|
||||||
|
|
||||||
successTitle = 'Email updated!';
|
successTitle = 'Email updated!';
|
||||||
successMessage = 'Email updated successfully.';
|
successMessage = 'Email updated successfully.';
|
||||||
@@ -120,7 +114,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
user.hashed_password = hashedNewPassword;
|
user.hashed_password = hashedNewPassword;
|
||||||
|
|
||||||
await updateUser(user);
|
await UserModel.update(user);
|
||||||
|
|
||||||
successTitle = 'Password changed!';
|
successTitle = 'Password changed!';
|
||||||
successMessage = 'Password changed successfully.';
|
successMessage = 'Password changed successfully.';
|
||||||
@@ -139,7 +133,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
user.extra.dav_hashed_password = hashedNewDavPassword;
|
user.extra.dav_hashed_password = hashedNewDavPassword;
|
||||||
|
|
||||||
await updateUser(user);
|
await UserModel.update(user);
|
||||||
|
|
||||||
successTitle = 'DAV Password changed!';
|
successTitle = 'DAV Password changed!';
|
||||||
successMessage = 'DAV Password changed successfully.';
|
successMessage = 'DAV Password changed successfully.';
|
||||||
@@ -152,7 +146,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
throw new Error('Invalid current password.');
|
throw new Error('Invalid current password.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteUser(user.id);
|
await UserModel.delete(user.id);
|
||||||
|
|
||||||
return new Response('Account deleted successfully', {
|
return new Response('Account deleted successfully', {
|
||||||
status: 303,
|
status: 303,
|
||||||
@@ -167,7 +161,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
user.extra.expenses_currency = newCurrencySymbol;
|
user.extra.expenses_currency = newCurrencySymbol;
|
||||||
|
|
||||||
await updateUser(user);
|
await UserModel.update(user);
|
||||||
|
|
||||||
successTitle = 'Currency changed!';
|
successTitle = 'Currency changed!';
|
||||||
successMessage = 'Currency changed successfully.';
|
successMessage = 'Currency changed successfully.';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
|
|||||||
import { generateHash, helpEmail, validateEmail } from '/lib/utils/misc.ts';
|
import { generateHash, helpEmail, validateEmail } from '/lib/utils/misc.ts';
|
||||||
import { PASSWORD_SALT } from '/lib/auth.ts';
|
import { PASSWORD_SALT } from '/lib/auth.ts';
|
||||||
import { FormField, generateFieldHtml, getFormDataField } from '/lib/form-utils.tsx';
|
import { FormField, generateFieldHtml, getFormDataField } from '/lib/form-utils.tsx';
|
||||||
import { createUser, createVerificationCode, getUserByEmail, updateUser } from '/lib/data/user.ts';
|
import { UserModel, VerificationCodeModel } from '/lib/models/user.ts';
|
||||||
import { sendVerifyEmailEmail } from '/lib/providers/brevo.ts';
|
import { sendVerifyEmailEmail } from '/lib/providers/brevo.ts';
|
||||||
import { isEmailEnabled, isSignupAllowed } from '/lib/config.ts';
|
import { isEmailEnabled, isSignupAllowed } from '/lib/config.ts';
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
@@ -54,7 +54,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
throw new Error(`Password is too short.`);
|
throw new Error(`Password is too short.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUser = await getUserByEmail(email);
|
const existingUser = await UserModel.getByEmail(email);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new Error('Email is already in use. Perhaps you want to login instead?');
|
throw new Error('Email is already in use. Perhaps you want to login instead?');
|
||||||
@@ -62,10 +62,10 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
const hashedPassword = await generateHash(`${password}:${PASSWORD_SALT}`, 'SHA-256');
|
const hashedPassword = await generateHash(`${password}:${PASSWORD_SALT}`, 'SHA-256');
|
||||||
|
|
||||||
const user = await createUser(email, hashedPassword);
|
const user = await UserModel.create(email, hashedPassword);
|
||||||
|
|
||||||
if (isEmailEnabled()) {
|
if (isEmailEnabled()) {
|
||||||
const verificationCode = await createVerificationCode(user, user.email, 'email');
|
const verificationCode = await VerificationCodeModel.create(user, user.email, 'email');
|
||||||
|
|
||||||
await sendVerifyEmailEmail(user.email, verificationCode);
|
await sendVerifyEmailEmail(user.email, verificationCode);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user