completed changes
This commit is contained in:
parent
1917903b67
commit
bbbbc9f9b8
|
|
@ -0,0 +1,14 @@
|
|||
APP_ENV=dev|stage|production
|
||||
APP_PORT=3000
|
||||
|
||||
SESSION_SECRET=someString
|
||||
JWT_SECRET=someString
|
||||
|
||||
# Databse
|
||||
# YOUR_MONGO_DB_NAME
|
||||
DB_NAME=someString
|
||||
DB_HOST=someString
|
||||
DB_PORT=27017
|
||||
DB_USER=someUser
|
||||
DB_USER_PWD=secret
|
||||
|
||||
|
|
@ -18,3 +18,13 @@
|
|||
#System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment Files
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# logs
|
||||
/logs
|
||||
|
||||
# build
|
||||
/buid
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
import { IUserModel } from '../models/user-model';
|
||||
|
||||
|
||||
export interface IArticle {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
body: string;
|
||||
tagList?: [string];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
favorited: boolean;
|
||||
favoritesCount: number;
|
||||
author: IUserModel;
|
||||
}
|
||||
|
||||
|
||||
export interface IQuery {
|
||||
tagList: {$in: any[]};
|
||||
author: string;
|
||||
_id: {$in: any[]};
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
|
||||
import { Request } from 'express';
|
||||
import { IUserModel } from '../models/user-model';
|
||||
import { IArticleModel } from '../models/article-model';
|
||||
|
||||
|
||||
// Add jwt payload details to Express Request
|
||||
export interface JWTRequest extends Request {
|
||||
payload: {
|
||||
id: string,
|
||||
username: string,
|
||||
exp: number,
|
||||
iat: number
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Add profile details to JWT Request
|
||||
export interface ProfileRequest extends JWTRequest {
|
||||
profile: IUserModel;
|
||||
}
|
||||
|
||||
|
||||
// Add article details to ProfileRequest
|
||||
export interface ArticleRequest extends ProfileRequest {
|
||||
article: IArticleModel;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
|
||||
export interface IUser {
|
||||
email: string;
|
||||
username: string;
|
||||
bio?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IProfile {
|
||||
username: string;
|
||||
bio: string;
|
||||
image: string;
|
||||
following: boolean;
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
|
||||
import { model, Model, Schema, Document } from 'mongoose';
|
||||
import { IArticle } from '../interfaces/article-interface';
|
||||
import { IUserModel, User } from './user-model';
|
||||
|
||||
|
||||
export interface IArticleModel extends IArticle, Document {
|
||||
formatAsArticleJSON(user);
|
||||
}
|
||||
|
||||
|
||||
const ArticleSchema = new Schema({
|
||||
slug: {type: String}, // FIXME: Add , lowercase: true, unique: true
|
||||
title: String,
|
||||
description: String,
|
||||
body: String,
|
||||
tagList: [String],
|
||||
favoritesCount: {type: Number, default: 0},
|
||||
author: {type: Schema.Types.ObjectId, ref: 'User'}
|
||||
}, {timestamps: true});
|
||||
|
||||
|
||||
ArticleSchema.methods.formatAsArticleJSON = function(user: IUserModel) {
|
||||
|
||||
return {
|
||||
slug: this.slug,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
body: this.body,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
tagList: this.tagList,
|
||||
favorited: user ? user.isFavorite(this._id) : false,
|
||||
favoritesCount: this.favoritesCount,
|
||||
author: this.author.formatAsProfileJSON(user)
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const Article: Model<IArticleModel> = model<IArticleModel>('Article', ArticleSchema);
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
|
||||
import { Document, Schema, Model, model } from 'mongoose';
|
||||
import { IUser } from '../interfaces/user-interface';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { jwtSecret } from '../utilities/authentication';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
|
||||
export interface IUserModel extends IUser, Document {
|
||||
token?: string;
|
||||
favorites: [Schema.Types.ObjectId];
|
||||
generateJWT();
|
||||
formatAsUserJSON();
|
||||
setPassword(password);
|
||||
passwordIsValid(password);
|
||||
formatAsProfileJSON(user);
|
||||
isFollowing(id);
|
||||
follow(id);
|
||||
unfollow(id);
|
||||
isFavorite(id);
|
||||
}
|
||||
|
||||
|
||||
// ISSUE: Own every parameter and any missing dependencies
|
||||
const UserSchema = new Schema({
|
||||
username: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"],
|
||||
match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
|
||||
email: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"],
|
||||
match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
|
||||
bio: String,
|
||||
image: String,
|
||||
favorites: [{ type: Schema.Types.ObjectId, ref: 'Article' }],
|
||||
following: [{ type: Schema.Types.ObjectId, ref: 'User' }],
|
||||
hash: String,
|
||||
salt: String
|
||||
}, {timestamps: true});
|
||||
|
||||
|
||||
UserSchema.methods.generateJWT = function(): string {
|
||||
const today = new Date();
|
||||
const exp = new Date(today);
|
||||
exp.setDate(today.getDate() + 60);
|
||||
|
||||
return jwt.sign({
|
||||
id: this._id,
|
||||
username: this.username,
|
||||
exp: exp.getTime() / 1000
|
||||
}, jwtSecret);
|
||||
};
|
||||
|
||||
|
||||
UserSchema.methods.formatAsUserJSON = function() {
|
||||
return {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
token: this.generateJWT(),
|
||||
bio: this.bio,
|
||||
image: this.image
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
UserSchema.methods.setPassword = function(password: string) {
|
||||
this.salt = crypto.randomBytes(16).toString('hex');
|
||||
this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
|
||||
};
|
||||
|
||||
|
||||
UserSchema.methods.passwordIsValid = function(password: string) {
|
||||
const hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
|
||||
return hash === this.hash;
|
||||
};
|
||||
|
||||
|
||||
UserSchema.methods.formatAsProfileJSON = function(user: IUserModel) {
|
||||
return {
|
||||
username: this.username,
|
||||
bio: this.bio,
|
||||
image: this.image || 'https://static.productionready.io/images/smiley-cyrus.jpg',
|
||||
following: user ? user.isFollowing(this._id) : false
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
UserSchema.methods.isFollowing = function(id: Schema.Types.ObjectId): boolean {
|
||||
return this.following.some( (followingId: Schema.Types.ObjectId) => {
|
||||
return followingId.toString() === id.toString();
|
||||
});
|
||||
};
|
||||
// ISSUE: Why did these have to be converted to strings to evaluate correctly?
|
||||
|
||||
|
||||
UserSchema.methods.follow = function(id: Schema.Types.ObjectId) {
|
||||
if (this.following.indexOf(id) === -1) {
|
||||
this.following.push(id);
|
||||
}
|
||||
return this.save();
|
||||
};
|
||||
|
||||
|
||||
UserSchema.methods.unfollow = function(id: Schema.Types.ObjectId) {
|
||||
this.following.remove(id);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
|
||||
UserSchema.methods.isFavorite = function(id: Schema.Types.ObjectId): boolean {
|
||||
return this.following.some( (favoriteId: Schema.Types.ObjectId) => {
|
||||
return favoriteId.toString() === id.toString();
|
||||
});
|
||||
};
|
||||
// ISSUE: Why did these have to be converted to strings to evaluate correctly?
|
||||
|
||||
|
||||
export const User: Model<IUserModel> = model<IUserModel>('User', UserSchema);
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
|
||||
import { Router, NextFunction, Response } from 'express';
|
||||
import { authentication } from '../utilities/authentication';
|
||||
import { ArticleRequest, JWTRequest, ProfileRequest } from '../interfaces/requests-interface';
|
||||
import { Article, IArticleModel } from '../models/article-model';
|
||||
import { IUserModel, User } from '../models/user-model';
|
||||
import { IQuery } from '../interfaces/article-interface';
|
||||
import { Schema } from 'mongoose';
|
||||
import * as slugify from 'slugify';
|
||||
|
||||
const router: Router = Router();
|
||||
const Promise = require('bluebird'); // FIXME: how to handle this in Typescript?
|
||||
|
||||
|
||||
/**
|
||||
* PARAM :slug
|
||||
*/
|
||||
|
||||
router.param('slug', (req: ArticleRequest, res: Response, next: NextFunction, slug: string) => {
|
||||
|
||||
Article
|
||||
.findOne({slug})
|
||||
.populate('author')
|
||||
.then( (article: IArticleModel) => {
|
||||
req.article = article;
|
||||
return next();
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to determine the requesting user (if any)
|
||||
*/
|
||||
// FIXME: Not sure there is a req.profile... make this robust...
|
||||
function establishRequestingUser(req: ProfileRequest): IUserModel {
|
||||
|
||||
// Try to determine the user making the request
|
||||
let thisUser: IUserModel;
|
||||
|
||||
// If authentication was performed was successful look up the profile relative to authenticated user
|
||||
if (req.payload) {
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then( (user: IUserModel) => {
|
||||
return thisUser = user.formatAsProfileJSON(user);
|
||||
})
|
||||
.catch();
|
||||
|
||||
// If authentication was NOT performed or successful look up profile relative to that same user (following = false)
|
||||
} else {
|
||||
thisUser = req.profile;
|
||||
}
|
||||
return thisUser;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/articles
|
||||
*/
|
||||
// FIXME: authorized user who has favorited own articles showing false.
|
||||
// Should show true for all returned 'josh' articles
|
||||
router.get('/', authentication.optional, (req: ProfileRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
// Parse URL query strings and create a query object
|
||||
const limit: number = req.query.limit ? Number(req.query.limit) : 20;
|
||||
const offset: number = req.query.offset ? Number(req.query.offset) : 0;
|
||||
|
||||
const query = <IQuery> {}; // ISSUE: how to resolve?
|
||||
|
||||
// Handle single tag or multiple tags in query (...&tag=git&tag=node...)
|
||||
if (typeof req.query.tag !== 'undefined') {
|
||||
if (Array.isArray(req.query.tag)) {
|
||||
query.tagList = {$in: req.query.tag};
|
||||
} else {
|
||||
query.tagList = {$in: [req.query.tag]};
|
||||
}
|
||||
}
|
||||
|
||||
Promise
|
||||
.all([
|
||||
req.query.author ? User.findOne({username: req.query.author}) : 'noAuthor',
|
||||
req.query.favorited
|
||||
? User
|
||||
.find({username: req.query.favorited})
|
||||
.then(users => {
|
||||
let favoritedArticles: [Schema.Types.ObjectId];
|
||||
users.forEach((user, userIndex) => {
|
||||
user.favorites.forEach((favorite, favoriteIndex) => {
|
||||
if (userIndex === 0 && favoriteIndex === 0) {
|
||||
favoritedArticles = [favorite];
|
||||
} else {
|
||||
favoritedArticles.push(favorite);
|
||||
}
|
||||
});
|
||||
});
|
||||
return favoritedArticles;
|
||||
})
|
||||
: 'noFavorites'
|
||||
])
|
||||
.then( results => {
|
||||
const author = results[0];
|
||||
const favoritedArticleIds = results[1];
|
||||
|
||||
// Return no articles for unknown author, but ignore author filter if none was provided
|
||||
if (author !== 'noAuthor') {
|
||||
query.author = author;
|
||||
}
|
||||
|
||||
/* Restrict the query results to only article IDs that are
|
||||
favorited by the username(s) specified in the query string.
|
||||
|
||||
Note: Choosing to interpret multiple usernames as an 'or' operation,
|
||||
meaning that articles favorited by ANY of the users will be returned,
|
||||
as opposed to an 'and' operation wherein only articles favorited by
|
||||
ALL usernames would be returned.
|
||||
*/
|
||||
if (favoritedArticleIds !== 'noFavorites') {
|
||||
query._id = {$in: favoritedArticleIds};
|
||||
}
|
||||
|
||||
|
||||
// Define promises
|
||||
const p1 = establishRequestingUser(req);
|
||||
|
||||
const p2 = Article.count(query).exec();
|
||||
/* ISSUE: Should count be MIN(count, limit)? or should it count all results,
|
||||
even if not displayed due to limit or offset query string parameter
|
||||
*/
|
||||
|
||||
const p3 =
|
||||
Article
|
||||
.find(query)
|
||||
.limit(limit)
|
||||
.skip(offset) // FIXME: does order matter?
|
||||
.populate('author')
|
||||
.exec();
|
||||
|
||||
// Resolve and use promise results
|
||||
Promise
|
||||
.all([p1, p2, p3])
|
||||
.then(results => {
|
||||
const user: IUserModel = results[0];
|
||||
const articlesCount: number = results[1];
|
||||
const articles = results[2];
|
||||
|
||||
res.json(
|
||||
{articles: articles.map((article: IArticleModel) => {
|
||||
return article.formatAsArticleJSON(user);
|
||||
}),
|
||||
articlesCount});
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* POST /api/articles
|
||||
*/
|
||||
router.post('/', authentication.required, (req: JWTRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
// Examine the request body for completeness
|
||||
const article: IArticleModel = new Article();
|
||||
|
||||
if (typeof req.body.article.title !== 'undefined' &&
|
||||
typeof req.body.article.description !== 'undefined' &&
|
||||
typeof req.body.article.body !== 'undefined') {
|
||||
article.title = req.body.article.title;
|
||||
article.description = req.body.article.description;
|
||||
article.body = req.body.article.body;
|
||||
article.slug = slugify(article.title, {lower: true});
|
||||
} else {
|
||||
res.json('Error in article input: missing title, desc, or body.');
|
||||
}
|
||||
|
||||
if (typeof req.body.article.tagList !== 'undefined') {
|
||||
article.tagList = req.body.article.tagList;
|
||||
}
|
||||
|
||||
// Verify authentication successful, then save and return article
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then(user => {
|
||||
article.author = user;
|
||||
return article.save().then(() => {
|
||||
return res.json({article: article.formatAsArticleJSON(user)});
|
||||
});
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/articles/:slug
|
||||
*/
|
||||
// ISSUE: Possibly not showing following correctly for auth user...
|
||||
router.get('/:slug', authentication.optional, (req: ArticleRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
const user = establishRequestingUser(req);
|
||||
console.log(user);
|
||||
console.log(req.article);
|
||||
|
||||
const article: IArticleModel = req.article;
|
||||
|
||||
if (article) {
|
||||
res.json(article.formatAsArticleJSON(user));
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* DELETE /api/articles/:slug
|
||||
*/
|
||||
router.delete('/:slug', authentication.required, (req: ArticleRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
Article
|
||||
.findOneAndRemove({slug: req.article.slug}, () => {
|
||||
return res.json();
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* PUT /api/articles/:slug
|
||||
*/
|
||||
router.put('/:slug', authentication.required, (req: ArticleRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
// TODO: Remaining routes
|
||||
// GET /api/articles/feed
|
||||
|
||||
// POST /api/articles/:slug/comments
|
||||
// GET /api/articles/:slug/comments
|
||||
// DELETE /api/articles/:slug/comments/:id
|
||||
|
||||
// POST /api/articles/:slug/favorite
|
||||
// DELETE /api/articles/:slug/favorite
|
||||
|
||||
|
||||
export const ArticlesRoutes: Router = router;
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
|
||||
import { NextFunction, Response, Router } from 'express';
|
||||
import { IUserModel, User } from '../models/user-model';
|
||||
import { authentication } from '../utilities/authentication';
|
||||
import { ProfileRequest } from '../interfaces/requests-interface';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
|
||||
/**
|
||||
* PARAM :username
|
||||
*/
|
||||
|
||||
router.param('username', (req: ProfileRequest, res: Response, next: NextFunction, username: string) => {
|
||||
|
||||
User
|
||||
.findOne({username})
|
||||
.then( (user: IUserModel) => {
|
||||
req.profile = user;
|
||||
return next();
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/profiles/:username
|
||||
*/
|
||||
router.get('/:username', authentication.optional, (req: ProfileRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
// If authentication was performed and was successful look up the profile relative to authenticated user
|
||||
if (req.payload) {
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then( (user: IUserModel) => {
|
||||
res.status(200).json({profile: req.profile.formatAsProfileJSON(user)});
|
||||
})
|
||||
.catch(next);
|
||||
|
||||
// If authentication was NOT performed or successful look up profile relative to that same user (following = false)
|
||||
} else {
|
||||
res.status(200).json({profile: req.profile.formatAsProfileJSON(req.profile)});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* POST /api/profiles/:username/follow
|
||||
*/
|
||||
router.post('/:username/follow', authentication.required, (req: ProfileRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
const profileId = req.profile._id;
|
||||
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
user.follow(profileId);
|
||||
return user.save().then(() => {
|
||||
return res.json({profile: req.profile.formatAsProfileJSON(user)});
|
||||
});
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* DELETE /api/profiles/:username/follow
|
||||
*/
|
||||
router.delete('/:username/follow', authentication.required, (req: ProfileRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
const profileId = req.profile._id;
|
||||
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
user.unfollow(profileId);
|
||||
return user.save().then(() => {
|
||||
return res.json({profile: req.profile.formatAsProfileJSON(user)});
|
||||
});
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
export const ProfilesRoutes: Router = router;
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
import { Article } from '../models/article-model';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
|
||||
// FIXME: Rewrite to pull from Articles...
|
||||
router.get('/', (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
Article
|
||||
.find()
|
||||
.distinct('tagList')
|
||||
.then((tagsArray: [string]) => {
|
||||
return res.json({tags: tagsArray});
|
||||
})
|
||||
.catch(next);
|
||||
|
||||
});
|
||||
|
||||
|
||||
export const TagRoutes: Router = router;
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
|
||||
import { IUserModel, User } from '../models/user-model';
|
||||
import { authentication } from '../utilities/authentication';
|
||||
import { NextFunction, Response, Router } from 'express';
|
||||
import { JWTRequest } from '../interfaces/requests-interface';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/user
|
||||
*/
|
||||
router.get('/', authentication.required, (req: JWTRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
res.status(200).json({user: user.formatAsUserJSON()});
|
||||
}
|
||||
)
|
||||
.catch(next);
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* PUT /api/user
|
||||
*/
|
||||
router.put('/', authentication.required, (req: JWTRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
// Update only fields that have values:
|
||||
// ISSUE: DRY out code?
|
||||
if (typeof req.body.user.email !== 'undefined' ) {
|
||||
user.email = req.body.user.email;
|
||||
}
|
||||
if (typeof req.body.user.username !== 'undefined') {
|
||||
user.username = req.body.user.username;
|
||||
}
|
||||
if (typeof req.body.user.password !== 'undefined') {
|
||||
user.setPassword(req.body.user.password);
|
||||
}
|
||||
if (typeof req.body.user.image !== 'undefined') {
|
||||
user.image = req.body.user.image;
|
||||
}
|
||||
if (typeof req.body.user.bio !== 'undefined') {
|
||||
user.bio = req.body.user.bio;
|
||||
}
|
||||
|
||||
return user.save().then( () => {
|
||||
return res.json({user: user.formatAsUserJSON()});
|
||||
});
|
||||
})
|
||||
.catch(next);
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
|
||||
export const UserRoutes: Router = router;
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
|
||||
import { Router, Response, NextFunction, Request } from 'express';
|
||||
import { IUserModel, User } from '../models/user-model';
|
||||
import * as passport from 'passport';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
|
||||
/**
|
||||
* POST /api/users
|
||||
*/
|
||||
router.post('/', (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
const user: IUserModel = new User();
|
||||
|
||||
user.username = req.body.user.username;
|
||||
user.email = req.body.user.email;
|
||||
user.setPassword(req.body.user.password);
|
||||
user.bio = '';
|
||||
user.image = '';
|
||||
|
||||
return user.save()
|
||||
.then( () => {
|
||||
return res.json({user: user.formatAsUserJSON()});
|
||||
})
|
||||
.catch(next);
|
||||
|
||||
});
|
||||
|
||||
|
||||
// ISSUE: How does this work with the trailing (req, res, next)?
|
||||
/**
|
||||
* POST /api/users/login
|
||||
*/
|
||||
router.post('/login', (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
if (!req.body.user.email) {
|
||||
return res.status(422).json( {errors: {email: "Can't be blank"}} );
|
||||
}
|
||||
|
||||
if (!req.body.user.password) {
|
||||
return res.status(422).json( {errors: {password: "Can't be blank"}} );
|
||||
}
|
||||
|
||||
passport.authenticate('local', {session: false}, (err, user, info) => {
|
||||
if (err) { return next(err); }
|
||||
|
||||
if (user) {
|
||||
user.token = user.generateJWT();
|
||||
return res.json({user: user.formatAsUserJSON()});
|
||||
|
||||
} else {
|
||||
return res.status(422).json(info);
|
||||
}
|
||||
})(req, res, next);
|
||||
|
||||
});
|
||||
|
||||
|
||||
export const UsersRoutes: Router = router;
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
|
||||
import { Request } from 'express';
|
||||
import * as jwt from 'express-jwt';
|
||||
|
||||
|
||||
export const jwtSecret = process.env.NODE_ENV === 'production' ? process.env.SECRET : 'secret';
|
||||
|
||||
|
||||
function getTokenFromHeader(req: Request): string | null {
|
||||
|
||||
const headerAuth: string | string[] = req.headers.authorization;
|
||||
|
||||
if (headerAuth !== undefined && headerAuth !== null) {
|
||||
|
||||
if (Array.isArray(headerAuth)) {
|
||||
return splitToken(headerAuth[0]);
|
||||
} else {
|
||||
return splitToken(headerAuth);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function splitToken(authString: string) {
|
||||
|
||||
if (authString.split(' ')[0] === 'Token') {
|
||||
return authString.split(' ')[1];
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const auth = {
|
||||
required: jwt({
|
||||
credentialsRequired: true,
|
||||
secret: jwtSecret,
|
||||
getToken: getTokenFromHeader,
|
||||
userProperty: 'payload'}),
|
||||
|
||||
optional: jwt({
|
||||
credentialsRequired: false,
|
||||
secret: jwtSecret,
|
||||
getToken: getTokenFromHeader,
|
||||
userProperty: 'payload'})
|
||||
};
|
||||
|
||||
|
||||
// TODO: What was this for?
|
||||
// function isRevokedCallback() {
|
||||
//
|
||||
// }
|
||||
|
||||
|
||||
export const authentication = auth;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
|
||||
import * as mongoose from 'mongoose';
|
||||
import * as Bluebird from 'bluebird';
|
||||
|
||||
|
||||
// Use bluebird promises in lieu of mongoose promises throughout application.
|
||||
(mongoose as any).Promise = Bluebird;
|
||||
|
||||
|
||||
export function connectToMongoDB() {
|
||||
|
||||
const dbUri = 'mongodb://localhost:27017/conduit';
|
||||
|
||||
mongoose.connect(dbUri, {
|
||||
useMongoClient: true
|
||||
});
|
||||
|
||||
mongoose.set('debug', true); // FIXME: Allow for dev only
|
||||
|
||||
mongoose.connection.on('connected', () => {
|
||||
console.log('Mongoose connected to ' + dbUri);
|
||||
});
|
||||
|
||||
mongoose.connection.on('disconnected', () => {
|
||||
console.log('Mongoose disconnected');
|
||||
});
|
||||
|
||||
mongoose.connection.on('error', err => {
|
||||
console.log('Mongoose connection error: ' + err);
|
||||
});
|
||||
|
||||
// ADDITIONAL PROCESS EVENTS FOR UNIX MACHINES ONLY:
|
||||
|
||||
// CTRL-C
|
||||
process.on('SIGINT', () => {
|
||||
mongoose.connection.close(() => {
|
||||
console.log('Mongoose disconnected through app termination (SIGINT)');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Used on services on Heroku
|
||||
process.on('SIGTERM', () => {
|
||||
mongoose.connection.close(() => {
|
||||
console.log('Mongoose disconnected through app termination (SIGTERM)');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Node restart
|
||||
process.once('SIGUSR2', () => {
|
||||
mongoose.connection.close(() => {
|
||||
console.log('Mongoose disconnected through app termination (SIGUSR2)');
|
||||
process.kill(process.pid, 'SIGUSR2');
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
|
||||
import { Application } from 'express';
|
||||
|
||||
export function loadErrorHandlers(app: Application) {
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// catch 404 errors and forward to error handler
|
||||
app.use( (req, res, next) => {
|
||||
|
||||
interface BetterError extends Error {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
const err: BetterError = new Error('Not Found');
|
||||
err.status = 404;
|
||||
next(err);
|
||||
});
|
||||
|
||||
// Dev error handler
|
||||
if (!isProduction) {
|
||||
app.use( (err, req, res, next) => {
|
||||
console.log(err.stack);
|
||||
|
||||
res.status(err.status || 500);
|
||||
|
||||
res.json({errors: {
|
||||
message: err.message,
|
||||
error: err
|
||||
}});
|
||||
});
|
||||
}
|
||||
|
||||
// Production error handler (no stack traces displayed)
|
||||
app.use( (err, req, res, next) => {
|
||||
|
||||
res.status(err.status || 500);
|
||||
|
||||
res.json({errors: {
|
||||
message: err.message,
|
||||
error: {}
|
||||
}});
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
|
||||
import * as passport from 'passport';
|
||||
import { User } from '../models/user-model';
|
||||
import * as passportLocal from 'passport-local';
|
||||
|
||||
|
||||
const LocalStrategy = passportLocal.Strategy;
|
||||
|
||||
|
||||
passport.use(new LocalStrategy( {
|
||||
|
||||
// Strategy is based on username & password. Substitute email for username.
|
||||
usernameField: 'user[email]',
|
||||
passwordField: 'user[password]'},
|
||||
|
||||
(email, password, done) => {
|
||||
|
||||
User
|
||||
.findOne( {email})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
return done(null, false, { message: 'Incorrect email.' });
|
||||
}
|
||||
if (!user.passwordIsValid(password)) {
|
||||
return done(null, false, { message: 'Incorrect password.' });
|
||||
}
|
||||
return done(null, user);
|
||||
})
|
||||
.catch(done);
|
||||
}));
|
||||
30
app.ts
30
app.ts
|
|
@ -1,30 +0,0 @@
|
|||
|
||||
import * as express from 'express';
|
||||
import { Application } from 'express';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import { MainRouter } from './api/routes/index';
|
||||
import { connectToMongoDB } from './api/utilities/database';
|
||||
import { loadErrorHandlers } from './api/utilities/error-handling';
|
||||
import './api/utilities/passport';
|
||||
// FIXME: Sort out passport stuff...
|
||||
// import * as passport from 'passport';
|
||||
import * as session from 'express-session';
|
||||
|
||||
|
||||
const app: Application = express();
|
||||
|
||||
|
||||
connectToMongoDB();
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(session({ secret: 'conduit', cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false }));
|
||||
// app.use(passport.initialize());
|
||||
// app.use(passport.session());
|
||||
app.use('/api', MainRouter);
|
||||
|
||||
loadErrorHandlers(app);
|
||||
|
||||
|
||||
const server = app.listen( 3000, () => {
|
||||
console.log('Listening on port ' + server.address().port);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
70
package.json
70
package.json
|
|
@ -2,10 +2,16 @@
|
|||
"name": "typescript-node-express-realworld-example-app",
|
||||
"version": "0.0.0",
|
||||
"description": "> ### [YOUR_FRAMEWORK] codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API.",
|
||||
"main": "app.ts",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "nodemon --exec 'ts-node' ./app.ts"
|
||||
"start": "npm run build && npm run watch && npm run serve",
|
||||
"serve": "node build/server.js",
|
||||
"build": "npm run build-ts",
|
||||
"watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"",
|
||||
"watch-node": "nodemon -r dotenv/config build/server.js",
|
||||
"build-ts": "tsc",
|
||||
"watch-ts": "tsc -w",
|
||||
"test": "newman run ./tests/api-tests.postman.json -e ./tests/env-api-tests.postman.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -18,29 +24,45 @@
|
|||
},
|
||||
"homepage": "https://",
|
||||
"devDependencies": {
|
||||
"@types/bluebird": "^3.5.8",
|
||||
"@types/express": "^4.0.37",
|
||||
"@types/express-jwt": "0.0.37",
|
||||
"@types/express-session": "^1.15.3",
|
||||
"@types/jsonwebtoken": "^7.2.3",
|
||||
"@types/mongodb": "^2.2.11",
|
||||
"@types/mongoose": "^4.7.21",
|
||||
"@types/passport": "^0.3.4",
|
||||
"@types/passport-local": "^1.0.31",
|
||||
"nodemon": "^1.11.0"
|
||||
"@types/body-parser": "1.19.0",
|
||||
"@types/compression": "1.7.0",
|
||||
"@types/cors": "2.8.6",
|
||||
"@types/express": "4.17.6",
|
||||
"@types/express-jwt": "0.0.42",
|
||||
"@types/express-serve-static-core": "4.17.8",
|
||||
"@types/express-session": "^1.17.0",
|
||||
"@types/helmet": "0.0.47",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/mongodb": "^3.5.25",
|
||||
"@types/mongoose": "^5.7.31",
|
||||
"@types/mongoose-unique-validator": "1.0.4",
|
||||
"@types/node": "14.0.22",
|
||||
"@types/passport": "^1.0.4",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
"newman": "^3.8.2",
|
||||
"nodemon": "2.0.4",
|
||||
"ts-node": "8.10.2",
|
||||
"tslint": "6.1.2",
|
||||
"typescript": "^3.9.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"express": "^4.15.4",
|
||||
"express-jwt": "^5.3.0",
|
||||
"express-session": "^1.15.5",
|
||||
"jsonwebtoken": "^7.4.3",
|
||||
"mongoose": "^4.11.9",
|
||||
"passport": "^0.4.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "1.7.4",
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-jwt": "^6.0.0",
|
||||
"express-session": "^1.17.1",
|
||||
"helmet": "^3.23.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.19",
|
||||
"mongoose": "^5.9.23",
|
||||
"mongoose-unique-validator": "2.0.3",
|
||||
"passport": "^0.4.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"slugify": "^1.2.1",
|
||||
"ts-node": "^3.3.0",
|
||||
"typescript": "^2.5.2"
|
||||
"slugify": "^1.4.4",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import express from 'express';
|
||||
import { Application } from 'express';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import { MainRouter } from './routes';
|
||||
import { loadErrorHandlers } from './utilities/error-handling';
|
||||
import session from 'express-session';
|
||||
import helmet from "helmet";
|
||||
import compression from "compression";
|
||||
import { SESSION_SECRET } from "./utilities/secrets";
|
||||
import './database'; // initialize database
|
||||
import './utilities/passport'
|
||||
|
||||
|
||||
|
||||
const app: Application = express();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(compression());
|
||||
app.use(bodyParser.json());
|
||||
app.use(session({
|
||||
secret: SESSION_SECRET,
|
||||
cookie: {
|
||||
maxAge: 60000
|
||||
},
|
||||
resave : false,
|
||||
saveUninitialized: false
|
||||
}
|
||||
));
|
||||
app.use('/api', MainRouter);
|
||||
|
||||
loadErrorHandlers(app);
|
||||
|
||||
|
||||
export default app;
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import mongoose from 'mongoose';
|
||||
import logger from "../utilities/logger";
|
||||
import { DB } from "../utilities/secrets";
|
||||
|
||||
// Build the connection string
|
||||
const dbURI = `mongodb://${DB.USER}:${encodeURIComponent(DB.PASSWORD)}@${DB.HOST}:${DB.PORT}/${
|
||||
DB.NAME
|
||||
}`;
|
||||
|
||||
const options = {
|
||||
useNewUrlParser : true,
|
||||
useCreateIndex : true,
|
||||
useUnifiedTopology: true,
|
||||
useFindAndModify : false,
|
||||
autoIndex : true,
|
||||
poolSize : 10, // Maintain up to 10 socket connections
|
||||
// If not connected, return errors immediately rather than waiting for reconnect
|
||||
bufferMaxEntries : 0,
|
||||
connectTimeoutMS : 10000, // Give up initial connection after 10 seconds
|
||||
socketTimeoutMS : 45000, // Close sockets after 45 seconds of inactivity
|
||||
};
|
||||
|
||||
logger.debug(dbURI);
|
||||
|
||||
// Create the database connection
|
||||
mongoose
|
||||
.connect(dbURI, options)
|
||||
.then(() => {
|
||||
logger.info('Mongoose connection done');
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.info('Mongoose connection error');
|
||||
logger.error(e);
|
||||
});
|
||||
|
||||
// CONNECTION EVENTS
|
||||
// When successfully connected
|
||||
mongoose.connection.on('connected', () => {
|
||||
logger.info('Mongoose default connection open to ' + dbURI);
|
||||
});
|
||||
|
||||
// If the connection throws an error
|
||||
mongoose.connection.on('error', (err) => {
|
||||
logger.error('Mongoose default connection error: ' + err);
|
||||
});
|
||||
|
||||
// When the connection is disconnected
|
||||
mongoose.connection.on('disconnected', () => {
|
||||
logger.info('Mongoose default connection disconnected');
|
||||
});
|
||||
|
||||
// If the Node process ends, close the Mongoose connection
|
||||
process.on('SIGINT', () => {
|
||||
mongoose.connection.close(() => {
|
||||
logger.info('Mongoose default connection disconnected through app termination');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { Document, Model, model, Schema } from 'mongoose';
|
||||
import { IArticle } from '../../interfaces/article-interface';
|
||||
import IUserModel, { User } from './user.model';
|
||||
import mongooseUniqueValidator from 'mongoose-unique-validator';
|
||||
import slugify from "slugify";
|
||||
|
||||
export default interface IArticleModel extends IArticle, Document {
|
||||
toJSONFor(user: IUserModel): any;
|
||||
|
||||
slugify(): string;
|
||||
|
||||
updateFavoriteCount(): Promise<IArticleModel>;
|
||||
}
|
||||
|
||||
const ArticleSchema = new Schema({
|
||||
slug : {
|
||||
type : Schema.Types.String,
|
||||
lowercase: true,
|
||||
unique : true
|
||||
},
|
||||
title : {
|
||||
type: Schema.Types.String
|
||||
},
|
||||
description : {
|
||||
type: Schema.Types.String
|
||||
},
|
||||
body : {
|
||||
type: Schema.Types.String
|
||||
},
|
||||
tagList : [
|
||||
{
|
||||
type: Schema.Types.String
|
||||
}
|
||||
],
|
||||
favoritesCount: {
|
||||
type : Schema.Types.Number,
|
||||
default: 0
|
||||
},
|
||||
author : {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref : 'User'
|
||||
},
|
||||
comments : [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref : 'Comment'
|
||||
}
|
||||
],
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
ArticleSchema.methods.slugify = function () {
|
||||
this.slug = slugify(this.title) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
|
||||
};
|
||||
|
||||
ArticleSchema.plugin(mongooseUniqueValidator, {message: 'is already taken'});
|
||||
|
||||
ArticleSchema.pre<IArticleModel>('validate', (function (next) {
|
||||
if (!this.slug) {
|
||||
this.slugify();
|
||||
}
|
||||
next();
|
||||
}));
|
||||
|
||||
ArticleSchema.methods.updateFavoriteCount = function () {
|
||||
const article = this;
|
||||
return User.count({favorites: {$in: [article._id]}}).then(function (count) {
|
||||
article.favoritesCount = count;
|
||||
return article.save();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
ArticleSchema.methods.toJSONFor = function (user: IUserModel) {
|
||||
return {
|
||||
slug : this.slug,
|
||||
title : this.title,
|
||||
description : this.description,
|
||||
body : this.body,
|
||||
createdAt : this.createdAt,
|
||||
updatedAt : this.updatedAt,
|
||||
tagList : this.tagList,
|
||||
favorited : user ? user.isFavorite(this._id) : false,
|
||||
favoritesCount: this.favoritesCount,
|
||||
author : this.author.toProfileJSONFor(user)
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
export const Article: Model<IArticleModel> = model<IArticleModel>('Article', ArticleSchema);
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { Document, model, Model, Schema } from "mongoose";
|
||||
import { IComment } from "../../interfaces/comment-interface";
|
||||
import IUserModel from "./user.model";
|
||||
|
||||
export default interface ICommentModel extends IComment, Document {
|
||||
toJSONFor(user: IUserModel): any;
|
||||
}
|
||||
|
||||
const CommentSchema = new Schema({
|
||||
body : {
|
||||
type: Schema.Types.String
|
||||
},
|
||||
author : {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref : 'User'
|
||||
},
|
||||
article: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref : 'Article'
|
||||
}
|
||||
}, {timestamps: true});
|
||||
|
||||
CommentSchema.methods.toJSONFor = function (user: IUserModel) {
|
||||
return {
|
||||
id : this._id,
|
||||
body : this.body,
|
||||
createdAt: this.createdAt,
|
||||
author : this.author.toProfileJSONFor(user)
|
||||
};
|
||||
};
|
||||
|
||||
export const Comment: Model<ICommentModel> = model<ICommentModel>('Comment', CommentSchema);
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import { Document, Model, model, Schema } from 'mongoose';
|
||||
import { IUser } from '../../interfaces/user-interface';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import * as crypto from 'crypto';
|
||||
import { JWT_SECRET } from "../../utilities/secrets";
|
||||
import mongooseUniqueValidator = require("mongoose-unique-validator");
|
||||
|
||||
|
||||
export default interface IUserModel extends IUser, Document {
|
||||
token?: string;
|
||||
favorites: [Schema.Types.ObjectId];
|
||||
|
||||
generateJWT(): string;
|
||||
toAuthJSON(): any;
|
||||
setPassword(password: string): void;
|
||||
validPassword(password: string): boolean;
|
||||
toProfileJSONFor(user: IUserModel): any;
|
||||
isFollowing(id: string): boolean;
|
||||
follow(id: string): Promise<IUser>;
|
||||
unfollow(id: string): Promise<IUser>;
|
||||
favorite(id: string): Promise<IUser>;
|
||||
unfavorite(id: string): Promise<IUser>;
|
||||
isFavorite(id: string): boolean;
|
||||
}
|
||||
|
||||
|
||||
// ISSUE: Own every parameter and any missing dependencies
|
||||
const UserSchema = new Schema({
|
||||
username : {
|
||||
type : Schema.Types.String,
|
||||
lowercase: true,
|
||||
unique : true,
|
||||
required : [true, "can't be blank"],
|
||||
match : [/^[a-zA-Z0-9]+$/, 'is invalid'],
|
||||
index : true
|
||||
},
|
||||
email : {
|
||||
type : Schema.Types.String,
|
||||
lowercase: true,
|
||||
unique : true,
|
||||
required : [true, "can't be blank"],
|
||||
match : [/\S+@\S+\.\S+/, 'is invalid'],
|
||||
index : true
|
||||
},
|
||||
bio : {
|
||||
type: Schema.Types.String
|
||||
},
|
||||
image : {
|
||||
type: Schema.Types.String
|
||||
},
|
||||
favorites: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref : 'Article'
|
||||
}
|
||||
],
|
||||
following: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref : 'User'
|
||||
}
|
||||
],
|
||||
hash : {
|
||||
type: Schema.Types.String
|
||||
},
|
||||
salt : {
|
||||
type: Schema.Types.String
|
||||
},
|
||||
}, {timestamps: true});
|
||||
|
||||
|
||||
UserSchema.plugin(mongooseUniqueValidator, {message: 'is already taken.'});
|
||||
|
||||
UserSchema.methods.validPassword = function (password: string): boolean {
|
||||
const hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
|
||||
return this.hash === hash;
|
||||
};
|
||||
|
||||
UserSchema.methods.setPassword = function (password: string) {
|
||||
this.salt = crypto.randomBytes(16).toString('hex');
|
||||
this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
|
||||
};
|
||||
|
||||
UserSchema.methods.generateJWT = function (): string {
|
||||
const today = new Date();
|
||||
const exp = new Date(today);
|
||||
exp.setDate(today.getDate() + 60);
|
||||
|
||||
return jwt.sign({
|
||||
id : this._id,
|
||||
username: this.username,
|
||||
exp : exp.getTime() / 1000,
|
||||
}, JWT_SECRET);
|
||||
};
|
||||
|
||||
UserSchema.methods.toAuthJSON = function (): any {
|
||||
return {
|
||||
username: this.username,
|
||||
email : this.email,
|
||||
token : this.generateJWT(),
|
||||
bio : this.bio,
|
||||
image : this.image
|
||||
};
|
||||
};
|
||||
|
||||
UserSchema.methods.toProfileJSONFor = function (user: IUserModel) {
|
||||
return {
|
||||
username : this.username,
|
||||
bio : this.bio,
|
||||
image : this.image || 'https://static.productionready.io/images/smiley-cyrus.jpg',
|
||||
following: user ? user.isFollowing(this._id) : false
|
||||
};
|
||||
};
|
||||
|
||||
UserSchema.methods.favorite = function (id: string) {
|
||||
if (this.favorites.indexOf(id) === -1) {
|
||||
this.favorites.push(id);
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.unfavorite = function (id: string) {
|
||||
this.favorites.remove(id);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.isFavorite = function (id: string) {
|
||||
return this.favorites.some(function (favoriteId: string) {
|
||||
return favoriteId.toString() === id.toString();
|
||||
});
|
||||
};
|
||||
|
||||
UserSchema.methods.follow = function (id: string) {
|
||||
if (this.following.indexOf(id) === -1) {
|
||||
this.following.push(id);
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.unfollow = function (id: string) {
|
||||
this.following.remove(id);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.isFollowing = function (id: string) {
|
||||
return this.following.some(function (followId: string) {
|
||||
return followId.toString() === id.toString();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const User: Model<IUserModel> = model<IUserModel>('User', UserSchema);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import User from '../database/models/user.model';
|
||||
import Comment from '../database/models/comment.model';
|
||||
|
||||
|
||||
export interface IArticle {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
body: string;
|
||||
tagList?: [string];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
favorited: boolean;
|
||||
favoritesCount: number;
|
||||
author: User;
|
||||
comments: Comment[]
|
||||
}
|
||||
|
||||
|
||||
export interface IQuery {
|
||||
tagList: { $in: any[] };
|
||||
author: string;
|
||||
_id: { $in: any[] };
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import User from "../database/models/user.model";
|
||||
import Article from "../database/models/article.model";
|
||||
|
||||
export interface IComment {
|
||||
body: string;
|
||||
author: User;
|
||||
article: Article;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import User from "../database/models/user.model";
|
||||
|
||||
export interface IUser {
|
||||
email: string;
|
||||
username: string;
|
||||
bio?: string;
|
||||
image?: string;
|
||||
following: User[];
|
||||
}
|
||||
|
||||
|
||||
export interface IProfile {
|
||||
username: string;
|
||||
bio: string;
|
||||
image: string;
|
||||
following: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
import { Request, Response, Router } from 'express';
|
||||
import { authentication } from '../utilities/authentication';
|
||||
import { User } from '../database/models/user.model';
|
||||
import { Article } from "../database/models/article.model";
|
||||
import { Comment } from "../database/models/comment.model";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
// Preload article objects on routes with ':article'
|
||||
router.param('article', function (req: Request, res: Response, next, slug) {
|
||||
Article.findOne({slug: slug})
|
||||
.populate('author')
|
||||
.then(function (article) {
|
||||
if (!article) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
req.article = article;
|
||||
|
||||
return next();
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
router.param('comment', function (req: Request, res: Response, next, id) {
|
||||
Comment.findById(id).then(function (comment) {
|
||||
if (!comment) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
req.comment = comment;
|
||||
|
||||
return next();
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
router.get('/', authentication.optional, function (req: Request, res: Response, next) {
|
||||
const query: any = {};
|
||||
let limit = 20;
|
||||
let offset = 0;
|
||||
|
||||
if (typeof req.query.limit !== 'undefined') {
|
||||
limit = parseInt(req.query.limit as string);
|
||||
}
|
||||
|
||||
if (typeof req.query.offset !== 'undefined') {
|
||||
offset = parseInt(req.query.offset as string);
|
||||
}
|
||||
|
||||
if (typeof req.query.tag !== 'undefined') {
|
||||
query.tagList = {"$in": [req.query.tag]};
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
req.query.author ? User.findOne({username: req.query.author as string}) : null,
|
||||
req.query.favorited ? User.findOne({username: req.query.favorited as string}) : null
|
||||
]).then(function (results) {
|
||||
const author = results[0];
|
||||
const favoriter = results[1];
|
||||
|
||||
if (author) {
|
||||
query.author = author._id;
|
||||
}
|
||||
|
||||
if (favoriter) {
|
||||
query._id = {$in: favoriter.favorites};
|
||||
} else if (req.query.favorited) {
|
||||
query._id = {$in: []};
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
Article.find(query)
|
||||
.limit(Number(limit))
|
||||
.skip(Number(offset))
|
||||
.sort({createdAt: 'desc'})
|
||||
.populate('author')
|
||||
.exec(),
|
||||
Article.count(query).exec(),
|
||||
req.payload ? User.findById(req.payload.id) : null,
|
||||
]).then(function (results) {
|
||||
const articles = results[0];
|
||||
const articlesCount = results[1];
|
||||
const user = results[2];
|
||||
|
||||
return res.json({
|
||||
articles : articles.map(function (article) {
|
||||
return article.toJSONFor(user);
|
||||
}),
|
||||
articlesCount: articlesCount
|
||||
});
|
||||
});
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
router.get('/feed', authentication.required, function (req: Request, res: Response, next) {
|
||||
let limit = 20;
|
||||
let offset = 0;
|
||||
|
||||
if (typeof req.query.limit !== 'undefined') {
|
||||
limit = parseInt(req.query.limit as string);
|
||||
}
|
||||
|
||||
if (typeof req.query.offset !== 'undefined') {
|
||||
offset = parseInt(req.query.offset as string);
|
||||
}
|
||||
|
||||
User.findById(req.payload.id).then(function (user) {
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
Article.find({author: {$in: user.following}})
|
||||
.limit(Number(limit))
|
||||
.skip(Number(offset))
|
||||
.populate('author')
|
||||
.exec(),
|
||||
Article.count({author: {$in: user.following}})
|
||||
]).then(function (results) {
|
||||
const articles = results[0];
|
||||
const articlesCount = results[1];
|
||||
|
||||
return res.json({
|
||||
articles : articles.map(function (article) {
|
||||
return article.toJSONFor(user);
|
||||
}),
|
||||
articlesCount: articlesCount
|
||||
});
|
||||
}).catch(next);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/', authentication.required, function (req: Request, res: Response, next) {
|
||||
User.findById(req.payload.id).then(function (user) {
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
const article = new Article(req.body.article);
|
||||
|
||||
article.author = user;
|
||||
|
||||
return article.save().then(function () {
|
||||
console.log(article.author);
|
||||
return res.json({article: article.toJSONFor(user)});
|
||||
});
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
// return a article
|
||||
router.get('/:article', authentication.optional, function (req: Request, res: Response, next) {
|
||||
Promise.all([
|
||||
req.payload ? User.findById(req.payload.id) : null,
|
||||
req.article.populate('author').execPopulate()
|
||||
]).then(function (results) {
|
||||
const user = results[0];
|
||||
|
||||
return res.json({article: req.article.toJSONFor(user)});
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
// update article
|
||||
router.put('/:article', authentication.required, function (req: Request, res: Response, next) {
|
||||
User.findById(req.payload.id).then(function (user) {
|
||||
if (req.article.author._id.toString() === req.payload.id.toString()) {
|
||||
if (typeof req.body.article.title !== 'undefined') {
|
||||
req.article.title = req.body.article.title;
|
||||
}
|
||||
|
||||
if (typeof req.body.article.description !== 'undefined') {
|
||||
req.article.description = req.body.article.description;
|
||||
}
|
||||
|
||||
if (typeof req.body.article.body !== 'undefined') {
|
||||
req.article.body = req.body.article.body;
|
||||
}
|
||||
|
||||
if (typeof req.body.article.tagList !== 'undefined') {
|
||||
req.article.tagList = req.body.article.tagList
|
||||
}
|
||||
|
||||
req.article.save().then(function (article) {
|
||||
return res.json({article: article.toJSONFor(user)});
|
||||
}).catch(next);
|
||||
} else {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// delete article
|
||||
router.delete('/:article', authentication.required, function (req: Request, res: Response, next) {
|
||||
User.findById(req.payload.id).then(function (user) {
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
if (req.article.author._id.toString() === req.payload.id.toString()) {
|
||||
return req.article.remove().then(function () {
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
} else {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
// Favorite an article
|
||||
router.post('/:article/favorite', authentication.required, function (req: Request, res: Response, next) {
|
||||
const articleId = req.article._id;
|
||||
|
||||
User.findById(req.payload.id).then(function (user) {
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
return user.favorite(articleId).then(function () {
|
||||
return req.article.updateFavoriteCount().then(function (article) {
|
||||
return res.json({article: article.toJSONFor(user)});
|
||||
});
|
||||
});
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
// Unfavorite an article
|
||||
router.delete('/:article/favorite', authentication.required, function (req: Request, res: Response, next) {
|
||||
const articleId = req.article._id;
|
||||
|
||||
User.findById(req.payload.id).then(function (user) {
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
return user.unfavorite(articleId).then(function () {
|
||||
return req.article.updateFavoriteCount().then(function (article) {
|
||||
return res.json({article: article.toJSONFor(user)});
|
||||
});
|
||||
});
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
// return an article's comments
|
||||
router.get('/:article/comments', authentication.optional, function (req: Request, res: Response, next) {
|
||||
Promise.resolve(req.payload ? User.findById(req.payload.id) : null).then(function (user) {
|
||||
return req.article.populate({
|
||||
path : 'comments',
|
||||
populate: {
|
||||
path: 'author'
|
||||
},
|
||||
options : {
|
||||
sort: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
}
|
||||
}).execPopulate().then(function (article) {
|
||||
return res.json({
|
||||
comments: req.article.comments.map(function (comment) {
|
||||
return comment.toJSONFor(user);
|
||||
})
|
||||
});
|
||||
});
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
// create a new comment
|
||||
router.post('/:article/comments', authentication.required, function (req: Request, res: Response, next) {
|
||||
User.findById(req.payload.id)
|
||||
// @ts-ignore
|
||||
.then(function (user) {
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
const comment = new Comment(req.body.comment);
|
||||
comment.article = req.article;
|
||||
comment.author = user;
|
||||
|
||||
return comment.save().then(function () {
|
||||
req.article.comments.push(comment);
|
||||
|
||||
return req.article.save().then(function (article) {
|
||||
res.json({comment: comment.toJSONFor(user)});
|
||||
});
|
||||
});
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
router.delete('/:article/comments/:comment', authentication.required, function (req: Request, res: Response, next) {
|
||||
if (req.comment.author.toString() === req.payload.id.toString()) {
|
||||
// @ts-ignore
|
||||
req.article.comments.remove(req.comment._id);
|
||||
req.article.save()
|
||||
.then(() => Comment.find({_id: req.comment._id}).remove().exec())
|
||||
.then(function () {
|
||||
res.sendStatus(204);
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(403);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const ArticlesRoutes: Router = router;
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
|
||||
import { Router } from 'express';
|
||||
import { TagRoutes } from './tag-routes';
|
||||
import { UserRoutes } from './user-routes';
|
||||
import { UsersRoutes } from './users-routes';
|
||||
import { ProfilesRoutes } from './profiles-routes';
|
||||
import { ArticlesRoutes } from './articles-routes';
|
||||
|
|
@ -11,8 +9,7 @@ const router: Router = Router();
|
|||
|
||||
|
||||
router.use('/tags', TagRoutes);
|
||||
router.use('/user', UserRoutes);
|
||||
router.use('/users', UsersRoutes);
|
||||
router.use('/', UsersRoutes);
|
||||
router.use('/profiles', ProfilesRoutes);
|
||||
router.use('/articles', ArticlesRoutes);
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import IUserModel, { User } from '../database/models/user.model';
|
||||
import { authentication } from '../utilities/authentication';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
|
||||
/**
|
||||
* PARAM :username
|
||||
*/
|
||||
|
||||
router.param('username', (req: Request, res: Response, next: NextFunction, username: string) => {
|
||||
|
||||
User
|
||||
.findOne({username})
|
||||
.then((user: IUserModel) => {
|
||||
req.profile = user;
|
||||
return next();
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/profiles/:username
|
||||
*/
|
||||
router.get('/:username', authentication.optional, (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
// If authentication was performed and was successful look up the profile relative to authenticated user
|
||||
if (req.payload) {
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
res.status(200).json({profile: req.profile.toProfileJSONFor(user)});
|
||||
})
|
||||
.catch(next);
|
||||
|
||||
// If authentication was NOT performed or successful look up profile relative to that same user (following = false)
|
||||
} else {
|
||||
res.status(200).json({profile: req.profile.toProfileJSONFor(req.profile)});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* POST /api/profiles/:username/follow
|
||||
*/
|
||||
router.post('/:username/follow', authentication.required, (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
const profileId = req.profile._id;
|
||||
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
return user.follow(profileId).then(() => {
|
||||
return res.json({profile: req.profile.toProfileJSONFor(user)});
|
||||
});
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* DELETE /api/profiles/:username/follow
|
||||
*/
|
||||
router.delete('/:username/follow', authentication.required, (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
const profileId = req.profile._id;
|
||||
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
return user.unfollow(profileId).then(() => {
|
||||
return res.json({profile: req.profile.toProfileJSONFor(user)});
|
||||
});
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
export const ProfilesRoutes: Router = router;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Article } from '../database/models/article.model';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
|
||||
// FIXME: Rewrite to pull from Articles...
|
||||
router.get('/', (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
Article
|
||||
.find()
|
||||
.distinct('tagList')
|
||||
.then((tagsArray: [string]) => {
|
||||
return res.json({tags: tagsArray});
|
||||
})
|
||||
.catch(next);
|
||||
|
||||
});
|
||||
|
||||
|
||||
export const TagRoutes: Router = router;
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import IUserModel, { User } from '../database/models/user.model';
|
||||
import passport from 'passport';
|
||||
import { authentication } from "../utilities/authentication";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/user
|
||||
*/
|
||||
router.get('/user', authentication.required, (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
res.status(200).json({user: user.toAuthJSON()});
|
||||
}
|
||||
)
|
||||
.catch(next);
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* PUT /api/user
|
||||
*/
|
||||
router.put('/user', authentication.required, (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
User
|
||||
.findById(req.payload.id)
|
||||
.then((user: IUserModel) => {
|
||||
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
// Update only fields that have values:
|
||||
// ISSUE: DRY out code?
|
||||
if (typeof req.body.user.email !== 'undefined') {
|
||||
user.email = req.body.user.email;
|
||||
}
|
||||
if (typeof req.body.user.username !== 'undefined') {
|
||||
user.username = req.body.user.username;
|
||||
}
|
||||
if (typeof req.body.user.password !== 'undefined') {
|
||||
user.setPassword(req.body.user.password);
|
||||
}
|
||||
if (typeof req.body.user.image !== 'undefined') {
|
||||
user.image = req.body.user.image;
|
||||
}
|
||||
if (typeof req.body.user.bio !== 'undefined') {
|
||||
user.bio = req.body.user.bio;
|
||||
}
|
||||
|
||||
return user.save().then(() => {
|
||||
return res.json({user: user.toAuthJSON()});
|
||||
});
|
||||
})
|
||||
.catch(next);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* POST /api/users
|
||||
*/
|
||||
router.post('/users', (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
const user: IUserModel = new User();
|
||||
|
||||
user.username = req.body.user.username;
|
||||
user.email = req.body.user.email;
|
||||
user.setPassword(req.body.user.password);
|
||||
user.bio = '';
|
||||
user.image = '';
|
||||
|
||||
return user.save()
|
||||
.then(() => {
|
||||
return res.json({user: user.toAuthJSON()});
|
||||
})
|
||||
.catch(next);
|
||||
|
||||
});
|
||||
|
||||
|
||||
// ISSUE: How does this work with the trailing (req, res, next)?
|
||||
/**
|
||||
* POST /api/users/login
|
||||
*/
|
||||
router.post('/users/login', (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
if (!req.body.user.email) {
|
||||
return res.status(422).json({errors: {email: "Can't be blank"}});
|
||||
}
|
||||
|
||||
if (!req.body.user.password) {
|
||||
return res.status(422).json({errors: {password: "Can't be blank"}});
|
||||
}
|
||||
|
||||
passport.authenticate('local', {session: false}, (err, user, info) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
user.token = user.generateJWT();
|
||||
return res.json({user: user.toAuthJSON()});
|
||||
|
||||
} else {
|
||||
return res.status(422).json(info);
|
||||
}
|
||||
})(req, res, next);
|
||||
|
||||
});
|
||||
|
||||
|
||||
export const UsersRoutes: Router = router;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import app from './app';
|
||||
import { APP_PORT } from "./utilities/secrets";
|
||||
import logger from "./utilities/logger";
|
||||
|
||||
app
|
||||
.listen(APP_PORT, () => {
|
||||
logger.info(`server running on port : ${APP_PORT}`);
|
||||
console.log(`server running on port : ${APP_PORT}`);
|
||||
})
|
||||
.on('error', (e) => logger.error(e));
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import Article from "../database/models/article.model";
|
||||
import Comment from "../database/models/comment.model";
|
||||
import User from "../database/models/user.model";
|
||||
|
||||
declare module "express" {
|
||||
|
||||
export interface Request {
|
||||
article?: Article;
|
||||
comment?: Comment;
|
||||
profile?: User;
|
||||
payload?: {
|
||||
id: string,
|
||||
username: string,
|
||||
exp: number,
|
||||
iat: number
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { Request } from 'express';
|
||||
const jwt = require('express-jwt');
|
||||
import { JWT_SECRET } from "./secrets";
|
||||
|
||||
|
||||
function getTokenFromHeader(req: Request): string | null {
|
||||
|
||||
const headerAuth: string | string[] = req.headers.authorization;
|
||||
|
||||
if (headerAuth !== undefined && headerAuth !== null) {
|
||||
|
||||
if (Array.isArray(headerAuth)) {
|
||||
return splitToken(headerAuth[0]);
|
||||
} else {
|
||||
return splitToken(headerAuth);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function splitToken(authString: string) {
|
||||
|
||||
if (authString.split(' ')[0] === 'Token') {
|
||||
return authString.split(' ')[1];
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const auth = {
|
||||
required: jwt({
|
||||
credentialsRequired: true,
|
||||
secret : JWT_SECRET,
|
||||
getToken : getTokenFromHeader,
|
||||
userProperty : 'payload',
|
||||
// @ts-ignore
|
||||
algorithms : ['HS256']
|
||||
}),
|
||||
|
||||
optional: jwt({
|
||||
credentialsRequired: false,
|
||||
secret : JWT_SECRET,
|
||||
getToken : getTokenFromHeader,
|
||||
userProperty : 'payload',
|
||||
algorithms : ['HS256']
|
||||
})
|
||||
};
|
||||
|
||||
export const authentication = auth;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { Application, Request, Response } from 'express';
|
||||
import { IS_PRODUCTION } from "./secrets";
|
||||
import logger from "./logger";
|
||||
|
||||
export function loadErrorHandlers(app: Application) {
|
||||
|
||||
// catch 404 errors and forward to error handler
|
||||
app.use((req, res, next) => {
|
||||
|
||||
interface BetterError extends Error {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
const err: BetterError = new Error('Not Found');
|
||||
err.status = 404;
|
||||
next(err);
|
||||
});
|
||||
|
||||
app.use((err: any, req: Request, res: Response, next: any) => {
|
||||
|
||||
if (err.name === 'ValidationError') {
|
||||
return res.status(422).json({
|
||||
errors: Object.keys(err.errors).reduce(function (errors: any, key: string) {
|
||||
errors[key] = err.errors[key].message;
|
||||
|
||||
return errors;
|
||||
}, {})
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(err);
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
errors: {
|
||||
message: err.message,
|
||||
error : !IS_PRODUCTION ? err : {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { createLogger, format, transports } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import { ENVIRONMENT, LOG_DIRECTORY } from "./secrets";
|
||||
|
||||
let dir = LOG_DIRECTORY;
|
||||
|
||||
// create directory if it is not present
|
||||
if (!fs.existsSync(dir)) {
|
||||
// Create the directory if it does not exist
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
|
||||
const logLevel = ENVIRONMENT === 'dev' ? 'debug' : 'warn';
|
||||
|
||||
const options = {
|
||||
file: {
|
||||
level : logLevel,
|
||||
filename : dir + '/%DATE%.log',
|
||||
datePattern : 'YYYY-MM-DD',
|
||||
zippedArchive : true,
|
||||
timestamp : true,
|
||||
handleExceptions : true,
|
||||
humanReadableUnhandledException: true,
|
||||
prettyPrint : true,
|
||||
json : true,
|
||||
maxSize : '20m',
|
||||
colorize : true,
|
||||
maxFiles : '14d',
|
||||
},
|
||||
};
|
||||
|
||||
export default createLogger({
|
||||
transports : [
|
||||
new transports.Console({
|
||||
stderrLevels: ["info", "error"],
|
||||
format: format.combine(format.errors({stack: true}), format.prettyPrint()),
|
||||
}),
|
||||
],
|
||||
exceptionHandlers: [new DailyRotateFile(options.file)],
|
||||
exitOnError : false, // do not exit on handled exceptions
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import passport from 'passport';
|
||||
import { User } from '../database/models/user.model';
|
||||
import passportLocal from 'passport-local';
|
||||
|
||||
|
||||
const LocalStrategy = passportLocal.Strategy;
|
||||
|
||||
|
||||
passport.use(new LocalStrategy({
|
||||
|
||||
// Strategy is based on username & password. Substitute email for username.
|
||||
usernameField: 'user[email]',
|
||||
passwordField: 'user[password]'
|
||||
},
|
||||
|
||||
(email, password, done) => {
|
||||
|
||||
User
|
||||
.findOne({email})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
return done(null, false, {message: 'Incorrect email.'});
|
||||
}
|
||||
if (!user.validPassword(password)) {
|
||||
return done(null, false, {message: 'Incorrect password.'});
|
||||
}
|
||||
return done(null, user);
|
||||
})
|
||||
.catch(done);
|
||||
}));
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import * as dotenv from "dotenv";
|
||||
import * as _ from "lodash";
|
||||
import * as path from "path";
|
||||
|
||||
dotenv.config({path: ".env"});
|
||||
|
||||
export const ENVIRONMENT = _.defaultTo(process.env.APP_ENV, "dev");
|
||||
export const IS_PRODUCTION = ENVIRONMENT === "production";
|
||||
export const APP_PORT = _.defaultTo(parseInt(process.env.APP_PORT), 3000);
|
||||
export const LOG_DIRECTORY = _.defaultTo(process.env.LOG_DIRECTORY, path.resolve('logs'));
|
||||
export const JWT_SECRET = _.defaultTo(process.env.JWT_SECRET, "secret");
|
||||
export const SESSION_SECRET = _.defaultTo(process.env.SESSION_SECRET, "secret");
|
||||
export const DB = {
|
||||
USER : _.defaultTo(process.env.DB_USER, "root"),
|
||||
PASSWORD: _.defaultTo(process.env.DB_USER_PWD, "secret"),
|
||||
HOST : _.defaultTo(process.env.DB_HOST, "localhost"),
|
||||
NAME : _.defaultTo(process.env.DB_NAME, "conduit"),
|
||||
PORT : _.defaultTo(parseInt(process.env.DB_PORT), 27017),
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "4aa60b52-97fc-456d-4d4f-14a350e95dff",
|
||||
"name": "Conduit API Tests - Environment",
|
||||
"values": [{
|
||||
"enabled": true,
|
||||
"key": "apiUrl",
|
||||
"value": "http://localhost:3000/api",
|
||||
"type": "text"
|
||||
}],
|
||||
"timestamp": 1505871382668,
|
||||
"_postman_variable_scope": "environment",
|
||||
"_postman_exported_at": "2017-09-20T01:36:34.835Z",
|
||||
"_postman_exported_using": "Postman/5.2.0"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es2017",
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "build",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/*",
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"rules": {
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"no-var-keyword": true,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-escape"
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always",
|
||||
"ignore-bound-class-methods"
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-module",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
],
|
||||
"no-internal-module": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-null-keyword": false,
|
||||
"prefer-const": true,
|
||||
"jsdoc-format": true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue