Merge branch 'master' into master

This commit is contained in:
Orta 2019-06-19 14:55:39 -07:00 коммит произвёл GitHub
Родитель 3dbae08075 dfa9150489
Коммит a548d8876a
17 изменённых файлов: 4680 добавлений и 4629 удалений

2
.gitignore поставляемый
Просмотреть файл

@ -37,3 +37,5 @@ Thumbs.db
# Ignore built ts files
dist/**/*
# ignore yarn.lock
yarn.lock

Просмотреть файл

@ -1,12 +1,14 @@
{
"language": "node_js",
"node_js": "8",
"services": [
"mongodb"
],
"script": [
"npm run build",
"npm run test"
]
}
sudo: false
language: node_js
node_js:
- 8
- 9
- 10
services:
- mongodb
cache:
directories:
- node_modules
script:
- npm run build
- npm run test

3
.vscode/extensions.json поставляемый
Просмотреть файл

@ -1,7 +1,6 @@
{
"recommendations": [
"eg2.tslint",
"ms-vscode.vscode-typescript-tslint-plugin",
"ms-azuretools.vscode-cosmosdb",
"streetsidesoftware.code-spell-checker"
]
}

9
.vscode/settings.json поставляемый
Просмотреть файл

@ -10,9 +10,8 @@
"tslint.ignoreDefinitionFiles": false,
"tslint.autoFixOnSave": true,
"tslint.exclude": "**/node_modules/**/*",
"cSpell.words": [
"csrf",
"definitelytyped",
"promisified"
]
"appService.zipIgnorePattern": [
".vscode{,/**}"
],
"appService.deploySubpath": ""
}

Просмотреть файл

@ -7,7 +7,7 @@
![image](https://user-images.githubusercontent.com/820883/36764267-abbdb7f8-1be0-11e8-9678-2a9ea448d7f8.png)
The main purpose of this repository is to show a good end-to-end project setup and workflow for writing Node code in TypeScript.
I will try to keep this as up-to-date as possible, but community contributions and recommendations for improvements are encouraged and will be most welcome.
We will try to keep this as up-to-date as possible, but community contributions and recommendations for improvements are encouraged and will be most welcome.
# Pre-reqs
@ -45,7 +45,7 @@ npm start
Or, if you're using VS Code, you can use `cmd + shift + b` to run the default build task (which is mapped to `npm run build`), and then you can use the command palette (`cmd + shift + p`) and select `Tasks: Run Task` > `npm: start` to run `npm start` for you.
> **Note on editors!** - TypeScript has great support in [every editor](http://www.typescriptlang.org/index.html#download-links), but this project has been pre-configured for use with [VS Code](https://code.visualstudio.com/).
Throughout the README I'll try to call out specific places where VS Code really shines or where this project has been setup to take advantage of specific features.
Throughout the README We will try to call out specific places where VS Code really shines or where this project has been setup to take advantage of specific features.
Finally, navigate to `http://localhost:3000` and you should see the template being served and rendered locally!
@ -53,7 +53,7 @@ Finally, navigate to `http://localhost:3000` and you should see the template bei
There are many ways to deploy an Node app, and in general, nothing about the deployment process changes because you're using TypeScript.
In this section, I'll walk you through how to deploy this app to Azure App Service using the extensions available in VS Code because I think it is the easiest and fastest way to get started, as well as the most friendly workflow from a developer's perspective.
## Pre-reqs
## Prerequisites
- [**Azure account**](https://azure.microsoft.com/en-us/free/) - If you don't have one, you can sign up for free.
The Azure free tier gives you plenty of resources to play around with including up to 10 App Service instances, which is what we will be using.
- [**VS Code**](https://code.visualstudio.com/) - We'll be using the interface provided by VS Code to quickly deploy our app.
@ -64,7 +64,7 @@ The easiest way to achieve this is by using a managed cloud database.
There are many different providers, but the easiest one to get started with is [MongoLab](#mlab).
### <a name="mlab"></a> Create a managed MongoDB with MongoLab
1. Navigate to [MongoLab's Website](https://mlab.com/), sign up for a free account, and then log in.
1. Navigate to [mLab's Website](https://mlab.com/), sign up for a free account, and then log in.
2. In the **MongoDB Deployments** section, click the **Create New** button.
3. Select any provider (I recommend **Microsoft Azure** as it provides an easier path to upgrading to globally distributed instances later).
4. Select **Sandbox** to keep it free unless you know what you're doing, and hit **Continue**.
@ -104,7 +104,7 @@ You can confirm that everything worked by seeing your Azure subscription listed
Additionally you should see the email associated with your account listed in the status bar at the bottom of VS Code.
### Build the app
Building the app locally is required for before a zip deploy because the App Service won't execute build tasks.
Building the app locally is required before a zip deploy because the App Service won't execute build tasks.
Build the app however you normally would:
- `ctrl + shift + b` - kicks off default build in VS Code
- execute `npm run build` from a terminal window
@ -143,7 +143,7 @@ Deployment can fail for various reasons, if you get stuck with a page that says
# TypeScript + Node
In the next few sections I will call out everything that changes when adding TypeScript to an Express project.
Note that all of this has already been setup for this project, but feel free to use this as a reference for converting other Node.js project to TypeScript.
Note that all of this has already been setup for this project, but feel free to use this as a reference for converting other Node.js projects to TypeScript.
## Getting TypeScript
TypeScript itself is simple to add to any project with `npm`.
@ -176,7 +176,7 @@ The full folder structure of this app is explained below:
| **src/public** | Static assets that will be used client side |
| **src/types** | Holds .d.ts files not found on DefinitelyTyped. Covered more in this [section](#type-definition-dts-files) |
| **src**/server.ts | Entry point to your express app |
| **test** | Contains your tests. Seperate from source because there is a different build process. |
| **test** | Contains your tests. Separate from source because there is a different build process. |
| **views** | Views define how your app renders on the client. In this case we're using pug |
| .env.example | API keys, tokens, passwords, database URI. Clone this, but don't check it in to public repos. |
| .travis.yml | Used to configure Travis CI build |
@ -188,7 +188,7 @@ The full folder structure of this app is explained below:
| tslint.json | Config settings for TSLint code style checking |
## Building the project
It is rare for JavaScript projects not to have some kind of build pipeline these days, however Node projects typically have the least amount build configuration.
It is rare for JavaScript projects not to have some kind of build pipeline these days. However, Node projects typically have the least amount of build configuration.
Because of this I've tried to keep the build as simple as possible.
If you're concerned about compile time, the main watch task takes ~2s to refresh.
@ -229,7 +229,7 @@ Let's dissect this project's `tsconfig.json`, starting with the `compilerOptions
The rest of the file define the TypeScript project context.
The rest of the file defines the TypeScript project context.
The project context is basically a set of options that determine which files are compiled when the compiler is invoked with a specific `tsconfig.json`.
In this case, we use the following to define our project context:
```json
@ -363,7 +363,7 @@ Source maps allow you to drop break points in your TypeScript source code and ha
> **Note!** - Source maps aren't specific to TypeScript.
Anytime JavaScript is transformed (transpiled, compiled, optimized, minified, etc) you need source maps so that the code that is executed at runtime can be _mapped_ back to the source that generated it.
The best part of source maps is when configured correctly, you don't even know they exist! So let's take a look at how we do that in this project.
The best part of source maps is, when configured correctly, you don't even know they exist! So let's take a look at how we do that in this project.
#### Configuring source maps
First you need to make sure your `tsconfig.json` has source map generation enabled:
@ -523,7 +523,7 @@ In that file you'll find two sections:
| node-sass | Allows to compile .scss files to .css |
| nodemon | Utility that automatically restarts node process when it crashes |
| supertest | HTTP assertion library. |
| ts-jest | A preprocessor with sourcemap support to help use TypeScript wit Jest.|
| ts-jest | A preprocessor with sourcemap support to help use TypeScript with Jest.|
| ts-node | Enables directly running TS files. Used to run `copy-static-assets.ts` |
| tslint | Linter (similar to ESLint) for TypeScript files |
| typescript | JavaScript compiler/type checker that boosts JavaScript productivity |
@ -532,3 +532,7 @@ To install or update these dependencies you can use `npm install` or `npm update
# Hackathon Starter Project
A majority of this quick start's content was inspired or adapted from Sahat's excellent [Hackathon Starter project](https://github.com/sahat/hackathon-starter).
## License
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [MIT](LICENSE.txt) License.

Просмотреть файл

@ -1,7 +1,7 @@
module.exports = {
globals: {
'ts-jest': {
tsConfigFile: 'tsconfig.json'
tsConfig: 'tsconfig.json'
}
},
moduleFileExtensions: [
@ -9,10 +9,10 @@ module.exports = {
'js'
],
transform: {
'^.+\\.(ts|tsx)$': './node_modules/ts-jest/preprocessor.js'
'^.+\\.(ts|tsx)$': 'ts-jest'
},
testMatch: [
'**/test/**/*.test.(ts|js)'
],
testEnvironment: 'node'
};
};

9135
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -42,13 +42,13 @@
"fbgraph": "^1.4.1",
"lodash": "^4.17.5",
"lusca": "^1.5.2",
"mongoose": "^4.13.11",
"mongoose": "^5.4.2",
"nodemailer": "^4.4.1",
"passport": "^0.4.0",
"passport-facebook": "^2.1.1",
"passport-local": "^1.0.0",
"pug": "^2.0.0-rc.4",
"request": "^2.83.0",
"pug": "^2.0.3",
"request": "^2.88.0",
"request-promise": "^4.2.2",
"winston": "^2.4.0"
},
@ -64,7 +64,7 @@
"@types/express": "^4.11.1",
"@types/express-session": "^1.15.8",
"@types/jest": "^22.1.3",
"@types/jquery": "^3.2.17",
"@types/jquery": "^3.3.29",
"@types/lodash": "^4.14.91",
"@types/lusca": "^1.5.0",
"@types/mongodb": "^3.0.5",
@ -81,14 +81,14 @@
"@types/winston": "^2.3.7",
"chai": "^4.1.2",
"concurrently": "^3.5.1",
"jest": "^22.0.4",
"node-sass": "^4.7.2",
"nodemon": "^1.13.0",
"jest": "^24.0.0",
"node-sass": "^4.12.0",
"nodemon": "^1.18.10",
"shelljs": "^0.8.1",
"supertest": "^3.0.0",
"ts-jest": "^22.0.4",
"ts-jest": "^24.0.0",
"ts-node": "^5.0.0",
"tslint": "^5.9.1",
"typescript": "^2.7.2"
"typescript": "^3.4.5"
}
}

Просмотреть файл

@ -2,7 +2,6 @@ import express from "express";
import compression from "compression"; // compresses requests
import session from "express-session";
import bodyParser from "body-parser";
import logger from "./util/logger";
import lusca from "lusca";
import dotenv from "dotenv";
import mongo from "connect-mongo";
@ -35,7 +34,8 @@ const app = express();
// Connect to MongoDB
const mongoUrl = MONGODB_URI;
(<any>mongoose).Promise = bluebird;
mongoose.connect(mongoUrl, {useMongoClient: true}).then(
mongoose.connect(mongoUrl, { useMongoClient: true }).then(
() => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ },
).catch(err => {
console.log("MongoDB connection error. Please make sure MongoDB is running. " + err);
@ -122,4 +122,4 @@ app.get("/auth/facebook/callback", passport.authenticate("facebook", { failureRe
res.redirect(req.session.returnTo || "/");
});
export default app;
export default app;

Просмотреть файл

@ -1,11 +1,10 @@
import passport from "passport";
import request from "request";
import passportLocal from "passport-local";
import passportFacebook from "passport-facebook";
import _ from "lodash";
// import { User, UserType } from '../models/User';
import { default as User } from "../models/User";
import { User } from "../models/User";
import { Request, Response, NextFunction } from "express";
const LocalStrategy = passportLocal.Strategy;
@ -121,7 +120,7 @@ passport.use(new FacebookStrategy({
/**
* Login Required middleware.
*/
export let isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
if (req.isAuthenticated()) {
return next();
}
@ -131,7 +130,7 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
/**
* Authorization Required middleware.
*/
export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
export const isAuthorized = (req: Request, res: Response, next: NextFunction) => {
const provider = req.path.split("/").slice(-1)[0];
if (_.find(req.user.tokens, { kind: provider })) {

Просмотреть файл

@ -1,7 +1,5 @@
"use strict";
import async from "async";
import request from "request";
import graph from "fbgraph";
import { Response, Request, NextFunction } from "express";
@ -10,7 +8,7 @@ import { Response, Request, NextFunction } from "express";
* GET /api
* List of API examples.
*/
export let getApi = (req: Request, res: Response) => {
export const getApi = (req: Request, res: Response) => {
res.render("api/index", {
title: "API Examples"
});
@ -20,7 +18,7 @@ export let getApi = (req: Request, res: Response) => {
* GET /api/facebook
* Facebook API example.
*/
export let getFacebook = (req: Request, res: Response, next: NextFunction) => {
export const getFacebook = (req: Request, res: Response, next: NextFunction) => {
const token = req.user.tokens.find((token: any) => token.kind === "facebook");
graph.setAccessToken(token.accessToken);
graph.get(`${req.user.facebook}?fields=id,name,email,first_name,last_name,gender,link,locale,timezone`, (err: Error, results: graph.FacebookUser) => {

Просмотреть файл

@ -13,7 +13,7 @@ const transporter = nodemailer.createTransport({
* GET /contact
* Contact form page.
*/
export let getContact = (req: Request, res: Response) => {
export const getContact = (req: Request, res: Response) => {
res.render("contact", {
title: "Contact"
});
@ -23,7 +23,7 @@ export let getContact = (req: Request, res: Response) => {
* POST /contact
* Send a contact form via Nodemailer.
*/
export let postContact = (req: Request, res: Response) => {
export const postContact = (req: Request, res: Response) => {
req.assert("name", "Name cannot be blank").notEmpty();
req.assert("email", "Email is not valid").isEmail();
req.assert("message", "Message cannot be blank").notEmpty();

Просмотреть файл

@ -4,7 +4,7 @@ import { Request, Response } from "express";
* GET /
* Home page.
*/
export let index = (req: Request, res: Response) => {
export const index = (req: Request, res: Response) => {
res.render("home", {
title: "Home"
});

Просмотреть файл

@ -2,19 +2,18 @@ import async from "async";
import crypto from "crypto";
import nodemailer from "nodemailer";
import passport from "passport";
import { default as User, UserModel, AuthToken } from "../models/User";
import { User, UserDocument, AuthToken } from "../models/User";
import { Request, Response, NextFunction } from "express";
import { IVerifyOptions } from "passport-local";
import { WriteError } from "mongodb";
import request from "express-validator";
import "../config/passport";
/**
* GET /login
* Login page.
*/
export let getLogin = (req: Request, res: Response) => {
export const getLogin = (req: Request, res: Response) => {
if (req.user) {
return res.redirect("/");
}
@ -27,7 +26,7 @@ export let getLogin = (req: Request, res: Response) => {
* POST /login
* Sign in using email and password.
*/
export let postLogin = (req: Request, res: Response, next: NextFunction) => {
export const postLogin = (req: Request, res: Response, next: NextFunction) => {
req.assert("email", "Email is not valid").isEmail();
req.assert("password", "Password cannot be blank").notEmpty();
req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
@ -39,7 +38,7 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => {
return res.redirect("/login");
}
passport.authenticate("local", (err: Error, user: UserModel, info: IVerifyOptions) => {
passport.authenticate("local", (err: Error, user: UserDocument, info: IVerifyOptions) => {
if (err) { return next(err); }
if (!user) {
req.flash("errors", info.message);
@ -57,7 +56,7 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => {
* GET /logout
* Log out.
*/
export let logout = (req: Request, res: Response) => {
export const logout = (req: Request, res: Response) => {
req.logout();
res.redirect("/");
};
@ -66,7 +65,7 @@ export let logout = (req: Request, res: Response) => {
* GET /signup
* Signup page.
*/
export let getSignup = (req: Request, res: Response) => {
export const getSignup = (req: Request, res: Response) => {
if (req.user) {
return res.redirect("/");
}
@ -79,7 +78,7 @@ export let getSignup = (req: Request, res: Response) => {
* POST /signup
* Create a new local account.
*/
export let postSignup = (req: Request, res: Response, next: NextFunction) => {
export const postSignup = (req: Request, res: Response, next: NextFunction) => {
req.assert("email", "Email is not valid").isEmail();
req.assert("password", "Password must be at least 4 characters long").len({ min: 4 });
req.assert("confirmPassword", "Passwords do not match").equals(req.body.password);
@ -119,7 +118,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
* GET /account
* Profile page.
*/
export let getAccount = (req: Request, res: Response) => {
export const getAccount = (req: Request, res: Response) => {
res.render("account/profile", {
title: "Account Management"
});
@ -129,7 +128,7 @@ export let getAccount = (req: Request, res: Response) => {
* POST /account/profile
* Update profile information.
*/
export let postUpdateProfile = (req: Request, res: Response, next: NextFunction) => {
export const postUpdateProfile = (req: Request, res: Response, next: NextFunction) => {
req.assert("email", "Please enter a valid email address.").isEmail();
req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
@ -140,7 +139,7 @@ export let postUpdateProfile = (req: Request, res: Response, next: NextFunction)
return res.redirect("/account");
}
User.findById(req.user.id, (err, user: UserModel) => {
User.findById(req.user.id, (err, user: UserDocument) => {
if (err) { return next(err); }
user.email = req.body.email || "";
user.profile.name = req.body.name || "";
@ -165,7 +164,7 @@ export let postUpdateProfile = (req: Request, res: Response, next: NextFunction)
* POST /account/password
* Update current password.
*/
export let postUpdatePassword = (req: Request, res: Response, next: NextFunction) => {
export const postUpdatePassword = (req: Request, res: Response, next: NextFunction) => {
req.assert("password", "Password must be at least 4 characters long").len({ min: 4 });
req.assert("confirmPassword", "Passwords do not match").equals(req.body.password);
@ -176,7 +175,7 @@ export let postUpdatePassword = (req: Request, res: Response, next: NextFunction
return res.redirect("/account");
}
User.findById(req.user.id, (err, user: UserModel) => {
User.findById(req.user.id, (err, user: UserDocument) => {
if (err) { return next(err); }
user.password = req.body.password;
user.save((err: WriteError) => {
@ -191,7 +190,7 @@ export let postUpdatePassword = (req: Request, res: Response, next: NextFunction
* POST /account/delete
* Delete user account.
*/
export let postDeleteAccount = (req: Request, res: Response, next: NextFunction) => {
export const postDeleteAccount = (req: Request, res: Response, next: NextFunction) => {
User.remove({ _id: req.user.id }, (err) => {
if (err) { return next(err); }
req.logout();
@ -204,7 +203,7 @@ export let postDeleteAccount = (req: Request, res: Response, next: NextFunction)
* GET /account/unlink/:provider
* Unlink OAuth provider.
*/
export let getOauthUnlink = (req: Request, res: Response, next: NextFunction) => {
export const getOauthUnlink = (req: Request, res: Response, next: NextFunction) => {
const provider = req.params.provider;
User.findById(req.user.id, (err, user: any) => {
if (err) { return next(err); }
@ -222,7 +221,7 @@ export let getOauthUnlink = (req: Request, res: Response, next: NextFunction) =>
* GET /reset/:token
* Reset Password page.
*/
export let getReset = (req: Request, res: Response, next: NextFunction) => {
export const getReset = (req: Request, res: Response, next: NextFunction) => {
if (req.isAuthenticated()) {
return res.redirect("/");
}
@ -245,7 +244,7 @@ export let getReset = (req: Request, res: Response, next: NextFunction) => {
* POST /reset/:token
* Process the reset password request.
*/
export let postReset = (req: Request, res: Response, next: NextFunction) => {
export const postReset = (req: Request, res: Response, next: NextFunction) => {
req.assert("password", "Password must be at least 4 characters long.").len({ min: 4 });
req.assert("confirm", "Passwords must match.").equals(req.body.password);
@ -278,7 +277,7 @@ export let postReset = (req: Request, res: Response, next: NextFunction) => {
});
});
},
function sendResetPasswordEmail(user: UserModel, done: Function) {
function sendResetPasswordEmail(user: UserDocument, done: Function) {
const transporter = nodemailer.createTransport({
service: "SendGrid",
auth: {
@ -307,7 +306,7 @@ export let postReset = (req: Request, res: Response, next: NextFunction) => {
* GET /forgot
* Forgot Password page.
*/
export let getForgot = (req: Request, res: Response) => {
export const getForgot = (req: Request, res: Response) => {
if (req.isAuthenticated()) {
return res.redirect("/");
}
@ -320,7 +319,7 @@ export let getForgot = (req: Request, res: Response) => {
* POST /forgot
* Create a random token, then the send user an email with a reset link.
*/
export let postForgot = (req: Request, res: Response, next: NextFunction) => {
export const postForgot = (req: Request, res: Response, next: NextFunction) => {
req.assert("email", "Please enter a valid email address.").isEmail();
req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
@ -352,7 +351,7 @@ export let postForgot = (req: Request, res: Response, next: NextFunction) => {
});
});
},
function sendForgotPasswordEmail(token: AuthToken, user: UserModel, done: Function) {
function sendForgotPasswordEmail(token: AuthToken, user: UserDocument, done: Function) {
const transporter = nodemailer.createTransport({
service: "SendGrid",
auth: {

Просмотреть файл

@ -2,7 +2,7 @@ import bcrypt from "bcrypt-nodejs";
import crypto from "crypto";
import mongoose from "mongoose";
export type UserModel = mongoose.Document & {
export type UserDocument = mongoose.Document & {
email: string,
password: string,
passwordResetToken: string,
@ -77,10 +77,7 @@ userSchema.methods.comparePassword = comparePassword;
/**
* Helper method for getting user's gravatar.
*/
userSchema.methods.gravatar = function (size: number) {
if (!size) {
size = 200;
}
userSchema.methods.gravatar = function (size: number = 200) {
if (!this.email) {
return `https://gravatar.com/avatar/?s=${size}&d=retro`;
}
@ -88,6 +85,4 @@ userSchema.methods.gravatar = function (size: number) {
return `https://gravatar.com/avatar/${md5}?s=${size}&d=retro`;
};
// export const User: UserType = mongoose.model<UserType>('User', userSchema);
const User = mongoose.model("User", userSchema);
export default User;
export const User = mongoose.model<UserDocument>("User", userSchema);

Просмотреть файл

@ -1,6 +1,5 @@
import winston from "winston";
import { Logger } from "winston";
import { ENVIRONMENT } from "./secrets";
const logger = new (Logger)({
transports: [

Просмотреть файл

@ -21,6 +21,10 @@ if (!SESSION_SECRET) {
}
if (!MONGODB_URI) {
logger.error("No mongo connection string. Set MONGODB_URI environment variable.");
if (prod) {
logger.error("No mongo connection string. Set MONGODB_URI environment variable.");
} else {
logger.error("No mongo connection string. Set MONGODB_URI_LOCAL environment variable.");
}
process.exit(1);
}