Bootstrap static json applications definition

This commit is contained in:
Romaric Mourgues
2023-04-14 10:07:34 +02:00
parent 1d83a583dc
commit 18e10b6a8b
19 changed files with 620 additions and 704 deletions
@@ -0,0 +1,42 @@
import { Prefix, TdriveService } from "../../core/platform/framework";
import WebServerAPI from "../../core/platform/services/webserver/provider";
import web from "./web/index";
import FastProxy from "fast-proxy";
import globalResolver from "../global-resolver";
@Prefix("/api")
export default class ApplicationsApiService extends TdriveService<undefined> {
version = "1";
name = "applicationsapi";
public async doInit(): Promise<this> {
const fastify = this.context.getProvider<WebServerAPI>("webserver").getServer();
fastify.register((instance, _opts, next) => {
web(instance, { prefix: this.prefix });
next();
});
//Redirect requests from /plugins/* to the plugin server (if installed)
const apps = await globalResolver.services.applications.marketplaceApps.list(null);
for (const app of apps) {
const domain = app.internal_domain;
const prefix = app.external_prefix;
if (domain && prefix) {
const { proxy, close } = FastProxy({
base: domain,
});
fastify.addHook("onClose", close);
fastify.all("/" + prefix.replace(/(\/$|^\/)/, "") + "/*", (req, rep) => {
proxy(req.raw, rep.raw, req.url, {});
});
}
}
return this;
}
// TODO: remove
api(): undefined {
return undefined;
}
}
@@ -0,0 +1,13 @@
import { Channel } from "../channels/entities";
import { Message } from "../messages/entities/messages";
import { Thread } from "../messages/entities/threads";
export type HookType = {
type: "message";
application_id: string;
company_id: string;
channel?: Channel;
thread: Thread;
message: Message;
};
@@ -0,0 +1,181 @@
import { FastifyInstance, FastifyReply, FastifyRequest, HTTPMethods } from "fastify";
import { ApplicationObject } from "../../../applications/entities/application";
import {
ApplicationApiExecutionContext,
ApplicationLoginRequest,
ApplicationLoginResponse,
ConfigureRequest,
} from "../types";
import { ResourceGetResponse } from "../../../../utils/types";
import { CrudException } from "../../../../core/platform/framework/api/crud-service";
import { localEventBus } from "../../../../core/platform/framework/event-bus";
import {
RealtimeApplicationEvent,
RealtimeBaseBusEvent,
} from "../../../../core/platform/services/realtime/types";
import gr from "../../../global-resolver";
import _ from "lodash";
import { v4 } from "uuid";
export class ApplicationsApiController {
async token(
request: FastifyRequest<{ Body: ApplicationLoginRequest }>,
): Promise<ResourceGetResponse<ApplicationLoginResponse>> {
const context = getExecutionContext(request);
if (!request.body.id || !request.body.secret) {
throw CrudException.forbidden("Application not found");
}
const app = await gr.services.applications.marketplaceApps.get(
{
id: request.body.id,
},
context,
);
if (!app) {
throw CrudException.forbidden("Application not found");
}
if (!app.api.private_key || app.api.private_key !== request.body.secret) {
throw CrudException.forbidden("Secret key is not valid");
}
return {
resource: {
access_token: gr.platformServices.auth.generateJWT(request.body.id, null, {
track: false,
provider_id: "",
application_id: request.body.id,
}),
},
};
}
async me(
request: FastifyRequest,
_reply: FastifyReply,
): Promise<ResourceGetResponse<ApplicationObject>> {
const context = getExecutionContext(request);
const entity = await gr.services.applications.marketplaceApps.get(
{
id: context.application_id,
},
context,
);
if (!entity) {
throw CrudException.notFound("Application not found");
}
return { resource: entity.getApplicationObject() };
}
async configure(
request: FastifyRequest<{ Body: ConfigureRequest }>,
_reply: FastifyReply,
): Promise<Record<string, string>> {
const app_id = request.currentUser.application_id;
const context = getExecutionContext(request);
const application = await gr.services.applications.marketplaceApps.get({ id: app_id }, context);
if (!application) {
throw CrudException.forbidden("Application not found");
}
const body = request.body;
const data = {
action: "configure",
application: {
id: app_id,
identity: application.identity,
},
form: body.form,
connection_id: body.connection_id,
hidden_data: {},
configurator_id: v4(),
};
localEventBus.publish("realtime:event", {
room: "/me/" + body.user_id,
type: "application",
data,
} as RealtimeBaseBusEvent<RealtimeApplicationEvent>);
return { status: "ok" };
}
async proxy(
request: FastifyRequest<{ Params: { company_id: string; service: string; version: string } }>,
reply: FastifyReply,
fastify: FastifyInstance,
): Promise<void> {
// Check the application has access to this company
const company_id = request.params.company_id;
const companyApplication = gr.services.applications.companyApps.get({
company_id,
application_id: request.currentUser.application_id,
id: undefined,
});
if (!companyApplication) {
throw CrudException.forbidden("This application is not installed in the requested company");
}
const context = getExecutionContext(request);
const app = await gr.services.applications.marketplaceApps.get(
{
id: request.currentUser.application_id,
},
context,
);
// Check call can be done from this IP
if (
app.api.allowed_ips.trim() &&
app.api.allowed_ips !== "*" &&
!_.includes(
app.api.allowed_ips
.split(",")
.map(a => a.trim())
.filter(a => a),
request.ip,
)
) {
throw CrudException.forbidden(
`This application is not allowed to access from this IP (${request.ip})`,
);
}
//TODO Check application access rights (write, read, remove for each micro services)
const _access = app.access;
//TODO save some statistics about API usage for application and per companies
const route = request.url.replace("/api/", "/internal/services/");
fastify.inject(
{
method: request.method as HTTPMethods,
url: route,
payload: request.body as any,
headers: _.pick(request.headers, "authorization"),
},
(err, response) => {
reply.headers(response.headers);
reply.send(response.payload);
},
);
}
}
function getExecutionContext(request: FastifyRequest): ApplicationApiExecutionContext {
return {
application_id: request.currentUser?.application_id,
user: request.currentUser,
url: request.url,
method: request.routerMethod,
transport: "http",
};
}
@@ -0,0 +1,10 @@
import { FastifyInstance, FastifyRegisterOptions } from "fastify";
import routes from "./routes";
export default (
fastify: FastifyInstance,
options: FastifyRegisterOptions<{ prefix: string }>,
): void => {
fastify.log.debug("Configuring /internal/services/applications/v1 routes");
fastify.register(routes, options);
};
@@ -0,0 +1,54 @@
import { FastifyInstance, FastifyPluginCallback, FastifyRequest } from "fastify";
import { ApplicationsApiController } from "./controllers";
import { ApplicationApiBaseRequest } from "./types";
import { logger as log } from "../../../core/platform/framework";
import { configureRequestSchema } from "./schemas";
const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) => {
const controller = new ApplicationsApiController();
const checkApplication = async (request: FastifyRequest<{ Body: ApplicationApiBaseRequest }>) => {
if (!request.currentUser.application_id) {
log.debug(request.currentUser);
throw fastify.httpErrors.forbidden("You should log in as application");
}
};
//Authenticate the application
fastify.route({
method: "POST",
url: "/console/v1/login",
handler: controller.token.bind(controller),
});
//Get myself as an application
fastify.route({
method: "GET",
url: "/console/v1/me",
preValidation: [fastify.authenticate],
preHandler: [checkApplication],
handler: controller.me.bind(controller),
});
//Open a configuration popup on the client side
fastify.route({
method: "POST",
url: "/console/v1/configure",
preValidation: [fastify.authenticate],
schema: configureRequestSchema,
handler: controller.configure.bind(controller),
});
//Get myself as an application
fastify.route({
method: ["POST", "GET", "DELETE", "PUT"],
url: "/:service/:version/companies/:company_id/*",
preValidation: [fastify.authenticate],
handler: (request, reply) => controller.proxy.bind(controller)(request, reply, fastify),
});
next();
};
export default routes;
@@ -0,0 +1,16 @@
export const applicationsSchema = {
type: "object",
properties: {},
};
export const configureRequestSchema = {
body: {
type: "object",
properties: {
user_id: { type: "string" },
connection_id: { type: "string" },
form: {},
},
required: ["user_id", "connection_id"],
},
};
@@ -0,0 +1,23 @@
import { AccessToken } from "../../../utils/types";
import { ExecutionContext } from "../../../core/platform/framework/api/crud-service";
export interface ApplicationApiBaseRequest {
id: string;
secret: string;
}
export type ApplicationLoginRequest = ApplicationApiBaseRequest;
export interface ApplicationLoginResponse {
access_token: AccessToken;
}
export interface ApplicationApiExecutionContext extends ExecutionContext {
application_id: string;
}
export interface ConfigureRequest {
user_id: string;
connection_id: string;
form?: any;
}