diff --git a/src/constants.ts b/src/constants.ts index 1b0305d..aeaa4bc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,3 +10,7 @@ export const GROUP_LISTING_PARTITION_KEY = 'gpk' export const APP_PARTITION_KEY = 'apk' export const INSTALLATION_PARTITION_KEY = 'ipk' export const MEDIA_PARTITION_KEY = 'pk' + +export const ADMINS = [ + 'dwayneh@gmail.com', +] diff --git a/src/plugins/api/apps.ts b/src/plugins/api/apps.ts index e9538a1..b142fab 100644 --- a/src/plugins/api/apps.ts +++ b/src/plugins/api/apps.ts @@ -12,14 +12,14 @@ import { Server, IncomingMessage, ServerResponse } from 'http' import pick from 'lodash/pick' import { appSchema, errorSchema } from '../../schemas' -import { getUsers, userIdIsValid, userIsValid, updateItem } from '../../lib/collections' +import { getUsers, userIdIsValid, userIsValid, updateItem, getUser } from '../../lib/collections' import { generateString } from '../../lib/crypto' import { containerFor, getItem, normalize, queryItems, createQuerySpec } from '../../lib/database' import { unauthorizedError, serverError, badRequestError, notFoundError, forbiddenError } from '../../lib/errors' import { attachMedia, deleteMedia } from '../../lib/media' import { createInstallationId } from '../../lib/utils' -import { APP_PARTITION_KEY, MAX_NAME_LENGTH, INSTALLATION_PARTITION_KEY } from '../../constants' +import { APP_PARTITION_KEY, MAX_NAME_LENGTH, INSTALLATION_PARTITION_KEY, ADMINS } from '../../constants' import { App, User, Installation, InstallationSettings } from '../../types/collections' import { PluginOptions } from '../../types' @@ -754,11 +754,68 @@ function installationsRoute(server: FastifyInstance) { - interface Headers { - adminkey: string +function pendingRoute(server: FastifyInstance) { + interface Query { + continuation?: string } + const options: RouteShorthandOptions = { + schema: { + description: 'Get Apps awaiting activation.', + tags: ['app', 'admin'], + querystring: { + type: 'object', + properties: { + continuation: { type: 'string' }, + }, + }, + response: { + 200: { + description: 'Successful response.', + type: 'object', + properties: { + apps: { + type: 'array', + items: appSchema, + }, + continuation: { type: 'string' }, + } + }, + 400: errorSchema, + }, + }, + } + + server.get('/v1/apps/pending', options, async (request, reply) => { + if (!server.database) return serverError(reply) + if (!request.viewer) return unauthorizedError(reply) + + const viewer = await getUser(server.database.client, request.viewer.id) + if (!viewer || !ADMINS.includes(viewer.email)) return forbiddenError(reply) + + const { continuation } = request.query + const container = containerFor(server.database.client, 'Apps') + + const { resources: apps, requestCharge, continuation: newContinuation } = await container.items.query( + createQuerySpec('SELECT * FROM Apps a WHERE a.pk = @pk AND a.active = false', { + pk: APP_PARTITION_KEY, + }), + { + maxItemCount: 20, + continuation, + } + ).fetchAll() + + request.log.trace('Query: %d', requestCharge) + + return { + apps, + continuation: newContinuation, + } + }) +} + +function activateRoute(server: FastifyInstance) { interface Params { id: string } @@ -766,14 +823,7 @@ function activateRoute(server: FastifyInstance('/v1/app/:id/activate', options, async (request, reply) => { + server.post('/v1/app/:id/activate', options, async (request, reply) => { if (!server.database) return serverError(reply) - if (request.headers.adminkey !== process.env.ADMIN_KEY) return serverError(reply) + if (!request.viewer) return unauthorizedError(reply) + + const viewer = await getUser(server.database.client, request.viewer.id) + if (!viewer || !ADMINS.includes(viewer.email)) return forbiddenError(reply) await updateItem(containerFor(server.database.client, 'Apps').item(request.params.id, APP_PARTITION_KEY), { active: true }) reply.code(204) @@ -800,10 +853,6 @@ function activateRoute(server: FastifyInstance) { - interface Headers { - adminkey: string - } - interface Params { id: string } @@ -811,14 +860,7 @@ function setPreinstallRoute(server: FastifyInstance('/v1/app/:id/preinstall', options, async (request, reply) => { if (!server.database) return serverError(reply) - if (request.headers.adminkey !== process.env.ADMIN_KEY) return serverError(reply) + if (!request.viewer) return unauthorizedError(reply) + + const viewer = await getItem({ + container: containerFor(server.database.client, 'Users'), + id: request.viewer.id, + }) + + if (!viewer || !ADMINS.includes(viewer.email)) return forbiddenError(reply) await updateItem(containerFor(server.database.client, 'Apps').item(request.params.id, APP_PARTITION_KEY), { preinstall: true, diff --git a/src/plugins/api/authentication.ts b/src/plugins/api/authentication.ts index 4e339de..fe793c3 100644 --- a/src/plugins/api/authentication.ts +++ b/src/plugins/api/authentication.ts @@ -28,6 +28,7 @@ import { INSTALLATION_PARTITION_KEY, APP_PARTITION_KEY, USER_LISTING_PARTITION_KEY, + ADMINS, } from '../../constants' import { @@ -462,7 +463,10 @@ function selfRoute(server: FastifyInstance) { - interface Headers { - adminkey: string +function pendingRoute(server: FastifyInstance) { + interface Query { + continuation?: string + } + + const options: RouteShorthandOptions = { + schema: { + description: 'Get Groups awaiting activation.', + tags: ['group', 'admin'], + querystring: { + type: 'object', + properties: { + continuation: { type: 'string' }, + }, + }, + response: { + 200: { + description: 'Successful response.', + type: 'object', + properties: { + groups: { + type: 'array', + items: groupSchema, + }, + continuation: { type: 'string' }, + } + }, + 400: errorSchema, + }, + }, } + server.get('/v1/groups/pending', options, async (request, reply) => { + if (!server.database) return serverError(reply) + if (!request.viewer) return unauthorizedError(reply) + + const viewer = await getUser(server.database.client, request.viewer.id) + if (!viewer || !ADMINS.includes(viewer.email)) return forbiddenError(reply) + + const { continuation } = request.query + const container = containerFor(server.database.client, 'Groups') + + const { resources: groups, requestCharge, continuation: newContinuation } = await container.items.query( + createQuerySpec('SELECT * FROM Groups g WHERE g.t = @type AND g.status = @status', { + type: GroupItemType.Group, + status: GroupStatus.Pending, + }), + { + maxItemCount: 20, + continuation, + } + ).fetchAll() + + request.log.trace('Query: %d', requestCharge) + + return { + groups, + continuation: newContinuation, + } + }) +} + +function activateRoute(server: FastifyInstance) { interface Params { id: string } @@ -540,14 +598,7 @@ function activateRoute(server: FastifyInstance('/v1/group/:id/activate', options, async (request, reply) => { + server.post('/v1/group/:id/activate', options, async (request, reply) => { if (!server.database) return serverError(reply) - if (request.headers.adminkey !== process.env.ADMIN_KEY) return serverError(reply) + if (!request.viewer) return unauthorizedError(reply) + + const viewer = await getUser(server.database.client, request.viewer.id) + if (!viewer || !ADMINS.includes(viewer.email)) return forbiddenError(reply) const container = containerFor(server.database.client, 'Groups') const groupItem = container.item(request.params.id, request.params.id) @@ -1164,6 +1218,7 @@ const plugin: Plugin = a updateRoute(server) blockRoute(server) unblockRoute(server) + pendingRoute(server) activateRoute(server) listRoute(server) membersRoute(server) diff --git a/src/schemas.ts b/src/schemas.ts index d8adb1b..2b2344a 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -155,6 +155,7 @@ export const selfSchema: JSONSchema = { membership: { type: 'string' }, posts: { type: 'number' }, points: { type: 'number' }, + admin: { type: 'boolean' }, created: { type: 'number' }, }, }