|
|
@ -8,14 +8,16 @@ import { |
|
|
|
DefaultHeaders, |
|
|
|
} from 'fastify' |
|
|
|
|
|
|
|
import merge from 'lodash/merge' |
|
|
|
import { JSONObject } from '@azure/cosmos' |
|
|
|
import { Server, IncomingMessage, ServerResponse } from 'http' |
|
|
|
import merge from 'lodash/merge' |
|
|
|
|
|
|
|
import { MIN_ID_LENGTH, MAX_NAME_LENGTH, GROUP_LISTING_PARTITION_KEY } from '../../constants' |
|
|
|
import { errorSchema, groupListingSchema, userSchema } from '../../schemas' |
|
|
|
import { unauthorizedError, badRequestError, notFoundError, serverError } from '../../lib/errors' |
|
|
|
import { getUsers } from '../../lib/collections' |
|
|
|
import { getUsers, getUserMembership } from '../../lib/collections' |
|
|
|
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database' |
|
|
|
import { createInvitationCode } from '../../lib/util' |
|
|
|
|
|
|
|
import { |
|
|
|
User, |
|
|
@ -29,7 +31,9 @@ import { |
|
|
|
GroupMembershipType, |
|
|
|
GroupItemType, |
|
|
|
BlockType, |
|
|
|
UserItemType |
|
|
|
UserItemType, |
|
|
|
GroupLog, |
|
|
|
GroupInvitation, |
|
|
|
} from '../../types/collections' |
|
|
|
|
|
|
|
import { PluginOptions } from '../../types' |
|
|
@ -72,7 +76,6 @@ function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, Serv |
|
|
|
const group = await getItem<Group>({ |
|
|
|
container: containerFor(server.database.client, 'Groups'), |
|
|
|
id, |
|
|
|
logger: request.log, |
|
|
|
}) |
|
|
|
|
|
|
|
return { |
|
|
@ -124,8 +127,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp |
|
|
|
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 { resource: viewer } = await viewerItem.read<User>() |
|
|
|
|
|
|
|
const groupContainer = containerFor(server.database.client, 'Groups') |
|
|
|
|
|
|
@ -134,11 +136,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp |
|
|
|
const { name, about, registration } = request.body |
|
|
|
const id = normalize(name) |
|
|
|
|
|
|
|
const existingGroup = await getItem<Group>({ |
|
|
|
container: groupContainer, |
|
|
|
id, |
|
|
|
logger: request.log |
|
|
|
}) |
|
|
|
const existingGroup = await getItem<Group>({ container: groupContainer, id }) |
|
|
|
if (existingGroup) return badRequestError(reply, 'Name already used') |
|
|
|
|
|
|
|
const group: Group = { |
|
|
@ -177,6 +175,14 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp |
|
|
|
}, |
|
|
|
}) |
|
|
|
|
|
|
|
await groupContainer.items.create<GroupLog>({ |
|
|
|
pk: group.id, |
|
|
|
t: GroupItemType.Log, |
|
|
|
userId: request.viewer.id, |
|
|
|
content: 'created', |
|
|
|
created: Date.now(), |
|
|
|
}) |
|
|
|
|
|
|
|
return { |
|
|
|
id: group.id, |
|
|
|
} |
|
|
@ -211,13 +217,11 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons |
|
|
|
container: containerFor(server.database.client, 'GroupDirectory'), |
|
|
|
id: request.params.id, |
|
|
|
partitionKey: 'pk', |
|
|
|
logger: request.log, |
|
|
|
}) |
|
|
|
|
|
|
|
const group = await getItem<Group>({ |
|
|
|
container: groupContainer, |
|
|
|
id: request.params.id, |
|
|
|
logger: request.log, |
|
|
|
}) |
|
|
|
|
|
|
|
const combine = async (group: Group, listing: GroupListing) => { |
|
|
@ -256,6 +260,82 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|
|
|
interface Body { |
|
|
|
name?: string |
|
|
|
about?: string |
|
|
|
registration?: string |
|
|
|
} |
|
|
|
|
|
|
|
const options: RouteShorthandOptions = { |
|
|
|
schema: { |
|
|
|
body: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
name: { |
|
|
|
type: 'string', |
|
|
|
minLength: MIN_ID_LENGTH, |
|
|
|
maxLength: MAX_NAME_LENGTH, |
|
|
|
}, |
|
|
|
about: { type: 'string' }, |
|
|
|
registration: { |
|
|
|
type: 'string', |
|
|
|
enum: ['open', 'approval', 'closed'], |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
response: { |
|
|
|
400: errorSchema, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
server.put<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/group/:id', options, async (request, reply) => { |
|
|
|
if (!server.database) return serverError(reply) |
|
|
|
if (!request.viewer) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const membership = await getUserMembership(server.database.client, request.viewer.id, request.log) |
|
|
|
if (!membership || membership.membership !== GroupMembershipType.Admin) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const groupContainer = containerFor(server.database.client, 'Groups') |
|
|
|
const directoryContainer = containerFor(server.database.client, 'GroupDirectory') |
|
|
|
const groupItem = groupContainer.item(request.params.id, request.params.id) |
|
|
|
const groupListingItem = directoryContainer.item(request.params.id, GROUP_LISTING_PARTITION_KEY) |
|
|
|
|
|
|
|
const { resource: group } = await groupItem.read<Group>() |
|
|
|
if (!group) return notFoundError(reply) |
|
|
|
|
|
|
|
const { resource: groupListing } = await groupListingItem.read<GroupListing>() |
|
|
|
|
|
|
|
let updates: JSONObject = {} |
|
|
|
if (request.body.name) updates.name = request.body.name |
|
|
|
if (request.body.about) updates.about = request.body.about |
|
|
|
if (request.body.registration) updates.registration = request.body.registration |
|
|
|
|
|
|
|
await groupItem.replace<Group>({ |
|
|
|
...group, |
|
|
|
...updates, |
|
|
|
}) |
|
|
|
|
|
|
|
if (groupListing) { |
|
|
|
await groupListingItem.replace<GroupListing>({ |
|
|
|
...groupListing, |
|
|
|
...updates, |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
await groupContainer.items.create<GroupLog>({ |
|
|
|
pk: group.id, |
|
|
|
t: GroupItemType.Log, |
|
|
|
userId: request.viewer.id, |
|
|
|
content: 'updated', |
|
|
|
created: Date.now(), |
|
|
|
}) |
|
|
|
|
|
|
|
reply.code(204) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|
|
|
interface Params { |
|
|
|
id: string |
|
|
@ -287,11 +367,7 @@ function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespo |
|
|
|
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, |
|
|
|
}) |
|
|
|
const group = await getItem<Group>({ container: groupContainer, id: request.params.id }) |
|
|
|
if (!group) return notFoundError(reply) |
|
|
|
|
|
|
|
await containerFor(server.database.client, 'Users').items.create<UserBlock>({ |
|
|
@ -338,11 +414,7 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes |
|
|
|
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 |
|
|
|
}) |
|
|
|
const group = await getItem<Group>({ container: groupContainer, id: request.params.id }) |
|
|
|
if (!group) return notFoundError(reply) |
|
|
|
|
|
|
|
const userBlockQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.pk = @pk AND u.blockedId = @blocked AND u.t = @type`, { |
|
|
@ -406,9 +478,7 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe |
|
|
|
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) |
|
|
|
|
|
|
|
const { resource: group } = await groupItem.read<Group>() |
|
|
|
if (!group) return notFoundError(reply) |
|
|
|
|
|
|
|
if (group.active && group.status === 'paid') { |
|
|
@ -423,8 +493,7 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe |
|
|
|
|
|
|
|
const directoryContainer = containerFor(server.database.client, 'GroupDirectory') |
|
|
|
const listingItem = directoryContainer.item(request.params.id, GROUP_LISTING_PARTITION_KEY) |
|
|
|
const { resource: listing, requestCharge: listingRequestCharge } = await listingItem.read<GroupListing>() |
|
|
|
request.log.trace('Get: %d', listingRequestCharge) |
|
|
|
const { resource: listing } = await listingItem.read<GroupListing>() |
|
|
|
|
|
|
|
if (!listing) { |
|
|
|
await directoryContainer.items.create<GroupListing>({ |
|
|
@ -550,11 +619,7 @@ function membersRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes |
|
|
|
|
|
|
|
const groupContainer = containerFor(server.database.client, 'Groups') |
|
|
|
|
|
|
|
const group = await getItem<Group>({ |
|
|
|
container: groupContainer, |
|
|
|
id: request.params.id, |
|
|
|
logger: request.log |
|
|
|
}) |
|
|
|
const group = await getItem<Group>({ container: groupContainer, id: request.params.id }) |
|
|
|
if (!group) return notFoundError(reply) |
|
|
|
|
|
|
|
const { type, continuation } = request.query |
|
|
@ -590,15 +655,157 @@ function membersRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function createInvitationRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|
|
|
interface Body { |
|
|
|
expiration?: number |
|
|
|
limit?: number |
|
|
|
} |
|
|
|
|
|
|
|
const options: RouteShorthandOptions = { |
|
|
|
schema: { |
|
|
|
body: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
expiration: { type: 'number' }, |
|
|
|
limit: { type: 'number' }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
response: { |
|
|
|
200: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
code: { type: 'string' }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/group/:id/invitation', options, async (request, reply) => { |
|
|
|
if (!server.database) return serverError(reply) |
|
|
|
if (!request.viewer) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const membership = await getUserMembership(server.database.client, request.viewer.id, request.log) |
|
|
|
if (!membership || membership.membership !== GroupMembershipType.Admin) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const container = containerFor(server.database.client, 'Groups') |
|
|
|
|
|
|
|
const group = await getItem<Group>({ container, id: request.params.id }) |
|
|
|
if (!group) return notFoundError(reply) |
|
|
|
|
|
|
|
const code = createInvitationCode() |
|
|
|
|
|
|
|
await container.items.create<GroupInvitation>({ |
|
|
|
id: code, |
|
|
|
pk: group.id, |
|
|
|
t: GroupItemType.Invitation, |
|
|
|
userId: request.viewer.id, |
|
|
|
limit: request.body.limit, |
|
|
|
expiration: request.body.expiration, |
|
|
|
uses: 0, |
|
|
|
active: true, |
|
|
|
created: Date.now(), |
|
|
|
}) |
|
|
|
|
|
|
|
await container.items.create<GroupLog>({ |
|
|
|
pk: group.id, |
|
|
|
t: GroupItemType.Log, |
|
|
|
userId: request.viewer.id, |
|
|
|
content: `created invitation: ${code}`, |
|
|
|
created: Date.now(), |
|
|
|
}) |
|
|
|
|
|
|
|
return { |
|
|
|
code, |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function logsRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|
|
|
interface Query { |
|
|
|
continuation?: string |
|
|
|
} |
|
|
|
|
|
|
|
const options: RouteShorthandOptions = { |
|
|
|
schema: { |
|
|
|
querystring: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
continuation: { type: 'string' }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
response: { |
|
|
|
200: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
logs: { |
|
|
|
type: 'array', |
|
|
|
items: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
user: userSchema, |
|
|
|
content: { type: 'string' }, |
|
|
|
created: { type: 'number' }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
continuation: { type: 'string' }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
400: errorSchema, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/group/:id/logs', options, async (request, reply) => { |
|
|
|
if (!server.database) return serverError(reply) |
|
|
|
if (!request.viewer) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const membership = await getUserMembership(server.database.client, request.viewer.id, request.log) |
|
|
|
if (!membership || membership.membership !== GroupMembershipType.Admin) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const container = containerFor(server.database.client, 'Groups') |
|
|
|
|
|
|
|
const group = await getItem<Group>({ container, id: request.params.id }) |
|
|
|
if (!group) return notFoundError(reply) |
|
|
|
|
|
|
|
const { continuation } = request.query |
|
|
|
|
|
|
|
const { resources: logs, requestCharge, continuation: newContinuation } = await container.items.query<GroupLog>( |
|
|
|
`SELECT * FROM Groups g WHERE g.pk = '${group.id}' AND g.t = '${GroupItemType.Log}' ORDER BY g.created DESC`, |
|
|
|
{ |
|
|
|
maxItemCount: 80, |
|
|
|
continuation, |
|
|
|
} |
|
|
|
).fetchAll() |
|
|
|
|
|
|
|
request.log.trace('Query: %d', requestCharge) |
|
|
|
|
|
|
|
const users = await getUsers(server.database.client, logs.map(log => log.userId), request.log) |
|
|
|
|
|
|
|
return { |
|
|
|
logs: logs.map(log => ({ |
|
|
|
...log, |
|
|
|
user: users.find(user => user.id === log.userId), |
|
|
|
userId: undefined, |
|
|
|
})), |
|
|
|
continuation: newContinuation, |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => { |
|
|
|
availabilityRoute(server) |
|
|
|
createRoute(server) |
|
|
|
getRoute(server) |
|
|
|
updateRoute(server) |
|
|
|
blockRoute(server) |
|
|
|
unblockRoute(server) |
|
|
|
activateRoute(server) |
|
|
|
listRoute(server) |
|
|
|
membersRoute(server) |
|
|
|
createInvitationRoute(server) |
|
|
|
logsRoute(server) |
|
|
|
} |
|
|
|
|
|
|
|
export default plugin |