Build + offer docker image and docker-compose.yml file for easier self-hosting
Tweak login and auth for IP-based setups and setups without email enabled.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
PORT=8000
|
||||
BASE_URL="http://localhost:8000"
|
||||
|
||||
POSTGRESQL_HOST="localhost"
|
||||
POSTGRESQL_HOST="postgresql" # docker container name or external hostname/IP
|
||||
POSTGRESQL_USER="postgres"
|
||||
POSTGRESQL_PASSWORD="fake"
|
||||
POSTGRESQL_DBNAME="bewcloud"
|
||||
|
||||
47
.github/workflows/build-docker-image.yml
vendored
Normal file
47
.github/workflows/build-docker-image.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: "Build Docker Image"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -2,6 +2,8 @@ FROM denoland/deno:ubuntu-1.41.3
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
RUN apt-get update && apt-get install -y make
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# These steps will be re-run upon each file change in your working directory:
|
||||
|
||||
11
README.md
11
README.md
@@ -9,7 +9,14 @@ This is the [bewCloud app](https://bewcloud.com) built using [Fresh](https://fre
|
||||
|
||||
## Self-host it!
|
||||
|
||||
Check the [Development section below](#development).
|
||||
Download/copy [`docker-compose.yml`](/docker-compose.yml) and [`.env.sample`](/.env.sample) as `.env`.
|
||||
|
||||
```sh
|
||||
$ docker compose up # makes the app available at http://localhost:8000
|
||||
$ docker compose run website bash -c "cd /app && make migrate-db" # initializes/updates the database (only needs to be executed the first time and on any updates)
|
||||
```
|
||||
|
||||
Alternatively, check the [Development section below](#development).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Even with signups disabled (`CONFIG_ALLOW_SIGNUPS="false"`), the first signup will work and become an admin.
|
||||
@@ -25,7 +32,7 @@ Don't forget to set up your `.env` file based on `.env.sample`.
|
||||
## Development
|
||||
|
||||
```sh
|
||||
$ docker compose up # (optional) runs docker with postgres, locally
|
||||
$ docker compose -f docker-compose.dev.yml up # (optional) runs docker with postgres, locally
|
||||
$ make migrate-db # runs any missing database migrations
|
||||
$ make start # runs the app
|
||||
$ make format # formats the code
|
||||
|
||||
20
docker-compose.dev.yml
Normal file
20
docker-compose.dev.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=fake
|
||||
- POSTGRES_DB=bewcloud
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
driver: local
|
||||
@@ -1,4 +1,14 @@
|
||||
services:
|
||||
website:
|
||||
build: ghcr.io/bewcloud/bewcloud
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:8000:8000
|
||||
mem_limit: '256m'
|
||||
user: "${UID}:${GID}" # if you run into issues with permissions for the data-files volume below, see other options at https://stackoverflow.com/a/56904335
|
||||
volumes:
|
||||
- ./data-files:/app/data-files
|
||||
|
||||
postgresql:
|
||||
image: postgres:15
|
||||
environment:
|
||||
@@ -7,14 +17,15 @@ services:
|
||||
- POSTGRES_DB=bewcloud
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- bewcloud-db:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
- 127.0.0.1:5432:5432
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
mem_limit: '256m'
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
bewcloud-db:
|
||||
driver: local
|
||||
|
||||
25
lib/auth.ts
25
lib/auth.ts
@@ -18,6 +18,8 @@ export interface JwtData {
|
||||
};
|
||||
}
|
||||
|
||||
const isBaseUrlAnIp = () => /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(baseUrl);
|
||||
|
||||
const textToData = (text: string) => new TextEncoder().encode(text);
|
||||
|
||||
export const dataToText = (data: Uint8Array) => new TextDecoder().decode(data);
|
||||
@@ -152,15 +154,18 @@ export async function logoutUser(request: Request) {
|
||||
name: COOKIE_NAME,
|
||||
value: '',
|
||||
expires: tomorrow,
|
||||
domain: isRunningLocally(request)
|
||||
? 'localhost'
|
||||
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0],
|
||||
path: '/',
|
||||
secure: isRunningLocally(request) ? false : true,
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
};
|
||||
|
||||
if (!isBaseUrlAnIp()) {
|
||||
cookie.domain = isRunningLocally(request)
|
||||
? 'localhost'
|
||||
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
|
||||
}
|
||||
|
||||
const response = new Response('Logged Out', {
|
||||
status: 303,
|
||||
headers: { 'Location': '/', 'Content-Type': 'text/html; charset=utf-8' },
|
||||
@@ -203,13 +208,18 @@ export async function createSessionCookie(
|
||||
name: COOKIE_NAME,
|
||||
value: token,
|
||||
expires: newSession.expires_at,
|
||||
domain: isRunningLocally(request) ? 'localhost' : baseUrl.replace('https://', ''),
|
||||
path: '/',
|
||||
secure: isRunningLocally(request) ? false : true,
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
};
|
||||
|
||||
if (!isBaseUrlAnIp()) {
|
||||
cookie.domain = isRunningLocally(request)
|
||||
? 'localhost'
|
||||
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
|
||||
}
|
||||
|
||||
setCookie(response.headers, cookie);
|
||||
|
||||
return response;
|
||||
@@ -227,13 +237,18 @@ export async function updateSessionCookie(
|
||||
name: COOKIE_NAME,
|
||||
value: token,
|
||||
expires: userSession.expires_at,
|
||||
domain: isRunningLocally(request) ? 'localhost' : baseUrl.replace('https://', ''),
|
||||
path: '/',
|
||||
secure: isRunningLocally(request) ? false : true,
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
};
|
||||
|
||||
if (!isBaseUrlAnIp()) {
|
||||
cookie.domain = isRunningLocally(request)
|
||||
? 'localhost'
|
||||
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
|
||||
}
|
||||
|
||||
setCookie(response.headers, cookie);
|
||||
|
||||
return response;
|
||||
|
||||
@@ -31,7 +31,11 @@ export const handler: Handlers<Data, FreshContextState> = {
|
||||
email = searchParams.get('email') || '';
|
||||
formData.set('email', email);
|
||||
|
||||
notice = `You have received a code in your email. Use it to verify your email and login.`;
|
||||
if (isEmailEnabled()) {
|
||||
notice = `You have received a code in your email. Use it to verify your email and login.`;
|
||||
} else {
|
||||
notice = `Your account was created successfully. Login below.`;
|
||||
}
|
||||
}
|
||||
|
||||
return await context.render({ notice, email, formData });
|
||||
@@ -153,7 +157,7 @@ export default function Login({ data }: PageProps<Data, FreshContextState>) {
|
||||
: null}
|
||||
|
||||
<form method='POST' class='mb-12'>
|
||||
{formFields(data?.email, data?.notice?.includes('verify your email')).map((field) =>
|
||||
{formFields(data?.email, data?.notice?.includes('verify your email') && isEmailEnabled()).map((field) =>
|
||||
generateFieldHtml(field, data?.formData || new FormData())
|
||||
)}
|
||||
<section class='flex justify-center mt-8 mb-4'>
|
||||
|
||||
Reference in New Issue
Block a user