Fix tests, run against ci
Some checks failed
ci / build-image (push) Successful in 50s
ci / test-image (push) Failing after 3s

This commit is contained in:
Jonathan Cremin 2025-06-13 09:32:37 +01:00
parent b352b65a0c
commit 677dfe25af
6 changed files with 175 additions and 113 deletions

25
.envrc.example Normal file
View file

@ -0,0 +1,25 @@
export DEBUG="hostr*"
export NODE_ENV=development
export PORT=4040
export WEB_BASE_URL=http://localhost:$PORT
export API_BASE_URL=$WEB_BASE_URL/api
export UPLOAD_STORAGE_PATH=$HOME/.hostr/uploads
export COOKIE_KEY=INSECURE
export EMAIL_FROM=
export EMAIL_NAME=
export STATSD_HOST=localhost
export DATABASE_URL=postgresql://hostr:hostr@database:5432/hostr
export REDIS_URL=redis://localhost:6379
export SENDGRID_KEY=
export STRIPE_SECRET_KEY=
export STRIPE_PUBLIC_KEY=
# optional, some functionality will be disabled
export AWS_ENDPOINT= # only for AWS-like providers, not AWS
export AWS_ACCESS_KEY_ID=
export AWS_SECRET_ACCESS_KEY=
export AWS_BUCKET=
export VIRUSTOTAL_KEY=
export SENTRY_DSN=

View file

@ -1,40 +0,0 @@
name: ci
on:
push:
branches: main
pull_request:
types: [opened, synchronize, reopened]
jobs:
build-image:
runs-on: self-hosted
steps:
- name: Set current date as env variable
run: echo "NOW=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_ENV
- name: Fix for bad os check
run: echo "RUNNER_OS=Linux" >> $GITHUB_ENV
- name: Login to Forgejo Registry
uses: https://cremin.dev/actions/podman-login@v1
with:
registry: cremin.dev
username: ${{ github.actor }}
password: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
- name: Check out repository
uses: https://cremin.dev/actions/checkout@v4
- name: Build image
uses: https://cremin.dev/actions/buildah-build@v2
with:
containerfiles: ./Containerfile
context: ./
oci: true
layers: true
image: hostr
tags: latest ${{ github.sha }}
- name: Push image
uses: https://cremin.dev/actions/push-to-registry@v2
with:
registry: cremin.dev/jonathan
username: ${{ github.actor }}
password: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
image: hostr
tags: latest ${{ github.sha }}

85
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,85 @@
name: ci
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
services:
database:
image: postgres:10-alpine
env:
POSTGRES_PASSWORD: hostr
POSTGRES_USER: hostr
POSTGRES_DB: hostr
ports:
- 5432:5432
redis:
image: redis:4.0.2-alpine
ports:
- 6379:6379
minio:
image: minio/minio
env:
MINIO_ACCESS_KEY: 7HYV3KPRGQ8Z5YCDNWC6
MINIO_SECRET_KEY: 0kWP/ZkgIwQzgL9t4SGv9Uc93rO//OdyqMH329b/
ports:
- 9000:9000
cmd: server /export
jobs:
build-image:
runs-on: self-hosted
steps:
- name: Set current date as env variable
run: echo "NOW=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_ENV
- name: Fix for bad os check
run: echo "RUNNER_OS=Linux" >> $GITHUB_ENV
- name: Login to Forgejo Registry
uses: https://cremin.dev/actions/podman-login@v1
with:
registry: cremin.dev
username: ${{ github.actor }}
password: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
- name: Check out repository
uses: https://cremin.dev/actions/checkout@v4
- name: Build image
uses: https://cremin.dev/actions/buildah-build@v2
with:
containerfiles: ./Containerfile
context: ./
oci: true
layers: true
image: hostr
tags: latest ${{ github.sha }}
- name: Push image
uses: https://cremin.dev/actions/push-to-registry@v2
with:
registry: cremin.dev/jonathan
username: ${{ github.actor }}
password: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
image: hostr
tags: latest ${{ github.sha }}
test-image:
runs-on: self-hosted
needs: build-image
steps:
- name: Check out repository
uses: https://cremin.dev/actions/checkout@v4
- name: Test image
env:
WEB_BASE_URL: http://localhost:3000
API_BASE_URL: http://localhost:3000/api
UPLOAD_STORAGE_PATH: /hostr/uploads
COOKIE_KEY: TESTING
EMAIL_FROM: jonathan@hostr.co
EMAIL_NAME: "Jonathan from Hostr"
DATABASE_URL: postgresql://hostr:hostr@database:5432/hostr
REDIS_URL: redis://redis:6379
AWS_ENDPOINT: http://minio:9000
AWS_ACCESS_KEY_ID: 7HYV3KPRGQ8Z5YCDNWC6
AWS_SECRET_ACCESS_KEY: 0kWP/ZkgIwQzgL9t4SGv9Uc93rO//OdyqMH329b/
AWS_BUCKET: hostr
run: |
podman run --rm --env-host -it cremin.dev/jonathan/hostr:${{ github.sha }} yarn test

3
.gitignore vendored
View file

@ -1,4 +1,4 @@
.env* .envrc
.DS_Store .DS_Store
.sass-cache/ .sass-cache/
node_modules node_modules
@ -7,6 +7,7 @@ jspm_packages
npm-debug.log npm-debug.log
web/public/build web/public/build
web/public/styles/*.css web/public/styles/*.css
web/public/styles/*.css.map
*.gz *.gz
minigun*.json minigun*.json
test*.json test*.json

View file

@ -10,36 +10,36 @@ help:
.PHONY: build .PHONY: build
build: ## Run `yarn run build` build: ## Run `yarn run build`
docker-compose run --rm app yarn run build podman compose run --rm app yarn run build
.PHONY: test .PHONY: test
test: ## Run tests test: ## Run tests
docker-compose run --rm app yarn test podman compose run --rm app yarn test
.PHONY: logs .PHONY: logs
logs: ## Tail the app and worker logs logs: ## Tail the app and worker logs
docker-compose logs -f app worker podman compose logs -f app worker
.PHONY: migrate .PHONY: migrate
migrate: ## Migrate database schema migrate: ## Migrate database schema
docker-compose run --rm app yarn run initdb podman compose run --rm app yarn run initdb
.PHONY: init .PHONY: init
init: ## Migrate database schema init: ## Migrate database schema
docker-compose run --rm app yarn run init podman compose run --rm app yarn run init
.PHONY: watch-frontend .PHONY: watch-frontend
watch-frontend: ## Build and watch for changes watch-frontend: ## Build and watch for changes
docker-compose run --rm app yarn run watch podman compose run --rm app yarn run watch
.PHONY: docker-compose-up .PHONY: podman compose-up
docker-compose-up: ## Start (and create) docker containers podman compose-up: ## Start (and create) docker containers
docker-compose up -d podman compose up -d
.PHONY: yarn .PHONY: yarn
yarn: ## Update yarn dependencies yarn: ## Update yarn dependencies
docker-compose run --rm app yarn podman compose run --rm app yarn
.PHONY: shell .PHONY: shell
shell: ## Run shell shell: ## Run shell
docker-compose run --rm app sh podman compose run --rm app sh

View file

@ -1,29 +1,29 @@
import crypto from 'crypto'; import crypto from "crypto";
import { join } from 'path'; import { join } from "path";
import passwords from 'passwords'; import passwords from "passwords";
import uuid from 'node-uuid'; import uuid from "node-uuid";
import views from 'co-views'; import views from "co-views";
import debugname from 'debug'; import debugname from "debug";
import sendgrid from '@sendgrid/mail'; import sendgrid from "@sendgrid/mail";
import models from '../../models'; import models from "../../models";
const render = views(join(__dirname, '..', 'views'), { default: 'ejs' }); const render = views(join(__dirname, "..", "views"), { default: "ejs" });
const debug = debugname('hostr-web:auth'); const debug = debugname("hostr-web:auth");
sendgrid.setApiKey(process.env.SENDGRID_KEY); sendgrid.setApiKey(process.env.SENDGRID_KEY);
const from = process.env.EMAIL_FROM; const from = process.env.EMAIL_FROM;
const fromname = process.env.EMAIL_NAME; const fromname = process.env.EMAIL_NAME;
export async function authenticate(email, password) { export async function authenticate(email, password) {
const remoteIp = this.headers['x-forwarded-for'] || this.ip; const remoteIp = this.headers["x-forwarded-for"] || this.ip;
if (!password || password.length < 6) { if (!password || password.length < 6) {
debug('No password, or password too short'); debug("No password, or password too short");
return new Error('Invalid login details'); return new Error("Invalid login details");
} }
const count = await models.login.count({ const count = await models.login.count({
where: { where: {
ip: remoteIp.split(',')[0], ip: remoteIp.split(",")[0],
successful: false, successful: false,
createdAt: { createdAt: {
$gt: Math.ceil(Date.now()) - 600000, $gt: Math.ceil(Date.now()) - 600000,
@ -32,8 +32,8 @@ export async function authenticate(email, password) {
}); });
if (count > 25) { if (count > 25) {
debug('Throttling brute force'); debug("Throttling brute force");
return new Error('Invalid login details'); return new Error("Invalid login details");
} }
const user = await models.user.findOne({ const user = await models.user.findOne({
where: { where: {
@ -43,30 +43,29 @@ export async function authenticate(email, password) {
}); });
const login = await models.login.create({ const login = await models.login.create({
ip: remoteIp.split(',')[0], ip: remoteIp.split(",")[0],
successful: false, successful: false,
}); });
if (user && user.password) { if (user && user.password) {
login.userId = user.id; login.userId = user.id;
if (await passwords.verify(password, user.password)) { if (await passwords.verify(password, user.password)) {
debug('Password verified'); debug("Password verified");
login.successful = true; login.successful = true;
await login.save(); await login.save();
return user; return user;
} }
debug('Password invalid'); debug("Password invalid");
} }
await login.save(); await login.save();
return false; return false;
} }
export async function setupSession(user) { export async function setupSession(user) {
debug('Setting up session'); debug("Setting up session");
const token = uuid.v4(); const token = uuid.v4();
debug(user)
await this.redis.set(token, user.id, 'EX', 604800); await this.redis.set(token, user.id, "EX", 604800);
const sessionUser = { const sessionUser = {
id: user.id, id: user.id,
@ -76,27 +75,26 @@ export async function setupSession(user) {
joined: user.createdAt, joined: user.createdAt,
plan: user.plan, plan: user.plan,
uploadsToday: await models.file.count({ userId: user.id }), uploadsToday: await models.file.count({ userId: user.id }),
md5: crypto.createHash('md5').update(user.email).digest('hex'), md5: crypto.createHash("md5").update(user.email).digest("hex"),
token, token,
}; };
if (sessionUser.plan === 'Pro') { if (sessionUser.plan === "Pro") {
sessionUser.maxFileSize = 524288000; sessionUser.maxFileSize = 524288000;
sessionUser.dailyUploadAllowance = 'unlimited'; sessionUser.dailyUploadAllowance = "unlimited";
} }
this.session.user = sessionUser; this.session.user = sessionUser;
if (this.request.body.remember && this.request.body.remember === 'on') { if (this.request.body.remember && this.request.body.remember === "on") {
const remember = await models.remember.create({ const remember = await models.remember.create({
id: uuid(), id: uuid(),
userId: user.id, userId: user.id,
}); });
this.cookies.set('r', remember.id, { maxAge: 1209600000, httpOnly: true }); this.cookies.set("r", remember.id, { maxAge: 1209600000, httpOnly: true });
} }
debug('Session set up'); debug("Session set up");
} }
export async function signup(email, password, ip) { export async function signup(email, password, ip) {
const existingUser = await models.user.findOne({ const existingUser = await models.user.findOne({
where: { where: {
@ -105,26 +103,29 @@ export async function signup(email, password, ip) {
}, },
}); });
if (existingUser) { if (existingUser) {
debug('Email already in use.'); debug("Email already in use.");
throw new Error('Email already in use.'); throw new Error("Email already in use.");
} }
const cryptedPassword = await passwords.crypt(password); const cryptedPassword = await passwords.crypt(password);
const user = await models.user.create({ const user = await models.user.create(
{
email, email,
password: cryptedPassword, password: cryptedPassword,
ip, ip,
plan: 'Free', plan: "Free",
activation: { activation: {
id: uuid(), id: uuid(),
email, email,
}, },
}, { },
{
include: [models.activation], include: [models.activation],
}); },
);
await user.save(); await user.save();
const html = await render('email/inlined/activate', { const html = await render("email/inlined/activate", {
activationUrl: `${process.env.WEB_BASE_URL}/activate/${user.activation.id}`, activationUrl: `${process.env.WEB_BASE_URL}/activate/${user.activation.id}`,
}); });
const text = `Thanks for signing up to Hostr! const text = `Thanks for signing up to Hostr!
@ -136,18 +137,15 @@ ${process.env.WEB_BASE_URL}/activate/${user.activation.id}
`; `;
sendgrid.send({ sendgrid.send({
to: user.email, to: user.email,
subject: 'Welcome to Hostr', subject: "Welcome to Hostr",
from, from,
fromname, fromname,
html, html,
text, text,
categories: [ categories: ["activate"],
'activate',
],
}); });
} }
export async function sendResetToken(email) { export async function sendResetToken(email) {
const user = await models.user.findOne({ const user = await models.user.findOne({
where: { where: {
@ -159,7 +157,7 @@ export async function sendResetToken(email) {
id: uuid.v4(), id: uuid.v4(),
userId: user.id, userId: user.id,
}); });
const html = await render('email/inlined/forgot', { const html = await render("email/inlined/forgot", {
forgotUrl: `${process.env.WEB_BASE_URL}/forgot/${reset.id}`, forgotUrl: `${process.env.WEB_BASE_URL}/forgot/${reset.id}`,
}); });
const text = `It seems you've forgotten your password :( const text = `It seems you've forgotten your password :(
@ -167,38 +165,32 @@ Visit ${process.env.WEB_BASE_URL}/forgot/${reset.id} to set a new one.
`; `;
sendgrid.send({ sendgrid.send({
to: user.email, to: user.email,
from: 'jonathan@hostr.co', from: "jonathan@hostr.co",
fromname: 'Jonathan from Hostr', fromname: "Jonathan from Hostr",
subject: 'Hostr Password Reset', subject: "Hostr Password Reset",
html, html,
text, text,
categories: [ categories: ["password-reset"],
'password-reset',
],
}); });
} else { } else {
throw new Error('There was an error looking up your email address.'); throw new Error("There was an error looking up your email address.");
} }
} }
export async function fromToken(token) { export async function fromToken(token) {
const userId = await this.redis.get(token); const userId = await this.redis.get(token);
return models.user.findByPk(userId); return models.user.findByPk(userId);
} }
export async function fromCookie(rememberId) { export async function fromCookie(rememberId) {
const userId = await models.remember.findByPk(rememberId); const userId = await models.remember.findByPk(rememberId);
return models.user.findByPk(userId); return models.user.findByPk(userId);
} }
export async function validateResetToken(resetId) { export async function validateResetToken(resetId) {
return models.reset.findByPk(resetId); return models.reset.findByPk(resetId);
} }
export async function updatePassword(userId, password) { export async function updatePassword(userId, password) {
const cryptedPassword = await passwords.crypt(password); const cryptedPassword = await passwords.crypt(password);
const user = await models.user.findByPk(userId); const user = await models.user.findByPk(userId);
@ -206,7 +198,6 @@ export async function updatePassword(userId, password) {
await user.save(); await user.save();
} }
export async function activateUser(code) { export async function activateUser(code) {
const activation = await models.activation.findOne({ const activation = await models.activation.findOne({
where: { where: {