Comparar commits

...

55 Commits

Autor SHA1 Mensagem Data
James Trott 6efec4d56e updates to support navigable debug with bunyan in VSCode + minor cleanup 2019-08-23 14:03:37 -07:00
James Trott 7d91521374 resolve merge from johnshew/login, added additional logging 2019-08-23 13:37:58 -07:00
John Shewchuk 601e19dc3a Merge branch 'johnshew/login' of https://github.com/microsoft/VoTT into johnshew/login 2019-08-23 13:28:21 -07:00
John Shewchuk e45c9e8a43 Remove server session dependency 2019-08-23 13:28:02 -07:00
John Shewchuk 1d875fe04c Remove server session dependency 2019-08-23 13:26:46 -07:00
John Shewchuk f620b2aa33 Adjust scope to read/wrote profile not email 2019-08-23 12:11:16 -07:00
John Shewchuk eb8d4b4a8b redirect support and clean up
* improve logging
* add .env.template
* move more to .env and config
2019-08-23 09:49:49 -07:00
John Shewchuk 56407823b9 get PORT from env 2019-08-22 17:16:28 -07:00
John Shewchuk 1f428b3e41 Merge branch 'johnshew/login' of https://github.com/microsoft/VoTT into johnshew/login 2019-08-22 17:02:08 -07:00
John Shewchuk 41b8214ae4 Listen on 80 for deployment to Azure 2019-08-22 17:01:21 -07:00
My Ho aa20c45723 fix start script 2019-08-22 15:57:42 -07:00
John Shewchuk 6d52386bc2 Disable linting 2019-08-22 15:22:17 -07:00
John Shewchuk ef766911ee Update deployment directories 2019-08-22 15:19:22 -07:00
John Shewchuk f2ae6de538 fix yml bug - extra pool 2019-08-22 15:08:13 -07:00
John Shewchuk 8137d1a1cd Update deployment 2019-08-22 15:03:48 -07:00
John Shewchuk e54a27655f Set up CI with Azure Pipelines
[skip ci]
2019-08-22 14:54:57 -07:00
John Shewchuk d8407839c3 rename yaml file 2019-08-22 14:53:49 -07:00
John Shewchuk bfa5829c42 Added yaml and fixed logging 2019-08-22 14:43:49 -07:00
John Shewchuk 666e2d0c56 Skeleton APIs running
* Updatres to express to serve static pages
* In memory connection management
2019-08-22 13:42:11 -07:00
John Shewchuk 6203d61587 Security and execution environment
* seperate out config into .env
* move views to public directory
2019-08-22 12:09:57 -07:00
John Shewchuk 52042db1dc Merge branch 'master' of https://github.com/microsoft/VoTT into johnshew/login 2019-08-22 10:07:10 -07:00
John Shewchuk c2bb5e8f37 working server
* updated all node modules
* converted to express 4
* converted to typescript
* fixed all ts-lint issues
2019-08-22 10:06:41 -07:00
John Shewchuk 94830cc1f7 initial typescript server 2019-08-21 16:05:47 -07:00
John Shewchuk 586aebad04 port 2019-08-20 12:19:29 -07:00
Wallace Breza 745e854cc4 Release 2.1.0 (#790)
Updates package version and changelog for 2.1.0 release
2019-04-29 14:39:24 -07:00
Wallace Breza 2234c8a0cc fix: Updates backwards compat & fixes cntk export image bug (#789)
Fixes an issue where the images exported out of a video file were missing file extension for video projects.
2019-04-29 14:18:45 -07:00
Wallace Breza 4d02db4215 fix: Updates export options for pascalVOC rename (#788)
Adds a check during project load to update the export options if project was using previous pascalVOC name.
2019-04-29 14:18:45 -07:00
Lee, Jebum 90754dc74b fix: change method for alloc string to buffer (#777)
String.length is not appropriate for calculating buffer size
when non-alphabet letter is included in content.
Change the method Buffer.alloc to Buffer.from as directed by the nodejs document.
2019-04-29 14:18:45 -07:00
Jacopo Mangiavacchi f29963c89e feat: Add CSV Exporter (#757)
Adds CSV export provider
2019-04-29 14:18:45 -07:00
Tanner Barlow acbbc86151 fix: Fix display of tag color picker (#782)
Resolves issue of tag color picker not being shown on alt-click or color-click + edit button. Also adds several tests for increased test coverage of tagInput.tsx
2019-04-29 14:18:45 -07:00
Wallace Breza 921dbac155 feat: Active Learning Updates (#778)
Adds new active learning form
Moves active learning settings from project settings to here
Refactored and created activeLearningService
2019-04-29 14:18:45 -07:00
P.J. Little a2ef52c7a4 docs: updates to readme and changelog (#781)
Minor updates and corrections to the main readme and changelog.
2019-04-29 14:18:45 -07:00
Wallace Breza 25b4aa2dc8 Create CODE_OF_CONDUCT.md (#779)
Adds code of conduct
2019-04-29 14:18:45 -07:00
Wallace Breza 0429590bec doc: Add bug & feature templates (#780)
Adds bug and feature github templates
2019-04-29 14:18:45 -07:00
Wallace Breza 48805dcb85 test: Verify tag update/delete project actions 2019-04-29 14:18:45 -07:00
Wallace Breza 0b06d6ac5b test: Refactored editor page tests 2019-04-29 14:18:45 -07:00
Wallace Breza 3998b6efc8 fix: Refactored project tag/delete updates 2019-04-29 14:18:45 -07:00
Tanner Barlow 4a0dcb2905 Dummy commit to kick off build again 2019-04-29 14:18:45 -07:00
Tanner Barlow 354623ec21 Lint fixes 2019-04-29 14:18:45 -07:00
Tanner Barlow 8b34db5724 Clean up and docs 2019-04-29 14:18:45 -07:00
Tanner Barlow bbd83a4df5 Fix tag removal test for editor page 2019-04-29 14:18:45 -07:00
Tanner Barlow 5b4610b3d9 Rename tag function in editor page 2019-04-29 14:18:45 -07:00
Tanner Barlow 996a555333 Delete tag working 2019-04-29 14:18:45 -07:00
Tanner Barlow 8439574dc5 Saving assets in async foreach loop 2019-04-29 14:18:45 -07:00
Tanner Barlow 4193bc0e6a Register mixins and run async loop 2019-04-29 14:18:45 -07:00
Tanner Barlow f394ea3d10 Editor Page and canvas upates 2019-04-29 14:18:45 -07:00
Tanner Barlow 4f325dfe4b Split out project functions and asset functions into respective services 2019-04-29 14:18:45 -07:00
Tanner Barlow 2ef4e1387f Added confirm and strings for tag and rename operations 2019-04-29 14:18:45 -07:00
Tanner Barlow 1001528a16 Added tests for project service 2019-04-29 14:18:45 -07:00
Tanner Barlow 6974aef9d1 Project service functions 2019-04-29 14:18:45 -07:00
Tanner Barlow 37234ec2e9 refactor: Remove editor footer 2019-04-29 14:18:45 -07:00
Wallace Breza d6a059447d Fix: Enables selection of azure region for custom vision export (#765)
Adds ability to select azure region where your custom vision service is hosted.
Filters domain list by project type

Resolves #759, #770
2019-04-29 14:18:45 -07:00
Wallace Breza c10c971caf feat: CNTK Export Provider (#771)
Adds CNTK export provider into v2

Resolves #754
2019-04-29 14:18:45 -07:00
Wallace Breza 0fe63863b1 feat: Save partial project progress during project creation (#769)
This adds functionality to persist partial project information when creating a new project. Right now when creating a new connection inline within the create project flow and returning to the create project screen your partial project information is lost. Partial form progress is now saved into local storage and bound when returning to the form.

Resolves #758
2019-04-29 14:18:45 -07:00
Jacopo Mangiavacchi 39521f2b61 Fix ymax and rename Tensorflow nama everywhere (#763)
fix issue #760 [AB#18223]
2019-04-29 14:18:45 -07:00
22 arquivos alterados com 8145 adições e 2 exclusões
+6
Ver Arquivo
@@ -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
+18
Ver Arquivo
@@ -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)
+9 -1
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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",
+5
Ver Arquivo
@@ -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/
+23
Ver Arquivo
@@ -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",
}
]
}
+3
Ver Arquivo
@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}
+24
Ver Arquivo
@@ -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"
}
]
}
+75
Ver Arquivo
@@ -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'
+7135
Ver Arquivo
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
+56
Ver Arquivo
@@ -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"
}
}
+13
Ver Arquivo
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h2>Welcome! Please log in.</h2>
<a href="/login">Log In</a>
</body>
</html>
+11
Ver Arquivo
@@ -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>
<% } %>
+8
Ver Arquivo
@@ -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>
<% } %>
+21
Ver Arquivo
@@ -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>
+278
Ver Arquivo
@@ -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);
+101
Ver Arquivo
@@ -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)
+8
Ver Arquivo
@@ -0,0 +1,8 @@
/*
* GET home page.
*/
exports.index = function(req, res){
res.render('index', { title: 'Express' });
};
+8
Ver Arquivo
@@ -0,0 +1,8 @@
/*
* GET users listing.
*/
exports.list = function(req, res){
res.send("respond with a resource");
};
+249
Ver Arquivo
@@ -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;
}
+60
Ver Arquivo
@@ -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__"
]
}
+32
Ver Arquivo
@@ -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
}
}