Dwayne Harris 5 years ago
parent
commit
5c6137ff0f
  1. 14
      package-lock.json
  2. 1
      package.json
  3. 4
      src/awards.ts
  4. 4
      src/lib/authentication.ts
  5. 62
      src/lib/collections.ts
  6. 10
      src/lib/crypto.ts
  7. 29
      src/lib/database.ts
  8. 53
      src/lib/errors.ts
  9. 55
      src/plugins/api/authentication.ts
  10. 89
      src/plugins/api/groups.ts
  11. 20
      src/plugins/api/index.ts
  12. 152
      src/plugins/api/posts.ts
  13. 72
      src/plugins/api/users.ts
  14. 1
      src/schemas.ts
  15. 5
      src/server.ts
  16. 80
      src/types/collections.ts
  17. 16
      src/types/index.ts
  18. 10
      tsconfig.json

14
package-lock.json

@ -1033,6 +1033,15 @@
"tiny-lru": "^6.0.1"
}
},
"fastify-cors": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fastify-cors/-/fastify-cors-2.1.3.tgz",
"integrity": "sha512-ZHFzKn1DddymsxzvdtjEQfkuMfGgwcp++FKuTTMmAN2KFB7hJRmOINffjfRdmUcgXdE4LoSy5XJROWKx/b+CPQ==",
"requires": {
"fastify-plugin": "^1.5.0",
"vary": "^1.1.2"
}
},
"fastify-helmet": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fastify-helmet/-/fastify-helmet-3.0.1.tgz",
@ -3706,6 +3715,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",

1
package.json

@ -25,6 +25,7 @@
"argon2": "^0.24.0",
"dotenv": "^8.0.0",
"fastify": "^2.7.1",
"fastify-cors": "^2.1.3",
"fastify-helmet": "^3.0.1",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.15",

4
src/awards.ts

@ -1,6 +1,6 @@
import { IAwardDefinition } from './types'
import { AwardDefinition } from './types'
const awards: IAwardDefinition[] = [
const awards: AwardDefinition[] = [
{
id: 'like',
name: 'Like',

4
src/lib/authentication.ts

@ -1,12 +1,12 @@
import { v1 } from 'uuid'
import { JWT } from '../lib/crypto'
import { IUserToken } from '../types/collections'
import { UserToken } from '../types/collections'
export async function createAccessToken(userId: string): Promise<string> {
return await JWT.sign({ sub: userId }, { expiresIn: process.env.TOKEN_EXPIRATION })
}
export function createRefreshToken(userId: string, userAgent: string, ip: string): IUserToken {
export function createRefreshToken(userId: string, userAgent: string, ip: string): UserToken {
return {
id: 'r' + v1().replace(/-/g, ''),
pk: userId,

62
src/lib/collections.ts

@ -1,37 +1,53 @@
import { CosmosClient } from '@azure/cosmos'
import { Logger } from 'fastify'
import uniq from 'lodash/uniq'
import { IDatabaseItem } from '../types'
import { DatabaseItem } from '../types'
import { containerFor, createQuerySpec, queryItems } from './database'
import { IUser, IUserSubscription, IUserBlock } from '../types/collections'
import { User, UserSubscription, UserBlock } from '../types/collections'
export async function getUsers(client: CosmosClient, ids: string[], logger?: Logger): Promise<IUser[]> {
return await queryItems<IUser>(containerFor(client, 'Users'), createQuerySpec(
'SELECT u.id, u.name, u.imageUrl, u.coverImageUrl, u.group, u.created FROM Users u WHERE ARRAY_CONTAINS(@ids, u.id)', {
ids: uniq(ids),
}), logger)
export async function getUsers(client: CosmosClient, ids: string[], logger?: Logger): Promise<User[]> {
return await queryItems<User>({
container: containerFor(client, 'Users'),
query: createQuerySpec(
'SELECT u.id, u.name, u.imageUrl, u.coverImageUrl, u.group, u.created FROM Users u WHERE ARRAY_CONTAINS(@ids, u.id)',
{
ids: uniq(ids),
}
),
logger,
})
}
export async function getUsersFromItems<T extends IDatabaseItem>(client: CosmosClient, items: T[]): Promise<IUser[]> {
export async function getUsersFromItems<T extends DatabaseItem>(client: CosmosClient, items: T[], logger?: Logger): Promise<User[]> {
return await getUsers(client, items.map(i => i.id))
}
export async function getApprovedSubscriptions(client: CosmosClient, from: string, to: string, logger?: Logger): Promise<IUserSubscription[]> {
return await queryItems<IUserSubscription>(containerFor(client, 'Users'), createQuerySpec(`
SELECT u.id FROM Users u WHERE
u.subscriberId = @to
u.pk = @from AND
u.t = 'subscription' AND
u.pending = false
`, { from, to }), logger)
export async function getApprovedSubscriptions(client: CosmosClient, from: string, to: string, logger?: Logger): Promise<UserSubscription[]> {
return await queryItems<UserSubscription>({
container: containerFor(client, 'Users'),
query: createQuerySpec(
`SELECT u.id FROM Users u WHERE
u.subscriberId = @to
u.pk = @from AND
u.t = 'subscription' AND
u.pending = false`,
{ from, to }
),
logger,
})
}
export async function getUserBlocks(client: CosmosClient, from: string, to: string[], logger?: Logger): Promise<IUserBlock[]> {
return await queryItems<IUserBlock>(containerFor(client, 'Users'), createQuerySpec(`
SELECT u.id FROM Users u WHERE
u.pk = @from
AND u.t = 'block'
AND ARRAY_CONTAINS(@to, u.blockedId)
`, { from, to }), logger)
export async function getUserBlocks(client: CosmosClient, from: string, to: string[], logger?: Logger): Promise<UserBlock[]> {
return await queryItems<UserBlock>({
container: containerFor(client, 'Users'),
query: createQuerySpec(
`SELECT u.id FROM Users u WHERE
u.pk = @from
AND u.t = 'block'
AND ARRAY_CONTAINS(@to, u.blockedId)`,
{ from, to }
),
logger,
})
}

10
src/lib/crypto.ts

@ -10,12 +10,12 @@ export async function verifyPassword(hash: string, password: string): Promise<bo
}
export namespace JWT {
export interface IJWTData {
export interface JWTData {
sub?: string
exp?: number
}
export function sign(data: IJWTData, options: SignOptions = {}): Promise<string> {
export function sign(data: JWTData, options: SignOptions = {}): Promise<string> {
return new Promise<string>((resolve, reject) => {
jwt.sign(data, process.env.TOKEN_SECRET!, options, (err, token) => {
if (err) return reject(err)
@ -24,9 +24,9 @@ export namespace JWT {
})
}
export function verify(token: string, options?: VerifyOptions): Promise<IJWTData> {
return new Promise<IJWTData>((resolve, reject) => {
jwt.verify(token, process.env.TOKEN_SECRET!, options, (err, decoded: IJWTData | string) => {
export function verify(token: string, options?: VerifyOptions): Promise<JWTData> {
return new Promise<JWTData>((resolve, reject) => {
jwt.verify(token, process.env.TOKEN_SECRET!, options, (err, decoded: JWTData | string) => {
if (err) return reject(err)
if (typeof decoded === 'string') return reject('Invalid token')

29
src/lib/database.ts

@ -1,12 +1,12 @@
import { CosmosClient, Container, SqlQuerySpec } from '@azure/cosmos'
import { Logger } from 'fastify'
import { IQueryParams } from '../types'
import { QueryParams } from '../types'
export function containerFor(client: CosmosClient, containerId: string): Container {
return client.database('Flexor').container(containerId)
}
export function createQuerySpec(query: string, params: IQueryParams = {}): SqlQuerySpec {
export function createQuerySpec(query: string, params: QueryParams = {}): SqlQuerySpec {
return {
query,
parameters: Object.entries(params).map(([key, value]) => {
@ -18,19 +18,30 @@ export function createQuerySpec(query: string, params: IQueryParams = {}): SqlQu
}
}
export async function queryItems<T>(container: Container, querySpec: SqlQuerySpec, logger?: Logger): Promise<T[]> {
const { resources, requestCharge } = await container.items.query<T>(querySpec, {}).fetchAll()
interface QueryItemsOptions {
container: Container,
query: string | SqlQuerySpec,
logger?: Logger
}
export async function queryItems<T>(options: QueryItemsOptions): Promise<T[]> {
const { container, query, logger } = options
const { resources, requestCharge } = await container.items.query<T>(query, {}).fetchAll()
if (logger) logger.trace('Query: %d', requestCharge)
return resources
}
export async function getItem<T>(container: Container, id: string, logger?: Logger): Promise<T | undefined>
export async function getItem<T>(container: Container, id: string, partitionKey: string, logger?: Logger): Promise<T | undefined>
export async function getItem<T>(container: Container, id: string, partitionKeyOrLogger?: string | Logger, logger?: Logger): Promise<T | undefined> {
const partitionKey = (typeof partitionKeyOrLogger === 'string') ? partitionKeyOrLogger : id
interface GetItemOptions {
container: Container
id: string
partitionKey?: string
logger?: Logger
}
const { resource, requestCharge } = await container.item(id, partitionKey).read<T>()
export async function getItem<T>(options: GetItemOptions): Promise<T | undefined> {
const { container, id, partitionKey, logger } = options
const { resource, requestCharge } = await container.item(id, partitionKey || id).read<T>()
if (logger) logger.trace('Get: %d', requestCharge)
return resource

53
src/lib/errors.ts

@ -1,49 +1,50 @@
import { FastifyReply, JSONSchema } from 'fastify'
import { FastifyReply } from 'fastify'
import { ServerResponse } from 'http'
import { HttpError, HttpFormError } from '../types'
interface IHttpError {
statusCode: number
error: string
field?: string
export function badRequestError(reply: FastifyReply<ServerResponse>, message: string = 'Bad Request', errors?: HttpFormError[]): HttpError {
reply.code(400)
return {
message,
errors,
}
}
export function badRequestError(reply: FastifyReply<ServerResponse>, error: string = 'Bad Request', field?: string): IHttpError {
const statusCode = 400
reply.code(statusCode)
export function badRequestFormError(reply: FastifyReply<ServerResponse>, field: string, message: string): HttpError {
reply.code(400)
return {
statusCode,
error,
field,
message: 'Bad Request',
errors: [
{
field,
message,
}
]
}
}
export function unauthorizedError(reply: FastifyReply<ServerResponse>): IHttpError {
const statusCode = 401
reply.code(statusCode)
export function unauthorizedError(reply: FastifyReply<ServerResponse>): HttpError {
reply.code(401)
return {
statusCode,
error: 'Unauthorized',
message: 'Unauthorized',
}
}
export function notFoundError(reply: FastifyReply<ServerResponse>): IHttpError {
const statusCode = 404
reply.code(statusCode)
export function notFoundError(reply: FastifyReply<ServerResponse>): HttpError {
reply.code(404)
return {
statusCode,
error: 'Not Found'
message: 'Not Found',
}
}
export function serverError(reply: FastifyReply<ServerResponse>, error: string = 'Server Error'): IHttpError {
const statusCode = 500
reply.code(statusCode)
export function serverError(reply: FastifyReply<ServerResponse>, message: string = 'Server Error'): HttpError {
reply.code(500)
return {
statusCode,
error,
message,
}
}

55
src/plugins/api/authentication.ts

@ -15,10 +15,10 @@ 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 { badRequestError, unauthorizedError, serverError } from '../../lib/errors'
import { badRequestError, badRequestFormError, unauthorizedError, serverError } from '../../lib/errors'
import { tokenFromHeader } from '../../lib/http'
import { IUser, IUserToken, IGroup, IGroupPartial } from '../../types/collections'
import { User, UserToken, Group, GroupPartial } from '../../types/collections'
interface PluginOptions {}
@ -71,16 +71,26 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
const id = normalize(request.body.id)
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')
const existingUser = await getItem<User>({
container: userContainer,
id,
logger: request.log
})
if (existingUser) return badRequestFormError(reply, 'id', 'User id already taken')
let userPending = false
let groupPartial: IGroupPartial | undefined
let groupPartial: GroupPartial | undefined
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')
const group = await getItem<Group>({
container: containerFor(server.database.client, 'Groups'),
id: request.body.group,
logger: request.log
})
if (!group) return badRequestFormError(reply, 'group', 'Group not found')
if (!group.open) return badRequestFormError(reply, 'group', 'Group registration closed')
if (group.requiresApproval) userPending = true
@ -92,7 +102,7 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
}
}
const user: IUser = {
const user: User = {
id,
pk: id,
t: 'user',
@ -158,11 +168,15 @@ function authenticateRoute(server: FastifyInstance<Server, IncomingMessage, Serv
const container = containerFor(server.database.client, 'Users')
const id = normalize(request.body.id)
const user = await getItem<IUser>(container, id, request.log)
if (!user) return badRequestError(reply, 'User not found')
const user = await getItem<User>({
container,
id,
logger: request.log
})
if (!user) return badRequestFormError(reply, 'id', 'User not found')
const result = await verifyPassword(user.passwordHash, request.body.password)
if (!result) return badRequestError(reply, 'Incorrect credentials')
if (!result) return badRequestFormError(reply, 'password', 'Incorrect password')
const refreshToken = createRefreshToken(user.id, request.headers['user-agent'], request.ip)
await container.items.create(refreshToken)
@ -201,7 +215,7 @@ function refreshRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
},
},
response: {
200: tokenResponseSchema,
201: tokenResponseSchema,
400: errorSchema,
},
},
@ -234,7 +248,7 @@ function refreshRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
const tokenItem = container.item(request.body.refresh, userId)
if (!tokenItem) return badRequestError(reply, 'Invalid refresh token')
const { resource: token, requestCharge } = await tokenItem.read<IUserToken>()
const { resource: token, requestCharge } = await tokenItem.read<UserToken>()
request.log.trace('Get: %d', requestCharge)
if (token.expires < Date.now()) return badRequestError(reply, 'Refresh token expired')
@ -244,10 +258,13 @@ function refreshRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
const newRefreshToken = createRefreshToken(userId, request.headers['user-agent'], request.ip)
await container.items.create(newRefreshToken)
reply.code(201)
return {
id: userId,
access: await createAccessToken(userId),
refresh: newRefreshToken.id
refresh: newRefreshToken.id,
expires: newRefreshToken.expires,
}
})
}
@ -266,9 +283,13 @@ function selfRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
const viewer = await getItem<IUser>(containerFor(server.database.client, 'Users'), request.viewer.id, request.log)
if (!viewer) return unauthorizedError(reply)
const viewer = await getItem<User>({
container: containerFor(server.database.client, 'Users'),
id: request.viewer.id,
logger: request.log
})
if (!viewer) return unauthorizedError(reply)
return viewer
})
}

89
src/plugins/api/groups.ts

@ -14,7 +14,7 @@ import { MIN_ID_LENGTH, MAX_NAME_LENGTH } from '../../constants'
import { errorSchema, groupListingSchema } from '../../schemas'
import { unauthorizedError, badRequestError, notFoundError, serverError } from '../../lib/errors'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import { IUser, IGroup, IGroupListing, IGroupMembership, IUserBlock, IGroupBlock } from '../../types/collections'
import { User, Group, GroupListing, GroupMembership, UserBlock, GroupBlock } from '../../types/collections'
interface PluginOptions {}
@ -59,7 +59,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if (!request.viewer) return unauthorizedError(reply)
const viewerItem = containerFor(server.database.client, 'Users').item(request.viewer.id, request.viewer.id)
const { resource: viewer, requestCharge } = await viewerItem.read<IUser>()
const { resource: viewer, requestCharge } = await viewerItem.read<User>()
request.log.trace('Get: %d', requestCharge)
const groupContainer = containerFor(server.database.client, 'Groups')
@ -69,10 +69,14 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
const { name, about, open, requiresApproval } = request.body
const id = normalize(name)
const existingGroup = await getItem<IGroup>(groupContainer, id, request.log)
const existingGroup = await getItem<Group>({
container: groupContainer,
id,
logger: request.log
})
if (existingGroup) return badRequestError(reply, 'Name already used')
const group: IGroup = {
const group: Group = {
id: id,
pk: id,
t: 'group',
@ -86,7 +90,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
created: Date.now(),
}
const membership: IGroupMembership = {
const membership: GroupMembership = {
id: request.viewer.id,
pk: id,
t: 'membership',
@ -99,7 +103,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
await groupContainer.items.create(group)
await groupContainer.items.create(membership)
await viewerItem.replace<IUser>({
await viewerItem.replace<User>({
...viewer,
group: {
id: group.id,
@ -115,6 +119,39 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
})
}
function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Params {
id: string
}
const options: RouteShorthandOptions = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
response: {
200: groupListingSchema,
},
},
}
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/group/:id', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const listing = await getItem<GroupListing>({
container: containerFor(server.database.client, 'GroupDirectory'),
id: request.params.id,
logger: request.log,
})
if (!listing) return notFoundError(reply)
return listing
})
}
function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Params {
id: string
@ -146,10 +183,14 @@ function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespo
if (!request.viewer) return unauthorizedError(reply)
const groupContainer = containerFor(server.database.client, 'Groups')
const group = await getItem<IGroup>(groupContainer, request.params.id, request.log)
const group = await getItem<Group>({
container: groupContainer,
id: request.params.id,
logger: request.log,
})
if (!group) return notFoundError(reply)
await containerFor(server.database.client, 'Users').items.create<IUserBlock>({
await containerFor(server.database.client, 'Users').items.create<UserBlock>({
blockedId: group.id,
pk: request.viewer.id,
t: 'block',
@ -158,7 +199,7 @@ function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespo
created: Date.now(),
})
await groupContainer.items.create<IGroupBlock>({
await groupContainer.items.create<GroupBlock>({
pk: group.id,
t: 'block',
blockedId: group.id,
@ -193,7 +234,11 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
const userContainer = containerFor(server.database.client, 'Users')
const groupContainer = containerFor(server.database.client, 'Groups')
const group = await getItem<IGroup>(groupContainer, request.params.id, request.log)
const group = await getItem<Group>({
container: groupContainer,
id: request.params.id,
logger: request.log
})
if (!group) return notFoundError(reply)
const userBlockQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.pk = @pk AND u.blockedId = @blocked AND u.type = 'block'`, {
@ -201,7 +246,11 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
blocked: group.id,
})
const userBlocks = await queryItems<IUserBlock>(userContainer, userBlockQuery, request.log)
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()
}
@ -215,7 +264,12 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
}
)
const groupBlocks = await queryItems<IUserBlock>(groupContainer, groupBlockQuery, request.log)
const groupBlocks = await queryItems<UserBlock>({
container: groupContainer,
query: groupBlockQuery,
logger: request.log
})
for (const groupBlock of groupBlocks) {
await groupContainer.item(groupBlock.id!, group.id).delete()
}
@ -246,7 +300,7 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
const container = containerFor(server.database.client, 'Groups')
const groupItem = container.item(request.params.id, request.params.id)
const { resource: group, requestCharge: groupRequestCharge } = await groupItem.read<IGroup>()
const { resource: group, requestCharge: groupRequestCharge } = await groupItem.read<Group>()
request.log.trace('Get: %d', groupRequestCharge)
if (!group) return notFoundError(reply)
@ -255,7 +309,7 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
return badRequestError(reply, 'Already activated')
}
await groupItem.replace<IGroup>({
await groupItem.replace<Group>({
...group,
active: true,
status: 'paid',
@ -263,11 +317,11 @@ function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
const directoryContainer = containerFor(server.database.client, 'GroupDirectory')
const listingItem = directoryContainer.item(request.params.id, 'pk')
const { resource: listing, requestCharge: listingRequestCharge } = await listingItem.read<IGroupListing>()
const { resource: listing, requestCharge: listingRequestCharge } = await listingItem.read<GroupListing>()
request.log.trace('Get: %d', listingRequestCharge)
if (!listing) {
await directoryContainer.items.create<IGroupListing>({
await directoryContainer.items.create<GroupListing>({
id: group.id,
name: group.name,
pk: 'pk',
@ -331,7 +385,7 @@ function listRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
}
const container = containerFor(server.database.client, 'GroupDirectory')
const { resources: groups, requestCharge, continuation: newContinuation } = await container.items.query<IGroupListing>(
const { resources: groups, requestCharge, continuation: newContinuation } = await container.items.query<GroupListing>(
`SELECT * FROM GroupDirectory d WHERE d.pk = 'pk' AND d.open = true ${requiresApprovalString} ORDER BY ${sort}`,
{
maxItemCount: 40,
@ -350,6 +404,7 @@ function listRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
createRoute(server)
getRoute(server)
blockRoute(server)
unblockRoute(server)
activateRoute(server)

20
src/plugins/api/index.ts

@ -10,21 +10,21 @@ import groups from './groups'
import posts from './posts'
import users from './users'
interface IDatabase {
interface Database {
client: CosmosClient
}
interface IUserInfo {
interface UserInfo {
id: string
}
declare module "fastify" {
interface FastifyInstance {
database?: IDatabase
database?: Database
}
interface FastifyRequest{
viewer?: IUserInfo
viewer?: UserInfo
}
}
@ -33,7 +33,7 @@ interface PluginOptions {
}
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
const database: IDatabase = {
const database: Database = {
client: new CosmosClient({
endpoint: process.env.DATABASE_ENDPOINT!,
key: process.env.DATABASE_PRIMARY_KEY,
@ -48,7 +48,7 @@ const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = a
if (!token) return
try {
const data: JWT.IJWTData = await JWT.verify(token)
const data: JWT.JWTData = await JWT.verify(token)
if (!data.sub) throw new Error('Invalid token')
req.viewer = {
@ -60,6 +60,14 @@ const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = a
}
})
server.setErrorHandler(function(error, request, reply) {
request.log.error('Error: %s', error.message)
reply.send({
message: error.message,
})
})
server.register(authentication)
server.register(groups)
server.register(posts)

152
src/plugins/api/posts.ts

@ -12,21 +12,21 @@ import { Server, IncomingMessage, ServerResponse } from 'http'
import { SHORT_TEXT_LENGTH, SUBSCRIBER_MAX_SIZE } from '../../constants'
import { userSchema, postSchema, errorSchema } from '../../schemas'
import { unauthorizedError, serverError, badRequestError, notFoundError } from '../../lib/errors'
import { unauthorizedError, serverError, badRequestError, badRequestFormError, notFoundError } from '../../lib/errors'
import { trimContent, createPostId } from '../../lib/util'
import { getUsers, getApprovedSubscriptions, getUserBlocks } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import {
IPost,
IPostAttachment,
IUser,
IUserPost,
IUserSubscription,
IUserTimelinePost,
IGroupBlock,
IPostRelationship,
IStatus,
Post,
PostAttachment,
User,
UserPost,
UserSubscription,
UserTimelinePost,
GroupBlock,
PostRelationship,
Status,
} from '../../types/collections'
interface PluginOptions {}
@ -36,8 +36,8 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
text?: string
cover?: string
visible: boolean
status?: IStatus
attachments: IPostAttachment[]
status?: Status
attachments: PostAttachment[]
parent: string
}
@ -102,13 +102,18 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
let newPostRelationship: IPostRelationship | undefined
let newPostRelationship: PostRelationship | undefined
const postContainer = containerFor(server.database.client, 'Posts')
const ancestryContainer = containerFor(server.database.client, 'Ancestry')
const userContainer = containerFor(server.database.client, 'Users')
const viewer = await getItem<IUser>(userContainer, request.viewer.id, request.log)
const viewer = await getItem<User>({
container: userContainer,
id: request.viewer.id,
logger: 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')
@ -116,10 +121,21 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
const postId = createPostId()
if (request.body.parent) {
const parent = await getItem<IPost>(postContainer, request.body.parent, request.log)
if (!parent) return badRequestError(reply, 'Invalid parent', 'parent')
const parent = await getItem<Post>({
container: postContainer,
id: request.body.parent,
logger: request.log
})
if (!parent) return badRequestFormError(reply, 'parent', 'Invalid parent')
const parentRelationship = await getItem<PostRelationship>({
container: ancestryContainer,
id: request.body.parent,
partitionKey: parent.root,
logger: request.log
})
const parentRelationship = await getItem<IPostRelationship>(ancestryContainer, request.body.parent, parent.root, request.log)
const parents = parentRelationship ? parentRelationship.parents : []
newPostRelationship = {
@ -132,7 +148,7 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
}
}
const post: IPost = {
const post: Post = {
id: postId,
pk: postId,
t: 'post',
@ -148,24 +164,28 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
created: Date.now(),
}
const userPost: IUserPost = {
const userPost: UserPost = {
postId,
pk: request.viewer.id,
t: 'post',
created: Date.now(),
}
await postContainer.items.create<IPost>(post)
await userContainer.items.create<IUserPost>(userPost)
await postContainer.items.create<Post>(post)
await userContainer.items.create<UserPost>(userPost)
if (newPostRelationship) await ancestryContainer.items.create<IPostRelationship>(newPostRelationship)
if (newPostRelationship) await ancestryContainer.items.create<PostRelationship>(newPostRelationship)
const query = createQuerySpec(`SELECT u.id FROM Users u WHERE u.pk = @pk AND u.type = 'subscription'`, { pk: request.viewer.id })
const subscribers = await queryItems<IUserSubscription>(userContainer, query, request.log)
const subscribers = await queryItems<UserSubscription>({
container: userContainer,
query,
logger: request.log
})
if (subscribers.length < SUBSCRIBER_MAX_SIZE) {
for (const subscriber of subscribers) {
await userContainer.items.create<IUserTimelinePost>({
await userContainer.items.create<UserTimelinePost>({
postId,
pk: subscriber.id!,
t: 'timeline',
@ -214,7 +234,11 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
const id = normalize(request.params.id)
const userContainer = containerFor(server.database.client, 'Users')
const user = await getItem<IUser>(userContainer, id, request.log)
const user = await getItem<User>({
container: userContainer,
id,
logger: request.log
})
if (!user) return notFoundError(reply)
if (!user.group) return notFoundError(reply)
@ -233,7 +257,12 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
case 'group': {
if (!request.viewer) return unauthorizedError(reply)
const viewer = await getItem<IUser>(userContainer, request.viewer.id, request.log)
const viewer = await getItem<User>({
container: userContainer,
id: request.viewer.id,
logger: request.log
})
if (!viewer) return serverError(reply)
if (!viewer.group) return unauthorizedError(reply)
@ -245,7 +274,12 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
}
if (request.viewer) {
const viewer = await getItem<IUser>(userContainer, request.viewer.id, request.log)
const viewer = await getItem<User>({
container: userContainer,
id: request.viewer.id,
logger: request.log
})
if (!viewer) return serverError(reply)
if (!viewer.group) return unauthorizedError(reply)
@ -254,11 +288,19 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
}
const userPostsQuery = createQuerySpec(`SELECT p.id FROM Users p WHERE p.pk = @user AND p.type = 'post'`, { user: id })
const userPosts = await queryItems<IUserPost>(userContainer, userPostsQuery)
const userPosts = await queryItems<UserPost>({
container: userContainer,
query: userPostsQuery,
logger: request.log
})
const posts = await queryItems<IPost>(containerFor(server.database.client, 'Posts'), createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@posts, p.id)', {
posts: userPosts.map(p => p.id!),
}), request.log)
const posts = await queryItems<Post>({
container: containerFor(server.database.client, 'Posts'),
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@posts, p.id)', {
posts: userPosts.map(p => p.id!),
}),
logger: request.log
})
return {
user,
@ -308,7 +350,12 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
if (!server.database) return serverError(reply)
const postContainer = containerFor(server.database.client, 'Posts')
const post = await getItem<IPost>(postContainer, request.params.id, request.log)
const post = await getItem<Post>({
container: postContainer,
id: request.params.id,
logger: request.log
})
if (!post) return notFoundError(reply)
const query = createQuerySpec('SELECT * FROM Ancestry a WHERE a.pk = @pk AND ARRAY_CONTAINS(a.parents, @id)', {
@ -316,17 +363,29 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
id: post.id,
})
const descendantRelationships = await queryItems<IPostRelationship>(containerFor(server.database.client, 'Ancestry'), query, request.log)
const descendantRelationships = await queryItems<PostRelationship>({
container: containerFor(server.database.client, 'Ancestry'),
query,
logger: request.log
})
const descendants = await queryItems<IPost>(postContainer, createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@descendants, p.id)', {
descendants: descendantRelationships.map(r => r.id),
}), request.log)
const descendants = await queryItems<Post>({
container: postContainer,
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@descendants, p.id)', {
descendants: descendantRelationships.map(r => r.id),
}),
logger: request.log
})
const ancestors = await queryItems<IPost>(postContainer, createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@parents, p.id)', {
parents: post.parents,
}), request.log)
const ancestors = await queryItems<Post>({
container: postContainer,
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@parents, p.id)', {
parents: post.parents,
}),
logger: request.log
})
const getUserId = (post: IPost) => post.userId
const getUserId = (post: Post) => post.userId
const userIds = [
...descendants.map(getUserId),
@ -337,7 +396,12 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
const users = await getUsers(server.database.client, userIds, request.log)
if (request.viewer) {
const viewer = await getItem<IUser>(containerFor(server.database.client, 'Users'), request.viewer.id, request.log)
const viewer = await getItem<User>({
container: containerFor(server.database.client, 'Users'),
id: request.viewer.id,
logger: request.log
})
if (!viewer) return serverError(reply)
if (!viewer.group) return unauthorizedError(reply)
@ -353,7 +417,11 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
ids: userIds,
})
const blocks = await queryItems<IGroupBlock>(containerFor(server.database.client, 'Groups'), blockQuery, request.log)
const blocks = await queryItems<GroupBlock>({
container: containerFor(server.database.client, 'Groups'),
query: blockQuery,
logger: request.log
})
const blockedUserIds = blocks.map(b => b.userId)
if (blockedUserIds.includes(post.userId)) return unauthorizedError(reply)

72
src/plugins/api/users.ts

@ -15,7 +15,7 @@ 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'
import { User, UserSubscription, UserBlock, GroupBlock, UserPrivacyType } from '../../types/collections'
interface PluginOptions {}
@ -28,7 +28,7 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
name?: string
about?: string
requiresApproval?: boolean
privacy?: IUserPrivacyType
privacy?: UserPrivacyType
}
const options: RouteShorthandOptions = {
@ -66,7 +66,7 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if (!request.viewer) return unauthorizedError(reply)
const viewerItem = containerFor(server.database.client, 'Users').item(request.viewer.id, request.viewer.id)
const { resource: viewer, requestCharge } = await viewerItem.read<IUser>()
const { resource: viewer, requestCharge } = await viewerItem.read<User>()
request.log.trace('Get: %d', requestCharge)
if (!viewer) return serverError(reply)
@ -93,7 +93,7 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
viewer.privacy = request.body.privacy
}
await viewerItem.replace<IUser>(viewer)
await viewerItem.replace<User>(viewer)
return viewer
})
@ -123,12 +123,21 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
if (!server.database) return serverError(reply)
const userContainer = containerFor(server.database.client, 'Users')
const user = await getItem<IUser>(userContainer, request.params.id, request.log)
const user = await getItem<User>({
container: userContainer,
id: request.params.id,
logger: request.log
})
if (!user) return notFoundError(reply)
if (request.viewer) {
const viewer = await getItem<IUser>(userContainer, request.viewer.id, request.log)
const viewer = await getItem<User>({
container: userContainer,
id: request.viewer.id,
logger: request.log
})
if (!viewer) return serverError(reply)
if (!viewer.group) return unauthorizedError(reply)
@ -166,8 +175,8 @@ function subscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
if (request.viewer.id === request.params.id) return badRequestError(reply)
const userContainer = containerFor(server.database.client, 'Users')
const user = await getItem<IUser>(userContainer, request.params.id, request.log)
const viewer = await getItem<IUser>(userContainer, request.viewer.id, request.log)
const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log })
const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id, logger: request.log })
if (!user) return notFoundError(reply)
if (!viewer) return serverError(reply)
@ -178,7 +187,7 @@ function subscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
viewer: viewer.id,
})
const subscriptions = await queryItems<IUserSubscription>(userContainer, subscriptionQuery, request.log)
const subscriptions = await queryItems<UserSubscription>({ container: userContainer, query: subscriptionQuery, logger: request.log })
if (subscriptions.length > 0) return badRequestError(reply, 'Already subscribed')
let pending = false
@ -204,10 +213,15 @@ function subscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
viewerGroup: viewer.group.id,
})
const blocks = await queryItems<IGroupBlock>(containerFor(server.database.client, 'Groups'), blockQuery, request.log)
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<IUserSubscription>({
await userContainer.items.create<UserSubscription>({
subscriberId: user.id,
pk: request.viewer.id,
t: 'subscription',
@ -240,8 +254,8 @@ function unsubscribeRoute(server: FastifyInstance<Server, IncomingMessage, Serve
if (!request.viewer) return unauthorizedError(reply)
const userContainer = containerFor(server.database.client, 'Users')
const user = await getItem<IUser>(userContainer, request.params.id, request.log)
const viewer = await getItem<IUser>(userContainer, request.viewer.id, request.log)
const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log })
const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id, logger: request.log })
if (!user) return notFoundError(reply)
if (!viewer) return serverError(reply)
@ -251,7 +265,7 @@ function unsubscribeRoute(server: FastifyInstance<Server, IncomingMessage, Serve
viewer: viewer.id,
})
const subscriptions = await queryItems<IUserSubscription>(userContainer, subscriptionQuery, request.log)
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()
}
@ -294,12 +308,16 @@ function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespo
if (!request.viewer) return unauthorizedError(reply)
const userContainer = containerFor(server.database.client, 'Users')
const user = await getItem<IUser>(userContainer, request.params.id, request.log)
const user = await getItem<User>({
container: userContainer,
id: request.params.id,
logger: request.log
})
if (!user) return notFoundError(reply)
if (!user.group) return badRequestError(reply)
await userContainer.items.create<IUserBlock>({
await userContainer.items.create<UserBlock>({
blockedId: user.id,
pk: request.viewer.id,
t: 'block',
@ -308,7 +326,7 @@ function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespo
created: Date.now(),
})
await containerFor(server.database.client, 'Groups').items.create<IGroupBlock>({
await containerFor(server.database.client, 'Groups').items.create<GroupBlock>({
pk: user.group.id,
t: 'block',
blockedId: user.id,
@ -343,7 +361,11 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
const userContainer = containerFor(server.database.client, 'Users')
const groupContainer = containerFor(server.database.client, 'Groups')
const user = await getItem<IUser>(userContainer, request.params.id, request.log)
const user = await getItem<User>({
container: userContainer,
id: request.params.id,
logger: request.log
})
if (!user) return notFoundError(reply)
if (!user.group) return badRequestError(reply, 'Invalid operation')
@ -353,7 +375,12 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
blocked: user.id,
})
const userBlocks = await queryItems<IUserBlock>(userContainer, userBlockQuery, request.log)
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()
}
@ -367,7 +394,12 @@ function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
}
)
const groupBlocks = await queryItems<IGroupBlock>(groupContainer, groupBlockQuery, request.log)
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()
}

1
src/schemas.ts

@ -6,6 +6,7 @@ export const tokenResponseSchema: JSONSchema = {
id: { type: 'string' },
access: { type: 'string' },
refresh: { type: 'string' },
expires: { type: 'number' },
},
}

5
src/server.ts

@ -1,6 +1,7 @@
import { config } from 'dotenv'
import fastify from 'fastify'
import helmet from 'fastify-helmet'
import cors from 'fastify-cors'
import api from './plugins/api'
@ -14,6 +15,10 @@ const server = fastify({
})
server.register(helmet)
server.register(cors, {
origin: true,
})
server.register(api)
const start = async () => {

80
src/types/collections.ts

@ -13,16 +13,16 @@
// Points: total reward value + likes
export type IUserItemType = 'user' | 'token' | 'post' | 'follow' | 'timeline'
export type IUserPrivacyType = 'public' | 'group' | 'subscribers' | 'private'
export type IUserTransactionType = 'purchase' | 'award'
export type IGroupStatus = 'pending' | 'paid'
export type IGroupItemType = 'group' | 'membership' | 'report' | 'block'
export type IGroupMembershipType = 'admin' | 'moderator' | 'member'
export type IReportStatus = 'pending' | 'complete'
export type IBlockType = 'user' | 'group'
export type UserItemType = 'user' | 'token' | 'post' | 'follow' | 'timeline'
export type UserPrivacyType = 'public' | 'group' | 'subscribers' | 'private'
export type UserTransactionType = 'purchase' | 'award'
export type GroupStatus = 'pending' | 'paid'
export type GroupItemType = 'group' | 'membership' | 'report' | 'block'
export type GroupMembershipType = 'admin' | 'moderator' | 'member'
export type ReportStatus = 'pending' | 'complete'
export type BlockType = 'user' | 'group'
export interface IGroupListing {
export interface GroupListing {
id: string
pk: 'pk'
name: string
@ -32,11 +32,11 @@ export interface IGroupListing {
posts: number
awards: number
points: number
latestAwards: IPostAwardPartial[]
latestAwards: PostAwardPartial[]
created: number
}
export interface IGroup {
export interface Group {
id: string
pk: string // ID
t: 'group'
@ -47,29 +47,29 @@ export interface IGroup {
coverImageUrl?: string
open: boolean
requiresApproval: boolean
status: IGroupStatus
status: GroupStatus
active: boolean
created: number
}
export interface IGroupPartial {
export interface GroupPartial {
id: string
name: string
imageUrl?: string
coverImageUrl?: string
}
export interface IGroupMembership {
export interface GroupMembership {
id: string
pk: string // Group ID
t: 'membership'
userId: string
pending: boolean
membership: IGroupMembershipType
membership: GroupMembershipType
created: number
}
export interface IGroupReport {
export interface GroupReport {
id: string
pk: string // Group ID
t: 'report'
@ -80,7 +80,7 @@ export interface IGroupReport {
created: number
}
export interface IGroupBlock {
export interface GroupBlock {
id?: string
pk: string // Group ID
t: 'block'
@ -89,11 +89,11 @@ export interface IGroupBlock {
created: number
}
export interface IUser {
export interface User {
id: string
pk: string // ID
t: 'user'
group?: IGroupPartial
group?: GroupPartial
name: string
about?: string
imageUrl?: string
@ -101,7 +101,7 @@ export interface IUser {
email: string
emailVerified: boolean
passwordHash: string
installations: IInstallation[]
installations: Installation[]
awards: number // Total Awards
points: number
balance: number // Currency (Flex)
@ -109,13 +109,13 @@ export interface IUser {
subscribedCount: number
pending: boolean
requiresApproval: boolean
privacy: IUserPrivacyType
privacy: UserPrivacyType
paid: boolean
active: boolean
created: number // Timestamp
}
export interface IUserToken {
export interface UserToken {
id: string
pk: string // userId
t: 'token'
@ -125,7 +125,7 @@ export interface IUserToken {
created: number
}
export interface IUserPost {
export interface UserPost {
id?: string
postId: string
pk: string // userId
@ -133,7 +133,7 @@ export interface IUserPost {
created: number
}
export interface IUserSubscription {
export interface UserSubscription {
id?: string
subscriberId: string
pk: string
@ -142,12 +142,12 @@ export interface IUserSubscription {
created: number
}
export interface IUserBlock {
export interface UserBlock {
id?: string
blockedId: string
pk: string
t: 'block'
blockType: IBlockType
blockType: BlockType
description?: string
created: number
}
@ -156,14 +156,14 @@ export interface IUserTransaction {
id: string
pk: string
t: 'transaction'
transactionType: IUserTransactionType
transactionType: UserTransactionType
fromUserId: string
toUserId: string
amount: number
created: number
}
export interface IUserTimelinePost {
export interface UserTimelinePost {
id?: string
postId: string
pk: string // userId
@ -171,19 +171,19 @@ export interface IUserTimelinePost {
created: number
}
export interface IPostAttachment {
export interface PostAttachment {
imageUrl: string
caption?: string
cover?: string
}
export interface IStatus {
export interface Status {
imageUrl: string
text: string
created: number
}
export interface IPost {
export interface Post {
id: string
pk: string // postId
t: 'post'
@ -192,11 +192,11 @@ export interface IPost {
parents: string[] // Post IDs
text?: string
cover?: string
attachments: IPostAttachment[]
status?: IStatus
attachments: PostAttachment[]
status?: Status
visible: boolean
awards: number
latestAwards: IPostAwardPartial[]
latestAwards: PostAwardPartial[]
created: number
}
@ -213,7 +213,7 @@ export interface IPostAward {
created: number
}
export interface IPostAwardPartial {
export interface PostAwardPartial {
userId: string
imageUrl: string
text: string
@ -221,20 +221,20 @@ export interface IPostAwardPartial {
created: number
}
export interface IPostRelationship {
export interface PostRelationship {
id: string
parents: string[] // Post IDs
pk: string // root post ID
}
export interface IInstallation {
export interface Installation {
id: string
appId: string
settings: object
created: number
}
interface IAppRevision {
export interface AppRevision {
version: string
displayName: string
imageUrl: string
@ -250,13 +250,13 @@ interface IAppRevision {
composeCallbackUrl: string
}
interface IApp {
export interface App {
id: string
userId: string
name: string
version: string
rating: number
revisions: IAppRevision[]
revisions: AppRevision[]
currentRevisionIndex: number
publicKey: string
privateKey: string

16
src/types/index.ts

@ -1,12 +1,22 @@
export interface IQueryParams {
export interface HttpFormError {
field?: string
message: string
}
export interface HttpError {
message: string
errors?: HttpFormError[]
}
export interface QueryParams {
[key: string]: string | string[] | number | boolean
}
export interface IDatabaseItem {
export interface DatabaseItem {
id: string
}
export interface IAwardDefinition {
export interface AwardDefinition {
id: string
name: string
imageUrl: string

10
tsconfig.json

@ -12,11 +12,11 @@
"paths": {
"*": [
"node_modules/*",
"src/types/*",
],
},
"src/types/*"
]
}
},
"include": [
"src/**/*",
],
"src/**/*"
]
}
Loading…
Cancel
Save