|
|
import { FastifyInstance, Plugin, DefaultQuery, DefaultParams, RouteShorthandOptions, DefaultHeaders, DefaultBody, } from 'fastify'
import { Server, IncomingMessage, ServerResponse } from 'http'
import { MAX_NAME_LENGTH } from '../../constants' import { userSchema, errorSchema } from '../../schemas' import { unauthorizedError, serverError, notFoundError, badRequestError } from '../../lib/errors' import { getUserBlocks } from '../../lib/collections' import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import { User, UserSubscription, UserBlock, GroupBlock, UserPrivacyType } from '../../types/collections' import { PluginOptions } from '../../types'
function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { interface Body { name: string }
const options: RouteShorthandOptions = { schema: { body: { type: 'object', required: ['name'], properties: { name: { type: 'string', maxLength: MAX_NAME_LENGTH, }, }, }, response: { 200: { type: 'object', properties: { id: { type: 'string' }, available: { type: 'boolean' }, }, }, 400: errorSchema, }, }, }
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/user/available', options, async (request, reply) => { if (!server.database) return serverError(reply)
const id = normalize(request.body.name)
const user = await getItem<User>({ container: containerFor(server.database.client, 'Users'), id, logger: request.log, })
return { id, available: !!user, } }) }
function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { interface Body { name?: string about?: string requiresApproval?: boolean privacy?: UserPrivacyType }
const options: RouteShorthandOptions = { schema: { body: { type: 'object', properties: { name: { type: 'string', maxLength: MAX_NAME_LENGTH, }, about: { type: 'string' }, requiresApproval: { type: 'boolean' }, privacy: { type: 'string', enum: ['public', 'group', 'subscribers', 'private'], }, }, }, response: { 200: userSchema, 400: errorSchema, }, }, }
server.put<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/self', options, async (request, reply) => { if (!server.database) return serverError(reply) if (!request.viewer) return unauthorizedError(reply)
const viewerItem = containerFor(server.database.client, 'Users').item(request.viewer.id, request.viewer.id) const { resource: viewer, requestCharge } = await viewerItem.read<User>() request.log.trace('Get: %d', requestCharge)
if (!viewer) return serverError(reply)
if (request.body.name) { const name = request.body.name.trim() if (name !== '') { viewer.name = name } }
if (request.body.about) { const about = request.body.about.trim() if (about !== '') { viewer.about = about } }
if (request.body.requiresApproval !== undefined) { viewer.requiresApproval = request.body.requiresApproval }
if (request.body.privacy) { viewer.privacy = request.body.privacy }
await viewerItem.replace<User>(viewer)
return viewer }) }
function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { interface Params { id: string }
const options: RouteShorthandOptions = { schema: { params: { type: 'object', properties: { id: { type: 'string' }, }, }, response: { 200: userSchema, 400: errorSchema, }, }, }
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id', options, async (request, reply) => { if (!server.database) return serverError(reply)
const userContainer = containerFor(server.database.client, 'Users') const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log })
if (!user) return notFoundError(reply)
if (request.viewer) { const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id, logger: request.log })
if (!viewer) return serverError(reply) if (!viewer.group) return unauthorizedError(reply)
const blocks = await getUserBlocks(server.database.client, user.id, [viewer.id, viewer.group.id], request.log) if (blocks.length > 0) return unauthorizedError(reply) }
return user }) }
function subscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { interface Params { id: string }
const options: RouteShorthandOptions = { schema: { params: { type: 'object', properties: { id: { type: 'string' }, }, }, response: { 400: errorSchema, }, }, }
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/subscribe', options, async (request, reply) => { if (!server.database) return serverError(reply) if (!request.viewer) return unauthorizedError(reply)
if (request.viewer.id === request.params.id) return badRequestError(reply)
const userContainer = containerFor(server.database.client, 'Users') const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log }) const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id, logger: request.log })
if (!user) return notFoundError(reply) if (!viewer) return serverError(reply) if (!viewer.group) return unauthorizedError(reply)
const subscriptionQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.subscriberId = @user AND u.pk = @viewer AND u.t = 'subscription'`, { user: user.id, viewer: viewer.id, })
const subscriptions = await queryItems<UserSubscription>({ container: userContainer, query: subscriptionQuery, logger: request.log }) if (subscriptions.length > 0) return badRequestError(reply, 'Already subscribed') let pending = false
switch (user.privacy) { case 'private': return unauthorizedError(reply) case 'group': if (user.group !== viewer.group) return unauthorizedError(reply) case 'subscribers': pending = true break }
const blockQuery = createQuerySpec(`
SELECT g.id FROM Groups g WHERE g.pk = @viewerGroup AND g.t = 'block' AND g.userId = @user AND (g.blockedId = @viewer OR g.blockedId = @viewerGroup) `, {
user: user.id, viewerGroup: viewer.group.id, })
const blocks = await queryItems<GroupBlock>({ container: containerFor(server.database.client, 'Groups'), query: blockQuery, logger: request.log })
if (blocks.length > 0) return badRequestError(reply, 'Invalid operation')
await userContainer.items.create<UserSubscription>({ subscriberId: user.id, pk: request.viewer.id, t: 'subscription', pending, created: Date.now(), })
reply.code(204) }) }
function unsubscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { interface Params { id: string }
const options: RouteShorthandOptions = { schema: { params: { type: 'object', properties: { id: { type: 'string' }, }, }, }, }
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/unsubscribe', options, async (request, reply) => { if (!server.database) return serverError(reply) if (!request.viewer) return unauthorizedError(reply)
const userContainer = containerFor(server.database.client, 'Users') const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log }) const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id, logger: request.log })
if (!user) return notFoundError(reply) if (!viewer) return serverError(reply)
const subscriptionQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.subscriberId = @user AND u.pk = @viewer AND u.t = 'subscription'`, { user: user.id, viewer: viewer.id, })
const subscriptions = await queryItems<UserSubscription>({ container: userContainer, query: subscriptionQuery, logger: request.log }) for (const subscription of subscriptions) { await userContainer.item(subscription.id!, viewer.id).delete() }
reply.code(204) }) }
function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { interface Params { id: string }
interface Body { description?: string }
const options: RouteShorthandOptions = { schema: { params: { type: 'object', properties: { id: { type: 'string' }, }, }, body: { type: 'object', properties: { description: { type: 'string' }, }, }, response: { 400: errorSchema, } }, }
server.post<DefaultQuery, Params, DefaultHeaders, Body>('/api/user/:id/block', options, async (request, reply) => { if (!server.database) return serverError(reply) if (!request.viewer) return unauthorizedError(reply)
const userContainer = containerFor(server.database.client, 'Users') const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log })
if (!user) return notFoundError(reply) if (!user.group) return badRequestError(reply)
await userContainer.items.create<UserBlock>({ blockedId: user.id, pk: request.viewer.id, t: 'block', blockType: 'user', description: request.body.description, created: Date.now(), })
await containerFor(server.database.client, 'Groups').items.create<GroupBlock>({ pk: user.group.id, t: 'block', blockedId: user.id, userId: request.viewer.id, created: Date.now(), })
reply.code(204) }) }
function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { interface Params { id: string }
const options: RouteShorthandOptions = { schema: { params: { type: 'object', properties: { id: { type: 'string' }, }, }, }, }
server.post<DefaultQuery, Params, DefaultHeaders, Body>('/api/user/:id/unblock', options, async (request, reply) => { if (!server.database) return serverError(reply) if (!request.viewer) return unauthorizedError(reply)
const userContainer = containerFor(server.database.client, 'Users') const groupContainer = containerFor(server.database.client, 'Groups')
const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log })
if (!user) return notFoundError(reply) if (!user.group) return badRequestError(reply, 'Invalid operation')
const userBlockQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.pk = @pk AND u.blockedId = @blocked AND u.type = 'block'`, { pk: request.viewer.id, blocked: user.id, })
const userBlocks = await queryItems<UserBlock>({ container: userContainer, query: userBlockQuery, logger: request.log })
for (const userBlock of userBlocks) { await userContainer.item(userBlock.id!, request.viewer.id).delete() }
const groupBlockQuery = createQuerySpec( `SELECT g.id FROM Groups g WHERE g.pk = @pk AND u.blockedId = @blocked AND u.userId = @viewer AND u.type = 'block'`, { pk: user.group.id, blocked: user.id, viewer: request.viewer.id, } )
const groupBlocks = await queryItems<GroupBlock>({ container: groupContainer, query: groupBlockQuery, logger: request.log }) for (const groupBlock of groupBlocks) { await groupContainer.item(groupBlock.id!, user.group.id).delete() }
reply.code(204) }) }
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => { availabilityRoute(server) updateRoute(server) getRoute(server) subscribeRoute(server) unsubscribeRoute(server) blockRoute(server) unblockRoute(server) }
export default plugin
|