diff --git a/client/Utility/Metrics.ts b/client/Utility/Metrics.ts index 7b0aa3ae..ff021b59 100644 --- a/client/Utility/Metrics.ts +++ b/client/Utility/Metrics.ts @@ -48,7 +48,9 @@ export class Metrics { } if (typeof gtag == "function") { - gtag("event", name, eventData); + try { + gtag("event", name, eventData); + } catch (e) {} } if (!env.SendMetrics) { @@ -77,7 +79,9 @@ export class Metrics { } if (typeof gtag == "function") { - gtag("event", name, eventData); + try { + gtag("event", name, eventData); + } catch (e) {} } if (!env.SendMetrics) { diff --git a/package-lock.json b/package-lock.json index 8a389021..78c38197 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "improved-initiative", - "version": "3.7.7", + "version": "3.7.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "improved-initiative", - "version": "3.7.7", + "version": "3.7.8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index be03319e..bfcf0a05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "improved-initiative", - "version": "3.7.7", + "version": "3.7.8", "description": "Combat tracker for Dungeons and Dragons (D&D) 5th Edition", "license": "MIT", "repository": { diff --git a/server/patreon.ts b/server/patreon.ts index 4bf056a1..4f59aa49 100644 --- a/server/patreon.ts +++ b/server/patreon.ts @@ -3,7 +3,8 @@ import * as crypto from "crypto"; import * as express from "express"; import * as _ from "lodash"; -import * as patreon from "patreon"; +import axios from "axios"; +import * as querystring from "querystring"; import * as request from "request"; @@ -25,10 +26,10 @@ const tiersWithAccountSyncEntitled = [ const tiersWithEpicEntitled = ["1937132", "8749940"]; -const baseUrl = process.env.BASE_URL, - patreonClientId = process.env.PATREON_CLIENT_ID, - patreonClientSecret = process.env.PATREON_CLIENT_SECRET, - patreonUrl = process.env.PATREON_URL; +const baseUrl = process.env.BASE_URL; +const patreonClientId = process.env.PATREON_CLIENT_ID; +const patreonClientSecret = process.env.PATREON_CLIENT_SECRET; +const patreonUrl = process.env.PATREON_URL; interface Post { attributes: { @@ -60,17 +61,22 @@ export function configureLoginRedirect(app: express.Application): void { app.get(redirectPath, async (req: Req, res: Res) => { try { - console.log("req.query >" + JSON.stringify(req.query)); - console.log("req.body >" + JSON.stringify(req.body)); - const code = req.query.code; - - const OAuthClient = patreon.oauth(patreonClientId, patreonClientSecret); - - const tokens = await OAuthClient.getTokens(code, redirectUri); - - const APIClient = patreon.patreon(tokens.access_token); - const { rawJson } = await APIClient(`/current_user`); - await handleCurrentUser(req, res, rawJson); + const code = req.query.code as string; + + const tokens = await getTokens(code, redirectUri); + + const userResponse = await axios.get( + `https://www.patreon.com/api/oauth2/v2/identity` + + `?${encodeURIComponent("fields[user]")}=email` + + `&include=memberships.currently_entitled_tiers`, + { + headers: { + authorization: "Bearer " + tokens.access_token + } + } + ); + + await handleCurrentUser(req, res, userResponse.data); } catch (err) { console.error("Patreon login flow failed:", JSON.stringify(err)); res @@ -82,6 +88,23 @@ export function configureLoginRedirect(app: express.Application): void { }); } +async function getTokens(code: string, redirectUri: string) { + const tokensResponse = await axios.post( + "https://www.patreon.com/api/oauth2/token", + querystring.stringify({ + code: code, + grant_type: "authorization_code", + client_id: patreonClientId, + client_secret: patreonClientSecret, + redirect_uri: redirectUri + }), + { headers: { "content-type": "application/x-www-form-urlencoded" } } + ); + + const tokens = tokensResponse.data; + return tokens; +} + export async function handleCurrentUser( req: Req, res: Res, @@ -92,16 +115,10 @@ export async function handleCurrentUser( encounterId = (req.query.state as string).replace(/['"]/g, ""); } - const pledges = (apiResponse.included || []).filter( - item => item.type == "pledge" && item.attributes.declined_since == null - ); - - const userRewards = pledges.map((r: Pledge) => - _.get(r, "relationships.reward.data.id", "none") - ); + const entitledTierIds = getEntitledTierIds(apiResponse); const userId = apiResponse.data.id; - const standing = getUserAccountLevel(userId, userRewards); + const standing = getUserAccountLevel(userId, entitledTierIds); const emailAddress = _.get(apiResponse, "data.attributes.email", ""); console.log( `User login: ${emailAddress}, API response: ${JSON.stringify( @@ -124,13 +141,20 @@ export async function handleCurrentUser( res.redirect(`/e/${encounterId}?login=patreon`); } -export function updateSessionAccountFeatures( - session: Express.Session, - standing: AccountStatus -): void { - session.hasStorage = standing == "pledge" || standing == "epic"; - session.hasEpicInitiative = standing == "epic"; - session.isLoggedIn = true; +function getEntitledTierIds(apiResponse: Record) { + const memberships = apiResponse.included?.filter(i => i.type === "member"); + if (!memberships) { + return []; + } + + const entitledTierIds = _.flatMap( + memberships, + m => m.relationships?.currently_entitled_tiers?.data + ) + .filter(d => d?.type === "tier") + .map(d => d.id); + + return entitledTierIds; } function getUserAccountLevel( @@ -158,6 +182,15 @@ function getUserAccountLevel( return standing; } +export function updateSessionAccountFeatures( + session: Express.Session, + standing: AccountStatus +): void { + session.hasStorage = standing == "pledge" || standing == "epic"; + session.hasEpicInitiative = standing == "epic"; + session.isLoggedIn = true; +} + export function configureLogout(app: express.Application): void { const logoutPath = "/logout"; app.get(logoutPath, (req: Req, res: Res) => { @@ -270,8 +303,6 @@ async function handleWebhook(req: Req, res: Res) { } function verifySender(req: Req, res: Res, next) { - console.log(req.rawBody); - const webhookSecret = process.env.PATREON_WEBHOOK_SECRET; if (!webhookSecret) { return res.status(501).send("Webhook not configured"); @@ -279,12 +310,12 @@ function verifySender(req: Req, res: Res, next) { const signature = req.header("X-Patreon-Signature"); if (!signature) { - console.log("Signature not found."); + console.warn("Signature not found."); return res.status(401).send("Signature not found."); } if (!verifySignature(signature, webhookSecret, req.rawBody)) { - console.log("Signature mismatch with provided signature: " + signature); + console.warn("Signature mismatch with provided signature: " + signature); return res.status(401).send("Signature mismatch."); } diff --git a/server/routes.ts b/server/routes.ts index 9681235e..480c20c4 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -40,7 +40,8 @@ const getClientOptions = (session: Express.Session) => { "http://www.patreon.com/oauth2/authorize" + `?response_type=code&client_id=${patreonClientId}` + `&redirect_uri=${baseUrl}/r/patreon` + - `&scope=users pledges-to-me` + + `&scope=` + + encodeURIComponent(`identity identity.memberships identity[email]`) + `&state=${encounterId}`; const environment: ClientEnvironment = {