import { addMinutes } from 'date-fns'; import { client } from './trpc.server'; import type { User } from '../auth.server'; import type { Result } from '../types'; import type { RouterInputs, RouterOutputs } from './trpc.server'; import type { ClientApp, Rename } from '../types'; import { getBaseUrl } from '../utils/utils.server'; import type { TZippoRouteTag } from 'zippo-interface'; export const NO_TEAM_MARKER = '__not_init' as const; export type ZippoApp = NonNullable; export type OnChainTagParams = Rename; const zippoAppToClientApp = (zippoApp: ZippoApp): ClientApp => { return { id: zippoApp.id, name: zippoApp.name, description: zippoApp.description, apiKeys: zippoApp.apiKeys, teamId: zippoApp.integratorTeamId, productAccess: zippoApp.integratorAccess.map((access) => access.routeTag as TZippoRouteTag), onChainTag: zippoApp.integratorExternalApp ? { name: zippoApp.integratorExternalApp.name } : undefined, }; }; export async function doesSessionExist({ userId, sessionToken, }: { userId: string; sessionToken: string; }): Promise> { try { const session = await client.session.getSession.query(sessionToken); if (!session) { return { result: 'ERROR', error: new Error('Invalid session'), }; } if (session.userId !== userId) { return { result: 'ERROR', error: new Error('Invalid session'), }; } return { result: 'SUCCESS', data: true, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function createUserWithEmailAndPassword({ firstName, lastName, email, password, }: { firstName: string; lastName: string; email: string; password: string; }): Promise> { // currently stubbed, but this is where we would check with the backend to create a new user try { const result = await client.user.create.mutate({ firstName, lastName, email, password, }); if (!result) { return { result: 'ERROR', error: new Error('Failed to create user'), }; } return { result: 'SUCCESS', data: result.id, }; } catch (e) { console.warn(e); if (e instanceof Error && e.message.includes('Unique constraint failed on the fields: (`email`)')) { return { result: 'ERROR', error: new Error('User already exists'), }; } return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function sendVerificationEmail({ email, userId, }: { email: string; userId: string; }): Promise> { console.log('Verification email sent'); try { await client.user.sendEmailVerifyEmail.mutate({ newEmail: email, userId, verifyUrl: `${getBaseUrl()}/create-account/verify-email?email=${encodeURIComponent(email)}`, }); return { result: 'SUCCESS', data: undefined }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function verifyEmailVerificationToken({ email, token, }: { email: string; token: string; }): Promise { try { const result = await client.user.verifyEmail.mutate({ email, verificationToken: token }); if (!result) { return false; } return true; } catch (e) { console.warn(e); return false; } } export async function createTeam({ userId, teamName, productType, }: { userId: string; teamName: string; productType: string; }): Promise> { // Currently, ZIPPO auto creates teams on user creation, so instead of actually creating a team, // we update the user's team to the team name and team type provided try { const zippoUser = await client.user.getById.query(userId); if (!zippoUser) { return { result: 'ERROR', error: new Error('User not found'), }; } const result = await client.team.update.mutate({ id: zippoUser.integratorTeamId, name: teamName, productType }); if (!result) { return { result: 'ERROR', error: new Error('Failed to create team'), }; } return { result: 'SUCCESS', data: result.name, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function sendResetPasswordEmail({ userId, email, }: { userId: string; email: string; }): Promise> { try { await client.user.sendPasswordResetEmail.mutate({ userId, verifyUrl: `${getBaseUrl()}/reset-password/set-password?email=${encodeURIComponent(email)}`, }); return { result: 'SUCCESS', data: true, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function resetPassword({ password, verificationToken, }: { password: string; verificationToken: string; }): Promise> { try { const result = await client.user.resetPassword.mutate({ password, verificationToken }); if (!result) { return { result: 'ERROR', error: new Error('Invalid token'), }; } return { result: 'SUCCESS', data: true, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function getUserByEmail({ email }: { email: string }): Promise> { try { const user = await client.user.getByEmail.query(email); if (!user) { return { result: 'ERROR', error: new Error('User not found'), }; } return { result: 'SUCCESS', data: { id: user.id, email: user.email!, // we only allow signup via email teamId: user.integratorTeamId, sessionToken: user.integratorTeamId, expiresAt: addMinutes(new Date(), 15).toISOString(), }, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function loginWithEmailAndPassword({ email, password, }: { email: string; password: string; }): Promise> { try { const session = await client.session.login.mutate({ email, password }); if (!session) { return { result: 'ERROR', error: new Error('Invalid email or password'), }; } const user = await client.user.getById.query(session.userId); // we should have a user and a session at this point if (!user) { return { result: 'ERROR', error: new Error('Unknown error'), }; } const team = await client.team.getById.query(user.integratorTeamId); return { result: 'SUCCESS', data: { id: user.id, email: user.email!, // we only allow signup via email teamName: team?.name, sessionToken: session.sessionToken, expiresAt: addMinutes(new Date(), 15).toISOString(), teamId: user.integratorTeamId, }, }; } catch (error) { console.warn(error); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function invalidateZippoSession({ sessionToken }: { sessionToken: string }): Promise { try { await client.session.logout.mutate(sessionToken); } catch (e) { console.warn(e); } } export async function getTeam({ userId, }: { userId: string; // i hate this but i want to tie it to the return type of the query }): Promise>> { try { const user = await client.user.getById.query(userId); if (!user) { return { result: 'ERROR', error: new Error('User not found'), }; } const team = await client.team.getById.query(user.integratorTeamId); if (!team) { return { result: 'ERROR', error: new Error('Team not found'), }; } return { result: 'SUCCESS', data: team, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function getTeamById({ teamId, }: { teamId: string; }): Promise>>> { try { const team = await client.team.getById.query(teamId); if (!team) { return { result: 'ERROR', error: new Error('Team not found'), }; } return { result: 'SUCCESS', data: team, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function createApp({ appName, teamId, onChainTag, onChainTagId, ...rest }: Rename< RouterInputs['app']['create'], { integratorTeamId: 'teamId'; name: 'appName'; integratorExternalApp: 'onChainTag'; integratorExternalAppId: 'onChainTagId'; } >): Promise> { try { const app = await client.app.create.mutate({ integratorTeamId: teamId, name: appName, integratorExternalApp: onChainTag, integratorExternalAppId: onChainTagId, ...rest, }); if (!app) { return { result: 'ERROR', error: new Error('Failed to create app'), }; } return { result: 'SUCCESS', data: zippoAppToClientApp(app), }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function updateApp({ appId, onChainTagId, ...rest }: Rename): Promise< Result > { try { const app = await client.app.update.mutate({ id: appId, integratorExternalAppId: onChainTagId, ...rest }); if (!app) { return { result: 'ERROR', error: new Error('Failed to update app'), }; } return { result: 'SUCCESS', data: zippoAppToClientApp(app), }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function createOnChainTag({ teamId, ...rest }: OnChainTagParams): Promise>>> { try { const externalApp = await client.externalApp.create.mutate({ integratorTeamId: teamId, ...rest }); if (!externalApp) { return { result: 'ERROR', error: new Error('Failed to create external app'), }; } return { result: 'SUCCESS', data: externalApp, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error while creating onchain tag'), }; } } export async function updateProvisionAccess({ appId, rateLimits, routeTags, }: Rename): Promise> { try { const appRes = await client.app.getById.query(appId); if (!appRes) { return { result: 'ERROR', error: new Error('Failed to update app'), }; } const routeTagsWithRateLimits = routeTags.map((routeTag, idx) => { return { routeTag, rateLimit: rateLimits[idx], }; }); const currentProducts = appRes.integratorAccess.map((access) => access.routeTag); const productsToDelete = currentProducts.filter((product) => !routeTags.includes(product as TZippoRouteTag)); const productsToAdd = routeTagsWithRateLimits.filter((product) => !currentProducts.includes(product.routeTag)); try { await client.app.provisionAccess.mutate({ id: appId, rateLimits: productsToAdd.map((product) => product.rateLimit), routeTags: productsToAdd.map((product) => product.routeTag) as TZippoRouteTag[], }); } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Failed to add products'), }; } try { await client.app.deprovisionAccess.mutate({ id: appId, routeTags: productsToDelete as TZippoRouteTag[], }); } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Failed to remove products'), }; } const updatedApp = await client.app.getById.query(appId); if (!updatedApp) { return { result: 'ERROR', error: new Error('Failed to retrieve updated app'), }; } return { result: 'SUCCESS', data: zippoAppToClientApp(updatedApp), }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function addProvisionAccess({ appId, rateLimits, routeTags, }: Rename): Promise> { try { const res = await client.app.provisionAccess.mutate({ id: appId, rateLimits, routeTags }); if (!res) { return { result: 'ERROR', error: new Error('Failed to provision access'), }; } return { result: 'SUCCESS', data: zippoAppToClientApp(res), }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function generateAPIKey({ appId, teamId, description, }: Rename): Promise< Result > { try { const res = await client.app.key.create.mutate({ integratorAppId: appId, integratorTeamId: teamId, description, }); if (!res || !res.apiKeys || !res.apiKeys.length) { return { result: 'ERROR', error: new Error('Failed to create API key'), }; } const newestKey = res.apiKeys[res.apiKeys.length - 1]; return { result: 'SUCCESS', data: newestKey.apiKey, }; } catch (e) { console.warn(e); return { result: 'ERROR', error: new Error('Unknown error'), }; } } export async function appsList(integratorTeamId: RouterInputs['app']['list']): Promise> { try { const apps = await client.app.list.query(integratorTeamId); if (!apps || !apps.length) { return { result: 'SUCCESS', data: [], }; } return { result: 'SUCCESS', data: apps.map(zippoAppToClientApp), }; } catch (error) { console.warn(error); return { result: 'ERROR', error: new Error('Failed to fetch apps'), }; } } export async function getAppById(id: RouterInputs['app']['getById']): Promise> { try { const app = await client.app.getById.query(id); if (!app) { return { result: 'ERROR', error: new Error('App not found'), }; } return { result: 'SUCCESS', data: zippoAppToClientApp(app), }; } catch (error) { console.warn(error); return { result: 'ERROR', error: new Error('Failed to fetch app'), }; } } export async function deleteAppKey(id: RouterInputs['app']['key']['delete']): Promise> { try { const app = await client.app.key.delete.mutate(id); if (!app) { return { result: 'ERROR', error: new Error('Key not found'), }; } return { result: 'SUCCESS', data: zippoAppToClientApp(app), }; } catch (error) { console.warn(error); return { result: 'ERROR', error: new Error('Failed to delete key'), }; } } export async function createAppKey({ appId, teamId, description, }: Rename): Promise< Result > { try { const app = await client.app.key.create.mutate({ integratorAppId: appId, integratorTeamId: teamId, description, }); if (!app) { return { result: 'ERROR', error: new Error('Failed to create key'), }; } return { result: 'SUCCESS', data: zippoAppToClientApp(app), }; } catch (error) { console.warn(error); return { result: 'ERROR', error: new Error('Failed to create key'), }; } }