Skip to content

Commit 3ced3e9

Browse files
author
derisen
committed
sync
1 parent 98270b3 commit 3ced3e9

14 files changed

Lines changed: 102 additions & 83 deletions

File tree

2-Authorization-I/1-call-graph/App/app.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
* Licensed under the MIT License.
44
*/
5+
56
const express = require('express');
67
const session = require('express-session');
7-
const bodyParser = require('body-parser');
88
const path = require('path');
99

1010
const router = require('./routes/router');
11+
const config = require('../appSettings.json');
12+
const cache = require('./utils/cachePlugin');
13+
const msalWrapper = require('msal-express-wrapper');
1114

1215
const SERVER_PORT = process.env.PORT || 4000;
1316

@@ -19,11 +22,23 @@ app.set('view engine', 'ejs');
1922
app.use('/css', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/css')));
2023
app.use('/js', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/js')));
2124

22-
app.use(bodyParser.urlencoded({extended: false}));
25+
app.use(express.urlencoded({ extended: false }));
2326

2427
app.use(express.static(path.join(__dirname, './public')));
2528

26-
app.use(session({secret: 'your-secret', resave: false, saveUninitialized: false}));
29+
/**
30+
* Using express-session middleware. Be sure to familiarize yourself with available options
31+
* and set as desired. Visit: https://www.npmjs.com/package/express-session
32+
*/
33+
app.use(session({
34+
secret: 'ENTER_YOUR_SECRET_HERE',
35+
resave: false,
36+
saveUninitialized: false
37+
}));
38+
39+
// initialize wrapper
40+
const authProvider = new msalWrapper.AuthProvider(config, cache);
41+
app.locals.authProvider = authProvider;
2742

2843
app.use(router);
2944

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
1+
const fetchManager = require('../utils/fetchManager');
2+
const appSettings = require('../../appSettings.json');
3+
14
exports.getHomePage = (req, res, next) => {
2-
const isAuthenticated = req.session.isAuthenticated;
3-
res.render('home', { isAuthenticated: isAuthenticated });
5+
res.render('home', { isAuthenticated: req.session.isAuthenticated });
46
}
57

68
exports.getIdPage = (req, res, next) => {
7-
const isAuthenticated = req.session.isAuthenticated;
8-
99
const claims = {
1010
name: req.session.idTokenClaims.name,
1111
preferred_username: req.session.idTokenClaims.preferred_username,
1212
oid: req.session.idTokenClaims.oid,
1313
sub: req.session.idTokenClaims.sub
1414
};
15-
16-
res.render('id', {isAuthenticated: isAuthenticated, claims: claims});
15+
16+
res.render('id', { isAuthenticated: req.session.isAuthenticated, claims: claims });
1717
}
1818

19-
exports.getProfilePage = (req, res, next) => {
20-
const isAuthenticated = req.session.isAuthenticated;
21-
const profile = req.session.graphAPI["resourceResponse"]; // the name of your web API in auth.json
22-
res.render('profile', {isAuthenticated: isAuthenticated, profile: profile});
19+
exports.getProfilePage = async(req, res, next) => {
20+
console.log(req.app);
21+
let profile;
22+
23+
try {
24+
profile = await fetchManager.callAPI(appSettings.resources.graphAPI.endpoint, req.session["graphAPI"].accessToken);
25+
} catch (error) {
26+
console.log(error)
27+
}
28+
29+
res.render('profile', { isAuthenticated: req.session.isAuthenticated, profile: profile });
2330
}
2431

25-
exports.getTenantPage = (req, res, next) => {
26-
const isAuthenticated = req.session.isAuthenticated;
27-
const tenant = req.session.armAPI["resourceResponse"]; // the name of your web API in auth.json
28-
res.render('tenant', {isAuthenticated: isAuthenticated, tenant: tenant.value[0]});
32+
exports.getTenantPage = async(req, res, next) => {
33+
let tenant;
34+
35+
try {
36+
tenant = await fetchManager.callAPI(appSettings.resources.armAPI.endpoint, req.session["armAPI"].accessToken);
37+
} catch (error) {
38+
console.log(error)
39+
}
40+
41+
res.render('tenant', { isAuthenticated: req.session.isAuthenticated, tenant: tenant.value[0] });
2942
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"Account":{"a283a601-6ad4-4528-975d-6abbefa5edd7.cbaf2168-de14-4c72-9d88-f5f05366dbef-login.windows.net-cbaf2168-de14-4c72-9d88-f5f05366dbef":{"home_account_id":"a283a601-6ad4-4528-975d-6abbefa5edd7.cbaf2168-de14-4c72-9d88-f5f05366dbef","environment":"login.windows.net","realm":"cbaf2168-de14-4c72-9d88-f5f05366dbef","local_account_id":"a283a601-6ad4-4528-975d-6abbefa5edd7","username":"admin@msaltestingjs.onmicrosoft.com","authority_type":"MSSTS","name":"Dogan Erisen","client_info":"eyJ1aWQiOiJhMjgzYTYwMS02YWQ0LTQ1MjgtOTc1ZC02YWJiZWZhNWVkZDciLCJ1dGlkIjoiY2JhZjIxNjgtZGUxNC00YzcyLTlkODgtZjVmMDUzNjZkYmVmIn0"}},"IdToken":{"a283a601-6ad4-4528-975d-6abbefa5edd7.cbaf2168-de14-4c72-9d88-f5f05366dbef-login.windows.net-idtoken-72e2c5fe-7c72-479a-b620-7bb0dd21df96-cbaf2168-de14-4c72-9d88-f5f05366dbef-":{"home_account_id":"a283a601-6ad4-4528-975d-6abbefa5edd7.cbaf2168-de14-4c72-9d88-f5f05366dbef","environment":"login.windows.net","credential_type":"IdToken","client_id":"72e2c5fe-7c72-479a-b620-7bb0dd21df96","secret":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiI3MmUyYzVmZS03YzcyLTQ3OWEtYjYyMC03YmIwZGQyMWRmOTYiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vY2JhZjIxNjgtZGUxNC00YzcyLTlkODgtZjVmMDUzNjZkYmVmL3YyLjAiLCJpYXQiOjE2MjA1NDQxNTgsIm5iZiI6MTYyMDU0NDE1OCwiZXhwIjoxNjIwNTQ4MDU4LCJuYW1lIjoiRG9nYW4gRXJpc2VuIiwib2lkIjoiYTI4M2E2MDEtNmFkNC00NTI4LTk3NWQtNmFiYmVmYTVlZGQ3IiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW5AbXNhbHRlc3Rpbmdqcy5vbm1pY3Jvc29mdC5jb20iLCJyaCI6IjAuQVVVQWFDR3Z5eFRlY2t5ZGlQWHdVMmJiN183RjRuSnlmSnBIdGlCN3NOMGgzNVpGQURnLiIsInN1YiI6IjlMS3JPeGhMX1pXcUE3QXpEYnpZMzZJVGNMZnQ2WDY2Z1JhNjlOZFM5RVkiLCJ0aWQiOiJjYmFmMjE2OC1kZTE0LTRjNzItOWQ4OC1mNWYwNTM2NmRiZWYiLCJ1dGkiOiIyaER2ZnlGSjYwYXhKbEwzTTBOQUFBIiwidmVyIjoiMi4wIn0.amMOEhOO2Dbu7AdaF8sIZyfpV243wKdlS9Nez7I8qZlB0xMxXeaw5xpj64PYSMUv8j4xixl9IG2i7FOEdwepkavBrX5ygu-bYDEnpi5jQTsJlwaJDvbGvP57k-PBo2FVKIni3p98T6RNhULuRs1F4sgfLj3tIYpeUdpLgACYFAWgfWQ7VtAHMG7sUGLGrOkyep_CieJ2oL6AIrE76-Kd-d62ejAQr_ZdqBH454LQ-hDr3g3i_KLp9A985Cqfae_eFTChWT0cpYsfK2fU4YoceLJUg6HVo3C14oFy2WzNwhj0fcVDCxcXqnBkPsWjMJP_EZGWgA4rlN1U7QTz-ATmWQ","realm":"cbaf2168-de14-4c72-9d88-f5f05366dbef"}},"AccessToken":{"a283a601-6ad4-4528-975d-6abbefa5edd7.cbaf2168-de14-4c72-9d88-f5f05366dbef-login.windows.net-accesstoken-72e2c5fe-7c72-479a-b620-7bb0dd21df96-cbaf2168-de14-4c72-9d88-f5f05366dbef-openid profile email user.read":{"home_account_id":"a283a601-6ad4-4528-975d-6abbefa5edd7.cbaf2168-de14-4c72-9d88-f5f05366dbef","environment":"login.windows.net","credential_type":"AccessToken","client_id":"72e2c5fe-7c72-479a-b620-7bb0dd21df96","secret":"eyJ0eXAiOiJKV1QiLCJub25jZSI6IjlFdHBxOFNMZDZIcUZQeWF2aENleHdhU0UwMm5IUkhmamJ5M0pYQ2hRX0UiLCJhbGciOiJSUzI1NiIsIng1dCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9jYmFmMjE2OC1kZTE0LTRjNzItOWQ4OC1mNWYwNTM2NmRiZWYvIiwiaWF0IjoxNjIwNTQ0MTU4LCJuYmYiOjE2MjA1NDQxNTgsImV4cCI6MTYyMDU0ODA1OCwiYWNjdCI6MCwiYWNyIjoiMSIsImFjcnMiOlsidXJuOnVzZXI6cmVnaXN0ZXJzZWN1cml0eWluZm8iLCJ1cm46bWljcm9zb2Z0OnJlcTEiLCJ1cm46bWljcm9zb2Z0OnJlcTIiLCJ1cm46bWljcm9zb2Z0OnJlcTMiLCJjMSIsImMyIiwiYzMiLCJjNCIsImM1IiwiYzYiLCJjNyIsImM4IiwiYzkiLCJjMTAiLCJjMTEiLCJjMTIiLCJjMTMiLCJjMTQiLCJjMTUiLCJjMTYiLCJjMTciLCJjMTgiLCJjMTkiLCJjMjAiLCJjMjEiLCJjMjIiLCJjMjMiLCJjMjQiLCJjMjUiXSwiYWlvIjoiRTJaZ1lGQUxhdFl6Vy9HWHMwRC9pdnlNS2I5Nkp2azVaaWptY2hvVmliRWQ3WHFhekFFQSIsImFtciI6WyJwd2QiXSwiYXBwX2Rpc3BsYXluYW1lIjoiRXhwcmVzc1Rlc3RBcHAiLCJhcHBpZCI6IjcyZTJjNWZlLTdjNzItNDc5YS1iNjIwLTdiYjBkZDIxZGY5NiIsImFwcGlkYWNyIjoiMSIsImZhbWlseV9uYW1lIjoiRXJpc2VuIiwiZ2l2ZW5fbmFtZSI6IkRvZ2FuIiwiaWR0eXAiOiJ1c2VyIiwiaXBhZGRyIjoiMTcyLjEwMy4yMzMuMTY3IiwibmFtZSI6IkRvZ2FuIEVyaXNlbiIsIm9pZCI6ImEyODNhNjAxLTZhZDQtNDUyOC05NzVkLTZhYmJlZmE1ZWRkNyIsInBsYXRmIjoiNSIsInB1aWQiOiIxMDAzMjAwMDk2NERCNDlEIiwicmgiOiIwLkFVVUFhQ0d2eXhUZWNreWRpUFh3VTJiYjdfN0Y0bkp5ZkpwSHRpQjdzTjBoMzVaRkFEZy4iLCJzY3AiOiJvcGVuaWQgcHJvZmlsZSBVc2VyLlJlYWQgZW1haWwiLCJzdWIiOiJKM3NMUUZhUm9LWjJVYjMwTmpDYThOOTlsRVppcGZqb2g2Ymx6QlJmTmZnIiwidGVuYW50X3JlZ2lvbl9zY29wZSI6Ik5BIiwidGlkIjoiY2JhZjIxNjgtZGUxNC00YzcyLTlkODgtZjVmMDUzNjZkYmVmIiwidW5pcXVlX25hbWUiOiJhZG1pbkBtc2FsdGVzdGluZ2pzLm9ubWljcm9zb2Z0LmNvbSIsInVwbiI6ImFkbWluQG1zYWx0ZXN0aW5nanMub25taWNyb3NvZnQuY29tIiwidXRpIjoiMmhEdmZ5Rko2MGF4SmxMM00wTkFBQSIsInZlciI6IjEuMCIsIndpZHMiOlsiNjJlOTAzOTQtNjlmNS00MjM3LTkxOTAtMDEyMTc3MTQ1ZTEwIiwiYjc5ZmJmNGQtM2VmOS00Njg5LTgxNDMtNzZiMTk0ZTg1NTA5Il0sInhtc19zdCI6eyJzdWIiOiI5TEtyT3hoTF9aV3FBN0F6RGJ6WTM2SVRjTGZ0Nlg2NmdSYTY5TmRTOUVZIn0sInhtc190Y2R0IjoxNTc5MzA5NDA0fQ.gWsOcF0KkFs00eYj70g952VW444Vk4NUnHIuxnNu9FBqcGAv-Sp7g7QP1m0KK74nRQm5QjW3l2uEcupvmXR0_8BpL9cMpGXQWxzHNmdXShGMJwifFI3kBA6M05QiW2-TDx1JUjy2qJ3_0KYAfH_rBY8Gw8Qf8NdzOrUDjZl0iPO1AE_PhCZ5UsePseJYW4RsHfZFJl87ltali4vJFNRmxptg-27ELVgiWoyL6M5ClYHRi0A2aBtkKFNSLBNW6dRDaxuEq9Xihb3iTOv4wowJLXnyy-guRJsW5xQSIKcRvcjJOdkw2aK2ZTM2bNwGQsAsS3-r_lsf3uW65iU5wr--Eg","realm":"cbaf2168-de14-4c72-9d88-f5f05366dbef","target":"openid profile email User.Read","cached_at":"1620544459","expires_on":"1620548057","extended_expires_on":"1620551656","token_type":"Bearer"}},"RefreshToken":{"a283a601-6ad4-4528-975d-6abbefa5edd7.cbaf2168-de14-4c72-9d88-f5f05366dbef-login.windows.net-refreshtoken-72e2c5fe-7c72-479a-b620-7bb0dd21df96--":{"home_account_id":"a283a601-6ad4-4528-975d-6abbefa5edd7.cbaf2168-de14-4c72-9d88-f5f05366dbef","environment":"login.windows.net","credential_type":"RefreshToken","client_id":"72e2c5fe-7c72-479a-b620-7bb0dd21df96","secret":"0.AUUAaCGvyxTeckydiPXwU2bb7_7F4nJyfJpHtiB7sN0h35ZFADg.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P_kkLOUnyZQuh0pJYFlDYbzGPtdlKlvqnjBYCmD1wq72SkIZNiH5xVWibxv0bt_D-q7sMv_xnpf6mnYEcs524KjlP_Kdva54ezxgHwRRICketTUsP5FlRtshITaf3AFV7_q5PpivA1V_thXlLnX7F3grLvrxVX0FFRT5CslSp1sAG4bW0xyD8mcNkDcd-6pEOpXQWY_Mv2S8JgF6wAcPkGPpc0r8fqod2Fwhb4eWzc0jbJ6aXyP7p2AdSUJPsNK94d71iOZYmh8eESTFGhyq9vVE0UH9pxWEm_t1HrrbXanVBseB1LZYzMpP6mIjgJow1cg3PDV1xwwzrNKAiqyJkgCrelPgVdHm2ctVRGgD0r5tAFYzLLHmP0f-GWxn_UnvN14MLbhl-uehA-jqxVBAS1vqXmNKTTFHTxXU5eCdB1B6gSilcFxtupvMvW4UqEN_oXUdiltbMX2A6e1Pcp-z20g_jqlb6roMqVYbzxiXyqf8iQO1pUFkt4Rxij8DDJVq6gbSADsJbxtfz7lzrcAVSGnjEv4yYi6CK8dWgex9MiZd3EfyT7ePvwuL0gLk-R5QB_xOzrDXfAD7THgiU64mLCh10BZjbLTJg2_CjFAWbXMuFfZoqKTGzkOTPhRRd1CotTcz7Rlcb00NK-tDATWnY9aXYgfqJW3rz_4_TrMQO8nOE7lr2FGRWKDcI7vW-sQqr6CeMmlVnN6wZPU2Ui6bdnyfjGqzudCyDysTGmTcmt-FEI2KNLZ54QXV_J-WUhXX0W-V3Nzi-ivlFUjDJGflQj5Q4Myp0ovpvozO6ZJvFVmUng8T2dLLRJVuJsk4LkR4CyjQyVSdJ16L1BJGkL57a_B40MlZodv6DyxY-9bvo2_RB0dyYdq_icPaeznlm1uFA0rukY"}},"AppMetadata":{}}

2-Authorization-I/1-call-graph/App/routes/router.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ const express = require('express');
22

33
const mainController = require('../controllers/mainController');
44

5-
const config = require('../../auth.json');
5+
const config = require('../../appSettings.json');
66
const cache = require('../utils/cachePlugin');
7+
const msalWrapper = require('msal-express-wrapper');
78

8-
const MsalNodeWrapper = require('MsalNodeWrapper/MsalNodeWrapper');
9-
10-
const msal = new MsalNodeWrapper(config, cache);
9+
// initialize wrapper
10+
const authProvider = new msalWrapper.AuthProvider(config, cache);
1111

1212
// initialize router
1313
const router = express.Router();
@@ -16,15 +16,15 @@ const router = express.Router();
1616
router.get('/', (req, res, next) => res.redirect('/home'));
1717
router.get('/home', mainController.getHomePage);
1818

19-
// authentication routes
20-
router.get('/signin', msal.signIn);
21-
router.get('/signout', msal.signOut);
22-
router.get('/redirect', msal.handleRedirect);
19+
// // authentication routes
20+
router.get('/signin', authProvider.signIn);
21+
router.get('/signout', authProvider.signOut);
22+
router.get('/redirect', authProvider.handleRedirect);
2323

24-
// authorized routes
25-
router.get('/id', msal.isAuthenticated, mainController.getIdPage);
26-
router.get('/profile', msal.isAuthenticated, msal.getToken, mainController.getProfilePage); // get token for this route to call web API
27-
router.get('/tenant', msal.isAuthenticated, msal.getToken, mainController.getTenantPage) // get token for this route to call web API
24+
// authenticated routes
25+
router.get('/id', authProvider.isAuthenticated, mainController.getIdPage);
26+
router.get('/profile', authProvider.isAuthenticated, authProvider.getToken, mainController.getProfilePage); // get token for this route to call web API
27+
router.get('/tenant', authProvider.isAuthenticated, authProvider.getToken, mainController.getTenantPage) // get token for this route to call web API
2828

2929
// 404
3030
router.get('*', (req, res) => res.status(404).redirect('/404.html'));

2-Authorization-I/1-call-graph/App/utils/cachePlugin.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs = require("fs");
2-
const cachePath = './cache.json' // replace this string with the path to your valid cache file.
2+
const cachePath = './App/data/cache.json' // replace this string with the path to your valid cache file.
33

44
const beforeCacheAccess = async (cacheContext) => {
55
return new Promise(async (resolve, reject) => {
@@ -13,7 +13,7 @@ const beforeCacheAccess = async (cacheContext) => {
1313
}
1414
});
1515
} else {
16-
fs.writeFile(cachePath, cacheContext.tokenCache.serialize(), (err) => {
16+
fs.writeFile(cachePath, cacheContext.tokenCache.serialize(), (err) => {
1717
if (err) {
1818
reject();
1919
}
@@ -23,7 +23,7 @@ const beforeCacheAccess = async (cacheContext) => {
2323
};
2424

2525
const afterCacheAccess = async (cacheContext) => {
26-
if(cacheContext.cacheHasChanged){
26+
if (cacheContext.cacheHasChanged) {
2727
await fs.writeFile(cachePath, cacheContext.tokenCache.serialize(), (err) => {
2828
if (err) {
2929
console.log(err);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
const { default: axios } = require('axios');
7+
8+
callAPI = async(endpoint, accessToken) => {
9+
10+
if (!accessToken || accessToken === "") {
11+
throw new Error('No tokens found')
12+
}
13+
14+
const options = {
15+
headers: {
16+
Authorization: `Bearer ${accessToken}`
17+
}
18+
};
19+
20+
console.log('request made to web API at: ' + new Date().toString());
21+
22+
try {
23+
const response = await axios.default.get(endpoint, options);
24+
return response.data;
25+
} catch(error) {
26+
console.log(error)
27+
return error;
28+
}
29+
}
30+
31+
module.exports = {
32+
callAPI
33+
};
File renamed without changes.

2-Authorization-I/1-call-graph/App/views/includes/footer.ejs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<footer class="footer font-small blue">
22
<% if (isAuthenticated) { %>
3+
<br>
34
<div class="footer text-center py-3">How did we do?
45
<a href="https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUQkRCSVdRSk8wUjdZSkg2NEZGOFFaTkxQVyQlQCN0PWcu" target="_blank">Share your experience with us!</a>
56
</div>

2-Authorization-I/1-call-graph/auth.json renamed to 2-Authorization-I/1-call-graph/appSettings.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"credentials": {
3-
"clientId": "45969834-3083-4671-b33e-e7de11e358a1",
3+
"clientId": "72e2c5fe-7c72-479a-b620-7bb0dd21df96",
44
"tenantId": "cbaf2168-de14-4c72-9d88-f5f05366dbef",
5-
"clientSecret": "8BOZbL3ldfThP3PsEnx4q6GEnoOuhHfAW9o/XRpcu98="
5+
"clientSecret": "T1LQgCXkIL.p554u6-9LPD1Vu-n4Tx.vZe"
66
},
7-
"configuration": {
7+
"settings": {
88
"homePageRoute": "/home",
99
"redirectUri": "http://localhost:4000/redirect",
1010
"postLogoutRedirectUri": "http://localhost:4000/"

2-Authorization-I/1-call-graph/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"ejs": "^3.0.1",
1717
"express": "^4.17.1",
1818
"express-session": "^1.17.1",
19-
"MsalNodeWrapper": "file:../../MsalNodeWrapper"
19+
"msal-express-wrapper": "file:../../msal-express-wrapper"
2020
},
2121
"devDependencies": {
2222
"nodemon": "^2.0.2"

0 commit comments

Comments
 (0)