Add CardDav and CalDav servers (#80)
* Add CardDav and CalDav servers This implements the servers, but not the clients (yet). The implementation is essentially a proxy to Radicale (as a container in `docker-compose.yml`), with certain security assurances. If you're upgrading, basically you'll need to create a new `data-radicale` directory, and everything else should just work. This will also release v2.3.0 with those enabled by default. Tested with Thunderbird and Apple Calendar + Contacts. To disable these, simply add the new config details and comment out or don't add the new `radicale` service from `docker-compose.yml`. Related to #56
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,3 +15,6 @@ data-files/
|
|||||||
|
|
||||||
# Config
|
# Config
|
||||||
bewcloud.config.ts
|
bewcloud.config.ts
|
||||||
|
|
||||||
|
# Radicale files
|
||||||
|
data-radicale/
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -26,7 +26,7 @@ Download/copy [`docker-compose.yml`](/docker-compose.yml), [`.env.sample`](/.env
|
|||||||
> `1993:1993` below comes from deno's [docker image](https://github.com/denoland/deno_docker/blob/2abfe921484bdc79d11c7187a9d7b59537457c31/ubuntu.dockerfile#L20-L22) where `1993` is the default user id in it. It might change in the future since I don't control it.
|
> `1993:1993` below comes from deno's [docker image](https://github.com/denoland/deno_docker/blob/2abfe921484bdc79d11c7187a9d7b59537457c31/ubuntu.dockerfile#L20-L22) where `1993` is the default user id in it. It might change in the future since I don't control it.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ mkdir data-files # local directory for storing user-uploaded files
|
$ mkdir data-files data-radicale # local directories for storing user-uploaded files and radicale data
|
||||||
$ sudo chown -R 1993:1993 data-files # solves permission-related issues in the container with uploading files
|
$ sudo chown -R 1993:1993 data-files # solves permission-related issues in the container with uploading files
|
||||||
$ docker compose up -d # makes the app available at http://localhost:8000
|
$ docker compose up -d # makes the app available at http://localhost:8000
|
||||||
$ docker compose run --rm website bash -c "cd /app && make migrate-db" # initializes/updates the database (only needs to be executed the first time and on any data updates)
|
$ docker compose run --rm website bash -c "cd /app && make migrate-db" # initializes/updates the database (only needs to be executed the first time and on any data updates)
|
||||||
@@ -89,11 +89,14 @@ $ make build # generates all static files for production deploy
|
|||||||
|
|
||||||
Just push to the `main` branch.
|
Just push to the `main` branch.
|
||||||
|
|
||||||
## Where's Contacts/Calendar (CardDav/CalDav)?! Wasn't this supposed to be a core Nextcloud replacement?
|
## How does Contacts/CardDav and Calendar/CalDav work?
|
||||||
|
|
||||||
[Check this tag/release for more info and the code where/when that was being done](https://github.com/bewcloud/bewcloud/releases/tag/v0.0.1-self-made-carddav-caldav). Contacts/CardDav worked and Calendar/CalDav mostly worked as well at that point.
|
CalDav/CardDav is now available since [v2.3.0](https://github.com/bewcloud/bewcloud/releases/tag/v2.3.0), using [Radicale](https://radicale.org/v3.html) via Docker, which is already _very_ efficient (and battle-tested). The clients are not yet implemented. [Check this tag/release for custom-made server and clients where it was all mostly working, except for many edge cases](https://github.com/bewcloud/bewcloud/releases/tag/v0.0.1-self-made-carddav-caldav).
|
||||||
|
|
||||||
My focus was to get me to replace Nextcloud for me and my family ASAP, and it turns out it's not easy to do it all in a single, installable _thing_, so I focused on the Files UI, sync, and sharing, since [Radicale](https://radicale.org/v3.html) solved my other issues better than my own solution (and it's already _very_ efficient).
|
In order to share a calendar, you can either have a shared user, or you can symlink the calendar to the user's own calendar (simply `ln -s /<absolute-path-to-data-radicale>/collections/collection-root/<owner-user-id>/<calendar-to-share> /<absolute-path-to-data-radicale>/collections/collection-root/<user-id-to-share-with>/`).
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you're running radicale with docker, the symlink needs to point to the container's directory, usually starting with `/data` if you didn't change the `radicale-config/config`, otherwise the container will fail to load the linked directory.
|
||||||
|
|
||||||
## How does private file sharing work?
|
## How does private file sharing work?
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ const config: PartialDeep<Config> = {
|
|||||||
// host: 'localhost',
|
// host: 'localhost',
|
||||||
// port: 465,
|
// port: 465,
|
||||||
// },
|
// },
|
||||||
|
// contacts: {
|
||||||
|
// enableCardDavServer: true,
|
||||||
|
// cardDavUrl: 'http://127.0.0.1:5232',
|
||||||
|
// },
|
||||||
|
// calendar: {
|
||||||
|
// enableCalDavServer: true,
|
||||||
|
// calDavUrl: 'http://127.0.0.1:5232',
|
||||||
|
// },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -48,11 +48,22 @@ interface MainFilesProps {
|
|||||||
initialPath: string;
|
initialPath: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
isFileSharingAllowed: boolean;
|
isFileSharingAllowed: boolean;
|
||||||
|
isCardDavEnabled?: boolean;
|
||||||
|
isCalDavEnabled?: boolean;
|
||||||
fileShareId?: string;
|
fileShareId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MainFiles(
|
export default function MainFiles(
|
||||||
{ initialDirectories, initialFiles, initialPath, baseUrl, isFileSharingAllowed, fileShareId }: MainFilesProps,
|
{
|
||||||
|
initialDirectories,
|
||||||
|
initialFiles,
|
||||||
|
initialPath,
|
||||||
|
baseUrl,
|
||||||
|
isFileSharingAllowed,
|
||||||
|
isCardDavEnabled,
|
||||||
|
isCalDavEnabled,
|
||||||
|
fileShareId,
|
||||||
|
}: MainFilesProps,
|
||||||
) {
|
) {
|
||||||
const isAdding = useSignal<boolean>(false);
|
const isAdding = useSignal<boolean>(false);
|
||||||
const isUploading = useSignal<boolean>(false);
|
const isUploading = useSignal<boolean>(false);
|
||||||
@@ -879,6 +890,24 @@ export default function MainFiles(
|
|||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
|
{!fileShareId && isCardDavEnabled
|
||||||
|
? (
|
||||||
|
<section class='flex flex-row items-center justify-start my-12'>
|
||||||
|
<span class='font-semibold'>CardDav URL:</span>{' '}
|
||||||
|
<code class='bg-slate-600 mx-2 px-2 py-1 rounded-md'>{baseUrl}/carddav</code>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{!fileShareId && isCalDavEnabled
|
||||||
|
? (
|
||||||
|
<section class='flex flex-row items-center justify-start my-12'>
|
||||||
|
<span class='font-semibold'>CalDav URL:</span>{' '}
|
||||||
|
<code class='bg-slate-600 mx-2 px-2 py-1 rounded-md'>{baseUrl}/caldav</code>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
|
||||||
{!fileShareId
|
{!fileShareId
|
||||||
? (
|
? (
|
||||||
<CreateDirectoryModal
|
<CreateDirectoryModal
|
||||||
|
|||||||
@@ -14,6 +14,29 @@ services:
|
|||||||
memlock:
|
memlock:
|
||||||
soft: -1
|
soft: -1
|
||||||
hard: -1
|
hard: -1
|
||||||
|
mem_limit: '256m'
|
||||||
|
|
||||||
|
# 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.4.0
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:5232:5232
|
||||||
|
init: true
|
||||||
|
read_only: true
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- SETUID
|
||||||
|
- SETGID
|
||||||
|
- CHOWN
|
||||||
|
- KILL
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data-radicale:/data
|
||||||
|
- ./radicale-config:/config:ro
|
||||||
|
mem_limit: '256m'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
website:
|
website:
|
||||||
image: ghcr.io/bewcloud/bewcloud:v2.2.3
|
image: ghcr.io/bewcloud/bewcloud:v2.3.0
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8000:8000
|
- 127.0.0.1:8000:8000
|
||||||
@@ -29,6 +29,28 @@ services:
|
|||||||
hard: -1
|
hard: -1
|
||||||
mem_limit: '256m'
|
mem_limit: '256m'
|
||||||
|
|
||||||
|
# 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.4.0
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:5232:5232
|
||||||
|
init: true
|
||||||
|
read_only: true
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- SETUID
|
||||||
|
- SETGID
|
||||||
|
- CHOWN
|
||||||
|
- KILL
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./data-radicale:/data
|
||||||
|
- ./radicale-config:/config:ro
|
||||||
|
mem_limit: '256m'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
bewcloud-db:
|
bewcloud-db:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// This file SHOULD be checked into source version control.
|
// This file SHOULD be checked into source version control.
|
||||||
// This file is automatically updated during development when running `dev.ts`.
|
// This file is automatically updated during development when running `dev.ts`.
|
||||||
|
|
||||||
|
import * as $_well_known_caldav from './routes/.well-known/caldav.tsx';
|
||||||
|
import * as $_well_known_carddav from './routes/.well-known/carddav.tsx';
|
||||||
import * as $_404 from './routes/_404.tsx';
|
import * as $_404 from './routes/_404.tsx';
|
||||||
import * as $_app from './routes/_app.tsx';
|
import * as $_app from './routes/_app.tsx';
|
||||||
import * as $_middleware from './routes/_middleware.tsx';
|
import * as $_middleware from './routes/_middleware.tsx';
|
||||||
@@ -45,6 +47,8 @@ import * as $api_news_import_feeds from './routes/api/news/import-feeds.tsx';
|
|||||||
import * as $api_news_mark_read from './routes/api/news/mark-read.tsx';
|
import * as $api_news_mark_read from './routes/api/news/mark-read.tsx';
|
||||||
import * as $api_news_refresh_articles from './routes/api/news/refresh-articles.tsx';
|
import * as $api_news_refresh_articles from './routes/api/news/refresh-articles.tsx';
|
||||||
import * as $api_notes_save from './routes/api/notes/save.tsx';
|
import * as $api_notes_save from './routes/api/notes/save.tsx';
|
||||||
|
import * as $caldav from './routes/caldav.tsx';
|
||||||
|
import * as $carddav from './routes/carddav.tsx';
|
||||||
import * as $dashboard from './routes/dashboard.tsx';
|
import * as $dashboard from './routes/dashboard.tsx';
|
||||||
import * as $dav from './routes/dav.tsx';
|
import * as $dav from './routes/dav.tsx';
|
||||||
import * as $expenses from './routes/expenses.tsx';
|
import * as $expenses from './routes/expenses.tsx';
|
||||||
@@ -82,6 +86,8 @@ import type { Manifest } from '$fresh/server.ts';
|
|||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
routes: {
|
routes: {
|
||||||
|
'./routes/.well-known/caldav.tsx': $_well_known_caldav,
|
||||||
|
'./routes/.well-known/carddav.tsx': $_well_known_carddav,
|
||||||
'./routes/_404.tsx': $_404,
|
'./routes/_404.tsx': $_404,
|
||||||
'./routes/_app.tsx': $_app,
|
'./routes/_app.tsx': $_app,
|
||||||
'./routes/_middleware.tsx': $_middleware,
|
'./routes/_middleware.tsx': $_middleware,
|
||||||
@@ -125,6 +131,8 @@ const manifest = {
|
|||||||
'./routes/api/news/mark-read.tsx': $api_news_mark_read,
|
'./routes/api/news/mark-read.tsx': $api_news_mark_read,
|
||||||
'./routes/api/news/refresh-articles.tsx': $api_news_refresh_articles,
|
'./routes/api/news/refresh-articles.tsx': $api_news_refresh_articles,
|
||||||
'./routes/api/notes/save.tsx': $api_notes_save,
|
'./routes/api/notes/save.tsx': $api_notes_save,
|
||||||
|
'./routes/caldav.tsx': $caldav,
|
||||||
|
'./routes/carddav.tsx': $carddav,
|
||||||
'./routes/dashboard.tsx': $dashboard,
|
'./routes/dashboard.tsx': $dashboard,
|
||||||
'./routes/dav.tsx': $dav,
|
'./routes/dav.tsx': $dav,
|
||||||
'./routes/expenses.tsx': $expenses,
|
'./routes/expenses.tsx': $expenses,
|
||||||
|
|||||||
@@ -7,12 +7,23 @@ interface FilesWrapperProps {
|
|||||||
initialPath: string;
|
initialPath: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
isFileSharingAllowed: boolean;
|
isFileSharingAllowed: boolean;
|
||||||
|
isCardDavEnabled?: boolean;
|
||||||
|
isCalDavEnabled?: boolean;
|
||||||
fileShareId?: string;
|
fileShareId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This wrapper is necessary because islands need to be the first frontend component, but they don't support functions as props, so the more complex logic needs to live in the component itself
|
// This wrapper is necessary because islands need to be the first frontend component, but they don't support functions as props, so the more complex logic needs to live in the component itself
|
||||||
export default function FilesWrapper(
|
export default function FilesWrapper(
|
||||||
{ initialDirectories, initialFiles, initialPath, baseUrl, isFileSharingAllowed, fileShareId }: FilesWrapperProps,
|
{
|
||||||
|
initialDirectories,
|
||||||
|
initialFiles,
|
||||||
|
initialPath,
|
||||||
|
baseUrl,
|
||||||
|
isFileSharingAllowed,
|
||||||
|
isCardDavEnabled,
|
||||||
|
isCalDavEnabled,
|
||||||
|
fileShareId,
|
||||||
|
}: FilesWrapperProps,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<MainFiles
|
<MainFiles
|
||||||
@@ -21,6 +32,8 @@ export default function FilesWrapper(
|
|||||||
initialPath={initialPath}
|
initialPath={initialPath}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
isFileSharingAllowed={isFileSharingAllowed}
|
isFileSharingAllowed={isFileSharingAllowed}
|
||||||
|
isCardDavEnabled={isCardDavEnabled}
|
||||||
|
isCalDavEnabled={isCalDavEnabled}
|
||||||
fileShareId={fileShareId}
|
fileShareId={fileShareId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ export class AppConfig {
|
|||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 465,
|
port: 465,
|
||||||
},
|
},
|
||||||
|
contacts: {
|
||||||
|
enableCardDavServer: true,
|
||||||
|
cardDavUrl: 'http://127.0.0.1:5232',
|
||||||
|
},
|
||||||
|
calendar: {
|
||||||
|
enableCalDavServer: true,
|
||||||
|
calDavUrl: 'http://127.0.0.1:5232',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,4 +184,16 @@ export class AppConfig {
|
|||||||
|
|
||||||
return this.config.email;
|
return this.config.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getContactsConfig(): Promise<Config['contacts']> {
|
||||||
|
await this.loadConfig();
|
||||||
|
|
||||||
|
return this.config.contacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getCalendarConfig(): Promise<Config['calendar']> {
|
||||||
|
await this.loadConfig();
|
||||||
|
|
||||||
|
return this.config.calendar;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
lib/types.ts
12
lib/types.ts
@@ -200,6 +200,18 @@ export interface Config {
|
|||||||
/** The SMTP port to send emails from */
|
/** The SMTP port to send emails from */
|
||||||
port: number;
|
port: number;
|
||||||
};
|
};
|
||||||
|
contacts: {
|
||||||
|
/** If true, the CardDAV server will be enabled (proxied) */
|
||||||
|
enableCardDavServer: boolean;
|
||||||
|
/** The CardDAV server URL to proxy to */
|
||||||
|
cardDavUrl: string;
|
||||||
|
};
|
||||||
|
calendar: {
|
||||||
|
/** If true, the CalDAV server will be enabled (proxied) */
|
||||||
|
enableCalDavServer: boolean;
|
||||||
|
/** The CalDAV server URL to proxy to */
|
||||||
|
calDavUrl: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MultiFactorAuthMethodType = 'totp' | 'passkey' | 'email';
|
export type MultiFactorAuthMethodType = 'totp' | 'passkey' | 'email';
|
||||||
|
|||||||
149
radicale-config/config
Normal file
149
radicale-config/config
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# -*- mode: conf -*-
|
||||||
|
# vim:ft=cfg
|
||||||
|
|
||||||
|
# Config file for Radicale - A simple calendar server
|
||||||
|
|
||||||
|
[server]
|
||||||
|
|
||||||
|
# CalDAV server hostnames separated by a comma
|
||||||
|
# IPv4 syntax: address:port
|
||||||
|
# IPv6 syntax: [address]:port
|
||||||
|
hosts = 0.0.0.0:5232
|
||||||
|
|
||||||
|
# Max parallel connections
|
||||||
|
#max_connections = 8
|
||||||
|
|
||||||
|
# Max size of request body (bytes)
|
||||||
|
#max_content_length = 100000000
|
||||||
|
|
||||||
|
# Socket timeout (seconds)
|
||||||
|
#timeout = 30
|
||||||
|
|
||||||
|
|
||||||
|
[encoding]
|
||||||
|
|
||||||
|
# Encoding for responding requests
|
||||||
|
#request = utf-8
|
||||||
|
|
||||||
|
# Encoding for storing local collections
|
||||||
|
#stock = utf-8
|
||||||
|
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
|
||||||
|
# Authentication method
|
||||||
|
#type = none
|
||||||
|
type = http_x_remote_user
|
||||||
|
|
||||||
|
# Cache logins for until expiration time
|
||||||
|
#cache_logins = false
|
||||||
|
|
||||||
|
# Expiration time for caching successful logins in seconds
|
||||||
|
#cache_successful_logins_expiry = 15
|
||||||
|
|
||||||
|
## Expiration time of caching failed logins in seconds
|
||||||
|
#cache_failed_logins_expiry = 90
|
||||||
|
|
||||||
|
# IMAP server hostname
|
||||||
|
# Syntax: address | address:port | [address]:port | imap.server.tld
|
||||||
|
#imap_host = localhost
|
||||||
|
|
||||||
|
# Secure the IMAP connection
|
||||||
|
# Value: tls | starttls | none
|
||||||
|
#imap_security = tls
|
||||||
|
|
||||||
|
# OAuth2 token endpoint URL
|
||||||
|
#oauth2_token_endpoint = <URL>
|
||||||
|
|
||||||
|
|
||||||
|
[rights]
|
||||||
|
|
||||||
|
# Rights backend
|
||||||
|
# Value: authenticated | owner_only | owner_write | from_file
|
||||||
|
#type = owner_only
|
||||||
|
|
||||||
|
# File for rights management from_file
|
||||||
|
#file = /etc/radicale/rights
|
||||||
|
|
||||||
|
# Permit delete of a collection (global)
|
||||||
|
#permit_delete_collection = True
|
||||||
|
|
||||||
|
# Permit overwrite of a collection (global)
|
||||||
|
#permit_overwrite_collection = True
|
||||||
|
|
||||||
|
# URL Decode the given username (when URL-encoded by the client - useful for iOS devices when using email address)
|
||||||
|
# urldecode_username = False
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
|
||||||
|
# Storage backend
|
||||||
|
# Value: multifilesystem | multifilesystem_nolock
|
||||||
|
#type = multifilesystem
|
||||||
|
|
||||||
|
# Folder for storing local collections, created if not present
|
||||||
|
filesystem_folder = /data/collections
|
||||||
|
|
||||||
|
# Delete sync token that are older (seconds)
|
||||||
|
#max_sync_token_age = 2592000
|
||||||
|
|
||||||
|
# Skip broken item instead of triggering an exception
|
||||||
|
#skip_broken_item = True
|
||||||
|
|
||||||
|
[web]
|
||||||
|
|
||||||
|
# Web interface backend
|
||||||
|
# Value: none | internal
|
||||||
|
#type = internal
|
||||||
|
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
|
||||||
|
# Threshold for the logger
|
||||||
|
# Value: debug | info | warning | error | critical
|
||||||
|
#level = info
|
||||||
|
|
||||||
|
# Don't include passwords in logs
|
||||||
|
#mask_passwords = True
|
||||||
|
|
||||||
|
# Log bad PUT request content
|
||||||
|
#bad_put_request_content = False
|
||||||
|
|
||||||
|
# Log backtrace on level=debug
|
||||||
|
#backtrace_on_debug = False
|
||||||
|
|
||||||
|
# Log request header on level=debug
|
||||||
|
#request_header_on_debug = False
|
||||||
|
|
||||||
|
# Log request content on level=debug
|
||||||
|
#request_content_on_debug = False
|
||||||
|
|
||||||
|
# Log response content on level=debug
|
||||||
|
#response_content_on_debug = False
|
||||||
|
|
||||||
|
# Log rights rule which doesn't match on level=debug
|
||||||
|
#rights_rule_doesnt_match_on_debug = False
|
||||||
|
|
||||||
|
# Log storage cache actions on level=debug
|
||||||
|
#storage_cache_actions_on_debug = False
|
||||||
|
|
||||||
|
[headers]
|
||||||
|
|
||||||
|
# Additional HTTP headers
|
||||||
|
#Access-Control-Allow-Origin = *
|
||||||
|
|
||||||
|
|
||||||
|
[hook]
|
||||||
|
|
||||||
|
# Hook types
|
||||||
|
# Value: none | rabbitmq
|
||||||
|
#type = none
|
||||||
|
#rabbitmq_endpoint =
|
||||||
|
#rabbitmq_topic =
|
||||||
|
#rabbitmq_queue_type = classic
|
||||||
|
|
||||||
|
|
||||||
|
[reporting]
|
||||||
|
|
||||||
|
# When returning a free-busy report, limit the number of returned
|
||||||
|
# occurences per event to prevent DOS attacks.
|
||||||
|
#max_freebusy_occurrence = 10000
|
||||||
9
routes/.well-known/caldav.tsx
Normal file
9
routes/.well-known/caldav.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Handler } from 'fresh/server.ts';
|
||||||
|
|
||||||
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
|
|
||||||
|
interface Data {}
|
||||||
|
|
||||||
|
export const handler: Handler<Data, FreshContextState> = () => {
|
||||||
|
return new Response('Redirecting...', { status: 301, headers: { 'Location': '/caldav' } });
|
||||||
|
};
|
||||||
9
routes/.well-known/carddav.tsx
Normal file
9
routes/.well-known/carddav.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Handler } from 'fresh/server.ts';
|
||||||
|
|
||||||
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
|
|
||||||
|
interface Data {}
|
||||||
|
|
||||||
|
export const handler: Handler<Data, FreshContextState> = () => {
|
||||||
|
return new Response('Redirecting...', { status: 301, headers: { 'Location': '/carddav' } });
|
||||||
|
};
|
||||||
@@ -7,7 +7,10 @@ export const handler = [
|
|||||||
async function handleCors(request: Request, context: FreshContext<FreshContextState>) {
|
async function handleCors(request: Request, context: FreshContext<FreshContextState>) {
|
||||||
const path = new URL(request.url).pathname;
|
const path = new URL(request.url).pathname;
|
||||||
|
|
||||||
if (request.method == 'OPTIONS' && path !== '/dav' && !path.startsWith('/dav/')) {
|
if (
|
||||||
|
request.method == 'OPTIONS' && path !== '/dav' && !path.startsWith('/dav/') && path !== '/carddav' &&
|
||||||
|
!path.startsWith('/carddav/') && path !== '/caldav' && !path.startsWith('/caldav/')
|
||||||
|
) {
|
||||||
const response = new Response(null, {
|
const response = new Response(null, {
|
||||||
status: 204,
|
status: 204,
|
||||||
});
|
});
|
||||||
@@ -68,10 +71,14 @@ export const handler = [
|
|||||||
const response = await context.next();
|
const response = await context.next();
|
||||||
|
|
||||||
console.info(`${new Date().toISOString()} - [${response.status}] ${request.method} ${request.url}`);
|
console.info(`${new Date().toISOString()} - [${response.status}] ${request.method} ${request.url}`);
|
||||||
// NOTE: Uncomment when debugging WebDav stuff
|
// NOTE: Uncomment when debugging WebDav/CardDav/CalDav stuff
|
||||||
// if (request.url.includes('/dav')) {
|
// if (request.url.includes('/dav') || request.url.includes('/carddav') || request.url.includes('/caldav')) {
|
||||||
// console.info(`Request`, request.headers);
|
// console.info(`Request`, request.headers);
|
||||||
|
// try {
|
||||||
// console.info((await request.clone().text()) || '<No Body>');
|
// console.info((await request.clone().text()) || '<No Body>');
|
||||||
|
// } catch (_error) {
|
||||||
|
// console.info('<No Body>');
|
||||||
|
// }
|
||||||
// console.info(`Response`, response.headers);
|
// console.info(`Response`, response.headers);
|
||||||
// console.info(`Status`, response.status);
|
// console.info(`Status`, response.status);
|
||||||
// }
|
// }
|
||||||
|
|||||||
69
routes/caldav.tsx
Normal file
69
routes/caldav.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Handler, RouteConfig } from 'fresh/server.ts';
|
||||||
|
|
||||||
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
|
import { AppConfig } from '/lib/config.ts';
|
||||||
|
|
||||||
|
interface Data {}
|
||||||
|
|
||||||
|
export const config: RouteConfig = {
|
||||||
|
routeOverride: '/caldav/:path*',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||||
|
const calendarConfig = await AppConfig.getCalendarConfig();
|
||||||
|
|
||||||
|
if (!calendarConfig.enableCalDavServer) {
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.state.user) {
|
||||||
|
return new Response('Unauthorized', {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { path } = context.params;
|
||||||
|
|
||||||
|
const userId = context.state.user.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestBodyText = await request.clone().text();
|
||||||
|
|
||||||
|
// Remove the `/caldav/` prefix from the hrefs in the request
|
||||||
|
const parsedRequestBodyText = requestBodyText.replaceAll('<href>/caldav/', `<href>/`).replaceAll(
|
||||||
|
':href>/caldav/',
|
||||||
|
`:href>/`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await fetch(`${calendarConfig.calDavUrl}/${path}`, {
|
||||||
|
headers: {
|
||||||
|
...Object.fromEntries(request.headers.entries()),
|
||||||
|
'X-Remote-User': `${userId}`,
|
||||||
|
},
|
||||||
|
method: request.method,
|
||||||
|
body: parsedRequestBodyText,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return new Response(null, { status: 204 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBodyText = await response.clone().text();
|
||||||
|
|
||||||
|
// Add the `/caldav/` prefix to the hrefs in the response
|
||||||
|
const parsedBodyResponseText = responseBodyText.replaceAll('<href>/', `<href>/caldav/`).replaceAll(
|
||||||
|
':href>/',
|
||||||
|
`:href>/caldav/`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Response(parsedBodyResponseText, {
|
||||||
|
status: response.status,
|
||||||
|
headers: response.headers,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, { status: 405 });
|
||||||
|
};
|
||||||
69
routes/carddav.tsx
Normal file
69
routes/carddav.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Handler, RouteConfig } from 'fresh/server.ts';
|
||||||
|
|
||||||
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
|
import { AppConfig } from '/lib/config.ts';
|
||||||
|
|
||||||
|
interface Data {}
|
||||||
|
|
||||||
|
export const config: RouteConfig = {
|
||||||
|
routeOverride: '/carddav/:path*',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||||
|
const contactsConfig = await AppConfig.getContactsConfig();
|
||||||
|
|
||||||
|
if (!contactsConfig.enableCardDavServer) {
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.state.user) {
|
||||||
|
return new Response('Unauthorized', {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { path } = context.params;
|
||||||
|
|
||||||
|
const userId = context.state.user.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestBodyText = await request.clone().text();
|
||||||
|
|
||||||
|
// Remove the `/carddav/` prefix from the hrefs in the request
|
||||||
|
const parsedRequestBodyText = requestBodyText.replaceAll('<href>/carddav/', `<href>/`).replaceAll(
|
||||||
|
':href>/carddav/',
|
||||||
|
`:href>/`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await fetch(`${contactsConfig.cardDavUrl}/${path}`, {
|
||||||
|
headers: {
|
||||||
|
...Object.fromEntries(request.headers.entries()),
|
||||||
|
'X-Remote-User': `${userId}`,
|
||||||
|
},
|
||||||
|
method: request.method,
|
||||||
|
body: parsedRequestBodyText,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return new Response(null, { status: 204 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBodyText = await response.clone().text();
|
||||||
|
|
||||||
|
// Add the `/carddav/` prefix to the hrefs in the response
|
||||||
|
const parsedBodyResponseText = responseBodyText.replaceAll('<href>/', `<href>/carddav/`).replaceAll(
|
||||||
|
':href>/',
|
||||||
|
`:href>/carddav/`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Response(parsedBodyResponseText, {
|
||||||
|
status: response.status,
|
||||||
|
headers: response.headers,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, { status: 405 });
|
||||||
|
};
|
||||||
@@ -11,6 +11,8 @@ interface Data {
|
|||||||
currentPath: string;
|
currentPath: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
isFileSharingAllowed: boolean;
|
isFileSharingAllowed: boolean;
|
||||||
|
isCardDavEnabled: boolean;
|
||||||
|
isCalDavEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handler: Handlers<Data, FreshContextState> = {
|
export const handler: Handlers<Data, FreshContextState> = {
|
||||||
@@ -40,6 +42,11 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
const userFiles = await FileModel.list(context.state.user.id, currentPath);
|
const userFiles = await FileModel.list(context.state.user.id, currentPath);
|
||||||
|
|
||||||
const isPublicFileSharingAllowed = await AppConfig.isPublicFileSharingAllowed();
|
const isPublicFileSharingAllowed = await AppConfig.isPublicFileSharingAllowed();
|
||||||
|
const contactsConfig = await AppConfig.getContactsConfig();
|
||||||
|
const calendarConfig = await AppConfig.getCalendarConfig();
|
||||||
|
|
||||||
|
const isCardDavEnabled = contactsConfig.enableCardDavServer;
|
||||||
|
const isCalDavEnabled = calendarConfig.enableCalDavServer;
|
||||||
|
|
||||||
return await context.render({
|
return await context.render({
|
||||||
userDirectories,
|
userDirectories,
|
||||||
@@ -47,6 +54,8 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
currentPath,
|
currentPath,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
isFileSharingAllowed: isPublicFileSharingAllowed,
|
isFileSharingAllowed: isPublicFileSharingAllowed,
|
||||||
|
isCardDavEnabled,
|
||||||
|
isCalDavEnabled,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -60,6 +69,8 @@ export default function FilesPage({ data }: PageProps<Data, FreshContextState>)
|
|||||||
initialPath={data.currentPath}
|
initialPath={data.currentPath}
|
||||||
baseUrl={data.baseUrl}
|
baseUrl={data.baseUrl}
|
||||||
isFileSharingAllowed={data.isFileSharingAllowed}
|
isFileSharingAllowed={data.isFileSharingAllowed}
|
||||||
|
isCardDavEnabled={data.isCardDavEnabled}
|
||||||
|
isCalDavEnabled={data.isCalDavEnabled}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user