tada: Initial commit
This commit is contained in:
parent
19300b5352
commit
3529e9e4ee
32 changed files with 6714 additions and 1 deletions
13
packages/backend/.editorconfig
Normal file
13
packages/backend/.editorconfig
Normal 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
67
packages/backend/.gitignore
vendored
Normal 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
|
|
@ -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> <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
5804
packages/backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
54
packages/backend/package.json
Normal file
54
packages/backend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
3
packages/backend/src/config/db.js
Normal file
3
packages/backend/src/config/db.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const uri = process.env.FS_MONGO_URI || 'mongodb://localhost/freesewing';
|
||||||
|
|
||||||
|
export default { uri }
|
3
packages/backend/src/config/encryption.js
Normal file
3
packages/backend/src/config/encryption.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const key = process.env.MONGO_ENC_KEY;
|
||||||
|
|
||||||
|
export default { key }
|
5
packages/backend/src/config/index.js
Normal file
5
packages/backend/src/config/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import db from "./db";
|
||||||
|
import languages from "./languages";
|
||||||
|
import encryption from "./encryption";
|
||||||
|
|
||||||
|
export default { db, languages, encryption }
|
1
packages/backend/src/config/languages.js
Normal file
1
packages/backend/src/config/languages.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export default ["en", "de", "es", "fr", "nl"];
|
16
packages/backend/src/controllers/comment.js
Normal file
16
packages/backend/src/controllers/comment.js
Normal 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;
|
10
packages/backend/src/controllers/draft.js
Normal file
10
packages/backend/src/controllers/draft.js
Normal 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;
|
12
packages/backend/src/controllers/model.js
Normal file
12
packages/backend/src/controllers/model.js
Normal 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;
|
6
packages/backend/src/controllers/referral.js
Normal file
6
packages/backend/src/controllers/referral.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const referral = {};
|
||||||
|
|
||||||
|
// CRUD basics
|
||||||
|
referral.create = (req, res) => { }
|
||||||
|
|
||||||
|
export default referral;
|
82
packages/backend/src/controllers/user.js
Normal file
82
packages/backend/src/controllers/user.js
Normal 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;
|
57
packages/backend/src/index.js
Normal file
57
packages/backend/src/index.js
Normal 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}`);
|
||||||
|
});
|
9
packages/backend/src/middleware/bodyParser.js
Normal file
9
packages/backend/src/middleware/bodyParser.js
Normal 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());
|
||||||
|
}
|
5
packages/backend/src/middleware/cors.js
Normal file
5
packages/backend/src/middleware/cors.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import cors from "cors";
|
||||||
|
|
||||||
|
export default (app) => {
|
||||||
|
app.use(cors());
|
||||||
|
}
|
4
packages/backend/src/middleware/index.js
Normal file
4
packages/backend/src/middleware/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import bodyParser from "./bodyParser";
|
||||||
|
import cors from "./cors";
|
||||||
|
|
||||||
|
export default { bodyParser, cors }
|
39
packages/backend/src/models/comment.js
Normal file
39
packages/backend/src/models/comment.js
Normal 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);
|
8
packages/backend/src/models/index.js
Normal file
8
packages/backend/src/models/index.js
Normal 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;
|
72
packages/backend/src/models/model.js
Normal file
72
packages/backend/src/models/model.js
Normal 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);
|
160
packages/backend/src/models/user.js
Normal file
160
packages/backend/src/models/user.js
Normal 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);
|
20
packages/backend/src/routes/admin.js
Normal file
20
packages/backend/src/routes/admin.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
35
packages/backend/src/routes/comment.js
Normal file
35
packages/backend/src/routes/comment.js
Normal 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
|
||||||
|
|
||||||
|
}
|
32
packages/backend/src/routes/draft.js
Normal file
32
packages/backend/src/routes/draft.js
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
7
packages/backend/src/routes/index.js
Normal file
7
packages/backend/src/routes/index.js
Normal 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 }
|
33
packages/backend/src/routes/model.js
Normal file
33
packages/backend/src/routes/model.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
11
packages/backend/src/routes/referral.js
Normal file
11
packages/backend/src/routes/referral.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import referral from "../controllers/referral";
|
||||||
|
|
||||||
|
export default (app) => {
|
||||||
|
// Log referral
|
||||||
|
app.post('/referral', referral.create);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
77
packages/backend/src/routes/user.js
Normal file
77
packages/backend/src/routes/user.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
5
packages/backend/src/utils/email/index.js
Normal file
5
packages/backend/src/utils/email/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const email = (data) => {
|
||||||
|
console.log("FIXME: Send email", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default email;
|
5
packages/backend/src/utils/index.js
Normal file
5
packages/backend/src/utils/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import mailer from "./email";
|
||||||
|
import logger from "./log";
|
||||||
|
|
||||||
|
export const email = mailer;
|
||||||
|
export const log = logger;
|
32
packages/backend/src/utils/log/index.js
Normal file
32
packages/backend/src/utils/log/index.js
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue