├── .babelrc ├── server ├── api │ ├── config.js │ ├── index.js │ └── routes │ │ ├── token.js │ │ ├── auth.js │ │ └── users.js ├── index.js ├── db │ ├── config.js │ └── index.js ├── auth │ ├── strategies │ │ ├── client-basic.js │ │ ├── bearer.js │ │ └── facebook.js │ ├── oauth2.js │ └── index.js ├── middleware │ └── index.js └── models │ ├── client.js │ ├── access-token.js │ └── user.js ├── .gitignore ├── index.js ├── test ├── index.js └── api │ ├── index.js │ └── routes │ └── users.js ├── .eslintrc ├── README.md ├── bin └── www.js ├── LICENSE └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /server/api/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const prefix = '/api'; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | .idea 4 | node_modules 5 | server/auth/provider.js 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-register'); 4 | require('babel-polyfill'); 5 | 6 | require('./bin/www'); 7 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-register'); 4 | require('babel-polyfill'); 5 | 6 | require('./api'); 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "no-console": 0, 6 | "max-len": [1, 80, 2], 7 | "no-param-reassign": [2, { "props": false }] 8 | }, 9 | "env": { 10 | "mocha": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Koa from 'koa'; 4 | 5 | import middleware from './middleware'; 6 | import auth from './auth'; 7 | import api from './api'; 8 | 9 | const app = new Koa(); 10 | 11 | app.keys = ['secret']; 12 | 13 | app.use(middleware()); 14 | app.use(auth()); 15 | app.use(api()); 16 | app.use(ctx => ctx.status = 404); 17 | 18 | export default app; 19 | -------------------------------------------------------------------------------- /server/db/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const development = 'mongodb://localhost/koa-rest-api'; 4 | 5 | export const test = 'mongodb://localhost/koa-rest-api-test'; 6 | 7 | export const localClient = { 8 | name: 'local', 9 | id: 'local', 10 | secret: 'local', 11 | }; 12 | 13 | export const adminUser = { 14 | name: 'Admin', 15 | email: 'matthias.fey@tu-dortmund.de', 16 | password: 'adminadmin', 17 | }; 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starter project for an ES6 RESTFul Koa2 API with Mongoose and OAuth2 2 | 3 | currently in development... 4 | 5 | ## OAuth2 Provider 6 | 7 | You need to create a `provider.js` file in `server/auth` and add your oAuth2 provider credentials, like: 8 | 9 | ```js 10 | 'use strict'; 11 | 12 | export const facebook = { 13 | clientId: YOUR_CLIENT_ID, 14 | clientSecret: YOUR_CLIENT_SECRET, 15 | route: '/auth/facebook', 16 | callbackRoute: '/auth/facebook/callback', 17 | }; 18 | ``` 19 | -------------------------------------------------------------------------------- /server/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import compose from 'koa-compose'; 4 | import Router from 'koa-router'; 5 | import importDir from 'import-dir'; 6 | import { prefix } from './config'; 7 | 8 | const routes = importDir('./routes'); 9 | 10 | export default function api() { 11 | const router = new Router({ prefix }); 12 | 13 | Object.keys(routes).forEach(name => routes[name](router)); 14 | 15 | return compose([ 16 | router.routes(), 17 | router.allowedMethods(), 18 | ]); 19 | } 20 | -------------------------------------------------------------------------------- /server/auth/strategies/client-basic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { BasicStrategy } from 'passport-http'; 4 | import Client from '../../models/client'; 5 | 6 | export default new BasicStrategy((id, secret, done) => { 7 | (async () => { 8 | try { 9 | const client = await Client.findOne({ id }); 10 | 11 | if (!client) return done(null, false); 12 | 13 | if (secret !== client.secret) return done(null, false); 14 | 15 | return done(null, client); 16 | } catch (error) { 17 | return done(error); 18 | } 19 | })(); 20 | }); 21 | -------------------------------------------------------------------------------- /server/middleware/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import compose from 'koa-compose'; 4 | import convert from 'koa-convert'; 5 | import logger from 'koa-logger'; 6 | import helmet from 'koa-helmet'; 7 | import cors from 'koa-cors'; 8 | import bodyParser from 'koa-bodyparser'; 9 | import session from 'koa-generic-session'; 10 | 11 | export default function middleware() { 12 | return compose([ 13 | logger(), 14 | helmet(), // reset HTTP headers (e.g. remove x-powered-by) 15 | convert(cors()), 16 | convert(bodyParser()), 17 | convert(session()), 18 | ]); 19 | } 20 | -------------------------------------------------------------------------------- /server/auth/strategies/bearer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Strategy as BearerStrategy } from 'passport-http-bearer'; 4 | import AccessToken from '../../models/access-token'; 5 | 6 | export default new BearerStrategy(async (token, done) => { 7 | (async () => { 8 | try { 9 | const accessToken = await AccessToken 10 | .findOne({ token }) 11 | .populate('user') 12 | .exec(); 13 | 14 | if (!accessToken) return done(null, false); 15 | 16 | return done(null, accessToken.user, { scope: '*' }); 17 | } catch (error) { 18 | return done(error); 19 | } 20 | })(); 21 | }); 22 | -------------------------------------------------------------------------------- /bin/www.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import app from '../server'; 4 | import { 5 | connectDatabase, 6 | registerLocalClient, 7 | registerAdminUser, 8 | } from '../server/db'; 9 | import { development } from '../server/db/config'; 10 | 11 | const port = process.env.PORT || 3000; 12 | 13 | (async() => { 14 | try { 15 | const info = await connectDatabase(development); 16 | console.log(`Connected to ${info.host}:${info.port}/${info.name}`); 17 | } catch (error) { 18 | console.error('Unable to connect to database'); 19 | } 20 | 21 | try { 22 | await registerLocalClient(); 23 | await registerAdminUser(); 24 | 25 | await app.listen(port); 26 | console.log(`Server started on port ${port}`); 27 | } catch (error) { 28 | console.log(error); 29 | } 30 | })(); 31 | -------------------------------------------------------------------------------- /server/api/routes/token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import AccessToken from '../../models/access-token'; 4 | import { isBearerAuthenticated } from '../../auth'; 5 | 6 | export default (router) => { 7 | router 8 | .get('/token', 9 | isBearerAuthenticated(), 10 | async ctx => { 11 | const accessToken = await AccessToken.findOne({ 12 | user: ctx.passport.user._id, 13 | }); 14 | if (accessToken) { 15 | ctx.body = { 16 | access_token: accessToken, 17 | token_type: 'Bearer', 18 | }; 19 | } 20 | } 21 | ) 22 | .delete('/token', 23 | isBearerAuthenticated(), 24 | async ctx => { 25 | await AccessToken.findOneAndRemove({ user: ctx.passport.user._id }); 26 | ctx.status = 204; 27 | } 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /server/api/routes/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { token } from '../../auth/oauth2'; 4 | import AccessToken from '../../models/access-token'; 5 | import { 6 | isFacebookAuthenticated, 7 | isFacebookAuthenticatedCallback, 8 | } from '../../auth'; 9 | import * as provider from '../../auth/provider'; 10 | 11 | export default (router) => { 12 | router 13 | .post('/auth', token()); 14 | 15 | router 16 | .get(provider.facebook.route, isFacebookAuthenticated()) 17 | .get(provider.facebook.callbackRoute, 18 | isFacebookAuthenticatedCallback(), 19 | async ctx => { 20 | const accessToken = await AccessToken.findOne({ 21 | user: ctx.passport.user._id, 22 | }); 23 | 24 | ctx.body = { 25 | access_token: accessToken, 26 | token_type: 'Bearer', 27 | }; 28 | } 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /test/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import importDir from 'import-dir'; 4 | import supertest from 'supertest-as-promised'; 5 | import mongoose from 'mongoose'; 6 | import chai from 'chai'; 7 | import app from '../../server'; 8 | import { 9 | connectDatabase, 10 | registerLocalClient, 11 | registerAdminUser, 12 | } from '../../server/db'; 13 | import { test } from '../../server/db/config'; 14 | 15 | const routes = importDir('./routes'); 16 | const request = supertest.agent(app.listen()); 17 | chai.should(); 18 | 19 | describe('Routes', () => { 20 | before(async () => { 21 | await connectDatabase(test); 22 | }); 23 | 24 | beforeEach(async () => { 25 | Object.keys(mongoose.models).forEach(async name => { 26 | await mongoose.model(name).remove(); 27 | }); 28 | 29 | await registerLocalClient(); 30 | await registerAdminUser(); 31 | }); 32 | 33 | Object.keys(routes).forEach(name => routes[name](request)); 34 | }); 35 | -------------------------------------------------------------------------------- /server/auth/strategies/facebook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Strategy as FacebookStrategy } from 'passport-facebook'; 4 | import { facebook } from '../provider'; 5 | import User from '../../models/user'; 6 | import AccessToken from '../../models/access-token'; 7 | 8 | export default new FacebookStrategy({ 9 | clientID: facebook.clientId, 10 | clientSecret: facebook.clientSecret, 11 | profileFields: ['displayName', 'email'], 12 | }, (accessToken, refreshToken, profile, done) => { 13 | (async () => { 14 | try { 15 | const email = profile._json.email; 16 | 17 | let user = await User.findOne({ email }); 18 | if (!user) { 19 | user = await User.create({ 20 | email, 21 | name: profile.displayName, 22 | provider: 'facebook', 23 | }); 24 | } 25 | 26 | await AccessToken.findOneAndRemove({ user: user._id }); 27 | 28 | await AccessToken.create({ 29 | user: user._id, 30 | }); 31 | 32 | return done(null, user); 33 | } catch (error) { 34 | return done(error); 35 | } 36 | })(); 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Matthias Fey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /server/models/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import mongoose from 'mongoose'; 4 | import uid from 'uid'; 5 | import idValidator from 'mongoose-id-validator'; 6 | 7 | const clientSchema = new mongoose.Schema({ 8 | name: { 9 | type: String, 10 | unique: true, 11 | required: true, 12 | }, 13 | id: { 14 | type: String, 15 | unique: true, 16 | required: true, 17 | }, 18 | secret: { 19 | type: String, 20 | required: true, 21 | }, 22 | trusted: { 23 | type: Boolean, 24 | required: true, 25 | default: false, 26 | }, 27 | user: { 28 | type: mongoose.Schema.Types.ObjectId, 29 | ref: 'User', 30 | }, 31 | }, { 32 | versionKey: false, 33 | timestamps: { 34 | createdAt: 'created_at', 35 | updatedAt: 'updated_at', 36 | }, 37 | toJSON: { 38 | transform(doc, ret) { 39 | delete ret._id; 40 | delete ret.hashed_secret; 41 | }, 42 | }, 43 | }); 44 | 45 | clientSchema.plugin(idValidator); 46 | 47 | clientSchema.pre('validate', function preSave(next) { 48 | if (this.isNew) { 49 | if (!this.id) this.id = uid(16); 50 | if (!this.id) this.secret = uid(32); 51 | } 52 | next(); 53 | }); 54 | 55 | export default mongoose.model('Client', clientSchema); 56 | -------------------------------------------------------------------------------- /server/api/routes/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import User from '../../models/user'; 4 | import { isBearerAuthenticated } from '../../auth'; 5 | 6 | export default (router) => { 7 | router 8 | .get('/users', 9 | async ctx => ctx.body = await User.find({})) 10 | .post('/users', async ctx => { 11 | ctx.body = await User.create({ 12 | name: ctx.request.body.name, 13 | email: ctx.request.body.email, 14 | password: ctx.request.body.password, 15 | confirm_password: ctx.request.body.confirm_password, 16 | }); 17 | }) 18 | .get('/users/:id', 19 | async ctx => { 20 | const user = await User.findById(ctx.params.id); 21 | if (user) ctx.body = user; 22 | } 23 | ) 24 | .put('/users/:id', async ctx => { 25 | const user = await User.findByIdAndUpdate(ctx.params.id, { 26 | name: ctx.request.body.name, 27 | }, { 28 | new: true, 29 | runValidators: true, 30 | }); 31 | if (user) ctx.body = user; 32 | }) 33 | .delete('/users/:id', 34 | isBearerAuthenticated(), 35 | async ctx => { 36 | const user = await User.findByIdAndRemove(ctx.params.id); 37 | if (user) ctx.status = 204; 38 | } 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /server/auth/oauth2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import oauth2orize from 'oauth2orize-koa'; 4 | import Client from '../models/client'; 5 | import User from '../models/user'; 6 | import AccessToken from '../models/access-token'; 7 | import compose from 'koa-compose'; 8 | import bcrypt from 'bcrypt-as-promised'; 9 | import { isClientAuthenticated } from '../auth'; 10 | 11 | const server = oauth2orize.createServer(); 12 | 13 | server.serializeClient(client => client._id); 14 | server.deserializeClient(async id => await Client.findById(id)); 15 | 16 | server.exchange( 17 | oauth2orize.exchange.password(async (client, email, password) => { 18 | if (!client.trusted) return false; 19 | 20 | const user = await User.findOne({ email: email.toLowerCase() }); 21 | 22 | if (!user) return false; 23 | 24 | const isMatch = await bcrypt.compare(password, user.hashed_password); 25 | if (!isMatch) return false; 26 | 27 | await AccessToken.findOneAndRemove({ user: user._id }); 28 | 29 | const accessToken = await AccessToken.create({ 30 | user: user._id, 31 | client: client._id, 32 | }); 33 | 34 | return accessToken; 35 | })); 36 | 37 | export function token() { 38 | return compose([ 39 | isClientAuthenticated(), 40 | async (ctx, next) => { 41 | ctx.state.user = ctx.passport.user; 42 | await next(); 43 | }, 44 | server.token(), 45 | server.errorHandler(), 46 | ]); 47 | } 48 | -------------------------------------------------------------------------------- /server/models/access-token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import mongoose from 'mongoose'; 4 | import uid from 'uid'; 5 | import idValidator from 'mongoose-id-validator'; 6 | 7 | const duration = 3600; 8 | 9 | const accessTokenSchema = new mongoose.Schema({ 10 | token: { 11 | type: String, 12 | unique: true, 13 | required: true, 14 | }, 15 | user: { 16 | type: mongoose.Schema.Types.ObjectId, 17 | ref: 'User', 18 | unique: true, 19 | required: true, 20 | }, 21 | client: { 22 | type: mongoose.Schema.Types.ObjectId, 23 | ref: 'Client', 24 | }, 25 | created_at: { 26 | type: Date, 27 | expires: duration, 28 | }, 29 | }, { 30 | versionKey: false, 31 | toJSON: { 32 | virtuals: true, 33 | transform(doc, ret) { 34 | delete ret._id; 35 | delete ret.user; 36 | delete ret.client; 37 | delete ret.id; 38 | }, 39 | }, 40 | }); 41 | 42 | accessTokenSchema.virtual('expires_in') 43 | .get(function getExpiresIn() { 44 | const expirationTime = this.created_at.getTime() + (duration * 1000); 45 | return parseInt((expirationTime - Date.now()) / 1000, 10); 46 | }); 47 | 48 | accessTokenSchema.plugin(idValidator); 49 | 50 | accessTokenSchema.pre('validate', function preSave(next) { 51 | if (this.isNew) { 52 | this.token = uid(194); 53 | this.created_at = Date.now(); 54 | } 55 | next(); 56 | }); 57 | 58 | export default mongoose.model('AccessToken', accessTokenSchema); 59 | -------------------------------------------------------------------------------- /server/auth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import passport from 'koa-passport'; 4 | import compose from 'koa-compose'; 5 | import importDir from 'import-dir'; 6 | import User from '../models/user'; 7 | import { prefix } from '../api/config'; 8 | import * as provider from './provider'; 9 | 10 | const strategies = importDir('./strategies'); 11 | 12 | Object.keys(strategies).forEach(name => { 13 | passport.use(name, strategies[name]); 14 | }); 15 | 16 | passport.serializeUser((user, done) => done(null, user._id)); 17 | 18 | passport.deserializeUser((id, done) => { 19 | (async () => { 20 | try { 21 | const user = await User.findById(id); 22 | done(null, user); 23 | } catch (error) { 24 | done(error); 25 | } 26 | })(); 27 | }); 28 | 29 | export default function auth() { 30 | return compose([ 31 | passport.initialize(), 32 | passport.session(), 33 | ]); 34 | } 35 | 36 | export function isClientAuthenticated() { 37 | return passport.authenticate('client-basic', { session: false }); 38 | } 39 | 40 | export function isBearerAuthenticated() { 41 | return passport.authenticate('bearer', { session: false }); 42 | } 43 | 44 | const facebookCallbackURL = prefix + provider.facebook.callbackRoute; 45 | 46 | export function isFacebookAuthenticated() { 47 | return passport.authenticate('facebook', { 48 | scope: ['email'], 49 | callbackURL: facebookCallbackURL, 50 | }); 51 | } 52 | 53 | export function isFacebookAuthenticatedCallback() { 54 | return passport.authenticate('facebook', { 55 | failureRedirect: '/login', 56 | callbackURL: facebookCallbackURL, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/api/routes/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const user = { 4 | name: 'Test User', 5 | email: 'test.user@gmail.com'.toUpperCase(), 6 | password: 'secret', 7 | confirm_password: 'secret', 8 | }; 9 | 10 | export default function testUsers(request) { 11 | describe('Users', () => { 12 | it('should create user', async () => { 13 | const res = await request.post('/api/users') 14 | .send(user) 15 | .expect(200) 16 | .expect('Content-Type', /json/); 17 | 18 | Object.keys(res.body).should.have.length(7); 19 | res.body.should.have.property('_id'); 20 | res.body.name.should.equal(user.name); 21 | res.body.email.should.equal(user.email.toLowerCase()); 22 | res.body.provider.should.equal('local'); 23 | res.body.admin.should.equal(false); 24 | res.body.should.have.property('created_at'); 25 | res.body.should.have.property('updated_at'); 26 | }); 27 | 28 | it('should get user', async () => { 29 | let res = await request.post('/api/users') 30 | .send(user) 31 | .expect(200); 32 | 33 | res = await request.get(`/api/users/${res.body._id}`) 34 | .expect(200) 35 | .expect('Content-Type', /json/); 36 | 37 | Object.keys(res.body).should.have.length(7); 38 | res.body.should.have.property('_id'); 39 | res.body.name.should.equal(user.name); 40 | res.body.email.should.equal(user.email.toLowerCase()); 41 | res.body.provider.should.equal('local'); 42 | res.body.admin.should.equal(false); 43 | res.body.should.have.property('created_at'); 44 | res.body.should.have.property('updated_at'); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /server/db/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import mongoose from 'mongoose'; 4 | import { localClient, adminUser } from './config'; 5 | import Client from '../models/client'; 6 | import User from '../models/user'; 7 | 8 | export function connectDatabase(uri) { 9 | return new Promise((resolve, reject) => { 10 | mongoose.connection 11 | .on('error', error => reject(error)) 12 | .on('close', () => console.log('Database connection closed.')) 13 | .once('open', () => resolve(mongoose.connections[0])); 14 | 15 | mongoose.connect(uri); 16 | }); 17 | } 18 | 19 | export async function registerLocalClient() { 20 | return new Promise((resolve, reject) => { 21 | (async () => { 22 | try { 23 | const client = await Client.findOne({ id: localClient.id }); 24 | if (!client) { 25 | await Client.create({ 26 | name: localClient.name, 27 | id: localClient.id, 28 | secret: localClient.secret, 29 | trusted: true, 30 | }); 31 | } 32 | resolve(); 33 | } catch (error) { 34 | reject(error); 35 | } 36 | })(); 37 | }); 38 | } 39 | 40 | export async function registerAdminUser() { 41 | return new Promise((resolve, reject) => { 42 | (async () => { 43 | try { 44 | const user = await User.findOne({ email: adminUser.email }); 45 | if (!user) { 46 | await User.create({ 47 | name: adminUser.name, 48 | email: adminUser.email, 49 | password: adminUser.password, 50 | confirm_password: adminUser.password, 51 | admin: true, 52 | }); 53 | } 54 | resolve(); 55 | } catch (error) { 56 | reject(error); 57 | } 58 | })(); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import mongoose from 'mongoose'; 4 | import validate from 'mongoose-validator'; 5 | import bcrypt from 'bcrypt-as-promised'; 6 | import * as provider from '../auth/provider'; 7 | 8 | const userSchema = new mongoose.Schema({ 9 | name: { 10 | type: String, 11 | trim: true, 12 | required: true, 13 | minlength: 3, 14 | }, 15 | email: { 16 | type: String, 17 | lowercase: true, 18 | unique: true, 19 | required: true, 20 | validate: validate({ 21 | validator: 'isEmail', 22 | message: 'is not valid', 23 | }), 24 | }, 25 | hashed_password: { 26 | type: String, 27 | }, 28 | admin: { 29 | type: Boolean, 30 | required: true, 31 | default: false, 32 | }, 33 | provider: { 34 | type: String, 35 | required: true, 36 | enum: ['local', ...Object.keys(provider)], 37 | default: 'local', 38 | }, 39 | }, { 40 | versionKey: false, 41 | timestamps: { 42 | createdAt: 'created_at', 43 | updatedAt: 'updated_at', 44 | }, 45 | toJSON: { 46 | transform(doc, ret) { 47 | delete ret.hashed_password; 48 | }, 49 | }, 50 | }); 51 | 52 | userSchema.virtual('password') 53 | .set(function setPassword(value) { this._password = value; }) 54 | .get(function getPassword() { return this._password; }); 55 | 56 | userSchema.virtual('confirm_password') 57 | .set(function setConfirmPassword(value) { this._confirm_password = value; }) 58 | .get(function getConfirmPassword() { return this._confirm_password; }); 59 | 60 | userSchema.pre('validate', function preValidate(next) { 61 | if (this.provider !== 'local') return next(); 62 | 63 | if (!this.hashed_password && !this.password) { 64 | this.invalidate('password', 'is required'); 65 | } else if (this.password.length < 6) { 66 | this.invalidate('password', 'must be at least 5 characters'); 67 | } else if (this.password !== this.confirm_password) { 68 | this.invalidate('password', 'doesn\'t match the confirmation password'); 69 | } 70 | 71 | next(); 72 | }); 73 | 74 | userSchema.pre('save', async function preSave(next) { 75 | if (!this.password) return next(); 76 | 77 | try { 78 | this.hashed_password = await bcrypt.hash(this.password); 79 | next(); 80 | } catch (error) { 81 | next(error); 82 | } 83 | }); 84 | 85 | export default mongoose.model('User', userSchema); 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa2-rest-api", 3 | "version": "0.0.0", 4 | "description": "Starter project for an ES6 RESTFul Koa2 API with Mongoose and OAuth2", 5 | "keywords": [ 6 | "node", 7 | "es6", 8 | "es2015", 9 | "babel", 10 | "koa", 11 | "koa2", 12 | "koa v2", 13 | "rest", 14 | "api", 15 | "mongo", 16 | "mongodb", 17 | "mongoose", 18 | "authentification", 19 | "authorization", 20 | "basic", 21 | "facebook", 22 | "google", 23 | "twitter", 24 | "oauth2" 25 | ], 26 | "author": { 27 | "name": "Matthias Fey", 28 | "email": "matthias.fey@tu-dortmund.de", 29 | "url": "http://rusty1s.github.io" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/rusty1s/koa2-rest-api.git" 34 | }, 35 | "license": "MIT", 36 | "licenses": [ 37 | { 38 | "type": "MIT", 39 | "url": "http://www.opensource.org/licenses/MIT" 40 | } 41 | ], 42 | "readmeFilename": "README.md", 43 | "bugs": { 44 | "url": "http://github.com/rusty1s/koa2-rest-api/issues" 45 | }, 46 | "main": "index.js", 47 | "scripts": { 48 | "build": "eslint index.js server/** bin/** test/**", 49 | "start": "nodemon index.js", 50 | "test": "mocha test" 51 | }, 52 | "devDependencies": { 53 | "babel-eslint": "*", 54 | "babel-polyfill": "*", 55 | "babel-preset-es2015": "*", 56 | "babel-preset-stage-0": "*", 57 | "babel-register": "*", 58 | "chai": "^3.5.0", 59 | "eslint": "*", 60 | "eslint-config-airbnb": "*", 61 | "mocha": "*", 62 | "nodemon": "*", 63 | "supertest": "*", 64 | "supertest-as-promised": "*" 65 | }, 66 | "dependencies": { 67 | "bcrypt-as-promised": "^1.1.0", 68 | "import-dir": "0.0.1", 69 | "kerberos": "0.0.18", 70 | "koa": "^2.0.0-alpha.3", 71 | "koa-bodyparser": "^2.0.1", 72 | "koa-compose": "^3.0.0", 73 | "koa-convert": "^1.2.0", 74 | "koa-cors": "0.0.16", 75 | "koa-generic-session": "^1.10.1", 76 | "koa-helmet": "^2.0.0-alpha.1", 77 | "koa-logger": "^2.0.0", 78 | "koa-passport": "^2.0.1", 79 | "koa-router": "^7.0.1", 80 | "mongoose": "^4.3.7", 81 | "mongoose-id-validator": "^0.1.10", 82 | "mongoose-validator": "^1.2.4", 83 | "oauth2orize-koa": "^1.3.2", 84 | "passport-facebook": "^2.1.0", 85 | "passport-http": "^0.3.0", 86 | "passport-http-bearer": "^1.0.1", 87 | "uid": "0.0.2" 88 | } 89 | } 90 | --------------------------------------------------------------------------------