diff --git a/.envrc.example b/.envrc.example deleted file mode 100644 index ce12b2f..0000000 --- a/.envrc.example +++ /dev/null @@ -1,25 +0,0 @@ -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= diff --git a/.forgejo/workflows/.env b/.forgejo/workflows/.env deleted file mode 100644 index 0c2f98d..0000000 --- a/.forgejo/workflows/.env +++ /dev/null @@ -1,12 +0,0 @@ -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 diff --git a/.forgejo/workflows/build-image.yml b/.forgejo/workflows/build-image.yml new file mode 100644 index 0000000..82fa8f7 --- /dev/null +++ b/.forgejo/workflows/build-image.yml @@ -0,0 +1,40 @@ +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 }} diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index b1c0994..0000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,57 +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 Docker Hub - uses: https://cremin.dev/actions/docker-login@v3 - with: - registry: cremin.dev - username: ${{ github.actor }} - password: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} - - name: Check out repository - uses: https://cremin.dev/actions/checkout@v4 - - name: Set up Docker Buildx - uses: https://cremin.dev/actions/docker-setup-buildx@v3 - - name: Build and push - uses: https://cremin.dev/actions/docker-build-push@v6 - with: - file: ./Containerfile - context: ./ - tags: cremin.dev/jonathan/hostr:latest,cremin.dev/jonathan/hostr:${{ github.sha }} - push: true - test-image: - runs-on: node22 - needs: build-image - services: - database: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: hostr - POSTGRES_USER: hostr - POSTGRES_DB: hostr - redis: - image: redis:4.0.2-alpine - minio: - image: minio/minio - env: - MINIO_ACCESS_KEY: 7HYV3KPRGQ8Z5YCDNWC6 - MINIO_SECRET_KEY: 0kWP/ZkgIwQzgL9t4SGv9Uc93rO//OdyqMH329b/ - cmd: ["server", "/export"] - steps: - - name: Check out repository - uses: https://cremin.dev/actions/checkout@v4 - - name: Test image - run: | - docker run --env-file ./.forgejo/workflows/.env --rm -it cremin.dev/jonathan/hostr:${{ github.sha }} yarn test diff --git a/.gitignore b/.gitignore index 9bebce6..064a275 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.envrc +.env* .DS_Store .sass-cache/ node_modules @@ -7,7 +7,6 @@ jspm_packages npm-debug.log web/public/build web/public/styles/*.css -web/public/styles/*.css.map *.gz minigun*.json test*.json diff --git a/Makefile b/Makefile index 67a7951..65974ab 100644 --- a/Makefile +++ b/Makefile @@ -10,36 +10,36 @@ help: .PHONY: build build: ## Run `yarn run build` - podman compose run --rm app yarn run build + docker-compose run --rm app yarn run build .PHONY: test test: ## Run tests - podman compose run --rm app yarn test + docker-compose run --rm app yarn test .PHONY: logs logs: ## Tail the app and worker logs - podman compose logs -f app worker + docker-compose logs -f app worker .PHONY: migrate migrate: ## Migrate database schema - podman compose run --rm app yarn run initdb + docker-compose run --rm app yarn run initdb .PHONY: init init: ## Migrate database schema - podman compose run --rm app yarn run init + docker-compose run --rm app yarn run init .PHONY: watch-frontend watch-frontend: ## Build and watch for changes - podman compose run --rm app yarn run watch + docker-compose run --rm app yarn run watch -.PHONY: podman compose-up -podman compose-up: ## Start (and create) docker containers - podman compose up -d +.PHONY: docker-compose-up +docker-compose-up: ## Start (and create) docker containers + docker-compose up -d .PHONY: yarn yarn: ## Update yarn dependencies - podman compose run --rm app yarn + docker-compose run --rm app yarn .PHONY: shell shell: ## Run shell - podman compose run --rm app sh + docker-compose run --rm app sh diff --git a/docker-compose.yml b/docker-compose.yml index d36dff3..fe33cd8 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,7 +61,7 @@ services: - "3001:3000" command: yarn run worker database: - image: "postgres:14-alpine" + image: "postgres:10-alpine" ports: - "5432:5432" environment: @@ -69,7 +69,7 @@ services: POSTGRES_USER: "hostr" POSTGRES_DB: "hostr" redis: - image: "redis:8-alpine" + image: "redis:4.0.2-alpine" ports: - "6379:6379" minio: diff --git a/web/lib/auth.js b/web/lib/auth.js index bbba83c..45165b9 100644 --- a/web/lib/auth.js +++ b/web/lib/auth.js @@ -1,29 +1,29 @@ -import crypto from "crypto"; -import { join } from "path"; -import passwords from "passwords"; -import uuid from "node-uuid"; -import views from "co-views"; -import debugname from "debug"; -import sendgrid from "@sendgrid/mail"; -import models from "../../models"; +import crypto from 'crypto'; +import { join } from 'path'; +import passwords from 'passwords'; +import uuid from 'node-uuid'; +import views from 'co-views'; +import debugname from 'debug'; +import sendgrid from '@sendgrid/mail'; +import models from '../../models'; -const render = views(join(__dirname, "..", "views"), { default: "ejs" }); -const debug = debugname("hostr-web:auth"); +const render = views(join(__dirname, '..', 'views'), { default: 'ejs' }); +const debug = debugname('hostr-web:auth'); sendgrid.setApiKey(process.env.SENDGRID_KEY); const from = process.env.EMAIL_FROM; const fromname = process.env.EMAIL_NAME; 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) { - debug("No password, or password too short"); - return new Error("Invalid login details"); + debug('No password, or password too short'); + return new Error('Invalid login details'); } const count = await models.login.count({ where: { - ip: remoteIp.split(",")[0], + ip: remoteIp.split(',')[0], successful: false, createdAt: { $gt: Math.ceil(Date.now()) - 600000, @@ -32,8 +32,8 @@ export async function authenticate(email, password) { }); if (count > 25) { - debug("Throttling brute force"); - return new Error("Invalid login details"); + debug('Throttling brute force'); + return new Error('Invalid login details'); } const user = await models.user.findOne({ where: { @@ -43,29 +43,30 @@ export async function authenticate(email, password) { }); const login = await models.login.create({ - ip: remoteIp.split(",")[0], + ip: remoteIp.split(',')[0], successful: false, }); if (user && user.password) { login.userId = user.id; if (await passwords.verify(password, user.password)) { - debug("Password verified"); + debug('Password verified'); login.successful = true; await login.save(); return user; } - debug("Password invalid"); + debug('Password invalid'); } await login.save(); return false; } -export async function setupSession(user) { - debug("Setting up session"); - const token = uuid.v4(); - await this.redis.set(token, user.id, "EX", 604800); +export async function setupSession(user) { + debug('Setting up session'); + const token = uuid.v4(); + debug(user) + await this.redis.set(token, user.id, 'EX', 604800); const sessionUser = { id: user.id, @@ -75,26 +76,27 @@ export async function setupSession(user) { joined: user.createdAt, plan: user.plan, 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, }; - if (sessionUser.plan === "Pro") { + if (sessionUser.plan === 'Pro') { sessionUser.maxFileSize = 524288000; - sessionUser.dailyUploadAllowance = "unlimited"; + sessionUser.dailyUploadAllowance = 'unlimited'; } 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({ id: uuid(), 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) { const existingUser = await models.user.findOne({ where: { @@ -103,29 +105,26 @@ export async function signup(email, password, ip) { }, }); if (existingUser) { - debug("Email already in use."); - throw new Error("Email already in use."); + debug('Email already in use.'); + throw new Error('Email already in use.'); } const cryptedPassword = await passwords.crypt(password); - const user = await models.user.create( - { + const user = await models.user.create({ + email, + password: cryptedPassword, + ip, + plan: 'Free', + activation: { + id: uuid(), email, - password: cryptedPassword, - ip, - plan: "Free", - activation: { - id: uuid(), - email, - }, }, - { - include: [models.activation], - }, - ); + }, { + include: [models.activation], + }); 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}`, }); const text = `Thanks for signing up to Hostr! @@ -137,15 +136,18 @@ ${process.env.WEB_BASE_URL}/activate/${user.activation.id} `; sendgrid.send({ to: user.email, - subject: "Welcome to Hostr", + subject: 'Welcome to Hostr', from, fromname, html, text, - categories: ["activate"], + categories: [ + 'activate', + ], }); } + export async function sendResetToken(email) { const user = await models.user.findOne({ where: { @@ -157,7 +159,7 @@ export async function sendResetToken(email) { id: uuid.v4(), 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}`, }); const text = `It seems you've forgotten your password :( @@ -165,32 +167,38 @@ Visit ${process.env.WEB_BASE_URL}/forgot/${reset.id} to set a new one. `; sendgrid.send({ to: user.email, - from: "jonathan@hostr.co", - fromname: "Jonathan from Hostr", - subject: "Hostr Password Reset", + from: 'jonathan@hostr.co', + fromname: 'Jonathan from Hostr', + subject: 'Hostr Password Reset', html, text, - categories: ["password-reset"], + categories: [ + 'password-reset', + ], }); } 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) { const userId = await this.redis.get(token); return models.user.findByPk(userId); } + export async function fromCookie(rememberId) { const userId = await models.remember.findByPk(rememberId); return models.user.findByPk(userId); } + export async function validateResetToken(resetId) { return models.reset.findByPk(resetId); } + export async function updatePassword(userId, password) { const cryptedPassword = await passwords.crypt(password); const user = await models.user.findByPk(userId); @@ -198,6 +206,7 @@ export async function updatePassword(userId, password) { await user.save(); } + export async function activateUser(code) { const activation = await models.activation.findOne({ where: { diff --git a/yarn.lock b/yarn.lock index eb2f01c..d13c92b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1659,11 +1659,16 @@ busboy@^0.3.0: dependencies: dicer "0.3.0" -bytes@3.1.0, bytes@^3.0.0, bytes@^3.1.0: +bytes@3.1.0, bytes@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + cacache@^12.0.2: version "12.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" @@ -2097,7 +2102,7 @@ component-emitter@^1.2.0, component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== -compressible@^2.0.0: +compressible@^2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== @@ -2546,7 +2551,7 @@ depd@2.0.0, depd@^2.0.0, depd@~2.0.0: depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== des.js@^1.0.0: version "1.0.1" @@ -3987,7 +3992,7 @@ http-errors@1.7.3, http-errors@^1.6.3, http-errors@^1.7.3, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.8.0: +http-errors@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== @@ -4753,15 +4758,14 @@ koa-compose@^4.1.0: integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== koa-compress@~5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-5.1.0.tgz#7b9fe24f4c1b28d9cae90864597da472c2fcf701" - integrity sha512-G3Ppo9jrUwlchp6qdoRgQNMiGZtM0TAHkxRZQ7EoVvIG8E47J4nAsMJxXHAUQ+0oc7t0MDxSdONWTFcbzX7/Bg== + version "5.1.1" + resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-5.1.1.tgz#4f1599cfcaab23de1cd97d0a0ff9b5d05b0ffa52" + integrity sha512-UgMIN7ZoEP2DuoSQmD6CYvFSLt0NReGlc2qSY4bO4Oq0L56OiD9pDG41Kj/zFmVY/A3Wvmn4BqKcfq5H30LGIg== dependencies: - bytes "^3.0.0" - compressible "^2.0.0" - http-errors "^1.8.0" + bytes "^3.1.2" + compressible "^2.0.18" + http-errors "^1.8.1" koa-is-json "^1.0.0" - statuses "^2.0.1" koa-convert@^2.0.0: version "2.0.0" @@ -4814,7 +4818,7 @@ koa-helmet@^5.2.0: koa-is-json@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" - integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ= + integrity sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw== koa-logger@~3.2.1: version "3.2.1" @@ -5287,11 +5291,16 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +"mime-db@>= 1.43.0 < 2": + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.26, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" @@ -7466,12 +7475,7 @@ statsy@~0.2.0: "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0, statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -statuses@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== stream-browserify@^2.0.1: version "2.0.2"