From 735b14544a48864fe21996b40644b1450ff4dd94 Mon Sep 17 00:00:00 2001 From: Bruno Bernardino Date: Tue, 9 Apr 2024 13:22:05 +0100 Subject: [PATCH] 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. --- .env.sample | 2 +- .github/workflows/build-docker-image.yml | 47 ++++++++++++++++++++++++ Dockerfile | 2 + README.md | 11 +++++- docker-compose.dev.yml | 20 ++++++++++ docker-compose.yml | 17 +++++++-- lib/auth.ts | 25 ++++++++++--- routes/login.tsx | 8 +++- 8 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/build-docker-image.yml create mode 100644 docker-compose.dev.yml diff --git a/.env.sample b/.env.sample index a21c4a3..cdc5713 100644 --- a/.env.sample +++ b/.env.sample @@ -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" diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml new file mode 100644 index 0000000..537faab --- /dev/null +++ b/.github/workflows/build-docker-image.yml @@ -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 }} diff --git a/Dockerfile b/Dockerfile index 2b916e5..7860c92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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: diff --git a/README.md b/README.md index 267128e..c224008 100644 --- a/README.md +++ b/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 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..dd7f967 --- /dev/null +++ b/docker-compose.dev.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index dd7f967..b04f3d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/lib/auth.ts b/lib/auth.ts index c4a72e7..54f2081 100644 --- a/lib/auth.ts +++ b/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; diff --git a/routes/login.tsx b/routes/login.tsx index 9650cf0..b8c76f0 100644 --- a/routes/login.tsx +++ b/routes/login.tsx @@ -31,7 +31,11 @@ export const handler: Handlers = { 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) { : null}
- {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()) )}