|
|
@ -14,8 +14,9 @@ import pick from 'lodash/pick' |
|
|
|
import { appSchema, errorSchema } from '../../schemas' |
|
|
|
import { getUsers } from '../../lib/collections' |
|
|
|
import { generateString } from '../../lib/crypto' |
|
|
|
import { containerFor, getItem, normalize } from '../../lib/database' |
|
|
|
import { containerFor, getItem, normalize, queryItems, createQuerySpec } from '../../lib/database' |
|
|
|
import { unauthorizedError, serverError, badRequestError, notFoundError } from '../../lib/errors' |
|
|
|
import { createInstallationId } from '../../lib/utils' |
|
|
|
|
|
|
|
import { APP_PARTITION_KEY, MAX_NAME_LENGTH } from '../../constants' |
|
|
|
|
|
|
@ -120,6 +121,7 @@ function appsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon |
|
|
|
a.websiteUrl, |
|
|
|
a.companyName |
|
|
|
a.rating, |
|
|
|
a.users, |
|
|
|
a.created |
|
|
|
FROM Apps a |
|
|
|
WHERE a.pk = '${APP_PARTITION_KEY}' AND a.active = true ORDER BY a.${sort}`,
|
|
|
@ -193,6 +195,7 @@ function selfAppsRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe |
|
|
|
a.websiteUrl, |
|
|
|
a.companyName |
|
|
|
a.rating, |
|
|
|
a.users, |
|
|
|
a.created |
|
|
|
FROM Apps a |
|
|
|
WHERE a.pk = '${APP_PARTITION_KEY}' AND a.userId = ${request.viewer.id} ORDER BY a.${sort}`,
|
|
|
@ -309,6 +312,7 @@ function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp |
|
|
|
initCallbackUrl, |
|
|
|
composeCallbackUrl, |
|
|
|
rating: 0, |
|
|
|
users: 0, |
|
|
|
publicKey: generateString(20), |
|
|
|
privateKey: generateString(40), |
|
|
|
preinstall: false, |
|
|
@ -479,24 +483,206 @@ function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespons |
|
|
|
}) |
|
|
|
|
|
|
|
if (!app) return notFoundError(reply) |
|
|
|
const userContainer = containerFor(server.database.client, 'Users') |
|
|
|
|
|
|
|
const user = await getItem<User>({ |
|
|
|
container: containerFor(server.database.client, 'Users'), |
|
|
|
id: app.userId, |
|
|
|
}) |
|
|
|
let installed = false |
|
|
|
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) { |
|
|
|
const attributes = ['id', 'version', 'name', 'imageUrl', 'coverImageUrl', 'iconImageUrl', 'about', 'websiteUrl', 'companyName', 'rating', 'updated', 'created', 'publicKey', 'privateKey', 'revisions'] |
|
|
|
return { |
|
|
|
...pick(app, attributes), |
|
|
|
user, |
|
|
|
attributes = [...attributes, 'publicKey', 'privateKey', 'revisions'] |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
...pick(app, attributes), |
|
|
|
user: await getItem<User>({ |
|
|
|
container: userContainer, |
|
|
|
id: app.userId, |
|
|
|
}), |
|
|
|
installed, |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function installRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|
|
|
interface Params { |
|
|
|
id: string |
|
|
|
} |
|
|
|
|
|
|
|
const options: RouteShorthandOptions = { |
|
|
|
schema: { |
|
|
|
params: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
id: { type: 'string' }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
response: { |
|
|
|
400: errorSchema, |
|
|
|
} |
|
|
|
} else { |
|
|
|
const attributes = ['id', 'version', 'name', 'imageUrl', 'coverImageUrl', 'iconImageUrl', 'about', 'websiteUrl', 'companyName', 'rating', 'updated', 'created'] |
|
|
|
return { |
|
|
|
...pick(app, attributes), |
|
|
|
user, |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/app/:id/install', options, async (request, reply) => { |
|
|
|
if (!server.database) return serverError(reply) |
|
|
|
if (!request.viewer) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const appContainer = containerFor(server.database.client, 'Apps') |
|
|
|
const userContainer = containerFor(server.database.client, 'Users') |
|
|
|
|
|
|
|
const appItem = appContainer.item(request.params.id, APP_PARTITION_KEY) |
|
|
|
const viewerItem = userContainer.item(request.viewer.id, request.viewer.id) |
|
|
|
|
|
|
|
const { resource: app } = await appItem.read<App>() |
|
|
|
if (!app) return notFoundError(reply) |
|
|
|
|
|
|
|
const { resource: viewer } = await viewerItem.read<User>() |
|
|
|
|
|
|
|
if (viewer.installations.find(i => i.appId === app.id)) { |
|
|
|
reply.code(204) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
await appItem.replace<App>({ |
|
|
|
...app, |
|
|
|
users: app.users + 1, |
|
|
|
}) |
|
|
|
|
|
|
|
await viewerItem.replace<User>({ |
|
|
|
...viewer, |
|
|
|
installations: [ |
|
|
|
...viewer.installations, |
|
|
|
{ |
|
|
|
id: createInstallationId(), |
|
|
|
appId: app.id, |
|
|
|
settings: {}, |
|
|
|
created: Date.now(), |
|
|
|
}, |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
reply.code(204) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function uninstallRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|
|
|
interface Params { |
|
|
|
id: string |
|
|
|
} |
|
|
|
|
|
|
|
const options: RouteShorthandOptions = { |
|
|
|
schema: { |
|
|
|
params: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
id: { type: 'string' }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
response: { |
|
|
|
400: errorSchema, |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/app/:id/uninstall', options, async (request, reply) => { |
|
|
|
if (!server.database) return serverError(reply) |
|
|
|
if (!request.viewer) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const appContainer = containerFor(server.database.client, 'Apps') |
|
|
|
const userContainer = containerFor(server.database.client, 'Users') |
|
|
|
|
|
|
|
const appItem = appContainer.item(request.params.id, APP_PARTITION_KEY) |
|
|
|
const viewerItem = userContainer.item(request.viewer.id, request.viewer.id) |
|
|
|
|
|
|
|
const { resource: app } = await appItem.read<App>() |
|
|
|
if (!app) return notFoundError(reply) |
|
|
|
|
|
|
|
const { resource: viewer } = await viewerItem.read<User>() |
|
|
|
|
|
|
|
if (!viewer.installations.find(i => i.appId === app.id)) { |
|
|
|
reply.code(204) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
await appItem.replace<App>({ |
|
|
|
...app, |
|
|
|
users: app.users - 1, |
|
|
|
}) |
|
|
|
|
|
|
|
await viewerItem.replace<User>({ |
|
|
|
...viewer, |
|
|
|
installations: viewer.installations.filter(i => i.appId !== app.id), |
|
|
|
}) |
|
|
|
|
|
|
|
reply.code(204) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function installationsRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|
|
|
const options: RouteShorthandOptions = { |
|
|
|
schema: { |
|
|
|
response: { |
|
|
|
200: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
installations: { |
|
|
|
type: 'array', |
|
|
|
items: { |
|
|
|
type: 'object', |
|
|
|
properties: { |
|
|
|
app: appSchema, |
|
|
|
settings: { type: 'object' }, |
|
|
|
created: { type: 'number' }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
400: errorSchema, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/api/installations', options, async (request, reply) => { |
|
|
|
if (!server.database) return serverError(reply) |
|
|
|
if (!request.viewer) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const viewer = await getItem<User>({ |
|
|
|
container: containerFor(server.database.client, 'Users'), |
|
|
|
id: request.viewer.id, |
|
|
|
}) |
|
|
|
|
|
|
|
if (!viewer) return unauthorizedError(reply) |
|
|
|
|
|
|
|
const apps = await queryItems<User>({ |
|
|
|
container: containerFor(server.database.client, 'Apps'), |
|
|
|
query: createQuerySpec( |
|
|
|
`SELECT * FROM Apps a WHERE ARRAY_CONTAINS(@ids, a.id)`, |
|
|
|
{ |
|
|
|
ids: viewer.installations.map(installation => installation.appId), |
|
|
|
} |
|
|
|
), |
|
|
|
logger: request.log, |
|
|
|
}) |
|
|
|
|
|
|
|
return { |
|
|
|
installations: viewer.installations.map(installation => { |
|
|
|
return { |
|
|
|
...installation, |
|
|
|
app: apps.find(app => app.id === installation.appId), |
|
|
|
appId: undefined, |
|
|
|
} |
|
|
|
}), |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
@ -508,6 +694,9 @@ const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = a |
|
|
|
createRoute(server) |
|
|
|
updateRoute(server) |
|
|
|
getRoute(server) |
|
|
|
installRoute(server) |
|
|
|
uninstallRoute(server) |
|
|
|
installationsRoute(server) |
|
|
|
} |
|
|
|
|
|
|
|
export default plugin |