1
0
Fork 0

tada: Initial commit

This commit is contained in:
Joost De Cock 2018-10-19 16:25:35 +02:00 committed by Joost De Cock
parent 19300b5352
commit 3529e9e4ee
32 changed files with 6714 additions and 1 deletions

View file

@ -0,0 +1,13 @@
# 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

67
packages/backend/.gitignore vendored Normal file
View file

@ -0,0 +1,67 @@
# Compiled code
build
dist
tests/dist
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
coverage.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next

View file

@ -1,2 +1,28 @@
<p align="center">
<a title="Go to freesewing.org" href="https://freesewing.org/"><img src="https://freesewing.org/img/logo/black.svg" align="center" width="150px" alt="Freesewing logo"/></a>
</p>
<h4 align="center"><em>&nbsp;<a title="Go to freesewing.org" href="https://freesewing.org/">freesewing</a></em>
<br><sup>a library for made-to-measure sewing patterns</sup>
</h4>
<p align="center">
<a href="https://gitter.im/freesewing/freesewing"><img src="https://badgen.net/badge/chat/on%20Gitter/cyan" alt="Chat on Gitter"></a>
<a href="https://freesewing.org/patrons/join"><img src="https://badgen.net/badge/become/a%20Patron/FF5B77" alt="Become a Patron"></a>
</p>
# backend # backend
The future backend of freesewing
[Freesewing](https://freesewing.org/) is an open source platform for made-to-measure sewing pattern.
This is our backend API.
## Links
- 💻 Website: [freesewing.org](https://freesewing.org)
- 💬 Chat: [Gitter](https://gitter.im/freesewing/freesewing)
- 🐦 Twitter: [@freesewing_org](https://twitter.com/freesewing_org)
- 📷 Instagram: [@freesewing_org](https://instagram.com/freesewing_org)
## Getting started
This is a REST API built with Express, and currently a work in progress.
If you have questions, please join [our chatroom](https://gitter.im/freesewing/freesewing).

5804
packages/backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
{
"name": "@freesewing/backend",
"version": "0.0.1",
"description": "The freesewing.org backend",
"main": "dist/index.js",
"module": "dist/index.mjs",
"scripts": {
"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": "echo \"Error: no test specified\" && exit 1",
"clean": "rimraf dist",
"pretty": "npx prettier --write \"src/*.js\"",
"lint": "eslint --fix \"src/*.js\"",
"dev": "backpack",
"build": "backpack build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/freesewing/backend.git"
},
"author": "Joost De Cock",
"license": "MIT",
"bugs": {
"url": "https://github.com/freesewing/backend/issues"
},
"homepage": "https://github.com/freesewing/backend#readme",
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,json}": [
"prettier --write",
"git add"
]
},
"dependencies": {
"body-parser": "1.18.3",
"chalk": "2.4.1",
"cors": "2.8.4",
"dateformat": "3.0.3",
"express": "4.16.4",
"mongoose": "5.3.3",
"mongoose-bcrypt": "1.6.0",
"mongoose-encryption": "2.0.1",
"passport": "0.4.0"
},
"devDependencies": {
"backpack-core": "0.7.0"
}
}

View file

@ -0,0 +1,3 @@
const uri = process.env.FS_MONGO_URI || 'mongodb://localhost/freesewing';
export default { uri }

View file

@ -0,0 +1,3 @@
const key = process.env.MONGO_ENC_KEY;
export default { key }

View file

@ -0,0 +1,5 @@
import db from "./db";
import languages from "./languages";
import encryption from "./encryption";
export default { db, languages, encryption }

View file

@ -0,0 +1 @@
export default ["en", "de", "es", "fr", "nl"];

View file

@ -0,0 +1,16 @@
const comment = {};
// CRUD basics
comment.create = (req, res) => { }
comment.read = (req, res) => { }
comment.update = (req, res) => { }
comment.delete = (req, res) => { }
// Page or recent comments
comment.pageComments = (req, res) => { }
comment.recentComments = (req, res) => { }
// Webhook
comment.replyFromEmail = (req, res) => { }
export default comment;

View file

@ -0,0 +1,10 @@
const draft = {};
// CRUD basics
draft.create = (req, res) => { }
draft.read = (req, res) => { }
draft.readShared = (req, res) => { }
draft.update = (req, res) => { }
draft.delete = (req, res) => { }
export default draft;

View file

@ -0,0 +1,12 @@
const model = {};
// CRUD basics
model.create = (req, res) => { }
model.read = (req, res) => { }
model.update = (req, res) => { }
model.delete = (req, res) => { }
// Clone
model.clone = (req, res) => { }
export default model;

View file

@ -0,0 +1,6 @@
const referral = {};
// CRUD basics
referral.create = (req, res) => { }
export default referral;

View file

@ -0,0 +1,82 @@
import { User } from "../models";
import crypto from "crypto";
import bcrypt from "bcryptjs";
import { log } from "../utils";
const userController = {};
// Login
userController.login = (req, res) => {
if (!req.body) return res.sendStatus(400);
User.findOne({
$or: [
{ username: req.body.username },
{ ehash: ehash(req.body.username) }
]
}, (err, user) => {
if (err) return res.sendStatus(400);
if(user === null) return res.sendStatus(401);
user.verifyPassword(req.body.password, (err, valid) => {
if (err) return res.sendStatus(400);
else if (valid) {
log.info('login', { user, req });
user.updateLoginTime(() => res.send(user.account()));
} else {
log.warning('wrongPassword', { user, req });
return res.sendStatus(401);
}
});
});
}
// CRUD basics
userController.create = (req, res) => { }
userController.readAccount = (req, res) => { }
userController.readOwnProfile = (req, res) => { }
userController.readProfile = (req, res) => { }
userController.update = (req, res) => { }
userController.delete = (req, res) => { }
// Signup flow
userController.signup = (req, res) => { }
userController.confirmSignupEmail = (req, res) => { }
userController.removeConfirmation = (req, res) => { }
userController.resendActivationEmail = (req, res) => { }
// Reset/recover/change email
userController.recoverPassword = (req, res) => { }
userController.resetPassword = (req, res) => { }
userController.confirmChangedEmail = (req, res) => { }
// Other
userController.patronList = (req, res) => { }
userController.exportData = (req, res) => { }
userController.findOne = (req, res) => {
User.find({"username":"joost"})
.then( users => {
res.send(users);
}).catch(err => {
res.status(500).send({
message: err.message || "An error occurred."
});
});
}
const clean = (email) => email.toLowerCase().trim();
const ehash = (email) => {
let hash = crypto.createHash("sha256");
hash.update(clean(email));
return hash.digest("hex");
}
const passwordMatches = async (password, hash) => {
let match = await bcrypt.compare(password, hash);
return match;
}
export default userController;

View file

@ -0,0 +1,57 @@
import express from "express";
import mongoose from "mongoose";
import chalk from "chalk";
import config from "./config";
import middleware from "./middleware";
import routes from "./routes";
const app = express();
// Load middleware
for (let type of Object.keys(middleware)) middleware[type](app);
// Load routes
for (let type of Object.keys(routes)) routes[type](app);
// 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();
});
app.get("/", async (req, res) => {
try {
const thing = await Promise.resolve({ one: "two" }); // async/await!
return res.json({ ...thing, hello: "world" }); // object-rest-spread!
} catch (e) {
return res.json({ error: e.message });
}
});
const port = process.env.PORT || 3000;
app.listen(port, err => {
if (err) {
console.error(err);
}
if (__DEV__) {
// webpack flags!
console.log("> in development");
}
console.log(`> listening on port ${port}`);
});

View file

@ -0,0 +1,9 @@
import bodyParser from "body-parser";
export default (app) => {
// application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }));
// application/json
app.use(bodyParser.json());
}

View file

@ -0,0 +1,5 @@
import cors from "cors";
export default (app) => {
app.use(cors());
}

View file

@ -0,0 +1,4 @@
import bodyParser from "./bodyParser";
import cors from "./cors";
export default { bodyParser, cors }

View file

@ -0,0 +1,39 @@
import mongoose, { Schema } from "mongoose";
const CommentSchema = new Schema({
id: {
type: Number,
required: true,
unique: true,
index: true
},
user: {
type: String,
required: true,
lowercase: true,
trim: true,
index: true
},
page: {
type: String,
required: true,
lowercase: true,
index: true,
trim: true
},
comment: {
type: String,
trim: true
},
parent: Number,
time: Date,
status: {
type: String,
enum: ["active", "removed", "restricted"],
default: "active"
}
},{ timestamps: true });
CommentSchema.index({ id: 1, user: 1 , page: 1});
export default mongoose.model('Comment', CommentSchema);

View file

@ -0,0 +1,8 @@
import mongoose from "mongoose";
import CommentModel from "./comment";
import ModelModel from "./model";
import UserModel from "./user";
export const Model = ModelModel;
export const Comment = CommentModel;
export const User = UserModel;

View file

@ -0,0 +1,72 @@
import mongoose, { Schema } from "mongoose";
const ModelSchema = 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: String,
units: {
type: String,
enum: ["metric", "imperial"],
default: "metric"
},
created: Date,
notes: {
type: String,
trim: true
},
measurements: {
acrossBack: Number,
bicepsCircumference: Number,
bustSpan: Number,
centerBackNeckToWaist: Number,
chestCircumference: Number,
headCircumference: Number,
highBust: Number,
highPointShoulderToBust: Number,
hipsCircumference: Number,
hipsToUpperLeg: Number,
inseam: Number,
naturalWaist: Number,
naturalWaistToFloor: Number,
naturalWaistToHip: Number,
naturalWaistToKnee: Number,
naturalWaistToSeat: Number,
naturalWaistToUnderbust: Number,
neckCircumference: Number,
seatCircumference: Number,
seatDepth: Number,
shoulderSlope: Number,
shoulderToElbow: Number,
shoulderToShoulder: Number,
shoulderToWrist: Number,
underBust: Number,
upperLegCircumference: Number,
wristCircumference: Number
}
},{ timestamps: true });
ModelSchema.index({ user: 1 , handle: 1});
export default mongoose.model('Model', ModelSchema);

View file

@ -0,0 +1,160 @@
import mongoose, { Schema } from "mongoose";
import bcrypt from 'mongoose-bcrypt';
import { email, log } from "../utils";
import encrypt from 'mongoose-encryption';
import config from "../config";
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,
},
patron: {
type: Number,
enum: [0, 2, 4, 8],
default: 0
},
bio: {
type: String,
default: ""
},
picture: {
type: String,
trim: true,
},
status: {
type: String,
enum: ["active", "blocked", "frozen"],
default: "active",
required: true
},
password: {
type: String,
required: true
},
settings: {
language: {
type: String,
default: "en",
enum: config.languages,
},
units: {
type: String,
enum: ["metric", "imperial"],
default: "metric"
}
},
consent: {
profile: {
type: Boolean,
default: false
},
model: {
type: Boolean,
default: false
},
openData: {
type: Boolean,
default: true
}
},
time: {
created: Date,
migrated: Date,
login: Date,
patron: Date
},
social: {
twitter: String,
instagram: String,
github: String
}
},{ timestamps: true });
UserSchema.pre('save', function(next) {
if (!this.isNew) next();
mailer({
type: 'welcome',
email: this.email
})
.then(() => { next(); })
.catch(err => {
logger.error(err);
next();
});
});
UserSchema.pre('remove', function(next) {
mailer({
type: 'goodbye',
email: this.email
})
.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: false
});
UserSchema.methods.account = function() {
let account = this.toObject();
delete account.password;
delete account.ehash;
return account;
}
UserSchema.methods.updateLoginTime = function(callback) {
this.set({time: {login: new Date()}});
this.save(function(err, user) {
return callback();
});
}
export default mongoose.model('User', UserSchema);

View file

@ -0,0 +1,20 @@
import admin from "../controllers/admin";
export default (app) => {
// Impersonate user
app.get('/admin/impersonate/:handle', admin.impersonate);
/* User cRUD endpoints */
app.get('/admin/user/{handle}', admin.readUser); // Read
app.put('/admin/user/{handle}', admin.updateUser); // Update
app.delete('/admin/user/{handle}', admin.deleteUser); // Delete
// Find users
app.get('/admin/find/users/:filter', admin.findUsers);
}

View file

@ -0,0 +1,35 @@
import comment from "../controllers/comment";
export default (app) => {
/**********************************************
* *
* ANONYMOUS ROUTES *
* *
*********************************************/
// Webhook: Reply to comment via email
app.post('/webhook/comment/reply', comment.replyFromEmail);
// Load page comments
app.get('/comments/page/:page', comment.pageComments);
// Load recent comments
app.get('/comments/recent/:count', comment.recentComments);
/**********************************************
* *
* AUTHENTICATED ROUTES *
* *
*********************************************/
/* CRUD endpoints */
app.post('/comment', comment.create); // Create
app.get('/comment/:id', comment.read); // Read
app.put('/comment/:id', comment.update); // Update
app.delete('/comment/:id', comment.delete); // Delete
}

View file

@ -0,0 +1,32 @@
import draft from "../controllers/draft";
export default (app) => {
/**********************************************
* *
* ANONYMOUS ROUTES *
* *
*********************************************/
// Load shared draft
app.get('/shared/draft/:handle', draft.readShared);
/**********************************************
* *
* AUTHENTICATED ROUTES *
* *
*********************************************/
/* CRUD endpoints */
app.post('/draft', draft.create); // Create
app.get('/draft/:handle', draft.read); // Read
app.put('/draft/:handle', draft.update); // Update
app.delete('/draft/:handle', draft.delete); // Delete
}

View file

@ -0,0 +1,7 @@
import comment from "./comment";
import draft from "./draft";
import model from "./model";
import referral from "./referral";
import user from "./user";
export default { comment, user, draft, model, referral }

View file

@ -0,0 +1,33 @@
import model from "../controllers/model";
export default (app) => {
/**********************************************
* *
* ANONYMOUS ROUTES *
* *
*********************************************/
/**********************************************
* *
* AUTHENTICATED ROUTES *
* *
*********************************************/
/* CRUD endpoints */
app.post('/model', model.create); // Create
app.get('/model/:handle', model.read); // Read
app.put('/model/:handle', model.update); // Update
app.delete('/model/:handle', model.delete); // Delete
// Clone model
app.post('/clone/model/:handle', model.clone);
}

View file

@ -0,0 +1,11 @@
import referral from "../controllers/referral";
export default (app) => {
// Log referral
app.post('/referral', referral.create);
}

View file

@ -0,0 +1,77 @@
import userController from "../controllers/user";
export default (app) => {
app.get('/user', userController.findOne);
/**********************************************
* *
* ANONYMOUS ROUTES *
* *
*********************************************/
/* Sign-up flow */
// Sign up user
app.post('/signup', userController.signup);
// Resend user activation email
app.post('/resend/activation/email', userController.resendActivationEmail);
// Create account from confirmation / Consent for data processing given
app.post('/user', userController.create);
// Remove confirmation / No consent for data processing given
app.delete('/remove/confirmation/:token', userController.removeConfirmation);
/* Login flow */
// User login
app.post('/login', userController.login);
// Recover user password
app.post('/recover/password', userController.recoverPassword);
// Reset user password
app.post('/reset/password', userController.resetPassword);
/* Email confirmation endpoints */
// (these are always GET because they are links in an email)
// Confirm email address at signup
app.get('/confirm/signup/email/:token', userController.confirmSignupEmail);
// Confirm user email change
app.get('/confirm/changed/email:handle/:token', userController.confirmChangedEmail);
/* Email confirmation endpoints */
// Load patron list
app.get('/patrons/list', userController.patronList);
/**********************************************
* *
* AUTHENTICATED ROUTES *
* *
*********************************************/
/* CRUD endpoints */
app.get('/account', userController.readAccount); // Read account (own data)
app.get('/user', userController.readOwnProfile); // Read profile (own data)
app.get('/user/:handle', userController.readProfile); // Read profile (own data)
// Create is a non-authenticated route part of sign-up flow
app.put('/user', userController.update); // Update
app.delete('/user', userController.delete); // Delete
// Export data
app.get('/export', userController.exportData);
}

View file

@ -0,0 +1,5 @@
const email = (data) => {
console.log("FIXME: Send email", data);
}
export default email;

View file

@ -0,0 +1,5 @@
import mailer from "./email";
import logger from "./log";
export const email = mailer;
export const log = logger;

View file

@ -0,0 +1,32 @@
import dateFormat from "dateformat";
// FIXME: This needs work
const now = () => dateFormat(new Date(), "yyyy-mm-dd hh:MM:ss");
const logWorthy = (msg, data) => {
let d = {at: now()};
switch(msg) {
case 'login':
case 'wrongPassword':
d.user = data.user.handle;
d.from = data.req.ip;
d.with = data.req.headers['user-agent'];
break;
}
return d;
}
const log = (type, msg, data) => {
console.log(type, msg, logWorthy(msg, data));
}
log.info = (msg, data) => log('info', msg, data);
log.warning = (msg, data) => log('warning', msg, data);
log.error = (msg, data) => log('error', msg, data);
export default log;