[ABANDONED] API server for Flexor social network.
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.
 

453 lines
14 KiB

import {
FastifyInstance,
Plugin,
DefaultQuery,
DefaultParams,
RouteShorthandOptions,
DefaultHeaders,
DefaultBody,
} from 'fastify'
import { Server, IncomingMessage, ServerResponse } from 'http'
import { unauthorizedError, serverError, notFoundError, badRequestError } from '../../lib/errors'
import { getUserBlocks } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import { MAX_NAME_LENGTH } from '../../constants'
import { userSchema, selfSchema, errorSchema } from '../../schemas'
import {
User,
UserSubscription,
UserBlock,
GroupBlock,
UserPrivacyType,
UserItemType,
GroupItemType,
BlockType,
} 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,
})
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: selfSchema,
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 } = await viewerItem.read<User>()
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 })
if (!user) return notFoundError(reply)
if (request.viewer) {
const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id })
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 })
const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id })
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 = @type`, {
user: user.id,
viewer: viewer.id,
type: UserItemType.Subscription,
})
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 UserPrivacyType.Private:
return unauthorizedError(reply)
case UserPrivacyType.Group:
if (user.group !== viewer.group) return unauthorizedError(reply)
case UserPrivacyType.Subscribers:
pending = true
break
}
const blockQuery = createQuerySpec(`
SELECT g.id FROM Groups g WHERE
g.pk = @viewerGroup AND
g.t = @type AND
g.userId = @user AND
(g.blockedId = @viewer OR g.blockedId = @viewerGroup)
`, {
user: user.id,
viewerGroup: viewer.group.id,
type: GroupItemType.Block,
})
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: UserItemType.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 })
const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id })
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 = @type`, {
user: user.id,
viewer: viewer.id,
type: UserItemType.Subscription,
})
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 })
if (!user) return notFoundError(reply)
if (!user.group) return badRequestError(reply)
await userContainer.items.create<UserBlock>({
blockedId: user.id,
pk: request.viewer.id,
t: UserItemType.Block,
blockType: BlockType.User,
description: request.body.description,
created: Date.now(),
})
await containerFor(server.database.client, 'Groups').items.create<GroupBlock>({
pk: user.group.id,
t: GroupItemType.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 })
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.t = @type`, {
pk: request.viewer.id,
blocked: user.id,
type: UserItemType.Block,
})
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.t = @type`,
{
pk: user.group.id,
blocked: user.id,
viewer: request.viewer.id,
type: GroupItemType.Block,
}
)
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