diff --git a/sites/backend/.editorconfig b/sites/backend/.editorconfig deleted file mode 100644 index 9142239769f..00000000000 --- a/sites/backend/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# editorconfig.org -root = true - -[*] -indent_size = 2 -indent_style = space -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/sites/backend/Dockerfile b/sites/backend/Dockerfile deleted file mode 100644 index 602a76ae9b5..00000000000 --- a/sites/backend/Dockerfile +++ /dev/null @@ -1,56 +0,0 @@ -FROM keymetrics/pm2:latest-alpine - -# For documentation, see https://freesewing.dev/containers/backend - -# Extra build argument for when you're using a private NPM registry -ARG npm_registry - -# Environment variables -ENV http_proxy=$http_proxy \ - https_proxy=$https_proxy \ - no_proxy=$no_proxy \ - NPM_CONFIG_REGISTRY=$npm_registry \ - FS_BACKEND=$FS_BACKEND \ - FS_SITE=$FS_SITE \ - FS_MONGO_URI=$FS_MONGO_URI \ - FS_ENC_KEY=$FS_ENC_KEY \ - FS_JWT_ISSUER=$FS_JWT_ISSUER \ - FS_SMTP_HOST=$FS_SMTP_HOST \ - FS_SMTP_USER=$FS_SMTP_USER \ - FS_SMTP_PASS=$FS_SMTP_PASS \ - FS_GITHUB_CLIENT_ID=$FS_GITHUB_CLIENT_ID \ - FS_GITHUB_CLIENT_SECRET=$FS_GITHUB_CLIENT_SECRET \ - FS_GOOGLE_CLIENT_ID=$FS_GOOGLE_CLIENT_ID \ - FS_GOOGLE_CLIENT_SECRET=$FS_GOOGLE_CLIENT_SECRET \ - FS_STATIC=/storage/static \ - FS_STORAGE=/storage/api \ - NODE_ENV=production - -# Install OS dependencies (needed to compile sharp) -RUN apk add git python make g++ - -# Create storage structure -RUN mkdir -p /storage/static && mkdir /storage/api && mkdir -p /backend/src - -# Creat and set workdir -WORKDIR /backend - -# Add user to run the app -RUN addgroup -S freesewing \ - && adduser -S freesewing -G freesewing \ - && chown -R freesewing . - -# Copy source -COPY ./package.json . -COPY ./package-lock.json* . -COPY ./src ./src/ - -# Install Node.js dependencies (will also compile sharp) -RUN npm install && npm install -g backpack-core - -# Build code -RUN backpack build - -# Drop privleges and run app -USER freesewing -CMD pm2-runtime /backend/build/main.js diff --git a/sites/backend/LICENSE b/sites/backend/LICENSE deleted file mode 100644 index 7c49b11ae2d..00000000000 --- a/sites/backend/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Joost De Cock - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/sites/backend/README.md b/sites/backend/README.md index f6cde96df92..3edf11f2e2d 100644 --- a/sites/backend/README.md +++ b/sites/backend/README.md @@ -1,110 +1,6 @@ - +## FreeSewing backend -# FreeSewing / backend +This is a work in process to port the v2 backend to a new v3 backend. -This is the backend for [FreeSewing.org](https://freesewing.org/), our maker site. - -Our backend is a REST API built with [Express](https://expressjs.com/), -using [MongoDB](https://www.mongodb.com/) as our database. - -This API is required if you want to use your own instance -of [freesewing.org](https://github.com/freesewing/backend), -in which case you have two ways to do so: - -## Run with docker - -### Using docker-compose - -You can use [docker-compose](https://docs.docker.com/compose/) to spin up both the backend -API and a mongo instance. Clone this repository, create a `.env` file (See [Configuration](#configuration)), and then run: - -``` -docker-compose up -``` - -Your backend will now be available at http://localhost:3000 - -### Using our docker image and your own database - -If you just want the backend and provide your own mongo instance, -you can run [our docker image](https://hub.docker.com/r/freesewing/backend) directly -from the internet: - -``` -docker run --env-file .env --name fs_backend -d -p 3000:3000 freesewing/backend -``` - -Your backend will now be available at http://localhost:3000 - -## Run from source - -To run the backend from source, you'll need to clone this repository -and intall dependencies. - -``` -git clone git@github.com:freesewing/backend -cd backend -npm install -npm install --global backpack-core -``` - -> Note that we're installing [backpack-core](https://www.npmjs.com/package/backpack-core) globally for ease-of-use - -While developing, you can run: - -``` -npm run develop -``` - -And backpack will compile the backend, and spin it up. -It will also watch for changes and re-compile every time. Handy! - -If you want to run this in production, you should build the code: - -``` -npm run build -``` - -Then use something like [PM2](http://pm2.keymetrics.io/) to run it and keep it running. - -## Configuration - -This backend can be configured with environment variables. They are detailed below. - -> **Note:** -> -> If you're using docker (or docker-compose) you can use an environment file (See [example.env](example.env)). -> -> If you're running from source, you need to set these manually, or via a script. - - -| Variable | Example | Description | -|---------------|-------------|-----------------| -| `FS_BACKEND` | `http://localhost:3000` | URL on which the backend is hosted | -| `FS_SITE` | `http://localhost:8000` | URL on which the frontend is hosted | -| `FS_MONGO_URI` | `mongodb://mongo/freesewing` | URL for the Mongo database | -| `FS_ENC_KEY` | `someLongAndComplexString` | Secret used for encryption of data at rest | -| `FS_JWT_ISSUER` | `freesewing.org` | The JSON Web Token issuer | -| `FS_SMTP_HOST` | `smtp.google.com` | SMTP relay through which to send outgoing emails | -| `FS_SMTP_USER` | `your.username@gmail.com` | SMTP relay username| -| `FS_SMTP_PASS` | `yourPasswordHere` | SMTP relay password| -| `FS_GITHUB_CLIENT_ID` | `clientIdForOathViaGithub` | Github client ID for signup/login via GitHub | -| `FS_GITHUB_CLIENT_SECRET` | `clientSecretForOathViaGithub` | Github client ID for signup/login via GitHub | -| `FS_GOOGLE_CLIENT_ID` | `clientIdForOathViaGoogle` | Google client ID for signup/login via Google | -| `FS_GOOGLE_CLIENT_SECRET` | `clientSecretForOathViaGoogle` | Google client ID for signup/login via Google | - - -## Links - - - - 💻 Maker site: [freesewing.org](https://freesewing.org) - - 👩💻 Developer site: [freesewing.dev](https://freesewing.dev) - - 💬 Chat/Support: [Gitter](https://gitter.im/freesewing/freesewing) - - 🐦 Twitter: [@freesewing_org](https://twitter.com/freesewing_org) - - 📷 Instagram: [@freesewing_org](https://instagram.com/freesewing_org) - -## License - -Copyright (c) 2019 Joost De Cock - Available under the MIT license. - -See the LICENSE file for more details. +It will be based on Express using Prisma with a SQLite database. +Watch this space. diff --git a/sites/backend/dev.sqlite b/sites/backend/dev.sqlite new file mode 100644 index 00000000000..5c194ad230b Binary files /dev/null and b/sites/backend/dev.sqlite differ diff --git a/sites/backend/docker-compose.yml b/sites/backend/docker-compose.yml deleted file mode 100644 index 225ca87addf..00000000000 --- a/sites/backend/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: '3' - -services: - fs_backend: - image: freesewing/backend:beta - env_file: - - .env - ports: - - "3000:3000" - depends_on: - - mongo - networks: - - public - - private - volumes: - - fs-backend-static:/storage/static - - fs-backend-api:/storage/api - - fs-backend-mongo:/data/db - dns_search: . - -### External Services - - mongo: - image: mongo:3.4 - env_file: - - .env - networks: - - private - volumes: - - fs-backend-mongo:/data/db - dns_search: . - -volumes: - fs-backend-static: - fs-backend-api: - fs-backend-mongo: - -networks: - public: - driver: bridge - private: - driver: bridge diff --git a/sites/backend/example.env b/sites/backend/example.env deleted file mode 100644 index 2ace3f400e7..00000000000 --- a/sites/backend/example.env +++ /dev/null @@ -1,35 +0,0 @@ -# Where to find things -FS_BACKEND=http://localhost:3000 -FS_STATIC=https://static.freesewing.org -FS_STORAGE=/tmp/backendstorage - -# Database -FS_MONGO_URI=mongodb://mongo:27017/freesewing - -# Secret to encrypt data in mongo -FS_ENC_KEY=longRandomStringHere - -# Strapi -FS_STRAPI_HOST=posts.freesewing.org -FS_STRAPI_PROTOCOL=https -FS_STRAPI_PORT=443 -FS_STRAPI_USERNAME=REPLACEME -FS_STRAPI_PASSWORD=REPLACEME -FS_STRAPI_TMP=/fs/ramdisk - -# SMTP (email) -FS_SMTP_USER=smtpRelayUsername -FS_SMTP_PASS=smtpRelayPassword -FS_SMTP_HOST=smtp.relay.somedomain.com - -# Github -FS_GITHUB_TOKEN=githubTokenHere - -# Oauth -FS_GITHUB_CLIENT_ID=githubClientIdHere -FS_GITHUB_CLIENT_SECRET=githubClientSecretHere -FS_GOOGLE_CLIENT_ID=googleClientIdHere -FS_GOOGLE_CLIENT_SECRET=googleClientSecretHere - -# The JSON Web Token issuer -FS_JWT_ISSUER=freesewing.org diff --git a/sites/backend/nodemon.json b/sites/backend/nodemon.json new file mode 100644 index 00000000000..04f0edbc23c --- /dev/null +++ b/sites/backend/nodemon.json @@ -0,0 +1,4 @@ +{ + "verbose": true, + "ignore": ["tests/**.test.mjs"] +} diff --git a/sites/backend/package.json b/sites/backend/package.json index a5bef444454..73c2171fd14 100644 --- a/sites/backend/package.json +++ b/sites/backend/package.json @@ -1,94 +1,24 @@ { - "name": "@freesewing/backend", - "version": "3.0.0-alpha.2", - "description": "The freesewing.org backend", - "private": true, - "main": "build/main.js", - "module": "build/main.mjs", - "scripts": { - "cli": "babel-node --presets '@babel/preset-env' src/cli/index.js --help", - "clear:users": "babel-node --presets '@babel/preset-env' src/cli/index.js --clearUsers", - "clear:models": "babel-node --presets '@babel/preset-env' src/cli/index.js --clearModels", - "clear:patterns": "babel-node --presets '@babel/preset-env' src/cli/index.js --clearPatterns", - "clear:confirmations": "babel-node --presets '@babel/preset-env' src/cli/index.js --clearConfirmations", - "clear:all": "babel-node --presets '@babel/preset-env' src/cli/index.js --clearAll", - "clear:reboot": "babel-node --presets '@babel/preset-env' src/cli/index.js --reboot", - "precommit": "npm run pretty && lint-staged", - "patch": "npm version patch -m ':bookmark: v%s' && npm run build", - "minor": "npm version minor -m ':bookmark: v%s' && npm run build", - "major": "npm version major -m ':bookmark: v%s' && npm run build", - "test": "SEND_TEST_EMAILS=0 ./node_modules/.bin/mocha tests/index.js", - "testall": "SEND_TEST_EMAILS=1 ./node_modules/.bin/mocha tests/index.js", - "testci_IGNORE": "babel-node --presets '@babel/preset-env' scripts/testci.js", - "clean": "rimraf dist", - "prettier": "npx prettier --write 'src/**' 'tests/**'", - "develop": "backpack", - "build": "backpack build", - "start:prod": "backpack build && pm2 start build/main.js --name freesewing-backend", - "start:canary": "backpack build && pm2 start build/main.js --name canary-backend", - "upgrade:freesewing": "git checkout main && npm run build && pm2 stop freesewing-backend && pm2 delete freesewing-backend && pm2 start build/main.js --name freesewing-backend", - "upgrade:next": "git checkout develop && npm run build && pm2 stop canary-backend && pm2 delete canary-backend && pm2 start build/main.js --name canary-backend" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/freesewing/freesewing.git" - }, - "author": "Joost De Cock", + "name": "backend.freesewing.org", + "version": "0.0.1", "license": "MIT", - "bugs": { - "url": "https://github.com/freesewing/freesewing/issues" - }, - "homepage": "https://github.com/freesewing/freesewing#readme", - "prettier": "@freesewing/prettier-config", - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "*.{js,json}": [ - "prettier --write", - "git add" - ] + "scripts": { + "dev": "nodemon src/index.mjs", + "test": "npx mocha tests/*.test.mjs", + "initdb": "npx prisma db push", + "newdb": "node ./scripts/newdb.mjs", + "rmdb": "node ./scripts/rmdb.mjs" }, "dependencies": { - "@freesewing/i18n": "^3.0.0-alpha.2", - "axios": "1.1.3", - "body-parser": "1.20.1", - "chai": "^4.3.4", - "chai-http": "^4.3.0", - "chalk": "2.4.1", - "command-line-args": "^5.1.1", - "cors": "2.8.5", - "data-uri-to-buffer": "^3.0.1", - "data-uri-to-file": "^0.1.8", - "dateformat": "4.6.3", - "express": "4.18.2", - "form-data": "^4.0.0", - "formidable": "2.0.1", - "jsonwebtoken": "8.5.1", - "jszip": "3.10.1", - "mdast-util-to-string": "2", - "mocha": "^10.1.0", - "mongodb-memory-server": "^8.3.0", - "mongoose": "^6.1.8", - "mongoose-bcrypt": "^1.8.1", - "mongoose-encryption": "^2.1.0", - "nodemailer": "6.8.0", - "passport": "0.6.0", - "passport-jwt": "4.0.0", - "query-string": "7.1.1", - "remark": "14", - "remark-frontmatter": "3", - "remark-parse": "^9.0.0", - "remark-plain-text": "^0.2.0", - "rimraf": "2.6.2", - "sharp": "^0.31.0", - "tlds": "^1.221.1", - "yaml": "^2.1.1" + "@prisma/client": "4.5.0", + "crypto": "^1.0.1", + "express": "4.18.2" }, "devDependencies": { - "backpack-core": "0.8.4", - "prettier": "^2.3.1" + "mocha": "^10.1.0", + "prisma": "4.5.0" + }, + "prisma": { + "seed": "node prisma/seed.mjs" } } diff --git a/sites/backend/prisma/schema.prisma b/sites/backend/prisma/schema.prisma new file mode 100644 index 00000000000..e49b1035118 --- /dev/null +++ b/sites/backend/prisma/schema.prisma @@ -0,0 +1,81 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("API_DB_URL") +} + +model Confirmation { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + data String + type Int + updatedAt DateTime @updatedAt +} + +model NewsletterSubscriber { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + data String + ehash String @unique + email String + updatedAt DateTime @updatedAt +} + +model User { + id Int @id @default(autoincrement()) + bio String @default("") + consent Int @default(0) + data String @default("{}") + ehash String @unique + email String + ihash String + initial String + lastlogin DateTime + newsletter Boolean @default(false) + password String + patron Int @default(0) + people Person[] + patterns Pattern[] + picture String + role String @default("user") + status Int @default(0) + units Boolean @default(false) + username String @unique + + @@index([ihash]) +} + +model Pattern { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + data String? + design String? + name String + notes String? + person Person? @relation(fields: [personId], references: [id]) + personId Int? + user User @relation(fields: [userId], references: [id]) + userId Int + updatedAt DateTime @updatedAt + + @@index([userId, personId]) +} + +model Person { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + data String? + notes String? + user User @relation(fields: [userId], references: [id]) + userId Int + name String + picture String + units Boolean @default(false) + measies String @default("{}") + Pattern Pattern[] + + @@index([userId]) +} diff --git a/sites/backend/prisma/schema.sqlite b/sites/backend/prisma/schema.sqlite new file mode 100644 index 00000000000..c710b8099cd Binary files /dev/null and b/sites/backend/prisma/schema.sqlite differ diff --git a/sites/backend/prisma/seed.mjs b/sites/backend/prisma/seed.mjs new file mode 100644 index 00000000000..8814b41db0f --- /dev/null +++ b/sites/backend/prisma/seed.mjs @@ -0,0 +1,27 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +const userData = [ +] + +async function main() { + console.log(`Start seeding ...`) + for (const u of userData) { + const user = await prisma.user.create({ + data: u, + }) + console.log(`Created user with id: ${user.id}`) + } + console.log(`Seeding finished.`) +} + +main() + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/sites/backend/public/avatar.svg b/sites/backend/public/avatar.svg new file mode 100644 index 00000000000..edbe95b3167 --- /dev/null +++ b/sites/backend/public/avatar.svg @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/sites/backend/public/css/style.css b/sites/backend/public/css/style.css new file mode 100644 index 00000000000..1b43ad808ea --- /dev/null +++ b/sites/backend/public/css/style.css @@ -0,0 +1,44 @@ +body { + margin: 0; + padding: 0; + font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif; + color: #fafafa; + line-height: 1.25; + font-size: 24px; + background: #171717; +} +div.wrapper { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + height: calc(100vh - 8rem); + padding: 4rem 2rem; + text-align: center; +} +div.msg { + max-width: 60ch; +} +h1 { + font-size: 3rem; + font-weight: 700; + margin: 0 0 2rem; +} +h2 { + font-size: 2rem; + font-weight: 700; + margin: 0 0 1rem; + line-height: 1.25; +} +p { + line-height: 1.5; +} +img { + max-width: 166px; +} +a, +a:visited, +a:active { + color: #d0bfff !important; + text-decoration: none; +} diff --git a/sites/backend/run-mongo.sh b/sites/backend/run-mongo.sh deleted file mode 100755 index 23c093393bd..00000000000 --- a/sites/backend/run-mongo.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -docker run -d --name mongo -p 27017:27017 mongo diff --git a/sites/backend/scripts/newdb.mjs b/sites/backend/scripts/newdb.mjs new file mode 100644 index 00000000000..2e453d8e90d --- /dev/null +++ b/sites/backend/scripts/newdb.mjs @@ -0,0 +1,34 @@ +import path from 'path' +import fs from 'fs' +import chalk from 'chalk' +import { banner } from '../../../scripts/banner.mjs' +import dotenv from 'dotenv' +dotenv.config() + +const newDb = () => { + // Say hi + console.log(banner + '\n') + const db = process.env.API_DB_URL.slice(6) + console.log(db) + const schema = path.resolve('./prisma/schema.sqlite') + try { + if (fs.existsSync(db)) { + console.log(` ⛔ Database detected - Not proceeding`) + console.log(` If you want to create a new database, remove this file: ${chalk.cyan(db)}`) + } + else { + console.log(` 🚨 Going to create a database at ${chalk.cyan(db)}`) + fs.copyFile(schema, db, err => { + if (err) console.log(` ⚠️ ${chalk.red(err)}: Unable to create database file`, err) + else { + console.log(` ${chalk.green('Database created')}`) + } + }) + } + } catch(err) { + console.log(` ERROR: Unable to detect database file at ${db}`, err) + } +} + +newDb() + diff --git a/sites/backend/scripts/prebuild.mjs b/sites/backend/scripts/prebuild.mjs new file mode 100644 index 00000000000..7d875def645 --- /dev/null +++ b/sites/backend/scripts/prebuild.mjs @@ -0,0 +1,29 @@ +import yaml from 'js-yaml' +import i18nConfig from '../next-i18next.config.js' +import fs from 'fs' +import path from 'path' + +// This will load YAML translation files and store them as JSON +const generateTranslationFiles = async () => { + const promises = [] + for (const locale of i18nConfig.i18n.locales) { + for (const namespace of i18nConfig.namespaces) { + const content = yaml.load( + fs.readFileSync(path.resolve(path.join('locales', locale, namespace + '.yaml')), 'utf-8') + ) + console.log(`Generating ${locale}/${namespace}.json translation file`) + fs.writeFileSync( + path.resolve(path.join('public', 'locales', locale, namespace + '.json')), + JSON.stringify(content) + ) + } + } +} + +// Wrapper method +const prebuild = async () => { + await generateTranslationFiles() +} + +// Get to work +prebuild() diff --git a/sites/backend/scripts/resetrootpassword.mjs b/sites/backend/scripts/resetrootpassword.mjs new file mode 100644 index 00000000000..87236ea1def --- /dev/null +++ b/sites/backend/scripts/resetrootpassword.mjs @@ -0,0 +1,43 @@ +import config from '../vahi.config.mjs' +import sqlite3 from 'sqlite3' +import path from 'path' +import { generatePassword } from '../api/utils.mjs' +import { vascii } from '../api/utils.mjs' +import kleur from 'kleur' + +const reset = () => { + // Connect to database + const db = new sqlite3.Database(path.resolve(config.db.path)) + console.log(` + ${kleur.green(vascii)} + 🔓 Resetting root admin password... + `) + // Check if database exists + let exists = false + db.exec('SELECT * FROM Admin;', err => { + if (err) console.log(` + VaHI: WARNING - Database does not exist. + You can create it by running: npm run initdb + ( or: yarn initdb ) + `) + else { + // Update admin user + const [pwd, hash, salt] = generatePassword() + db.exec(`UPDATE Admin SET password = "${hash}:${salt}" WHERE email = "${config.root.email}";`, err => { + if (err) console.log(`WARNING: Failed to update root admin user password. The error was:`, err) + else console.log(` + You can now login with the root admin account: + + username: ${kleur.yellow(config.root.email)} + password: ${kleur.yellow(pwd)} + + Please write this password down. + You can restore it by running ${kleur.cyan('npm run resetrootpassword')} + `) + }) + } + }) +} + +reset() + diff --git a/sites/backend/scripts/rmdb.mjs b/sites/backend/scripts/rmdb.mjs new file mode 100644 index 00000000000..e799e20709c --- /dev/null +++ b/sites/backend/scripts/rmdb.mjs @@ -0,0 +1,47 @@ +import path from 'path' +import fs from 'fs' +import prompts from 'prompts' +import chalk from 'chalk' +import { banner } from '../../../scripts/banner.mjs' +import dotenv from 'dotenv' +dotenv.config() + + +const rmdb = async () => { + // Say hi + console.log(banner + '\n') + + console.log(` + 🚨 This will ${chalk.yellow('remove your database')} + ⚠️ There is ${chalk.bold('no way back')} from this - proceed with caution + `) + + const answer = await prompts([{ + type: 'confirm', + name: 'confirms', + message: 'Are you sure you want to completely remove your FreeSewing database?', + initial: false + }]) + + if (answer.confirms) { + console.log() + // Nuke it from orbit + const db = process.env.API_DB_URL.slice(6) + fs.access(db, fs.constants.W_OK, err => { + if (err) console.log(` ⛔ Cannot remove ${chalk.green(db)} 🤔`) + else { + fs.unlinkSync(db) + console.log(` 🔥 Removed ${chalk.red(db)} 😬`) + } + console.log() + }) + } else { + console.log() + console.log(chalk.green(' 😅 Not removing database - Phew')) + console.log() + } +} + +rmdb() + + diff --git a/sites/backend/scripts/secret.mjs b/sites/backend/scripts/secret.mjs new file mode 100644 index 00000000000..9002812f7a5 --- /dev/null +++ b/sites/backend/scripts/secret.mjs @@ -0,0 +1,25 @@ +import { randomString } from '../api/utils.mjs' +import kleur from 'kleur' +import { vascii } from '../api/utils.mjs' + +const genKey = async () => { + const secret = randomString(32) + console.log(` + ${kleur.green(vascii)} + 🔐 Here's a random secret to encrypt the JSON Web Tokens: + + ${kleur.cyan(secret)} + + + To use this secret, you can set ${kleur.yellow('config.jwt.secret')} explicitly in ${kleur.green('vahi.config.mjs')} + Or set the ${kleur.yellow('VAHI_SECRET')} environment variable in a ${kleur.green('.env')} file like this: + + ${kleur.yellow('VAHI_SECRET="'+secret+'"')} + + 🗒️ Check the documentation for more details + `) + + return +} + +genKey() diff --git a/sites/backend/scripts/testci.js b/sites/backend/scripts/testci.js deleted file mode 100644 index 582e8ef06dc..00000000000 --- a/sites/backend/scripts/testci.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Starts an in-memory database and a server before running tests. - */ - -import '../tests/env.js'; -import mongoose from 'mongoose'; -import { MongoMemoryServer } from 'mongodb-memory-server'; -import { loadSampleData, runTasks } from '../src/cli/lib'; -import { startApp } from '../src/app'; -import { spawn } from 'child_process'; - -(async () => { - return MongoMemoryServer.create({ instance: { port: 27017 } }); -})() - .then((mongoServer) => { - mongoose.connect(mongoServer.getUri() + "freesewing", { useNewUrlParser: true }); - }) - .then(() => { runTasks({ reboot: true }) }) - .then(loadSampleData) - .then(startApp) - .then(() => { - // Forward command-line args to test process. - const args = ['run', 'test', '--', '--reporter', '../../tests/reporters/terse.js'].concat(process.argv.slice(2)); - spawn('npm', args, { stdio: 'inherit' }) - .on('exit', function(code) { - // Propagate exit code so that test failures are recognized. - process.exit(code); - }); - }); - diff --git a/sites/backend/src/app.js b/sites/backend/src/app.js deleted file mode 100644 index c852c7a691c..00000000000 --- a/sites/backend/src/app.js +++ /dev/null @@ -1,59 +0,0 @@ -import './env' -import express from 'express' -import mongoose from 'mongoose' -import config from './config/index' -import chalk from 'chalk' -import passport from 'passport' -import verifyConfig from './config/verify' -import expressMiddleware from './middleware/express' -import passportMiddleware from './middleware/passport' -import routes from './routes' -import path from 'path' -import fs from 'fs' - -export const connectToDb = () => { - // Connecting to the database - mongoose.Promise = global.Promise - mongoose - .connect(config.db.uri, { - useNewUrlParser: true, - }) - .then(() => { - console.log(chalk.green('Successfully connected to the database')) - }) - .catch((err) => { - console.log(chalk.red('Could not connect to the database. Exiting now...'), err) - process.exit() - }) -} - -export const startApp = () => { - // Verify configuration - verifyConfig(config, chalk) - - // Start Express - const app = express() - - // Load Express middleware - for (let type of Object.keys(expressMiddleware)) expressMiddleware[type](app) - - // Load Passport middleware - for (let type of Object.keys(passportMiddleware)) passportMiddleware[type](passport) - - // Load routes - for (let type of Object.keys(routes)) routes[type](app, passport) - - // Catch-all route (Load index.html once instead of at every request) - const index = fs.readFileSync(path.resolve(__dirname, 'landing', 'index.html')) - app.get('/', async (req, res) => res.set('Content-Type', 'text/html').status(200).send(index)) - - const port = process.env.PORT || 3000 - - app.listen(port, (err) => { - if (err) console.error(chalk.red('Error occured'), err) - if (process.env.NODE_ENV === 'development') console.log(chalk.yellow('> in development')) - console.log(chalk.green(`> listening on port ${port}`)) - }) - - return app -} diff --git a/sites/backend/src/cli/data.js b/sites/backend/src/cli/data.js deleted file mode 100644 index b0aa71e102b..00000000000 --- a/sites/backend/src/cli/data.js +++ /dev/null @@ -1,120 +0,0 @@ -import { withBreasts, withoutBreasts } from '@freesewing/models' - -export default { - users: [ - { - email: 'test1@freesewing.org', - username: 'test1_user', - handle: 'tusr1', - password: 'test1', - role: 'user', - settings: { - language: 'nl', - units: 'imperial', - }, - patron: 2, - consent: { - profile: true, - measurements: true, - openData: true, - }, - status: 'active', - }, - { - email: 'test@freesewing.org', - username: 'test_user', - handle: 'tuser', - password: 'test', - role: 'user', - settings: { - language: 'nl', - units: 'imperial', - }, - patron: 4, - consent: { - profile: true, - measurements: true, - openData: true, - }, - status: 'active', - }, - { - email: 'admin@freesewing.org', - username: 'admin', - password: 'admin', - role: 'admin', - handle: 'admin', - social: { - github: 'freesewing-bot', - twitter: 'freesewing_org', - instagram: 'freesewing_org', - }, - patron: 8, - settings: { - language: 'en', - units: 'metric', - }, - consent: { - profile: true, - measurements: true, - openData: true, - }, - newsletter: true, - status: 'active', - }, - ], - people: [ - { - handle: 'persa', - picture: 'persa.svg', - user: 'tuser', - name: 'Example person - No breasts', - breasts: false, - units: 'metric', - notes: 'This is an example person', - measurements: withoutBreasts.size42, - }, - { - handle: 'persb', - picture: 'persb.svg', - user: 'tuser', - name: 'Example person - With breasts', - breasts: true, - units: 'metric', - notes: 'This is an example person', - measurements: { - ...withBreasts.size36, - doesNotExist: 234, - }, - }, - ], - patterns: [ - { - handle: 'recip', - name: 'Example pattern', - notes: 'These are the pattern notes', - data: { - settings: { - sa: 10, - complete: true, - paperless: false, - units: 'imperial', - measurements: { - biceps: 335, - hpsToWaist: 520, - chest: 1080, - waistToHips: 145, - neck: 420, - shoulderSlope: 13, - shoulderToShoulder: 465, - hips: 990, - }, - }, - design: 'aaron', - }, - created: '2019-08-14T09:47:27.163Z', - user: 'tuser', - person: 'persa', - }, - ], -} diff --git a/sites/backend/src/cli/index.js b/sites/backend/src/cli/index.js deleted file mode 100644 index b5eceedbc50..00000000000 --- a/sites/backend/src/cli/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import '../env' -import mongoose from 'mongoose' -import config from '../config/index' -import chalk from 'chalk' -import verifyConfig from '../config/verify' -import optionDefinitions from './options' -import commandLineArgs from 'command-line-args' -import { showHelp, loadSampleData, runTasks } from './lib' - -const options = commandLineArgs(optionDefinitions) -if (options.help) { - showHelp() - process.exit() -} - -// Verify configuration -verifyConfig(config, chalk) - -// Connecting to the database -mongoose.Promise = global.Promise -mongoose - .connect(config.db.uri, { - useNewUrlParser: true, - }) - .then(() => { - console.log(chalk.green('Successfully connected to the database')) - console.log() - runTasks(options).then(() => { - if (options.reboot) { - loadSampleData().then(() => { - console.log('⚡ Data loaded') - process.exit() - }) - } else { - console.log() - process.exit() - } - }) - }) - .catch((err) => { - console.log(chalk.red('Could not connect to the database. Exiting now...'), err) - process.exit() - }) diff --git a/sites/backend/src/cli/lib.js b/sites/backend/src/cli/lib.js deleted file mode 100644 index 67d21da982d..00000000000 --- a/sites/backend/src/cli/lib.js +++ /dev/null @@ -1,101 +0,0 @@ -import chalk from 'chalk' -import { User, Person, Pattern, Confirmation, Newsletter } from '../models' -import { ehash } from '../utils' -import data from './data' - -export const showHelp = () => { - console.log() - console.log(chalk.yellow('Use one of the following:')) - console.log() - console.log(' ', chalk.bold.blue('npm run clear:users'), '👉 Truncate the users collection') - console.log(' ', chalk.bold.blue('npm run clear:people'), '👉 Truncate the people collection') - console.log( - ' ', - chalk.bold.blue('npm run clear:patterns'), - '👉 Truncate the patterns collection' - ) - console.log( - ' ', - chalk.bold.blue('npm run clear:confirmations'), - '👉 Truncate the confirmations collection' - ) - console.log(' ', chalk.bold.blue('npm run clear:all'), '👉 Empty the entire database') - console.log( - ' ', - chalk.bold.blue('npm run clear:reboot'), - '👉 Empty database, then load sample data' - ) - console.log() - process.exit() -} - -export const clearUsers = async () => { - await User.deleteMany().then((result) => { - if (result.ok) console.log('🔥 Users deleted') - else console.log('🚨 Could not remove users', result) - }) -} -export const clearPeople = async () => { - await Person.deleteMany().then((result) => { - if (result.ok) console.log('🔥 People removed') - else console.log('🚨 Could not remove people', result) - }) -} -export const clearPatterns = async () => { - await Pattern.deleteMany().then((result) => { - if (result.ok) console.log('🔥 Patterns deleted') - else console.log('🚨 Could not remove patterns', result) - }) -} -export const clearConfirmations = async () => { - await Confirmation.deleteMany().then((result) => { - if (result.ok) console.log('🔥 Confirmations deleted') - else console.log('🚨 Could not remove confirmations', result) - }) -} -export const clearNewsletterSubscribers = async () => { - await Newsletter.deleteMany().then((result) => { - if (result.ok) console.log('🔥 Newsletter subscriptions deleted') - else console.log('🚨 Could not remove newsletter subscriptions', result) - }) -} - -export const loadSampleData = async () => { - let promises = [] - for (let sample of data.users) { - let user = new User({ - ...sample, - initial: sample.email, - ehash: ehash(sample.email), - picture: sample.handle + '.svg', - time: { - created: new Date(), - }, - }) - user.createAvatar() - promises.push(user.save()) - } - for (let sample of data.people) { - let person = new Person(sample) - person.createAvatar() - promises.push(person.save()) - } - for (let sample of data.patterns) { - promises.push(new Pattern(sample).save()) - } - - return Promise.all(promises) -} - -export const runTasks = (options) => { - let promises = [] - if (options.clearAll || options.reboot || options.clearUsers) promises.push(clearUsers()) - if (options.clearAll || options.reboot || options.clearPeople) promises.push(clearPeople()) - if (options.clearAll || options.reboot || options.clearPatterns) promises.push(clearPatterns()) - if (options.clearAll || options.reboot || options.clearConfirmations) - promises.push(clearConfirmations()) - if (options.clearAll || options.reboot || options.clearNewsletterSubscriptions) - promises.push(clearNewsletterSubscribers()) - - return Promise.all(promises) -} diff --git a/sites/backend/src/cli/options.js b/sites/backend/src/cli/options.js deleted file mode 100644 index 8e5bbcde0b7..00000000000 --- a/sites/backend/src/cli/options.js +++ /dev/null @@ -1,34 +0,0 @@ -export default [ - { - name: 'clearUsers', - type: Boolean, - }, - { - name: 'clearModels', - type: Boolean, - }, - { - name: 'clearPatterns', - type: Boolean, - }, - { - name: 'clearConfirmations', - type: Boolean, - }, - { - name: 'clearNewsletterSubscribers', - type: Boolean, - }, - { - name: 'clearAll', - type: Boolean, - }, - { - name: 'reboot', - type: Boolean, - }, - { - name: 'help', - type: Boolean, - }, -] diff --git a/sites/backend/src/config.mjs b/sites/backend/src/config.mjs new file mode 100644 index 00000000000..464a30d18aa --- /dev/null +++ b/sites/backend/src/config.mjs @@ -0,0 +1,181 @@ +import chalk from 'chalk' +// Load environment variables +import dotenv from 'dotenv' +dotenv.config() + +const port = process.env.API_PORT || 3000 +// Construct config object +const config = { + port, + api: process.env.API_URL || `http://localhost:${port}`, + website: { + domain: process.env.API_WEBSITE_DOMAIN || 'freesewing.org', + scheme: process.env.API_WEBSITE_SCHEME || 'https', + }, + db: { + url: process.env.API_DB_URL, + }, + static: process.env.API_STATIC, + storage: process.env.API_STORAGE, + hashing: { + saltRounds: 10, + }, + encryption: { + key: process.env.API_ENC_KEY, + }, + jwt: { + secretOrKey: process.env.API_ENC_KEY, + issuer: process.env.API_JWT_ISSUER, + audience: process.env.API_JWT_ISSUER, + expiresIn: process.env.API_JWT_EXPIRY || '36 days', + }, + languages: ['en', 'de', 'es', 'fr', 'nl'], + ses: { + }, + oauth: { + github: { + clientId: process.env.API_GITHUB_CLIENT_ID, + clientSecret: process.env.API_GITHUB_CLIENT_SECRET, + tokenUri: 'https://github.com/login/oauth/access_token', + dataUri: 'https://api.github.com/user', + emailUri: 'https://api.github.com/user/emails', + }, + google: { + clientId: process.env.API_GOOGLE_CLIENT_ID, + clientSecret: process.env.API_GOOGLE_CLIENT_SECRET, + tokenUri: 'https://oauth2.googleapis.com/token', + dataUri: + 'https://people.googleapis.com/v1/people/me?personFields=emailAddresses,names,photos', + }, + }, + github: { + token: process.env.API_GITHUB_TOKEN, + api: 'https://api.github.com', + bot: { + user: process.env.API_GITHUB_USER || 'freesewing-robot', + name: process.env.API_GITHUB_USER_NAME || 'Freesewing bot', + email: process.env.API_GITHUB_USER_EMAIL || 'bot@freesewing.org', + }, + notify: { + specific: { + albert: ['woutervdub'], + bee: ['bobgeorgethe3rd'], + benjamin: ['woutervdub'], + cornelius: ['woutervdub'], + diana: ['alfalyr'], + holmes: ['alfalyr'], + hortensia: ['woutervdub'], + lunetius: ['starfetch'], + penelope: ['woutervdub'], + tiberius: ['starfetch'], + sandy: ['alfalyr'], + ursula: ['nataliasayang'], + yuri: ['biou', 'hellgy'], + walburga: ['starfetch'], + waralee: ['woutervdub'], + }, + dflt: [process.env.API_GITHUB_NOTIFY_DEFAULT_USER || 'joostdecock'], + }, + }, +} + +/* + * This method is how you load the config. + * + * It will verify whether whether everyting is setup correctly + * which is not a given since there's a number of environment + * variables that need to be set for this backend to function. + */ +export function verifyConfig() { + const nonEmptyString = (input) => { + if (typeof input === 'string' && input.length > 0) return true + return false + } + const warnings = [] + const errors = [] + + /* + * Required (error when missing) + */ + + // Database + if (!nonEmptyString(config.db.url)) errors.push({ e: 'API_DB_URL', i: './dev.sqlite' }) + + // Encryption + //if (!nonEmptyString(config.encryption.key)) errors.push({ e: 'FS_ENC_KEY', i: 'encryption' }) + + /* + * Wanted (warning when missing) + */ + // API + //if (!nonEmptyString(config.api)) warnings.push({ e: 'FS_BACKEND', i: 'api' }) + + // Site + //if (!nonEmptyString(config.api)) warnings.push({ e: 'FS_SITE', i: 'site' }) + + // SMTP + //if (!nonEmptyString(config.smtp.host)) warnings.push({ e: 'FS_SMTP_HOST', i: 'smtp' }) + + //if (!nonEmptyString(config.smtp.user)) warnings.push({ e: 'FS_SMTP_USER', i: 'smtp' }) + + //if (!nonEmptyString(config.smtp.pass)) warnings.push({ e: 'FS_SMTP_PASS', i: 'smtp' }) + + // OAUTH + //if (!nonEmptyString(config.oauth.github.clientId)) + // warnings.push({ e: 'FS_GITHUB_CLIENT_ID', i: 'oauth' }) + + //if (!nonEmptyString(config.oauth.github.clientSecret)) + // warnings.push({ e: 'FS_GITHUB_CLIENT_SECRET', i: 'oauth' }) + + //if (!nonEmptyString(config.oauth.google.clientId)) + // warnings.push({ e: 'FS_GOOGLE_CLIENT_ID', i: 'oauth' }) + + //if (!nonEmptyString(config.oauth.google.clientSecret)) + // warnings.push({ e: 'FS_GOOGLE_CLIENT_SECRET', i: 'oauth' }) + + for (let { e, i } of warnings) { + console.log( + chalk.yellow('Warning:'), + 'Missing', + chalk.yellow(e), + "environment variable. Some features won't be available.", + '\n', + chalk.yellow('See: '), + chalk.yellow.bold('https://dev.freesewing.org/backend/configuration#' + i), + '\n' + ) + } + + for (let { e, i } of errors) { + console.log( + chalk.redBright('Error:'), + 'Required environment variable', + chalk.redBright(e), + "is missing. The backend won't start without it.", + '\n', + chalk.yellow('See: '), + chalk.yellow.bold('https://dev.freesewing.org/backend/configuration#' + i), + '\n' + ) + } + + if (errors.length > 0) { + console.log(chalk.redBright('Invalid configuration. Stopping here...')) + return process.exit(1) + } + + if (process.env.API_DUMP_CONFIG_AT_STARTUP) { + console.log("Dumping configuration:", JSON.stringify({ + ...config, + encryption: { + ...config.encryption, + key: config.encryption.key.slice(0, 4) + '[ REDACTED ]' + config.encryption.key.slice(-4), + }, + jwt: { + secretOrKey: config.jwt.secretOrKey.slice(0, 4) + '[ REDACTED ]' + config.jwt.secretOrKey.slice(-4), + } + }, null, 2)) + } + + return config +} diff --git a/sites/backend/src/config/index.js b/sites/backend/src/config/index.js deleted file mode 100644 index 065e47038cc..00000000000 --- a/sites/backend/src/config/index.js +++ /dev/null @@ -1,108 +0,0 @@ -// Load environment variables -import dotenv from 'dotenv' -dotenv.config() - -// Construct config object -const config = { - api: process.env.FS_BACKEND, - website: { - domain: 'freesewing.org', - scheme: 'https', - }, - static: process.env.FS_STATIC, - storage: process.env.FS_STORAGE, - avatar: { - sizes: { - l: 1000, - m: 500, - s: 250, - xs: 100, - }, - }, - db: { - uri: process.env.FS_MONGO_URI || 'mongodb://localhost/freesewing', - }, - hashing: { - saltRounds: 10, - }, - encryption: { - key: process.env.FS_ENC_KEY || '', // Prevent mongoose plugin from throwing an error - }, - jwt: { - secretOrKey: process.env.FS_ENC_KEY, - issuer: process.env.FS_JWT_ISSUER, - audience: process.env.FS_JWT_ISSUER, - expiresIn: '36 days', - }, - languages: ['en', 'de', 'es', 'fr', 'nl'], - smtp: { - host: process.env.FS_SMTP_HOST, - user: process.env.FS_SMTP_USER, - pass: process.env.FS_SMTP_PASS, - }, - oauth: { - github: { - clientId: process.env.FS_GITHUB_CLIENT_ID, - clientSecret: process.env.FS_GITHUB_CLIENT_SECRET, - tokenUri: 'https://github.com/login/oauth/access_token', - dataUri: 'https://api.github.com/user', - emailUri: 'https://api.github.com/user/emails', - }, - google: { - clientId: process.env.FS_GOOGLE_CLIENT_ID, - clientSecret: process.env.FS_GOOGLE_CLIENT_SECRET, - tokenUri: 'https://oauth2.googleapis.com/token', - dataUri: - 'https://people.googleapis.com/v1/people/me?personFields=emailAddresses,names,photos', - }, - }, - github: { - token: process.env.FS_GITHUB_TOKEN, - api: 'https://api.github.com', - bot: { - user: 'freesewing-robot', - name: 'Freesewing bot', - email: 'bot@freesewing.org', - }, - notify: { - specific: { - albert: ['woutervdub'], - bee: ['bobgeorgethe3rd'], - benjamin: ['woutervdub'], - cornelius: ['woutervdub'], - diana: ['alfalyr'], - holmes: ['alfalyr'], - hortensia: ['woutervdub'], - lunetius: ['starfetch'], - penelope: ['woutervdub'], - tiberius: ['starfetch'], - sandy: ['alfalyr'], - ursula: ['nataliasayang'], - yuri: ['biou', 'hellgy'], - walburga: ['starfetch'], - waralee: ['woutervdub'], - }, - dflt: ['joostdecock'], - }, - }, - strapi: { - protocol: process.env.FS_STRAPI_PROTOCOL, - host: process.env.FS_STRAPI_HOST, - port: process.env.FS_STRAPI_PORT, - username: process.env.FS_STRAPI_USERNAME, - password: process.env.FS_STRAPI_PASSWORD, - tmp: process.env.FS_STRAPI_TMP, - }, - og: { - template: ['..', '..', 'artwork', 'og', 'template.svg'], - chars: { - title_1: 18, - title_2: 19, - title_3: 20, - intro: 34, - sub: 42, - }, - }, -} - -export default config diff --git a/sites/backend/src/config/verify.js b/sites/backend/src/config/verify.js deleted file mode 100644 index 2195a988483..00000000000 --- a/sites/backend/src/config/verify.js +++ /dev/null @@ -1,79 +0,0 @@ -const verifyConfig = (config, chalk) => { - const nonEmptyString = (input) => { - if (typeof input === 'string' && input.length > 0) return true - return false - } - const warnings = [] - const errors = [] - - // Required (error when missing) - // - // Database - if (!nonEmptyString(config.db.uri)) errors.push({ e: 'FS_MONGO_URI', i: 'mongo' }) - - // Encryption - if (!nonEmptyString(config.encryption.key)) errors.push({ e: 'FS_ENC_KEY', i: 'encryption' }) - - // Wanted (warning when missing) - // - // API - if (!nonEmptyString(config.api)) warnings.push({ e: 'FS_BACKEND', i: 'api' }) - - // Site - if (!nonEmptyString(config.api)) warnings.push({ e: 'FS_SITE', i: 'site' }) - - // SMTP - if (!nonEmptyString(config.smtp.host)) warnings.push({ e: 'FS_SMTP_HOST', i: 'smtp' }) - - if (!nonEmptyString(config.smtp.user)) warnings.push({ e: 'FS_SMTP_USER', i: 'smtp' }) - - if (!nonEmptyString(config.smtp.pass)) warnings.push({ e: 'FS_SMTP_PASS', i: 'smtp' }) - - // OAUTH - if (!nonEmptyString(config.oauth.github.clientId)) - warnings.push({ e: 'FS_GITHUB_CLIENT_ID', i: 'oauth' }) - - if (!nonEmptyString(config.oauth.github.clientSecret)) - warnings.push({ e: 'FS_GITHUB_CLIENT_SECRET', i: 'oauth' }) - - if (!nonEmptyString(config.oauth.google.clientId)) - warnings.push({ e: 'FS_GOOGLE_CLIENT_ID', i: 'oauth' }) - - if (!nonEmptyString(config.oauth.google.clientSecret)) - warnings.push({ e: 'FS_GOOGLE_CLIENT_SECRET', i: 'oauth' }) - - for (let { e, i } of warnings) { - console.log( - chalk.yellow('Warning:'), - 'Missing', - chalk.yellow(e), - "environment variable. Some features won't be available.", - '\n', - chalk.yellow('See: '), - chalk.yellow.bold('https://dev.freesewing.org/backend/configuration#' + i), - '\n' - ) - } - - for (let { e, i } of errors) { - console.log( - chalk.redBright('Error:'), - 'Required environment variable', - chalk.redBright(e), - "is missing. The backend won't start without it.", - '\n', - chalk.yellow('See: '), - chalk.yellow.bold('https://dev.freesewing.org/backend/configuration#' + i), - '\n' - ) - } - - if (errors.length > 0) { - console.log(chalk.redBright('Invalid configuration. Stopping here...')) - return process.exit(1) - } - - return true -} - -export default verifyConfig diff --git a/sites/backend/src/controllers/user.js b/sites/backend/src/controllers/user.mjs similarity index 90% rename from sites/backend/src/controllers/user.js rename to sites/backend/src/controllers/user.mjs index be4de55816d..becc7765ffa 100644 --- a/sites/backend/src/controllers/user.js +++ b/sites/backend/src/controllers/user.mjs @@ -1,15 +1,126 @@ -import { User, Confirmation, Person, Pattern } from '../models' -import { log, email, ehash, newHandle, uniqueHandle } from '../utils' +//import { log, email, ehash, newHandle, uniqueHandle } from '../utils' import jwt from 'jsonwebtoken' -import config from '../config' -import path from 'path' -import fs from 'fs' +//import path from 'path' +//import fs from 'fs' import Zip from 'jszip' -import rimraf from 'rimraf' +//import rimraf from 'rimraf' +import { ehash } from '../utils/crypto.mjs' -function UserController() {} +export function UserController() {} -UserController.prototype.login = function (req, res) { +// Signup +UserController.prototype.signup = async (req, res, tools) => { + if (!req.body) return res.sendStatus(400) + if (!req.body.email) return res.status(400).send('emailMissing') + if (!req.body.password) return res.status(400).send('passwordMissing') + if (!req.body.language) return res.status(400).send('languageMissing') + + // Requests looks ok - does the user exist? + const ehash = ehash(req.body.email) + + // Destructure what we need from tools + const { prisma, config, encrypt } = tools + if ((await prisma.user.findUnique({ where: { ehash } }))) return res.status(400).send('emailExists') + + // It does not. Creating user entry + /* + const user = await.prisma.user.create({ + ehash, + email + + const handle = uniqueHandle() + let handle = uniqueHandle() + let username = 'temporary-username-' + time() + req.body.email, + let user = new User({ + email: req.body.email, + initial: req.body.email, + ehash: ehash(req.body.email), + handle, + username, + password: req.body.password, + settings: { language: req.body.language }, + status: 'pending', + picture: handle + '.svg', + time: { + created: new Date(), + }, + }) + ehash String @unique + email String + handle String @unique + ihash String + initial String + lastlogin DateTime + newsletter Boolean @default(false) + password String + patron Int @default(0) + people Person[] + patterns Pattern[] + picture String + role String @default("user") + status Int @default(0) + units Boolean @default(false) + username String @unique +*/ + return res.status(200).send(exists) + /* + (err, user) => { + if (err) return res.sendStatus(500) + if (user !== null) return res.status(400).send('userExists') + else { + let handle = uniqueHandle() + let username = 'user-' + handle + let user = new User({ + email: req.body.email, + initial: req.body.email, + ehash: ehash(req.body.email), + handle, + username, + password: req.body.password, + settings: { language: req.body.language }, + status: 'pending', + picture: handle + '.svg', + time: { + created: new Date(), + }, + }) + user.save(function (err) { + if (err) { + log.error('accountCreationFailed', user) + console.log(err) + return res.sendStatus(500) + } + log.info('accountCreated', { handle: user.handle }) + user.createAvatar(handle) + let confirmation = new Confirmation({ + type: 'signup', + data: { + language: req.body.language, + email: req.body.email, + password: req.body.password, + handle, + }, + }) + confirmation.save(function (err) { + if (err) return res.sendStatus(500) + log.info('signupRequest', { email: req.body.email, confirmation: confirmation._id }) + email.signup(req.body.email, req.body.language, confirmation._id) + return res.sendStatus(200) + }) + }) + } + } + ) + */ +} + +/* + + + + + +UserController.prototype.login = function (req, res, prisma, config) { if (!req.body || !req.body.username) return res.sendStatus(400) User.findOne( { @@ -256,65 +367,6 @@ UserController.prototype.isUsernameAvailable = (req, res) => { }) } -// // Signup flow -UserController.prototype.signup = (req, res) => { - if (!req.body) return res.sendStatus(400) - if (!req.body.email) return res.status(400).send('emailMissing') - if (!req.body.password) return res.status(400).send('passwordMissing') - if (!req.body.language) return res.status(400).send('languageMissing') - User.findOne( - { - ehash: ehash(req.body.email), - }, - (err, user) => { - if (err) return res.sendStatus(500) - if (user !== null) return res.status(400).send('userExists') - else { - let handle = uniqueHandle() - let username = 'user-' + handle - let user = new User({ - email: req.body.email, - initial: req.body.email, - ehash: ehash(req.body.email), - handle, - username, - password: req.body.password, - settings: { language: req.body.language }, - status: 'pending', - picture: handle + '.svg', - time: { - created: new Date(), - }, - }) - user.save(function (err) { - if (err) { - log.error('accountCreationFailed', user) - console.log(err) - return res.sendStatus(500) - } - log.info('accountCreated', { handle: user.handle }) - user.createAvatar(handle) - let confirmation = new Confirmation({ - type: 'signup', - data: { - language: req.body.language, - email: req.body.email, - password: req.body.password, - handle, - }, - }) - confirmation.save(function (err) { - if (err) return res.sendStatus(500) - log.info('signupRequest', { email: req.body.email, confirmation: confirmation._id }) - email.signup(req.body.email, req.body.language, confirmation._id) - return res.sendStatus(200) - }) - }) - } - } - ) -} - // // Re-send activation email UserController.prototype.resend = (req, res) => { if (!req.body) return res.sendStatus(400) @@ -476,7 +528,7 @@ const loadAvatar = async (user) => { else return false } -/** restrict processing of data, aka freeze account */ +// restrict processing of data, aka freeze account UserController.prototype.restrict = (req, res) => { if (!req.user._id) return res.sendStatus(400) User.findById(req.user._id, (err, user) => { @@ -492,7 +544,7 @@ UserController.prototype.restrict = (req, res) => { }) } -/** Remove account */ +// Remove account UserController.prototype.remove = (req, res) => { if (!req.user._id) return res.sendStatus(400) User.findById(req.user._id, (err, user) => { @@ -540,4 +592,4 @@ const createTempDir = () => { const uri = (path) => config.static + path.substring(config.storage.length) -export default UserController +*/ diff --git a/sites/backend/src/env.js b/sites/backend/src/env.js deleted file mode 100644 index ca5ac26a577..00000000000 --- a/sites/backend/src/env.js +++ /dev/null @@ -1,2 +0,0 @@ -import * as dotenv from 'dotenv' -dotenv.config() diff --git a/sites/backend/src/index.js b/sites/backend/src/index.js deleted file mode 100644 index 3721ac0eb86..00000000000 --- a/sites/backend/src/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import { connectToDb, startApp } from './app' - -connectToDb() - -const app = startApp() - -export default app diff --git a/sites/backend/src/index.mjs b/sites/backend/src/index.mjs new file mode 100644 index 00000000000..a4a36f73922 --- /dev/null +++ b/sites/backend/src/index.mjs @@ -0,0 +1,46 @@ +// Dependencies +import express from 'express' +import chalk from 'chalk' +import path from 'path' +import fs from 'fs' +import { PrismaClient } from '@prisma/client' +import passport from 'passport' +// Routes +import { routes } from './routes/index.mjs' +// Config +import { verifyConfig } from './config.mjs' +// Middleware +import { loadExpressMiddleware, loadPassportMiddleware } from './middleware.mjs' +// Encryption +import { ehash, encryption } from './utils/crypto.mjs' + +// Bootstrap +const config = verifyConfig() +const prisma = new PrismaClient() +const app = express() +app.use(express.json()) +app.use(express.static('public')) + +// Load middleware +loadExpressMiddleware(app) +loadPassportMiddleware(passport, config) + +const params = { + app, + passport, + ...encryption(config.encryption.key), + config +} +// Load routes +for (const type in routes) routes[type](params) + +// Catch-all route (Load index.html once instead of at every request) +const index = fs.readFileSync(path.resolve('.', 'src', 'landing', 'index.html')) +app.get('/', async (req, res) => res.set('Content-Type', 'text/html').status(200).send(index)) + +// Start listening for requests +app.listen(config.port, (err) => { + if (err) console.error(chalk.red('Error occured'), err) + if (process.env.NODE_ENV === 'development') console.log(chalk.yellow('> in development')) + console.log(chalk.green(`🚀 REST API ready, listening on ${config.api}`)) +}) diff --git a/sites/backend/src/landing/index.html b/sites/backend/src/landing/index.html index a313597a81c..b529eb88e16 100644 --- a/sites/backend/src/landing/index.html +++ b/sites/backend/src/landing/index.html @@ -2,59 +2,27 @@
-
- This is the FreeSewing backend.
-
- Try freesewing.org instead.
-
- For questions, join us at - discord.freesewing.org -
++ For info about FreeSewing, try freesewing.org instead. +
++ For info about this backend, refer to the FreeSewing backend refefence documentation. +
++ For questions, join us at + discord.freesewing.org +
+
-
|
-
-
-
|
-