Dwayne Harris 4 years ago
parent
commit
022de50e64
  1. 2
      .vscode/settings.json
  2. 3
      src/constants.ts
  3. 51
      src/lib/collections.ts
  4. 18
      src/lib/errors.ts
  5. 36
      src/plugins/api/apps.ts
  6. 42
      src/plugins/api/authentication.ts
  7. 348
      src/plugins/api/groups.ts
  8. 6
      src/plugins/api/media.ts
  9. 25
      src/plugins/api/posts.ts
  10. 100
      src/plugins/api/users.ts
  11. 7
      src/schemas.ts
  12. 23
      src/types/collections.ts

2
.vscode/settings.json

@ -1,3 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
"typescript.tsdk": "node_modules/typescript/lib"
}

3
src/constants.ts

@ -5,7 +5,8 @@ export const MIN_PASSWORD_LENGTH = 8
export const SHORT_TEXT_LENGTH = 100
export const SUBSCRIBER_MAX_SIZE = 100
export const GROUP_LISTING_PARTITION_KEY = 'pk'
export const USER_LISTING_PARTITION_KEY = 'upk'
export const GROUP_LISTING_PARTITION_KEY = 'gpk'
export const APP_PARTITION_KEY = 'apk'
export const INSTALLATION_PARTITION_KEY = 'ipk'
export const MEDIA_PARTITION_KEY = 'pk'

51
src/lib/collections.ts

@ -2,10 +2,11 @@ import { CosmosClient } from '@azure/cosmos'
import { Logger } from 'fastify'
import compact from 'lodash/compact'
import uniq from 'lodash/uniq'
import { DatabaseItem } from '../types'
import { containerFor, createQuerySpec, queryItems, getItem } from './database'
import { User, UserSubscription, UserBlock, Group, GroupMembership, UserItemType, GroupItemType } from '../types/collections'
import { USER_LISTING_PARTITION_KEY } from '../constants'
import { DatabaseItem } from '../types'
import { User, UserSubscription, UserBlock, Group, GroupMembership, UserItemType, GroupItemType, UserListing } from '../types/collections'
export async function getUser(client: CosmosClient, id: string): Promise<User | undefined> {
const user = await getItem<User>({
@ -89,7 +90,7 @@ export async function getUserBlocks(client: CosmosClient, from: string, to: stri
})
}
export async function getUserMembership(client: CosmosClient, userId: string, logger?: Logger): Promise<GroupMembership | undefined> {
export async function getUserMembership(client: CosmosClient, userId: string, logger?: Logger) {
const user = await getItem<User>({
container: containerFor(client, 'Users'),
id: userId,
@ -117,3 +118,47 @@ export async function getUserMembership(client: CosmosClient, userId: string, lo
if (memberships.length > 0) return memberships[0]
}
export async function getUserIdFromEmail(client: CosmosClient, email: string, logger?: Logger) {
const listings = await queryItems<UserListing>({
container: containerFor(client, 'Directory'),
query: createQuerySpec('SELECT d.id FROM Directory d WHERE d.pk = @pk AND d.email = @email', {
pk: USER_LISTING_PARTITION_KEY,
email,
}),
logger,
})
const listing = listings[0]
if (listing) return listing.id
}
export async function getUserIdFromPhone(client: CosmosClient, phone: string, logger?: Logger) {
const listings = await queryItems<UserListing>({
container: containerFor(client, 'Directory'),
query: createQuerySpec('SELECT d.id FROM Directory d WHERE d.pk = @pk AND d.phone = @phone', {
pk: USER_LISTING_PARTITION_KEY,
phone,
}),
logger,
})
const listing = listings[0]
if (listing) return listing.id
}
export function userIsValid(user?: User) {
if (!user) return false
if (user.pending) return false
if (!user.groupId) return false
if (!user.active) return false
return true
}
export async function userIdIsValid(client: CosmosClient, id: string) {
return userIsValid(await getItem<User>({
container: containerFor(client, 'Users'),
id,
}))
}

18
src/lib/errors.ts

@ -27,24 +27,20 @@ export function badRequestFormError(reply: FastifyReply<ServerResponse>, field:
export function unauthorizedError(reply: FastifyReply<ServerResponse>): HttpError {
reply.code(401)
return { message: 'Unauthorized' }
}
return {
message: 'Unauthorized',
}
export function forbiddenError(reply: FastifyReply<ServerResponse>): HttpError {
reply.code(403)
return { message: 'Forbidden' }
}
export function notFoundError(reply: FastifyReply<ServerResponse>): HttpError {
reply.code(404)
return {
message: 'Not Found',
}
return { message: 'Not Found' }
}
export function serverError(reply: FastifyReply<ServerResponse>, message: string = 'Server Error'): HttpError {
reply.code(500)
return {
message,
}
return { message }
}

36
src/plugins/api/apps.ts

@ -12,7 +12,7 @@ import { Server, IncomingMessage, ServerResponse } from 'http'
import pick from 'lodash/pick'
import { appSchema, errorSchema } from '../../schemas'
import { getUsers } from '../../lib/collections'
import { getUsers, userIdIsValid, userIsValid } from '../../lib/collections'
import { generateString } from '../../lib/crypto'
import { containerFor, getItem, normalize, queryItems, createQuerySpec } from '../../lib/database'
import { unauthorizedError, serverError, badRequestError, notFoundError } from '../../lib/errors'
@ -20,7 +20,6 @@ import { attachMedia, deleteMedia } from '../../lib/media'
import { createInstallationId } from '../../lib/utils'
import { APP_PARTITION_KEY, MAX_NAME_LENGTH, INSTALLATION_PARTITION_KEY } from '../../constants'
import { App, User, Installation, InstallationSettings } from '../../types/collections'
import { PluginOptions } from '../../types'
@ -57,7 +56,7 @@ function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, Serv
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/app/available', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/v1/app/available', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const id = normalize(request.body.name)
@ -109,7 +108,7 @@ function appsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
},
}
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/apps', options, async (request, reply) => {
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/v1/apps', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const { sort = 'created', continuation } = request.query
@ -146,7 +145,7 @@ function selfAppsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
const options: RouteShorthandOptions = {
schema: {
description: 'Get installed Apps.',
tags: ['app'],
tags: ['app', 'authentication'],
querystring: {
type: 'object',
properties: {
@ -169,7 +168,7 @@ function selfAppsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
},
}
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/self/apps', options, async (request, reply) => {
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/v1/self/apps', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -250,9 +249,10 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/app', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/v1/app', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
if (!(await userIdIsValid(server.database.client, request.viewer.id))) return unauthorizedError(reply)
const container = containerFor(server.database.client, 'Apps')
const id = normalize(request.body.name)
@ -379,9 +379,10 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
},
}
server.put<DefaultQuery, Params, DefaultHeaders, Body>('/api/app/:id', options, async (request, reply) => {
server.put<DefaultQuery, Params, DefaultHeaders, Body>('/v1/app/:id', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
if (!(await userIdIsValid(server.database.client, request.viewer.id))) return unauthorizedError(reply)
const container = containerFor(server.database.client, 'Apps')
const mediaContainer = containerFor(server.database.client, 'Media')
@ -480,7 +481,7 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
},
}
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/app/:id', options, async (request, reply) => {
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/v1/app/:id', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const app = await getItem<App>({
@ -533,7 +534,7 @@ function installRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
},
}
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/app/:id/install', options, async (request, reply) => {
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/v1/app/:id/install', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -547,6 +548,7 @@ function installRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
if (!app) return notFoundError(reply)
const { resource: viewer } = await viewerItem.read<User>()
if (!userIsValid(viewer)) return unauthorizedError(reply)
const installations = await queryItems<Installation>({
container: appContainer,
@ -619,7 +621,7 @@ function uninstallRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
},
}
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/app/:id/uninstall', options, async (request, reply) => {
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/v1/app/:id/uninstall', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -633,6 +635,7 @@ function uninstallRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
if (!app) return notFoundError(reply)
const { resource: viewer } = await viewerItem.read<User>()
if (!userIsValid(viewer)) return unauthorizedError(reply)
const installations = await queryItems<Installation>({
container: appContainer,
@ -675,7 +678,7 @@ function installationsRoute(server: FastifyInstance<Server, IncomingMessage, Ser
const options: RouteShorthandOptions = {
schema: {
description: 'Get authenticated user Installations.',
tags: ['app'],
tags: ['app', 'authentication'],
response: {
200: {
description: 'Successful response.',
@ -703,7 +706,7 @@ function installationsRoute(server: FastifyInstance<Server, IncomingMessage, Ser
},
}
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/api/installations', options, async (request, reply) => {
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/v1/installations', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -713,6 +716,7 @@ function installationsRoute(server: FastifyInstance<Server, IncomingMessage, Ser
})
if (!viewer) return unauthorizedError(reply)
if (!userIsValid(viewer)) return unauthorizedError(reply)
const container = containerFor(server.database.client, 'Apps')
@ -782,7 +786,7 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
},
}
server.post<DefaultQuery, Params, Headers, DefaultBody>('/api/app/:id/activate', options, async (request, reply) => {
server.post<DefaultQuery, Params, Headers, DefaultBody>('/v1/app/:id/activate', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (request.headers.adminkey !== process.env.ADMIN_KEY) return serverError(reply)
@ -835,7 +839,7 @@ function setPreinstallRoute(server: FastifyInstance<Server, IncomingMessage, Ser
},
}
server.post<DefaultQuery, Params, Headers, DefaultBody>('/api/app/:id/preinstall', options, async (request, reply) => {
server.post<DefaultQuery, Params, Headers, DefaultBody>('/v1/app/:id/preinstall', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (request.headers.adminkey !== process.env.ADMIN_KEY) return serverError(reply)
@ -883,7 +887,7 @@ function updateSettingsRoute(server: FastifyInstance<Server, IncomingMessage, Se
},
}
server.put<DefaultQuery, Params, DefaultHeaders, Body>('/api/installation/:id/settings', options, async (request, reply) => {
server.put<DefaultQuery, Params, DefaultHeaders, Body>('/v1/installation/:id/settings', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)

42
src/plugins/api/authentication.ts

@ -13,7 +13,7 @@ import { Server, IncomingMessage, ServerResponse } from 'http'
import { MIN_ID_LENGTH, MAX_ID_LENGTH, MAX_NAME_LENGTH, MIN_PASSWORD_LENGTH, INSTALLATION_PARTITION_KEY } from '../../constants'
import { tokenResponseSchema, selfSchema, errorSchema } from '../../schemas'
import { createAccessToken, createRefreshToken } from '../../lib/authentication'
import { getUser } from '../../lib/collections'
import { getUser, getUserIdFromEmail, getUserIdFromPhone } from '../../lib/collections'
import { hashPassword, comparePassword, JWT } from '../../lib/crypto'
import { containerFor, getItem, queryItems, normalize, createQuerySpec } from '../../lib/database'
import { badRequestError, badRequestFormError, unauthorizedError, serverError } from '../../lib/errors'
@ -46,17 +46,19 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
requiresApproval: boolean
privacy: string
about?: string
phone?: string
imageUrl?: string
coverImageUrl?: string
theme: string
group?: string
invitation?: string
intro?: string
}
const options: RouteShorthandOptions = {
schema: {
description: 'Create a new User.',
tags: ['authentication'],
tags: ['authentication', 'user'],
body: {
type: 'object',
required: ['id', 'email', 'password', 'theme'],
@ -84,11 +86,13 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
enum: ['public', 'group', 'subscribers', 'private'],
},
about: { type: 'string' },
phone: { type: 'string' },
imageUrl: { type: 'string' },
coverImageUrl: { type: 'string' },
theme: { type: 'string' },
group: { type: 'string' },
invitation: { type: 'string' },
intro: { type: 'string' },
},
},
response: {
@ -98,11 +102,13 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/register', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/v1/register', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const { name, email, password, requiresApproval, privacy, about, imageUrl, coverImageUrl, theme, invitation: code } = request.body
const { name, email, password, requiresApproval, privacy, about, phone, imageUrl, coverImageUrl, theme, invitation: code, intro } = request.body
const id = normalize(request.body.id)
const emailT = email.trim()
const phoneT = phone ? phone.trim() : undefined
const userContainer = containerFor(server.database.client, 'Users')
const groupContainer = containerFor(server.database.client, 'Groups')
@ -110,6 +116,14 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
const existingUser = await getItem<User>({ container: userContainer, id })
if (existingUser) return badRequestFormError(reply, 'id', 'User id already taken')
const emailListingId = await getUserIdFromEmail(server.database.client, emailT, request.log)
if (emailListingId) return badRequestFormError(reply, 'email', 'Email address already used')
if (phoneT) {
const phoneListingId = await getUserIdFromPhone(server.database.client, phoneT, request.log)
if (phoneListingId) return badRequestFormError(reply, 'phone', 'Phone number already used')
}
let userPending = false
let invitation: GroupInvitation | undefined
let group: Group | undefined
@ -179,6 +193,8 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
about,
email,
emailVerified: false,
phone,
phoneVerified: false,
passwordHash: await hashPassword(password),
imageUrl,
coverImageUrl,
@ -197,7 +213,6 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
pending: userPending,
requiresApproval,
privacy: privacy as UserPrivacyType,
paid: false,
active: true,
created: Date.now(),
}
@ -215,6 +230,7 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
pending: userPending,
membership: GroupMembershipType.Member,
invitation: code,
intro,
created: Date.now(),
})
@ -228,6 +244,14 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
active: invitation.limit ? uses > invitation.limit : true,
})
}
if (!userPending) {
const groupItem = groupContainer.item(group.id, group.id)
await groupItem.replace<Group>({
...group,
members: group.members + 1,
})
}
}
const mediaContainer = containerFor(server.database.client, 'Media')
@ -268,7 +292,7 @@ function authenticateRoute(server: FastifyInstance<Server, IncomingMessage, Serv
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/authenticate', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/v1/authenticate', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const container = containerFor(server.database.client, 'Users')
@ -325,7 +349,7 @@ function refreshRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
},
}
server.post<DefaultQuery, DefaultParams, Headers, Body>('/api/refresh', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, Headers, Body>('/v1/refresh', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const tokenString = tokenFromHeader(request.headers.authorization)
@ -376,7 +400,7 @@ function selfRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
const options: RouteShorthandOptions = {
schema: {
description: 'Get authenticated User.',
tags: ['authentication'],
tags: ['authentication', 'user'],
response: {
200: selfSchema,
400: errorSchema,
@ -384,7 +408,7 @@ function selfRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
},
}
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/api/self', options, async (request, reply) => {
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/v1/self', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)

348
src/plugins/api/groups.ts

@ -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, groupListingSchema, 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: groupListingSchema,
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, 'GroupDirectory')
const directoryContainer = containerFor(server.database.client, 'Directory')
const groupItem = groupContainer.item(request.params.id, request.params.id)
const groupListingItem = directoryContainer.item(request.params.id, GROUP_LISTING_PARTITION_KEY)
const listingItem = 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>()
const { resource: listing } = await listingItem.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, 'GroupDirectory')
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: groupListingSchema,
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

6
src/plugins/api/media.ts

@ -37,7 +37,7 @@ function getSASRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
},
}
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/api/sas', options, async () => {
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/v1/sas', options, async () => {
return {
sas: generateSAS('arcw', 5),
id: createId(),
@ -77,7 +77,7 @@ function addRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/media', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/v1/media', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const { name, size, type, originalName } = request.body
@ -131,7 +131,7 @@ function deleteRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/media/delete', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/v1/media/delete', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const mediaItem = containerFor(server.database.client, 'Media').item(request.body.name, MEDIA_PARTITION_KEY)

25
src/plugins/api/posts.ts

@ -16,9 +16,9 @@ import { CosmosClient } from '@azure/cosmos'
import { SHORT_TEXT_LENGTH, INSTALLATION_PARTITION_KEY, APP_PARTITION_KEY } from '../../constants'
import { userSchema, postSchema, errorSchema } from '../../schemas'
import { unauthorizedError, serverError, badRequestError, badRequestFormError, notFoundError } from '../../lib/errors'
import { unauthorizedError, serverError, badRequestError, badRequestFormError, notFoundError, forbiddenError } from '../../lib/errors'
import { trimContent, createPostId } from '../../lib/utils'
import { getUsers, getApprovedSubscriptions, getUserBlocks, getUser } from '../../lib/collections'
import { getUsers, getApprovedSubscriptions, getUserBlocks, getUser, userIsValid } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import {
@ -109,8 +109,7 @@ async function createPost(client: CosmosClient, userId: string, appId: string, b
const viewer = await getItem<User>({ container: userContainer, id: userId })
if (!viewer) return serverError(reply)
if (viewer.pending) return badRequestError(reply, 'User requires approval')
if (!viewer.groupId) return badRequestError(reply, 'User must belong to a group')
if (!userIsValid(viewer)) return forbiddenError(reply)
const postId = createPostId()
@ -190,6 +189,14 @@ async function createPost(client: CosmosClient, userId: string, appId: string, b
})
}
const viewerItem = userContainer.item(viewer.id, viewer.id)
await viewerItem.replace<User>({
...viewer,
posts: viewer.posts + 1,
})
// TODO: Figure out how to update Group effeciently
return {
id: postId,
}
@ -214,7 +221,7 @@ function createPostRoute(server: FastifyInstance<Server, IncomingMessage, Server
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, PostBody>('/api/post', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, PostBody>('/v1/post', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -274,7 +281,7 @@ function createAppPostRoute(server: FastifyInstance<Server, IncomingMessage, Ser
},
}
server.post<DefaultQuery, DefaultParams, Headers, PostBody>('/api/app/post', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, Headers, PostBody>('/v1/app/post', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const { timestamp, signature } = request.headers
@ -342,7 +349,7 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
},
}
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/posts', options, async (request, reply) => {
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/v1/user/:id/posts', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const id = normalize(request.params.id)
@ -440,7 +447,7 @@ function timelineRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
},
}
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/timeline', options, async (request, reply) => {
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/v1/timeline', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -521,7 +528,7 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
},
}
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/post/:id', options, async (request, reply) => {
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/v1/post/:id', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const postContainer = containerFor(server.database.client, 'Posts')

100
src/plugins/api/users.ts

@ -10,12 +10,12 @@ import {
import { Server, IncomingMessage, ServerResponse } from 'http'
import { unauthorizedError, serverError, notFoundError, badRequestError } from '../../lib/errors'
import { getUserBlocks, getUser } from '../../lib/collections'
import { unauthorizedError, serverError, notFoundError, badRequestError, badRequestFormError } from '../../lib/errors'
import { getUserBlocks, getUser, getUserIdFromPhone, getUserIdFromEmail } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import { deleteMedia, attachMedia } from '../../lib/media'
import { MAX_NAME_LENGTH } from '../../constants'
import { MAX_NAME_LENGTH, USER_LISTING_PARTITION_KEY } from '../../constants'
import { userSchema, selfSchema, errorSchema, userSettingsSchema } from '../../schemas'
import {
@ -28,6 +28,7 @@ import {
GroupItemType,
BlockType,
UserInverseSubscription,
UserListing,
UserSettings,
} from '../../types/collections'
@ -66,7 +67,7 @@ function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, Serv
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/user/available', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/v1/user/available', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const id = normalize(request.body.name)
@ -86,6 +87,8 @@ function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, Serv
function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Body {
name?: string
email?: string
phone?: string
about?: string
requiresApproval?: boolean
privacy?: UserPrivacyType
@ -106,6 +109,10 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
type: 'string',
maxLength: MAX_NAME_LENGTH,
},
email: {
type: 'string',
format: 'email',
},
about: { type: 'string' },
requiresApproval: { type: 'boolean' },
privacy: {
@ -125,7 +132,7 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
},
}
server.put<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/self', options, async (request, reply) => {
server.put<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/v1/self', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -134,33 +141,74 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if (!viewer) return serverError(reply)
if (request.body.name) {
const name = request.body.name.trim()
if (name !== '') viewer.name = name
const {
name,
email,
phone,
about,
requiresApproval,
privacy,
imageUrl,
coverImageUrl,
theme,
settings,
} = request.body
if (name) viewer.name = name.trim()
if (about) viewer.about = about.trim()
if (email) {
const emailT = email.trim()
if (viewer.email !== emailT) {
const id = await getUserIdFromEmail(server.database.client, emailT)
if (id) return badRequestFormError(reply, 'email', 'Email address already used')
viewer.email = emailT
viewer.emailVerified = false
}
}
if (request.body.about) {
const about = request.body.about.trim()
if (about !== '') viewer.about = about
if (phone) {
const phoneT = phone.trim()
if (viewer.phone !== phoneT) {
const id = await getUserIdFromPhone(server.database.client, phoneT)
if (id) return badRequestFormError(reply, 'phone', 'Phone number already used')
viewer.phone = phoneT
viewer.phoneVerified = false
}
}
if (request.body.requiresApproval !== undefined) viewer.requiresApproval = request.body.requiresApproval
if (request.body.privacy) viewer.privacy = request.body.privacy
if (requiresApproval !== undefined) viewer.requiresApproval = requiresApproval
if (privacy) viewer.privacy = privacy
const mediaContainer = containerFor(server.database.client, 'Media')
if (viewer.imageUrl && !request.body.imageUrl) await deleteMedia(viewer.imageUrl)
if (viewer.coverImageUrl && !request.body.coverImageUrl) await deleteMedia(viewer.coverImageUrl)
if (viewer.imageUrl && !imageUrl) await deleteMedia(viewer.imageUrl)
if (viewer.coverImageUrl && !coverImageUrl) await deleteMedia(viewer.coverImageUrl)
if (!viewer.imageUrl && request.body.imageUrl) await attachMedia(mediaContainer, request.body.imageUrl)
if (!viewer.coverImageUrl && request.body.coverImageUrl) await attachMedia(mediaContainer, request.body.coverImageUrl)
if (!viewer.imageUrl && imageUrl) await attachMedia(mediaContainer, imageUrl)
if (!viewer.coverImageUrl && coverImageUrl) await attachMedia(mediaContainer, coverImageUrl)
if (request.body.imageUrl) viewer.imageUrl = request.body.imageUrl
if (request.body.coverImageUrl) viewer.coverImageUrl = request.body.coverImageUrl
if (request.body.theme) viewer.theme = request.body.theme
if (request.body.settings) viewer.settings = request.body.settings
if (imageUrl) viewer.imageUrl = imageUrl
if (coverImageUrl) viewer.coverImageUrl = coverImageUrl
if (theme) viewer.theme = theme
if (settings) viewer.settings = settings
await viewerItem.replace<User>(viewer)
const listingItem = containerFor(server.database.client, 'Directory').item(request.viewer.id, USER_LISTING_PARTITION_KEY)
const { resource: listing } = await listingItem.read<UserListing>()
if (listing) {
if (email) listing.email = email.trim()
if (phone) listing.phone = phone.trim()
await listingItem.replace<UserListing>(listing)
}
return viewer
})
}
@ -193,7 +241,7 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
},
}
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id', options, async (request, reply) => {
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/v1/user/:id', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const userContainer = containerFor(server.database.client, 'Users')
@ -279,7 +327,7 @@ function subscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
},
}
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/subscribe', options, async (request, reply) => {
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/v1/user/:id/subscribe', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -379,7 +427,7 @@ function unsubscribeRoute(server: FastifyInstance<Server, IncomingMessage, Serve
},
}
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/unsubscribe', options, async (request, reply) => {
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/v1/user/:id/unsubscribe', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -451,7 +499,7 @@ function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespo
},
}
server.post<DefaultQuery, Params, DefaultHeaders, Body>('/api/user/:id/block', options, async (request, reply) => {
server.post<DefaultQuery, Params, DefaultHeaders, Body>('/v1/user/:id/block', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
@ -506,7 +554,7 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
},
}
server.post<DefaultQuery, Params, DefaultHeaders, Body>('/api/user/:id/unblock', options, async (request, reply) => {
server.post<DefaultQuery, Params, DefaultHeaders, Body>('/v1/user/:id/unblock', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)

7
src/schemas.ts

@ -20,7 +20,7 @@ export const userSettingsSchema: JSONSchema = {
},
}
export const groupListingSchema: JSONSchema = {
export const groupSchema: JSONSchema = {
description: 'Group entity.',
type: 'object',
properties: {
@ -54,7 +54,7 @@ export const userSchema: JSONSchema = {
type: 'object',
properties: {
id: { type: 'string' },
group: groupListingSchema,
group: groupSchema,
name: { type: 'string' },
about: { type: 'string' },
imageUrl: { type: 'string' },
@ -65,6 +65,7 @@ export const userSchema: JSONSchema = {
items: subscriptionSchema,
},
membership: { type: 'string' },
intro: { type: 'string' },
posts: { type: 'number' },
points: { type: 'number' },
created: { type: 'number' },
@ -141,7 +142,7 @@ export const selfSchema: JSONSchema = {
type: 'object',
properties: {
id: { type: 'string' },
group: groupListingSchema,
group: groupSchema,
name: { type: 'string' },
email: { type: 'string' },
about: { type: 'string' },

23
src/types/collections.ts

@ -4,15 +4,15 @@
// - Partition Key: pk (userId)
// Groups
// - Partition Key: pk (groupId)
// GroupDirectory
// Directory
// - Partition Key: pk
// Posts
// - Partition Key: pk (postId)
// Ancestry
// - Partition Key: pk (postId)
// Points: total reward value + likes
import {
USER_LISTING_PARTITION_KEY,
GROUP_LISTING_PARTITION_KEY,
APP_PARTITION_KEY,
INSTALLATION_PARTITION_KEY,
@ -74,11 +74,19 @@ export enum BlockType {
Group = 'group',
}
export interface UserListing {
id: string
pk: typeof USER_LISTING_PARTITION_KEY
email: string
phone: string
posts: number
points: number
created: number
}
export interface GroupListing {
id: string
pk: typeof GROUP_LISTING_PARTITION_KEY
name: string
about?: string
registration: GroupRegistrationType
members: number
posts: number
@ -99,6 +107,9 @@ export interface Group {
iconImageUrl?: string
theme: string
registration: GroupRegistrationType
members: number
posts: number
points: number
status: GroupStatus
active: boolean
created: number
@ -111,6 +122,7 @@ export interface GroupMembership {
userId: string
pending: boolean
membership: GroupMembershipType
intro?: string
invitation?: string
created: number
}
@ -175,6 +187,8 @@ export interface User {
theme: string
email: string
emailVerified: boolean
phone?: string
phoneVerified: boolean
passwordHash: string
settings: UserSettings
installations: string[]
@ -186,7 +200,6 @@ export interface User {
pending: boolean
requiresApproval: boolean
privacy: UserPrivacyType
paid: boolean
active: boolean
created: number // Timestamp
}

Loading…
Cancel
Save