Dwayne Harris 5 years ago
parent
commit
9a4afa524d
  1. 5
      src/constants.ts
  2. 9
      src/lib/errors.ts
  3. 46
      src/plugins/api/authentication.ts
  4. 9
      src/plugins/api/groups.ts
  5. 45
      src/plugins/api/posts.ts
  6. 28
      src/plugins/api/users.ts
  7. 9
      src/schemas.ts

5
src/constants.ts

@ -1,3 +1,6 @@
export const MIN_ID_LENGTH = 5
export const MAX_ID_LENGTH = 40
export const MAX_NAME_LENGTH = 80
export const SUBSCRIBER_BATCH_SIZE = 100
export const MIN_PASSWORD_LENGTH = 8
export const SHORT_TEXT_LENGTH = 100
export const SUBSCRIBER_MAX_SIZE = 100

9
src/lib/errors.ts

@ -7,15 +7,6 @@ interface IHttpError {
field?: string
}
export const errorSchema: JSONSchema = {
type: 'object',
properties: {
statusCode: { type: 'number' },
error: { type: 'string' },
field: { type: ['string', 'null'] },
}
}
export function badRequestError(reply: FastifyReply<ServerResponse>, error: string = 'Bad Request', field?: string): IHttpError {
const statusCode = 400
reply.code(statusCode)

46
src/plugins/api/authentication.ts

@ -10,11 +10,12 @@ import {
import { Server, IncomingMessage, ServerResponse } from 'http'
import { tokenResponseSchema, userSchema } from '../../schemas'
import { MIN_ID_LENGTH, MAX_ID_LENGTH, MAX_NAME_LENGTH, MIN_PASSWORD_LENGTH } from '../../constants'
import { tokenResponseSchema, userSchema, errorSchema } from '../../schemas'
import { createAccessToken, createRefreshToken } from '../../lib/authentication'
import { hashPassword, verifyPassword, JWT } from '../../lib/crypto'
import { containerFor, getItem, normalize } from '../../lib/database'
import { errorSchema, badRequestError, unauthorizedError, serverError } from '../../lib/errors'
import { badRequestError, unauthorizedError, serverError } from '../../lib/errors'
import { tokenFromHeader } from '../../lib/http'
import { IUser, IUserToken, IGroup, IGroupPartial } from '../../types/collections'
@ -27,7 +28,7 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
name: string
email: string
password: string
groupId: string
group: string
}
const options: RouteShorthandOptions = {
@ -36,11 +37,24 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
type: 'object',
required: ['id', 'email', 'password'],
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
password: { type: 'string' },
groupId: { type: 'string' },
id: {
type: 'string',
minLength: MIN_ID_LENGTH,
maxLength: MAX_ID_LENGTH,
},
name: {
type: 'string',
maxLength: MAX_NAME_LENGTH,
},
email: {
type: 'string',
format: 'email',
},
password: {
type: 'string',
minLength: MIN_PASSWORD_LENGTH,
},
group: { type: 'string' },
},
},
response: {
@ -53,17 +67,9 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/register', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const { name, email, password, groupId } = request.body
const { name, email, password } = request.body
const id = normalize(request.body.id)
if (!id || id === '') return badRequestError(reply, 'id is required', 'id')
if (id.length < 5 || id.length > 20) return badRequestError(reply, 'id must be between 5 and 20 characters', 'id')
if (name && name.length > 40) return badRequestError(reply, 'name must be less than 40 characters', 'name')
if (!email || email === '') return badRequestError(reply, 'email is required', 'email')
if (email.length < 5) return badRequestError(reply, 'email must be at least 5 characters', 'email')
if (!password || password === '') return badRequestError(reply, 'password is required', 'password')
if (password.length < 5) return badRequestError(reply, 'password must be at least 5 characters', 'password')
const userContainer = containerFor(server.database.client, 'Users')
const existingUser = await getItem<IUser>(userContainer, id, request.log)
if (existingUser) return badRequestError(reply, 'User id already taken', 'id')
@ -71,8 +77,8 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
let userPending = false
let groupPartial: IGroupPartial | undefined
if (groupId) {
const group = await getItem<IGroup>(containerFor(server.database.client, 'Groups'), groupId, request.log)
if (request.body.group) {
const group = await getItem<IGroup>(containerFor(server.database.client, 'Groups'), request.body.group, request.log)
if (!group) return badRequestError(reply, 'Group not found', 'groupId')
if (!group.open) return badRequestError(reply, 'Group registration closed', 'groupId')
@ -100,6 +106,7 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
subscriberCount: 0,
subscribedCount: 0,
pending: userPending,
requiresApproval: false,
privacy: 'public',
paid: false,
about: '',
@ -247,6 +254,7 @@ function selfRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
schema: {
response: {
200: userSchema,
400: errorSchema,
},
},
}

9
src/plugins/api/groups.ts

@ -9,6 +9,8 @@ import {
import { Server, IncomingMessage, ServerResponse } from 'http'
import { MIN_ID_LENGTH, MAX_NAME_LENGTH } from '../../constants'
import { errorSchema } from '../../schemas'
import { unauthorizedError, badRequestError, notFoundError, serverError } from '../../lib/errors'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import { IUser, IGroup, IGroupMembership, IUserBlock, IGroupBlock } from '../../types/collections'
@ -29,7 +31,11 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
type: 'object',
required: ['name', 'open', 'requiresApproval'],
properties: {
name: { type: 'string' },
name: {
type: 'string',
minLength: MIN_ID_LENGTH,
maxLength: MAX_NAME_LENGTH,
},
about: { type: 'string' },
open: { type: 'boolean' },
requiresApproval: { type: 'boolean' },
@ -42,6 +48,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
id: { type: 'string' },
},
},
400: errorSchema,
},
},
}

45
src/plugins/api/posts.ts

@ -10,10 +10,11 @@ import {
import { Server, IncomingMessage, ServerResponse } from 'http'
import { userSchema, postSchema } from '../../schemas'
import { SHORT_TEXT_LENGTH, SUBSCRIBER_MAX_SIZE } from '../../constants'
import { userSchema, postSchema, errorSchema } from '../../schemas'
import { unauthorizedError, serverError, badRequestError, notFoundError } from '../../lib/errors'
import { trimContent, createPostId } from '../../lib/util'
import { getUsers, getSubscriptions, getUserBlocks } from '../../lib/collections'
import { getUsers, getApprovedSubscriptions, getUserBlocks } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import {
@ -47,14 +48,20 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
required: ['visible'],
properties: {
text: { type: 'string' },
cover: { type: 'string' },
cover: {
type: 'string',
maxLength: SHORT_TEXT_LENGTH,
},
visible: { type: 'boolean' },
status: {
type: 'object',
required: ['date'],
properties: {
imageUrl: { type: 'string' },
text: { type: 'string' },
text: {
type: 'string',
maxLength: SHORT_TEXT_LENGTH,
},
date: { type: 'number' },
},
},
@ -65,8 +72,14 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
required: ['imageUrl'],
properties: {
imageUrl: { type: 'string' },
caption: { type: 'string' },
cover: { type: 'string' },
caption: {
type: 'string',
maxLength: SHORT_TEXT_LENGTH,
},
cover: {
type: 'string',
maxLength: SHORT_TEXT_LENGTH,
},
},
},
},
@ -80,6 +93,7 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
id: { type: 'string' },
},
},
400: errorSchema,
},
},
}
@ -96,7 +110,6 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
const viewer = await getItem<IUser>(userContainer, request.viewer.id, request.log)
if (!viewer) return serverError(reply)
if (viewer.pending) return badRequestError(reply, 'User requires approval')
if (!viewer.group) return badRequestError(reply, 'User must belong to a group')
@ -147,7 +160,7 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
const query = createQuerySpec(`SELECT u.id FROM Users u WHERE u.partitionKey = @partitionKey AND u.type = 'subscription'`, { partitionKey: request.viewer.id })
const subscribers = await queryItems<IUserSubscription>(userContainer, query, request.log)
if (subscribers.length < 100) {
if (subscribers.length < SUBSCRIBER_MAX_SIZE) {
for (const subscriber of subscribers) {
await userContainer.items.create<IUserTimelinePost>({
postId,
@ -188,6 +201,7 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
},
},
},
400: errorSchema,
},
},
}
@ -203,22 +217,28 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
if (!user.group) return notFoundError(reply)
switch (user.privacy) {
case 'approve':
case 'private':
return unauthorizedError(reply)
case 'subscribers': {
if (!request.viewer) return unauthorizedError(reply)
const subscriptions = await getSubscriptions(server.database.client, user.id, request.viewer.id, request.log)
const subscriptions = await getApprovedSubscriptions(server.database.client, user.id, request.viewer.id, request.log)
if (subscriptions.length === 0) return unauthorizedError(reply)
break
case 'group':
}
case 'group': {
if (!request.viewer) return unauthorizedError(reply)
const viewer = await getItem<IUser>(userContainer, request.viewer.id, request.log)
if (!viewer) return serverError(reply)
if (!viewer.group) return unauthorizedError(reply)
if (viewer.group.id !== user.group.id) return unauthorizedError(reply)
const subscriptions = await getApprovedSubscriptions(server.database.client, user.id, request.viewer.id, request.log)
if (viewer.group.id !== user.group.id && subscriptions.length === 0) return unauthorizedError(reply)
break
}
}
if (request.viewer) {
@ -276,6 +296,7 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
}
},
},
400: errorSchema,
},
},
}

28
src/plugins/api/users.ts

@ -11,8 +11,9 @@ import {
import { Server, IncomingMessage, ServerResponse } from 'http'
import { MAX_NAME_LENGTH } from '../../constants'
import { userSchema } from '../../schemas'
import { userSchema, errorSchema } from '../../schemas'
import { unauthorizedError, serverError, notFoundError, badRequestError } from '../../lib/errors'
import { getUserBlocks } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem } from '../../lib/database'
import { IUser, IUserSubscription, IUserBlock, IGroupBlock, IUserPrivacyType } from '../../types/collections'
@ -55,6 +56,7 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
},
response: {
200: userSchema,
400: errorSchema,
},
},
}
@ -110,10 +112,14 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
id: { type: 'string' },
},
},
response: {
200: userSchema,
400: errorSchema,
},
},
}
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id', options, async (request, reply) => {
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')
@ -121,10 +127,16 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
if (!user) return notFoundError(reply)
switch (user.privacy) {
case 'private':
break
if (request.viewer) {
const viewer = await getItem<IUser>(userContainer, request.viewer.id, 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
})
}
@ -141,6 +153,9 @@ function subscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
id: { type: 'string' },
},
},
response: {
400: errorSchema,
},
},
}
@ -268,6 +283,9 @@ function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespo
description: { type: 'string' },
},
},
response: {
400: errorSchema,
}
},
}

9
src/schemas.ts

@ -41,3 +41,12 @@ export const userSchema: JSONSchema = {
subscription: { type: 'string' },
},
}
export const errorSchema: JSONSchema = {
type: 'object',
properties: {
statusCode: { type: 'number' },
error: { type: 'string' },
field: { type: 'string' },
}
}
Loading…
Cancel
Save