From 299e6449b1bf9e4484c4863cf09256c0d4946d16 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 13:22:18 -0400 Subject: [PATCH 01/10] =?UTF-8?q?Initial=20commit=20=E2=80=94=20Google=20W?= =?UTF-8?q?orkspace=20to=20Front=20user=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yaml | 11 ++ .github/workflows/deploy.yaml | 20 ++ .gitignore | 6 + README.md | 94 +++++++++ cdk/bin/app.js | 13 ++ cdk/cdk.json | 3 + cdk/lib/google-user-sync-stack.js | 75 +++++++ cdk/package.json | 11 ++ lambda/index.js | 315 ++++++++++++++++++++++++++++++ lambda/package.json | 9 + 10 files changed, 557 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/deploy.yaml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cdk/bin/app.js create mode 100644 cdk/cdk.json create mode 100644 cdk/lib/google-user-sync-stack.js create mode 100644 cdk/package.json create mode 100644 lambda/index.js create mode 100644 lambda/package.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..225d9ce --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,11 @@ +name: CI +on: + pull_request: + branches: [main] + +jobs: + ci: + uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main + with: + run-typecheck: false + run-cdk-synth: true diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..0125e0c --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,20 @@ +name: Deploy +on: + push: + branches: [main] + +permissions: + id-token: write + contents: read + +concurrency: + group: deploy + cancel-in-progress: false + +jobs: + deploy: + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main + with: + cdk-dir: cdk + secrets: + deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f6b5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +cdk.out/ +.env +__pycache__/ +.aws-sam/ +PLAN.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac697bc --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# google-user-sync + +A scheduled Lambda that pulls user profile data (job title, phone) from Google Workspace Admin Directory API and syncs it to Front teammate custom fields via the Front Core API. Runs weekdays at 6:00 AM ET via EventBridge. + +## Architecture + +``` +EventBridge (weekday cron, 6:00 AM ET) + | + v +Lambda (Node.js 22, arm64) + | + +-- Secrets Manager --> google-user-sync/google-service-account + +-- Secrets Manager --> google-user-sync/front-api-token + | + +-- GET Google Admin Directory API --> list users in target OUs + +-- PATCH Front API /teammates/alt:email:{email} --> update custom_fields +``` + +## AWS Resources + +- **Stack:** `google-user-sync` (CDK, us-east-1) +- **Lambda:** `google-user-sync` — Node.js 22, arm64, 256 MB, 5 min timeout, 60-day log retention +- **EventBridge Rule:** `google-user-sync-daily` — weekdays at 11:00 UTC +- **Secrets Manager:** `google-user-sync/google-service-account`, `google-user-sync/front-api-token` + +## Prerequisites + +### 1. Create Custom Fields in Front + +1. Gear icon > Company Settings > Custom Fields > Teammates tab +2. Create two fields: + - **Job Title** (String) + - **Phone** (String) + +### 2. Generate a Front API Token + +1. Gear icon > Company Settings > Developers > API tokens +2. Create token with **Shared resources** scope (teammate read/write) + +### 3. Create a Google Cloud Service Account + +1. Enable the **Admin SDK API** in Google Cloud Console +2. Create a service account named `front-directory-sync` +3. Create a JSON key and download it +4. Enable Domain-Wide Delegation and copy the Client ID + +### 4. Authorize in Google Workspace Admin + +1. Security > Access and Data Control > API Controls > Domain-Wide Delegation +2. Add the Client ID with scope: `https://www.googleapis.com/auth/admin.directory.user.readonly` + +### 5. Populate Google User Profiles + +Ensure each user has their **Job title** and **Phone** filled in under Directory > Users. + +### 6. Store Secrets in AWS Secrets Manager + +```bash +aws secretsmanager create-secret \ + --name "google-user-sync/front-api-token" \ + --secret-string "YOUR_FRONT_API_TOKEN" \ + --region us-east-1 + +aws secretsmanager create-secret \ + --name "google-user-sync/google-service-account" \ + --secret-string file://path-to-service-account-key.json \ + --region us-east-1 +``` + +## Deployment + +```bash +cd cdk +npm install +npx cdk deploy google-user-sync +``` + +## Verification + +```bash +aws lambda invoke \ + --function-name google-user-sync \ + --region us-east-1 \ + output.json && cat output.json +``` + +Check Front > Company Settings > Teammates > pick a user > Custom Fields to confirm Job Title and Phone are populated. + +## Maintenance + +- **New teammates:** Automatically picked up if they exist in both Google Workspace and Front with the same email. +- **Schedule changes:** Update the EventBridge rule in `cdk/lib/google-user-sync-stack.js`. +- **Additional fields:** Add as custom fields in Front, then update the field mapping in `lambda/index.js` (`buildCustomFields()`). diff --git a/cdk/bin/app.js b/cdk/bin/app.js new file mode 100644 index 0000000..79e3601 --- /dev/null +++ b/cdk/bin/app.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +const cdk = require("aws-cdk-lib"); +const { GoogleUserSyncStack } = require("./lib/google-user-sync-stack"); + +const app = new cdk.App(); + +new GoogleUserSyncStack(app, "GoogleUserSyncStack", { + stackName: "google-user-sync", + env: { + account: "328440206208", + region: "us-east-1", + }, +}); diff --git a/cdk/cdk.json b/cdk/cdk.json new file mode 100644 index 0000000..d85f6d3 --- /dev/null +++ b/cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "node bin/app.js" +} diff --git a/cdk/lib/google-user-sync-stack.js b/cdk/lib/google-user-sync-stack.js new file mode 100644 index 0000000..333bf16 --- /dev/null +++ b/cdk/lib/google-user-sync-stack.js @@ -0,0 +1,75 @@ +const { Stack, Duration, RemovalPolicy, CfnOutput } = require("aws-cdk-lib"); +const lambda = require("aws-cdk-lib/aws-lambda"); +const events = require("aws-cdk-lib/aws-events"); +const targets = require("aws-cdk-lib/aws-events-targets"); +const secretsmanager = require("aws-cdk-lib/aws-secretsmanager"); +const logs = require("aws-cdk-lib/aws-logs"); +const path = require("path"); + +class GoogleUserSyncStack extends Stack { + constructor(scope, id, props) { + super(scope, id, props); + + // ----------------------------------------------------------------- + // Reference existing secrets (created manually per PLAN.md Step 6) + // ----------------------------------------------------------------- + const googleSecret = secretsmanager.Secret.fromSecretNameV2( + this, + "GoogleServiceAccountSecret", + "google-user-sync/google-service-account" + ); + + const frontSecret = secretsmanager.Secret.fromSecretNameV2( + this, + "FrontApiTokenSecret", + "google-user-sync/front-api-token" + ); + + // ----------------------------------------------------------------- + // Lambda function + // ----------------------------------------------------------------- + const syncFn = new lambda.Function(this, "GoogleUserSyncFn", { + functionName: "google-user-sync", + runtime: lambda.Runtime.NODEJS_22_X, + architecture: lambda.Architecture.ARM_64, + handler: "index.handler", + code: lambda.Code.fromAsset(path.join(__dirname, "..", "lambda")), + timeout: Duration.minutes(5), + memorySize: 256, + environment: { + GOOGLE_SECRET_ARN: googleSecret.secretArn, + FRONT_SECRET_ARN: frontSecret.secretArn, + GOOGLE_ADMIN_EMAIL: "adam@seahavenind.com", + GOOGLE_DOMAIN: "seahavenind.com", + GOOGLE_OUS: "/Office/Scheduling,/Office/Operations", + }, + logRetention: logs.RetentionDays.TWO_MONTHS, + }); + + // Grant the Lambda read access to both secrets + googleSecret.grantRead(syncFn); + frontSecret.grantRead(syncFn); + + // ----------------------------------------------------------------- + // EventBridge scheduled rule — daily at 6:00 AM ET (11:00 UTC) + // ----------------------------------------------------------------- + const rule = new events.Rule(this, "DailySyncRule", { + ruleName: "google-user-sync-daily", + schedule: events.Schedule.cron({ + minute: "0", + hour: "11", // 11:00 UTC = 6:00 AM ET (EST+5) / 7:00 AM EDT + month: "*", + weekDay: "MON-FRI", + year: "*", + }), + }); + + rule.addTarget(new targets.LambdaFunction(syncFn)); + + new CfnOutput(this, "SyncFunctionArn", { + value: syncFn.functionArn, + }); + } +} + +module.exports = { GoogleUserSyncStack }; diff --git a/cdk/package.json b/cdk/package.json new file mode 100644 index 0000000..cd30d83 --- /dev/null +++ b/cdk/package.json @@ -0,0 +1,11 @@ +{ + "name": "google-user-sync-cdk", + "version": "1.0.0", + "bin": { + "app": "bin/app.js" + }, + "dependencies": { + "aws-cdk-lib": "^2.150.0", + "constructs": "^10.0.0" + } +} diff --git a/lambda/index.js b/lambda/index.js new file mode 100644 index 0000000..bf67556 --- /dev/null +++ b/lambda/index.js @@ -0,0 +1,315 @@ +/** + * google-user-sync Lambda + * + * Pulls user profiles from Google Workspace Admin Directory API + * and syncs job title + phone to Front teammate custom fields. + * + * Environment variables: + * GOOGLE_SECRET_ARN - Secrets Manager ARN for Google service account JSON key + * FRONT_SECRET_ARN - Secrets Manager ARN for Front API token + * GOOGLE_ADMIN_EMAIL - Email of a Google Workspace admin to impersonate + * GOOGLE_DOMAIN - Domain to list users for (e.g. seahaven.com) + * GOOGLE_OUS - Comma-separated org unit paths to sync (e.g. /Office/Scheduling,/Office/Operations) + * SLACK_WEBHOOK_URL - (Optional) Slack incoming webhook for sync summaries + */ + +const { + SecretsManagerClient, + GetSecretValueCommand, +} = require("@aws-sdk/client-secrets-manager"); +const crypto = require("crypto"); + +const sm = new SecretsManagerClient({ region: "us-east-1" }); + +// --------------------------------------------------------------------------- +// Google Auth (JWT → Access Token using service account) +// --------------------------------------------------------------------------- + +function base64url(buf) { + return Buffer.from(buf) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); +} + +async function getGoogleAccessToken(serviceAccountKey, adminEmail) { + const now = Math.floor(Date.now() / 1000); + const header = { alg: "RS256", typ: "JWT" }; + const payload = { + iss: serviceAccountKey.client_email, + sub: adminEmail, + scope: "https://www.googleapis.com/auth/admin.directory.user.readonly", + aud: "https://oauth2.googleapis.com/token", + iat: now, + exp: now + 3600, + }; + + const segments = [ + base64url(JSON.stringify(header)), + base64url(JSON.stringify(payload)), + ]; + const signingInput = segments.join("."); + + const sign = crypto.createSign("RSA-SHA256"); + sign.update(signingInput); + const signature = sign.sign(serviceAccountKey.private_key); + const jwt = signingInput + "." + base64url(signature); + + const res = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: jwt, + }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Google token exchange failed (${res.status}): ${text}`); + } + + const data = await res.json(); + return data.access_token; +} + +// --------------------------------------------------------------------------- +// Google Admin Directory: list users for a specific OU +// --------------------------------------------------------------------------- + +async function listGoogleUsersInOU(accessToken, domain, orgUnitPath) { + const users = []; + let pageToken = null; + + do { + const params = new URLSearchParams({ + domain, + maxResults: "500", + projection: "full", + orderBy: "email", + query: `orgUnitPath='${orgUnitPath}'`, + }); + if (pageToken) params.set("pageToken", pageToken); + + const res = await fetch( + `https://admin.googleapis.com/admin/directory/v1/users?${params}`, + { headers: { Authorization: `Bearer ${accessToken}` } } + ); + + if (!res.ok) { + const text = await res.text(); + throw new Error( + `Google Directory API error for OU "${orgUnitPath}" (${res.status}): ${text}` + ); + } + + const data = await res.json(); + if (data.users) users.push(...data.users); + pageToken = data.nextPageToken || null; + } while (pageToken); + + return users; +} + +async function listGoogleUsers(accessToken, domain, orgUnits) { + const allUsers = []; + const seen = new Set(); + + for (const ou of orgUnits) { + console.log(`Fetching users from OU: ${ou}`); + const users = await listGoogleUsersInOU(accessToken, domain, ou); + console.log(` Found ${users.length} users in ${ou}`); + + for (const user of users) { + if (!seen.has(user.primaryEmail)) { + seen.add(user.primaryEmail); + allUsers.push(user); + } + } + } + + return allUsers; +} + +// --------------------------------------------------------------------------- +// Extract fields from a Google user object +// --------------------------------------------------------------------------- + +function extractUserFields(googleUser) { + const email = googleUser.primaryEmail; + + // Job title lives in organizations[].title + let jobTitle = ""; + if (googleUser.organizations && googleUser.organizations.length > 0) { + jobTitle = googleUser.organizations[0].title || ""; + } + + // Phone lives in phones[].value — prefer "work" type + let phone = ""; + if (googleUser.phones && googleUser.phones.length > 0) { + const workPhone = googleUser.phones.find((p) => p.type === "work"); + phone = workPhone + ? workPhone.value + : googleUser.phones[0].value || ""; + } + + return { email, jobTitle, phone }; +} + +// --------------------------------------------------------------------------- +// Front API: update teammate custom fields +// --------------------------------------------------------------------------- + +async function updateFrontTeammate(frontToken, email, customFields) { + const url = `https://api2.frontapp.com/teammates/alt:email:${encodeURIComponent(email)}`; + + const res = await fetch(url, { + method: "PATCH", + headers: { + Authorization: `Bearer ${frontToken}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ custom_fields: customFields }), + }); + + // 204 = success, 404 = user not in Front (skip), anything else = error + if (res.status === 204) return { status: "updated" }; + if (res.status === 404) return { status: "not_in_front" }; + + const text = await res.text(); + return { status: "error", code: res.status, message: text }; +} + +// --------------------------------------------------------------------------- +// Slack notification (optional) +// --------------------------------------------------------------------------- + +async function notifySlack(webhookUrl, summary) { + if (!webhookUrl) return; + + const text = [ + `:arrows_counterclockwise: *Front ← Google Directory Sync Complete*`, + `• Updated: ${summary.updated}`, + `• Skipped (not in Front): ${summary.notInFront}`, + `• Skipped (no data): ${summary.noData}`, + `• Errors: ${summary.errors}`, + ]; + + if (summary.errorDetails.length > 0) { + text.push(""); + text.push("*Errors:*"); + summary.errorDetails.slice(0, 5).forEach((e) => { + text.push(` - \`${e.email}\`: ${e.message}`); + }); + } + + await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: text.join("\n") }), + }); +} + +// --------------------------------------------------------------------------- +// Handler +// --------------------------------------------------------------------------- + +exports.handler = async (event) => { + console.log("Starting Front ← Google Directory sync"); + + // 1. Fetch secrets + const [googleSecretRes, frontSecretRes] = await Promise.all([ + sm.send( + new GetSecretValueCommand({ SecretId: process.env.GOOGLE_SECRET_ARN }) + ), + sm.send( + new GetSecretValueCommand({ SecretId: process.env.FRONT_SECRET_ARN }) + ), + ]); + + const googleKey = JSON.parse(googleSecretRes.SecretString); + const frontToken = frontSecretRes.SecretString; + + // 2. Get Google access token + const accessToken = await getGoogleAccessToken( + googleKey, + process.env.GOOGLE_ADMIN_EMAIL + ); + console.log("Obtained Google access token"); + + // 3. List Google Workspace users from specified OUs + const orgUnits = process.env.GOOGLE_OUS + .split(",") + .map((ou) => ou.trim()) + .filter(Boolean); + console.log(`Syncing OUs: ${orgUnits.join(", ")}`); + + const googleUsers = await listGoogleUsers( + accessToken, + process.env.GOOGLE_DOMAIN, + orgUnits + ); + console.log(`Found ${googleUsers.length} total users across ${orgUnits.length} OUs`); + + // 4. Sync each user to Front + const summary = { + updated: 0, + notInFront: 0, + noData: 0, + errors: 0, + errorDetails: [], + }; + + for (const googleUser of googleUsers) { + const { email, jobTitle, phone } = extractUserFields(googleUser); + + // Skip users with no title AND no phone — nothing to sync + if (!jobTitle && !phone) { + console.log(`Skipping ${email} — no title or phone in Google`); + summary.noData++; + continue; + } + + const customFields = {}; + if (jobTitle) customFields["Job Title"] = jobTitle; + if (phone) customFields["Phone"] = phone; + + try { + const result = await updateFrontTeammate(frontToken, email, customFields); + + if (result.status === "updated") { + console.log(`Updated ${email}: ${JSON.stringify(customFields)}`); + summary.updated++; + } else if (result.status === "not_in_front") { + console.log(`Skipped ${email} — not a Front teammate`); + summary.notInFront++; + } else { + console.error(`Error updating ${email}: ${result.code} ${result.message}`); + summary.errors++; + summary.errorDetails.push({ + email, + message: `${result.code}: ${result.message}`, + }); + } + } catch (err) { + console.error(`Exception updating ${email}:`, err); + summary.errors++; + summary.errorDetails.push({ email, message: err.message }); + } + + // Simple rate-limit courtesy — Front API has rate limits + await new Promise((r) => setTimeout(r, 200)); + } + + console.log("Sync summary:", JSON.stringify(summary)); + + // 5. Notify Slack + await notifySlack(process.env.SLACK_WEBHOOK_URL, summary); + + return { + statusCode: 200, + body: summary, + }; +}; diff --git a/lambda/package.json b/lambda/package.json new file mode 100644 index 0000000..443cfd4 --- /dev/null +++ b/lambda/package.json @@ -0,0 +1,9 @@ +{ + "name": "google-user-sync", + "version": "1.0.0", + "description": "Syncs Google Workspace user profiles to Front teammate custom fields", + "main": "index.js", + "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.600.0" + } +} From c7969fc22e7a9dec836dd6ce14fd2ff53fae6714 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 13:32:59 -0400 Subject: [PATCH 02/10] Move cdk.json to root and add lockfile for CI --- .github/workflows/deploy.yaml | 2 - cdk.json | 3 + cdk/cdk.json | 3 - cdk/lib/google-user-sync-stack.js | 2 +- package-lock.json | 444 ++++++++++++++++++++++++++++++ package.json | 9 + 6 files changed, 457 insertions(+), 6 deletions(-) create mode 100644 cdk.json delete mode 100644 cdk/cdk.json create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 0125e0c..e5462cf 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -14,7 +14,5 @@ concurrency: jobs: deploy: uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main - with: - cdk-dir: cdk secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} diff --git a/cdk.json b/cdk.json new file mode 100644 index 0000000..ded8e09 --- /dev/null +++ b/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "node cdk/bin/app.js" +} diff --git a/cdk/cdk.json b/cdk/cdk.json deleted file mode 100644 index d85f6d3..0000000 --- a/cdk/cdk.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "app": "node bin/app.js" -} diff --git a/cdk/lib/google-user-sync-stack.js b/cdk/lib/google-user-sync-stack.js index 333bf16..ccd056a 100644 --- a/cdk/lib/google-user-sync-stack.js +++ b/cdk/lib/google-user-sync-stack.js @@ -33,7 +33,7 @@ class GoogleUserSyncStack extends Stack { runtime: lambda.Runtime.NODEJS_22_X, architecture: lambda.Architecture.ARM_64, handler: "index.handler", - code: lambda.Code.fromAsset(path.join(__dirname, "..", "lambda")), + code: lambda.Code.fromAsset(path.join(__dirname, "..", "..", "lambda")), timeout: Duration.minutes(5), memorySize: 256, environment: { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a4d7d36 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,444 @@ +{ + "name": "google-user-sync", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "google-user-sync", + "version": "1.0.0", + "dependencies": { + "aws-cdk-lib": "^2.150.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.273", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz", + "integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "53.22.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.22.0.tgz", + "integrity": "sha512-2GLhjjf7Db697rRyYt6rjN06fwbBIiewPo/jxSCyUN259ejF7NQQRwvrdAQb9Aq2jPaIIp1cfHSoEdXyA6Bb7Q==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.253.1", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.253.1.tgz", + "integrity": "sha512-vy+hA15/ZfSQpivkNdlIn2ZDA2hesp3WJgmtIZJDFwu6xzwv7wH7glbAdu5xCHGcOjepOaTKZSvCPC6sN+0/Vw==", + "bundleDependencies": [ + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.273", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.2", + "@aws-cdk/cloud-assembly-schema": "^53.18.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.3", + "punycode": "^2.3.1", + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "constructs": "^10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.2", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=53.15.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.18.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "5.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "10.2.5", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.3", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/constructs": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", + "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", + "license": "Apache-2.0" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7d61a9 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "google-user-sync", + "version": "1.0.0", + "private": true, + "dependencies": { + "aws-cdk-lib": "^2.150.0", + "constructs": "^10.0.0" + } +} From 04cfb41b9ac40a94220c079c778374f477f1dd12 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 13:37:07 -0400 Subject: [PATCH 03/10] Flatten cdk/ subdirectory to match org convention --- {cdk/bin => bin}/app.js | 2 +- cdk.json | 2 +- cdk/package.json | 11 ----------- {cdk/lib => lib}/google-user-sync-stack.js | 2 +- 4 files changed, 3 insertions(+), 14 deletions(-) rename {cdk/bin => bin}/app.js (76%) delete mode 100644 cdk/package.json rename {cdk/lib => lib}/google-user-sync-stack.js (97%) diff --git a/cdk/bin/app.js b/bin/app.js similarity index 76% rename from cdk/bin/app.js rename to bin/app.js index 79e3601..cb99eaa 100644 --- a/cdk/bin/app.js +++ b/bin/app.js @@ -1,6 +1,6 @@ #!/usr/bin/env node const cdk = require("aws-cdk-lib"); -const { GoogleUserSyncStack } = require("./lib/google-user-sync-stack"); +const { GoogleUserSyncStack } = require("../lib/google-user-sync-stack"); const app = new cdk.App(); diff --git a/cdk.json b/cdk.json index ded8e09..d85f6d3 100644 --- a/cdk.json +++ b/cdk.json @@ -1,3 +1,3 @@ { - "app": "node cdk/bin/app.js" + "app": "node bin/app.js" } diff --git a/cdk/package.json b/cdk/package.json deleted file mode 100644 index cd30d83..0000000 --- a/cdk/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "google-user-sync-cdk", - "version": "1.0.0", - "bin": { - "app": "bin/app.js" - }, - "dependencies": { - "aws-cdk-lib": "^2.150.0", - "constructs": "^10.0.0" - } -} diff --git a/cdk/lib/google-user-sync-stack.js b/lib/google-user-sync-stack.js similarity index 97% rename from cdk/lib/google-user-sync-stack.js rename to lib/google-user-sync-stack.js index ccd056a..333bf16 100644 --- a/cdk/lib/google-user-sync-stack.js +++ b/lib/google-user-sync-stack.js @@ -33,7 +33,7 @@ class GoogleUserSyncStack extends Stack { runtime: lambda.Runtime.NODEJS_22_X, architecture: lambda.Architecture.ARM_64, handler: "index.handler", - code: lambda.Code.fromAsset(path.join(__dirname, "..", "..", "lambda")), + code: lambda.Code.fromAsset(path.join(__dirname, "..", "lambda")), timeout: Duration.minutes(5), memorySize: 256, environment: { From d6720061e19de95882aaf1f067a5274de482797e Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 13:44:23 -0400 Subject: [PATCH 04/10] Preserve existing custom fields on PATCH and prefer primary org/phone --- lambda/index.js | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/lambda/index.js b/lambda/index.js index bf67556..339d60b 100644 --- a/lambda/index.js +++ b/lambda/index.js @@ -139,19 +139,19 @@ async function listGoogleUsers(accessToken, domain, orgUnits) { function extractUserFields(googleUser) { const email = googleUser.primaryEmail; - // Job title lives in organizations[].title let jobTitle = ""; if (googleUser.organizations && googleUser.organizations.length > 0) { - jobTitle = googleUser.organizations[0].title || ""; + const primaryOrg = googleUser.organizations.find((o) => o.primary) || googleUser.organizations[0]; + jobTitle = primaryOrg.title || ""; } - // Phone lives in phones[].value — prefer "work" type let phone = ""; if (googleUser.phones && googleUser.phones.length > 0) { const workPhone = googleUser.phones.find((p) => p.type === "work"); + const primaryPhone = googleUser.phones.find((p) => p.primary) || googleUser.phones[0]; phone = workPhone ? workPhone.value - : googleUser.phones[0].value || ""; + : primaryPhone.value || ""; } return { email, jobTitle, phone }; @@ -163,23 +163,32 @@ function extractUserFields(googleUser) { async function updateFrontTeammate(frontToken, email, customFields) { const url = `https://api2.frontapp.com/teammates/alt:email:${encodeURIComponent(email)}`; + const headers = { + Authorization: `Bearer ${frontToken}`, + "Content-Type": "application/json", + Accept: "application/json", + }; - const res = await fetch(url, { + const getRes = await fetch(url, { headers }); + if (getRes.status === 404) return { status: "not_in_front" }; + if (!getRes.ok) { + const text = await getRes.text(); + return { status: "error", code: getRes.status, message: text }; + } + + const teammate = await getRes.json(); + const merged = { ...teammate.custom_fields, ...customFields }; + + const patchRes = await fetch(url, { method: "PATCH", - headers: { - Authorization: `Bearer ${frontToken}`, - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ custom_fields: customFields }), + headers, + body: JSON.stringify({ custom_fields: merged }), }); - // 204 = success, 404 = user not in Front (skip), anything else = error - if (res.status === 204) return { status: "updated" }; - if (res.status === 404) return { status: "not_in_front" }; + if (patchRes.status === 204) return { status: "updated" }; - const text = await res.text(); - return { status: "error", code: res.status, message: text }; + const text = await patchRes.text(); + return { status: "error", code: patchRes.status, message: text }; } // --------------------------------------------------------------------------- From eae9655c741bf5d90988b3aa84bde1a90097d94a Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 13:54:24 -0400 Subject: [PATCH 05/10] Remove unused Slack notification code --- lambda/index.js | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/lambda/index.js b/lambda/index.js index 339d60b..3d24bef 100644 --- a/lambda/index.js +++ b/lambda/index.js @@ -10,7 +10,6 @@ * GOOGLE_ADMIN_EMAIL - Email of a Google Workspace admin to impersonate * GOOGLE_DOMAIN - Domain to list users for (e.g. seahaven.com) * GOOGLE_OUS - Comma-separated org unit paths to sync (e.g. /Office/Scheduling,/Office/Operations) - * SLACK_WEBHOOK_URL - (Optional) Slack incoming webhook for sync summaries */ const { @@ -191,36 +190,6 @@ async function updateFrontTeammate(frontToken, email, customFields) { return { status: "error", code: patchRes.status, message: text }; } -// --------------------------------------------------------------------------- -// Slack notification (optional) -// --------------------------------------------------------------------------- - -async function notifySlack(webhookUrl, summary) { - if (!webhookUrl) return; - - const text = [ - `:arrows_counterclockwise: *Front ← Google Directory Sync Complete*`, - `• Updated: ${summary.updated}`, - `• Skipped (not in Front): ${summary.notInFront}`, - `• Skipped (no data): ${summary.noData}`, - `• Errors: ${summary.errors}`, - ]; - - if (summary.errorDetails.length > 0) { - text.push(""); - text.push("*Errors:*"); - summary.errorDetails.slice(0, 5).forEach((e) => { - text.push(` - \`${e.email}\`: ${e.message}`); - }); - } - - await fetch(webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text: text.join("\n") }), - }); -} - // --------------------------------------------------------------------------- // Handler // --------------------------------------------------------------------------- @@ -314,9 +283,6 @@ exports.handler = async (event) => { console.log("Sync summary:", JSON.stringify(summary)); - // 5. Notify Slack - await notifySlack(process.env.SLACK_WEBHOOK_URL, summary); - return { statusCode: 200, body: summary, From 31f1ba15ef01283b26ed45f797b5b719ddd67a01 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 13:57:30 -0400 Subject: [PATCH 06/10] Guard against phone entries missing value property --- lambda/index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lambda/index.js b/lambda/index.js index 3d24bef..9b46017 100644 --- a/lambda/index.js +++ b/lambda/index.js @@ -146,11 +146,9 @@ function extractUserFields(googleUser) { let phone = ""; if (googleUser.phones && googleUser.phones.length > 0) { - const workPhone = googleUser.phones.find((p) => p.type === "work"); - const primaryPhone = googleUser.phones.find((p) => p.primary) || googleUser.phones[0]; - phone = workPhone - ? workPhone.value - : primaryPhone.value || ""; + const workPhone = googleUser.phones.find((p) => p.type === "work" && p.value); + const primaryPhone = googleUser.phones.find((p) => p.primary && p.value) || googleUser.phones.find((p) => p.value); + phone = (workPhone || primaryPhone || {}).value || ""; } return { email, jobTitle, phone }; From 5e15036e51d8ee7629dbe4a9b2f21b41eeff1cf1 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 13:58:24 -0400 Subject: [PATCH 07/10] Update README.md Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index ac697bc..8b73762 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,8 @@ aws secretsmanager create-secret \ ## Deployment ```bash -cd cdk npm install npx cdk deploy google-user-sync -``` ## Verification From b0e047e5a6a5c220a12cd30cf928352e33b74043 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 13:58:38 -0400 Subject: [PATCH 08/10] Update lambda/index.js Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- lambda/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lambda/index.js b/lambda/index.js index 9b46017..970e66c 100644 --- a/lambda/index.js +++ b/lambda/index.js @@ -148,9 +148,7 @@ function extractUserFields(googleUser) { if (googleUser.phones && googleUser.phones.length > 0) { const workPhone = googleUser.phones.find((p) => p.type === "work" && p.value); const primaryPhone = googleUser.phones.find((p) => p.primary && p.value) || googleUser.phones.find((p) => p.value); - phone = (workPhone || primaryPhone || {}).value || ""; - } - + phone = (workPhone && workPhone.value) || primaryPhone.value || ""; return { email, jobTitle, phone }; } From 23235cfd760dd6394d1912ef98d2c8c54d2bef0b Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 14:10:45 -0400 Subject: [PATCH 09/10] Use secret names instead of partial ARNs and fix README paths --- README.md | 4 ++-- lambda/index.js | 8 ++++---- lib/google-user-sync-stack.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8b73762..1ab8087 100644 --- a/README.md +++ b/README.md @@ -88,5 +88,5 @@ Check Front > Company Settings > Teammates > pick a user > Custom Fields to conf ## Maintenance - **New teammates:** Automatically picked up if they exist in both Google Workspace and Front with the same email. -- **Schedule changes:** Update the EventBridge rule in `cdk/lib/google-user-sync-stack.js`. -- **Additional fields:** Add as custom fields in Front, then update the field mapping in `lambda/index.js` (`buildCustomFields()`). +- **Schedule changes:** Update the EventBridge rule in `lib/google-user-sync-stack.js`. +- **Additional fields:** Add as custom fields in Front, then update the field mapping in `lambda/index.js`. diff --git a/lambda/index.js b/lambda/index.js index 970e66c..a52addd 100644 --- a/lambda/index.js +++ b/lambda/index.js @@ -5,8 +5,8 @@ * and syncs job title + phone to Front teammate custom fields. * * Environment variables: - * GOOGLE_SECRET_ARN - Secrets Manager ARN for Google service account JSON key - * FRONT_SECRET_ARN - Secrets Manager ARN for Front API token + * GOOGLE_SECRET_NAME - Secrets Manager name for Google service account JSON key + * FRONT_SECRET_NAME - Secrets Manager name for Front API token * GOOGLE_ADMIN_EMAIL - Email of a Google Workspace admin to impersonate * GOOGLE_DOMAIN - Domain to list users for (e.g. seahaven.com) * GOOGLE_OUS - Comma-separated org unit paths to sync (e.g. /Office/Scheduling,/Office/Operations) @@ -196,10 +196,10 @@ exports.handler = async (event) => { // 1. Fetch secrets const [googleSecretRes, frontSecretRes] = await Promise.all([ sm.send( - new GetSecretValueCommand({ SecretId: process.env.GOOGLE_SECRET_ARN }) + new GetSecretValueCommand({ SecretId: process.env.GOOGLE_SECRET_NAME }) ), sm.send( - new GetSecretValueCommand({ SecretId: process.env.FRONT_SECRET_ARN }) + new GetSecretValueCommand({ SecretId: process.env.FRONT_SECRET_NAME }) ), ]); diff --git a/lib/google-user-sync-stack.js b/lib/google-user-sync-stack.js index 333bf16..be757a7 100644 --- a/lib/google-user-sync-stack.js +++ b/lib/google-user-sync-stack.js @@ -37,8 +37,8 @@ class GoogleUserSyncStack extends Stack { timeout: Duration.minutes(5), memorySize: 256, environment: { - GOOGLE_SECRET_ARN: googleSecret.secretArn, - FRONT_SECRET_ARN: frontSecret.secretArn, + GOOGLE_SECRET_NAME: "google-user-sync/google-service-account", + FRONT_SECRET_NAME: "google-user-sync/front-api-token", GOOGLE_ADMIN_EMAIL: "adam@seahavenind.com", GOOGLE_DOMAIN: "seahavenind.com", GOOGLE_OUS: "/Office/Scheduling,/Office/Operations", From fda51b2524353d250d4cf81c8bc16646e3f82356 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 11 May 2026 14:11:39 -0400 Subject: [PATCH 10/10] Fix missing brace from bad rebase and README fence --- README.md | 1 + lambda/index.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ab8087..dd05146 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ aws secretsmanager create-secret \ ```bash npm install npx cdk deploy google-user-sync +``` ## Verification diff --git a/lambda/index.js b/lambda/index.js index a52addd..d133ce4 100644 --- a/lambda/index.js +++ b/lambda/index.js @@ -148,7 +148,9 @@ function extractUserFields(googleUser) { if (googleUser.phones && googleUser.phones.length > 0) { const workPhone = googleUser.phones.find((p) => p.type === "work" && p.value); const primaryPhone = googleUser.phones.find((p) => p.primary && p.value) || googleUser.phones.find((p) => p.value); - phone = (workPhone && workPhone.value) || primaryPhone.value || ""; + phone = (workPhone || primaryPhone || {}).value || ""; + } + return { email, jobTitle, phone }; }