import { ForbiddenError } from '@/modules/shared/errors' import { isNullOrUndefined, isScope, Roles } from '@speckle/shared' import { getAppFactory, getAllPublicAppsFactory, getAllAppsCreatedByUserFactory, getAllAppsAuthorizedByUserFactory, createAppFactory, updateAppFactory, deleteAppFactory, revokeExistingAppCredentialsForUserFactory } from '@/modules/auth/repositories/apps' import { db } from '@/db/knex' import { Resolvers } from '@/modules/core/graph/generated/graphql' import { withOperationLogging } from '@/observability/domain/businessLogging' const getApp = getAppFactory({ db }) const getAllPublicApps = getAllPublicAppsFactory({ db }) const getAllAppsCreatedByUser = getAllAppsCreatedByUserFactory({ db }) const getAllAppsAuthorizedByUser = getAllAppsAuthorizedByUserFactory({ db }) const createApp = createAppFactory({ db }) const updateApp = updateAppFactory({ db }) const deleteApp = deleteAppFactory({ db }) const revokeExistingAppCredentialsForUser = revokeExistingAppCredentialsForUserFactory({ db }) export default { Query: { async app(_parent, args) { const app = await getApp({ id: args.id }) return app }, async apps() { return await getAllPublicApps() } }, ServerApp: { secret(parent, _args, context) { if ( context.auth && parent.author && parent.author.id && parent.author.id === context.userId ) return parent.secret return 'App secrets are only revealed to their author 😉' }, async scopes(parent, _args, context) { if ('scopes' in parent && parent.scopes?.length) return parent.scopes return await context.loaders.apps.getAppScopes.load(parent.id) } }, User: { async authorizedApps(_parent, _args, context) { const res = await getAllAppsAuthorizedByUser({ userId: context.userId! }) return res }, async createdApps(_parent, _args, context) { return await getAllAppsCreatedByUser({ userId: context.userId! }) } }, Mutation: { async appCreate(_parent, args, context) { const { id } = await withOperationLogging( async () => await createApp({ ...args.app, authorId: context.userId!, public: isNullOrUndefined(args.app.public) ? undefined : args.app.public, scopes: args.app.scopes.filter(isScope) }), { operationName: 'appCreate', operationDescription: 'Create a new app', logger: context.log } ) return id }, async appUpdate(_parent, args, context) { const app = await getApp({ id: args.app.id }) if (!app) { throw new ForbiddenError('You are not authorized to edit this app.') } // only admins can update the default apps, generated by the server if (!app?.author && context.role !== Roles.Server.Admin) throw new ForbiddenError('You are not authorized to edit this app.') // only the author or an admin can update a 3rd party app if (app?.author?.id !== context.userId && context.role !== Roles.Server.Admin) throw new ForbiddenError('You are not authorized to edit this app.') await withOperationLogging( async () => await updateApp({ app: { ...args.app, public: isNullOrUndefined(args.app.public) ? undefined : args.app.public, scopes: args.app.scopes.filter(isScope) } }), { operationName: 'appUpdate', operationDescription: 'Update an existing app', logger: context.log } ) return true }, async appDelete(_parent, args, context) { const app = await getApp({ id: args.appId }) if (!app) { //Possibly ould have been an UserInputError, but //we do not want to leak the existence of any app //the user may not own or have access to. throw new ForbiddenError('You are not authorized to edit this app.') } if (!app.author && context.role !== Roles.Server.Admin) throw new ForbiddenError('You are not authorized to edit this app.') if (app.author?.id !== context.userId && context.role !== Roles.Server.Admin) throw new ForbiddenError('You are not authorized to edit this app.') return await withOperationLogging( async () => (await deleteApp({ id: args.appId })) === 1, { operationName: 'appDelete', operationDescription: 'Delete an existing app', logger: context.log } ) }, async appRevokeAccess(_parent, args, context) { return await withOperationLogging( async () => !!(await revokeExistingAppCredentialsForUser({ appId: args.appId, userId: context.userId! })), { operationName: 'appRevokeAccess', operationDescription: 'Revoke access to an app', logger: context.log } ) } } } as Resolvers