Dwayne Harris 4 years ago
parent
commit
157ebf2045
  1. 2079
      package-lock.json
  2. 16
      package.json
  3. 10
      src/lib/collections.ts
  4. 59
      src/plugins/api/apps.ts
  5. 61
      src/plugins/api/authentication.ts
  6. 21
      src/plugins/api/groups.ts
  7. 24
      src/plugins/api/posts.ts
  8. 13
      src/types/collections.ts

2079
package-lock.json
File diff suppressed because it is too large
View File

16
package.json

@ -14,20 +14,20 @@
"@types/bcryptjs": "^2.4.2",
"@types/dotenv": "^8.2.0",
"@types/jsonwebtoken": "^8.3.5",
"@types/lodash": "^4.14.146",
"@types/lodash": "^4.14.149",
"@types/uuid": "^3.4.6",
"nodemon": "^1.19.4",
"nodemon": "^2.0.2",
"npm-run-all": "^4.1.5",
"pino-pretty": "^3.3.0",
"typescript": "^3.7.2"
"pino-pretty": "^3.5.0",
"typescript": "^3.7.3"
},
"dependencies": {
"@azure/cosmos": "^3.4.2",
"@azure/storage-blob": "^12.0.0",
"@azure/cosmos": "^3.5.2",
"@azure/storage-blob": "^12.0.1",
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"fastify": "^2.10.0",
"fastify-cors": "^2.2.0",
"fastify": "^2.11.0",
"fastify-cors": "^3.0.0",
"fastify-helmet": "^3.0.2",
"fastify-swagger": "^2.5.0",
"jsonwebtoken": "^8.5.1",

10
src/lib/collections.ts

@ -1,4 +1,4 @@
import { CosmosClient } from '@azure/cosmos'
import { CosmosClient, Item } from '@azure/cosmos'
import { Logger } from 'fastify'
import compact from 'lodash/compact'
import uniq from 'lodash/uniq'
@ -61,6 +61,14 @@ export async function getUsersFromItems<T extends DatabaseItem>(client: CosmosCl
return await getUsers(client, items.map(i => i.id), logger)
}
export async function updateItem<T>(item: Item, updates: Partial<T>) {
const { resource } = await item.read<T>()
await item.replace<T>({
...resource,
...updates,
})
}
export async function getApprovedSubscriptions(client: CosmosClient, from: string, to: string, logger?: Logger): Promise<UserSubscription[]> {
return await queryItems<UserSubscription>({
container: containerFor(client, 'Users'),

59
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, userIdIsValid, userIsValid } from '../../lib/collections'
import { getUsers, userIdIsValid, userIsValid, updateItem } from '../../lib/collections'
import { generateString } from '../../lib/crypto'
import { containerFor, getItem, normalize, queryItems, createQuerySpec } from '../../lib/database'
import { unauthorizedError, serverError, badRequestError, notFoundError, forbiddenError } from '../../lib/errors'
@ -112,15 +112,17 @@ function appsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
if (!server.database) return serverError(reply)
const { sort = 'created', continuation } = request.query
const query = `SELECT * FROM Apps a WHERE a.pk = '${APP_PARTITION_KEY}' AND a.active = true ORDER BY a.${sort}`
const container = containerFor(server.database.client, 'Apps')
const { resources: apps, requestCharge, continuation: newContinuation } = await container.items.query<App>(
`SELECT * FROM Apps a WHERE a.pk = '${APP_PARTITION_KEY}' AND a.active = true ORDER BY a.${sort}`,
{
maxItemCount: 40,
continuation,
}
).fetchAll()
const {
resources: apps,
requestCharge,
continuation: newContinuation,
} = await container.items.query<App>(query, {
maxItemCount: 40,
continuation,
}).fetchAll()
request.log.trace('Query: %d', requestCharge)
@ -172,16 +174,16 @@ function selfAppsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
const viewer = containerFor(server.database.client, 'Users').item(request.viewer.id, request.viewer.id).read<User>()
const viewer = await getItem<User>({
container: containerFor(server.database.client, 'Users'),
id: request.viewer.id,
})
const { sort = 'created' } = request.query
const query = `SELECT * FROM Apps a WHERE a.pk = '${APP_PARTITION_KEY}' AND a.userId = ${request.viewer.id} ORDER BY a.${sort}`
const container = containerFor(server.database.client, 'Apps')
const { resources: apps, requestCharge } = await container.items.query<App>(
`SELECT * FROM Apps a WHERE a.pk = '${APP_PARTITION_KEY}' AND a.userId = ${request.viewer.id} ORDER BY a.${sort}`,
{}
).fetchAll()
const { resources: apps, requestCharge } = await container.items.query<App>(query, {}).fetchAll()
request.log.trace('Query: %d', requestCharge)
return {
@ -252,7 +254,9 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
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 valid = await userIdIsValid(server.database.client, request.viewer.id)
if (!valid) return forbiddenError(reply)
const container = containerFor(server.database.client, 'Apps')
const id = normalize(request.body.name)
@ -312,9 +316,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if (coverImageUrl) await attachMedia(mediaContainer, coverImageUrl)
if (iconImageUrl) await attachMedia(mediaContainer, iconImageUrl)
return {
id,
}
return { id }
})
}
@ -382,7 +384,9 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
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 valid = await userIdIsValid(server.database.client, request.viewer.id)
if (!valid) return forbiddenError(reply)
const container = containerFor(server.database.client, 'Apps')
const mediaContainer = containerFor(server.database.client, 'Media')
@ -790,15 +794,7 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
if (!server.database) return serverError(reply)
if (request.headers.adminkey !== process.env.ADMIN_KEY) return serverError(reply)
const container = containerFor(server.database.client, 'Apps')
const item = container.item(request.params.id, APP_PARTITION_KEY)
const { resource: app } = await item.read<App>()
await item.replace<App>({
...app,
active: true,
})
await updateItem<App>(containerFor(server.database.client, 'Apps').item(request.params.id, APP_PARTITION_KEY), { active: true })
reply.code(204)
})
}
@ -843,12 +839,7 @@ function setPreinstallRoute(server: FastifyInstance<Server, IncomingMessage, Ser
if (!server.database) return serverError(reply)
if (request.headers.adminkey !== process.env.ADMIN_KEY) return serverError(reply)
const container = containerFor(server.database.client, 'Apps')
const item = container.item(request.params.id, APP_PARTITION_KEY)
const { resource: app } = await item.read<App>()
await item.replace<App>({
...app,
await updateItem<App>(containerFor(server.database.client, 'Apps').item(request.params.id, APP_PARTITION_KEY), {
preinstall: true,
active: true,
})

61
src/plugins/api/authentication.ts

@ -10,7 +10,6 @@ import {
import { Server, IncomingMessage, ServerResponse } from 'http'
import { MIN_ID_LENGTH, MAX_ID_LENGTH, MAX_NAME_LENGTH, MIN_PASSWORD_LENGTH, INSTALLATION_PARTITION_KEY, APP_PARTITION_KEY } from '../../constants'
import { tokenResponseSchema, selfSchema, errorSchema } from '../../schemas'
import { createAccessToken, createRefreshToken } from '../../lib/authentication'
import { getUser, getUserIdFromEmail, getUserIdFromPhone } from '../../lib/collections'
@ -21,6 +20,16 @@ import { tokenFromHeader } from '../../lib/http'
import { attachMedia } from '../../lib/media'
import { createInstallationId } from '../../lib/utils'
import {
MIN_ID_LENGTH,
MAX_ID_LENGTH,
MAX_NAME_LENGTH,
MIN_PASSWORD_LENGTH,
INSTALLATION_PARTITION_KEY,
APP_PARTITION_KEY,
USER_LISTING_PARTITION_KEY,
} from '../../constants'
import {
User,
UserToken,
@ -34,6 +43,7 @@ import {
GroupRegistrationType,
App,
Installation,
UserListing,
} from '../../types/collections'
import { PluginOptions } from '../../types'
@ -105,7 +115,21 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
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, phone, imageUrl, coverImageUrl, theme, invitation: code, intro } = 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
@ -188,7 +212,7 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
id,
pk: id,
t: UserItemType.User,
groupId: group ? group.id : undefined,
groupId: group?.id,
name,
about,
email,
@ -205,8 +229,6 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
autoPlayGifs: true,
},
installations,
points: 0,
balance: 0,
posts: 0,
subscriberCount: 0,
subscribedCount: 0,
@ -217,6 +239,16 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
created: Date.now(),
}
const directoryContainer = containerFor(server.database.client, 'Directory')
await directoryContainer.items.create<UserListing>({
id,
pk: USER_LISTING_PARTITION_KEY,
groupId: group?.id,
email,
phone,
created: Date.now(),
})
const refreshToken = createRefreshToken(id, request.headers['user-agent'], request.ip)
await userContainer.items.create<User>(user)
@ -295,9 +327,24 @@ function authenticateRoute(server: FastifyInstance<Server, IncomingMessage, Serv
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')
const id = normalize(request.body.id)
let id: string | undefined
const isEmail = /^\S+@\S+\.\S+$/.test(request.body.id)
if (isEmail) {
const listings = await queryItems<UserListing>({
container: containerFor(server.database.client, 'Directory'),
query: createQuerySpec(`SELECT * FROM Directory d WHERE d.pk = ${USER_LISTING_PARTITION_KEY} AND d.email = @email`, { email: request.body.id }),
logger: request.log,
})
if (listings.length < 1) return badRequestFormError(reply, 'id', 'Email address not found')
id = listings[0].id
} else {
id = normalize(request.body.id)
}
const container = containerFor(server.database.client, 'Users')
const user = await getItem<User>({ container, id })
if (!user) return badRequestFormError(reply, 'id', 'User not found')

21
src/plugins/api/groups.ts

@ -10,10 +10,10 @@ import {
import { Server, IncomingMessage, ServerResponse } from 'http'
import { MIN_ID_LENGTH, MAX_NAME_LENGTH, GROUP_LISTING_PARTITION_KEY } from '../../constants'
import { MIN_ID_LENGTH, MAX_NAME_LENGTH, GROUP_LISTING_PARTITION_KEY, USER_LISTING_PARTITION_KEY } from '../../constants'
import { errorSchema, groupSchema, userSchema } from '../../schemas'
import { unauthorizedError, badRequestError, notFoundError, serverError, forbiddenError } from '../../lib/errors'
import { getUsers, getUserMembership, getUser, userIsValid } from '../../lib/collections'
import { getUsers, getUserMembership, getUser, updateItem } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import { createInvitationCode } from '../../lib/utils'
import { attachMedia, deleteMedia } from '../../lib/media'
@ -33,6 +33,7 @@ import {
UserItemType,
GroupLog,
GroupInvitation,
UserListing,
} from '../../types/collections'
import { PluginOptions } from '../../types'
@ -141,14 +142,14 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
const viewerItem = containerFor(server.database.client, 'Users').item(request.viewer.id, request.viewer.id)
const { resource: viewer } = await viewerItem.read<User>()
const groupContainer = containerFor(server.database.client, 'Groups')
if (viewer.groupId) return badRequestError(reply)
if (viewer.groupId) return forbiddenError(reply)
const { name, about, registration, imageUrl, coverImageUrl, iconImageUrl, theme } = request.body
const id = normalize(name)
const groupContainer = containerFor(server.database.client, 'Groups')
const directoryContainer = containerFor(server.database.client, 'Directory')
const existingGroup = await getItem<Group>({ container: groupContainer, id })
if (existingGroup) return badRequestError(reply, 'Name already used')
@ -161,8 +162,6 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
about,
registration,
members: 0,
posts: 0,
points: 0,
imageUrl,
coverImageUrl,
iconImageUrl,
@ -190,6 +189,10 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
groupId: group.id,
})
await updateItem<UserListing>(directoryContainer.item(request.viewer.id, USER_LISTING_PARTITION_KEY), {
groupId: group.id,
})
await groupContainer.items.create<GroupLog>({
pk: group.id,
t: GroupItemType.Log,
@ -591,8 +594,6 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
pk: GROUP_LISTING_PARTITION_KEY,
registration: group.registration,
members: 1,
posts: 0,
points: 0,
created: Date.now(),
})
}

24
src/plugins/api/posts.ts

@ -18,7 +18,7 @@ import { SHORT_TEXT_LENGTH, INSTALLATION_PARTITION_KEY, APP_PARTITION_KEY } from
import { userSchema, postSchema, errorSchema } from '../../schemas'
import { unauthorizedError, serverError, badRequestError, badRequestFormError, notFoundError, forbiddenError } from '../../lib/errors'
import { trimContent, createPostId } from '../../lib/utils'
import { getUsers, getApprovedSubscriptions, getUserBlocks, getUser, userIsValid } from '../../lib/collections'
import { getUsers, getApprovedSubscriptions, getUserBlocks, getUser, userIsValid, updateItem } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import {
@ -195,8 +195,6 @@ async function createPost(client: CosmosClient, userId: string, appId: string, b
posts: viewer.posts + 1,
})
// TODO: Figure out how to update Group effeciently
return {
id: postId,
}
@ -397,7 +395,7 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
const userPosts = await queryItems<UserPost>({
container: userContainer,
query: userPostsQuery,
logger: request.log
logger: request.log,
})
const posts = await queryItems<Post>({
@ -405,7 +403,7 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@posts, p.id) ORDER BY p.created DESC', {
posts: userPosts.map(p => p.postId!),
}),
logger: request.log
logger: request.log,
})
return {
@ -472,7 +470,7 @@ function timelineRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@posts, p.id) ORDER BY p.created DESC', {
posts: timelinePosts.map(p => p.id),
}),
logger: request.log
logger: request.log,
})
const users = await getUsers(server.database.client, posts.map(p => p.userId))
@ -543,7 +541,7 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
const descendantRelationships = await queryItems<PostRelationship>({
container: containerFor(server.database.client, 'Ancestry'),
query,
logger: request.log
logger: request.log,
})
const descendants = await queryItems<Post>({
@ -551,7 +549,7 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@descendants, p.id)', {
descendants: descendantRelationships.map(r => r.id),
}),
logger: request.log
logger: request.log,
})
const ancestors = await queryItems<Post>({
@ -559,7 +557,7 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@parents, p.id)', {
parents: post.parents,
}),
logger: request.log
logger: request.log,
})
const getUserId = (post: Post) => post.userId
@ -573,7 +571,11 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
const users = await getUsers(server.database.client, userIds, request.log)
if (request.viewer) {
const viewer = await getItem<User>({ container: containerFor(server.database.client, 'Users'), id: request.viewer.id })
const viewer = await getItem<User>({
container: containerFor(server.database.client, 'Users'),
id: request.viewer.id,
})
if (!viewer) return serverError(reply)
if (!userIsValid(viewer)) return forbiddenError(reply)
@ -595,8 +597,8 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
query: blockQuery,
logger: request.log,
})
const blockedUserIds = blocks.map(b => b.userId)
const blockedUserIds = blocks.map(b => b.userId)
if (blockedUserIds.includes(post.userId)) return unauthorizedError(reply)
return {

13
src/types/collections.ts

@ -77,10 +77,9 @@ export enum BlockType {
export interface UserListing {
id: string
pk: typeof USER_LISTING_PARTITION_KEY
email: string
phone: string
posts: number
points: number
groupId?: string
email?: string
phone?: string
created: number
}
@ -89,8 +88,6 @@ export interface GroupListing {
pk: typeof GROUP_LISTING_PARTITION_KEY
registration: GroupRegistrationType
members: number
posts: number
points: number
created: number
}
@ -108,8 +105,6 @@ export interface Group {
theme: string
registration: GroupRegistrationType
members: number
posts: number
points: number
status: GroupStatus
active: boolean
created: number
@ -192,8 +187,6 @@ export interface User {
passwordHash: string
settings: UserSettings
installations: string[]
points: number
balance: number // Currency (Flex)
posts: number
subscriberCount: number
subscribedCount: number

Loading…
Cancel
Save