UNPKG

mosquito-transport

Version:

Quickly spawn server infrastructure along robust authentication, database, storage, and cross-platform compatibility

266 lines (231 loc) 9.28 kB
import { Validator } from "guard-object"; import { UserCountReadyListener } from "../../helpers/listeners"; import { getRandomString } from "../../helpers/utils" import { ADMIN_DB_NAME, ADMIN_DB_URL, AUTH_PROVIDER_ID, EnginePath, ERRORS } from "../../helpers/values"; import { Scoped } from "../../helpers/variables"; import { queryDocument, readDocument, writeDocument } from "../database"; import { destroyToken, signJWT, signRefreshToken, validateRefreshToken, verifyJWT } from "./tokenizer"; import { simplifyError } from 'simplify-error'; export const signupCustom = async ( email = '', password = '', signupMethod = AUTH_PROVIDER_ID.PASSWORD, profile = {}, projectName, customExtras = {} ) => { email = email.trim().toLowerCase(); const processID = `${projectName}${email}`; try { if (Scoped.pendingSignups[processID]) throw ERRORS.CONCURRENT_SIGNUP; Scoped.pendingSignups[processID] = true; const { enableSequentialUid, uidLength, mergeAuthAccount } = Scoped.InstancesData[projectName]; if (signupMethod === AUTH_PROVIDER_ID.PASSWORD) { if (!password || typeof password !== 'string') throw ERRORS.PASSWORD_REQUIRED; if (!Validator.EMAIL(email)) throw ERRORS.INVALID_EMAIL; const prevData = await queryDocument({ path: EnginePath.userAcct, find: { email } }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL); if (prevData.length) { if (prevData.find(v => v.password)) throw ERRORS.ACCOUNT_ALREADY_EXIST; if (mergeAuthAccount) { await writeDocument({ find: { _id: prevData[0]._id }, value: { $set: { password } }, path: EnginePath.userAcct, scope: 'updateOne' }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL); return { ...(await signinCustom(email, password, undefined, projectName)), isNewUser: false } } } } const { sub, metadata, profile: profilex, d_uid, passwordVerified } = customExtras; const newUid = (d_uid && typeof d_uid === 'string') ? d_uid : enableSequentialUid ? await getUserSequentialCount(projectName) : getRandomString(uidLength || 30); const tokenID = getRandomString(30); const refreshTokenID = getRandomString(30); const tokenData = { email, claims: {}, metadata: { ...metadata }, signupMethod, joinedOn: Date.now(), passwordVerified: !!passwordVerified, profile: { ...profile, ...profilex }, disabled: false }; const [token, refreshToken, acctRes] = await Promise.all([ signJWT( bakeToken({ ...tokenData, entityOf: refreshTokenID, uid: newUid, tokenID, lastLoginAt: Date.now(), currentAuthMethod: signupMethod }), projectName ), signRefreshToken({ uid: newUid, tokenID: refreshTokenID, isRefreshToken: true }, projectName), writeDocument({ path: EnginePath.userAcct, value: { ...tokenData, ...password ? { password } : {}, ...sub ? { [signupMethod]: sub } : {}, _id: newUid } }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL) ]); if (!acctRes?.acknowledged) { await Promise.all([ writeDocument({ path: EnginePath.tokenStore, find: { _id: tokenID }, scope: 'deleteOne' }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL), writeDocument({ path: EnginePath.refreshTokenStore, find: { _id: refreshTokenID }, scope: 'deleteOne' }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL) ]); throw ERRORS.UID_ALREADY_EXISTS(newUid); } return { token, refreshToken, isNewUser: true }; } catch (e) { throw e; } finally { delete Scoped.pendingSignups[processID]; } }; export const signinCustom = async (email = '', password = '', signinMethod = AUTH_PROVIDER_ID.PASSWORD, projectName, defaultRecord) => { email = email.trim().toLowerCase(); let userData = defaultRecord; if (signinMethod === AUTH_PROVIDER_ID.PASSWORD) { if (!password || typeof password !== 'string') throw ERRORS.PASSWORD_REQUIRED; if (!Validator.EMAIL(email)) ERRORS.INVALID_EMAIL; userData = await queryDocument({ path: EnginePath.userAcct, find: { email } }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL); if (userData.length) { const passworded = userData.find(v => v.password === password) || userData.find(v => v.password); if (passworded) { if (passworded.password === password) { userData = passworded; } else throw ERRORS.INCORRECT_PASSWORD; } else throw ERRORS.ACCOUNT_NO_PASSWORD; } else throw ERRORS.USER_NOT_FOUND; } const { metadata, signupMethod, joinedOn, passwordVerified, _id, profile, claims, disabled } = userData; const tokenID = getRandomString(30); const refreshTokenID = getRandomString(30); const tokenData = { email, metadata, signupMethod, currentAuthMethod: signinMethod, joinedOn, uid: _id, claims, passwordVerified, profile, disabled: !!disabled, tokenID, lastLoginAt: Date.now(), entityOf: refreshTokenID }; if (disabled) throw ERRORS.ACCOUNT_DISABLED; const [token, refreshToken] = await Promise.all([ signJWT(bakeToken({ ...tokenData }), projectName), signRefreshToken({ uid: _id, tokenID: refreshTokenID, isRefreshToken: true }, projectName) ]); return { token, refreshToken }; }; export const refreshToken = async ({ token, refToken }, projectName) => { const [{ uid, currentAuthMethod, lastLoginAt, entityOf }, refAuth] = await Promise.all([ verifyJWT(token, projectName), validateRefreshToken(refToken, projectName) ]); if (uid !== refAuth.uid) throw ERRORS.TOKEN_MISMATCH; if (entityOf !== refAuth.tokenID) throw ERRORS.ENTITY_MISMATCH; const userData = await readDocument({ path: EnginePath.userAcct, find: { _id: uid } }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL); if (!userData) throw ERRORS.TOKEN_USER_NOT_FOUND; const { metadata, signupMethod, joinedOn, _id, claims, passwordVerified, profile, disabled, email } = userData; const newTokenID = getRandomString(30); const tokenData = { email, metadata, signupMethod, currentAuthMethod, joinedOn, uid: _id, claims, passwordVerified, profile, disabled, lastLoginAt, tokenID: newTokenID, entityOf: refAuth.tokenID }; // if (disabled) throw ERRORS.TOKEN_ACCOUNT_DISABLED; const tokenx = await signJWT(bakeToken({ ...tokenData }), projectName); return { token: tokenx }; }; function bakeToken(tokenData) { tokenData.authVerified = tokenData.currentAuthMethod !== AUTH_PROVIDER_ID.PASSWORD || tokenData.passwordVerified; return tokenData; } export const invalidateToken = async (token, projectName, isRefreshToken) => { let data; try { data = await verifyJWT(token, projectName, isRefreshToken); } catch (e) { throw simplifyError('invalid_auth_token', `${e}`); } return await destroyToken(data.tokenID, projectName, isRefreshToken); }; export const cleanUserToken = (uid, projectName) => { return Promise.all([ queryDocument({ path: EnginePath.tokenStore, find: { uid } }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL).then(r => Promise.all(r.map(({ _id }) => destroyToken(_id, projectName) )) ), queryDocument({ path: EnginePath.refreshTokenStore, find: { uid } }, projectName, ADMIN_DB_NAME, ADMIN_DB_URL).then(r => Promise.all(r.map(({ _id }) => destroyToken(_id, projectName, true) )) ) ]); }; const getUserSequentialCount = (projectName) => new Promise(resolve => { if (isNaN(Scoped.SequentialUid[projectName])) { const l = UserCountReadyListener.listenTo(projectName, () => { if (!isNaN(Scoped.SequentialUid[projectName])) { resolve(++Scoped.SequentialUid[projectName]); l(); } }, true); } else resolve(++Scoped.SequentialUid[projectName]); });