Comparar commits
55 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| 6efec4d56e | |||
| 7d91521374 | |||
| 601e19dc3a | |||
| e45c9e8a43 | |||
| 1d875fe04c | |||
| f620b2aa33 | |||
| eb8d4b4a8b | |||
| 56407823b9 | |||
| 1f428b3e41 | |||
| 41b8214ae4 | |||
| aa20c45723 | |||
| 6d52386bc2 | |||
| ef766911ee | |||
| f2ae6de538 | |||
| 8137d1a1cd | |||
| e54a27655f | |||
| d8407839c3 | |||
| bfa5829c42 | |||
| 666e2d0c56 | |||
| 6203d61587 | |||
| 52042db1dc | |||
| c2bb5e8f37 | |||
| 94830cc1f7 | |||
| 586aebad04 | |||
| 745e854cc4 | |||
| 2234c8a0cc | |||
| 4d02db4215 | |||
| 90754dc74b | |||
| f29963c89e | |||
| acbbc86151 | |||
| 921dbac155 | |||
| a2ef52c7a4 | |||
| 25b4aa2dc8 | |||
| 0429590bec | |||
| 48805dcb85 | |||
| 0b06d6ac5b | |||
| 3998b6efc8 | |||
| 4a0dcb2905 | |||
| 354623ec21 | |||
| 8b34db5724 | |||
| bbd83a4df5 | |||
| 5b4610b3d9 | |||
| 996a555333 | |||
| 8439574dc5 | |||
| 4193bc0e6a | |||
| f394ea3d10 | |||
| 4f325dfe4b | |||
| 2ef4e1387f | |||
| 1001528a16 | |||
| 6974aef9d1 | |||
| 37234ec2e9 | |||
| d6a059447d | |||
| c10c971caf | |||
| 0fe63863b1 | |||
| 39521f2b61 |
@@ -19,6 +19,7 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
@@ -37,3 +38,8 @@ secrets.sh
|
||||
# complexity reports
|
||||
es6-src/
|
||||
report/
|
||||
|
||||
# VoTT Server
|
||||
server/lib
|
||||
server/node_modules
|
||||
server/coverage
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
<!-- cl-start -->
|
||||
|
||||
# [2.1.0](https://github.com/Microsoft/VoTT/compare/v2.0.0...v2.1.0) (04-29-2019)
|
||||
[GitHub Release](https://github.com/Microsoft/VoTT/releases/tag/v2.1.0)
|
||||
|
||||
- fix: Updates backwards compat & fixes cntk export image bug (#789)
|
||||
- fix: Updates export options for pascalVOC rename (#788)
|
||||
- fix: change method for alloc string to buffer (#777)
|
||||
- feat: Add CSV Exporter (#757)
|
||||
- fix: Fix display of tag color picker (#782)
|
||||
- feat: Active Learning Updates (#778)
|
||||
- doc: updates to readme and changelog (#781)
|
||||
- doc: Adds CODE_OF_CONDUCT.md (#779)
|
||||
- doc: Add bug & feature templates (#780)
|
||||
- fix: Refactored project tag/delete updates (#764)
|
||||
- fix: Enables selection of azure region for custom vision export (#765)
|
||||
- feat: CNTK Export Provider (#771)
|
||||
- feat: Save partial project progress during project creation (#769)
|
||||
- fix: Fixes ymax and rename Tensorflow nama everywhere (#763)
|
||||
|
||||
# [2.0.0](https://github.com/Microsoft/VoTT/compare/v2.0.0-preview.3...v2.0.0) (04-12-2019)
|
||||
[GitHub Release](https://github.com/Microsoft/VoTT/releases/tag/v2.0.0)
|
||||
|
||||
|
||||
gerado
+9
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vott",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -6253,6 +6253,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"express-request-id": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/express-request-id/-/express-request-id-1.4.1.tgz",
|
||||
"integrity": "sha512-qpxK6XhDYtdx9FvxwCHkUeZVWtkGbWR87hBAzGECfwYF/QQCPXEwwB2/9NGkOR1tT7/aLs9mma3CT0vjSzuZVw==",
|
||||
"requires": {
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vott",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"author": {
|
||||
"name": "Microsoft",
|
||||
"url": "https://github.com/Microsoft/VoTT"
|
||||
@@ -23,6 +23,7 @@
|
||||
"buffer-reverse": "^1.0.1",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"dotenv": "^7.0.0",
|
||||
"express-request-id": "^1.4.1",
|
||||
"google-protobuf": "^3.6.1",
|
||||
"jpeg-js": "^0.3.4",
|
||||
"json2csv": "^4.5.0",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
APP_ID=xyz
|
||||
APP_SECRET=asdf
|
||||
COOKIE_SECRETS="[ { key: '12345678901234567890123456789012', iv: '123456789012' }, { key: 'abcdefghijklmnopqrstuvwxyzabcdef', iv: 'abcdefghijkl' }, ])"
|
||||
ALLOW_HTTP=true
|
||||
BASE_URL=http://localhost:3000/
|
||||
externo
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceFolder}/lib/app.js", //"${workspaceFolder}\\lib\\app.js",
|
||||
"args": [
|
||||
"|",
|
||||
"bunyan"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"outputCapture": "std",
|
||||
}
|
||||
]
|
||||
}
|
||||
externo
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
externo
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"option": "watch",
|
||||
"problemMatcher": [
|
||||
"$tsc-watch"
|
||||
],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# Node.js
|
||||
# Build a general Node.js project with npm.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
|
||||
trigger:
|
||||
- johnshew/login
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection created during pipeline creation
|
||||
azureSubscription: 'fe7b93fe-e836-4a55-804c-883dbea6af24'
|
||||
|
||||
# Web app name
|
||||
webAppName: 'vott'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'ubuntu-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
displayName: 'Install Node.js'
|
||||
|
||||
- script: |
|
||||
npm install
|
||||
npm run build --if-present
|
||||
# npm run test --if-present
|
||||
workingDirectory: $(System.DefaultWorkingDirectory)/server
|
||||
displayName: 'npm install, build and test'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/server'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: Deploy
|
||||
environment: 'development'
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Azure Web App Deploy: vott'
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
appType: webAppLinux
|
||||
appName: $(webAppName)
|
||||
runtimeStack: 'NODE|10.10'
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
||||
startUpCommand: 'npm run start'
|
||||
gerado
+7135
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "vott-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Server to support VoTT with login",
|
||||
"main": "./lib/app.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node ./lib/app.js",
|
||||
"test:unit": "jest --coverage --detectOpenHandles",
|
||||
"test": "npm run lint && npm run test:unit",
|
||||
"watch": "concurrently --kill-others \"tsc -w\" \"nodemon --inspect ./lib/app.js\"",
|
||||
"lint": "tslint -q -p . -c tslint.json",
|
||||
"lint:fix": "tslint --fix -p . -c tslint.json"
|
||||
},
|
||||
"author": "Microsoft",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie-session": "^2.0.37",
|
||||
"body-parser": "^1.15.2",
|
||||
"bunyan": "*",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"cookie-session": "^1.3.3",
|
||||
"ejs": ">= 0.0.0",
|
||||
"ejs-locals": ">= 0.0.0",
|
||||
"express": "^4.17.1",
|
||||
"express-request-id": "^1.4.1",
|
||||
"method-override": "^3.0.0",
|
||||
"morgan": "^1.9.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"passport": "*",
|
||||
"passport-azure-ad": "^4.1.0",
|
||||
"ts-jest": "^24.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bunyan": "^1.8.6",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/express": "^4.17.1",
|
||||
"@types/express-request-id": "^1.4.1",
|
||||
"@types/jest": "^24.0.17",
|
||||
"@types/method-override": "0.0.31",
|
||||
"@types/morgan": "^1.7.37",
|
||||
"@types/node-fetch": "^2.5.0",
|
||||
"@types/passport": "^1.0.0",
|
||||
"@types/passport-azure-ad": "^4.0.3",
|
||||
"concurrently": "^4.1.1",
|
||||
"dotenv": "^8.1.0",
|
||||
"jest": "^24.8.0",
|
||||
"nodemon": "^1.19.1",
|
||||
"tslint": "^5.18.0",
|
||||
"typescript": "^3.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Welcome! Please log in.</h2>
|
||||
<a href="/login">Log In</a>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
<% if (!user) { %>
|
||||
<h2>Welcome! Please log in.</h2>
|
||||
<a href="/login">Log In</a>
|
||||
<% } else { %>
|
||||
<p>UPN: <%= user._json.upn %></p>
|
||||
<p>Profile ID: <%= user.id %></p>
|
||||
<p>Full Claims</p>
|
||||
<%- JSON.stringify(user) %>
|
||||
<p></p>
|
||||
<a href="/logout">Log Out</a>
|
||||
<% } %>
|
||||
@@ -0,0 +1,8 @@
|
||||
<% if (!user) { %>
|
||||
<h2>Welcome! Please log in.</h2>
|
||||
<a href="/login">Log In</a>
|
||||
<% } else { %>
|
||||
<h2>Hello, <%= user.displayName %>.</h2>
|
||||
<a href="/account">Account Info</a></br>
|
||||
<a href="/logout">Log Out</a>
|
||||
<% } %>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Passport-OpenID Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<% if (!user) { %>
|
||||
<p>
|
||||
<a href="/">Home</a> |
|
||||
<a href="/login">Log In</a>
|
||||
</p>
|
||||
<% } else { %>
|
||||
<p>
|
||||
<a href="/">Home</a> |
|
||||
<a href="/account">Account</a> |
|
||||
<a href="/logout">Log Out</a>
|
||||
</p>
|
||||
<% } %>
|
||||
<%- body %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,278 @@
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as morgan from 'morgan';
|
||||
import * as bunyan from 'bunyan';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import * as express from 'express';
|
||||
import cookieSession = require( 'cookie-session');
|
||||
import * as methodOverride from 'method-override';
|
||||
import * as passport from 'passport';
|
||||
import * as passportAzureAD from 'passport-azure-ad';
|
||||
import * as config from './config';
|
||||
import * as path from 'path';
|
||||
import * as express_request_id from 'express-request-id';
|
||||
|
||||
|
||||
const OIDCStrategyTemplate = {} as passportAzureAD.IOIDCStrategyOptionWithoutRequest;
|
||||
|
||||
const log = bunyan.createLogger({
|
||||
name: 'BUNYAN-LOGGER',
|
||||
src: true
|
||||
});
|
||||
|
||||
/******************************************************************************
|
||||
* Set up passport in the app
|
||||
******************************************************************************/
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// To support persistent login sessions, Passport needs to be able to
|
||||
// serialize users into and deserialize users out of the session. Typically,
|
||||
// this will be as simple as storing the user ID when serializing, and finding
|
||||
// the user by ID when deserializing.
|
||||
// -----------------------------------------------------------------------------
|
||||
passport.serializeUser((user: any, done) => {
|
||||
done(null, user.oid);
|
||||
});
|
||||
|
||||
passport.deserializeUser((oid: number, done) => {
|
||||
findByOid(oid, (err, user) => {
|
||||
done(err, user);
|
||||
});
|
||||
});
|
||||
|
||||
// array to hold logged in users
|
||||
const users: any[] = [];
|
||||
|
||||
const findByOid = (oid: number, fn: (err: Error, user: any) => void) => {
|
||||
log.info(`finding user by oid ${oid}`)
|
||||
for (let i = 0, len = users.length; i < len; i++) {
|
||||
const user = users[i];
|
||||
log.info('user: ', user);
|
||||
if (user.oid === oid) {
|
||||
return fn(null, user);
|
||||
}
|
||||
}
|
||||
return fn(null, null);
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Use the OIDCStrategy within Passport.
|
||||
//
|
||||
// Strategies in passport require a `verify` function, which accepts credentials
|
||||
// (in this case, the `oid` claim in id_token), and invoke a callback to find
|
||||
// the corresponding user object.
|
||||
//
|
||||
// The following are the accepted prototypes for the `verify` function
|
||||
// (1) function(iss, sub, done)
|
||||
// (2) function(iss, sub, profile, done)
|
||||
// (3) function(iss, sub, profile, access_token, refresh_token, done)
|
||||
// (4) function(iss, sub, profile, access_token, refresh_token, params, done)
|
||||
// (5) function(iss, sub, profile, jwtClaims, access_token, refresh_token, params, done)
|
||||
// (6) prototype (1)-(5) with an additional `req` parameter as the first parameter
|
||||
//
|
||||
// To do prototype (6), passReqToCallback must be set to true in the config.
|
||||
// -----------------------------------------------------------------------------
|
||||
passport.use(new passportAzureAD.OIDCStrategy({
|
||||
identityMetadata: config.creds.identityMetadata,
|
||||
clientID: config.creds.clientID,
|
||||
responseType: config.creds.responseType as typeof OIDCStrategyTemplate.responseType,
|
||||
responseMode: config.creds.responseMode as typeof OIDCStrategyTemplate.responseMode,
|
||||
redirectUrl: config.creds.redirectUrl,
|
||||
allowHttpForRedirectUrl: config.creds.allowHttpForRedirectUrl,
|
||||
clientSecret: config.creds.clientSecret,
|
||||
validateIssuer: config.creds.validateIssuer,
|
||||
isB2C: config.creds.isB2C,
|
||||
issuer: config.creds.issuer,
|
||||
passReqToCallback: false,
|
||||
scope: config.creds.scope,
|
||||
loggingLevel: config.creds.logLevel as typeof OIDCStrategyTemplate.loggingLevel,
|
||||
nonceLifetime: config.creds.nonceLifetime,
|
||||
nonceMaxAmount: config.creds.nonceMaxAmount,
|
||||
useCookieInsteadOfSession: config.creds.useCookieInsteadOfSession,
|
||||
cookieEncryptionKeys: config.creds.cookieEncryptionKeys,
|
||||
clockSkew: config.creds.clockSkew,
|
||||
},
|
||||
( iss: any, sub: any, profile: any, accessToken: any, refreshToken: any, done: any) => {
|
||||
if (!profile.oid) {
|
||||
return done(new Error('No oid found'), null);
|
||||
}
|
||||
// asynchronous verification, for effect...
|
||||
process.nextTick(() => {
|
||||
findByOid(profile.oid, (err, user) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
if (!user) {
|
||||
// "Auto-registration"
|
||||
log.info(`storing user`, profile)
|
||||
users.push(profile);
|
||||
return done(null, profile);
|
||||
}
|
||||
return done(null, user);
|
||||
});
|
||||
});
|
||||
},
|
||||
));
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Config the app, include middlewares
|
||||
// -----------------------------------------------------------------------------
|
||||
const app = express();
|
||||
app.use(morgan(config.httpLogFormat));
|
||||
app.set('views', path.join(__dirname, '../public/views'));
|
||||
app.set('view engine', 'ejs');
|
||||
app.use(express_request_id());
|
||||
app.use(methodOverride());
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(cookieSession({ secret: 'keyboard cat', maxAge: 1000 * 60 * 60 * 24 * 365 }));
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
// Initialize Passport! Also use passport.session() middleware, to support
|
||||
// persistent login sessions (recommended).
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Set up the route controller
|
||||
//
|
||||
// 1. For 'login' route and 'returnURL' route, use `passport.authenticate`.
|
||||
// This way the passport middleware can redirect the user to login page, receive
|
||||
// id_token etc from returnURL.
|
||||
//
|
||||
// 2. For the routes you want to check if user is already logged in, use
|
||||
// `ensureAuthenticated`. It checks if there is an user stored in session, if not
|
||||
// it will call `passport.authenticate` to ask for user to log in.
|
||||
// -----------------------------------------------------------------------------
|
||||
function ensureAuthenticated(req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
if (req.isAuthenticated()) { return next(); }
|
||||
res.redirect('/login');
|
||||
}
|
||||
|
||||
function ensureAuthenticatedApi(req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
if (req.isAuthenticated()) { return next(); }
|
||||
res.sendStatus(401).end();
|
||||
return next();
|
||||
}
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', { user: req.user });
|
||||
});
|
||||
|
||||
// '/account' is only available to logged in user
|
||||
app.get('/account', ensureAuthenticated, (req, res, next) => {
|
||||
res.render('account', { user: req.user });
|
||||
});
|
||||
|
||||
app.get('/login',
|
||||
(req, res, next) => {
|
||||
log.info('testing');
|
||||
passport.authenticate('azuread-openidconnect',
|
||||
{
|
||||
response: res, // required
|
||||
// resourceURL: config.creds.redirectUrl, // optional. Provide a value if you want to specify the resource.
|
||||
customState: 'my_state', // optional. Provide a value if you want to provide custom state value.
|
||||
failureRedirect: '/',
|
||||
// session: false,
|
||||
} as passport.AuthenticateOptions,
|
||||
)(req, res, next);
|
||||
},
|
||||
(req, res) => {
|
||||
log.info('login was called');
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// 'GET returnURL'
|
||||
// `passport.authenticate` will try to authenticate the content returned in
|
||||
// query (such as authorization code). If authentication fails, user will be
|
||||
// redirected to '/' (home page); otherwise, it passes to the next middleware.
|
||||
app.get('/auth/openid/return',
|
||||
(req, res, next) => {
|
||||
passport.authenticate('azuread-openidconnect',
|
||||
{
|
||||
response: res, // required
|
||||
failureRedirect: '/',
|
||||
// session: false,
|
||||
|
||||
} as passport.AuthenticateOptions,
|
||||
)(req, res, next);
|
||||
},
|
||||
(req, res, next) => {
|
||||
log.info('received a return from AzureAD.');
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// 'POST returnURL'
|
||||
// `passport.authenticate` will try to authenticate the content returned in
|
||||
// body (such as authorization code). If authentication fails, user will be
|
||||
// redirected to '/' (home page); otherwise, it passes to the next middleware.
|
||||
app.post('/auth/openid/return',
|
||||
(req, res, next) => {
|
||||
passport.authenticate('azuread-openidconnect',
|
||||
{
|
||||
response: res, // required
|
||||
failureRedirect: '/',
|
||||
// session: false,
|
||||
} as passport.AuthenticateOptions,
|
||||
)(req, res, next);
|
||||
},
|
||||
(req, res, next) => {
|
||||
log.info('received a return from AzureAD.');
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// 'logout' route, logout from passport, and destroy the session with AAD.
|
||||
app.get('/logout', (req, res) => {
|
||||
delete req.session;
|
||||
// req.session.destroy((err) => {
|
||||
req.logOut();
|
||||
res.redirect(config.destroySessionUrl);
|
||||
// });
|
||||
});
|
||||
|
||||
const cloudConnections = new Map<string, any>([
|
||||
['connection1', { foo: 'bar' }],
|
||||
['connection2', { foo: 'baz' }],
|
||||
]);
|
||||
|
||||
app.get('/api/v1.0/cloudconnections', ensureAuthenticatedApi,
|
||||
(req, res, next) => {
|
||||
res.json(Array.from(cloudConnections.keys()));
|
||||
res.end();
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
|
||||
(req, res, next) => {
|
||||
const id = req.params.id;
|
||||
res.json(cloudConnections.get(id));
|
||||
res.end();
|
||||
next();
|
||||
});
|
||||
|
||||
app.put('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
|
||||
(req, res, next) => {
|
||||
const id = req.params.id;
|
||||
const body = req.body;
|
||||
const status = cloudConnections.has(id) ? 200 : 201;
|
||||
cloudConnections.set(id, body);
|
||||
res.sendStatus(status);
|
||||
res.json(body);
|
||||
next();
|
||||
});
|
||||
|
||||
app.delete('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
|
||||
(req, res, next) => {
|
||||
const id = req.params.id;
|
||||
if (cloudConnections.has(id)) {
|
||||
res.sendStatus(404).end();
|
||||
return next();
|
||||
}
|
||||
cloudConnections.delete(id);
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use('/public', express.static(path.join(__dirname, '../public')));
|
||||
|
||||
app.listen(config.port);
|
||||
@@ -0,0 +1,101 @@
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
require('dotenv').config();
|
||||
|
||||
export const baseUrl = process.env.BASE_URL || 'http://localhost:3000/';
|
||||
export const redirectPath = 'auth/openid/return';
|
||||
export const port = process.env.PORT || '3000';
|
||||
export let loggingLevel = process.env.LOGGING_LEVEL || 'info';
|
||||
export let httpLogFormat = process.env.HTTP_LOG_FORMAT || 'dev';
|
||||
|
||||
export let creds = {
|
||||
// Required
|
||||
identityMetadata: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
|
||||
// 'https://login.microsoftonline.com/<tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration',
|
||||
// or equivalently: 'https://login.microsoftonline.com/<tenant_guid>/v2.0/.well-known/openid-configuration'
|
||||
//
|
||||
// or you can use the common endpoint
|
||||
// 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration'
|
||||
// To use the common endpoint, you have to either turn `validateIssuer` off, or provide the `issuer` value.
|
||||
|
||||
// Required, the client ID of your app in AAD
|
||||
clientID: process.env.APP_ID,
|
||||
|
||||
// Required, must be 'code', 'code id_token', 'id_token code' or 'id_token'
|
||||
// If you want to get access_token, you must use 'code', 'code id_token' or 'id_token code'
|
||||
responseType: 'code id_token',
|
||||
|
||||
// Required
|
||||
responseMode: 'form_post',
|
||||
|
||||
// Required, the reply URL registered in AAD for your app
|
||||
|
||||
redirectUrl: baseUrl + redirectPath,
|
||||
|
||||
// Required if we use http for redirectUrl
|
||||
allowHttpForRedirectUrl: process.env.ALLOW_HTTP ? process.env.ALLOW_HTTP === 'true' : true,
|
||||
|
||||
// Required if `responseType` is 'code', 'id_token code' or 'code id_token'.
|
||||
// If app key contains '\', replace it with '\\'.
|
||||
clientSecret: process.env.APP_SECRET,
|
||||
|
||||
// Required to set to false if you don't want to validate issuer
|
||||
validateIssuer: false,
|
||||
|
||||
// Required if you want to provide the issuer(s) you want to validate instead of using the issuer from metadata
|
||||
// issuer could be a string or an array of strings of the following form: 'https://sts.windows.net/<tenant_guid>/v2.0'
|
||||
issuer: null as string,
|
||||
|
||||
// !Bug - must be false in this sample
|
||||
// Required to set to true if the `verify` function has 'req' as the first parameter
|
||||
passReqToCallback: true,
|
||||
|
||||
// Recommended to set to true. By default we save state in express session, if this option is set to true, then
|
||||
// we encrypt state and save it in cookie instead. This option together with { session: false } allows your app
|
||||
// to be completely express session free.
|
||||
useCookieInsteadOfSession: true,
|
||||
|
||||
logLevel: loggingLevel,
|
||||
|
||||
// Required if `useCookieInsteadOfSession` is set to true. You can provide multiple set of key/iv pairs for key
|
||||
// rollover purpose. We always use the first set of key/iv pair to encrypt cookie, but we will try every set of
|
||||
// key/iv pair to decrypt cookie. Key can be any string of length 32, and iv can be any string of length 12.
|
||||
cookieEncryptionKeys: (process.env.COOKIES_SECRETS ? JSON.parse(process.env.COOKIES_SECRETS) :
|
||||
[
|
||||
{ key: '12345678901234567890123456789012', iv: '123456789012' },
|
||||
{ key: 'abcdefghijklmnopqrstuvwxyzabcdef', iv: 'abcdefghijkl' },
|
||||
]) as Array<{ key: string; iv: string; }>,
|
||||
|
||||
// The additional scopes we want besides 'openid'.
|
||||
// 'profile' scope is required, the rest scopes are optional.
|
||||
// (1) if you want to receive refresh_token, use 'offline_access' scope
|
||||
// (2) if you want to get access_token for graph api, use the graph api url like 'https://graph.microsoft.com/mail.read'
|
||||
scope: ['profile', 'offline_access', 'https://graph.microsoft.com/user.readwrite'],
|
||||
|
||||
|
||||
// Optional. The lifetime of nonce in session or cookie, the default value is 3600 (seconds).
|
||||
nonceLifetime: null as number,
|
||||
|
||||
// Optional. The max amount of nonce saved in session or cookie, the default value is 10.
|
||||
nonceMaxAmount: 5,
|
||||
|
||||
// Optional. The clock skew allowed in token validation, the default value is 300 seconds.
|
||||
clockSkew: null as number,
|
||||
|
||||
// Optional. Is B2C
|
||||
isB2C: false,
|
||||
};
|
||||
|
||||
// The url you need to go to destroy the session with AAD
|
||||
export let destroySessionUrl = 'https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=http://localhost:3000';
|
||||
|
||||
// If you want to use the mongoDB session store for session middleware, set to true; otherwise we will use the default
|
||||
// session store provided by express-session.
|
||||
// Note that the default session store is designed for development purpose only.
|
||||
export let useMongoDBSessionStore = false;
|
||||
|
||||
// If you want to use mongoDB, provide the uri here for the database.
|
||||
export let databaseUri = 'mongodb://localhost/OIDCStrategy';
|
||||
|
||||
// How long you want to keep session in mongoDB.
|
||||
export let mongoDBSessionMaxAge = 24 * 60 * 60; // 1 day (unit is second)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
/*
|
||||
* GET home page.
|
||||
*/
|
||||
|
||||
exports.index = function(req, res){
|
||||
res.render('index', { title: 'Express' });
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
/*
|
||||
* GET users listing.
|
||||
*/
|
||||
|
||||
exports.list = function(req, res){
|
||||
res.send("respond with a resource");
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
import * as http from 'http';
|
||||
import * as restify from 'restify';
|
||||
|
||||
import { OpenTypeExtension, OutlookTask } from '@microsoft/microsoft-graph-types-beta';
|
||||
import { User } from './users';
|
||||
import { logger } from './utils';
|
||||
|
||||
export class Server {
|
||||
public server: restify.Server;
|
||||
|
||||
constructor(port: string, requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void) {
|
||||
this.server = restify.createServer({ maxParamLength: 1000 } as restify.ServerOptions);
|
||||
configureServer(this.server);
|
||||
this.server.listen(port, () => {
|
||||
console.log(logger`${this.server.name} listening to ${this.server.url}`);
|
||||
});
|
||||
}
|
||||
|
||||
public async asyncClose(callback?: () => void): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.server.close(() => {
|
||||
console.log('Closed httpServer');
|
||||
if (callback) { callback(); }
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function configureServer(httpServer: restify.Server) {
|
||||
|
||||
httpServer.pre((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Headers', 'X-Requested-With');
|
||||
return next();
|
||||
});
|
||||
|
||||
httpServer.use(restify.plugins.bodyParser());
|
||||
httpServer.use(restify.plugins.queryParser());
|
||||
|
||||
httpServer.use((req, res, next) => {
|
||||
console.log(logger`Request for ${req.url} `);
|
||||
next();
|
||||
});
|
||||
|
||||
//// Static pages
|
||||
|
||||
httpServer.get('/', (req, res, next) => { res.redirect('./public/app.html', next); });
|
||||
httpServer.get('/public/app.html*', restify.plugins.serveStatic({ directory: __dirname + '/../public', file: 'app.html' }));
|
||||
httpServer.get('/public/*', restify.plugins.serveStatic({ directory: __dirname + '/..' }));
|
||||
|
||||
//// Authentication logic for Web
|
||||
|
||||
httpServer.get('/login', (req, res, next) => {
|
||||
const host = req.headers.host;
|
||||
const protocol = host.toLowerCase().includes('localhost') || host.includes('127.0.0.1') ? 'http://' : 'https://';
|
||||
const authUrl = app.authManager.authUrl({ redirect: new URL(AppConfig.authPath, protocol + host).href, state: protocol + host });
|
||||
console.log(logger`redirecting to ${authUrl} `);
|
||||
res.redirect(authUrl, next);
|
||||
});
|
||||
|
||||
httpServer.get('/auth', async (req, res, next) => {
|
||||
try {
|
||||
// look for authorization code coming in (indicates redirect from interative login/consent)
|
||||
const code = req.query.code;
|
||||
if (code) {
|
||||
const host = req.headers.host;
|
||||
const protocol = host.toLowerCase().includes('localhost') || host.includes('127.0.0.1') ? 'http://' : 'https://';
|
||||
const authContext = await app.authManager.newContextFromCode(code, protocol + host + '/auth');
|
||||
const profile = await app.graph.getProfile(await app.authManager.getAccessToken(authContext));
|
||||
const user: User = { oid: authContext.oid, authKey: authContext.authKey, authTokens: authContext };
|
||||
if (profile.preferredName) { user.preferredName = profile.preferredName; }
|
||||
if (profile.mail) { user.email = profile.mail; }
|
||||
await app.users.set(authContext.oid, user);
|
||||
res.header('Set-Cookie', 'userId=' + authContext.authKey + '; expires=' + new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString());
|
||||
const stateString: string = req.query.state;
|
||||
let state: any = {};
|
||||
try { state = JSON.parse(stateString); } catch (e) {
|
||||
console.log(logger`bad state string`);
|
||||
}
|
||||
if (!state.url) { state.url = '/'; }
|
||||
if (state.key) {
|
||||
// should send verification code to user via web and wait for it on the bot.
|
||||
// ignore for now.
|
||||
const conversation = await app.conversationManager.setOidForUnauthenticatedConversation(state.key, authContext.oid);
|
||||
await app.botService.processActivityInConversation(conversation, async (turnContext) => {
|
||||
return await turnContext.sendActivity('Connected.');
|
||||
});
|
||||
} // else no state.key so it is a plain web login
|
||||
res.redirect(state.url, next);
|
||||
return;
|
||||
}
|
||||
} catch (reason) {
|
||||
console.log('Error in /auth processing: ' + reason);
|
||||
}
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(htmlPageMessage('Error', 'Request to authorize failed', '<br/><a href="/">Continue</a>'));
|
||||
next();
|
||||
return;
|
||||
});
|
||||
|
||||
httpServer.get('/task/:taskId', async (req, res, next) => {
|
||||
try {
|
||||
const authContext = await app.authManager.getAuthContextFromAuthKey(getCookie(req, 'userId'));
|
||||
if (!authContext || !authContext.oid) {
|
||||
console.log('not logged in');
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(htmlPageMessage('Task', 'Not logged in.', '<br/><a href="/">Continue</a>'));
|
||||
return next();
|
||||
}
|
||||
const taskId = req.params.taskId;
|
||||
const accessToken = await app.authManager.getAccessTokenFromAuthKey(authContext.authKey);
|
||||
const data = await app.graph.get(accessToken, `https://graph.microsoft.com/beta/me/outlook/tasks/${taskId}?${app.graph.queryExpandNagExtensions}`);
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(htmlPageFromObject('task', '', data, '<br/><a href="/">Continue</a>'));
|
||||
return next();
|
||||
} catch (err) {
|
||||
console.log(`GET /task failed. (${err}()`);
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(htmlPageFromObject('Task', 'Error. Are you logged in', err, '<br/><a href="/">Continue</a>'));
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
// APIs - no html - just json response
|
||||
|
||||
httpServer.get('/api/v1.0/me', async (req, res, next) => {
|
||||
await graphGet(req, res, next, 'https://graph.microsoft.com/v1.0/me');
|
||||
});
|
||||
|
||||
httpServer.get('/api/v1.0/me/connections', async (req, res, next) => {
|
||||
let error: any;
|
||||
try {
|
||||
const accessToken = await app.authManager.getAccessTokenFromAuthKey(getCookie(req, 'userId'));
|
||||
const conversations = await app.graph.getConversations(accessToken);
|
||||
res.json(200, conversations);
|
||||
res.end();
|
||||
return next();
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
res.status(400);
|
||||
res.json({ error });
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
|
||||
httpServer.patch('/api/v1.0/me/connections/:id', async (req, res, next) => {
|
||||
const id = req.params.id; // this is ignored for now
|
||||
const data = req.body;
|
||||
let error: any;
|
||||
try {
|
||||
const authContext = await app.authManager.getAuthContextFromAuthKey(getCookie(req, 'userId'));
|
||||
if (!authContext || !authContext.oid) { throw new Error('/me/connections-PATCH: Could not identify user'); }
|
||||
app.conversationManager.upsert(authContext.oid, data);
|
||||
res.status(200);
|
||||
res.end();
|
||||
return next();
|
||||
} catch (err) { error = error; }
|
||||
res.status(400);
|
||||
res.json({ error });
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
|
||||
httpServer.del('/api/v1.0/me/connections/:id', async (req, res, next) => {
|
||||
const id = req.params.id; // this is ignored for now
|
||||
const data = req.body;
|
||||
let error: any;
|
||||
try {
|
||||
const authContext = await app.authManager.getAuthContextFromAuthKey(getCookie(req, 'userId'));
|
||||
if (!authContext || !authContext.oid) { throw new Error('/me/connections-PATCH: Could not identify user'); }
|
||||
app.conversationManager.delete(authContext.oid, data);
|
||||
res.status(200);
|
||||
res.end();
|
||||
return next();
|
||||
} catch (err) { error = err; }
|
||||
res.status(400);
|
||||
res.json({ error });
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
|
||||
|
||||
//// Automatic response generators for graph information
|
||||
|
||||
async function graphGet(req: restify.Request, res: restify.Response, next: restify.Next, url: string, composer?: (result: any) => string) {
|
||||
let errorMessage: string | null = null;
|
||||
try {
|
||||
const accessToken = await app.authManager.getAccessTokenFromAuthKey(getCookie(req, 'userId'));
|
||||
const data = await app.graph.get(accessToken, url);
|
||||
if (data) {
|
||||
if (composer) {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(composer(data));
|
||||
} else {
|
||||
res.json(data);
|
||||
res.end();
|
||||
}
|
||||
return next();
|
||||
}
|
||||
errorMessage = 'No value';
|
||||
} catch (err) {
|
||||
errorMessage = 'graphForwarder error. Detail: ' + err;
|
||||
}
|
||||
if (composer) {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(htmlPageFromList('Error', errorMessage, [], '<a href="/">Continue</a>'));
|
||||
} else {
|
||||
res.status(400);
|
||||
res.json({ errorMessage });
|
||||
res.end();
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
async function graphPatch(req: restify.Request, res: restify.Response, next: restify.Next, url: string, data: string) {
|
||||
let errorMessage = '';
|
||||
try {
|
||||
const accessToken = await app.authManager.getAccessTokenFromAuthKey(getCookie(req, 'userId'));
|
||||
const result = await app.graph.patch(accessToken, url, data);
|
||||
res.json(200, result);
|
||||
res.end();
|
||||
return next();
|
||||
} catch (err) {
|
||||
errorMessage = 'graphForwarder error. Detail: ' + err;
|
||||
}
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(htmlPageFromList('Error', errorMessage, [], '<a href="/">Continue</a>'));
|
||||
return next();
|
||||
}
|
||||
|
||||
//// Utiliies
|
||||
|
||||
function getCookie(req: restify.Request, key: string): string {
|
||||
const list = {} as { [index: string]: string };
|
||||
const rc = req.header('cookie');
|
||||
|
||||
if (rc) {
|
||||
rc.split(';').forEach((cookie) => {
|
||||
const parts = cookie.split('=');
|
||||
const name = parts.shift().trim();
|
||||
if (name) { list[name] = decodeURI(parts.join('=')); }
|
||||
});
|
||||
}
|
||||
|
||||
return (key && key in list) ? list[key] : null;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./lib", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
/* Strict Type-Checking Options */
|
||||
// "strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
// "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"data_models",
|
||||
"public",
|
||||
"lib",
|
||||
"temp",
|
||||
"src/__tests__"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-escape",
|
||||
"avoid-template"
|
||||
],
|
||||
"max-line-length": {
|
||||
"severity": "warning",
|
||||
"options": [
|
||||
160,
|
||||
{
|
||||
"ignore-pattern": "^import |^export {(.*?)} | //"
|
||||
}
|
||||
]
|
||||
},
|
||||
"no-console": [
|
||||
false
|
||||
],
|
||||
"max-classes-per-file": false,
|
||||
"ordered-imports": [
|
||||
false
|
||||
],
|
||||
"object-literal-sort-keys": false
|
||||
}
|
||||
}
|
||||
Referência em uma Nova Issue
Bloquear um usuário