diff --git a/apps/vpnmanager/.env.template b/apps/vpnmanager/.env.template index 91538d77e..3b37a599e 100644 --- a/apps/vpnmanager/.env.template +++ b/apps/vpnmanager/.env.template @@ -1,6 +1,6 @@ NEXT_APP_GOOGLE_CREDENTIALS=/path/to/credentials.json NEXT_APP_GOOGLE_SHEET_ID= -NEXT_APP_GOOGLE_SHEET_RANGE=New Hires!A:Z +NEXT_APP_GOOGLE_SHEET_NAME="New Hires" NEXT_APP_VPN_API_URL= SENTRY_AUTH_TOKEN= SENTRY_DSN= diff --git a/apps/vpnmanager/package.json b/apps/vpnmanager/package.json index 573837c07..dbc49f65a 100644 --- a/apps/vpnmanager/package.json +++ b/apps/vpnmanager/package.json @@ -7,7 +7,7 @@ "build": "pnpm run build-ts && next build", "start": "next start", "build-ts": "tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json", - "process-new-hires": "node dist/src/lib/processNewHires.js" + "process-spreadsheet": "node dist/scripts/processGsheet.js" }, "dependencies": { "@babel/core": "^7.23.6", @@ -20,6 +20,8 @@ "@emotion/styled": "^11.11.0", "@mui/material": "^5.14.20", "@mui/utils": "^5.14.20", + "@next/env": "^14.1.4", + "@sendgrid/mail": "^8.1.1", "@sentry/nextjs": "^7.105.0", "@svgr/webpack": "^8.1.0", "@types/jest": "^29.5.12", diff --git a/apps/vpnmanager/scripts/processGsheet.ts b/apps/vpnmanager/scripts/processGsheet.ts new file mode 100644 index 000000000..2a137d0df --- /dev/null +++ b/apps/vpnmanager/scripts/processGsheet.ts @@ -0,0 +1,7 @@ +import { processNewUsers } from "@/vpnmanager/lib/processUsers"; +import { loadEnvConfig } from "@next/env"; + +const projectDir = process.cwd(); +loadEnvConfig(projectDir); + +processNewUsers(); diff --git a/apps/vpnmanager/src/lib/data/spreadsheet.ts b/apps/vpnmanager/src/lib/data/spreadsheet.ts index 1c02307bb..bd41b49b1 100644 --- a/apps/vpnmanager/src/lib/data/spreadsheet.ts +++ b/apps/vpnmanager/src/lib/data/spreadsheet.ts @@ -6,12 +6,12 @@ import { toCamelCase } from "@/vpnmanager/utils"; function gSheet() { const auth = new google.auth.GoogleAuth({ keyFile: process.env.NEXT_APP_GOOGLE_CREDENTIALS, - scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"], + scopes: ["https://www.googleapis.com/auth/spreadsheets"], }); return google.sheets({ version: "v4", auth }); } -async function list( +async function fetchRange( spreadsheetId?: string, range?: string, ): Promise { @@ -44,16 +44,70 @@ async function list( return data; } -async function newHires() { +async function newUsers() { const spreadsheetId = process.env.NEXT_APP_GOOGLE_SHEET_ID; - const range = process.env.NEXT_APP_GOOGLE_SHEET_RANGE; - const data = await list(spreadsheetId, range); + const range = `${process.env.NEXT_APP_GOOGLE_SHEET_NAME}!A:Z`; + const data = await fetchRange(spreadsheetId, range); return data.filter( - (row: SheetRow) => row.emailAddress && row.keySent !== "Yes", + (row: SheetRow) => row.emailAddress && row.keySent?.trim() !== "Yes", ); } +function processRow(rows: string[][], row: Partial) { + const { emailAddress, outlineKeyCreated, keySent } = row; + const titles = rows[0]; + const emailIndex = titles.findIndex( + (item) => item?.toLowerCase()?.trim() === "email address", + ); + const rowIndexToUpdate = rows.findIndex( + (item: string[]) => item[emailIndex] === emailAddress, + ); + if (rowIndexToUpdate < 0) { + return rows; + } + const rowToUpdate = rows[rowIndexToUpdate]; + if (outlineKeyCreated) { + const outlineKeyCreatedIndex = titles.findIndex( + (item) => item?.toLowerCase()?.trim() === "outline key created?", + ); + rowToUpdate[outlineKeyCreatedIndex] = outlineKeyCreated; + } + if (keySent) { + const keySentIndex = titles.findIndex( + (item) => item?.toLowerCase()?.trim() === "key sent", + ); + rowToUpdate[keySentIndex] = keySent; + } + rows[rowIndexToUpdate] = rowToUpdate; + return rows; +} + +export async function updateSheet(toUpdate: Partial[]) { + const spreadsheetId = process.env.NEXT_APP_GOOGLE_SHEET_ID; + const range = `${process.env.NEXT_APP_GOOGLE_SHEET_NAME}!A:Z`; + const sheets = gSheet(); + const response = await sheets.spreadsheets.values.get({ + spreadsheetId, + range, + }); + const rows = response.data.values; + if (!rows?.length) { + return null; + } + + const values = toUpdate.reduce((acc, curr) => processRow(acc, curr), rows); + return sheets.spreadsheets.values.update({ + spreadsheetId, + range, + valueInputOption: "USER_ENTERED", + requestBody: { + values, + }, + }); +} + export default { - list, - newHires, + fetchRange, + newUsers, + updateSheet, }; diff --git a/apps/vpnmanager/src/lib/processNewHires.ts b/apps/vpnmanager/src/lib/processNewHires.ts deleted file mode 100644 index 4db039a2c..000000000 --- a/apps/vpnmanager/src/lib/processNewHires.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SheetRow } from "@/vpnmanager/types"; -import * as Sentry from "@sentry/nextjs"; - -import spreadsheet from "./data/spreadsheet"; - -export async function processEmployee(item: SheetRow) { - // Capture to test that it works - Sentry.captureException(item); -} - -export async function processNewHires() { - const newHires = await spreadsheet.newHires(); - const promises = newHires.map((item) => processEmployee(item)); - Promise.allSettled(promises); -} - -processNewHires(); diff --git a/apps/vpnmanager/src/lib/processUsers.ts b/apps/vpnmanager/src/lib/processUsers.ts new file mode 100644 index 000000000..8d1dfed9f --- /dev/null +++ b/apps/vpnmanager/src/lib/processUsers.ts @@ -0,0 +1,21 @@ +import spreadsheet, { updateSheet } from "@/vpnmanager/lib/data/spreadsheet"; +import { SheetRow } from "@/vpnmanager/types"; + +export async function processUser(item: SheetRow) { + return { + ...item, + keySent: "Yes", + }; +} + +export async function processNewUsers() { + const users = await spreadsheet.newUsers(); + const promises = users.map((item) => processUser(item)); + const settled = await Promise.allSettled(promises); + const fulfilled = settled + .filter((item) => item.status === "fulfilled") + .map(({ value }: any) => value); + if (fulfilled.length) { + updateSheet(fulfilled); + } +} diff --git a/apps/vpnmanager/src/utils/index.ts b/apps/vpnmanager/src/utils/index.ts index 513aefc31..07ed165ba 100644 --- a/apps/vpnmanager/src/utils/index.ts +++ b/apps/vpnmanager/src/utils/index.ts @@ -1,4 +1,3 @@ -import fetchJson from "./fetchJson"; import toCamelCase from "./wordToCamelCase"; -export { fetchJson, toCamelCase }; +export { toCamelCase }; diff --git a/apps/vpnmanager/tsconfig.server.json b/apps/vpnmanager/tsconfig.server.json index 4c04df5be..3155ccdca 100644 --- a/apps/vpnmanager/tsconfig.server.json +++ b/apps/vpnmanager/tsconfig.server.json @@ -8,5 +8,5 @@ "outDir": "./dist", "rootDir": "./" }, - "include": ["src/lib/**/*.ts", "src/utils/**/*.ts"] + "include": ["src/lib/**/*.ts", "src/utils/**/*.ts", "scripts/**/*.ts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13b915136..9d00cce1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -971,6 +971,12 @@ importers: "@mui/utils": specifier: ^5.14.20 version: 5.15.0(@types/react@18.2.45)(react@18.2.0) + "@next/env": + specifier: ^14.1.4 + version: 14.1.4 + "@sendgrid/mail": + specifier: ^8.1.1 + version: 8.1.1 "@sentry/nextjs": specifier: ^7.105.0 version: 7.106.1(next@14.1.3)(react@18.2.0)(webpack@5.89.0) @@ -7282,6 +7288,13 @@ packages: } dev: false + /@next/env@14.1.4: + resolution: + { + integrity: sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==, + } + dev: false + /@next/eslint-plugin-next@14.0.4: resolution: { @@ -8829,6 +8842,19 @@ packages: request: 2.88.2 dev: false + /@sendgrid/client@8.1.1: + resolution: + { + integrity: sha512-pg0gYhAdyQil3Aga7/xHVcZFpvDAjAQMNBgMy5njTSkjACoWHmpSi1nWBZM7nIH/ptcRNMpnBbm9B5EvQ8fX2w==, + } + engines: { node: ">=12.*" } + dependencies: + "@sendgrid/helpers": 8.0.0 + axios: 1.6.8 + transitivePeerDependencies: + - debug + dev: false + /@sendgrid/helpers@6.5.5: resolution: { @@ -8840,6 +8866,16 @@ packages: deepmerge: 4.3.1 dev: false + /@sendgrid/helpers@8.0.0: + resolution: + { + integrity: sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==, + } + engines: { node: ">= 12.0.0" } + dependencies: + deepmerge: 4.3.1 + dev: false + /@sendgrid/mail@6.5.5: resolution: { @@ -8851,6 +8887,19 @@ packages: "@sendgrid/helpers": 6.5.5 dev: false + /@sendgrid/mail@8.1.1: + resolution: + { + integrity: sha512-tNtmgWLtBA7ZxKtPuEGOaIdEZP1vZSXsj5zg9iuoDBPVj/fNz+7LWzndvTcKumHk5eaDrS0UPXJqBm61m3+H1A==, + } + engines: { node: ">=12.*" } + dependencies: + "@sendgrid/client": 8.1.1 + "@sendgrid/helpers": 8.0.0 + transitivePeerDependencies: + - debug + dev: false + /@sentry-internal/feedback@7.106.1: resolution: { @@ -13626,6 +13675,19 @@ packages: engines: { node: ">=4" } dev: false + /axios@1.6.8: + resolution: + { + integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==, + } + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.2.1: resolution: { @@ -18406,6 +18468,19 @@ packages: tabbable: 5.3.3 dev: false + /follow-redirects@1.15.6: + resolution: + { + integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==, + } + engines: { node: ">=4.0" } + peerDependencies: + debug: "*" + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {