feat: add web server for login support

Esse commit está contido em:
John Shewchuk
2019-09-03 13:45:23 -07:00
commit de GitHub
commit 54473577f8
22 arquivos alterados com 8344 adições e 0 exclusões
+6
Ver Arquivo
@@ -19,6 +19,7 @@
# misc # misc
.DS_Store .DS_Store
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
@@ -37,3 +38,8 @@ secrets.sh
# complexity reports # complexity reports
es6-src/ es6-src/
report/ report/
# VoTT Server
server/lib
server/node_modules
server/coverage
+8
Ver Arquivo
@@ -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": { "extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+1
Ver Arquivo
@@ -23,6 +23,7 @@
"buffer-reverse": "^1.0.1", "buffer-reverse": "^1.0.1",
"crypto-js": "^3.1.9-1", "crypto-js": "^3.1.9-1",
"dotenv": "^7.0.0", "dotenv": "^7.0.0",
"express-request-id": "^1.4.1",
"google-protobuf": "^3.6.1", "google-protobuf": "^3.6.1",
"jpeg-js": "^0.3.4", "jpeg-js": "^0.3.4",
"json2csv": "^4.5.0", "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/
+35
Ver Arquivo
@@ -0,0 +1,35 @@
{
// 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 via NPM",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"debug"
],
"port": 9229
},
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/lib/app.js", //"${workspaceFolder}\\lib\\app.js",
"args": [
"|",
"bunyan"
],
"outFiles": [
"${workspaceFolder}/**/*.js"
],
"console": "internalConsole",
"outputCapture": "std",
}
]
}
+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'
+6
Ver Arquivo
@@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
rootDir: "src",
coverageDirectory: "../coverage",
};
+7320
Ver Arquivo
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
+62
Ver Arquivo
@@ -0,0 +1,62 @@
{
"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 --runInBand",
"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",
"debug": "nodemon --inspect ./lib/app.js | bunyan"
},
"author": "Microsoft",
"license": "MIT",
"dependencies": {
"@microsoft/microsoft-graph-client": "^1.7.0",
"body-parser": "^1.15.2",
"bunyan": "*",
"cookie-parser": "^1.4.3",
"cookie-session": "^1.3.3",
"cookies": "^0.7.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",
"simple-oauth2": "^2.2.1"
},
"devDependencies": {
"@types/bunyan": "^1.8.6",
"@types/cookie-parser": "^1.4.2",
"@types/cookie-session": "^2.0.37",
"@types/cookies": "^0.7.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.1",
"@types/passport-azure-ad": "^4.0.3",
"@types/simple-oauth2": "^2.2.1",
"concurrently": "^4.1.1",
"dotenv": "^8.1.0",
"jest": "^24.8.0",
"nodemon": "^1.19.1",
"ts-jest": "^24.0.2",
"tslint": "^5.18.0",
"typescript": "^3.6.2"
},
"engines": {
"node": ">= 10.0.0"
}
}
+71
Ver Arquivo
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html>
<head>
<title>Simple Integration Tests</title>
</head>
<body>
<h2>Simple integration tests</h2>
<p>You must be logged in to use tests</p>
<button onclick="test_get_me()">GET /api/v1.0/me</button><br />
<button onclick="test_get_profile()">GET /api/v1.0/profile</button><br />
<button onclick="test_put_profile()">PUT /api/v1.0/profile { "foo": "bar" }</button><br />
<br />
<button onclick="test_get_connection()">GET /api/v1.0/cloudconnections/connection1</button><br />
<button onclick="test_put_connection()">PUT /api/v1.0/cloudconnections/connection1 { "foo": "bar" }</button><br />
<button onclick="test_put_connection_alt()">PUT /api/v1.0/cloudconnections/connection1 { "foo": "baz" }</button><br />
<button onclick="test_patch_connection()">PATCH /api/v1.0/cloudconnections/connection1 { "updated": ${now} }</button><br />
<button onclick="test_delete_connection()">DELETE /api/v1.0/cloudconnections/connection1</button><br />
<pre id='result'></pre>
<script>
function fetchOptions(method, thing) {
return { method, body: JSON.stringify(thing), headers: { 'Content-Type': 'application/json' } };
}
async function displayResponse(response) {
let json = null;
try { json = await response.json().catch(); } catch (err) { }
let result = (response.ok ? '*success*' : '*failed*') + '\n' + (json ? JSON.stringify(json, undefined, 2) : '');
document.getElementById("result").innerHTML = result;
}
async function test_get_me(e) {
let response = await fetch('/api/v1.0/me');
displayResponse(response);
}
async function test_get_profile(e) {
let response = await fetch('/api/v1.0/profile');
displayResponse(response);
}
async function test_put_profile(e) {
let response = await fetch('/api/v1.0/profile', fetchOptions("PUT", { foo: "bar" }));
displayResponse(response);
}
async function test_get_connection(e) {
let response = await fetch('/api/v1.0/cloudconnections/connection1');
displayResponse(response);
}
async function test_put_connection(e) {
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PUT", { foo: "bar" }));
displayResponse(response);
}
async function test_put_connection_alt(e) {
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PUT", { foo: "baz" }));
displayResponse(response);
}
async function test_patch_connection(e) {
let now = (new Date(Date.now())).toISOString();
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PATCH", { updated: now }));
displayResponse(response);
}
async function test_delete_connection(e) {
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("DELETE", undefined));
displayResponse(response);
}
</script>
</body>
</html>
+8
Ver Arquivo
@@ -0,0 +1,8 @@
<% if (!user) { %>
<h2>Welcome! Please log in.</h2>
<a href="/login">Log In</a>
<% } else { %>
<p>Profile ID: <%= user.oid %></p>
<p>Email: <%= user.mail %></p>
<pre><%- JSON.stringify(user, null, 2) %></pre>
<% } %>
+14
Ver Arquivo
@@ -0,0 +1,14 @@
<% if (!user) { %>
<h2>Welcome! Please log in.</h2>
<a href="/login">Log In</a></br>
<a href="https://myapps.microsoft.com">Manage your permissions (organization)</a></br>
<a href="https://account.live.com/consent/Manage">Manage account permissions (personal)</a>
<% } else { %>
<h2>Hello, <%= user.displayName %></h2>
<a href="/account">Account Info</a></br>
<a href="/public/test.html">Run tests</a></br>
<a href="/endsession">End session</a></br>
<a href="/logout">Log Out</a></br>
<a href="https://myapps.microsoft.com">Manage your permissions (organization)</a></br>
<a href="https://account.live.com/consent/Manage">Manage account permissions (personal)</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>
+50
Ver Arquivo
@@ -0,0 +1,50 @@
// import { default as fetch } from 'node-fetch';
import * as config from '../config';
import { app, server } from '../app';
import { ServerResponse } from 'http';
beforeAll(async (done) => {
if (!server.listening) {
server.on('listening', done())
} else {
done();
}
});
afterAll(async (done) => {
server.close(() => {
console.log('done');
done();
});
});
describe('App Server', () => {
afterAll(async (done) => {
server.close(() => {
console.log('done');
done();
});
});
test('initialized', async (done) => {
expect(app.name).toBeDefined();
expect(server.listening).toBe(true);
done();
});
test('loads app.html', async (done) => {
const response = await fetch(config.baseUrl);
expect(response.status).toBe(200);
done();
});
test('redirects to login', async (done) => {
const response = await fetch(config.baseUrl + '/api/v1.0/me');
expect(response.status).toBe(404);
done();
});
});
+13
Ver Arquivo
@@ -0,0 +1,13 @@
// import { default as fetch } from 'node-fetch';
import * as config from '../config';
describe('App Server', () => {
test('should be closed', async (done) => {
// do nothing
done();
});
});
+364
Ver Arquivo
@@ -0,0 +1,364 @@
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';
import * as simple_oath2 from 'simple-oauth2';
import * as graph from './graph';
import * as oauth2 from './oauth2';
export const log = bunyan.createLogger({
name: 'BUNYAN-LOGGER',
src: true,
});
// -----------------------------------------------------------------------------
// Passport Setup Follows
// -----------------------------------------------------------------------------
// Setup schemas for Passport's User object and the the session persistence format.
// Augument Passport's request.user with the Azure AD oauthToken
declare global {
namespace Express {
interface User {
oid: string;
oauthToken: oauth2.Token;
}
}
}
type FullUserSchema = Express.User; // This now includes the above declaration for User.
interface MinimizedUserSchema {
oid: string;
refresh_token: string;
}
passport.serializeUser((user: FullUserSchema, done) => {
const stored: MinimizedUserSchema = { oid: user.oid, refresh_token: user.oauthToken.refresh_token };
done(null, stored);
});
passport.deserializeUser(async (stored: MinimizedUserSchema, done) => {
if (!stored || !stored.refresh_token) { return done(Error('no user profile')); }
let oauthClient = await oauth2.client(stored);
if (oauthClient.expired()) {
oauthClient = await oauthClient.refresh(); // must reassign here.
}
const profile = await graph.user(oauthClient.token.access_token).catch(reason => { log.error('could not retrieve profile', reason); });
if (!profile) { return done(Error('no user profile')); }
const result = { ...profile, oid: stored.oid, oauthToken: oauthClient.token };
return done(null, result);
});
// -----------------------------------------------------------------------------
// Define the AzureAD OIDCStrategy Strategy
// -----------------------------------------------------------------------------
const OIDCStrategyTemplate = {} as passportAzureAD.IOIDCStrategyOptionWithoutRequest;
const azureStrategyOptions: passportAzureAD.IOIDCStrategyOptionWithRequest = {
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: true,
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,
};
// -----------------------------------------------------------------------------
// Use the Azure 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(azureStrategyOptions, processAzureStrategy));
async function processAzureStrategy(req: express.Request,
iss: string, sub: string, profile: passportAzureAD.IProfile, jwtClaims: any,
access_token: string, refresh_token: string, oauthToken: any,
done: passportAzureAD.VerifyCallback) {
if (!profile.oid) {
return done(new Error('No oid found'), null);
}
// asynchronous verification, for effect...
process.nextTick(async () => {
const fullProfile = await graph.user(access_token);
if (!fullProfile) {
return done(Error('no profile'));
}
const oauth = oauth2.client(oauthToken);
const result = { ...fullProfile, oid: profile.oid, oauthToken: oauth.token };
return done(null, result);
});
}
// -----------------------------------------------------------------------------
// Config the express app and all the required middleware
// -----------------------------------------------------------------------------
export const app = express();
app.use(morgan(config.httpLogFormat));
app.set('trust proxy', true);
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({ keys: config.creds.cookieEncryptionKeys.map(value => value.key), secure: false, maxAge: 1000 * 60 * 60 * 24 * 365 }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(passport.initialize());
app.use(passport.session());
// -----------------------------------------------------------------------------
// Define a couple of authencation request handlers.
// -----------------------------------------------------------------------------
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) => {
const user = { ...req.user, oauthToken: '[removed]'};
res.render('index', { user });
});
app.get('/account', ensureAuthenticated, (req, res, next) => {
const user = { ...req.user, oauthToken: '[removed]'};
res.render('account', { user });
});
app.get('/login', (req, res, next) => {
passport.authenticate('azuread-openidconnect',
{
response: res, // required
customState: 'my_state', // optional. Provide a value if you want to provide custom state value.
failureRedirect: '/',
} 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: '/',
} 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: '/',
} as passport.AuthenticateOptions,
)(req, res, next);
},
(req, res, next) => {
log.info('received a return from AzureAD.');
res.redirect('/');
});
// 'endsession' route, logout from passport, and destroy the session with AAD.
app.get('/endsession', (req, res) => {
req.session = null;
req.logOut();
res.redirect('/');
});
// 'logout' route, logout from passport, and destroy the session with AAD.
app.get('/logout', (req, res) => {
req.session = null;
// req.session.destroy((err) => {
req.logOut();
res.redirect(config.destroySessionUrl);
});
app.get('/api/v1.0/me', ensureAuthenticatedApi,
async (req, res, next) => {
try {
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const result = await graph.user(oauth.token.access_token);
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.get('/api/v1.0/profile', ensureAuthenticatedApi,
async (req, res, next) => {
try {
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const result = await graph.client(oauth.token.access_token).api('/me/extensions/com.code-with.vott').get();
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.put('/api/v1.0/profile', ensureAuthenticatedApi,
async (req, res, next) => {
try {
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const body = { ...req.body, extensionName: 'com.code-with.vott' };
let result = null;
try { // Handle bad graph open extension semantics
result = await graph.client(oauth.token.access_token).api('/me/extensions/').post(body);
} catch (error) {
// if it already exists and we are replacing. Delete and try again.
result = await graph.client(oauth.token.access_token).api('/me/extensions/com.code-with.vott').delete();
result = await graph.client(oauth.token.access_token).api('/me/extensions').post(body);
}
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.get('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
async (req, res, next) => {
try {
const id = req.params.id; // careful should only be a domain name pattern.
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).get();
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.put('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
async (req, res, next) => {
try {
const id = req.params.id; // careful should only be a domain name pattern.
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const body = { ...req.body, extensionName: `com.code-with.vott.${id}` };
let result = null;
try { // Handle bad graph open extension semantics
result = await graph.client(oauth.token.access_token).api('/me/extensions/').post(body);
} catch (error) {
// if it already exists and we are replacing. Delete and try again.
result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).delete();
result = await graph.client(oauth.token.access_token).api('/me/extensions').post(body);
}
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.patch('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
async (req, res, next) => {
try {
const id = req.params.id; // careful should only be a domain name pattern.
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const body = { ...req.body, extensionName: `com.code-with.vott.${id}` };
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).patch(body);
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.delete('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
async (req, res, next) => {
try {
const id = req.params.id; // careful should only be a domain name pattern.
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).delete();
res.end();
return next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.use('/public', express.static(path.join(__dirname, '../public')));
export const server = app.listen(config.port);
+94
Ver Arquivo
@@ -0,0 +1,94 @@
// 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 const loggingLevel = process.env.LOGGING_LEVEL || 'info';
export const httpLogFormat = process.env.HTTP_LOG_FORMAT || 'dev';
console.log('config values', process.env.APP_ID, process.env.APP_SECRET, baseUrl, redirectPath, port, loggingLevel, httpLogFormat);
export const 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 (!creds.clientID || !creds.clientSecret) {
console.log('issue with config');
throw Error('Missing configuration. You need a .env file or environment variables for APP_ID and APP_SECRET');
}
+30
Ver Arquivo
@@ -0,0 +1,30 @@
import * as graphClient from '@microsoft/microsoft-graph-client';
export async function user(access_token: string) {
const token = client(access_token);
const result = await token.api('/me').get();
return result;
}
export async function getEvents(access_token: string) {
const token = client(access_token);
const events = await token.api('/me/events')
.select('subject,organizer,start,end')
.orderby('createdDateTime DESC')
.get();
return events;
}
export function client(access_token: string): graphClient.Client {
// Initialize Graph client
const result = graphClient.Client.init({
// Use the provided access token to authenticate
// requests
authProvider: (done: (err: any, access_token: string) => void) => {
done(null, access_token);
},
});
return result;
}
+27
Ver Arquivo
@@ -0,0 +1,27 @@
import * as express from 'express';
import * as simple_oauth2 from 'simple-oauth2';
import * as config from './config';
export const oauth2 = simple_oauth2.create({
client: {
id: config.creds.clientID,
secret: config.creds.clientSecret,
},
auth: {
tokenHost: 'https://login.microsoftonline.com/common',
authorizePath: '/oauth2/v2.0/authorize',
tokenPath: '/oauth2/v2.0/token',
},
});
export interface Token {
refresh_token: string;
access_token?: string;
expires_at?: string | Date;
}
export function client(token: Token) {
token.expires_at = token.expires_at || new Date(0);
const result = oauth2.accessToken.create(token);
return result;
}
+62
Ver Arquivo
@@ -0,0 +1,62 @@
{
"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",
"coverage",
"data_models",
"public",
"lib",
"temp",
"jest.config.js",
"src/__tests__"
]
}
+48
Ver Arquivo
@@ -0,0 +1,48 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"linterOptions": {
"exclude": [
"lib",
"public",
"src/routes",
"jest.config.js"
]
},
"jsRules": {},
"rules": {
"no-console": false,
"arrow-parens": false,
"max-classes-per-file": false,
"ordered-imports": false,
"object-literal-sort-keys": false,
"align": false,
"interface-name": false,
"quotemark": [
true,
"single",
"avoid-escape",
"avoid-template"
],
"max-line-length": {
"severity": "warning",
"options": [
160,
{
"ignore-pattern": "^import |^export {(.*?)} | //"
}
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-leading-underscore",
"allow-pascal-case",
"allow-snake-case"
]
}
}
}