Dwayne Harris 5 years ago
parent
commit
2b8652c133
  1. 4
      src/constants.ts
  2. 36
      src/lib/media.ts
  3. 114
      src/plugins/api/apps.ts
  4. 51
      src/plugins/api/authentication.ts
  5. 58
      src/plugins/api/groups.ts
  6. 4
      src/plugins/api/index.ts
  7. 143
      src/plugins/api/media.ts
  8. 51
      src/plugins/api/uploads.ts
  9. 15
      src/plugins/api/users.ts
  10. 8
      src/schemas.ts
  11. 25
      src/types/collections.ts

4
src/constants.ts

@ -6,4 +6,6 @@ export const SHORT_TEXT_LENGTH = 100
export const SUBSCRIBER_MAX_SIZE = 100 export const SUBSCRIBER_MAX_SIZE = 100
export const GROUP_LISTING_PARTITION_KEY = 'pk' export const GROUP_LISTING_PARTITION_KEY = 'pk'
export const APP_PARTITION_KEY = 'pk'
export const APP_PARTITION_KEY = 'apk'
export const INSTALLATION_PARTITION_KEY = 'ipk'
export const MEDIA_PARTITION_KEY = 'pk'

36
src/lib/media.ts

@ -0,0 +1,36 @@
import { Container } from '@azure/cosmos'
import { BlockBlobURL, SharedKeyCredential, Aborter, ContainerSASPermissions, generateBlobSASQueryParameters, AnonymousCredential } from '@azure/storage-blob'
import moment from 'moment'
import { MEDIA_PARTITION_KEY } from '../constants'
import { Media } from '../types/collections'
export function generateSAS(permissions: string, expirationMinutes: number) {
const sharedKeyCredential = new SharedKeyCredential(process.env.BLOB_STORAGE_ACCOUNT!, process.env.BLOB_STORAGE_ACCOUNT_KEY!)
return generateBlobSASQueryParameters({
containerName: process.env.BLOB_STORAGE_CONTAINER!,
permissions: ContainerSASPermissions.parse(permissions).toString(),
startTime: new Date(),
expiryTime: moment().add(expirationMinutes, 'm').toDate(),
}, sharedKeyCredential).toString()
}
export async function deleteMedia(name: string) {
const blockBlobURL = new BlockBlobURL(
`https://${process.env.BLOB_STORAGE_ACCOUNT!}.blob.core.windows.net/${process.env.BLOB_STORAGE_CONTAINER!}/${name}`,
BlockBlobURL.newPipeline(new SharedKeyCredential(process.env.BLOB_STORAGE_ACCOUNT!, process.env.BLOB_STORAGE_ACCOUNT_KEY!))
)
await blockBlobURL.delete(Aborter.none)
}
export async function attachMedia(container: Container, name: string) {
const mediaItem = container.item(name, MEDIA_PARTITION_KEY)
const { resource: media } = await mediaItem.read<Media>()
await mediaItem.replace<Media>({
...media,
attached: true,
})
}

114
src/plugins/api/apps.ts

@ -16,11 +16,12 @@ import { getUsers } from '../../lib/collections'
import { generateString } from '../../lib/crypto' import { generateString } from '../../lib/crypto'
import { containerFor, getItem, normalize, queryItems, createQuerySpec } from '../../lib/database' import { containerFor, getItem, normalize, queryItems, createQuerySpec } from '../../lib/database'
import { unauthorizedError, serverError, badRequestError, notFoundError } from '../../lib/errors' import { unauthorizedError, serverError, badRequestError, notFoundError } from '../../lib/errors'
import { attachMedia, deleteMedia } from '../../lib/media'
import { createInstallationId } from '../../lib/utils' import { createInstallationId } from '../../lib/utils'
import { APP_PARTITION_KEY, MAX_NAME_LENGTH } from '../../constants'
import { APP_PARTITION_KEY, MAX_NAME_LENGTH, INSTALLATION_PARTITION_KEY } from '../../constants'
import { App, User } from '../../types/collections'
import { App, User, Installation } from '../../types/collections'
import { PluginOptions } from '../../types' import { PluginOptions } from '../../types'
function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
@ -322,6 +323,12 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
created: Date.now(), created: Date.now(),
}) })
const mediaContainer = containerFor(server.database.client, 'Media')
if (imageUrl) await attachMedia(mediaContainer, imageUrl)
if (coverImageUrl) await attachMedia(mediaContainer, coverImageUrl)
if (iconImageUrl) await attachMedia(mediaContainer, iconImageUrl)
return { return {
id, id,
} }
@ -388,6 +395,7 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if (!request.viewer) return unauthorizedError(reply) if (!request.viewer) return unauthorizedError(reply)
const container = containerFor(server.database.client, 'Apps') const container = containerFor(server.database.client, 'Apps')
const mediaContainer = containerFor(server.database.client, 'Media')
const { const {
version, version,
@ -410,6 +418,14 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
const { resource: app } = await appItem.read<App>() const { resource: app } = await appItem.read<App>()
if (!app) return notFoundError(reply) if (!app) return notFoundError(reply)
if (app.imageUrl && !imageUrl) await deleteMedia(app.imageUrl)
if (app.coverImageUrl && !coverImageUrl) await deleteMedia(app.coverImageUrl)
if (app.iconImageUrl && !iconImageUrl) await deleteMedia(app.iconImageUrl)
if (!app.imageUrl && imageUrl) await attachMedia(mediaContainer, imageUrl)
if (!app.coverImageUrl && coverImageUrl) await attachMedia(mediaContainer, coverImageUrl)
if (!app.iconImageUrl && iconImageUrl) await attachMedia(mediaContainer, iconImageUrl)
await appItem.replace<App>({ await appItem.replace<App>({
...app, ...app,
version, version,
@ -449,7 +465,7 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
updated: Date.now(), updated: Date.now(),
}) })
reply.code(201)
reply.code(204)
}) })
} }
@ -485,20 +501,8 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
if (!app) return notFoundError(reply) if (!app) return notFoundError(reply)
const userContainer = containerFor(server.database.client, 'Users') const userContainer = containerFor(server.database.client, 'Users')
let installed = false
let attributes = ['id', 'version', 'name', 'imageUrl', 'coverImageUrl', 'iconImageUrl', 'about', 'websiteUrl', 'companyName', 'rating', 'users', 'updated', 'created'] let attributes = ['id', 'version', 'name', 'imageUrl', 'coverImageUrl', 'iconImageUrl', 'about', 'websiteUrl', 'companyName', 'rating', 'users', 'updated', 'created']
if (request.viewer) {
const viewer = await getItem<User>({
container: userContainer,
id: request.viewer.id,
})
if (viewer && viewer.installations.find(i => i.appId === app.id)) {
installed = true
}
}
if (request.viewer && request.viewer.id === app.userId) { if (request.viewer && request.viewer.id === app.userId) {
attributes = [...attributes, 'publicKey', 'privateKey', 'revisions', 'composerUrl', 'rendererUrl'] attributes = [...attributes, 'publicKey', 'privateKey', 'revisions', 'composerUrl', 'rendererUrl']
} }
@ -509,7 +513,6 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons
container: userContainer, container: userContainer,
id: app.userId, id: app.userId,
}), }),
installed,
} }
}) })
} }
@ -548,7 +551,20 @@ function installRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
const { resource: viewer } = await viewerItem.read<User>() const { resource: viewer } = await viewerItem.read<User>()
if (viewer.installations.find(i => i.appId === app.id)) {
const installations = await queryItems<Installation>({
container: appContainer,
query: createQuerySpec(
`SELECT a.id FROM Apps a WHERE a.pk = @pk AND a.userId = @userId AND a.appId = @appId`,
{
pk: INSTALLATION_PARTITION_KEY,
userId: request.viewer.id,
appId: app.id,
}
),
logger: request.log,
})
if (installations.length > 0) {
reply.code(204) reply.code(204)
return return
} }
@ -558,16 +574,22 @@ function installRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
users: app.users + 1, users: app.users + 1,
}) })
const installation: Installation = {
id: createInstallationId(),
pk: INSTALLATION_PARTITION_KEY,
userId: request.viewer.id,
appId: app.id,
settings: {},
created: Date.now(),
}
await appContainer.items.create<Installation>(installation)
await viewerItem.replace<User>({ await viewerItem.replace<User>({
...viewer, ...viewer,
installations: [ installations: [
...viewer.installations, ...viewer.installations,
{
id: createInstallationId(),
appId: app.id,
settings: {},
created: Date.now(),
},
installation.id,
] ]
}) })
@ -609,11 +631,27 @@ function uninstallRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
const { resource: viewer } = await viewerItem.read<User>() const { resource: viewer } = await viewerItem.read<User>()
if (!viewer.installations.find(i => i.appId === app.id)) {
const installations = await queryItems<Installation>({
container: appContainer,
query: createQuerySpec(
`SELECT a.id FROM Apps a WHERE a.pk = @pk AND a.userId = @userId AND a.appId = @appId`,
{
pk: INSTALLATION_PARTITION_KEY,
userId: request.viewer.id,
appId: app.id,
}
),
logger: request.log,
})
if (installations.length === 0) {
reply.code(204) reply.code(204)
return return
} }
const installation = installations[0]
const installationItem = appContainer.item(installation.id, INSTALLATION_PARTITION_KEY)
await appItem.replace<App>({ await appItem.replace<App>({
...app, ...app,
users: app.users - 1, users: app.users - 1,
@ -621,9 +659,11 @@ function uninstallRoute(server: FastifyInstance<Server, IncomingMessage, ServerR
await viewerItem.replace<User>({ await viewerItem.replace<User>({
...viewer, ...viewer,
installations: viewer.installations.filter(i => i.appId !== app.id),
installations: viewer.installations.filter(i => i !== installation.id),
}) })
await installationItem.delete()
reply.code(204) reply.code(204)
}) })
} }
@ -640,6 +680,7 @@ function installationsRoute(server: FastifyInstance<Server, IncomingMessage, Ser
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
id: { type: 'string' },
app: appSchema, app: appSchema,
settings: { type: 'object' }, settings: { type: 'object' },
created: { type: 'number' }, created: { type: 'number' },
@ -664,19 +705,28 @@ function installationsRoute(server: FastifyInstance<Server, IncomingMessage, Ser
if (!viewer) return unauthorizedError(reply) if (!viewer) return unauthorizedError(reply)
const apps = await queryItems<User>({
container: containerFor(server.database.client, 'Apps'),
const container = containerFor(server.database.client, 'Apps')
const installations = await queryItems<Installation>({
container,
query: createQuerySpec( query: createQuerySpec(
`SELECT * FROM Apps a WHERE ARRAY_CONTAINS(@ids, a.id)`,
{
ids: viewer.installations.map(installation => installation.appId),
}
`SELECT * FROM Apps a WHERE a.pk = @pk AND ARRAY_CONTAINS(@ids, a.id)`,
{ ids: viewer.installations, pk: INSTALLATION_PARTITION_KEY }
),
logger: request.log,
})
const apps = await queryItems<App>({
container,
query: createQuerySpec(
`SELECT * FROM Apps a WHERE a.pk = @pk AND ARRAY_CONTAINS(@ids, a.id)`,
{ ids: installations.map(i => i.appId), pk: APP_PARTITION_KEY }
), ),
logger: request.log, logger: request.log,
}) })
return { return {
installations: viewer.installations.map(installation => {
installations: installations.map(installation => {
return { return {
...installation, ...installation,
app: apps.find(app => app.id === installation.appId), app: apps.find(app => app.id === installation.appId),

51
src/plugins/api/authentication.ts

@ -10,13 +10,14 @@ import {
import { Server, IncomingMessage, ServerResponse } from 'http' import { Server, IncomingMessage, ServerResponse } from 'http'
import { MIN_ID_LENGTH, MAX_ID_LENGTH, MAX_NAME_LENGTH, MIN_PASSWORD_LENGTH } from '../../constants'
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 { tokenResponseSchema, selfSchema, errorSchema } from '../../schemas'
import { createAccessToken, createRefreshToken } from '../../lib/authentication' import { createAccessToken, createRefreshToken } from '../../lib/authentication'
import { hashPassword, verifyPassword, JWT } from '../../lib/crypto' import { hashPassword, verifyPassword, JWT } from '../../lib/crypto'
import { containerFor, getItem, queryItems, normalize, createQuerySpec } from '../../lib/database' import { containerFor, getItem, queryItems, normalize, createQuerySpec } from '../../lib/database'
import { badRequestError, badRequestFormError, unauthorizedError, serverError } from '../../lib/errors' import { badRequestError, badRequestFormError, unauthorizedError, serverError } from '../../lib/errors'
import { tokenFromHeader } from '../../lib/http' import { tokenFromHeader } from '../../lib/http'
import { attachMedia } from '../../lib/media'
import { createInstallationId } from '../../lib/utils' import { createInstallationId } from '../../lib/utils'
import { import {
@ -32,6 +33,7 @@ import {
GroupMembershipType, GroupMembershipType,
GroupRegistrationType, GroupRegistrationType,
App, App,
Installation,
} from '../../types/collections' } from '../../types/collections'
import { PluginOptions } from '../../types' import { PluginOptions } from '../../types'
@ -43,6 +45,9 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
password: string password: string
requiresApproval: boolean requiresApproval: boolean
privacy: string privacy: string
about?: string
imageUrl?: string
coverImageUrl?: string
group?: string group?: string
invitation?: string invitation?: string
} }
@ -75,6 +80,9 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
type: 'string', type: 'string',
enum: ['public', 'group', 'subscribers', 'private'], enum: ['public', 'group', 'subscribers', 'private'],
}, },
about: { type: 'string' },
imageUrl: { type: 'string' },
coverImageUrl: { type: 'string' },
group: { type: 'string' }, group: { type: 'string' },
invitation: { type: 'string' }, invitation: { type: 'string' },
}, },
@ -89,7 +97,7 @@ 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>('/api/register', options, async (request, reply) => {
if (!server.database) return serverError(reply) if (!server.database) return serverError(reply)
const { name, email, password, requiresApproval, privacy, invitation: code } = request.body
const { name, email, password, requiresApproval, privacy, about, imageUrl, coverImageUrl, invitation: code } = request.body
const id = normalize(request.body.id) const id = normalize(request.body.id)
const userContainer = containerFor(server.database.client, 'Users') const userContainer = containerFor(server.database.client, 'Users')
@ -137,33 +145,47 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
groupPartial = { groupPartial = {
id: group.id, id: group.id,
name: group.name, name: group.name,
imageUrl: group.imageUrl,
coverImageUrl: group.coverImageUrl,
iconImageUrl: group.iconImageUrl,
} }
} }
const appContainer = containerFor(server.database.client, 'Apps')
const apps = await queryItems<App>({ const apps = await queryItems<App>({
container: containerFor(server.database.client, 'Apps'),
container: appContainer,
query: 'SELECT * FROM Apps a WHERE a.active = true AND a.preinstall = true', query: 'SELECT * FROM Apps a WHERE a.active = true AND a.preinstall = true',
logger: request.log, logger: request.log,
}) })
const installations: string[] = []
for (const app of apps) {
const installation: Installation = {
id: createInstallationId(),
pk: INSTALLATION_PARTITION_KEY,
userId: id,
appId: app.id,
settings: {},
created: Date.now(),
}
await appContainer.items.create<Installation>(installation)
installations.push(installation.id)
}
const user: User = { const user: User = {
id, id,
pk: id, pk: id,
t: UserItemType.User, t: UserItemType.User,
group: groupPartial, group: groupPartial,
name, name,
about: '',
about,
email, email,
emailVerified: false, emailVerified: false,
passwordHash: await hashPassword(password), passwordHash: await hashPassword(password),
installations: apps.map(app => ({
id: createInstallationId(),
appId: app.id,
settings: {},
created: Date.now(),
})),
imageUrl,
coverImageUrl,
installations,
awards: 0, awards: 0,
points: 0, points: 0,
balance: 0, balance: 0,
@ -205,6 +227,11 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
} }
} }
const mediaContainer = containerFor(server.database.client, 'Media')
if (imageUrl) await attachMedia(mediaContainer, imageUrl)
if (coverImageUrl) await attachMedia(mediaContainer, coverImageUrl)
return { return {
id, id,
access: await createAccessToken(id), access: await createAccessToken(id),

58
src/plugins/api/groups.ts

@ -17,6 +17,7 @@ import { unauthorizedError, badRequestError, notFoundError, serverError } from '
import { getUsers, getUserMembership } from '../../lib/collections' import { getUsers, getUserMembership } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database' import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import { createInvitationCode } from '../../lib/utils' import { createInvitationCode } from '../../lib/utils'
import { attachMedia, deleteMedia } from '../../lib/media'
import { import {
User, User,
@ -89,6 +90,9 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
name: string name: string
about?: string about?: string
registration: GroupRegistrationType registration: GroupRegistrationType
imageUrl?: string
coverImageUrl?: string
iconImageUrl?: string
} }
const options: RouteShorthandOptions = { const options: RouteShorthandOptions = {
@ -107,6 +111,9 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
type: 'string', type: 'string',
enum: ['open', 'approval', 'closed'], enum: ['open', 'approval', 'closed'],
}, },
imageUrl: { type: 'string' },
coverImageUrl: { type: 'string' },
iconImageUrl: { type: 'string' },
}, },
}, },
response: { response: {
@ -132,7 +139,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
if (viewer.group) return badRequestError(reply) if (viewer.group) return badRequestError(reply)
const { name, about, registration } = request.body
const { name, about, registration, imageUrl, coverImageUrl, iconImageUrl } = request.body
const id = normalize(name) const id = normalize(name)
const existingGroup = await getItem<Group>({ container: groupContainer, id }) const existingGroup = await getItem<Group>({ container: groupContainer, id })
@ -146,6 +153,9 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
name, name,
about, about,
registration, registration,
imageUrl,
coverImageUrl,
iconImageUrl,
status: GroupStatus.Pending, status: GroupStatus.Pending,
active: true, active: true,
created: Date.now(), created: Date.now(),
@ -169,8 +179,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
group: { group: {
id: group.id, id: group.id,
name: group.name, name: group.name,
imageUrl: group.imageUrl,
coverImageUrl: group.coverImageUrl,
iconImageUrl: group.iconImageUrl,
}, },
}) })
@ -182,6 +191,12 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
created: Date.now(), created: Date.now(),
}) })
const mediaContainer = containerFor(server.database.client, 'Media')
if (imageUrl) await attachMedia(mediaContainer, imageUrl)
if (coverImageUrl) await attachMedia(mediaContainer, coverImageUrl)
if (iconImageUrl) await attachMedia(mediaContainer, iconImageUrl)
return { return {
id: group.id, id: group.id,
} }
@ -264,6 +279,9 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
name?: string name?: string
about?: string about?: string
registration?: string registration?: string
imageUrl?: string
coverImageUrl?: string
iconImageUrl?: string
} }
const options: RouteShorthandOptions = { const options: RouteShorthandOptions = {
@ -281,6 +299,9 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
type: 'string', type: 'string',
enum: ['open', 'approval', 'closed'], enum: ['open', 'approval', 'closed'],
}, },
imageUrl: { type: 'string' },
coverImageUrl: { type: 'string' },
iconImageUrl: { type: 'string' },
}, },
}, },
response: { response: {
@ -306,16 +327,41 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
const { resource: groupListing } = await groupListingItem.read<GroupListing>() const { resource: groupListing } = await groupListingItem.read<GroupListing>()
const {
name,
about,
registration,
imageUrl,
coverImageUrl,
iconImageUrl,
} = request.body
const mediaContainer = containerFor(server.database.client, 'Media')
if (group.imageUrl && !imageUrl) await deleteMedia(group.imageUrl)
if (group.coverImageUrl && !coverImageUrl) await deleteMedia(group.coverImageUrl)
if (group.iconImageUrl && !iconImageUrl) await deleteMedia(group.iconImageUrl)
if (!group.imageUrl && imageUrl) await attachMedia(mediaContainer, imageUrl)
if (!group.coverImageUrl && coverImageUrl) await attachMedia(mediaContainer, coverImageUrl)
if (!group.iconImageUrl && iconImageUrl) await attachMedia(mediaContainer, iconImageUrl)
interface Updates { interface Updates {
name?: string name?: string
about?: string about?: string
registration?: GroupRegistrationType registration?: GroupRegistrationType
imageUrl?: string
coverImageUrl?: string
iconImageUrl?: string
} }
let updates: Updates = {} let updates: Updates = {}
if (request.body.name) updates.name = request.body.name
if (request.body.about) updates.about = request.body.about
if (request.body.registration) updates.registration = request.body.registration as GroupRegistrationType
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
await groupItem.replace<Group>({ await groupItem.replace<Group>({
...group, ...group,

4
src/plugins/api/index.ts

@ -9,8 +9,8 @@ import { tokenFromHeader } from '../../lib/http'
import apps from './apps' import apps from './apps'
import authentication from './authentication' import authentication from './authentication'
import groups from './groups' import groups from './groups'
import media from './media'
import posts from './posts' import posts from './posts'
import uploads from './uploads'
import users from './users' import users from './users'
import { PluginOptions, HttpError } from '../../types' import { PluginOptions, HttpError } from '../../types'
@ -83,8 +83,8 @@ const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = a
server.register(apps) server.register(apps)
server.register(authentication) server.register(authentication)
server.register(groups) server.register(groups)
server.register(media)
server.register(posts) server.register(posts)
server.register(uploads)
server.register(users) server.register(users)
} }

143
src/plugins/api/media.ts

@ -0,0 +1,143 @@
import {
FastifyInstance,
Plugin,
DefaultQuery,
DefaultParams,
DefaultHeaders,
DefaultBody,
RouteShorthandOptions,
} from 'fastify'
import { Server, IncomingMessage, ServerResponse } from 'http'
import { MEDIA_PARTITION_KEY } from '../../constants'
import { errorSchema } from '../../schemas'
import { containerFor, getItem } from '../../lib/database'
import { badRequestError, serverError } from '../../lib/errors'
import { deleteMedia, generateSAS } from '../../lib/media'
import { createId } from '../../lib/utils'
import { Media } from '../../types/collections'
import { PluginOptions } from '../../types'
function getSASRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
const options: RouteShorthandOptions = {
schema: {
response: {
200: {
type: 'object',
properties: {
sas: { type: 'string' },
id: { type: 'string' },
},
},
},
},
}
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/api/sas', options, async () => {
return {
sas: generateSAS('arcw', 5),
id: createId(),
}
})
}
function addRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Body {
name: string
size: number
type: string
originalName: string
}
const options: RouteShorthandOptions = {
schema: {
body: {
type: 'object',
required: ['name', 'size', 'type', 'originalName'],
properties: {
name: { type: 'string' },
size: { type: 'number' },
type: { type: 'string' },
originalName: { type: 'string' },
},
},
response: {
400: errorSchema,
},
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/media', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const { name, size, type, originalName } = request.body
const container = containerFor(server.database.client, 'Media')
const item = await getItem<Media>({
container,
id: name,
partitionKey: MEDIA_PARTITION_KEY,
})
reply.code(204)
if (item) return
await container.items.create<Media>({
id: name,
pk: MEDIA_PARTITION_KEY,
size,
type,
originalName,
attached: false,
created: Date.now(),
})
})
}
function deleteRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Body {
name: string
}
const options: RouteShorthandOptions = {
schema: {
body: {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string' },
},
},
response: {
400: errorSchema,
},
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/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)
const { resource: media } = await mediaItem.read<Media>()
if (!media) return badRequestError(reply)
reply.code(204)
if (media.attached) return
await mediaItem.delete()
await deleteMedia(request.body.name)
})
}
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
getSASRoute(server)
addRoute(server)
deleteRoute(server)
}
export default plugin

51
src/plugins/api/uploads.ts

@ -1,51 +0,0 @@
import {
FastifyInstance,
Plugin,
DefaultQuery,
DefaultParams,
DefaultHeaders,
DefaultBody,
RouteShorthandOptions,
} from 'fastify'
import { Server, IncomingMessage, ServerResponse } from 'http'
import moment from 'moment'
import { SharedKeyCredential, ContainerSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob'
import { createId } from '../../lib/utils'
import { PluginOptions } from '../../types'
function getSASRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
const options: RouteShorthandOptions = {
schema: {
response: {
200: {
type: 'object',
properties: {
sas: { type: 'string' },
id: { type: 'string' },
},
},
},
},
}
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/api/sas', options, async () => {
const sharedKeyCredential = new SharedKeyCredential(process.env.BLOB_STORAGE_ACCOUNT!, process.env.BLOB_STORAGE_ACCOUNT_KEY!)
return {
sas: generateBlobSASQueryParameters({
containerName: process.env.BLOB_STORAGE_CONTAINER!,
permissions: ContainerSASPermissions.parse('arcw').toString(),
startTime: new Date(),
expiryTime: moment().add(5, 'm').toDate(),
}, sharedKeyCredential).toString(),
id: createId(),
}
})
}
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
getSASRoute(server)
}
export default plugin

15
src/plugins/api/users.ts

@ -13,6 +13,7 @@ import { Server, IncomingMessage, ServerResponse } from 'http'
import { unauthorizedError, serverError, notFoundError, badRequestError } from '../../lib/errors' import { unauthorizedError, serverError, notFoundError, badRequestError } from '../../lib/errors'
import { getUserBlocks } from '../../lib/collections' import { getUserBlocks } from '../../lib/collections'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database' 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 } from '../../constants'
import { userSchema, selfSchema, errorSchema } from '../../schemas' import { userSchema, selfSchema, errorSchema } from '../../schemas'
@ -140,14 +141,20 @@ function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
viewer.privacy = request.body.privacy viewer.privacy = request.body.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 && request.body.imageUrl) await attachMedia(mediaContainer, request.body.imageUrl)
if (!viewer.coverImageUrl && request.body.coverImageUrl) await attachMedia(mediaContainer, request.body.coverImageUrl)
if (request.body.imageUrl) { if (request.body.imageUrl) {
const imageUrl = request.body.imageUrl.trim()
if (imageUrl !== '') viewer.imageUrl = imageUrl
viewer.imageUrl = request.body.imageUrl
} }
if (request.body.coverImageUrl) { if (request.body.coverImageUrl) {
const coverImageUrl = request.body.coverImageUrl.trim()
if (coverImageUrl !== '') viewer.coverImageUrl = coverImageUrl
viewer.coverImageUrl = request.body.coverImageUrl
} }
await viewerItem.replace<User>(viewer) await viewerItem.replace<User>(viewer)

8
src/schemas.ts

@ -35,8 +35,7 @@ export const userSchema: JSONSchema = {
properties: { properties: {
id: { type: 'string' }, id: { type: 'string' },
name: { type: 'string' }, name: { type: 'string' },
imageUrl: { type: 'string' },
coverImageUrl: { type: 'string' },
iconImageUrl: { type: 'string' },
}, },
}, },
subscription: { type: 'string' }, subscription: { type: 'string' },
@ -63,7 +62,6 @@ export const appSchema: JSONSchema = {
users: { type: 'number' }, users: { type: 'number' },
updated: { type: 'number' }, updated: { type: 'number' },
created: { type: 'number' }, created: { type: 'number' },
installed: { type: 'boolean' },
publicKey: { type: 'string' }, publicKey: { type: 'string' },
privateKey: { type: 'string' }, privateKey: { type: 'string' },
@ -92,8 +90,7 @@ export const selfSchema: JSONSchema = {
properties: { properties: {
id: { type: 'string' }, id: { type: 'string' },
name: { type: 'string' }, name: { type: 'string' },
imageUrl: { type: 'string' },
coverImageUrl: { type: 'string' },
iconImageUrl: { type: 'string' },
}, },
}, },
requiresApproval: { type: 'boolean' }, requiresApproval: { type: 'boolean' },
@ -122,6 +119,7 @@ export const groupListingSchema: JSONSchema = {
about: { type: 'string' }, about: { type: 'string' },
imageUrl: { type: 'string' }, imageUrl: { type: 'string' },
coverImageUrl: { type: 'string' }, coverImageUrl: { type: 'string' },
iconImageUrl: { type: 'string' },
requiresApproval: { type: 'boolean' }, requiresApproval: { type: 'boolean' },
members: { type: 'number' }, members: { type: 'number' },
posts: { type: 'number' }, posts: { type: 'number' },

25
src/types/collections.ts

@ -12,7 +12,12 @@
// - Partition Key: pk (postId) // - Partition Key: pk (postId)
// Points: total reward value + likes // Points: total reward value + likes
import { GROUP_LISTING_PARTITION_KEY, APP_PARTITION_KEY } from '../constants'
import {
GROUP_LISTING_PARTITION_KEY,
APP_PARTITION_KEY,
INSTALLATION_PARTITION_KEY,
MEDIA_PARTITION_KEY,
} from '../constants'
export enum UserItemType { export enum UserItemType {
User = 'user', User = 'user',
@ -102,6 +107,7 @@ export interface Group {
codeOfConduct?: string codeOfConduct?: string
imageUrl?: string imageUrl?: string
coverImageUrl?: string coverImageUrl?: string
iconImageUrl?: string
registration: GroupRegistrationType registration: GroupRegistrationType
status: GroupStatus status: GroupStatus
active: boolean active: boolean
@ -111,8 +117,7 @@ export interface Group {
export interface GroupPartial { export interface GroupPartial {
id: string id: string
name: string name: string
imageUrl?: string
coverImageUrl?: string
iconImageUrl?: string
} }
export interface GroupMembership { export interface GroupMembership {
@ -179,7 +184,7 @@ export interface User {
email: string email: string
emailVerified: boolean emailVerified: boolean
passwordHash: string passwordHash: string
installations: Installation[]
installations: string[]
awards: number // Total Awards awards: number // Total Awards
points: number points: number
balance: number // Currency (Flex) balance: number // Currency (Flex)
@ -307,6 +312,8 @@ export interface PostRelationship {
export interface Installation { export interface Installation {
id: string id: string
pk: typeof INSTALLATION_PARTITION_KEY
userId: string
appId: string appId: string
settings: object settings: object
created: number created: number
@ -358,3 +365,13 @@ export interface App {
updated: number updated: number
created: number created: number
} }
export interface Media {
id: string
pk: typeof MEDIA_PARTITION_KEY
size: number
type: string
originalName: string
attached: boolean
created: number
}
Loading…
Cancel
Save