You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
465 lines
14 KiB
465 lines
14 KiB
import {
|
|
FastifyInstance,
|
|
Plugin,
|
|
DefaultQuery,
|
|
DefaultParams,
|
|
DefaultBody,
|
|
RouteShorthandOptions,
|
|
DefaultHeaders,
|
|
} from 'fastify'
|
|
|
|
import { Server, IncomingMessage, ServerResponse } from 'http'
|
|
|
|
import { MIN_ID_LENGTH, MAX_NAME_LENGTH } from '../../constants'
|
|
import { errorSchema, groupListingSchema } from '../../schemas'
|
|
import { unauthorizedError, badRequestError, notFoundError, serverError } from '../../lib/errors'
|
|
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
|
|
import { User, Group, GroupListing, GroupMembership, UserBlock, GroupBlock, GroupRegistrationType } 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/group/available', options, async (request, reply) => {
|
|
if (!server.database) return serverError(reply)
|
|
|
|
const id = normalize(request.body.name)
|
|
|
|
const group = await getItem<Group>({
|
|
container: containerFor(server.database.client, 'Groups'),
|
|
id,
|
|
logger: request.log,
|
|
})
|
|
|
|
return {
|
|
id,
|
|
available: !group,
|
|
}
|
|
})
|
|
}
|
|
|
|
function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
|
|
interface Body {
|
|
name: string
|
|
about?: string
|
|
registration: GroupRegistrationType
|
|
}
|
|
|
|
const options: RouteShorthandOptions = {
|
|
schema: {
|
|
body: {
|
|
type: 'object',
|
|
required: ['name', 'registration'],
|
|
properties: {
|
|
name: {
|
|
type: 'string',
|
|
minLength: MIN_ID_LENGTH,
|
|
maxLength: MAX_NAME_LENGTH,
|
|
},
|
|
about: { type: 'string' },
|
|
registration: {
|
|
type: 'string',
|
|
enum: ['open', 'approval', 'closed'],
|
|
},
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string' },
|
|
},
|
|
},
|
|
400: errorSchema,
|
|
},
|
|
},
|
|
}
|
|
|
|
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/group', 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)
|
|
|
|
const groupContainer = containerFor(server.database.client, 'Groups')
|
|
|
|
if (viewer.group) return badRequestError(reply)
|
|
|
|
const { name, about, registration } = request.body
|
|
const id = normalize(name)
|
|
|
|
const existingGroup = await getItem<Group>({
|
|
container: groupContainer,
|
|
id,
|
|
logger: request.log
|
|
})
|
|
if (existingGroup) return badRequestError(reply, 'Name already used')
|
|
|
|
const group: Group = {
|
|
id: id,
|
|
pk: id,
|
|
t: 'group',
|
|
userId: request.viewer.id,
|
|
name,
|
|
about,
|
|
registration,
|
|
status: 'pending',
|
|
active: true,
|
|
created: Date.now(),
|
|
}
|
|
|
|
const membership: GroupMembership = {
|
|
id: request.viewer.id,
|
|
pk: id,
|
|
t: 'membership',
|
|
userId: request.viewer.id,
|
|
pending: false,
|
|
membership: 'admin',
|
|
created: Date.now(),
|
|
}
|
|
|
|
await groupContainer.items.create(group)
|
|
await groupContainer.items.create(membership)
|
|
|
|
await viewerItem.replace<User>({
|
|
...viewer,
|
|
group: {
|
|
id: group.id,
|
|
name: group.name,
|
|
imageUrl: group.imageUrl,
|
|
coverImageUrl: group.coverImageUrl,
|
|
},
|
|
})
|
|
|
|
return {
|
|
id: group.id,
|
|
}
|
|
})
|
|
}
|
|
|
|
function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
|
|
interface Params {
|
|
id: string
|
|
}
|
|
|
|
const options: RouteShorthandOptions = {
|
|
schema: {
|
|
params: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string' },
|
|
},
|
|
},
|
|
response: {
|
|
200: groupListingSchema,
|
|
},
|
|
},
|
|
}
|
|
|
|
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/group/:id', options, async (request, reply) => {
|
|
if (!server.database) return serverError(reply)
|
|
|
|
const listing = await getItem<GroupListing>({
|
|
container: containerFor(server.database.client, 'GroupDirectory'),
|
|
id: request.params.id,
|
|
logger: request.log,
|
|
})
|
|
|
|
if (!listing) return notFoundError(reply)
|
|
return listing
|
|
})
|
|
}
|
|
|
|
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' },
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
server.post<DefaultQuery, Params, DefaultHeaders, Body>('/api/group/:id/block', options, async (request, reply) => {
|
|
if (!server.database) return serverError(reply)
|
|
if (!request.viewer) return unauthorizedError(reply)
|
|
|
|
const groupContainer = containerFor(server.database.client, 'Groups')
|
|
const group = await getItem<Group>({
|
|
container: groupContainer,
|
|
id: request.params.id,
|
|
logger: request.log,
|
|
})
|
|
if (!group) return notFoundError(reply)
|
|
|
|
await containerFor(server.database.client, 'Users').items.create<UserBlock>({
|
|
blockedId: group.id,
|
|
pk: request.viewer.id,
|
|
t: 'block',
|
|
blockType: 'group',
|
|
description: request.body.description,
|
|
created: Date.now(),
|
|
})
|
|
|
|
await groupContainer.items.create<GroupBlock>({
|
|
pk: group.id,
|
|
t: 'block',
|
|
blockedId: group.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, DefaultBody>('/api/group/: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 group = await getItem<Group>({
|
|
container: groupContainer,
|
|
id: request.params.id,
|
|
logger: request.log
|
|
})
|
|
if (!group) return notFoundError(reply)
|
|
|
|
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: group.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: group.id,
|
|
blocked: group.id,
|
|
viewer: request.viewer.id,
|
|
}
|
|
)
|
|
|
|
const groupBlocks = await queryItems<UserBlock>({
|
|
container: groupContainer,
|
|
query: groupBlockQuery,
|
|
logger: request.log
|
|
})
|
|
|
|
for (const groupBlock of groupBlocks) {
|
|
await groupContainer.item(groupBlock.id!, group.id).delete()
|
|
}
|
|
|
|
reply.code(204)
|
|
})
|
|
}
|
|
|
|
function activateRoute(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/group/:id/activate', options, async (request, reply) => {
|
|
if (!server.database) return serverError(reply)
|
|
|
|
const container = containerFor(server.database.client, 'Groups')
|
|
const groupItem = container.item(request.params.id, request.params.id)
|
|
|
|
const { resource: group, requestCharge: groupRequestCharge } = await groupItem.read<Group>()
|
|
request.log.trace('Get: %d', groupRequestCharge)
|
|
|
|
if (!group) return notFoundError(reply)
|
|
|
|
if (group.active && group.status === 'paid') {
|
|
return badRequestError(reply, 'Already activated')
|
|
}
|
|
|
|
await groupItem.replace<Group>({
|
|
...group,
|
|
active: true,
|
|
status: 'paid',
|
|
})
|
|
|
|
const directoryContainer = containerFor(server.database.client, 'GroupDirectory')
|
|
const listingItem = directoryContainer.item(request.params.id, 'pk')
|
|
const { resource: listing, requestCharge: listingRequestCharge } = await listingItem.read<GroupListing>()
|
|
request.log.trace('Get: %d', listingRequestCharge)
|
|
|
|
if (!listing) {
|
|
await directoryContainer.items.create<GroupListing>({
|
|
id: group.id,
|
|
name: group.name,
|
|
pk: 'pk',
|
|
registration: group.registration,
|
|
members: 0,
|
|
posts: 0,
|
|
awards: 0,
|
|
points: 0,
|
|
latestAwards: [],
|
|
created: Date.now(),
|
|
})
|
|
}
|
|
|
|
reply.code(204)
|
|
})
|
|
}
|
|
|
|
function listRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
|
|
interface Query {
|
|
sort?: string
|
|
registration?: GroupRegistrationType
|
|
continuation?: string
|
|
}
|
|
|
|
const options: RouteShorthandOptions = {
|
|
schema: {
|
|
querystring: {
|
|
type: 'object',
|
|
properties: {
|
|
sort: {
|
|
type: 'string',
|
|
enum: ['name', 'members', 'points'],
|
|
},
|
|
registration: {
|
|
type: 'string',
|
|
enum: ['open', 'approval', 'closed'],
|
|
},
|
|
continuation: { type: 'string' },
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
properties: {
|
|
groups: {
|
|
type: 'array',
|
|
items: groupListingSchema,
|
|
},
|
|
continuation: { type: 'string' },
|
|
}
|
|
},
|
|
400: errorSchema,
|
|
}
|
|
},
|
|
}
|
|
|
|
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/groups', options, async (request, reply) => {
|
|
if (!server.database) return serverError(reply)
|
|
const { sort = 'members', registration, continuation } = request.query
|
|
let registrationString = ''
|
|
|
|
if (registration) {
|
|
registrationString = `AND d.registration = ${registration}`
|
|
}
|
|
|
|
const container = containerFor(server.database.client, 'GroupDirectory')
|
|
const { resources: groups, requestCharge, continuation: newContinuation } = await container.items.query<GroupListing>(
|
|
`SELECT * FROM GroupDirectory d WHERE d.pk = 'pk' AND d.active = true ${registrationString} ORDER BY d.${sort}`,
|
|
{
|
|
maxItemCount: 40,
|
|
continuation,
|
|
}
|
|
).fetchAll()
|
|
|
|
request.log.trace('Query: %d', requestCharge)
|
|
|
|
return {
|
|
groups,
|
|
continuation: newContinuation,
|
|
}
|
|
})
|
|
}
|
|
|
|
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
|
|
availabilityRoute(server)
|
|
createRoute(server)
|
|
getRoute(server)
|
|
blockRoute(server)
|
|
unblockRoute(server)
|
|
activateRoute(server)
|
|
listRoute(server)
|
|
}
|
|
|
|
export default plugin
|