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
|
PORT=8000
|
||||||
BASE_URL="http://localhost:8000"
|
BASE_URL="http://localhost:8000"
|
||||||
|
|
||||||
POSTGRESQL_HOST="localhost"
|
POSTGRESQL_HOST="postgresql" # docker container name or external hostname/IP
|
||||||
POSTGRESQL_USER="postgres"
|
POSTGRESQL_USER="postgres"
|
||||||
POSTGRESQL_PASSWORD="fake"
|
POSTGRESQL_PASSWORD="fake"
|
||||||
POSTGRESQL_DBNAME="bewcloud"
|
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
|
EXPOSE 8000
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y make
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# These steps will be re-run upon each file change in your working directory:
|
# 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!
|
## 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]
|
> [!IMPORTANT]
|
||||||
> Even with signups disabled (`CONFIG_ALLOW_SIGNUPS="false"`), the first signup will work and become an admin.
|
> 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
|
## Development
|
||||||
|
|
||||||
```sh
|
```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 migrate-db # runs any missing database migrations
|
||||||
$ make start # runs the app
|
$ make start # runs the app
|
||||||
$ make format # formats the code
|
$ 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:
|
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:
|
postgresql:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
environment:
|
environment:
|
||||||
@@ -7,14 +17,15 @@ services:
|
|||||||
- POSTGRES_DB=bewcloud
|
- POSTGRES_DB=bewcloud
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- bewcloud-db:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 127.0.0.1:5432:5432
|
||||||
ulimits:
|
ulimits:
|
||||||
memlock:
|
memlock:
|
||||||
soft: -1
|
soft: -1
|
||||||
hard: -1
|
hard: -1
|
||||||
|
mem_limit: '256m'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
bewcloud-db:
|
||||||
driver: local
|
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);
|
const textToData = (text: string) => new TextEncoder().encode(text);
|
||||||
|
|
||||||
export const dataToText = (data: Uint8Array) => new TextDecoder().decode(data);
|
export const dataToText = (data: Uint8Array) => new TextDecoder().decode(data);
|
||||||
@@ -152,15 +154,18 @@ export async function logoutUser(request: Request) {
|
|||||||
name: COOKIE_NAME,
|
name: COOKIE_NAME,
|
||||||
value: '',
|
value: '',
|
||||||
expires: tomorrow,
|
expires: tomorrow,
|
||||||
domain: isRunningLocally(request)
|
|
||||||
? 'localhost'
|
|
||||||
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0],
|
|
||||||
path: '/',
|
path: '/',
|
||||||
secure: isRunningLocally(request) ? false : true,
|
secure: isRunningLocally(request) ? false : true,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isBaseUrlAnIp()) {
|
||||||
|
cookie.domain = isRunningLocally(request)
|
||||||
|
? 'localhost'
|
||||||
|
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
|
||||||
|
}
|
||||||
|
|
||||||
const response = new Response('Logged Out', {
|
const response = new Response('Logged Out', {
|
||||||
status: 303,
|
status: 303,
|
||||||
headers: { 'Location': '/', 'Content-Type': 'text/html; charset=utf-8' },
|
headers: { 'Location': '/', 'Content-Type': 'text/html; charset=utf-8' },
|
||||||
@@ -203,13 +208,18 @@ export async function createSessionCookie(
|
|||||||
name: COOKIE_NAME,
|
name: COOKIE_NAME,
|
||||||
value: token,
|
value: token,
|
||||||
expires: newSession.expires_at,
|
expires: newSession.expires_at,
|
||||||
domain: isRunningLocally(request) ? 'localhost' : baseUrl.replace('https://', ''),
|
|
||||||
path: '/',
|
path: '/',
|
||||||
secure: isRunningLocally(request) ? false : true,
|
secure: isRunningLocally(request) ? false : true,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isBaseUrlAnIp()) {
|
||||||
|
cookie.domain = isRunningLocally(request)
|
||||||
|
? 'localhost'
|
||||||
|
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
|
||||||
|
}
|
||||||
|
|
||||||
setCookie(response.headers, cookie);
|
setCookie(response.headers, cookie);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -227,13 +237,18 @@ export async function updateSessionCookie(
|
|||||||
name: COOKIE_NAME,
|
name: COOKIE_NAME,
|
||||||
value: token,
|
value: token,
|
||||||
expires: userSession.expires_at,
|
expires: userSession.expires_at,
|
||||||
domain: isRunningLocally(request) ? 'localhost' : baseUrl.replace('https://', ''),
|
|
||||||
path: '/',
|
path: '/',
|
||||||
secure: isRunningLocally(request) ? false : true,
|
secure: isRunningLocally(request) ? false : true,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isBaseUrlAnIp()) {
|
||||||
|
cookie.domain = isRunningLocally(request)
|
||||||
|
? 'localhost'
|
||||||
|
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
|
||||||
|
}
|
||||||
|
|
||||||
setCookie(response.headers, cookie);
|
setCookie(response.headers, cookie);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
email = searchParams.get('email') || '';
|
email = searchParams.get('email') || '';
|
||||||
formData.set('email', email);
|
formData.set('email', email);
|
||||||
|
|
||||||
|
if (isEmailEnabled()) {
|
||||||
notice = `You have received a code in your email. Use it to verify your email and login.`;
|
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 });
|
return await context.render({ notice, email, formData });
|
||||||
@@ -153,7 +157,7 @@ export default function Login({ data }: PageProps<Data, FreshContextState>) {
|
|||||||
: null}
|
: null}
|
||||||
|
|
||||||
<form method='POST' class='mb-12'>
|
<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())
|
generateFieldHtml(field, data?.formData || new FormData())
|
||||||
)}
|
)}
|
||||||
<section class='flex justify-center mt-8 mb-4'>
|
<section class='flex justify-center mt-8 mb-4'>
|
||||||
|
|||||||
Reference in New Issue
Block a user