@ -9,12 +9,11 @@ import {
} from 'fastify'
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 , groupListing Schema , userSchema } from '../../schemas'
import { unauthorizedError , badRequestError , notFoundError , serverError } from '../../lib/errors'
import { getUsers , getUserMembership } from '../../lib/collections'
import { errorSchema , groupSchema , userSchema } from '../../schemas'
import { unauthorizedError , badRequestError , notFoundError , serverError , forbiddenError } from '../../lib/errors'
import { getUsers , getUserMembership , getUser , userIsValid } from '../../lib/collections'
import { containerFor , createQuerySpec , queryItems , getItem , normalize } from '../../lib/database'
import { createInvitationCode } from '../../lib/utils'
import { attachMedia , deleteMedia } from '../../lib/media'
@ -71,7 +70,7 @@ function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, Serv
} ,
}
server . post < DefaultQuery , DefaultParams , DefaultHeaders , Body > ( '/api /group/available' , options , async ( request , reply ) = > {
server . post < DefaultQuery , DefaultParams , DefaultHeaders , Body > ( '/v1 /group/available' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
const id = normalize ( request . body . name )
@ -136,7 +135,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
} ,
}
server . post < DefaultQuery , DefaultParams , DefaultHeaders , Body > ( '/api /group' , options , async ( request , reply ) = > {
server . post < DefaultQuery , DefaultParams , DefaultHeaders , Body > ( '/v1 /group' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
if ( ! request . viewer ) return unauthorizedError ( reply )
@ -161,6 +160,9 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
name ,
about ,
registration ,
members : 0 ,
posts : 0 ,
points : 0 ,
imageUrl ,
coverImageUrl ,
iconImageUrl ,
@ -226,61 +228,53 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
} ,
} ,
response : {
200 : groupListing Schema ,
200 : groupSchema ,
400 : errorSchema ,
} ,
} ,
}
server . get < DefaultQuery , Params , DefaultHeaders , DefaultBody > ( '/api /group/:id' , options , async ( request , reply ) = > {
server . get < DefaultQuery , Params , DefaultHeaders , DefaultBody > ( '/v1 /group/:id' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
const groupContainer = containerFor ( server . database . client , 'Groups' )
const listing = await getItem < GroupListing > ( {
container : containerFor ( server . database . client , 'GroupDirectory' ) ,
id : request.params.id ,
partitionKey : 'pk' ,
} )
const container = containerFor ( server . database . client , 'Groups' )
const group = await getItem < Group > ( {
container : groupContainer ,
container ,
id : request.params.id ,
} )
const combine = async ( group : Group , listing : GroupListing ) = > {
if ( request . viewer ) {
const memberships = await queryItems < GroupMembership > ( {
container : groupContainer ,
query : createQuerySpec (
`
SELECT * FROM Groups g WHERE
g . pk = @pk AND
g . t = @type AND
g . userId = @userId AND
g . pending = false
` ,
{
pk : group.id ,
type : GroupItemType . Membership ,
userId : request.viewer.id ,
}
) ,
logger : request.log ,
} )
if ( memberships . length > 0 ) {
return merge ( group , listing , {
membership : memberships [ 0 ] . membership ,
} )
if ( ! group ) return notFoundError ( reply )
if ( request . viewer ) {
const memberships = await queryItems < GroupMembership > ( {
container ,
query : createQuerySpec (
`
SELECT * FROM Groups g WHERE
g . pk = @pk AND
g . t = @type AND
g . userId = @userId AND
g . pending = false
` ,
{
pk : group.id ,
type : GroupItemType . Membership ,
userId : request.viewer.id ,
}
) ,
logger : request.log ,
} )
if ( memberships . length > 0 ) {
return {
. . . group ,
membership : memberships [ 0 ] . membership ,
}
}
return merge ( group , listing )
}
if ( ! group || ! listing ) return notFoundError ( reply )
return combine ( group , listing )
return group
} )
}
@ -328,7 +322,7 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
} ,
}
server . put < DefaultQuery , DefaultParams , DefaultHeaders , Body > ( '/api /group/:id' , options , async ( request , reply ) = > {
server . put < DefaultQuery , DefaultParams , DefaultHeaders , Body > ( '/v1 /group/:id' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
if ( ! request . viewer ) return unauthorizedError ( reply )
@ -336,14 +330,14 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if ( ! membership || membership . membership !== GroupMembershipType . Admin ) return unauthorizedError ( reply )
const groupContainer = containerFor ( server . database . client , 'Groups' )
const directoryContainer = containerFor ( server . database . client , 'Group Directory' )
const directoryContainer = containerFor ( server . database . client , 'Directory' )
const groupItem = groupContainer . item ( request . params . id , request . params . id )
const groupL istingItem = directoryContainer . item ( request . params . id , GROUP_LISTING_PARTITION_KEY )
const l istingItem = directoryContainer . item ( request . params . id , GROUP_LISTING_PARTITION_KEY )
const { resource : group } = await groupItem . read < Group > ( )
if ( ! group ) return notFoundError ( reply )
const { resource : groupL isting } = await groupL istingItem. read < GroupListing > ( )
const { resource : l isting } = await l istingItem. read < GroupListing > ( )
const {
name ,
@ -365,35 +359,19 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if ( ! group . coverImageUrl && coverImageUrl ) await attachMedia ( mediaContainer , coverImageUrl )
if ( ! group . iconImageUrl && iconImageUrl ) await attachMedia ( mediaContainer , iconImageUrl )
interface Updates {
name? : string
about? : string
registration? : GroupRegistrationType
imageUrl? : string
coverImageUrl? : string
iconImageUrl? : string
theme? : string
}
if ( name ) group . name = name
if ( about ) group . about = about
if ( registration ) group . registration = registration as GroupRegistrationType
if ( imageUrl ) group . imageUrl = imageUrl
if ( coverImageUrl ) group . coverImageUrl = coverImageUrl
if ( iconImageUrl ) group . iconImageUrl = iconImageUrl
if ( theme ) group . theme = theme
let updates : Updates = { }
if ( name ) updates . name = name
if ( about ) updates . about = about
if ( registration ) updates . registration = registration as GroupRegistrationType
if ( imageUrl ) updates . imageUrl = imageUrl
if ( coverImageUrl ) updates . coverImageUrl = coverImageUrl
if ( iconImageUrl ) updates . iconImageUrl = iconImageUrl
if ( theme ) updates . theme = theme
await groupItem . replace < Group > ( group )
await groupItem . replace < Group > ( {
. . . group ,
. . . updates ,
} )
if ( groupListing ) {
await groupListingItem . replace < GroupListing > ( {
. . . groupListing ,
. . . updates ,
} )
if ( listing ) {
if ( registration ) listing . registration = registration as GroupRegistrationType
await listingItem . replace < GroupListing > ( listing )
}
await groupContainer . items . create < GroupLog > ( {
@ -443,7 +421,7 @@ function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespo
} ,
}
server . post < DefaultQuery , Params , DefaultHeaders , Body > ( '/api /group/:id/block' , options , async ( request , reply ) = > {
server . post < DefaultQuery , Params , DefaultHeaders , Body > ( '/v1 /group/:id/block' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
if ( ! request . viewer ) return unauthorizedError ( reply )
@ -497,7 +475,7 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
} ,
}
server . post < DefaultQuery , Params , DefaultHeaders , DefaultBody > ( '/api /group/:id/unblock' , options , async ( request , reply ) = > {
server . post < DefaultQuery , Params , DefaultHeaders , DefaultBody > ( '/v1 /group/:id/unblock' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
if ( ! request . viewer ) return unauthorizedError ( reply )
@ -583,7 +561,7 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
} ,
}
server . post < DefaultQuery , Params , Headers , DefaultBody > ( '/api /group/:id/activate' , options , async ( request , reply ) = > {
server . post < DefaultQuery , Params , Headers , DefaultBody > ( '/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 )
@ -603,14 +581,13 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
status : GroupStatus.Paid ,
} )
const directoryContainer = containerFor ( server . database . client , 'Group Directory' )
const directoryContainer = containerFor ( server . database . client , 'Directory' )
const listingItem = directoryContainer . item ( request . params . id , GROUP_LISTING_PARTITION_KEY )
const { resource : listing } = await listingItem . read < GroupListing > ( )
if ( ! listing ) {
await directoryContainer . items . create < GroupListing > ( {
id : group.id ,
name : group.name ,
pk : GROUP_LISTING_PARTITION_KEY ,
registration : group.registration ,
members : 1 ,
@ -656,7 +633,7 @@ function listRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
properties : {
groups : {
type : 'array' ,
items : groupListing Schema ,
items : groupSchema ,
} ,
continuation : { type : 'string' } ,
}
@ -666,18 +643,17 @@ function listRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
} ,
}
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/api /groups' , options , async ( request , reply ) = > {
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/v1 /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 } ' `
}
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 = ' ${ GROUP_LISTING_PARTITION_KEY } ' ${ registrationString } ORDER BY d. ${ sort } ` ,
const directoryContainer = containerFor ( server . database . client , 'Directory' )
const { resources : groups , requestCharge , continuation : newContinuation } = await directoryContainer . items . query < GroupListing > (
` SELECT d.id FROM Directory d WHERE d.pk = ' ${ GROUP_LISTING_PARTITION_KEY } ' ${ registrationString } ORDER BY d. ${ sort } ` ,
{
maxItemCount : 40 ,
continuation ,
@ -687,7 +663,14 @@ function listRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
request . log . trace ( 'Query: %d' , requestCharge )
return {
groups ,
groups : await queryItems < Group > ( {
container : containerFor ( server . database . client , 'Groups' ) ,
query : createQuerySpec (
'SELECT * FROM Groups g WHERE ARRAY_CONTAINS(@ids, g.id)' ,
{ ids : groups.map ( g = > g . id ) }
) ,
logger : request.log ,
} ) ,
continuation : newContinuation ,
}
} )
@ -730,7 +713,7 @@ function membersRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
} ,
}
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/api /group/:id/members' , options , async ( request , reply ) = > {
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/v1 /group/:id/members' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
const groupContainer = containerFor ( server . database . client , 'Groups' )
@ -745,8 +728,7 @@ function membersRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
typeString = ` AND g.membership = ' ${ type } ' `
}
const container = containerFor ( server . database . client , 'Groups' )
const { resources : memberships , requestCharge , continuation : newContinuation } = await container . items . query < GroupMembership > (
const { resources : memberships , requestCharge , continuation : newContinuation } = await groupContainer . items . query < GroupMembership > (
` SELECT g.userId, g.membership FROM Groups g WHERE g.pk = ' ${ group . id } ' AND g.t = ' ${ GroupItemType . Membership } ' ${ typeString } ORDER BY g.created DESC ` ,
{
maxItemCount : 100 ,
@ -801,7 +783,7 @@ function createInvitationRoute(server: FastifyInstance<Server, IncomingMessage,
} ,
}
server . post < DefaultQuery , DefaultParams , DefaultHeaders , Body > ( '/api /group/:id/invitation' , options , async ( request , reply ) = > {
server . post < DefaultQuery , DefaultParams , DefaultHeaders , Body > ( '/v1 /group/:id/invitation' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
if ( ! request . viewer ) return unauthorizedError ( reply )
@ -885,7 +867,7 @@ function invitationsRoute(server: FastifyInstance<Server, IncomingMessage, Serve
} ,
}
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/api /group/:id/invitations' , options , async ( request , reply ) = > {
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/v1 /group/:id/invitations' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
if ( ! request . viewer ) return unauthorizedError ( reply )
@ -966,7 +948,7 @@ function logsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
} ,
}
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/api /group/:id/logs' , options , async ( request , reply ) = > {
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/v1 /group/:id/logs' , options , async ( request , reply ) = > {
if ( ! server . database ) return serverError ( reply )
if ( ! request . viewer ) return unauthorizedError ( reply )
@ -1003,6 +985,177 @@ function logsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
} )
}
function pendingMembersRoute ( server : FastifyInstance < Server , IncomingMessage , ServerResponse > ) {
interface Query {
continuation? : string
}
const options : RouteShorthandOptions = {
schema : {
description : 'Get pending Group members.' ,
tags : [ 'group' , 'user' ] ,
querystring : {
type : 'object' ,
properties : {
continuation : { type : 'string' } ,
} ,
} ,
response : {
200 : {
description : 'Successful response.' ,
type : 'object' ,
properties : {
users : {
type : 'array' ,
items : userSchema ,
} ,
continuation : { type : 'string' } ,
}
} ,
400 : errorSchema ,
}
} ,
}
server . get < Query , DefaultParams , DefaultHeaders , DefaultBody > ( '/v1/group/members/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 ) return unauthorizedError ( reply )
if ( ! viewer . group ) return badRequestError ( reply )
const membership = await getUserMembership ( server . database . client , viewer . id , request . log )
if ( ! membership ) return serverError ( reply )
if ( membership . membership !== GroupMembershipType . Admin ) return forbiddenError ( reply )
const groupContainer = containerFor ( server . database . client , 'Groups' )
const { resources : memberships , requestCharge , continuation : newContinuation } = await groupContainer . items . query < GroupMembership > (
` SELECT g.userId, g.intro FROM Groups g WHERE g.pk = ' ${ viewer . group . id } ' AND g.t = ' ${ GroupItemType . Membership } ' AND g.pending = true ORDER BY g.created DESC ` ,
{
maxItemCount : 100 ,
continuation : request.query.continuation ,
}
) . fetchAll ( )
request . log . trace ( 'Query: %d' , requestCharge )
const users = await getUsers ( server . database . client , memberships . map ( membership = > membership . userId ) , request . log )
return {
users : users.map ( user = > {
const m = memberships . find ( membership = > membership . userId === user . id )
return {
. . . user ,
intro : m ? m.intro : undefined ,
}
} ) ,
continuation : newContinuation ,
}
} )
}
function approveMemberRoute ( server : FastifyInstance < Server , IncomingMessage , ServerResponse > ) {
interface Params {
id : string
}
const options : RouteShorthandOptions = {
schema : {
description : 'Approve a pending Group member.' ,
tags : [ 'group' , 'user' ] ,
params : {
type : 'object' ,
properties : {
id : { type : 'string' } ,
} ,
} ,
response : {
200 : {
description : 'Member approved.' ,
type : 'object' ,
} ,
400 : errorSchema ,
} ,
} ,
}
server . post < DefaultQuery , Params , DefaultHeaders , DefaultBody > ( '/v1/group/member/:id/approve' , 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 ) return unauthorizedError ( reply )
if ( ! viewer . group ) return badRequestError ( reply )
const viewerMembership = await getUserMembership ( server . database . client , viewer . id , request . log )
if ( ! viewerMembership ) return serverError ( reply )
if ( viewerMembership . membership !== GroupMembershipType . Admin ) return forbiddenError ( reply )
const membership = await getUserMembership ( server . database . client , request . params . id , request . log )
if ( ! membership ) return notFoundError ( reply )
if ( ! membership . pending ) return badRequestError ( reply )
const groupContainer = containerFor ( server . database . client , 'Groups' )
const groupItem = groupContainer . item ( viewer . group . id , viewer . group . id )
const membershipItem = groupContainer . item ( membership . id ! , membership . pk )
await groupItem . replace < Group > ( {
. . . viewer . group ,
members : viewer.group.members + 1 ,
} )
await membershipItem . replace < GroupMembership > ( {
. . . membership ,
pending : false ,
} )
} )
}
function rejectMemberRoute ( server : FastifyInstance < Server , IncomingMessage , ServerResponse > ) {
interface Params {
id : string
}
const options : RouteShorthandOptions = {
schema : {
description : 'Reject a pending Group member.' ,
tags : [ 'group' , 'user' ] ,
params : {
type : 'object' ,
properties : {
id : { type : 'string' } ,
} ,
} ,
response : {
200 : {
description : 'Member rejected.' ,
type : 'object' ,
} ,
400 : errorSchema ,
} ,
} ,
}
server . post < DefaultQuery , Params , DefaultHeaders , DefaultBody > ( '/v1/group/member/:id/reject' , 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 ) return unauthorizedError ( reply )
if ( ! viewer . group ) return badRequestError ( reply )
const viewerMembership = await getUserMembership ( server . database . client , viewer . id , request . log )
if ( ! viewerMembership ) return serverError ( reply )
if ( viewerMembership . membership !== GroupMembershipType . Admin ) return forbiddenError ( reply )
const membership = await getUserMembership ( server . database . client , request . params . id , request . log )
if ( ! membership ) return notFoundError ( reply )
if ( ! membership . pending ) return badRequestError ( reply )
// TODO
} )
}
const plugin : Plugin < Server , IncomingMessage , ServerResponse , PluginOptions > = async server = > {
availabilityRoute ( server )
createRoute ( server )
@ -1016,6 +1169,9 @@ const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = a
createInvitationRoute ( server )
invitationsRoute ( server )
logsRoute ( server )
pendingMembersRoute ( server )
approveMemberRoute ( server )
rejectMemberRoute ( server )
}
export default plugin