Skip to content

Commit

Permalink
💸 Tech Debt - Migrate CRMService (#1496)
Browse files Browse the repository at this point in the history
* Added allsettled

* Added scope as argument

* Created basic tester endpoint

* Added error handling

* schema changes

* Schema addition

* Created working Dynamics fetching

* Migrated internal speakers service to v3 Next.js API

* Removed log

* Added key vault secrets to pipeline

* Added warning for missing env variables

* Removed redundant try-catch, removed potentially exposing error messages

* Moved Dynamics scopes to const
  • Loading branch information
Harry-Ross authored Oct 11, 2023
1 parent 3c8fd38 commit 3254a7d
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 31 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ SHAREPOINT_SITE_ID=***
SHAREPOINT_EVENTS_LIST_ID=***
SHAREPOINT_EXTERNAL_PRESENTERS_LIST_ID=***

# Dynamics - Fetching Internal Speakers for Events
DYNAMICS_CLIENT_ID=***
DYNAMICS_CLIENT_SECRET=***

# Turn on the NextJS bundle analyzer (https://www.npmjs.com/package/@next/bundle-analyzer)
BUNDLE_ANALYSE=false

4 changes: 3 additions & 1 deletion .github/workflows/template-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ jobs:
"MICROSOFT_OAUTH_CLIENT_SECRET": "MICROSOFT-OAUTH-CLIENT-SECRET",
"SHAREPOINT_SITE_ID": "SHAREPOINT-SITE-ID",
"SHAREPOINT_EVENTS_LIST_ID": "SHAREPOINT-EVENTS-LIST-ID",
"SHAREPOINT_EXTERNAL_PRESENTERS_LIST_ID": "SHAREPOINT-EXTERNAL-PRESENTERS-LIST-ID"
"SHAREPOINT_EXTERNAL_PRESENTERS_LIST_ID": "SHAREPOINT-EXTERNAL-PRESENTERS-LIST-ID",
"DYNAMICS_CLIENT_ID": "DYNAMICS-CLIENT-ID",
"DYNAMICS_CLIENT_SECRET": "DYNAMICS-CLIENT-SECRET"
}
- name: AppInsight - Connection Strings
Expand Down
8 changes: 8 additions & 0 deletions infra/appSerivce-create-slot.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ var appSettings = [
name: 'RECAPTCHA_BYPASS_SECRET'
value: '@Microsoft.KeyVault(SecretUri=https://${keyVaultName}.vault.azure.net/secrets/SECRET-KEY-TO-BYPASS-RECAPTCHA)'
}
{
name: 'DYNAMICS_CLIENT_ID'
value: '@Microsoft.KeyVault(SecretUri=https://${keyVaultName}.vault.azure.net/secrets/DYNAMICS-CLIENT-ID)'
}
{
name: 'DYNAMICS_CLIENT_SECRET'
value: '@Microsoft.KeyVault(SecretUri=https://${keyVaultName}.vault.azure.net/secrets/DYNAMICS-CLIENT-SECRET)'
}
]


Expand Down
8 changes: 8 additions & 0 deletions infra/appService.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ var appSettings = [
name: 'RECAPTCHA_BYPASS_SECRET'
value: '@Microsoft.KeyVault(SecretUri=https://${keyVaultName}.vault.azure.net/secrets/SECRET-KEY-TO-BYPASS-RECAPTCHA)'
}
{
name: 'DYNAMICS_CLIENT_ID'
value: '@Microsoft.KeyVault(SecretUri=https://${keyVaultName}.vault.azure.net/secrets/DYNAMICS-CLIENT-ID)'
}
{
name: 'DYNAMICS_CLIENT_SECRET'
value: '@Microsoft.KeyVault(SecretUri=https://${keyVaultName}.vault.azure.net/secrets/DYNAMICS-CLIENT-SECRET)'
}
]

resource appService 'Microsoft.Web/sites@2022-03-01' = {
Expand Down
2 changes: 1 addition & 1 deletion pages/api/get-past-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default async function handler(
properties,
severity: appInsights.Contracts.SeverityLevel.Error,
});
res.status(500).json({ message: err.message });
res.status(500).json({ message: "SharePoint request failed" });
}
} else {
res.status(405).json({ message: "Unsupported method" });
Expand Down
2 changes: 1 addition & 1 deletion pages/api/get-speakers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default async function handler(
properties,
severity: appInsights.Contracts.SeverityLevel.Error,
});
res.status(500).json({ message: err.message });
res.status(500).json({ message: "Speaker request failed" });
}
} else {
res.status(405).json({ message: "Unsupported method" });
Expand Down
2 changes: 1 addition & 1 deletion pages/api/get-upcoming-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default async function handler(
properties,
severity: appInsights.Contracts.SeverityLevel.Error,
});
res.status(500).json({ message: err.message });
res.status(500).json({ message: "SharePoint request failed" });
}
} else {
res.status(405).json({ message: "Unsupported method" });
Expand Down
122 changes: 95 additions & 27 deletions services/server/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@ const EVENTS_LIST_ID = process.env.SHAREPOINT_EVENTS_LIST_ID;
const EXTERNAL_PRESENTERS_LIST_ID =
process.env.SHAREPOINT_EXTERNAL_PRESENTERS_LIST_ID;

export const getToken = async () => {
const SHAREPOINT_SCOPES = ["https://graph.microsoft.com/.default"];
const DYNAMICS_SCOPES = ["https://ssw.crm6.dynamics.com/.default"];

export const getToken = async (
scopes: string[],
clientId: string,
clientSecret
) => {
const clientConfig = {
auth: {
clientId: process.env.MICROSOFT_OAUTH_CLIENT_ID,
clientId,
authority: `https://login.microsoftonline.com/${process.env.MICROSOFT_OAUTH_TENANT_ID}`,
clientSecret: process.env.MICROSOFT_OAUTH_CLIENT_SECRET,
clientSecret,
},
};

const clientApp = new msal.ConfidentialClientApplication(clientConfig);

const authParams = {
scopes: ["https://graph.microsoft.com/.default"],
scopes,
};

const authRes = await clientApp.acquireTokenByClientCredential(authParams);
Expand All @@ -37,7 +44,11 @@ export const getEvents = async (odataFilter: string): Promise<EventInfo[]> => {
return [];
}

const token = await getToken();
const token = await getToken(
SHAREPOINT_SCOPES,
process.env.MICROSOFT_OAUTH_CLIENT_ID,
process.env.MICROSOFT_OAUTH_CLIENT_SECRET
);

const eventsRes = await axios.get<{ value: { fields: EventInfo }[] }>(
`https://graph.microsoft.com/v1.0/sites/${SITE_ID}/lists/${EVENTS_LIST_ID}/items?expand=fields&${odataFilter}`,
Expand Down Expand Up @@ -69,7 +80,11 @@ export const getSpeakersInfo = async (ids?: number[], emails?: string[]) => {
}

if (ids?.length) {
const token = await getToken();
const token = await getToken(
SHAREPOINT_SCOPES,
process.env.MICROSOFT_OAUTH_CLIENT_ID,
process.env.MICROSOFT_OAUTH_CLIENT_SECRET
);

const idSpeakers: SpeakerInfo[] = await Promise.all(
ids.map(async (id) => {
Expand All @@ -94,35 +109,88 @@ export const getSpeakersInfo = async (ids?: number[], emails?: string[]) => {
}

if (emails?.length) {
await Promise.all(
emails.map(async (email) => {
const internalSpeakerRes = await axios.get<InternalSpeakerInfo>(
"https://www.ssw.com.au/ssw/CRMService.aspx",
{
params: { odata: encodeURIComponent(email) },
}
);
const internalSpeakers = await getInternalSpeakers(emails);

if (internalSpeakerRes.status === 200 && internalSpeakerRes.data) {
const internalSpeaker = internalSpeakerRes.data;
speakers.push({
Title: internalSpeaker.Nickname
? `${internalSpeaker.FirstName} (${internalSpeaker.Nickname}) ${internalSpeaker.LastName}`
: `${internalSpeaker.FirstName} ${internalSpeaker.LastName}`,
PresenterProfileImage: {
Url: internalSpeaker.PhotoURL,
},
PresenterShortDescription: internalSpeaker.ShortDescription,
PresenterProfileLink: internalSpeaker.ProfileURL,
});
}
const internalSpeakersInfo: SpeakerInfo[] = internalSpeakers.map(
(internalSpeaker) => ({
Title: internalSpeaker.Nickname
? `${internalSpeaker.FirstName} (${internalSpeaker.Nickname}) ${internalSpeaker.LastName}`
: `${internalSpeaker.FirstName} ${internalSpeaker.LastName}`,
PresenterProfileImage: {
Url: internalSpeaker.PhotoURL,
},
PresenterShortDescription: internalSpeaker.ShortDescription,
PresenterProfileLink: internalSpeaker.ProfileURL,
})
);

speakers.push(...internalSpeakersInfo);
}

return speakers;
};

export const getInternalSpeakers = async (
emails: string[]
): Promise<InternalSpeakerInfo[]> => {
if (
process.env.NODE_ENV === "development" &&
(!process.env.DYNAMICS_CLIENT_ID || !process.env.DYNAMICS_CLIENT_SECRET)
) {
console.warn(
"⚠️ You are missing the Dynamics 365 environment variables required for speakers. Please see the .env.example file for the required variables."
);
return [];
}

const accessToken = await getToken(
DYNAMICS_SCOPES,
process.env.DYNAMICS_CLIENT_ID,
process.env.DYNAMICS_CLIENT_SECRET
);

let odataFilter = "";

emails.forEach((email, index) => {
if (index === emails.length - 1) {
odataFilter += `internalemailaddress eq '${email}'`;
} else {
odataFilter += `internalemailaddress eq '${email}' or `;
}
});

const internalSpeakersRes = await axios.get(
"https://ssw.crm6.dynamics.com/api/data/v9.2/systemusers?$select=firstname,lastname,nickname,photourl,ssw_githuburl,ssw_publicprofileurl,ssw_shortdescription,internalemailaddress&$filter=" +
odataFilter,
{
headers: {
Authorization: "Bearer " + accessToken,
Accept: "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
},
}
);

if (internalSpeakersRes?.data?.value?.length > 0) {
const speakers: InternalSpeakerInfo[] = internalSpeakersRes.data.value.map(
(user) => ({
FirstName: user.firstname,
LastName: user.lastname,
Nickname: user.nickname,
PhotoURL: user.photourl,
GitHubURL: user.ssw_githuburl,
ProfileURL: user.ssw_publicprofileurl,
ShortDescription: user.ssw_shortdescription,
})
);

return speakers;
} else {
return [];
}
};

export type BookingFormSubmissionData = {
Name: string;
Topic: string;
Expand Down

0 comments on commit 3254a7d

Please sign in to comment.