diff --git a/src/api/scalar/ScalarService.ts b/src/api/scalar/ScalarService.ts index 5774626..b7c9b1d 100644 --- a/src/api/scalar/ScalarService.ts +++ b/src/api/scalar/ScalarService.ts @@ -100,4 +100,10 @@ export class ScalarService { return {user_id: userId}; } + @GET + @Path("ping") + public async ping(): Promise { + return {}; // 200 OK + } + } \ No newline at end of file diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 9464dd9..ef6a4a4 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -110,6 +110,7 @@ import { AdminSlackBridgeManageSelfhostedComponent } from "./admin/bridges/slack import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component"; import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.service"; import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component"; +import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-test/manager-test.component"; @NgModule({ imports: [ @@ -200,6 +201,7 @@ import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-ex AdminSlackBridgeManageSelfhostedComponent, AdminSlackBridgeComponent, ReauthExampleWidgetWrapperComponent, + ManagerTestWidgetWrapperComponent, // Vendor ], diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 9c5a578..74f8053 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -43,6 +43,7 @@ import { AdminCustomBotsComponent } from "./admin/custom-bots/custom-bots.compon import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component"; import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge.component"; import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component"; +import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-test/manager-test.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -266,6 +267,7 @@ const routes: Routes = [ {path: "tradingview", component: TradingViewWidgetWrapperComponent}, {path: "spotify", component: SpotifyWidgetWrapperComponent}, {path: "reauth", component: ReauthExampleWidgetWrapperComponent}, + {path: "manager-test", component: ManagerTestWidgetWrapperComponent}, ] }, ]; diff --git a/web/app/shared/services/scalar/scalar-server-api.service.ts b/web/app/shared/services/scalar/scalar-server-api.service.ts index cc90dde..69dbac9 100644 --- a/web/app/shared/services/scalar/scalar-server-api.service.ts +++ b/web/app/shared/services/scalar/scalar-server-api.service.ts @@ -13,6 +13,10 @@ export class ScalarServerApiService extends AuthedApi { super(http) } + public ping(): Promise { + return this.http.get("/api/v1/scalar/ping").map(res => res.json()).toPromise(); + } + public getAccount(): Promise { return this.authedGet("/api/v1/scalar/account").map(res => res.json()).toPromise(); } diff --git a/web/app/widget-wrappers/capable-widget.ts b/web/app/widget-wrappers/capable-widget.ts index 3e7182f..80b494f 100644 --- a/web/app/widget-wrappers/capable-widget.ts +++ b/web/app/widget-wrappers/capable-widget.ts @@ -2,16 +2,23 @@ import { OnDestroy, OnInit } from "@angular/core"; import { Subscription } from "rxjs/Subscription"; import { ScalarWidgetApi } from "../shared/services/scalar/scalar-widget.api"; import * as semver from "semver"; +import { FE_ScalarOpenIdRequestBody } from "../shared/models/scalar-server-responses"; export const WIDGET_API_VERSION_BASIC = "0.0.1"; export const WIDGET_API_VERSION_OPENID = "0.0.2"; export const WIDGET_API_DIMENSION_VERSIONS = [WIDGET_API_VERSION_BASIC, WIDGET_API_VERSION_OPENID]; +export interface OpenIdResponse { + openId: FE_ScalarOpenIdRequestBody; + blocked: boolean; +} + export abstract class CapableWidget implements OnInit, OnDestroy { private requestSubscription: Subscription; private responseSubscription: Subscription; + private openIdRequest: { resolve: (a: OpenIdResponse) => void, promise: Promise } = null; // The capabilities we support protected supportsScreenshots = false; @@ -34,12 +41,29 @@ export abstract class CapableWidget implements OnInit, OnDestroy { this.onCapabilitiesSent(); } else if (request.action === "supported_api_versions") { ScalarWidgetApi.replySupportedVersions(request, WIDGET_API_DIMENSION_VERSIONS); + } else if (request.action === "openid_credentials" && this.openIdRequest) { + if (request.data.success) { + this.openIdRequest.resolve({openId: request.data, blocked: false}); + } else { + this.openIdRequest.resolve({openId: null, blocked: true}); + } + this.openIdRequest = null; } }); this.responseSubscription = ScalarWidgetApi.replyReceived.subscribe(request => { - if (request.action === "supported_api_versions" && request.response) { + if (!request.response) return; + + if (request.action === "supported_api_versions") { this.clientWidgetApiVersions = request.response.supported_versions || []; this.onSupportedVersionsFound(); + } else if (request.action === "get_openid" && this.openIdRequest) { + if (request.response.state === "allowed") { + this.openIdRequest.resolve({openId: request.response, blocked: false}); + this.openIdRequest = null; + } else if (request.response.state === "blocked") { + this.openIdRequest.resolve({openId: null, blocked: true}); + this.openIdRequest = null; + } } }); } @@ -65,4 +89,13 @@ export abstract class CapableWidget implements OnInit, OnDestroy { } return false; } + + protected getOpenIdInfo(): Promise { + if (this.openIdRequest) return this.openIdRequest.promise; + const promise = new Promise(((resolve, _reject) => { + this.openIdRequest = {resolve: resolve, promise}; + ScalarWidgetApi.requestOpenID(); + })); + return promise; + } } \ No newline at end of file diff --git a/web/app/widget-wrappers/manager-test/manager-test.component.html b/web/app/widget-wrappers/manager-test/manager-test.component.html new file mode 100644 index 0000000..f4cc133 --- /dev/null +++ b/web/app/widget-wrappers/manager-test/manager-test.component.html @@ -0,0 +1,33 @@ +
+
+
+
+
+ + You +
+ +
+ + Integrations +
+ +
+ + Homeserver +
+
+

+ Your client is too old to use this widget. Try upgrading your client to the latest available version, + or contact the author to try and diagnose the problem. Your client needs to support OpenID information + exchange. +

+
+

{{message}}

+ +
+
+
+
\ No newline at end of file diff --git a/web/app/widget-wrappers/manager-test/manager-test.component.scss b/web/app/widget-wrappers/manager-test/manager-test.component.scss new file mode 100644 index 0000000..d08461d --- /dev/null +++ b/web/app/widget-wrappers/manager-test/manager-test.component.scss @@ -0,0 +1,66 @@ +// component styles are encapsulated and only applied to their components +@import "../../../style/themes/themes"; + +@include themifyComponent() { + .wrapper { + display: table; + position: absolute; + height: 100%; + width: 100%; + background-color: themed(troubleshooterBgColor); + } + + .boat { + display: table-cell; + vertical-align: middle; + } + + .prompt { + margin-left: auto; + margin-right: auto; + width: 90%; + text-align: center; + } + + p { + font-size: 0.9em; + } + + .diagram { + margin: 20px; + + div { + display: inline-block; + } + + .circle { + width: 120px; + height: 120px; + border: 5px solid themed(troubleshooterNeutralColor); + border-radius: 120px; + vertical-align: middle; + padding-top: 22px; + transition: border-color 0.2s; + + .title { + display: block; + } + } + + .link { + width: 120px; + height: 1px; + border: 2px solid themed(troubleshooterNeutralColor); + margin: 5px; + transition: border-color 0.2s; + } + + .circle.error, .link.error { + border-color: themed(troubleshooterErrorColor); + } + + .circle.ok, .link.ok { + border-color: themed(troubleshooterOkColor); + } + } +} \ No newline at end of file diff --git a/web/app/widget-wrappers/manager-test/manager-test.component.ts b/web/app/widget-wrappers/manager-test/manager-test.component.ts new file mode 100644 index 0000000..ec344c0 --- /dev/null +++ b/web/app/widget-wrappers/manager-test/manager-test.component.ts @@ -0,0 +1,90 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api"; +import { CapableWidget, WIDGET_API_VERSION_OPENID } from "../capable-widget"; +import { ActivatedRoute } from "@angular/router"; +import { ScalarServerApiService } from "../../shared/services/scalar/scalar-server-api.service"; + +@Component({ + selector: "my-reauth-example-widget-wrapper", + templateUrl: "manager-test.component.html", + styleUrls: ["manager-test.component.scss"], +}) +export class ManagerTestWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy { + + public readonly STATE_NEUTRAL = 'neutral'; + public readonly STATE_OK = 'ok'; + public readonly STATE_ERROR = 'error'; + + public isBusy = true; + public isSupported = true; + public selfState = this.STATE_NEUTRAL; + public managerState = this.STATE_NEUTRAL; + public homeserverState = this.STATE_NEUTRAL; + public message = "Click the button to test your connection. This may cause your client to ask if it " + + "is okay to share your identity with the widget - this is required to test your connection to your " + + "homeserver."; + + constructor(activatedRoute: ActivatedRoute, + private scalarApi: ScalarServerApiService, + private changeDetector: ChangeDetectorRef) { + super(); + + const params: any = activatedRoute.snapshot.queryParams; + ScalarWidgetApi.widgetId = params.widgetId; + } + + protected onSupportedVersionsFound(): void { + super.onSupportedVersionsFound(); + this.isSupported = this.doesSupportAtLeastVersion(WIDGET_API_VERSION_OPENID); + this.isBusy = false; + if (!this.isSupported) { + this.selfState = this.STATE_ERROR; + } + this.changeDetector.detectChanges(); + } + + public async start(): Promise { + this.selfState = this.STATE_NEUTRAL; + this.managerState = this.STATE_NEUTRAL; + this.homeserverState = this.STATE_NEUTRAL; + + this.message = "Please accept the prompt to verify your identity."; + this.isBusy = true; + + const response = await this.getOpenIdInfo(); + if (response.blocked) { + this.isBusy = false; + this.selfState = this.STATE_ERROR; + this.message = "You have blocked this widget from verifying your identity."; + return; + } + + this.selfState = this.STATE_OK; + this.message = "Checking connectivity to integration manager..."; + + try { + await this.scalarApi.ping(); + this.managerState = this.STATE_OK; + this.message = "Checking connectivity to homeserver..."; + } catch (e) { + console.error(e); + this.isBusy = false; + this.managerState = this.STATE_ERROR; + this.message = "Error checking if the integration manager is alive. This usually means that the manager " + + "which served this widget has gone offline."; + return; + } + + try { + await this.scalarApi.register(response.openId); + this.homeserverState = this.STATE_OK; + this.message = "You're all set! Click the button below to re-run the test."; + this.isBusy = false; + } catch (e) { + this.isBusy = false; + this.homeserverState = this.STATE_ERROR; + this.message = "Error contacting homeserver. This usually means your federation setup is incorrect, or " + + "your homeserver is offline. Consult your homeserver's documentation for how to set up federation."; + } + } +} diff --git a/web/style/themes/dark.scss b/web/style/themes/dark.scss index 0f45723..48f9f61 100644 --- a/web/style/themes/dark.scss +++ b/web/style/themes/dark.scss @@ -50,6 +50,11 @@ $theme_dark: ( jitsiWelcomeBgColor: #fff, + troubleshooterBgColor: #2d2d2d, + troubleshooterNeutralColor: rgb(205, 215, 222), + troubleshooterOkColor: #59bb59, + troubleshooterErrorColor: #e84f4f, + genericControlBgColor: #eee, genericControlFgColor: #222, widgetBannedSymbolColor: #bd362f, diff --git a/web/style/themes/light.scss b/web/style/themes/light.scss index e4d7a8c..c6f7d78 100644 --- a/web/style/themes/light.scss +++ b/web/style/themes/light.scss @@ -50,6 +50,11 @@ $theme_light: ( jitsiWelcomeBgColor: #fff, + troubleshooterBgColor: #fff, + troubleshooterNeutralColor: rgb(205, 215, 222), + troubleshooterOkColor: #59bb59, + troubleshooterErrorColor: #e84f4f, + genericControlBgColor: #eee, genericControlFgColor: #222, widgetBannedSymbolColor: #bd362f,