wip(backend): Started work on v3 backend
This commit is contained in:
parent
94f0ca0e0b
commit
88d9b2a1e9
67 changed files with 1375 additions and 2842 deletions
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
BIN
sites/backend/dev.sqlite
Normal file
BIN
sites/backend/dev.sqlite
Normal file
Binary file not shown.
|
@ -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
|
|
@ -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
|
4
sites/backend/nodemon.json
Normal file
4
sites/backend/nodemon.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"verbose": true,
|
||||
"ignore": ["tests/**.test.mjs"]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
81
sites/backend/prisma/schema.prisma
Normal file
81
sites/backend/prisma/schema.prisma
Normal file
|
@ -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])
|
||||
}
|
BIN
sites/backend/prisma/schema.sqlite
Normal file
BIN
sites/backend/prisma/schema.sqlite
Normal file
Binary file not shown.
27
sites/backend/prisma/seed.mjs
Normal file
27
sites/backend/prisma/seed.mjs
Normal file
|
@ -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)
|
||||
})
|
8
sites/backend/public/avatar.svg
Normal file
8
sites/backend/public/avatar.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
44
sites/backend/public/css/style.css
Normal file
44
sites/backend/public/css/style.css
Normal file
|
@ -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;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
docker run -d --name mongo -p 27017:27017 mongo
|
34
sites/backend/scripts/newdb.mjs
Normal file
34
sites/backend/scripts/newdb.mjs
Normal file
|
@ -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()
|
||||
|
29
sites/backend/scripts/prebuild.mjs
Normal file
29
sites/backend/scripts/prebuild.mjs
Normal file
|
@ -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()
|
43
sites/backend/scripts/resetrootpassword.mjs
Normal file
43
sites/backend/scripts/resetrootpassword.mjs
Normal file
|
@ -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()
|
||||
|
47
sites/backend/scripts/rmdb.mjs
Normal file
47
sites/backend/scripts/rmdb.mjs
Normal file
|
@ -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()
|
||||
|
||||
|
25
sites/backend/scripts/secret.mjs
Normal file
25
sites/backend/scripts/secret.mjs
Normal file
|
@ -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()
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
}
|
|
@ -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()
|
||||
})
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
]
|
181
sites/backend/src/config.mjs
Normal file
181
sites/backend/src/config.mjs
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
*/
|
|
@ -1,2 +0,0 @@
|
|||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
|
@ -1,7 +0,0 @@
|
|||
import { connectToDb, startApp } from './app'
|
||||
|
||||
connectToDb()
|
||||
|
||||
const app = startApp()
|
||||
|
||||
export default app
|
46
sites/backend/src/index.mjs
Normal file
46
sites/backend/src/index.mjs
Normal file
|
@ -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}`))
|
||||
})
|
|
@ -2,59 +2,27 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>You found our backend</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Raleway:300,500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
color: #f8f9fa;
|
||||
line-height: 1.25;
|
||||
font-size: 24px;
|
||||
background: #212529;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Raleway', sans-serif;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 2rem;
|
||||
}
|
||||
img {
|
||||
width: 166px;
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: calc(50% - 83px);
|
||||
}
|
||||
div.msg {
|
||||
text-align: left;
|
||||
max-width: 36ch;
|
||||
margin: 6rem auto;
|
||||
}
|
||||
a,
|
||||
a:visited,
|
||||
a:active {
|
||||
color: #d0bfff !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<title>FreeSewing backend</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<span></span>
|
||||
<div class="msg">
|
||||
<h1>Hi</h1>
|
||||
<h1><span role="img">👋</span></h1>
|
||||
<h2>This is the FreeSewing backend</h2>
|
||||
<p>
|
||||
This is the FreeSewing backend.
|
||||
<br />
|
||||
Try <a href="https://freesewing.org/">freesewing.org</a> instead.
|
||||
For info about FreeSewing, try <a href="https://freesewing.org/">freesewing.org</a> instead.
|
||||
</p>
|
||||
<p>
|
||||
For info about this backend, refer to <a href="https://freesewing.dev/reference/backend">the FreeSewing backend refefence documentation</a>.
|
||||
</p>
|
||||
<p>
|
||||
For questions, join us at
|
||||
<a href="https://discord.freesewing.org/">discord.freesewing.org</a>
|
||||
</p>
|
||||
</div>
|
||||
<img src="https://freesewing.org/avatar.svg" />
|
||||
<img src="/avatar.svg" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
28
sites/backend/src/middleware.mjs
Normal file
28
sites/backend/src/middleware.mjs
Normal file
|
@ -0,0 +1,28 @@
|
|||
//import bodyParser from 'body-parser'
|
||||
import cors from 'cors'
|
||||
import jwt from 'passport-jwt'
|
||||
|
||||
function loadExpressMiddleware(app) {
|
||||
// FIXME: Is this still needed in FreeSewing v3?
|
||||
//app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.use(cors())
|
||||
}
|
||||
|
||||
function loadPassportMiddleware(passport, config) {
|
||||
passport.use(
|
||||
new jwt.Strategy(
|
||||
{
|
||||
jwtFromRequest: jwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
...config.jwt,
|
||||
},
|
||||
(jwt_payload, done) => {
|
||||
return done(null, jwt_payload)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
loadExpressMiddleware,
|
||||
loadPassportMiddleware,
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import bodyParser from 'body-parser'
|
||||
|
||||
export default (app) => {
|
||||
app.use(bodyParser.json({ limit: '20mb' }))
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import cors from 'cors'
|
||||
|
||||
export default (app) => {
|
||||
app.use(cors())
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
import bodyParser from './bodyParser'
|
||||
import cors from './cors'
|
||||
|
||||
export default { bodyParser, cors }
|
|
@ -1,3 +0,0 @@
|
|||
import jwt from './jwt'
|
||||
|
||||
export default { jwt }
|
|
@ -1,15 +0,0 @@
|
|||
import jwt from 'passport-jwt'
|
||||
import config from '../../config'
|
||||
|
||||
const options = {
|
||||
jwtFromRequest: jwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
...config.jwt,
|
||||
}
|
||||
|
||||
export default (passport) => {
|
||||
passport.use(
|
||||
new jwt.Strategy(options, (jwt_payload, done) => {
|
||||
return done(null, jwt_payload)
|
||||
})
|
||||
)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import mongoose, { Schema } from 'mongoose'
|
||||
import bcrypt from 'mongoose-bcrypt'
|
||||
import encrypt from 'mongoose-encryption'
|
||||
import config from '../config'
|
||||
|
||||
const ConfirmationSchema = new Schema({
|
||||
created: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['signup', 'emailchange', 'passwordreset', 'oauth', 'newsletter'],
|
||||
required: true,
|
||||
},
|
||||
data: {},
|
||||
})
|
||||
|
||||
ConfirmationSchema.plugin(bcrypt)
|
||||
|
||||
ConfirmationSchema.plugin(encrypt, {
|
||||
secret: config.encryption.key,
|
||||
encryptedFields: ['data'],
|
||||
decryptPostSave: false,
|
||||
})
|
||||
|
||||
export default mongoose.model('Confirmation', ConfirmationSchema)
|
|
@ -1,11 +0,0 @@
|
|||
import ConfirmationModel from './confirmation'
|
||||
import PersonModel from './person'
|
||||
import UserModel from './user'
|
||||
import PatternModel from './pattern'
|
||||
import NewsletterModel from './newsletter'
|
||||
|
||||
export const Confirmation = ConfirmationModel
|
||||
export const Person = PersonModel
|
||||
export const User = UserModel
|
||||
export const Pattern = PatternModel
|
||||
export const Newsletter = NewsletterModel
|
|
@ -1,34 +0,0 @@
|
|||
import mongoose, { Schema } from 'mongoose'
|
||||
import bcrypt from 'mongoose-bcrypt'
|
||||
import encrypt from 'mongoose-encryption'
|
||||
import config from '../config'
|
||||
|
||||
const NewsletterSchema = new Schema({
|
||||
created: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
ehash: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
},
|
||||
data: {},
|
||||
})
|
||||
|
||||
NewsletterSchema.plugin(bcrypt)
|
||||
|
||||
NewsletterSchema.index({ ehash: 1 })
|
||||
|
||||
NewsletterSchema.plugin(encrypt, {
|
||||
secret: config.encryption.key,
|
||||
encryptedFields: ['email'],
|
||||
decryptPostSave: false,
|
||||
})
|
||||
|
||||
export default mongoose.model('Newsletter', NewsletterSchema)
|
|
@ -1,62 +0,0 @@
|
|||
import mongoose, { Schema } from 'mongoose'
|
||||
|
||||
const PatternSchema = new Schema(
|
||||
{
|
||||
handle: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
index: true,
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
index: true,
|
||||
},
|
||||
person: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
index: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
created: Date,
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
{ timestamps: true }
|
||||
)
|
||||
|
||||
PatternSchema.index({ user: 1, handle: 1 })
|
||||
|
||||
PatternSchema.methods.info = function () {
|
||||
return this.toObject()
|
||||
}
|
||||
|
||||
PatternSchema.methods.export = function () {
|
||||
let pattern = this.toObject()
|
||||
for (let field of ['__v', '_id', '_v', 'created']) delete pattern[field]
|
||||
|
||||
return pattern
|
||||
}
|
||||
|
||||
PatternSchema.methods.anonymize = function () {
|
||||
let pattern = this.toObject()
|
||||
for (let field of ['__v', '_id', 'user', 'createdAt', 'updatedAt', '_v']) delete pattern[field]
|
||||
|
||||
return pattern
|
||||
}
|
||||
|
||||
export default mongoose.model('Pattern', PatternSchema)
|
|
@ -1,171 +0,0 @@
|
|||
import mongoose, { Schema } from 'mongoose'
|
||||
import config from '../config'
|
||||
import fs from 'fs'
|
||||
import { log, randomAvatar } from '../utils'
|
||||
import path from 'path'
|
||||
import sharp from 'sharp'
|
||||
|
||||
const PersonSchema = new Schema(
|
||||
{
|
||||
handle: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
index: true,
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
index: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
breasts: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: '',
|
||||
},
|
||||
units: {
|
||||
type: String,
|
||||
enum: ['metric', 'imperial'],
|
||||
default: 'metric',
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
measurements: {
|
||||
ankle: Number,
|
||||
biceps: Number,
|
||||
bustFront: Number,
|
||||
bustPointToUnderbust: Number,
|
||||
bustSpan: Number,
|
||||
chest: Number,
|
||||
crossSeam: Number,
|
||||
crossSeamFront: Number,
|
||||
crotchDepth: Number,
|
||||
head: Number,
|
||||
heel: Number,
|
||||
highBust: Number,
|
||||
highBustFront: Number,
|
||||
hips: Number,
|
||||
hpsToBust: Number,
|
||||
hpsToWaistBack: Number,
|
||||
hpsToWaistFront: Number,
|
||||
inseam: Number,
|
||||
knee: Number,
|
||||
neck: Number,
|
||||
seat: Number,
|
||||
seatBack: Number,
|
||||
shoulderSlope: Number,
|
||||
shoulderToElbow: Number,
|
||||
shoulderToShoulder: Number,
|
||||
shoulderToWrist: Number,
|
||||
underbust: Number,
|
||||
upperLeg: Number,
|
||||
waist: Number,
|
||||
waistBack: Number,
|
||||
waistToFloor: Number,
|
||||
waistToHips: Number,
|
||||
waistToKnee: Number,
|
||||
waistToSeat: Number,
|
||||
waistToUnderbust: Number,
|
||||
waistToUpperLeg: Number,
|
||||
wrist: Number,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
)
|
||||
|
||||
PersonSchema.index({ user: 1, handle: 1 })
|
||||
|
||||
PersonSchema.methods.info = function () {
|
||||
let person = this.toObject()
|
||||
delete person.__v
|
||||
delete person._id
|
||||
person.pictureUris = {
|
||||
l: this.avatarUri(),
|
||||
m: this.avatarUri('m'),
|
||||
s: this.avatarUri('s'),
|
||||
xs: this.avatarUri('xs'),
|
||||
}
|
||||
|
||||
return person
|
||||
}
|
||||
|
||||
PersonSchema.methods.avatarName = function (size = 'l') {
|
||||
let prefix = size === 'l' ? '' : size + '-'
|
||||
if (this.picture.slice(-4).toLowerCase() === '.svg') prefix = ''
|
||||
|
||||
return prefix + this.picture
|
||||
}
|
||||
|
||||
PersonSchema.methods.avatarUri = function (size = 'l') {
|
||||
return (
|
||||
config.static +
|
||||
'/users/' +
|
||||
this.user.substring(0, 1) +
|
||||
'/' +
|
||||
this.user +
|
||||
'/people/' +
|
||||
this.handle +
|
||||
'/' +
|
||||
this.avatarName(size)
|
||||
)
|
||||
}
|
||||
|
||||
PersonSchema.methods.storagePath = function () {
|
||||
return (
|
||||
config.storage +
|
||||
'/users/' +
|
||||
this.user.substring(0, 1) +
|
||||
'/' +
|
||||
this.user +
|
||||
'/people/' +
|
||||
this.handle +
|
||||
'/'
|
||||
)
|
||||
}
|
||||
|
||||
PersonSchema.methods.createAvatar = function () {
|
||||
let dir = this.storagePath()
|
||||
fs.mkdir(dir, { recursive: true }, (err) => {
|
||||
if (err) console.log('mkdirFailed', dir, err)
|
||||
fs.writeFile(path.join(dir, this.handle) + '.svg', randomAvatar(), (err) => {
|
||||
if (err) console.log('writeFileFailed', dir, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
PersonSchema.methods.saveAvatar = function (picture) {
|
||||
let type = picture.split(';').shift()
|
||||
type = type.split('/').pop()
|
||||
this.picture = this.handle + '.' + type
|
||||
|
||||
let dir = this.storagePath()
|
||||
let b64 = picture.split(';base64,').pop()
|
||||
fs.mkdir(dir, { recursive: true }, (err) => {
|
||||
if (err) log.error('mkdirFailed', err)
|
||||
let imgBuffer = Buffer.from(b64, 'base64')
|
||||
for (let size of Object.keys(config.avatar.sizes)) {
|
||||
sharp(imgBuffer)
|
||||
.resize(config.avatar.sizes[size], config.avatar.sizes[size])
|
||||
.toFile(path.join(dir, this.avatarName(size)), (err, info) => {
|
||||
if (err) log.error('avatarNotSaved', err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default mongoose.model('Person', PersonSchema)
|
|
@ -1,275 +0,0 @@
|
|||
import mongoose, { Schema } from 'mongoose'
|
||||
import bcrypt from 'mongoose-bcrypt'
|
||||
import { email, randomAvatar } from '../utils'
|
||||
import encrypt from 'mongoose-encryption'
|
||||
import config from '../config'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { log } from '../utils'
|
||||
import sharp from 'sharp'
|
||||
|
||||
const UserSchema = new Schema(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
ehash: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
},
|
||||
initial: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
trim: true,
|
||||
},
|
||||
handle: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
index: true,
|
||||
unique: true,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['user', 'moderator', 'admin'],
|
||||
required: true,
|
||||
default: 'user',
|
||||
},
|
||||
patron: {
|
||||
type: Number,
|
||||
enum: [0, 2, 4, 8],
|
||||
default: 0,
|
||||
},
|
||||
bio: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: '',
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'active', 'blocked', 'frozen'],
|
||||
default: 'pending',
|
||||
required: true,
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
bcrypt: true,
|
||||
},
|
||||
settings: {
|
||||
language: {
|
||||
type: String,
|
||||
default: 'en',
|
||||
enum: config.languages,
|
||||
},
|
||||
units: {
|
||||
type: String,
|
||||
enum: ['metric', 'imperial'],
|
||||
default: 'metric',
|
||||
},
|
||||
},
|
||||
consent: {
|
||||
profile: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
measurements: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
openData: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
time: {
|
||||
migrated: Date,
|
||||
login: Date,
|
||||
patron: Date,
|
||||
},
|
||||
social: {
|
||||
twitter: String,
|
||||
instagram: String,
|
||||
github: String,
|
||||
},
|
||||
newsletter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
)
|
||||
|
||||
UserSchema.pre('remove', function (next) {
|
||||
email
|
||||
.goodbye(this.email, this.settings.language)
|
||||
.then(() => {
|
||||
next()
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err)
|
||||
next()
|
||||
})
|
||||
})
|
||||
|
||||
UserSchema.plugin(bcrypt)
|
||||
UserSchema.index({ ehash: 1, username: 1, handle: 1 })
|
||||
|
||||
UserSchema.plugin(encrypt, {
|
||||
secret: config.encryption.key,
|
||||
encryptedFields: ['email', 'initial', 'social.twitter', 'social.instagram', 'social.github'],
|
||||
decryptPostSave: true,
|
||||
})
|
||||
|
||||
UserSchema.methods.account = function () {
|
||||
let account = this.toObject()
|
||||
delete account.password
|
||||
delete account.ehash
|
||||
delete account.pepper
|
||||
delete account.initial
|
||||
delete account._ac
|
||||
delete account._ct
|
||||
|
||||
account.pictureUris = {
|
||||
l: this.avatarUri(),
|
||||
m: this.avatarUri('m'),
|
||||
s: this.avatarUri('s'),
|
||||
xs: this.avatarUri('xs'),
|
||||
}
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
UserSchema.methods.profile = function () {
|
||||
let account = this.toObject()
|
||||
delete account.password
|
||||
delete account.ehash
|
||||
delete account.pepper
|
||||
delete account.email
|
||||
delete account.consent
|
||||
delete account.initial
|
||||
delete account.role
|
||||
delete account.status
|
||||
delete account.time
|
||||
delete account.picture
|
||||
delete account.__v
|
||||
delete account._id
|
||||
delete account._ac
|
||||
delete account._ct
|
||||
delete account._ct
|
||||
account.pictureUris = {
|
||||
l: this.avatarUri(),
|
||||
m: this.avatarUri('m'),
|
||||
s: this.avatarUri('s'),
|
||||
xs: this.avatarUri('xs'),
|
||||
}
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
UserSchema.methods.adminProfile = function () {
|
||||
let account = this.toObject()
|
||||
delete account.password
|
||||
delete account.ehash
|
||||
delete account.pepper
|
||||
delete account.__v
|
||||
delete account._id
|
||||
delete account._ac
|
||||
delete account._ct
|
||||
delete account._ct
|
||||
account.pictureUris = {
|
||||
l: this.avatarUri(),
|
||||
m: this.avatarUri('m'),
|
||||
s: this.avatarUri('s'),
|
||||
xs: this.avatarUri('xs'),
|
||||
}
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
UserSchema.methods.export = function () {
|
||||
let exported = this.toObject()
|
||||
delete exported.password
|
||||
delete exported.ehash
|
||||
delete exported.pepper
|
||||
delete exported._ac
|
||||
delete exported._ct
|
||||
|
||||
return exported
|
||||
}
|
||||
|
||||
UserSchema.methods.updateLoginTime = function (callback) {
|
||||
this.set({ time: { login: new Date() } })
|
||||
this.save(function (err, user) {
|
||||
return callback()
|
||||
})
|
||||
}
|
||||
|
||||
UserSchema.methods.avatarName = function (size = 'l') {
|
||||
let prefix = size === 'l' ? '' : size + '-'
|
||||
if (this.picture.slice(-4).toLowerCase() === '.svg') prefix = ''
|
||||
|
||||
return prefix + this.picture
|
||||
}
|
||||
|
||||
UserSchema.methods.storagePath = function () {
|
||||
return path.join(config.storage, 'users', this.handle.substring(0, 1), this.handle)
|
||||
}
|
||||
|
||||
UserSchema.methods.avatarUri = function (size = 'l') {
|
||||
if (!this.picture || this.picture.length < 5) return 'https://freesewing.org/avatar.svg'
|
||||
return (
|
||||
config.static +
|
||||
'/users/' +
|
||||
this.handle.substring(0, 1) +
|
||||
'/' +
|
||||
this.handle +
|
||||
'/' +
|
||||
this.avatarName(size)
|
||||
)
|
||||
}
|
||||
|
||||
UserSchema.methods.saveAvatar = function (picture) {
|
||||
let type = picture.split(';').shift()
|
||||
type = type.split('/').pop()
|
||||
this.picture = this.handle + '.' + type
|
||||
|
||||
let dir = this.storagePath()
|
||||
let b64 = picture.split(';base64,').pop()
|
||||
fs.mkdir(dir, { recursive: true }, (err) => {
|
||||
if (err) log.error('mkdirFailed', err)
|
||||
let imgBuffer = Buffer.from(b64, 'base64')
|
||||
for (let size of Object.keys(config.avatar.sizes)) {
|
||||
sharp(imgBuffer)
|
||||
.resize(config.avatar.sizes[size], config.avatar.sizes[size])
|
||||
.toFile(path.join(dir, this.avatarName(size)), (err, info) => {
|
||||
if (err) log.error('avatarNotSaved', err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
UserSchema.methods.createAvatar = function () {
|
||||
let dir = this.storagePath()
|
||||
fs.mkdirSync(dir, { recursive: true }, (err) => {
|
||||
if (err) console.log('mkdirFailed', dir, err)
|
||||
fs.writeFileSync(path.join(dir, this.handle) + '.svg', randomAvatar(), (err) => {
|
||||
if (err) console.log('writeFileFailed', dir, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default mongoose.model('User', UserSchema)
|
|
@ -1,10 +0,0 @@
|
|||
import Controller from '../controllers/auth'
|
||||
|
||||
const Auth = new Controller()
|
||||
|
||||
export default (app, passport) => {
|
||||
// Oauth
|
||||
app.post('/oauth/init', Auth.initOauth)
|
||||
app.get('/oauth/callback/from/:provider', Auth.providerCallback)
|
||||
app.post('/oauth/login', Auth.loginOauth)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import pattern from './pattern'
|
||||
import person from './person'
|
||||
import user from './user'
|
||||
import auth from './auth'
|
||||
import github from './github'
|
||||
import admin from './admin'
|
||||
import newsletter from './newsletter'
|
||||
import strapi from './strapi'
|
||||
import og from './og'
|
||||
|
||||
export default { user, pattern, person, auth, github, admin, newsletter, strapi, og }
|
17
sites/backend/src/routes/index.mjs
Normal file
17
sites/backend/src/routes/index.mjs
Normal file
|
@ -0,0 +1,17 @@
|
|||
//import pattern from './pattern'
|
||||
//import person from './person'
|
||||
import { userRoutes } from './user.mjs'
|
||||
//import oauth from './auth.mjs'
|
||||
//import github from './github'
|
||||
//import admin from './admin'
|
||||
//import newsletter from './newsletter'
|
||||
|
||||
export const routes = {
|
||||
userRoutes,
|
||||
// pattern,
|
||||
// person,
|
||||
// oauth,
|
||||
// github,
|
||||
// admin,
|
||||
// newsletter,
|
||||
}
|
10
sites/backend/src/routes/oauth.mjs
Normal file
10
sites/backend/src/routes/oauth.mjs
Normal file
|
@ -0,0 +1,10 @@
|
|||
import Controller from '../controllers/oauth.mjs'
|
||||
|
||||
const OAuth = new Controller()
|
||||
|
||||
export default (app, passport) => {
|
||||
// Oauth
|
||||
app.post('/oauth/init', OAuth.initOAuth)
|
||||
app.get('/oauth/callback/from/:provider', OAuth.providerCallback)
|
||||
app.post('/oauth/login', OAuth.loginOAuth)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import Controller from '../controllers/user'
|
||||
|
||||
const User = new Controller()
|
||||
|
||||
export default (app, passport) => {
|
||||
// account
|
||||
app.post('/account', User.create)
|
||||
app.get('/account', passport.authenticate('jwt', { session: false }), User.readAccount) // Read account (own data)
|
||||
app.put('/account', passport.authenticate('jwt', { session: false }), User.update) // Update
|
||||
app.delete('/account', passport.authenticate('jwt', { session: false }), User.remove)
|
||||
app.get('/account/export', passport.authenticate('jwt', { session: false }), User.export)
|
||||
app.get('/account/restrict', passport.authenticate('jwt', { session: false }), User.restrict)
|
||||
app.post('/account/recover', User.resetPassword)
|
||||
app.post(
|
||||
'/account/change/email',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
User.confirmChangedEmail
|
||||
)
|
||||
|
||||
// Sign up & log in
|
||||
app.post('/signup', User.signup)
|
||||
app.post('/resend', User.resend)
|
||||
app.post('/login', User.login)
|
||||
app.post('/confirm/login', User.confirmationLogin)
|
||||
|
||||
// Users
|
||||
app.get('/patrons', User.patronList)
|
||||
app.get('/users/:username', User.readProfile) // Read profile (other user's data)
|
||||
app.post(
|
||||
'/available/username',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
User.isUsernameAvailable
|
||||
)
|
||||
}
|
106
sites/backend/src/routes/user.mjs
Normal file
106
sites/backend/src/routes/user.mjs
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { UserController } from '../controllers/user.mjs'
|
||||
|
||||
const User = new UserController()
|
||||
const jwt = ['jwt', { session: false }]
|
||||
|
||||
|
||||
export function userRoutes(params) {
|
||||
const { app, passport } = params
|
||||
// Create account
|
||||
app.post(
|
||||
'/account',
|
||||
(req, res) => User.create(req, res, params)
|
||||
)
|
||||
|
||||
// Read account (own data)
|
||||
app.get(
|
||||
'/account',
|
||||
passport.authenticate(...jwt),
|
||||
(req, res) => User.readAccount(req, res, params)
|
||||
)
|
||||
|
||||
// Update account
|
||||
app.put(
|
||||
'/account',
|
||||
passport.authenticate(...jwt),
|
||||
(req, res) => User.update(req, res, params)
|
||||
)
|
||||
|
||||
// Remove account
|
||||
app.delete(
|
||||
'/account',
|
||||
passport.authenticate(...jwt),
|
||||
(req, res) => User.remove(req, res, params)
|
||||
)
|
||||
|
||||
// Export account data
|
||||
app.get(
|
||||
'/account/export',
|
||||
passport.authenticate(...jwt),
|
||||
(req, res) => User.export(req, res, params)
|
||||
)
|
||||
|
||||
// Restrict processing of account data
|
||||
app.get(
|
||||
'/account/restrict',
|
||||
passport.authenticate(...jwt),
|
||||
(req, res) => User.restrict(req, res, params)
|
||||
)
|
||||
|
||||
// Recover account / Reset password
|
||||
app.post(
|
||||
'/account/recover',
|
||||
(req, res) => User.resetPassword(req, res, params)
|
||||
)
|
||||
|
||||
// Update email address
|
||||
app.post(
|
||||
'/account/change/email',
|
||||
passport.authenticate(...jwt),
|
||||
(req, res) => User.confirmChangedEmail(req, res, params)
|
||||
)
|
||||
|
||||
// Sign up
|
||||
app.post(
|
||||
'/signup',
|
||||
(req, res) => User.signup(req, res, params)
|
||||
)
|
||||
|
||||
// Re-send account confirmation
|
||||
app.post(
|
||||
'/resend',
|
||||
(req, res) => User.resend(req, res, params)
|
||||
)
|
||||
|
||||
// Login
|
||||
app.post(
|
||||
'/login',
|
||||
(req, res) => User.login(req, res, params)
|
||||
)
|
||||
|
||||
// Account confirmation
|
||||
app.post(
|
||||
'/confirm/login',
|
||||
(req, res) => User.confirmationLogin(req, res, params)
|
||||
)
|
||||
|
||||
// Get list of patrons
|
||||
app.get(
|
||||
'/patrons',
|
||||
(req, res) => User.patronList(req, res, params)
|
||||
)
|
||||
|
||||
// Read profile (other user's data)
|
||||
app.get(
|
||||
'/users/:username',
|
||||
(req, res) => User.readProfile(req, res, params)
|
||||
)
|
||||
|
||||
// Check whether a username is available
|
||||
app.post(
|
||||
'/available/username',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
(req, res) => User.isUsernameAvailable(req, res, params)
|
||||
)
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -1,77 +0,0 @@
|
|||
const emailchange = {
|
||||
i18n: [
|
||||
'email.emailchangeTitle',
|
||||
'email.emailchangeCopy1',
|
||||
'email.emailchangeActionText',
|
||||
'email.questionsJustReply',
|
||||
'email.signature',
|
||||
],
|
||||
html: `<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 10px 15px 40px 15px;" class="section-padding">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="500" class="responsive-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left" style="font-size: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #212121; padding-top: 30px;" class="padding-copy">__emailchangeTitle__</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
__emailchangeCopy1__
|
||||
</td>
|
||||
</tr>
|
||||
<!-- BULLETPROOF BUTTON -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 30px 30px 30px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 3px;" bgcolor="#212121"><a href="__emailchangeActionLink__" target="_blank" style="font-size: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #111111; display: inline-block;">__emailchangeActionText__</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 0 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
<br>
|
||||
__questionsJustReply__
|
||||
<br><br>
|
||||
__signature__
|
||||
<br>
|
||||
joost
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`,
|
||||
text: `__emailchangeTitle__
|
||||
|
||||
__emailchangeCopy1__
|
||||
|
||||
__emailchangeActionLink__
|
||||
|
||||
__questionsJustReply__`,
|
||||
}
|
||||
|
||||
export default emailchange
|
|
@ -1,35 +0,0 @@
|
|||
const footer = {
|
||||
i18n: [],
|
||||
html: `<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 0px 15px 0px 15px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 500px;" >
|
||||
<!-- PERMISSION REMINDER -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 0 0 30px 0; color: #292B2C; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; font-weight: 300; line-height: 18px;" >
|
||||
<p style="margin: 0;">
|
||||
__footerWhy__
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`,
|
||||
text: '',
|
||||
}
|
||||
|
||||
export default footer
|
|
@ -1,45 +0,0 @@
|
|||
const goodbye = {
|
||||
i18n: ['goodbyeTitle', 'goodbyeCopy1', 'signature'],
|
||||
html: `<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 10px 15px 40px 15px;" class="section-padding">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="500" class="responsive-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left" style="font-size: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #212121; padding-top: 30px;" class="padding-copy">__goodbyeTitle__</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
__goodbyeCopy1__
|
||||
<br><br>
|
||||
__signature__
|
||||
<br>
|
||||
joost
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`,
|
||||
text: `__goodbyeTitle__
|
||||
|
||||
__goodbyeCopy1__`,
|
||||
}
|
||||
|
||||
export default goodbye
|
|
@ -1,119 +0,0 @@
|
|||
const header = {
|
||||
html: `<!DOCTYPE html>
|
||||
<html lang="__locale__">
|
||||
<head>
|
||||
<title>freesewing</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<style type="text/css">
|
||||
#outlook a{padding:0;}
|
||||
.ReadMsgBody{width:100%;} .ExternalClass{width:100%;}
|
||||
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
|
||||
body, table, td, a{-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;}
|
||||
table, td{mso-table-lspace:0pt; mso-table-rspace:0pt;}
|
||||
img{-ms-interpolation-mode:bicubic;}
|
||||
|
||||
body{margin:0; padding:0; color: #212121;}
|
||||
img{border:0; height:auto; line-height:100%; outline:none; text-decoration:none;}
|
||||
table{border-collapse:collapse !important;}
|
||||
body{height:100% !important; margin:0; padding:0; width:100% !important;}
|
||||
|
||||
a {color:#212121; text-decoration: underline;}
|
||||
a:hover {color:#212121; text-decoration: underline;}
|
||||
.appleBody a {color:#212121; text-decoration: underline;}
|
||||
.appleFooter a {color:#212121; text-decoration: underline;}
|
||||
|
||||
@media screen and (max-width: 525px) {
|
||||
|
||||
table[class="wrapper"]{
|
||||
width:100% !important;
|
||||
}
|
||||
|
||||
table[class="responsive-table"]{
|
||||
width:100%!important;
|
||||
}
|
||||
|
||||
td[class="padding"]{
|
||||
padding: 10px 5% 15px 5% !important;
|
||||
}
|
||||
|
||||
td[class="padding-copy"]{
|
||||
padding: 10px 5% 10px 5% !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td[class="padding-meta"]{
|
||||
padding: 30px 5% 0px 5% !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td[class="no-pad"]{
|
||||
padding: 0 0 20px 0 !important;
|
||||
}
|
||||
|
||||
td[class="no-padding"]{
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
td[class="section-padding"]{
|
||||
padding: 50px 15px 50px 15px !important;
|
||||
}
|
||||
|
||||
td[class="mobile-wrapper"]{
|
||||
padding: 10px 5% 15px 5% !important;
|
||||
}
|
||||
|
||||
table[class="mobile-button-container"]{
|
||||
margin:0 auto;
|
||||
width:100% !important;
|
||||
}
|
||||
|
||||
a[class="mobile-button"]{
|
||||
width:80% !important;
|
||||
padding: 15px !important;
|
||||
border: 0 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;">
|
||||
<!-- HIDDEN PREHEADER TEXT -->
|
||||
<div style="display: none; font-size: 1px; color: #ffffff; line-height: 1px; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
|
||||
__hiddenIntro__
|
||||
</div>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 0px 15px 0px 15px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 500px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 30px 0 0 0; color: #212121; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 16px; font-weight: 300; line-height: 18px;" >
|
||||
<p style="margin: 0; font-weight: 700; color: #111111;">freesewing</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 0; color: #292B2C; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; font-weight: 300; line-height: 18px;" >
|
||||
<p style="margin: 0;">__headerOpeningLine__<br><hr></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>`,
|
||||
text: '',
|
||||
}
|
||||
|
||||
export default header
|
|
@ -1,19 +0,0 @@
|
|||
import header from './header'
|
||||
import footer from './footer'
|
||||
import signup from './signup'
|
||||
import emailchange from './emailchange'
|
||||
import passwordreset from './passwordreset'
|
||||
import goodbye from './goodbye'
|
||||
import newsletterSubscribe from './newsletter-subscribe'
|
||||
import newsletterWelcome from './newsletter-welcome'
|
||||
|
||||
export default {
|
||||
header,
|
||||
footer,
|
||||
signup,
|
||||
emailchange,
|
||||
passwordreset,
|
||||
goodbye,
|
||||
newsletterSubscribe,
|
||||
newsletterWelcome,
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
const subscribe = {
|
||||
i18n: [],
|
||||
html: `<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 10px 15px 40px 15px;" class="section-padding">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="500" class="responsive-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left" style="font-size: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #212121; padding-top: 30px;" class="padding-copy">Confirm your newsletter subscription</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
Somebody asked to subscribe this email address to the FreeSewing newsletter. If it was you, please click below to confirm your subscription:
|
||||
</td>
|
||||
</tr>
|
||||
<!-- BULLETPROOF BUTTON -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 30px 30px 30px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 3px;" bgcolor="#212121"><a href="__newsletterConfirmationLink__" target="_blank" style="font-size: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #111111; display: inline-block;">Confirm your newsletter subscription</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`,
|
||||
text: `Confirm your newsletter subscription.
|
||||
|
||||
Somebody asked to subscribe this email address to the FreeSewing newsletter.
|
||||
If it was you, please click below to confirm your subscription:
|
||||
|
||||
__newsletterConfirmationLink__
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
export default subscribe
|
|
@ -1,44 +0,0 @@
|
|||
const welcome = {
|
||||
i18n: [],
|
||||
html: `<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 10px 15px 40px 15px;" class="section-padding">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="500" class="responsive-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left" style="font-size: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #212121; padding-top: 30px;" class="padding-copy">You are now subscribed to the FreeSewing newsletter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
If you'd like to catch up, we keep an online archive of previous editions at:
|
||||
<a href="https://freesewing.org/newsletter/">https://freesewing.org/newsletter/</a>
|
||||
<br><br>
|
||||
You can <a href="__newsletterUnsubscribeLink__">unsubscribe</a> at any time by visiting this link: <a href="__newsletterUnsubscribeLink__">__newsletterUnsubscribeLink__</a>
|
||||
<br><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`,
|
||||
text: `You are now subscribed to the FreeSewing newsletter
|
||||
|
||||
If you'd like to catch up, we keep an online archive of previous editions at: https://freesewing.org/newsletter/
|
||||
|
||||
You can unsubscribe at any time by visiting this link: __newsletterUnsubscribeLink__
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
export default welcome
|
|
@ -1,77 +0,0 @@
|
|||
const passwordreset = {
|
||||
i18n: [
|
||||
'passwordresetTitle',
|
||||
'passwordresetCopy1',
|
||||
'passwordresetActionText',
|
||||
'questionsJustReply',
|
||||
'signature',
|
||||
],
|
||||
html: `<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 10px 15px 40px 15px;" class="section-padding">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="500" class="responsive-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left" style="font-size: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #212121; padding-top: 30px;" class="padding-copy">__passwordresetTitle__</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
__passwordresetCopy1__
|
||||
</td>
|
||||
</tr>
|
||||
<!-- BULLETPROOF BUTTON -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 30px 30px 30px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 3px;" bgcolor="#212121"><a href="__passwordresetActionLink__" target="_blank" style="font-size: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #111111; display: inline-block;">__passwordresetActionText__</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 0 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
<br>
|
||||
__questionsJustReply__
|
||||
<br><br>
|
||||
__signature__
|
||||
<br>
|
||||
joost
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`,
|
||||
text: `__passwordresetTitle__
|
||||
|
||||
__passwordresetCopy1__
|
||||
|
||||
__passwordresetActionLink__
|
||||
|
||||
__questionsJustReply__`,
|
||||
}
|
||||
|
||||
export default passwordreset
|
|
@ -1,71 +0,0 @@
|
|||
const signup = {
|
||||
i18n: ['signupTitle', 'signupCopy1', 'signupActionText', 'questionsJustReply', 'signature'],
|
||||
html: `<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 10px 15px 40px 15px;" class="section-padding">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="500" class="responsive-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left" style="font-size: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #212121; padding-top: 30px;" class="padding-copy">__signupTitle__</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
__signupCopy1__
|
||||
</td>
|
||||
</tr>
|
||||
<!-- BULLETPROOF BUTTON -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 30px 30px 30px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 3px;" bgcolor="#212121"><a href="__signupActionLink__" target="_blank" style="font-size: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #111111; display: inline-block;">__signupActionText__</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 0 0 0 0; font-size: 16px; line-height: 25px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
<br>
|
||||
__questionsJustReply__
|
||||
<br><br>
|
||||
__signature__
|
||||
<br>
|
||||
joost
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="padding: 20px 0 0 0; font-size: 16px; line-height: 20px; font-family: -apple-system,'BlinkMacSystemFont','Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif; color: #292B2C;" class="padding-copy">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`,
|
||||
text: `__signupTitle__
|
||||
|
||||
__signupCopy1__
|
||||
|
||||
__signupActionLink__
|
||||
|
||||
__questionsJustReply__`,
|
||||
}
|
||||
|
||||
export default signup
|
101
sites/backend/src/utils/crypto.mjs
Normal file
101
sites/backend/src/utils/crypto.mjs
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { createHash, createCipheriv, createDecipheriv, scryptSync, randomBytes } from 'crypto'
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
/*
|
||||
* Cleans a string (typically email) for hashing
|
||||
*/
|
||||
export const clean = (email) => {
|
||||
if (typeof email !== 'string') throw("clean() only takes a string as input")
|
||||
|
||||
return email.toLowerCase().trim()
|
||||
}
|
||||
|
||||
// Hashes an email address
|
||||
export const ehash = (email) => createHash('sha256').update(clean(email)).digest('hex')
|
||||
|
||||
/*
|
||||
* Returns an object holding encrypt() and decrypt() methods
|
||||
*
|
||||
* These utility methods keep the crypto code out of the regular codebase
|
||||
* which makes things easier to read/understand for contributors, as well
|
||||
* as allowing scrutiny of the implementation in a single file.
|
||||
*/
|
||||
export const encryption = (stringKey, salt='FreeSewing') => {
|
||||
|
||||
// Shout-out to the OG crypto bros Joan and Vincent
|
||||
const algorithm = 'aes-256-cbc'
|
||||
|
||||
// Key and (optional) salt are passed in, prep them for aes-256
|
||||
const key = Buffer.from(scryptSync(stringKey, salt, 32))
|
||||
|
||||
return {
|
||||
encrypt: (data) => {
|
||||
|
||||
/*
|
||||
* This will encrypt almost anything, but undefined we cannot encrypt.
|
||||
* We could side-step this by assigning a default to data, but that would
|
||||
* lead to confusing bugs when people thing they pass in data and instead
|
||||
* get an encrypted default. So instead, let's bail out loudly
|
||||
*/
|
||||
if (typeof data === 'undefined') throw("Undefined cannot be uncrypted")
|
||||
|
||||
/*
|
||||
* With undefined out of the way, there's still thing we cannot encrypt.
|
||||
* Essentially, anything that can't be serialized to JSON, such as functions.
|
||||
* So let's catch the JSON.stringify() call and once again bail out if things
|
||||
* go off the rails here.
|
||||
*/
|
||||
try {
|
||||
data = JSON.stringify(data)
|
||||
}
|
||||
catch (err) {
|
||||
throw('Could not parse input to encrypt() call', err)
|
||||
}
|
||||
|
||||
/*
|
||||
* Even with the same salt, this initialization vector avoid that two
|
||||
* identical input strings would generate the same cyphertext
|
||||
*/
|
||||
const iv = randomBytes(16)
|
||||
|
||||
/*
|
||||
* The thing that does the thing
|
||||
*/
|
||||
const cipher = createCipheriv(algorithm, key, iv)
|
||||
|
||||
// Always return a string so we can store this in SQLite no problemo
|
||||
return JSON.stringify({
|
||||
iv: iv.toString('hex'),
|
||||
encrypted: Buffer.concat([cipher.update(data), cipher.final()]).toString('hex'),
|
||||
})
|
||||
},
|
||||
decrypt: (data) => {
|
||||
/*
|
||||
* Don't blindly assume this data is properly formatted cyphertext
|
||||
*/
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
}
|
||||
catch (err) {
|
||||
throw('Could not parse encrypted data in decrypt() call', err)
|
||||
}
|
||||
if (!data.iv || typeof data.encrypted === 'undefined') {
|
||||
throw('Encrypted data passed to decrypt() was malformed')
|
||||
}
|
||||
|
||||
/*
|
||||
* The thing that does the thing
|
||||
*/
|
||||
const decipher = createDecipheriv(algorithm, key, Buffer.from(data.iv, 'hex'))
|
||||
|
||||
// Parse this string as JSON so we return what was passed to encrypt()
|
||||
return JSON.parse(
|
||||
Buffer.concat([
|
||||
decipher.update(Buffer.from(data.encrypted, 'hex')),
|
||||
decipher.final()
|
||||
]).toString('utf-8')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
import { User, Person, Pattern } from '../models'
|
||||
import crypto from 'crypto'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import mailer from './email'
|
||||
import logger from './log'
|
||||
import config from '../config'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import sharp from 'sharp'
|
||||
import avatar from '../templates/avatar'
|
||||
|
||||
export const email = mailer
|
||||
export const log = logger
|
||||
|
||||
export const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1)
|
||||
|
||||
export const createUrl = (language, path) => {
|
||||
// Handle development mode
|
||||
if (config.api.indexOf('localhost') !== -1) return 'http://localhost:8000' + path
|
||||
else return config.website.scheme + '://' + language + '.' + config.website.domain + path
|
||||
}
|
||||
|
||||
export const getHash = (email) => {
|
||||
let hash = crypto.createHash('sha256')
|
||||
hash.update(clean(email))
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
export const clean = (email) => email.toLowerCase().trim()
|
||||
|
||||
export const getToken = (account) => {
|
||||
return jwt.sign(
|
||||
{
|
||||
_id: account._id,
|
||||
handle: account.handle,
|
||||
aud: config.jwt.audience,
|
||||
iss: config.jwt.issuer,
|
||||
},
|
||||
config.jwt.secretOrKey
|
||||
)
|
||||
}
|
||||
|
||||
export const getHandle = (type) => {
|
||||
let go, handle, exists
|
||||
if (type === 'person') go = Person
|
||||
else if (type === 'pattern') go = Pattern
|
||||
else go = User
|
||||
do {
|
||||
exists = false
|
||||
handle = createHandle()
|
||||
go.findOne({ handle: handle }, (err, result) => {
|
||||
if (result !== null) exists = true
|
||||
})
|
||||
} while (exists !== false)
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const createHandle = (length = 5) => {
|
||||
let handle = ''
|
||||
let possible = 'abcdefghijklmnopqrstuvwxyz'
|
||||
for (let i = 0; i < length; i++)
|
||||
handle += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const imageType = (contentType) => {
|
||||
if (contentType === 'image/png') return 'png'
|
||||
if (contentType === 'image/jpeg') return 'jpg'
|
||||
if (contentType === 'image/gif') return 'gif'
|
||||
if (contentType === 'image/bmp') return 'bmp'
|
||||
if (contentType === 'image/webp') return 'webp'
|
||||
}
|
||||
|
||||
export const saveAvatarFromBase64 = (data, handle, type) => {
|
||||
fs.mkdir(userStoragePath(handle), { recursive: true }, (err) => {
|
||||
if (err) log.error('mkdirFailed', err)
|
||||
let imgBuffer = Buffer.from(data, 'base64')
|
||||
for (let size of Object.keys(config.avatar.sizes)) {
|
||||
sharp(imgBuffer)
|
||||
.resize(config.avatar.sizes[size], config.avatar.sizes[size])
|
||||
.toFile(avatarPath(size, handle, type), (err, info) => {
|
||||
if (err) log.error('avatarNotSaved', err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const avatarPath = (size, handle, ext, type = 'user') => {
|
||||
let dir = userStoragePath(handle)
|
||||
if (size === 'l') return path.join(dir, handle + '.' + ext)
|
||||
else return path.join(dir, size + '-' + handle + '.' + ext)
|
||||
}
|
||||
|
||||
export const randomColor = () => (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6)
|
||||
|
||||
export const randomAvatar = () =>
|
||||
avatar.replace('000000', randomColor()).replace('FFFFFF', randomColor())
|
||||
|
||||
export const ehash = (email) => {
|
||||
let hash = crypto.createHash('sha256')
|
||||
hash.update(clean(email))
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
export const newHandle = (length = 5) => {
|
||||
let handle = ''
|
||||
let possible = 'abcdefghijklmnopqrstuvwxyz'
|
||||
for (let i = 0; i < length; i++)
|
||||
handle += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const uniqueHandle = () => {
|
||||
let handle, exists
|
||||
do {
|
||||
exists = false
|
||||
handle = newHandle()
|
||||
User.findOne({ handle: handle }, (err, user) => {
|
||||
if (user !== null) exists = true
|
||||
})
|
||||
} while (exists !== false)
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const userStoragePath = (handle) =>
|
||||
path.join(config.storage, 'users', handle.substring(0, 1), handle)
|
||||
|
||||
export const createAvatar = (handle) => {
|
||||
let dir = userStoragePath(handle)
|
||||
fs.mkdir(dir, { recursive: true }, (err) => {
|
||||
if (err) console.log('mkdirFailed', dir, err)
|
||||
fs.writeFile(path.join(dir, handle) + '.svg', randomAvatar(), (err) => {
|
||||
if (err) console.log('writeFileFailed', dir, err)
|
||||
})
|
||||
})
|
||||
}
|
12
sites/backend/src/utils/index.mjs
Normal file
12
sites/backend/src/utils/index.mjs
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { createHash } from 'node:crypto'
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
// Cleans an email for hashing
|
||||
export const clean = (email) => email.toLowerCase().trim()
|
||||
|
||||
// Hashes an email address
|
||||
export const ehash = (email) => createHash('sha256').update(clean(email)).digest('hex')
|
||||
|
||||
// Get key from environment
|
||||
const secret = createHash('sha256').update(process.env.API_ENC_KEY).digest()
|
|
@ -1,349 +0,0 @@
|
|||
module.exports = function tests(store, config, chai) {
|
||||
describe('Non language-specific User controller signup routes', () => {
|
||||
it('should not create signup without email address', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/signup')
|
||||
.send({
|
||||
password: config.user.password,
|
||||
language: 'en',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(400)
|
||||
res.text.should.equal('emailMissing')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create signup without password', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/signup')
|
||||
.send({
|
||||
email: config.user.email,
|
||||
language: 'en',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(400)
|
||||
res.text.should.equal('passwordMissing')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create signup without language', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/signup')
|
||||
.send({
|
||||
email: config.user.email,
|
||||
password: config.user.password,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(400)
|
||||
res.text.should.equal('languageMissing')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Login/Logout and session handling', () => {
|
||||
it('should login with the username', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/login')
|
||||
.send({
|
||||
username: config.user.username,
|
||||
password: config.user.password,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.username.should.equal(config.user.username)
|
||||
data.token.should.be.a('string')
|
||||
config.user.token = data.token
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should login with the email address', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/login')
|
||||
.send({
|
||||
username: config.user.email,
|
||||
password: config.user.password,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.username.should.equal(config.user.username)
|
||||
data.token.should.be.a('string')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should load account with JSON Web Token', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.get('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.end((err, res) => {
|
||||
if (err) console.log(err)
|
||||
let data = JSON.parse(res.text)
|
||||
res.should.have.status(200)
|
||||
data.account.username.should.equal(config.user.username)
|
||||
// Enable this once cleanup is implemented
|
||||
//Object.keys(data.recipes).length.should.equal(0)
|
||||
//Object.keys(data.people).length.should.equal(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Account management', () => {
|
||||
it('should update the account avatar', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
avatar: config.avatar,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.pictureUris.l.slice(-4).should.equal('.png')
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('should update the account username', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: config.user.username + '_updated',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.username.should.equal(config.user.username + '_updated')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should restore the account username', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: config.user.username,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.username.should.equal(config.user.username)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not update the account username if that username is taken', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: 'admin',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(400)
|
||||
res.text.should.equal('usernameTaken')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the account bio', (done) => {
|
||||
let bio = 'This is the test bio '
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
bio: bio,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.bio.should.equal(bio)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the account language', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
settings: {
|
||||
language: 'nl',
|
||||
},
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.settings.language.should.equal('nl')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the account units', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
settings: {
|
||||
units: 'imperial',
|
||||
},
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.settings.units.should.equal('imperial')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
for (let network of ['github', 'twitter', 'instagram']) {
|
||||
it(`should update the account's ${network} username`, (done) => {
|
||||
let data = { social: {} }
|
||||
data.social[network] = network
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send(data)
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
JSON.parse(res.text).account.social[network].should.equal(network)
|
||||
done()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
it('should update the account password', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
password: 'changeme',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should login with the new password', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/login')
|
||||
.send({
|
||||
username: config.user.username,
|
||||
password: 'changeme',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should restore the account password', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
password: config.user.password,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Other user endpoints', () => {
|
||||
it("should load a user's profile", (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.get('/users/admin')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.username.should.equal('admin')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should confirm that a username is available', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/available/username')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: Date.now() + ' ' + Date.now(),
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should confirm that a username is not available', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/available/username')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: 'admin',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(400)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should load the patron list', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.get('/patrons')
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data['2'].should.be.an('array')
|
||||
data['4'].should.be.an('array')
|
||||
data['8'].should.be.an('array')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should export the user data', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.get('/account/export')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.export.should.be.a('string')
|
||||
store.exportLink = data.export
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
371
sites/backend/tests/user.test.mjs
Normal file
371
sites/backend/tests/user.test.mjs
Normal file
|
@ -0,0 +1,371 @@
|
|||
import chai from 'chai'
|
||||
import http from 'chai-http'
|
||||
import { verifyConfig } from '../src/config.mjs'
|
||||
|
||||
const config = verifyConfig()
|
||||
const expect = chai.expect
|
||||
chai.use(http)
|
||||
|
||||
describe('Non language-specific User controller signup routes', () => {
|
||||
|
||||
it('Should return 400 on signup without body', (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.post('/signup')
|
||||
.send()
|
||||
.end((err, res) => {
|
||||
expect(err === null).to.equal(true)
|
||||
expect(res.status).to.equal(400)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
let data = {
|
||||
email: 'test@freesewing.org',
|
||||
language: 'en',
|
||||
password: 'one two one two, this is just a test'
|
||||
}
|
||||
Object.keys(data).map(key => {
|
||||
it(`Should not create signup without ${key}`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.post('/signup')
|
||||
.send(Object.fromEntries(Object.keys(data).filter(name => name !== key).map(name => [name, data[name]])))
|
||||
.end((err, res) => {
|
||||
expect(err === null).to.equal(true)
|
||||
expect(res.status).to.equal(400)
|
||||
expect(res.text).to.equal(`${key}Missing`)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create signup without password', (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.post('/signup')
|
||||
.send(data)
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.equal(400)
|
||||
done()
|
||||
})
|
||||
})
|
||||
/*
|
||||
|
||||
it('should not create signup without language', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/signup')
|
||||
.send({
|
||||
email: config.user.email,
|
||||
password: config.user.password,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(400)
|
||||
res.text.should.equal('languageMissing')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Login/Logout and session handling', () => {
|
||||
it('should login with the username', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/login')
|
||||
.send({
|
||||
username: config.user.username,
|
||||
password: config.user.password,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.username.should.equal(config.user.username)
|
||||
data.token.should.be.a('string')
|
||||
config.user.token = data.token
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should login with the email address', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/login')
|
||||
.send({
|
||||
username: config.user.email,
|
||||
password: config.user.password,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.username.should.equal(config.user.username)
|
||||
data.token.should.be.a('string')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should load account with JSON Web Token', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.get('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.end((err, res) => {
|
||||
if (err) console.log(err)
|
||||
let data = JSON.parse(res.text)
|
||||
res.should.have.status(200)
|
||||
data.account.username.should.equal(config.user.username)
|
||||
// Enable this once cleanup is implemented
|
||||
//Object.keys(data.recipes).length.should.equal(0)
|
||||
//Object.keys(data.people).length.should.equal(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Account management', () => {
|
||||
it('should update the account avatar', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
avatar: config.avatar,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.pictureUris.l.slice(-4).should.equal('.png')
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('should update the account username', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: config.user.username + '_updated',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.username.should.equal(config.user.username + '_updated')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should restore the account username', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: config.user.username,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.username.should.equal(config.user.username)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not update the account username if that username is taken', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: 'admin',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(400)
|
||||
res.text.should.equal('usernameTaken')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the account bio', (done) => {
|
||||
let bio = 'This is the test bio '
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
bio: bio,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.bio.should.equal(bio)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the account language', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
settings: {
|
||||
language: 'nl',
|
||||
},
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.settings.language.should.equal('nl')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the account units', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
settings: {
|
||||
units: 'imperial',
|
||||
},
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.account.settings.units.should.equal('imperial')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
for (let network of ['github', 'twitter', 'instagram']) {
|
||||
it(`should update the account's ${network} username`, (done) => {
|
||||
let data = { social: {} }
|
||||
data.social[network] = network
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send(data)
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
JSON.parse(res.text).account.social[network].should.equal(network)
|
||||
done()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
it('should update the account password', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
password: 'changeme',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should login with the new password', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/login')
|
||||
.send({
|
||||
username: config.user.username,
|
||||
password: 'changeme',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should restore the account password', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.put('/account')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
password: config.user.password,
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Other user endpoints', () => {
|
||||
it("should load a user's profile", (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.get('/users/admin')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.username.should.equal('admin')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should confirm that a username is available', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/available/username')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: Date.now() + ' ' + Date.now(),
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should confirm that a username is not available', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.post('/available/username')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.send({
|
||||
username: 'admin',
|
||||
})
|
||||
.end((err, res) => {
|
||||
res.should.have.status(400)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should load the patron list', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.get('/patrons')
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data['2'].should.be.an('array')
|
||||
data['4'].should.be.an('array')
|
||||
data['8'].should.be.an('array')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should export the user data', (done) => {
|
||||
chai
|
||||
.request(config.backend)
|
||||
.get('/account/export')
|
||||
.set('Authorization', 'Bearer ' + config.user.token)
|
||||
.end((err, res) => {
|
||||
res.should.have.status(200)
|
||||
let data = JSON.parse(res.text)
|
||||
data.export.should.be.a('string')
|
||||
store.exportLink = data.export
|
||||
done()
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue