Improve App Selection

This allows not enabling Dashboard and Files. It also sorts the apps in the menu according to the order in the `config.core.enabledApps` array.

Since this will require a major version upgrade (`v3.0.0`), I also took the opportunity to upgrade PostgreSQL. You can [follow this guide on how to upgrade PostgreSQL on Docker containers](https://news.onbrn.com/step-by-step-guide-upgrading-postgresql-docker-containers/).

Finally, this has some minor security improvements (confirming API endpoints won't work if their app is disabled in the config).

Closes #114
Closes #108
This commit is contained in:
Bruno Bernardino 2025-12-01 12:25:21 +00:00
parent 3fdda5b34e
commit d86e65475c
No known key found for this signature in database
GPG key ID: D1B0A69ADD114ECE
73 changed files with 384 additions and 62 deletions

View file

@ -21,7 +21,7 @@ const config: PartialDeep<Config> = {
// allowDirectoryDownloads: false, // If true, directories can be downloaded as zip files
// },
// core: {
// enabledApps: ['news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'], // dashboard and files cannot be disabled
// enabledApps: ['dashboard', 'files', 'news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'], // The apps to show, in order of appearance in the header. The first app will be the default one shown after logging in. At least one is required.
// },
// visuals: {
// title: 'My own cloud',

View file

@ -1,6 +1,7 @@
import { Head } from 'fresh/runtime.ts';
import { OptionalApp, User } from '/lib/types.ts';
import { capitalizeWord } from '/lib/utils/misc.ts';
interface Data {
route: string;
@ -23,52 +24,10 @@ export default function Header({ route, user, enabledApps }: Data) {
const iconWidthAndHeightInPixels = 20;
const potentialMenuItems: (MenuItem | null)[] = [
{
url: '/dashboard',
label: 'Dashboard',
},
enabledApps.includes('news')
? {
url: '/news',
label: 'News',
}
: null,
{
url: '/files',
label: 'Files',
},
enabledApps.includes('notes')
? {
url: '/notes',
label: 'Notes',
}
: null,
enabledApps.includes('photos')
? {
url: '/photos',
label: 'Photos',
}
: null,
enabledApps.includes('expenses')
? {
url: '/expenses',
label: 'Expenses',
}
: null,
enabledApps.includes('contacts')
? {
url: '/contacts',
label: 'Contacts',
}
: null,
enabledApps.includes('calendar')
? {
url: '/calendar',
label: 'Calendar',
}
: null,
];
const potentialMenuItems: (MenuItem | null)[] = enabledApps.map((app) => ({
url: `/${app}`,
label: capitalizeWord(app),
}));
const menuItems = potentialMenuItems.filter(Boolean) as MenuItem[];

View file

@ -1,13 +1,13 @@
services:
postgresql:
image: postgres:17
image: postgres:18.1
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=fake
- POSTGRES_DB=bewcloud
restart: on-failure
volumes:
- pgdata:/var/lib/postgresql/data
- pgdata:/var/lib/postgresql/18/docker
ports:
- 5432:5432
ulimits:
@ -18,7 +18,7 @@ services:
# NOTE: If you don't want to use the CardDav/CalDav servers, you can comment/remove this service.
radicale:
image: tomsquest/docker-radicale:3.5.7.0
image: tomsquest/docker-radicale:3.5.8.2
ports:
- 5232:5232
init: true

View file

@ -1,6 +1,6 @@
services:
website:
image: ghcr.io/bewcloud/bewcloud:v2.8.1
image: ghcr.io/bewcloud/bewcloud:v3.0.0
restart: always
ports:
- 127.0.0.1:8000:8000
@ -13,14 +13,14 @@ services:
- ./bewcloud.config.ts:/app/bewcloud.config.ts
postgresql:
image: postgres:17
image: postgres:18.1
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=fake
- POSTGRES_DB=bewcloud
restart: always
volumes:
- bewcloud-db:/var/lib/postgresql/data
- bewcloud-db:/var/lib/postgresql/18/docker
# NOTE: uncomment below only if you need to connect to the database from outside the container
# ports:
# - 127.0.0.1:5432:5432
@ -32,7 +32,7 @@ services:
# NOTE: If you don't want to use the CardDav/CalDav servers, you can comment/remove this service.
radicale:
image: tomsquest/docker-radicale:3.5.7.0
image: tomsquest/docker-radicale:3.5.8.2
# NOTE: uncomment below only if you need to connect to the CardDav/CalDav servers from outside the container
# ports:
# - 127.0.0.1:5232:5232

View file

@ -25,7 +25,7 @@ export class AppConfig {
allowDirectoryDownloads: false,
},
core: {
enabledApps: ['news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'],
enabledApps: ['dashboard', 'files', 'news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'],
},
visuals: {
title: '',
@ -96,6 +96,10 @@ export class AppConfig {
console.info('\nConfig loaded from bewcloud.config.ts', JSON.stringify(this.config, null, 2), '\n');
if (this.config.core.enabledApps.length === 0) {
throw new Error('At least one app must be enabled. Please check the config.core.enabledApps array.');
}
return;
} catch (error) {
console.error('Error loading config from bewcloud.config.ts. Using default config instead.', error);

View file

@ -184,7 +184,9 @@ export class OidcModel {
throw new Error('There was a problem signing up or logging in!');
}
let urlToRedirectTo = '/dashboard';
const firstEnabledApp = config.core.enabledApps[0];
let urlToRedirectTo = `/${firstEnabledApp}`;
if (urlSearchParams.has('state')) {
const state = this.parseState(urlSearchParams.get('state')!);

View file

@ -152,7 +152,7 @@ export const currencyMap = new Map<SupportedCurrencySymbol, SupportedCurrency>([
export type PartialDeep<T> = (T extends (infer U)[] ? PartialDeep<U>[] : { [P in keyof T]?: PartialDeep<T[P]> }) | T;
export type OptionalApp = 'news' | 'notes' | 'photos' | 'expenses' | 'contacts' | 'calendar';
export type OptionalApp = 'dashboard' | 'files' | 'news' | 'notes' | 'photos' | 'expenses' | 'contacts' | 'calendar';
export interface Config {
auth: {
@ -188,7 +188,7 @@ export interface Config {
allowDirectoryDownloads: boolean;
};
core: {
/** dashboard and files cannot be disabled */
/** The apps to show, in order of appearance in the header. The first app will be the default one shown after logging in. At least one is required. */
enabledApps: OptionalApp[];
};
visuals: {

View file

@ -3,6 +3,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { CalendarEvent, CalendarEventModel, CalendarModel } from '/lib/models/calendar.ts';
import { generateVCalendar, getDateRangeForCalendarView } from '/lib/utils/calendar.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -28,6 +29,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -3,6 +3,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { Calendar, CalendarModel } from '/lib/models/calendar.ts';
import { CALENDAR_COLOR_OPTIONS, getColorAsHex } from '/lib/utils/calendar.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (requestBody.name) {

View file

@ -3,6 +3,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { CalendarEvent, CalendarEventModel, CalendarModel } from '/lib/models/calendar.ts';
import { getDateRangeForCalendarView } from '/lib/utils/calendar.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -25,6 +26,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { Calendar, CalendarModel } from '/lib/models/calendar.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -20,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (requestBody.calendarId) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { CalendarEvent, CalendarEventModel } from '/lib/models/calendar.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -20,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.calendarIds) {

View file

@ -4,6 +4,7 @@ import { FreshContextState } from '/lib/types.ts';
import { CalendarEvent, CalendarEventModel, CalendarModel } from '/lib/models/calendar.ts';
import { concurrentPromises } from '/lib/utils/misc.ts';
import { getDateRangeForCalendarView, getIdFromVEvent, splitTextIntoVEvents } from '/lib/utils/calendar.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -26,6 +27,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { CalendarEvent, CalendarEventModel } from '/lib/models/calendar.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -4,6 +4,7 @@ import { FreshContextState } from '/lib/types.ts';
import { Calendar, CalendarModel } from '/lib/models/calendar.ts';
import { UserModel } from '/lib/models/user.ts';
import { CALENDAR_COLOR_OPTIONS, getColorAsHex } from '/lib/utils/calendar.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -25,6 +26,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (requestBody.id) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { AddressBook, ContactModel } from '/lib/models/contacts.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -20,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.name) {

View file

@ -3,6 +3,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { Contact, ContactModel } from '/lib/models/contacts.ts';
import { generateVCard } from '/lib/utils/contacts.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -23,6 +24,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.firstName || !requestBody.addressBookId) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { AddressBook, ContactModel } from '/lib/models/contacts.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -20,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.addressBookId) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { Contact, ContactModel } from '/lib/models/contacts.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.contactId || !requestBody.addressBookId) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { AddressBook, ContactModel } from '/lib/models/contacts.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -18,6 +19,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
return new Response('Forbidden', { status: 403 });
}
const addressBooks = await ContactModel.listAddressBooks(context.state.user.id);
const responseBody: ResponseBody = { success: true, addressBooks };

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { Contact, ContactModel } from '/lib/models/contacts.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -20,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.addressBookId) {

View file

@ -4,6 +4,7 @@ import { FreshContextState } from '/lib/types.ts';
import { Contact, ContactModel } from '/lib/models/contacts.ts';
import { concurrentPromises } from '/lib/utils/misc.ts';
import { getIdFromVCard, splitTextIntoVCards } from '/lib/utils/contacts.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -23,6 +24,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.vCards || !requestBody.addressBookId) {

View file

@ -1,6 +1,7 @@
import { Handlers } from 'fresh/server.ts';
import { DashboardLink, FreshContextState } from '/lib/types.ts';
import { AppConfig } from '/lib/config.ts';
import { DashboardModel } from '/lib/models/dashboard.ts';
interface Data {}
@ -19,6 +20,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('dashboard'))) {
return new Response('Forbidden', { status: 403 });
}
const userDashboard = await DashboardModel.getByUserId(context.state.user.id);
if (!userDashboard) {

View file

@ -1,6 +1,7 @@
import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { AppConfig } from '/lib/config.ts';
import { DashboardModel } from '/lib/models/dashboard.ts';
interface Data {}
@ -19,6 +20,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('dashboard'))) {
return new Response('Forbidden', { status: 403 });
}
const userDashboard = await DashboardModel.getByUserId(context.state.user.id);
if (!userDashboard) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Budget, FreshContextState } from '/lib/types.ts';
import { BudgetModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -23,6 +24,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
import { BudgetModel, ExpenseModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -26,6 +27,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { ExpenseModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -20,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.name || requestBody.name.length < 2) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Budget, FreshContextState } from '/lib/types.ts';
import { BudgetModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
import { BudgetModel, ExpenseModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -22,6 +23,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
import { BudgetModel, ExpenseModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const newExpenses = await ExpenseModel.getAllForExport(context.state.user.id);
const newBudgets = await BudgetModel.getAllForExport(context.state.user.id);

View file

@ -3,6 +3,7 @@ import { Handlers } from 'fresh/server.ts';
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
import { concurrentPromises } from '/lib/utils/misc.ts';
import { BudgetModel, deleteAllBudgetsAndExpenses, ExpenseModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -25,6 +26,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Budget, FreshContextState } from '/lib/types.ts';
import { BudgetModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -24,6 +25,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Budget, Expense, FreshContextState } from '/lib/types.ts';
import { BudgetModel, ExpenseModel } from '/lib/models/expenses.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -27,6 +28,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Directory, FreshContextState } from '/lib/types.ts';
import { DirectoryModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -27,6 +27,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Forbidden', { status: 403 });
}
const isPublicFileSharingAllowed = await AppConfig.isPublicFileSharingAllowed();
if (!isPublicFileSharingAllowed) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Directory, FreshContextState } from '/lib/types.ts';
import { DirectoryModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -29,6 +29,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Forbidden', { status: 403 });
}
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
import { FileModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -20,6 +20,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Directory downloads are not enabled', { status: 403 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const searchParams = new URL(request.url).searchParams;
const parentPath = searchParams.get('parentPath') || '/';
const name = searchParams.get('name');

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Directory, FreshContextState } from '/lib/types.ts';
import { DirectoryModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -27,6 +27,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Forbidden', { status: 403 });
}
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.fileShareId) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
import { FileModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -20,6 +21,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Directory, FreshContextState } from '/lib/types.ts';
import { DirectoryModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -22,6 +23,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
import { FileModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -22,6 +23,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Directory, FreshContextState } from '/lib/types.ts';
import { DirectoryModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -22,6 +23,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
import { FileModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -22,6 +23,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
import { searchFilesAndDirectories } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.searchTerm?.trim()) {

View file

@ -32,6 +32,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Forbidden', { status: 403 });
}
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
import { DirectoryModel, FileModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -17,6 +18,13 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().formData();
const pathInView = requestBody.get('path_in_view') as string;

View file

@ -3,6 +3,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState, NewsFeed } from '/lib/types.ts';
import { FeedModel } from '/lib/models/news.ts';
import { fetchNewArticles } from '/crons/news.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('news'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (requestBody.feedUrl) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState, NewsFeed } from '/lib/types.ts';
import { FeedModel } from '/lib/models/news.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -20,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('news'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (requestBody.feedId) {

View file

@ -4,6 +4,7 @@ import { FreshContextState, NewsFeed } from '/lib/types.ts';
import { concurrentPromises } from '/lib/utils/misc.ts';
import { FeedModel } from '/lib/models/news.ts';
import { fetchNewArticles } from '/crons/news.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -22,6 +23,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('news'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (requestBody.feedUrls) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { ArticleModel } from '/lib/models/news.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -19,6 +20,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('news'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (requestBody.articleId) {

View file

@ -3,6 +3,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState, NewsFeedArticle } from '/lib/types.ts';
import { ArticleModel, FeedModel } from '/lib/models/news.ts';
import { fetchNewArticles } from '/crons/news.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -19,6 +20,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('news'))) {
return new Response('Forbidden', { status: 403 });
}
const newsFeeds = await FeedModel.list(context.state.user.id);
if (!newsFeeds.length) {

View file

@ -2,6 +2,7 @@ import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { FileModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -21,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Unauthorized', { status: 401 });
}
if (!(await AppConfig.isAppEnabled('notes'))) {
return new Response('Forbidden', { status: 403 });
}
const requestBody = await request.clone().json() as RequestBody;
if (

View file

@ -29,6 +29,10 @@ export const handler: Handlers<Data, FreshContextState> = {
throw new Error('CalDAV server is not enabled');
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
throw new Error('Calendar app is not enabled');
}
const userId = context.state.user.id;
const timezoneId = context.state.user.extra.timezone?.id || 'UTC';
const timezoneUtcOffset = context.state.user.extra.timezone?.utcOffset || 0;

View file

@ -28,6 +28,10 @@ export const handler: Handlers<Data, FreshContextState> = {
throw new Error('CalDAV server is not enabled');
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
throw new Error('Calendar app is not enabled');
}
let { calendarEventId } = context.params;
const searchParams = new URL(request.url).searchParams;
@ -63,6 +67,10 @@ export const handler: Handlers<Data, FreshContextState> = {
throw new Error('CalDAV server is not enabled');
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
throw new Error('Calendar app is not enabled');
}
const { calendarEventId } = context.params;
const searchParams = new URL(request.url).searchParams;

View file

@ -21,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
throw new Error('CalDAV server is not enabled');
}
if (!(await AppConfig.isAppEnabled('calendar'))) {
throw new Error('Calendar app is not enabled');
}
const userCalendars = await CalendarModel.list(context.state.user.id);
return await context.render({ userCalendars });

View file

@ -28,6 +28,10 @@ export const handler: Handlers<Data, FreshContextState> = {
throw new Error('CardDAV server is not enabled');
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
throw new Error('Contacts app is not enabled');
}
const userId = context.state.user.id;
const searchParams = new URL(request.url).searchParams;

View file

@ -6,6 +6,7 @@ import { Contact, ContactModel } from '/lib/models/contacts.ts';
import { getFormDataField } from '/lib/form-utils.tsx';
import ViewContact, { formFields } from '/islands/contacts/ViewContact.tsx';
import { updateVCard } from '/lib/utils/contacts.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {
contact: Contact;
@ -21,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
throw new Error('Contacts app is not enabled');
}
const { contactId } = context.params;
const searchParams = new URL(request.url).searchParams;
@ -43,6 +48,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
}
if (!(await AppConfig.isAppEnabled('contacts'))) {
throw new Error('Contacts app is not enabled');
}
const { contactId } = context.params;
const searchParams = new URL(request.url).searchParams;

View file

@ -2,6 +2,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
import { Dashboard, FreshContextState } from '/lib/types.ts';
import { DashboardModel } from '/lib/models/dashboard.ts';
import { AppConfig } from '/lib/config.ts';
import Notes from '/islands/dashboard/Notes.tsx';
import Links from '/islands/dashboard/Links.tsx';
@ -15,6 +16,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
}
if (!(await AppConfig.isAppEnabled('dashboard'))) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/` } });
}
let userDashboard = await DashboardModel.getByUserId(context.state.user.id);
if (!userDashboard) {

View file

@ -27,6 +27,13 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
});
}
if (
!(await AppConfig.isAppEnabled('files')) && !(await AppConfig.isAppEnabled('photos')) &&
!(await AppConfig.isAppEnabled('notes'))
) {
return new Response('Forbidden', { status: 403 });
}
let { filePath } = context.params;
if (!filePath) {

View file

@ -19,7 +19,7 @@ export const handler: Handlers<Data, FreshContextState> = {
}
if (!(await AppConfig.isAppEnabled('expenses'))) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/files` } });
return new Response('Redirect', { status: 303, headers: { 'Location': `/` } });
}
const searchParams = new URL(request.url).searchParams;

View file

@ -35,6 +35,10 @@ export const handler: Handlers<Data, FreshContextState> = {
const baseUrl = (await AppConfig.getConfig()).auth.baseUrl;
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Not Found', { status: 404 });
}
const fileShare = await FileShareModel.getById(fileShareId);
if (!fileShare) {

View file

@ -21,6 +21,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Not Found', { status: 404 });
}
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Not Found', { status: 404 });
}
const fileShare = await FileShareModel.getById(fileShareId);
if (!fileShare) {

View file

@ -29,6 +29,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Not Found', { status: 404 });
}
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Not Found', { status: 404 });
}
const fileShare = await FileShareModel.getById(fileShareId);
if (!fileShare) {
@ -54,6 +58,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Not Found', { status: 404 });
}
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Not Found', { status: 404 });
}
const fileShare = await FileShareModel.getById(fileShareId);
if (!fileShare) {

View file

@ -22,6 +22,10 @@ export const handler: Handlers<Data, FreshContextState> = {
const baseUrl = (await AppConfig.getConfig()).auth.baseUrl;
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/` } });
}
const searchParams = new URL(request.url).searchParams;
let currentPath = searchParams.get('path') || '/';

View file

@ -1,6 +1,7 @@
import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { AppConfig } from '/lib/config.ts';
import { FileModel } from '/lib/models/files.ts';
interface Data {}
@ -17,6 +18,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Not Found', { status: 404 });
}
if (!(await AppConfig.isAppEnabled('files'))) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/` } });
}
const searchParams = new URL(request.url).searchParams;
let currentPath = searchParams.get('path') || '/';

View file

@ -1,13 +1,18 @@
import { Handlers } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
export const handler: Handlers<Data, FreshContextState> = {
GET(request, context) {
async GET(request, context) {
const config = await AppConfig.getConfig();
if (context.state.user) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/dashboard` } });
const firstEnabledApp = config.core.enabledApps[0];
return new Response('Redirect', { status: 303, headers: { 'Location': `/${firstEnabledApp}` } });
}
return new Response('Redirect', { status: 303, headers: { 'Location': '/login' } });

View file

@ -16,7 +16,7 @@ export const handler: Handlers<Data, FreshContextState> = {
}
if (!(await AppConfig.isAppEnabled('news'))) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/dashboard` } });
return new Response('Redirect', { status: 303, headers: { 'Location': `/` } });
}
const userArticles = await ArticleModel.listUnread(context.state.user.id);

View file

@ -2,6 +2,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
import { FreshContextState, NewsFeed } from '/lib/types.ts';
import { FeedModel } from '/lib/models/news.ts';
import { AppConfig } from '/lib/config.ts';
import Feeds from '/islands/news/Feeds.tsx';
interface Data {
@ -14,6 +15,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
}
if (!(await AppConfig.isAppEnabled('news'))) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/` } });
}
const userFeeds = await FeedModel.list(context.state.user.id);
return await context.render({ userFeeds });

View file

@ -3,6 +3,7 @@ import { Handlers, PageProps } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { FileModel } from '/lib/models/files.ts';
import Note from '/islands/notes/Note.tsx';
import { AppConfig } from '/lib/config.ts';
interface Data {
fileName: string;
@ -22,6 +23,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Not Found', { status: 404 });
}
if (!(await AppConfig.isAppEnabled('notes'))) {
throw new Error('Notes app is not enabled');
}
const searchParams = new URL(request.url).searchParams;
let currentPath = searchParams.get('path') || '/';

View file

@ -3,6 +3,7 @@ import sharp from 'sharp';
import { FreshContextState } from '/lib/types.ts';
import { FileModel } from '/lib/models/files.ts';
import { AppConfig } from '/lib/config.ts';
interface Data {}
@ -23,6 +24,10 @@ export const handler: Handlers<Data, FreshContextState> = {
return new Response('Not Found', { status: 404 });
}
if (!(await AppConfig.isAppEnabled('photos'))) {
throw new Error('Photos app is not enabled');
}
const searchParams = new URL(request.url).searchParams;
let currentPath = searchParams.get('path') || '/';