Add the frontend for self-service of webhooks

This commit is contained in:
Travis Ralston 2018-10-20 18:33:01 -06:00
parent 7b5285cd57
commit 3823788cc2
11 changed files with 224 additions and 38 deletions

View File

@ -1,38 +0,0 @@
import { FormParam, HeaderParam, Path, PathParam, POST } from "typescript-rest";
import Webhook from "../../db/models/Webhook";
import { ApiError } from "../ApiError";
import * as request from "request";
import { LogService } from "matrix-js-snippets";
/**
* API for proxying webhooks to other services.
*/
@Path("/api/v1/dimension/webhooks")
export class DimensionWebhookService {
@POST
@Path("/travisci/:webhookId")
public async postTravisCiWebhook(@PathParam("webhookId") webhookId: string, @FormParam("payload") payload: string, @HeaderParam("Signature") signature: string): Promise<any> {
const webhook = await Webhook.findByPrimary(webhookId).catch(() => null);
if (!webhook) throw new ApiError(404, "Webhook not found");
if (!webhook.targetUrl) throw new ApiError(400, "Webhook not configured");
return new Promise((resolve, _reject) => {
request({
method: "POST",
url: webhook.targetUrl,
form: {payload: payload},
headers: {
"Signature": signature,
},
}, (err, res, _body) => {
if (err) {
LogService.error("DimensionWebhooksService", "Error invoking travis-ci webhook");
LogService.error("DimensionWebhooksService", res.body);
throw new ApiError(500, "Internal Server Error");
} else resolve();
});
});
}
}

View File

@ -0,0 +1,69 @@
import { DELETE, FormParam, HeaderParam, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { ScalarService } from "../scalar/ScalarService";
import { SuccessResponse, WebhookConfiguration, WebhookOptions } from "../../bridges/models/webhooks";
import { WebhooksBridge } from "../../bridges/WebhooksBridge";
import Webhook from "../../db/models/Webhook";
import { ApiError } from "../ApiError";
import { LogService } from "matrix-js-snippets";
import * as request from "request";
/**
* API for interacting with the Webhooks bridge, and for setting up proxies to other
* services.
*/
@Path("/api/v1/dimension/webhooks")
export class DimensionWebhooksService {
@POST
@Path("/travisci/:webhookId")
public async postTravisCiWebhook(@PathParam("webhookId") webhookId: string, @FormParam("payload") payload: string, @HeaderParam("Signature") signature: string): Promise<any> {
const webhook = await Webhook.findByPrimary(webhookId).catch(() => null);
if (!webhook) throw new ApiError(404, "Webhook not found");
if (!webhook.targetUrl) throw new ApiError(400, "Webhook not configured");
return new Promise((resolve, _reject) => {
request({
method: "POST",
url: webhook.targetUrl,
form: {payload: payload},
headers: {
"Signature": signature,
},
}, (err, res, _body) => {
if (err) {
LogService.error("DimensionWebhooksService", "Error invoking travis-ci webhook");
LogService.error("DimensionWebhooksService", res.body);
throw new ApiError(500, "Internal Server Error");
} else resolve();
});
});
}
@POST
@Path("room/:roomId/webhooks/new")
public async newWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: WebhookOptions): Promise<WebhookConfiguration> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const webhooks = new WebhooksBridge(userId);
return webhooks.createWebhook(roomId, request);
}
@POST
@Path("room/:roomId/webhooks/:hookId")
public async updateWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string, request: WebhookOptions): Promise<WebhookConfiguration> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const webhooks = new WebhooksBridge(userId);
return webhooks.updateWebhook(roomId, hookId, request);
}
@DELETE
@Path("room/:roomId/webhooks/:hookId")
public async deleteWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string): Promise<SuccessResponse> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const webhooks = new WebhooksBridge(userId);
return webhooks.deleteWebhook(roomId, hookId);
}
}

View File

@ -88,6 +88,8 @@ import { TelegramCannotUnbridgeComponent } from "./configs/bridge/telegram/canno
import { AdminWebhooksBridgeManageSelfhostedComponent } from "./admin/bridges/webhooks/manage-selfhosted/manage-selfhosted.component";
import { AdminWebhooksBridgeComponent } from "./admin/bridges/webhooks/webhooks.component";
import { AdminWebhooksApiService } from "./shared/services/admin/admin-webhooks-api.service";
import { WebhooksApiService } from "./shared/services/integrations/webhooks-api.service";
import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhooks.bridge.component";
@NgModule({
imports: [
@ -162,6 +164,7 @@ import { AdminWebhooksApiService } from "./shared/services/admin/admin-webhooks-
TelegramCannotUnbridgeComponent,
AdminWebhooksBridgeManageSelfhostedComponent,
AdminWebhooksBridgeComponent,
WebhooksBridgeConfigComponent,
// Vendor
],
@ -184,6 +187,7 @@ import { AdminWebhooksApiService } from "./shared/services/admin/admin-webhooks-
AdminTelegramApiService,
TelegramApiService,
AdminWebhooksApiService,
WebhooksApiService,
{provide: Window, useValue: window},
// Vendor

View File

@ -30,6 +30,7 @@ import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-p
import { AdminTelegramBridgeComponent } from "./admin/bridges/telegram/telegram.component";
import { TelegramBridgeConfigComponent } from "./configs/bridge/telegram/telegram.bridge.component";
import { AdminWebhooksBridgeComponent } from "./admin/bridges/webhooks/webhooks.component";
import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhooks.bridge.component";
const routes: Routes = [
{path: "", component: HomeComponent},
@ -182,6 +183,11 @@ const routes: Routes = [
component: TelegramBridgeConfigComponent,
data: {breadcrumb: "Telegram Bridge Configuration", name: "Telegram Bridge Configuration"},
},
{
path: "webhooks",
component: WebhooksBridgeConfigComponent,
data: {breadcrumb: "Webhook Bridge Configuration", name: "Webhook Bridge Configuration"},
},
],
},
{

View File

@ -0,0 +1,55 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-ibox [isCollapsible]="true">
<h5 class="my-ibox-title">
Add a new webhook
</h5>
<div class="my-ibox-content">
<label class="label-block">
Webhook Name
<input title="webhook name" type="text" class="form-control form-control-sm" [(ngModel)]="webhookName" [disabled]="isBusy">
</label>
<div style="margin-top: 25px">
<button type="button" class="btn btn-sm btn-primary" [disabled]="isBusy" (click)="newHook()">
Create
</button>
</div>
</div>
</my-ibox>
<my-ibox [isCollapsible]="true">
<h5 class="my-ibox-title">
Webhooks
</h5>
<div class="my-ibox-content">
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>URL</th>
<th class="actions-col">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngIf="newConfig.webhooks.length === 0">
<td colspan="4">No webhooks</td>
</tr>
<tr *ngFor="let hook of newConfig.webhooks">
<td *ngIf="hook.label">{{ hook.label }}</td>
<td *ngIf="!hook.label"><i>No name</i></td>
<td>{{ hook.type }}</td>
<td class="webhook-url"><a [href]="hook.url" target="_blank">{{ hook.url }}</a></td>
<td class="actions-col">
<button type="button" class="btn btn-sm btn-outline-danger"
[disabled]="isBusy"
(click)="removeHook(hook)">
<i class="far fa-trash-alt"></i> Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</my-ibox>
</ng-template>
</my-bridge-config>

View File

@ -0,0 +1,3 @@
.webhook-url {
word-break: break-word;
}

View File

@ -0,0 +1,50 @@
import { Component } from "@angular/core";
import { BridgeComponent } from "../bridge.component";
import { FE_Webhook } from "../../../shared/models/webhooks";
import { WebhooksApiService } from "../../../shared/services/integrations/webhooks-api.service";
interface WebhooksConfig {
webhooks: FE_Webhook[];
}
@Component({
templateUrl: "webhooks.bridge.component.html",
styleUrls: ["webhooks.bridge.component.scss"],
})
export class WebhooksBridgeConfigComponent extends BridgeComponent<WebhooksConfig> {
public webhookName: string;
public isBusy = false;
constructor(private webhooks: WebhooksApiService) {
super("webhooks");
}
public newHook() {
this.isBusy = true;
this.webhooks.createWebhook(this.roomId, {label: this.webhookName}).then(hook => {
this.newConfig.webhooks.push(hook);
this.isBusy = false;
this.webhookName = "";
this.toaster.pop("success", "Webhook created");
}).catch(err => {
console.error(err);
this.isBusy = false;
this.toaster.pop("error", "Error creating webhook");
});
}
public removeHook(hook: FE_Webhook) {
this.isBusy = true;
this.webhooks.deleteWebhook(this.roomId, hook.id).then(() => {
const idx = this.newConfig.webhooks.indexOf(hook);
if (idx !== -1) this.newConfig.webhooks.splice(idx, 1);
this.isBusy = false;
this.toaster.pop("success", "Webhook deleted");
}).catch(err => {
console.error(err);
this.isBusy = false;
this.toaster.pop("error", "Error deleting webhook");
});
}
}

View File

@ -5,3 +5,16 @@ export interface FE_WebhooksBridge {
sharedSecret?: string;
isEnabled: boolean;
}
export interface FE_Webhook {
id: string;
label: string;
url: string;
userId: string;
roomId: string;
type: "incoming";
}
export interface FE_WebhookOptions {
label: string;
}

View File

@ -18,6 +18,7 @@ export class IntegrationsRegistry {
"bridge": {
"irc": {},
"telegram": {},
"webhooks": {},
},
"widget": {
"custom": {

View File

@ -0,0 +1,23 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { AuthedApi } from "../authed-api";
import { FE_Webhook, FE_WebhookOptions } from "../../models/webhooks";
@Injectable()
export class WebhooksApiService extends AuthedApi {
constructor(http: Http) {
super(http);
}
public createWebhook(roomId: string, options: FE_WebhookOptions): Promise<FE_Webhook> {
return this.authedPost("/api/v1/dimension/webhooks/room/" + roomId + "/webhooks/new", options).map(r => r.json()).toPromise();
}
public updateWebhook(roomId: string, hookId: string, options: FE_WebhookOptions): Promise<FE_Webhook> {
return this.authedPost("/api/v1/dimension/webhooks/room/" + roomId + "/webhooks/" + hookId, options).map(r => r.json()).toPromise();
}
public deleteWebhook(roomId: string, hookId: string): Promise<any> {
return this.authedDelete("/api/v1/dimension/webhooks/room/" + roomId + "/webhooks/" + hookId).map(r => r.json()).toPromise();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB