diff --git a/.gitignore b/.gitignore index 4c799eb..fd06b40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +AutomaticClockOuts/.env bin obj csx diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AutomaticClockOuts/clock_out.js b/AutomaticClockOuts/clock_out.js new file mode 100644 index 0000000..9807267 --- /dev/null +++ b/AutomaticClockOuts/clock_out.js @@ -0,0 +1,56 @@ +const CosmosClient = require("@azure/cosmos").CosmosClient; +const config = require("./config"); +const TimeEntry = require('./time_entry'); +const axios = require('axios'); +const MsalClient = require('./msal_client') + +const doClockOut = async (context, timer) => { + context.log(`I am going to check how many entries were not clocked out ${new Date()}`); + const {endpoint, key, databaseId, containerId, slackWebHook} = config; + const client = new CosmosClient({endpoint, key}); + const database = client.database(databaseId); + const container = database.container(containerId); + const response = await MsalClient.findUsersInMS(); + const users = response.data.value; + + const QUERY_WITHOUT_END_DATE = + "SELECT * FROM c WHERE (NOT IS_DEFINED(c.end_date) OR IS_NULL(c.end_date) = true) AND IS_DEFINED(c.start_date)" + + const {resources: entries} = await container.items + .query({query: QUERY_WITHOUT_END_DATE}) + .fetchAll(); + + context.log(`Checking for time-entries that need to be clocked out`); + let totalClockOutsExecuted = 0; + const usersWithClockOut = [] + await Promise.all(entries.map(async (timeEntryAsJson) => { + const timeEntry = new TimeEntry(timeEntryAsJson) + if (timeEntry.needsToBeClockedOut()) { + usersWithClockOut.push(findUser(users, timeEntry.timeEntry.owner_id)) + timeEntryAsJson.end_date = timeEntry.getTimeToClockOut() + await container.item(timeEntryAsJson.id, timeEntryAsJson.tenant_id).replace(timeEntryAsJson) + totalClockOutsExecuted++ + } + })); + if(totalClockOutsExecuted > 0){ + axios.post(slackWebHook, + { + "text": `Hey guys, I did a clock out for you. \nVisit https://timetracker.ioet.com/ and set the right end time for your entries :pls: \n- ${usersWithClockOut.join('\n- ')}` + } + ) + .then(function (response) { + // console.log(response); + }) + .catch(function (error) { + context.log(error); + }); + } + context.log(`I just clocked out ${totalClockOutsExecuted} entries, thanks are not needed...`); +} + +const findUser = (users, id) => { + const user = users.find( user => user.objectId === id) + return user.displayName +} + +module.exports = { doClockOut }; diff --git a/AutomaticClockOuts/config.js b/AutomaticClockOuts/config.js index 09eb6f8..2bdab84 100644 --- a/AutomaticClockOuts/config.js +++ b/AutomaticClockOuts/config.js @@ -1,9 +1,13 @@ const config = { - endpoint: "https://time-tracker-db.documents.azure.com:443/", - key: process.env["COSMOS_DB_KEY"], + endpoint: "xxx", + key: "xxx", databaseId: "time-tracker-db", containerId: "time_entry", - partitionKey: { kind: "Hash", paths: ["/category"] } + partitionKey: { kind: "Hash", paths: ["/category"] }, + clientId: "xxx", + authority: "xxx", + clientSecret: "xxx", + slackWebHook: "xxx" }; module.exports = config; diff --git a/AutomaticClockOuts/index.js b/AutomaticClockOuts/index.js index 155c030..22de912 100644 --- a/AutomaticClockOuts/index.js +++ b/AutomaticClockOuts/index.js @@ -1,33 +1,10 @@ const CosmosClient = require("@azure/cosmos").CosmosClient; -const moment = require("moment") const config = require("./config"); const TimeEntry = require('./time_entry'); +const axios = require('axios'); +const MsalClient = require('./msal_client'); +const ClockOut = require('./clock_out'); module.exports = async function (context, myTimer) { - - context.log(`I am going to check how many entries were not clocked out ${new Date()}`); - const {endpoint, key, databaseId, containerId} = config; - const client = new CosmosClient({endpoint, key}); - const database = client.database(databaseId); - const container = database.container(containerId); - - const QUERY_WITHOUT_END_DATE = - "SELECT * FROM c WHERE (NOT IS_DEFINED(c.end_date) OR IS_NULL(c.end_date) = true) AND IS_DEFINED(c.start_date)" - - const {resources: entries} = await container.items - .query({query: QUERY_WITHOUT_END_DATE}) - .fetchAll(); - - context.log(`Checking for time-entries that need to be clocked out`); - let totalClockOutsExecuted = 0; - await Promise.all(entries.map(async (timeEntryAsJson) => { - const timeEntry = new TimeEntry(timeEntryAsJson) - if (timeEntry.needsToBeClockedOut()) { - context.log(`I am going to clock out ${JSON.stringify(timeEntryAsJson.id)}`); - timeEntryAsJson.end_date = timeEntry.getTimeToClockOut() - await container.item(timeEntryAsJson.id, timeEntryAsJson.tenant_id).replace(timeEntryAsJson) - totalClockOutsExecuted++ - } - })); - context.log(`I just clocked out ${totalClockOutsExecuted} entries, thanks are not needed...`); + await ClockOut.doClockOut(context, myTimer) }; diff --git a/AutomaticClockOuts/msal_client.js b/AutomaticClockOuts/msal_client.js new file mode 100644 index 0000000..332cb5b --- /dev/null +++ b/AutomaticClockOuts/msal_client.js @@ -0,0 +1,27 @@ +const axios = require("axios") +const msal = require('@azure/msal-node'); +const config = require("./config"); + +const findUsersInMS = async() => { + const {clientId, authority, clientSecret} = config; + const endpoint = 'https://graph.windows.net/ioetec.onmicrosoft.com' + const configuration = { + auth: { + clientId: clientId, + authority: authority, + clientSecret: clientSecret + } + }; + + const cca = new msal.ConfidentialClientApplication(configuration); + const clientCredentialRequest = { + scopes: ['https://graph.windows.net/.default'], + }; + const response = await cca.acquireTokenByClientCredential(clientCredentialRequest) + const token = response.accessToken + return axios.get(`${endpoint}/users?api-version=1.6&$select=displayName,otherMails,objectId`, + { 'headers': { 'Authorization': token } }) +} + +module.exports = { findUsersInMS }; + diff --git a/AutomaticClockOuts/package-lock.json b/AutomaticClockOuts/package-lock.json index 6deb375..0cc7627 100644 --- a/AutomaticClockOuts/package-lock.json +++ b/AutomaticClockOuts/package-lock.json @@ -21,6 +21,58 @@ "uuid": "^3.3.2" } }, + "@azure/msal-common": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-1.2.0.tgz", + "integrity": "sha512-5MjxcUoalIxGo29MBHCdwMo6HCsCBt25HDkg+Du7x6nG7Y3NAUb9jM8oibLTth9iIRDaWYVvLfxnpXTKwBGs+A==", + "requires": { + "debug": "^4.1.1" + } + }, + "@azure/msal-node": { + "version": "1.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-alpha.5.tgz", + "integrity": "sha512-DkoEmnGy+PF5UZbViuLrO8qJVKRBftIojEP3xf8ck6q/vjOY18NUGXxrcKkRXfhRmTe4P2mRGCFuiil8+12IbA==", + "requires": { + "@azure/msal-common": "^1.2.0", + "axios": "^0.19.2", + "debug": "^4.1.1", + "jsonwebtoken": "^8.5.1" + }, + "dependencies": { + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", @@ -395,6 +447,14 @@ "yargs": "^15.4.1" } }, + "axios": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz", + "integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -466,6 +526,11 @@ "fill-range": "^7.0.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -859,12 +924,25 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "emittery": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.1.tgz", @@ -912,11 +990,6 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, - "esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -1005,6 +1078,11 @@ "path-exists": "^4.0.0" } }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1360,6 +1438,42 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -1412,6 +1526,41 @@ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -1607,6 +1756,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "msal": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.0.tgz", + "integrity": "sha512-NTxMFQh6t5g2QWMlvZTWTxL1bmcqiCv0cs2lxTHhUbWEuxWCfvaVRZfjxN8i+T0VltVVGaVIdML8QEoBnlbaSw==", + "requires": { + "tslib": "^1.9.3" + } + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -2093,8 +2250,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "semaphore": { "version": "1.1.0", diff --git a/AutomaticClockOuts/package.json b/AutomaticClockOuts/package.json index fb21eb7..e04938c 100644 --- a/AutomaticClockOuts/package.json +++ b/AutomaticClockOuts/package.json @@ -8,7 +8,11 @@ }, "dependencies": { "@azure/cosmos": "3.5.2", - "moment": "^2.27.0" + "@azure/msal-node": "^1.0.0-alpha.5", + "axios": "^0.20.0", + "dotenv": "^8.2.0", + "moment": "^2.27.0", + "msal": "^1.4.0" }, "author": "", "license": "ISC", diff --git a/AutomaticClockOuts/readme.md b/AutomaticClockOuts/readme.md index f53fc63..ae88d19 100644 --- a/AutomaticClockOuts/readme.md +++ b/AutomaticClockOuts/readme.md @@ -29,5 +29,12 @@ func azure functionapp publish time-tracker-azure-functions ``` NOTE: -Don't forget to set up the env variable `COSMOS_DB_KEY`, check the pinned -message in our slack channel to get this value +Don't forget to set the following environment variables to make this app work: + +```sh +COSMOS_DB_KEY +MS_CLIENT_ID +MS_AUTHORITY +MS_CLIENT_SECRET +``` +Check the pinned message in our slack channel to get these values diff --git a/AutomaticClockOuts/test/clock_out.spec.js b/AutomaticClockOuts/test/clock_out.spec.js new file mode 100644 index 0000000..d3b954a --- /dev/null +++ b/AutomaticClockOuts/test/clock_out.spec.js @@ -0,0 +1,12 @@ +const test = require('ava'); +const ClockOut = require('../clock_out') + +test('do clock out', async t => { + // const users = await MsalClient.findUsersInMS() + const log = (text) => console.log(text) + const context = { + log + } + // await ClockOut.doClockOut(context, null) + t.truthy(2>1) +}) diff --git a/AutomaticClockOuts/test/index.spec.js b/AutomaticClockOuts/test/index.spec.js deleted file mode 100644 index e69de29..0000000 diff --git a/AutomaticClockOuts/test/msal_client.spec.js b/AutomaticClockOuts/test/msal_client.spec.js new file mode 100644 index 0000000..0717be1 --- /dev/null +++ b/AutomaticClockOuts/test/msal_client.spec.js @@ -0,0 +1,8 @@ +const test = require('ava'); +const MsalClient = require('../msal_client') + +test('Response contains ioet.com since users has it as part their emails', async t => { + const users = await MsalClient.findUsersInMS() + + t.truthy(JSON.stringify(users.data.value).includes("ioet.com")) +})