Compare commits

...

11 commits
v4.0.0 ... main

Author SHA1 Message Date
Bruno Bernardino
6f871b72ad
Return 404 on WebDAV MOVE request if source doesn't exist
Some checks failed
Build Docker Image / build-and-push (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Run Tests / test (push) Has been cancelled
Fixes #155
2026-03-03 15:40:47 +00:00
Bruno Bernardino
f50423028b
Implement max file upload config options and simplify request method handling
Some checks are pending
Build Docker Image / build-and-push (push) Waiting to run
Deploy / deploy (push) Waiting to run
Run Tests / test (push) Waiting to run
Fixes #154
Related to #155
2026-03-02 19:52:00 +00:00
Bruno Bernardino
69a916d709
Fix typo, update default version
Some checks failed
Build Docker Image / build-and-push (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Run Tests / test (push) Has been cancelled
2026-02-26 13:35:33 +00:00
Piotr Łoboda
0d20b4a337
Enable SSO signups (#152)
* feat: enableSingleSignOnSignUp support

* chore: simplify return statement

* chore: linted

* feat: verbose error on not enabled SSO sign up

* chore: update config key to match others

* chore: add key to config sample

* chore: apply suggestions from code review

Co-authored-by: BrunoBernardino <me@brunobernardino.com>

---------

Co-authored-by: BrunoBernardino <me@brunobernardino.com>
2026-02-26 13:31:19 +00:00
Bruno Bernardino
917649b97a
Fix ring opacity in dropdowns
Some checks are pending
Build Docker Image / build-and-push (push) Waiting to run
Deploy / deploy (push) Waiting to run
Run Tests / test (push) Waiting to run
2026-02-25 15:14:18 +00:00
Bruno Bernardino
5d394045dd
Merge pull request #150 from loboda4450/main
feat: update README.md to include myself with helm chart
2026-02-25 14:35:29 +00:00
Piotr Łoboda
264d4da6e5 feat: update README.md to include myself 2026-02-25 01:01:14 +01:00
Bruno Bernardino
ec334fb8fa
Update managed cloud buy URL
Some checks are pending
Build Docker Image / build-and-push (push) Waiting to run
Deploy / deploy (push) Waiting to run
Run Tests / test (push) Waiting to run
2026-02-24 14:45:31 +00:00
Bruno Bernardino
bd141bf9dc
Update default docker image to v4.0.2
Some checks are pending
Build Docker Image / build-and-push (push) Waiting to run
Deploy / deploy (push) Waiting to run
Run Tests / test (push) Waiting to run
2026-02-23 17:32:41 +00:00
不做了睡大觉
1aca444b22
fix: properly strip HTML tags and resolve entities in feed article summaries (#149)
* fix: properly strip HTML tags and resolve entities in feed article summaries

Fixes #146

The parseTextFromHtml function was using document.textContent directly on
the parsed HTML document, which could leave raw HTML tags and unresolved
entities in feed article summaries.

Changes:
- Extract text from body element to avoid document wrapper artifacts
- Collapse multiple whitespace/newlines into single spaces for cleaner output
- Add early return for empty/whitespace-only input
- Use optional chaining for safer null handling

* fix: preserve single line breaks, only collapse 2+ consecutive whitespace

Address review feedback: the previous \s+ regex was too aggressive and
broke text-only summaries with legitimate line breaks.

Now:
- Collapse runs of 2+ non-newline whitespace into a single space
- Collapse 3+ consecutive newlines into double newline (paragraph break)
- Single line breaks are preserved

---------

Co-authored-by: User <user@example.com>
2026-02-23 17:29:09 +00:00
Bruno Bernardino
6d5ee7b53c
Fix logout page typo
Some checks failed
Build Docker Image / build-and-push (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Run Tests / test (push) Has been cancelled
Fixes #148
2026-02-21 07:18:04 +00:00
34 changed files with 96 additions and 187 deletions

View file

@ -10,7 +10,7 @@ If you're looking for the mobile app, it's at [`bewcloud-mobile`](https://github
## Self-host it! ## Self-host it!
[![Buy managed cloud (1 year)](https://img.shields.io/badge/Buy%20managed%20cloud%20(1%20year)-51a4fb?style=for-the-badge)](https://payment-links.mollie.com/payment/zeLCmenLMLQwTcvqPJMyb) [![Buy managed cloud (1 year)](https://img.shields.io/badge/Buy%20managed%20cloud%20(1%20year)-51a4fb?style=for-the-badge)](https://payment-links.mollie.com/payment/AiEHa5EpD6wkZN5r6Vs3c)
Or, to run on your own machine, start with these commands: Or, to run on your own machine, start with these commands:
@ -111,3 +111,6 @@ These are not officially endorsed, but are alternative ways of running bewCloud.
- [`bewcloud-nixos`](https://codeberg.org/ntninja/bewcloud-nixos/) by [@ntninja](https://codeberg.org/ntninja/) exposes bewCloud as an easy-to-use NixOS integration as an alternative to using Docker or running the app locally. - [`bewcloud-nixos`](https://codeberg.org/ntninja/bewcloud-nixos/) by [@ntninja](https://codeberg.org/ntninja/) exposes bewCloud as an easy-to-use NixOS integration as an alternative to using Docker or running the app locally.
- For installation, please see the [README](https://codeberg.org/ntninja/bewcloud-nixos/src/branch/main/README.md). - For installation, please see the [README](https://codeberg.org/ntninja/bewcloud-nixos/src/branch/main/README.md).
- [`bewcloud-helm`](https://github.com/loboda4450/charts/tree/main/charts/bewcloud) by [@loboda4450](https://github.com/loboda4450/) exposes bewCloud as a Helm chart.
- For installation, please see the [README](https://github.com/loboda4450/charts/blob/main/charts/bewcloud/README.md)
- Supports automatic migrations, Radicale installation (as a dependency chart) and streamlined, fully yaml-based configuration.

View file

@ -11,6 +11,7 @@ const config: PartialDeep<Config> = {
// allowedCookieDomains: ['example.com', 'example.net'], // Can be set to allow more than the baseUrl's domain for session cookies // allowedCookieDomains: ['example.com', 'example.net'], // Can be set to allow more than the baseUrl's domain for session cookies
// skipCookieDomainSecurity: true, // If true, the cookie domain will not be strictly set and checked against. This skipping slightly reduces security, but is usually necessary for reverse proxies like Cloudflare Tunnel // skipCookieDomainSecurity: true, // If true, the cookie domain will not be strictly set and checked against. This skipping slightly reduces security, but is usually necessary for reverse proxies like Cloudflare Tunnel
// enableSingleSignOn: false, // If true, single sign-on will be enabled // enableSingleSignOn: false, // If true, single sign-on will be enabled
// allowSignupsViaSingleSignOn: false, // If true, signups via single sign-on will be allowed, overriding allowSignups
// singleSignOnUrl: '', // The Discovery URL (AKA Issuer) of the identity/single sign-on provider // singleSignOnUrl: '', // The Discovery URL (AKA Issuer) of the identity/single sign-on provider
// singleSignOnEmailAttribute: 'email', // The attribute to prefer as email of the identity/single sign-on provider // singleSignOnEmailAttribute: 'email', // The attribute to prefer as email of the identity/single sign-on provider
// singleSignOnScopes: ['openid', 'email'], // The scopes to request from the identity/single sign-on provider // singleSignOnScopes: ['openid', 'email'], // The scopes to request from the identity/single sign-on provider
@ -19,9 +20,11 @@ const config: PartialDeep<Config> = {
// rootPath: 'data-files', // rootPath: 'data-files',
// allowPublicSharing: false, // If true, public file sharing will be allowed (still requires a user to enable sharing for a given file or directory) // allowPublicSharing: false, // If true, public file sharing will be allowed (still requires a user to enable sharing for a given file or directory)
// allowDirectoryDownloads: false, // If true, directories can be downloaded as zip files // allowDirectoryDownloads: false, // If true, directories can be downloaded as zip files
// maxUploadSizeInMegabytes: 100, // The maximum upload size in megabytes. Overrides the core.maxRequestSizeInMegabytes setting on /dav and /api/files/upload endpoints.
// }, // },
// core: { // core: {
// 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. // 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.
// maxRequestSizeInMegabytes: 12, // The maximum request size in megabytes.
// }, // },
// visuals: { // visuals: {
// title: 'My own cloud', // title: 'My own cloud',

View file

@ -448,7 +448,7 @@ export default function MainCalendar(
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!isViewOptionsDropdownOpen.value ? 'hidden' : '' !isViewOptionsDropdownOpen.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'
@ -509,7 +509,7 @@ export default function MainCalendar(
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!isImportExportOptionsDropdownOpen.value ? 'hidden' : '' !isImportExportOptionsDropdownOpen.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'

View file

@ -111,7 +111,7 @@ export default function SearchEvents({ calendars, onClickOpenEvent }: SearchEven
? ( ? (
<section class='relative inline-block text-left ml-2 text-xs'> <section class='relative inline-block text-left ml-2 text-xs'>
<section <section
class={`absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none`} class={`absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none`}
role='menu' role='menu'
aria-orientation='vertical' aria-orientation='vertical'
aria-labelledby='view-button' aria-labelledby='view-button'

View file

@ -359,7 +359,7 @@ export default function Contacts(
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right divide-y divide-slate-600 rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right divide-y divide-slate-600 rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!isAddressBooksDropdownOpen.value ? 'hidden' : '' !isAddressBooksDropdownOpen.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'
@ -424,7 +424,7 @@ export default function Contacts(
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!isOptionsDropdownOpen.value ? 'hidden' : '' !isOptionsDropdownOpen.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'

View file

@ -727,7 +727,7 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!areNewOptionsOption.value ? 'hidden' : '' !areNewOptionsOption.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'

View file

@ -751,7 +751,7 @@ export default function MainFiles(
</div> </div>
<div <div
class={`absolute left-0 z-10 mt-2 w-44 origin-top-left rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute left-0 z-10 mt-2 w-44 origin-top-left rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!areBulkOptionsOpen.value ? 'hidden' : '' !areBulkOptionsOpen.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'
@ -802,7 +802,7 @@ export default function MainFiles(
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!areNewOptionsOpen.value ? 'hidden' : '' !areNewOptionsOpen.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'

View file

@ -111,7 +111,7 @@ export default function SearchFiles() {
? ( ? (
<section class='relative inline-block text-left ml-2 text-sm'> <section class='relative inline-block text-left ml-2 text-sm'>
<section <section
class={`absolute right-0 z-10 mt-2 w-80 origin-top-right rounded-md bg-slate-600 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none overflow-y-scroll max-h-[80%] min-h-56`} class={`absolute right-0 z-10 mt-2 w-80 origin-top-right rounded-md bg-slate-600 shadow-lg ring-1 ring-black/15 focus:outline-none overflow-y-scroll max-h-[80%] min-h-56`}
role='menu' role='menu'
aria-orientation='vertical' aria-orientation='vertical'
aria-labelledby='view-button' aria-labelledby='view-button'

View file

@ -181,7 +181,7 @@ export default function Articles({ initialArticles }: ArticlesProps) {
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!isFilterDropdownOpen.value ? 'hidden' : '' !isFilterDropdownOpen.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'

View file

@ -254,7 +254,7 @@ export default function Feeds({ initialFeeds }: FeedsProps) {
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!isOptionsDropdownOpen.value ? 'hidden' : '' !isOptionsDropdownOpen.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'

View file

@ -250,7 +250,7 @@ export default function MainNotes({ initialDirectories, initialFiles, initialPat
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!areNewOptionsOption.value ? 'hidden' : '' !areNewOptionsOption.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'

View file

@ -166,7 +166,7 @@ export default function MainPhotos({ initialDirectories, initialFiles, initialPa
</div> </div>
<div <div
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${ class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${
!areNewOptionsOption.value ? 'hidden' : '' !areNewOptionsOption.value ? 'hidden' : ''
}`} }`}
role='menu' role='menu'

View file

@ -1,6 +1,6 @@
services: services:
website: website:
image: ghcr.io/bewcloud/bewcloud:v4.0.0 image: ghcr.io/bewcloud/bewcloud:v4.1.2
# NOTE: uncomment below (and comment above) only if you pulled the repo and want to build the image locally # NOTE: uncomment below (and comment above) only if you pulled the repo and want to build the image locally
# build: # build:
# context: . # context: .

View file

@ -16,6 +16,7 @@ export class AppConfig {
allowedCookieDomains: [], allowedCookieDomains: [],
skipCookieDomainSecurity: false, skipCookieDomainSecurity: false,
enableSingleSignOn: false, enableSingleSignOn: false,
allowSignupsViaSingleSignOn: false,
singleSignOnUrl: '', singleSignOnUrl: '',
singleSignOnEmailAttribute: 'email', singleSignOnEmailAttribute: 'email',
singleSignOnScopes: ['openid', 'email'], singleSignOnScopes: ['openid', 'email'],
@ -24,9 +25,11 @@ export class AppConfig {
rootPath: 'data-files', rootPath: 'data-files',
allowPublicSharing: false, allowPublicSharing: false,
allowDirectoryDownloads: false, allowDirectoryDownloads: false,
maxUploadSizeInMegabytes: 100,
}, },
core: { core: {
enabledApps: ['dashboard', 'files', 'news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'], enabledApps: ['dashboard', 'files', 'news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'],
maxRequestSizeInMegabytes: 12,
}, },
visuals: { visuals: {
title: '', title: '',
@ -117,11 +120,12 @@ export class AppConfig {
return this.config; return this.config;
} }
static async isSignupAllowed(): Promise<boolean> { static async isSignupAllowed({ viaSingleSignOn = false }: { viaSingleSignOn?: boolean } = {}): Promise<boolean> {
await this.loadConfig(); await this.loadConfig();
const areSignupsAllowed = this.config.auth.allowSignups; const areSignupsAllowed = viaSingleSignOn && !this.config.auth.allowSignups
? this.config.auth.allowSignupsViaSingleSignOn
: this.config.auth.allowSignups;
const areThereAdmins = await UserModel.isThereAnAdmin(); const areThereAdmins = await UserModel.isThereAnAdmin();
if (areSignupsAllowed || !areThereAdmins) { if (areSignupsAllowed || !areThereAdmins) {

View file

@ -222,13 +222,20 @@ export async function getUrlInfo(url: string): Promise<{ title: string; htmlBody
} }
export async function parseTextFromHtml(html: string): Promise<string> { export async function parseTextFromHtml(html: string): Promise<string> {
let text = ''; if (!html || !html.trim()) {
return '';
}
await initParser(); await initParser();
const document = new DOMParser().parseFromString(html, 'text/html'); const document = new DOMParser().parseFromString(html, 'text/html');
text = document!.textContent; // Extract text from body to avoid any artifacts from the document wrapper
const text = (document?.querySelector('body')?.textContent || document?.textContent || '')
// Collapse runs of 2+ whitespace/newline characters, preserving single line breaks
.replace(/[^\S\n]{2,}/g, ' ')
.replace(/\n{3,}/g, '\n\n')
.trim();
return text; return text;
} }

View file

@ -169,7 +169,7 @@ export class OidcModel {
throw new Error(`Missing user/${emailAttribute}`); throw new Error(`Missing user/${emailAttribute}`);
} }
const isSignupAllowed = await AppConfig.isSignupAllowed(); const isSignupAllowed = await AppConfig.isSignupAllowed({ viaSingleSignOn: true });
const isThereAnAdmin = await UserModel.isThereAnAdmin(); const isThereAnAdmin = await UserModel.isThereAnAdmin();
// Confirm the user exists (or signup if allowed) // Confirm the user exists (or signup if allowed)
@ -181,6 +181,10 @@ export class OidcModel {
} }
if (!user) { if (!user) {
if (!config.auth.allowSignupsViaSingleSignOn) {
throw new Error('Sign up via SSO is not allowed!');
}
throw new Error('There was a problem signing up or logging in!'); throw new Error('There was a problem signing up or logging in!');
} }

View file

@ -17,15 +17,7 @@ export interface Page {
patch?: RequestHandler; patch?: RequestHandler;
delete?: RequestHandler; delete?: RequestHandler;
options?: RequestHandler; options?: RequestHandler;
copy?: RequestHandler; catchAll?: RequestHandler;
move?: RequestHandler;
mkcol?: RequestHandler;
mkcalendar?: RequestHandler;
lock?: RequestHandler;
unlock?: RequestHandler;
propfind?: RequestHandler;
proppatch?: RequestHandler;
report?: RequestHandler;
} }
type AccessMode = 'public' | 'user'; type AccessMode = 'public' | 'user';
@ -61,15 +53,7 @@ export default function page(
patch, patch,
delete: deleteAction, delete: deleteAction,
options, options,
copy, catchAll,
move,
mkcol,
mkcalendar,
lock,
unlock,
propfind,
proppatch,
report,
accessMode, accessMode,
}: Params, }: Params,
): Page { ): Page {
@ -80,14 +64,6 @@ export default function page(
patch: patch ? permissioned(patch, accessMode) : undefined, patch: patch ? permissioned(patch, accessMode) : undefined,
delete: deleteAction ? permissioned(deleteAction, accessMode) : undefined, delete: deleteAction ? permissioned(deleteAction, accessMode) : undefined,
options: options ? permissioned(options, accessMode) : undefined, options: options ? permissioned(options, accessMode) : undefined,
copy: copy ? permissioned(copy, accessMode) : undefined, catchAll: catchAll ? permissioned(catchAll, accessMode) : undefined,
move: move ? permissioned(move, accessMode) : undefined,
mkcol: mkcol ? permissioned(mkcol, accessMode) : undefined,
mkcalendar: mkcalendar ? permissioned(mkcalendar, accessMode) : undefined,
lock: lock ? permissioned(lock, accessMode) : undefined,
unlock: unlock ? permissioned(unlock, accessMode) : undefined,
propfind: propfind ? permissioned(propfind, accessMode) : undefined,
proppatch: proppatch ? permissioned(proppatch, accessMode) : undefined,
report: report ? permissioned(report, accessMode) : undefined,
}; };
} }

View file

@ -159,6 +159,8 @@ export interface Config {
skipCookieDomainSecurity: boolean; skipCookieDomainSecurity: boolean;
/** If true, single sign-on will be enabled */ /** If true, single sign-on will be enabled */
enableSingleSignOn: boolean; enableSingleSignOn: boolean;
/** If true, signups via single sign-on will be allowed, overriding allowSignups */
allowSignupsViaSingleSignOn: boolean;
/** The Discovery URL (AKA Issuer) of the identity/single sign-on provider */ /** The Discovery URL (AKA Issuer) of the identity/single sign-on provider */
singleSignOnUrl: string; singleSignOnUrl: string;
/** The attribute to prefer as email of the identity/single sign-on provider */ /** The attribute to prefer as email of the identity/single sign-on provider */
@ -173,10 +175,14 @@ export interface Config {
allowPublicSharing: boolean; allowPublicSharing: boolean;
/** If true, directories can be downloaded as zip files */ /** If true, directories can be downloaded as zip files */
allowDirectoryDownloads: boolean; allowDirectoryDownloads: boolean;
/** The maximum upload size in megabytes. Overrides the core.maxRequestSizeInMegabytes setting on /dav and /api/files/upload endpoints. */
maxUploadSizeInMegabytes: number;
}; };
core: { core: {
/** 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. */ /** 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[]; enabledApps: OptionalApp[];
/** The maximum request size in megabytes. */
maxRequestSizeInMegabytes: number;
}; };
visuals: { visuals: {
/** An override title of the application. Empty shows the default title. */ /** An override title of the application. Empty shows the default title. */

13
main.ts
View file

@ -1,9 +1,13 @@
import routes, { Route } from './routes.ts'; import routes, { Route } from './routes.ts';
import { startCrons } from './crons/index.ts'; import { startCrons } from './crons/index.ts';
import { Page } from './lib/page.ts'; import { Page } from './lib/page.ts';
import { AppConfig } from './lib/config.ts';
const MAX_REQUEST_SIZE_IN_MEGABYTES = 12; const config = await AppConfig.getConfig();
const MAX_REQUEST_SIZE_IN_MEGABYTES = config.core.maxRequestSizeInMegabytes;
const MAX_UPLOAD_SIZE_IN_MEGABYTES = config.files.maxUploadSizeInMegabytes;
const MAX_REQUEST_SIZE_IN_BYTES = MAX_REQUEST_SIZE_IN_MEGABYTES * 1024 * 1024; const MAX_REQUEST_SIZE_IN_BYTES = MAX_REQUEST_SIZE_IN_MEGABYTES * 1024 * 1024;
const MAX_UPLOAD_SIZE_IN_BYTES = MAX_UPLOAD_SIZE_IN_MEGABYTES * 1024 * 1024;
function applyCorsHeadersToResponse(origin: string, response: Response) { function applyCorsHeadersToResponse(origin: string, response: Response) {
const headers = response.headers; const headers = response.headers;
@ -53,12 +57,15 @@ function handleLogging(request: Request, response: Response) {
async function handler(request: Request) { async function handler(request: Request) {
const contentLength = request.headers.get('content-length'); const contentLength = request.headers.get('content-length');
const path = new URL(request.url).pathname;
if (contentLength && parseInt(contentLength, 10) > MAX_REQUEST_SIZE_IN_BYTES) { const isUploadRequest = path.startsWith('/api/files/upload') || path.startsWith('/dav');
const maxSizeInBytes = isUploadRequest ? MAX_UPLOAD_SIZE_IN_BYTES : MAX_REQUEST_SIZE_IN_BYTES;
if (contentLength && parseInt(contentLength, 10) > maxSizeInBytes) {
return new Response('Payload too large', { status: 413 }); return new Response('Payload too large', { status: 413 });
} }
const path = new URL(request.url).pathname;
const origin = request.headers.get('Origin') || '*'; const origin = request.headers.get('Origin') || '*';
// CORS headers for non-DAV routes // CORS headers for non-DAV routes

View file

@ -75,14 +75,6 @@ export default page({
put: get, put: get,
delete: get, delete: get,
options: get, options: get,
copy: get, catchAll: get,
move: get,
mkcol: get,
mkcalendar: get,
lock: get,
unlock: get,
propfind: get,
proppatch: get,
report: get,
accessMode: 'public', accessMode: 'public',
}); });

View file

@ -75,13 +75,6 @@ export default page({
put: get, put: get,
delete: get, delete: get,
options: get, options: get,
copy: get, catchAll: get,
move: get,
mkcol: get,
lock: get,
unlock: get,
propfind: get,
proppatch: get,
report: get,
accessMode: 'public', accessMode: 'public',
}); });

View file

@ -142,6 +142,8 @@ async function handler({ request, user, match }: RequestHandlerParams): Promise<
console.error(error); console.error(error);
} }
} }
return new Response('Not Found', { status: 404 });
} }
if (request.method === 'MKCOL') { if (request.method === 'MKCOL') {
@ -257,12 +259,6 @@ export default page({
delete: handler, delete: handler,
put: handler, put: handler,
options: handler, options: handler,
copy: handler, catchAll: handler,
move: handler,
mkcol: handler,
lock: handler,
unlock: handler,
propfind: handler,
report: handler,
accessMode: 'public', accessMode: 'public',
}); });

View file

@ -361,7 +361,7 @@ export default function MainCalendar({
d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z", d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z",
"clip-rule": "evenodd" "clip-rule": "evenodd"
})))), h("div", { })))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!isViewOptionsDropdownOpen.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!isViewOptionsDropdownOpen.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "view-button", "aria-labelledby": "view-button",
@ -399,7 +399,7 @@ export default function MainCalendar({
d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z", d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z",
"clip-rule": "evenodd" "clip-rule": "evenodd"
})))), h("div", { })))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!isImportExportOptionsDropdownOpen.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!isImportExportOptionsDropdownOpen.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "import-export-button", "aria-labelledby": "import-export-button",

View file

@ -91,7 +91,7 @@ export default function SearchEvents({
}) : null, areResultsVisible.value ? h("section", { }) : null, areResultsVisible.value ? h("section", {
class: "relative inline-block text-left ml-2 text-xs" class: "relative inline-block text-left ml-2 text-xs"
}, h("section", { }, h("section", {
class: `absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none`, class: `absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "view-button", "aria-labelledby": "view-button",

View file

@ -278,7 +278,7 @@ export default function Contacts({
d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z", d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z",
"clip-rule": "evenodd" "clip-rule": "evenodd"
})))), h("div", { })))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right divide-y divide-slate-600 rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!isAddressBooksDropdownOpen.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right divide-y divide-slate-600 rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!isAddressBooksDropdownOpen.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "select-address-book-button", "aria-labelledby": "select-address-book-button",
@ -318,7 +318,7 @@ export default function Contacts({
d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z", d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z",
"clip-rule": "evenodd" "clip-rule": "evenodd"
})))), h("div", { })))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!isOptionsDropdownOpen.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!isOptionsDropdownOpen.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "import-export-button", "aria-labelledby": "import-export-button",

View file

@ -538,7 +538,7 @@ export default function MainExpenses({
width: 20, width: 20,
height: 20 height: 20
}))), h("div", { }))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!areNewOptionsOption.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!areNewOptionsOption.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "new-button", "aria-labelledby": "new-button",

View file

@ -543,7 +543,7 @@ export default function MainFiles({
width: 20, width: 20,
height: 20 height: 20
}))), h("div", { }))), h("div", {
class: `absolute left-0 z-10 mt-2 w-44 origin-top-left rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!areBulkOptionsOpen.value ? 'hidden' : ''}`, class: `absolute left-0 z-10 mt-2 w-44 origin-top-left rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!areBulkOptionsOpen.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "bulk-button", "aria-labelledby": "bulk-button",
@ -576,7 +576,7 @@ export default function MainFiles({
width: 20, width: 20,
height: 20 height: 20
}))), h("div", { }))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!areNewOptionsOpen.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!areNewOptionsOpen.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "new-button", "aria-labelledby": "new-button",

View file

@ -90,7 +90,7 @@ export default function SearchFiles() {
}) : null, areResultsVisible.value ? h("section", { }) : null, areResultsVisible.value ? h("section", {
class: "relative inline-block text-left ml-2 text-sm" class: "relative inline-block text-left ml-2 text-sm"
}, h("section", { }, h("section", {
class: `absolute right-0 z-10 mt-2 w-80 origin-top-right rounded-md bg-slate-600 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none overflow-y-scroll max-h-[80%] min-h-56`, class: `absolute right-0 z-10 mt-2 w-80 origin-top-right rounded-md bg-slate-600 shadow-lg ring-1 ring-black/15 focus:outline-none overflow-y-scroll max-h-[80%] min-h-56`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "view-button", "aria-labelledby": "view-button",

View file

@ -144,7 +144,7 @@ export default function Articles({
d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z", d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z",
"clip-rule": "evenodd" "clip-rule": "evenodd"
})))), h("div", { })))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!isFilterDropdownOpen.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!isFilterDropdownOpen.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "filter-button", "aria-labelledby": "filter-button",

View file

@ -192,7 +192,7 @@ export default function Feeds({
d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z", d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z",
"clip-rule": "evenodd" "clip-rule": "evenodd"
})))), h("div", { })))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!isOptionsDropdownOpen.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!isOptionsDropdownOpen.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "filter-button", "aria-labelledby": "filter-button",

View file

@ -186,7 +186,7 @@ export default function MainNotes({
width: 20, width: 20,
height: 20 height: 20
}))), h("div", { }))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!areNewOptionsOption.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!areNewOptionsOption.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "new-button", "aria-labelledby": "new-button",

View file

@ -124,7 +124,7 @@ export default function MainPhotos({
width: 20, width: 20,
height: 20 height: 20
}))), h("div", { }))), h("div", {
class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${!areNewOptionsOption.value ? 'hidden' : ''}`, class: `absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black/15 focus:outline-none ${!areNewOptionsOption.value ? 'hidden' : ''}`,
role: "menu", role: "menu",
"aria-orientation": "vertical", "aria-orientation": "vertical",
"aria-labelledby": "new-button", "aria-labelledby": "new-button",

View file

@ -1220,8 +1220,11 @@
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.ring-black { .ring-black\/15 {
--tw-ring-color: var(--color-black); --tw-ring-color: color-mix(in srgb, #000 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklab, var(--color-black) 15%, transparent);
}
} }
.ring-slate-800 { .ring-slate-800 {
--tw-ring-color: var(--color-slate-800); --tw-ring-color: var(--color-slate-800);

109
routes.ts
View file

@ -33,14 +33,7 @@ function createPageRouteHandler(id: string, pathname: string) {
patch, patch,
delete: deleteAction, delete: deleteAction,
options, options,
copy, catchAll,
move,
mkcol,
lock,
unlock,
propfind,
proppatch,
report,
} = page; } = page;
const { user, session, tokenData } = (await getDataFromRequest(request)) || {}; const { user, session, tokenData } = (await getDataFromRequest(request)) || {};
@ -148,95 +141,17 @@ function createPageRouteHandler(id: string, pathname: string) {
}); });
} }
break; break;
case 'COPY':
if (copy) {
return await copy({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
break;
case 'MOVE':
if (move) {
return await move({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
break;
case 'MKCOL':
if (mkcol) {
return await mkcol({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
break;
case 'LOCK':
if (lock) {
return await lock({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
break;
case 'UNLOCK':
if (unlock) {
return await unlock({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
break;
case 'PROPFIND':
if (propfind) {
return await propfind({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
break;
case 'PROPPATCH':
if (proppatch) {
return await proppatch({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
break;
case 'REPORT':
if (report) {
return await report({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
break;
default: default:
if (catchAll) {
return await catchAll({
request,
match,
user,
session: { userSession: session, tokenData },
isRunningLocally,
});
}
return new Response('Not Implemented', { status: 501 }); return new Response('Not Implemented', { status: 501 });
} }
@ -332,7 +247,7 @@ const routes: Routes = {
index: createPageRouteHandler('index.ts', '/'), index: createPageRouteHandler('index.ts', '/'),
login: createPageRouteHandler('login.tsx', '/login'), login: createPageRouteHandler('login.tsx', '/login'),
signup: createPageRouteHandler('signup.ts', '/signup'), signup: createPageRouteHandler('signup.ts', '/signup'),
logout: createPageRouteHandler('login.ts', '/logout'), logout: createPageRouteHandler('logout.ts', '/logout'),
caldav: createPageRouteHandler('caldav.ts', '/caldav/:path*{/}?'), caldav: createPageRouteHandler('caldav.ts', '/caldav/:path*{/}?'),
calendar: createPageRouteHandler('calendar.ts', '/calendar'), calendar: createPageRouteHandler('calendar.ts', '/calendar'),
calendars: createPageRouteHandler('calendars.ts', '/calendars'), calendars: createPageRouteHandler('calendars.ts', '/calendars'),