From be39368de7b198bd3eb725117c3b0b0845c125af Mon Sep 17 00:00:00 2001 From: tnagorra Date: Tue, 31 Mar 2026 19:12:04 +0545 Subject: [PATCH] feat: migrate changes made after the fork --- README.md | 4 + firebase.json | 8 + functions/src/index.ts | 426 ++++++++++++++++++-------------------- functions/src/osm_auth.ts | 94 ++++++--- 4 files changed, 289 insertions(+), 243 deletions(-) diff --git a/README.md b/README.md index fed4726..8f30d07 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,16 @@ Some specifics about the related functions: - Before deploying, set the required firebase config values in environment: FIXME: replace env vars with config value names - OSM_OAUTH_REDIRECT_URI `osm.redirect_uri`: `https://dev-auth.mapswipe.org/token` or `https://auth.mapswipe.org/token` + - OSM_OAUTH_REDIRECT_URI_WEB: `https://dev-auth.mapswipe.org/tokenweb` or `https://auth.mapswipe.org/tokenweb` - OSM_OAUTH_APP_LOGIN_LINK `osm.app_login_link`: 'devmapswipe://login/osm' or 'mapswipe://login/osm' + - OSM_OAUTH_APP_LOGIN_LINK_WEB: `https://web.mapswipe.org/dev/#/osm-callback` or `https://web.mapswipe.org/#/osm-callback` - OSM_OAUTH_API_URL `osm.api_url`: 'https://master.apis.dev.openstreetmap.org/' or 'https://www.openstreetmap.org/' (include the trailing slash) - OSM_OAUTH_CLIENT_ID `osm.client_id`: find it on the OSM application page - OSM_OAUTH_CLIENT_SECRET `osm.client_secret`: same as above. Note that this can only be seen once when the application is created. Do not lose it! + - OSM_OAUTH_CLIENT_ID_WEB: This is the ID of a __different__ registered OSM OAuth client for the web version that needs to have `https://dev-auth.mapswipe.org/tokenweb` or `https://auth.mapswipe.org/tokenweb` set as redirect URI. + - OSM_OAUTH_CLIENT_SECRET_WEB: This is the secret of the OSM OAuth client for MapSwipe web version. - Deploy the functions as explained above - Expose the functions publicly through firebase hosting, this is done in `/firebase/firebase.json` under the `hosting` key. diff --git a/firebase.json b/firebase.json index edb2ce0..ae90726 100644 --- a/firebase.json +++ b/firebase.json @@ -20,6 +20,14 @@ { "source": "/token", "function": "osmAuth-token" + }, + { + "source": "/redirectweb", + "function": "osmAuth-redirectweb" + }, + { + "source": "/tokenweb", + "function": "osmAuth-tokenweb" } ] }, diff --git a/functions/src/index.ts b/functions/src/index.ts index 52becec..c448cad 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -8,12 +8,12 @@ admin.initializeApp(); // all functions are bundled together. It's less than ideal, but it does not // seem possible to split them using the split system for multiple sites from // https://firebase.google.com/docs/hosting/multisites -import {redirect, token} from './osm_auth'; +import {redirect, token, redirectweb, tokenweb} from './osm_auth'; import { formatProjectTopic, formatUserName } from './utils'; exports.osmAuth = {}; -// expose HTTP exposed functions here so that we can pass the admin object +// expose HTTP expossed functions here so that we can pass the admin object // to them and only instantiate/initialize it once exports.osmAuth.redirect = functions.https.onRequest((req, res) => { redirect(req, res); @@ -23,63 +23,87 @@ exports.osmAuth.token = functions.https.onRequest((req, res) => { token(req, res, admin); }); -/** - * Log the userIds of all users who finished a group to /v2/userGroups/{projectId}/{groupId}/. - * Gets triggered when new results of a group are written to the database. - * This is the basis to calculate number of users who finished a group (requiredCount and finishedCount), - * which will be handled in the groupFinishedCountUpdater function. - * - * This function also writes to the `contributions` section in the user profile. - */ -exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{groupId}/{userId}/').onCreate(async (snapshot, context) => { - const db = admin.database(); - const { projectId, groupId, userId } = context.params; +exports.osmAuth.redirectweb = functions.https.onRequest((req, res) => { + redirectweb(req, res); +}); - // these references/values will be updated by this function - const groupUsersRef = db.ref(`/v2/groupsUsers/${projectId}/${groupId}`); - const userRef = db.ref(`/v2/users/${userId}`); +exports.osmAuth.tokenweb = functions.https.onRequest((req, res) => { + tokenweb(req, res, admin); +}); - const thisResultRef = db.ref(`/v2/results/${projectId}/${groupId}/${userId}`); +/* + Log the userIds of all users who finished a group to /v2/userGroups/{projectId}/{groupId}/. + Gets triggered when new results of a group are written to the database. + This is the basis to calculate number of users who finished a group (requiredCount and finishedCount), + which will be handled in the groupFinishedCountUpdater function. + This function also writes to the `contributions` section in the user profile. +*/ +exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{groupId}/{userId}/').onCreate(async (snapshot, context) => { + // these references/values will be updated by this function + const groupUsersRef = admin.database().ref('/v2/groupsUsers/' + context.params.projectId + '/' + context.params.groupId); + const userRef = admin.database().ref('/v2/users/' + context.params.userId); + const totalTaskContributionCountRef = userRef.child('taskContributionCount'); + const totalGroupContributionCountRef = userRef.child('groupContributionCount'); + const userContributionRef = userRef.child('contributions/' + context.params.projectId); + const taskContributionCountRef = userRef.child('contributions/' + context.params.projectId + '/taskContributionCount'); + const thisResultRef = admin.database().ref('/v2/results/' + context.params.projectId + '/' + context.params.groupId + '/' + context.params.userId ); + const userGroupsRef = admin.database().ref('/v2/userGroups/'); + + let appVersionString: string | undefined | null = undefined; + + type Args = Record + // eslint-disable-next-line require-jsdoc + function logger(message: string, extraArgs: Args = {}, logFunction: (typeof console.log) = console.log) { + const ctx: Args = { + message: message, + ...extraArgs, + project: context.params.projectId, + user: context.params.userId, + group: context.params.groupId, + version: appVersionString, + }; + const items = Object.keys(ctx).reduce( + (acc, key) => { + const value = ctx[key]; + if (value === undefined || value === null || value === '') { + return acc; + } + const item = `${key}[${value}]`; + return [...acc, item]; + }, + [] + ); + logFunction(items.join(' ')); + } // Check for specific user ids which have been identified as problematic. // These users have repeatedly uploaded harmful results. // Add new user ids to this list if needed. const userIds: string[] = []; - if (userIds.includes(userId)) { - console.log('suspicious user: ' + userId); - console.log('will remove this result and not update counters'); + if (userIds.includes(context.params.userId) ) { + console.log('Result removed because of suspicious user activity'); return thisResultRef.remove(); } - const result = snapshot.val() as { - appVersion: string | undefined | null, - results: unknown[], - endTime: string, - startTime: string, - }; - - + const result = snapshot.val(); // New versions of app will have the appVersion defined (> 2.2.5) // appVersion: 2.2.5 (14)-dev - const appVersionString = result.appVersion; + appVersionString = result.appVersion; // Check if the app is of older version // (no need to check for specific version since old app won't sent the version info) if (appVersionString === null || appVersionString === undefined || appVersionString.trim() === '') { - const projectRef = db.ref(`/v2/projects/${projectId}`); + const projectRef = admin.database().ref(`/v2/projects/${context.params.projectId}`); const dataSnapshot = await projectRef.once('value'); if (dataSnapshot.exists()) { - const project = dataSnapshot.val() as { - projectType: number, - customOptions: unknown[], - }; - // Check if project type is validate and also has + const project = dataSnapshot.val(); + // Check if project type is 'validate' and also has // custom options (i.e. these are new type of projects) if (project.projectType === 2 && project.customOptions) { // We remove the results submitted from older version of app (< v2.2.6) - console.info(`Result submitted for ${projectId} was discarded: submitted from older version of app`); + logger('Result removed because it was submitted from an older version', undefined, console.error); return thisResultRef.remove(); } } @@ -88,16 +112,13 @@ exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{gro // if result ref does not contain all required attributes we don't updated counters // e.g. due to some error when uploading from client if (!Object.prototype.hasOwnProperty.call(result, 'results')) { - console.log('no results attribute for ' + snapshot.ref); - console.log('will not update counters'); + logger('Not updating counters because results attribute was not found.', { result: String(snapshot.ref) }, console.error); return null; } else if (!Object.prototype.hasOwnProperty.call(result, 'endTime')) { - console.log('no endTime attribute for ' + snapshot.ref); - console.log('will not update counters'); + logger('Not updating counters because endTime attribute was not found.', { result: String(snapshot.ref) }, console.error); return null; } else if (!Object.prototype.hasOwnProperty.call(result, 'startTime')) { - console.log('no startTime attribute for ' + snapshot.ref); - console.log('will not update counters'); + logger('Not updating counters because startTime attribute was not found.', { result: String(snapshot.ref) }, console.error); return null; } @@ -110,8 +131,7 @@ exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{gro const mappingSpeed = (endTime - startTime) / numberOfTasks; if (mappingSpeed < 0.125) { // this about 8-times faster than the average time needed per task - console.log('unlikely high mapping speed: ' + mappingSpeed); - console.log('will remove this result and not update counters'); + logger('Result removed because of unlikely high mapping speed', { mappingSpeed: mappingSpeed }, console.warn); return thisResultRef.remove(); } @@ -122,20 +142,18 @@ exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{gro Update overall taskContributionCount and project taskContributionCount in the user profile based on the number of results submitted and the existing count values. */ - const dataSnapshot = await groupUsersRef.child(userId).once('value'); + const dataSnapshot = await groupUsersRef.child(context.params.userId).once('value'); if (dataSnapshot.exists()) { - console.log('group contribution exists already. user: '+userId+' project: '+projectId+' group: '+groupId); + logger('Group contribution already exists.'); return null; } + // Update contributions + const latestNumberOfTasks = Object.keys(result['results']).length; - const totalTaskContributionCountRef = userRef.child('taskContributionCount'); - const totalGroupContributionCountRef = userRef.child('groupContributionCount'); - const userContributionRef = userRef.child(`contributions/${projectId}`); - const taskContributionCountRef = userRef.child(`contributions/${projectId}/taskContributionCount`); await Promise.all([ - userContributionRef.child(groupId).set(true), - groupUsersRef.child(userId).set(true), + userContributionRef.child(context.params.groupId).set(true), + groupUsersRef.child(context.params.userId).set(true), totalTaskContributionCountRef.transaction((currentCount) => { return currentCount + latestNumberOfTasks; }), @@ -147,27 +165,24 @@ exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{gro }), ]); - // Tag userGroups of the user in the result + const userGroupsOfTheUserSnapshot = await userRef.child('userGroups').once('value'); if (!userGroupsOfTheUserSnapshot.exists()) { return null; } - const userGroupsRef = db.ref('/v2/userGroups/'); const allUserGroupsSnapshot = await userGroupsRef.once('value'); if (!allUserGroupsSnapshot.exists()) { return null; } - const userGroupsOfTheUserKeyList = Object.keys(userGroupsOfTheUserSnapshot.val() as object); + const userGroupsOfTheUserKeyList = Object.keys(userGroupsOfTheUserSnapshot.val()); if (userGroupsOfTheUserKeyList.length <= 0) { return null; } - const allUserGroups = allUserGroupsSnapshot.val() as { - [key: string]: { archivedAt?: unknown } | undefined, - }; + const allUserGroups = allUserGroupsSnapshot.val(); const nonArchivedUserGroupKeys = userGroupsOfTheUserKeyList.filter((key) => { const currentUserGroup = allUserGroups[key]; @@ -198,193 +213,108 @@ exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{gro }); -/** - * Set group finishedCount and group requiredCount. - * Gets triggered when new userId key is written to v2/groupsUsers/{projectId}/{groupId}. - * finishedCount and requiredCount of a group are calculated based on the number of userIds - * that are present in v2/groupsUsers/{projectId}/{groupId}. - */ -exports.groupFinishedCountUpdater = functions.database.ref('/v2/groupsUsers/{projectId}/{groupId}/').onWrite(async (_, context) => { - // Set group finishedCount based on number of users that finished this group. - // Calculate group requiredCount based on number of userIds and verification number. - // Verification number can be defined on the project level or on the group level. - // If a verification number is defined for the group, - // this will surpass the project verification number. - // This will allow us to either map specific groups more often - // or less often than other groups in this project. - const db = admin.database(); - const { projectId, groupId } = context.params; - - const groupVerificationNumberRef = db.ref(`/v2/groups/${projectId}/${groupId}/verificationNumber`); - const groupVerificationNumberSnaphost = await groupVerificationNumberRef.once('value'); - - // check if a verification number is set for this group - if (groupVerificationNumberSnaphost.exists()) { - console.log('using group verification number'); - const verificationNumber = groupVerificationNumberSnaphost.val() as number; - return verificationNumber; - } +/* + Set group finishedCount and group requiredCount. + Gets triggered when new userId key is written to v2/groupsUsers/{projectId}/{groupId}. + FinishedCount and requiredCount of a group are calculated based on the number of userIds + that are present in v2/groupsUsers/{projectId}/{groupId}. +*/ +exports.groupFinishedCountUpdater = functions.database.ref('/v2/groupsUsers/{projectId}/{groupId}/').onWrite((_, context) => { + const groupUsersRef = admin.database().ref('/v2/groupsUsers/' + context.params.projectId + '/' + context.params.groupId); + const projectVerificationNumberRef = admin.database().ref('/v2/projects/' + context.params.projectId + '/verificationNumber'); + const groupVerificationNumberRef = admin.database().ref('/v2/groups/' + context.params.projectId + '/' + context.params.groupId + '/verificationNumber'); - // use project verification number if it is not set for the group - const projectVerificationNumberRef = db.ref(`/v2/projects/${projectId}/verificationNumber`); - const projectVerificationNumberSnapshot = await projectVerificationNumberRef.once('value'); - const projectVerificationNumber = projectVerificationNumberSnapshot.val() as number; - - // FIXME: We should be able to use snapshot.val() instead - const groupUsersRef = db.ref(`/v2/groupsUsers/${projectId}/${groupId}`); - const groupUsersSnapshot = await groupUsersRef.once('value'); - const groupUsersCount = groupUsersSnapshot.numChildren(); - - // FIXME: Not sure if we only set these if we are using verification number from project - const groupFinishedCountRef = db.ref(`/v2/groups/${projectId}/${groupId}/finishedCount`); - const groupRequiredCountRef = db.ref(`/v2/groups/${projectId}/${groupId}/requiredCount`); - return Promise.all([ - groupFinishedCountRef.set(groupUsersCount), - groupRequiredCountRef.set(projectVerificationNumber - groupUsersCount), - ]); -}); + // these references/values will be updated by this function + const groupFinishedCountRef = admin.database().ref('/v2/groups/' + context.params.projectId + '/' + context.params.groupId + '/finishedCount'); + const groupRequiredCountRef = admin.database().ref('/v2/groups/' + context.params.projectId + '/' + context.params.groupId + '/requiredCount'); + /* + Set group finished count based on number of users that finished this group. + Calculate required count based on number of userIds and verification number. + Verification number can be defined on the project level or on the group level. + If a verification number is defined for the group, + this will surpass the project verification number. + This will allow us to either map specific groups more often + or less often than other groups in this project. + */ + return groupVerificationNumberRef.once('value') + .then((dataSnapshot) => { + // check if a verification number is set for this group + if (dataSnapshot.exists()) { + console.log('using group verification number'); + const verificationNumber = dataSnapshot.val(); + return verificationNumber; + } + + // use project verification number if it is not set for the group + // eslint-disable-next-line promise/no-nesting + const verificationNumber = projectVerificationNumberRef.once('value') + .then((dataSnapshot2) => { + return dataSnapshot2.val(); + }); + return verificationNumber; + }) + .then((verificationNumber) => { + // eslint-disable-next-line promise/no-nesting + return groupUsersRef.once('value') + .then((dataSnapshot3) => { + return Promise.all([ + groupFinishedCountRef.set(dataSnapshot3.numChildren()), + groupRequiredCountRef.set(verificationNumber - dataSnapshot3.numChildren()), + ]); + }); + }); +}); -/** - * Count how many projects a users has worked on at v2/users/{userId}/projectContributionCount. - * This is based on the number of projectIds set in the `contribution` part of the user profile. - */ -exports.projectContributionCounter = functions.database.ref('/v2/users/{userId}/contributions/').onWrite(async (snapshot, context) => { - const db = admin.database(); - const { userId } = context.params; +/* + Count how many projects a users has worked on at v2/users/{userId}/projectContributionCount. + This is based on the number of projectIds set in the `contribution` part of the user profile. +*/ +exports.projectContributionCounter = functions.database.ref('/v2/users/{userId}/contributions/').onWrite((snapshot, context) => { // using after here to check the data after the write operation - const contributions = snapshot.after.val() as object; + const contributions = snapshot.after.val(); // these references/values will be updated by this function - const projectContributionCountRef = db.ref(`/v2/users/${userId}/projectContributionCount`); + const projectContributionCountRef = admin.database().ref('/v2/users/'+context.params.userId+'/projectContributionCount'); // set number of projects a user contributed to - const contributionsCount = Object.keys(contributions).length; - return projectContributionCountRef.set(contributionsCount); + return projectContributionCountRef.set(Object.keys( contributions ).length); }); -/** - * Generate update commands for PSQL db? - * Gets triggered when username is changed - */ -exports.usernameUpdate = functions.database.ref('/v2/users/{userId}/username/').onWrite(async (_, context) => { - const db = admin.database(); - const { userId } = context.params; - - const updatesUserRef = db.ref('/v2/updates/users/'); - return updatesUserRef.child(userId).set(true); +// Generate updates when user name is changed +exports.usernameUpdate = functions.database.ref('/v2/users/{userId}/username/').onWrite((_, context) => { + const userId = context.params.userId; + return admin.database().ref('/v2/updates/users/').child(userId).set(true); }); -/** - * Generates update commands for PSQL db - * Gets triggered when new user group is created, update or deleted - */ -exports.userGroupWrite = functions.database.ref('/v2/userGroups/{userGroupId}/').onWrite(async (_, context) => { - const db = admin.database(); - const { userGroupId } = context.params; +/* +* Generates update commands for PSQL db +* Gets triggered when new user group is created, update or deleted +*/ +exports.userGroupWrite = functions.database.ref( + '/v2/userGroups/{userGroupId}/' +).onWrite((_, context) => { + const userGroupId = context.params.userGroupId; - // FIXME: Do we need to check for undefined userGroupId here? if (!userGroupId) { return null; } - const updatesUserGroupRef = db.ref('/v2/updates/userGroups/'); - return updatesUserGroupRef.child(userGroupId).set(true); + return admin.database().ref('/v2/updates/userGroups/').child(userGroupId).set(true); }); -/** - * Generate update commands for PSQL db? - * Gets triggered when user joins or leaves a usergroup - */ -exports.userGroupMembershipWrite = functions.database.ref('/v2/userGroupMembershipLogs/{membershipId}').onWrite(async (_, context) => { - // FIXME: We should use a function to leave/join a group instead - const db = admin.database(); - const { membershipId } = context.params; +exports.userGroupMembershipWrite = functions.database.ref( + '/v2/userGroupMembershipLogs/{membershipId}' +).onWrite((_, context) => { + const membershipId = context.params.membershipId; - // FIXME: Do we need to check for undefined userGroupId here? if (!membershipId) { return null; } - const updatesUserGroupMembershipRef = db.ref('/v2/updates/userGroupMembershipLogs'); - return updatesUserGroupMembershipRef.child(membershipId).set(true); -}); - - -/* - -MIGRATION CODE - -*/ - -exports.addProjectTopicKey = functions.https.onRequest(async (_, res) => { - const db = admin.database(); - - try { - const projectsRef = db.ref('v2/projects'); - const projectsSnapshot = await projectsRef.once('value'); - const projects = projectsSnapshot.val() as { [key: string]: { name?: string } | undefined }; - - const isEmptyProject = Object.keys(projects).length === 0; - if (isEmptyProject) { - res.status(404).send('No projects found'); - } else { - const newProjectData: { - [key: string]: string - } = {}; - - Object.entries(projects).forEach(([projectId, projectData]) => { - if (projectData?.name) { - const projectTopicKey = formatProjectTopic(projectData.name); - newProjectData[`v2/projects/${projectId}/projectTopicKey`] = projectTopicKey; - } - }); - - await db.ref().update(newProjectData); - - const updatedProjectsCount = Object.keys(newProjectData).length; - res.status(200).send(`Updated ${updatedProjectsCount} projects.`); - } - } catch (error) { - console.log(error); - res.status(500).send('Some error occurred'); - } -}); - -exports.addUserNameLowercase = functions.https.onRequest(async (_, res) => { - const db = admin.database(); - - try { - const usersRef = db.ref('v2/users'); - const usersSnapshot = await usersRef.once('value'); - const users = usersSnapshot.val() as { [key: string]: { username?: string } | undefined }; - - const isEmptyUser = Object.keys(users).length === 0; - if (isEmptyUser) { - res.status(404).send('No users found'); - } else { - const newUserData: { - [key: string]: string - } = {}; - - Object.entries(users).forEach(([id, user]) => { - if (user?.username) { - const usernameKey = formatUserName(user.username); - newUserData[`v2/users/${id}/usernameKey`] = usernameKey; - } - }); - - await db.ref().update(newUserData); - - const updatedUserCount = Object.keys(newUserData).length; - res.status(200).send(`Updated ${updatedUserCount} users.`); - } - } catch (error) { - console.log(error); - res.status(500).send('Some error occurred'); - } + return admin.database().ref('/v2/updates/userGroupMembershipLogs').child(membershipId).set(true); }); @@ -443,3 +373,61 @@ exports.incProjectProgress = functions.database.ref('/v2/projects/{projectId}/re exports.decProjectProgress = functions.database.ref('/v2/projects/{projectId}/requiredResults/').onUpdate(() => { return null; }); + +exports.addProjectTopicKey = functions.https.onRequest(async (_, res) => { + try { + const projectRef = await admin.database().ref('v2/projects').once('value'); + const data = projectRef.val(); + + const isEmptyProject = Object.keys(data).length === 0; + if (isEmptyProject) { + res.status(404).send('No projects found'); + } else { + const newProjectData: {[key: string]: string} = {}; + + Object.keys(data).forEach((id) => { + const projectData = data[id]; + + if (projectData?.name) { + const newProjectTopicKey = formatProjectTopic(projectData.name); + newProjectData[`v2/projects/${id}/projectTopicKey`] = newProjectTopicKey; + } + }); + + await admin.database().ref().update(newProjectData); + const updatedProjectsCount = Object.keys(newProjectData).length; + res.status(200).send(`Updated ${updatedProjectsCount} projects.`); + } + } catch (error) { + console.log(error); + res.status(500).send('Some error occurred'); + } +}); + +exports.addUserNameLowercase = functions.https.onRequest(async (_, res) => { + try { + const userRef = await admin.database().ref('v2/users').once('value'); + const data = userRef.val(); + + const isEmptyUser = Object.keys(data).length === 0; + if (isEmptyUser) { + res.status(404).send('No user found'); + } else { + const newUserData: {[key: string]: string} = {}; + + Object.keys(data).forEach((id) => { + if (data[id]?.username) { + const newUsernameKey = formatUserName(data[id].username); + newUserData[`v2/users/${id}/usernameKey`] = newUsernameKey; + } + }); + + await admin.database().ref().update(newUserData); + const updatedUserCount = Object.keys(newUserData).length; + res.status(200).send(`Updated ${updatedUserCount} users.`); + } + } catch (error) { + console.log(error); + res.status(500).send('Some error occurred'); + } +}); diff --git a/functions/src/osm_auth.ts b/functions/src/osm_auth.ts index d187b4e..9953f2e 100644 --- a/functions/src/osm_auth.ts +++ b/functions/src/osm_auth.ts @@ -1,4 +1,4 @@ -// Firebase cloud functions to allow authentication with OpenStreet Map +// Firebase cloud functions to allow authentication with OpenStreetMap // // There are really 2 functions, which must be publicly accessible via // an https endpoint. They can be hosted on firebase under a domain like @@ -20,8 +20,10 @@ import axios from 'axios'; // will get a cryptic error about the server not being able to continue // TODO: adjust the prefix based on which deployment is done (prod/dev) const OAUTH_REDIRECT_URI = functions.config().osm?.redirect_uri; +const OAUTH_REDIRECT_URI_WEB = functions.config().osm?.redirect_uri_web; const APP_OSM_LOGIN_DEEPLINK = functions.config().osm?.app_login_link; +const APP_OSM_LOGIN_DEEPLINK_WEB = functions.config().osm?.app_login_link_web; // the scope is taken from https://wiki.openstreetmap.org/wiki/OAuth#OAuth_2.0 // at least one seems to be required for the auth workflow to complete. @@ -36,11 +38,11 @@ const OSM_API_URL = functions.config().osm?.api_url; * Configure the `osm.client_id` and `osm.client_secret` * Google Cloud environment variables for the values below to exist */ -function osmOAuth2Client() { +function osmOAuth2Client(client_id: any, client_secret: any) { const credentials = { client: { - id: functions.config().osm?.client_id, - secret: functions.config().osm?.client_secret, + id: client_id, + secret: client_secret, }, auth: { tokenHost: OSM_API_URL, @@ -58,8 +60,8 @@ function osmOAuth2Client() { * NOT a webview inside MapSwipe, as this would break the promise of * OAuth that we do not touch their OSM credentials */ -export const redirect = (req: any, res: any) => { - const oauth2 = osmOAuth2Client(); +function redirect2OsmOauth(req: any, res: any, redirect_uri: string, client_id: string, client_secret: string) { + const oauth2 = osmOAuth2Client(client_id, client_secret); cookieParser()(req, res, () => { const state = @@ -75,17 +77,31 @@ export const redirect = (req: any, res: any) => { httpOnly: true, }); const redirectUri = oauth2.authorizationCode.authorizeURL({ - redirect_uri: OAUTH_REDIRECT_URI, + redirect_uri: redirect_uri, scope: OAUTH_SCOPES, state: state, }); functions.logger.log('Redirecting to:', redirectUri); res.redirect(redirectUri); }); +} + +export const redirect = (req: any, res: any) => { + const redirect_uri = OAUTH_REDIRECT_URI; + const client_id = functions.config().osm?.client_id; + const client_secret = functions.config().osm?.client_secret; + redirect2OsmOauth(req, res, redirect_uri, client_id, client_secret); +}; + +export const redirectweb = (req: any, res: any) => { + const redirect_uri = OAUTH_REDIRECT_URI_WEB; + const client_id = functions.config().osm?.client_id_web; + const client_secret = functions.config().osm?.client_secret_web; + redirect2OsmOauth(req, res, redirect_uri, client_id, client_secret); }; /** - * The OSM OAuth endpoing does not give us any info about the user, + * The OSM OAuth endpoint does not give us any info about the user, * so we need to get the user profile from this endpoint */ async function getOSMProfile(accessToken: string) { @@ -107,8 +123,8 @@ async function getOSMProfile(accessToken: string) { * The Firebase custom auth token, display name, photo URL and OSM access * token are sent back to the app via a deeplink redirect. */ -export const token = async (req: any, res: any, admin: any) => { - const oauth2 = osmOAuth2Client(); +function fbToken(req: any, res: any, admin: any, redirect_uri: string, osm_login_link: string, client_id: string, client_web: string) { + const oauth2 = osmOAuth2Client(client_id, client_web); try { return cookieParser()(req, res, async () => { @@ -139,7 +155,7 @@ export const token = async (req: any, res: any, admin: any) => { // this doesn't work results = await oauth2.authorizationCode.getToken({ code: req.query.code, - redirect_uri: OAUTH_REDIRECT_URI, + redirect_uri: redirect_uri, scope: OAUTH_SCOPES, state: req.query.state, }); @@ -177,7 +193,7 @@ export const token = async (req: any, res: any, admin: any) => { ); // build a deep link so we can send the token back to the app // from the browser - const signinUrl = `${APP_OSM_LOGIN_DEEPLINK}?token=${firebaseToken}`; + const signinUrl = `${osm_login_link}?token=${firebaseToken}`; functions.logger.log('redirecting user to', signinUrl); res.redirect(signinUrl); }); @@ -187,6 +203,22 @@ export const token = async (req: any, res: any, admin: any) => { // back into the app to allow the user to take action return res.json({ error: error.toString() }); } +} + +export const token = async (req: any, res: any, admin: any) => { + const redirect_uri = OAUTH_REDIRECT_URI; + const osm_login_link = APP_OSM_LOGIN_DEEPLINK; + const client_id = functions.config().osm?.client_id; + const client_secret = functions.config().osm?.client_secret; + fbToken(req, res, admin, redirect_uri, osm_login_link, client_id, client_secret); +}; + +export const tokenweb = async (req: any, res: any, admin: any) => { + const redirect_uri = OAUTH_REDIRECT_URI_WEB; + const osm_login_link = APP_OSM_LOGIN_DEEPLINK_WEB; + const client_id = functions.config().osm?.client_id_web; + const client_secret = functions.config().osm?.client_secret_web; + fbToken(req, res, admin, redirect_uri, osm_login_link, client_id, client_secret); }; /** @@ -204,23 +236,18 @@ async function createFirebaseAccount(admin: any, osmID: any, displayName: any, a // with a variable length. const uid = `osm:${osmID}`; + const profileRef = admin.database().ref(`v2/users/${uid}`); + + // check if profile exists on Firebase Realtime Database + const snapshot = await profileRef.once('value'); + const profileExists = snapshot.exists(); + // Save the access token to the Firebase Realtime Database. const databaseTask = admin .database() .ref(`v2/OSMAccessToken/${uid}`) .set(accessToken); - const profileTask = admin - .database() - .ref(`v2/users/${uid}/`) - .set({ - created: new Date().toISOString(), - groupContributionCount: 0, - projectContributionCount: 0, - taskContributionCount: 0, - displayName, - }); - // Create or update the firebase user account. // This does not login the user on the app, it just ensures that a firebase // user account (linked to the OSM account) exists. @@ -240,8 +267,27 @@ async function createFirebaseAccount(admin: any, osmID: any, displayName: any, a throw error; }); + // If profile exists, only update displayName -- else create new user profile + const tasks = [userCreationTask, databaseTask]; + if (profileExists) { + functions.logger.log('Sign in to existing OSM profile'); + const profileUpdateTask = profileRef.update({ displayName: displayName }); + tasks.push(profileUpdateTask); + } else { + functions.logger.log('Sign up new OSM profile'); + const profileCreationTask = profileRef + .set({ + created: new Date().toISOString(), + groupContributionCount: 0, + projectContributionCount: 0, + taskContributionCount: 0, + displayName, + }); + tasks.push(profileCreationTask); + } + // Wait for all async task to complete then generate and return a custom auth token. - await Promise.all([userCreationTask, databaseTask, profileTask]); + await Promise.all(tasks); // Create a Firebase custom auth token. functions.logger.log('In createFirebaseAccount: createCustomToken'); let authToken;