UNPKG

@earnaha/auth0-action-helper

Version:
384 lines (350 loc) 10.6 kB
/* eslint-disable camelcase */ /* eslint-disable no-console */ const { customAlphabet } = require('nanoid'); // TIP: nanoid 4 cannot use require statement! const BaseHelper = require('./base.js'); class PostLoginHelper extends BaseHelper { emailToNames(email) { const [validDisplayName] = email.split('@'); // TIP: symbols # & + are not allowed for using in url (public sharing link) // the new username from the first part of an email converts all symbols to dot. const noSymbolLowerCaseName = validDisplayName .replace(/[^a-zA-Z0-9]/g, ' ') .trim() .replace(/\s/g, '.') .replace(/(\.)+/g, '.') .toLowerCase(); const randomNumber = customAlphabet('1234567890', 13); const validNickName = `${noSymbolLowerCaseName}.${( new Date().getTime() + Number(randomNumber()) ).toString(36)}`; return { nickname: validNickName, name: validDisplayName, }; } async login(api, payload) { const accessKey = await super.getServerAccessKey(api); const res = await super.axiosPostWithBreadcrumb( `${super.DOMAIN}/auth/v3/login`, payload, { headers: { ServerAccessKey: accessKey, }, }, ); return res; } async claimAccount(api, payload) { const accessKey = await super.getServerAccessKey(api); const res = super.axiosPostWithBreadcrumb( `${super.DOMAIN}/auth/v3/member/guest/claim`, payload, { headers: { ServerAccessKey: accessKey, }, }, ); return res; } async userCount(api) { const accessKey = await super.getServerAccessKey(api); const res = await super.axiosGetWithBreadcrumb( `${super.DOMAIN}/auth/v3/member/count/raw`, { headers: { ServerAccessKey: accessKey, }, }, ); return res; } async checkIfOverwriteEmail(api, payload) { const state = super.safeEncodeParam( { user_id: payload.user_id, email: payload.email.toLowerCase(), identities: payload.identities, }, 'state', ); const accessKey = await super.getServerAccessKey(api); const res = await super.axiosGetWithBreadcrumb( `${super.DOMAIN}/auth/v3/login/existing-check?${state}`, { headers: { ServerAccessKey: accessKey, }, }, ); return res; } async updateReferrer(referrer, token) { if (referrer?.auth0Id && !referrer.auth0Id.startsWith('guest')) { const referrerUser = await super.getUserById( referrer.auth0Id, token, ); super.LOG.debug( { referrer: referrerUser }, `PostLogin.updateReferrer | referrer's profile in auth0`, ); if (referrerUser?.data && referrer?.userMetadata) { const referrerRes = await super.updateUserMetadata( referrerUser?.data, super.parseJson(referrer.userMetadata, {}), token, ); super.LOG.debug( { referrer: referrerRes?.data || referrerRes }, `PostLogin.updateReferrer | referrer's userMetadata updated result`, ); } } } async updateByMemberPayload(auth0User, memberPayload, token) { if (!auth0User || !memberPayload || !token) { return null; } super.LOGGER.setMeta({ auth0Id: auth0User.user_id, email: auth0User.email, }); const updateValues = { user_metadata: { ...super.parseJson(memberPayload.userMetadata, {}), memberId: memberPayload.id, }, }; const isNamePwdAuth = super.isNamePwdAuthUser(auth0User); if ( isNamePwdAuth === true && auth0User.nickname !== memberPayload.nickname ) { updateValues.nickname = memberPayload.nickname; } if (isNamePwdAuth === true && auth0User.name !== memberPayload.name) { updateValues.name = memberPayload.name; } const updateRes = await super.updateUser( auth0User, updateValues, token, ); super.LOG.debug( { updateRes: updateRes?.data }, `PostLogin.updateByMemberPayload | update results`, ); return updateRes?.data; } memberPayload(event, majorUser) { if (!event || !majorUser || !majorUser.user_id) { return null; } const { request, transaction, authorization } = event; super.LOGGER.setMeta({ auth0Id: majorUser.user_id, email: majorUser.email, }); const resData = { auth0Id: majorUser.user_id, email: majorUser.email, nickname: majorUser.nickname, name: majorUser.name, picture: majorUser.picture, loginsCount: (majorUser.logins_count || 0) + 1, lastIp: request?.ip, emailVerified: majorUser.email_verified, scope: (transaction?.requested_scopes || []).join(' '), appMetadata: majorUser.app_metadata, userMetadata: majorUser.user_metadata || {}, roles: authorization?.roles, identities: majorUser.identities, language: transaction?.locale, geoip: request?.geoip, service: super.SERVICE, phoneNumber: majorUser.phone_number, phoneNumberVerified: majorUser.phone_verified, }; // change to valid names, basically for brand new users const isValidNickname = super.isValidUserName(majorUser.nickname); if (isValidNickname !== true) { const namesFromEmail = this.emailToNames(majorUser.email); resData.nickname = namesFromEmail.nickname; resData.name = namesFromEmail.name; } super.LOG.debug( { resData }, `PostLogin.memberPayload | return payload`, ); return resData; } /** * https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow * Handler that will be invoked when this action is resuming after an external redirect. If your * onExecutePostLogin function does not perform a redirect, this function can be safely ignored. * @param {Event} event - Details about the user and the context in which they are logging in. * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login. */ async exec(event, api) { super.LOGGER.setMeta({ auth0Id: event?.user?.user_id, email: event?.user?.email, }); try { if (!event || !event?.user || !event?.user?.user_id) { return api.access.deny('Invalid auth event'); } const userToken = await super.getUserToken(); const majorUser = await super.getMajorUser(event, userToken, true); if (!majorUser || !majorUser?.user_id) { return api.access.deny('Primary user not found'); } const isThirdPartyAuth = super.isThirdPartyAuthUser(majorUser); if (isThirdPartyAuth === true) { const checkRes = await this.checkIfOverwriteEmail( api, majorUser, ); // TIP: this API may update Members.email // we should ensure it executes successfully if (!(checkRes && checkRes.data)) { return api.access.deny('Email validates failed'); } } // for calling api-auth apis const payload = this.memberPayload(event, majorUser); super.LOG.debug({ payload }, `PostLogin.exec | member payload`); const queryParams = super.organizeEventReqQueryParam(event); super.LOG.debug( { queryParams }, `PostLogin.exec | query parameters`, ); if (queryParams) { payload.queryParams = queryParams; payload.userMetadata = { ...payload.userMetadata, ...queryParams, }; } let authRes = null; // claim existing guest account if (queryParams?.claimUsername) { const { data: claimRes } = await this.claimAccount(api, { claimUsername: queryParams.claimUsername, memberPayload: payload, }); super.LOG.debug( { claimRes }, `PostLogin.exec | claim account results`, ); if (claimRes && claimRes.data) { authRes = claimRes.data; } } // general login or signup or claimAccount failed if (authRes === null) { const { data: loginRes } = await this.login(api, payload); super.LOG.debug( { loginRes }, `PostLogin.exec | login or signup results`, ); if (loginRes && loginRes.data) { authRes = loginRes.data; } } super.LOG.debug({ authRes }, `PostLogin.exec | api-auth response`); if (authRes === null) { return api.access.deny('Login failed'); } const { memberLogin, member, referrer, removeAuth0UserId, isSignup, } = authRes; /* const updateRes = await this.updateByMemberPayload( majorUser, member, userToken, ); super.LOG.debug( { updateRes }, `PostLogin.exec | update primary user results`, ); */ // Not gonna wait this.updateByMemberPayload(majorUser, member, userToken); /* const updatedRef = await this.updateReferrer(referrer, userToken); super.LOG.debug( { updatedRef }, `PostLogin.exec | update referrer results`, ); */ // Not gonna wait this.updateReferrer(referrer, userToken); if (removeAuth0UserId) { const delRes = await super.deleteUserById( removeAuth0UserId, userToken, ); super.LOG.debug( { delRes }, `PostLogin.exec | remove guest results`, ); } // -------------- handle session data (return to frontend) -------------------- if (event.authorization) { api.idToken.setCustomClaim(`roles`, event.authorization.roles); api.accessToken.setCustomClaim( `roles`, event.authorization.roles, ); } api.idToken.setCustomClaim('accessToken', memberLogin.accessToken); api.idToken.setCustomClaim( `provider`, payload.identities[0].provider, ); api.idToken.setCustomClaim(`memberId`, member.id); // api.idToken.setCustomClaim( `userMetadata`, super.parseJson(member.userMetadata, {}) ); api.idToken.setCustomClaim(`nickname`, member.nickname); api.idToken.setCustomClaim(`name`, member.name); api.accessToken.setCustomClaim(`memberId`, member.id); api.user.setUserMetadata('memberId', member.id); api.authentication.setPrimaryUser(member.auth0Id); api.idToken.setCustomClaim(`isSignup`, isSignup); const resData = { member: { ...member, // TIP: the returning data will be saved in cookie, // therefore we have to prevent too large data size. appMetadata: undefined, // super.parseJson(member.appMetadata), userMetadata: undefined, // super.parseJson(member.userMetadata), identities: undefined, // super.parseJson(member.identities), roles: super.parseJson(member.roles), geoip: super.parseJson(member.geoip), email_verified: member.auth0Id.includes('sms|') ? true : member.emailVerified, ...memberLogin, }, isSignup, }; super.LOG.info({ resData }, `PostLogin.exec | done`); return resData; } catch (e) { super.LOG.error( { error: e?.response?.data || e?.message || `${e}` }, `PostLogin.exec | caught exception`, ); super.SENTRY.captureException(e); return api.access.deny(e.message); } } } module.exports = PostLoginHelper;