diff --git a/src/api/admin/AdminService.ts b/src/api/admin/AdminService.ts index 5ab018b..0a4004f 100644 --- a/src/api/admin/AdminService.ts +++ b/src/api/admin/AdminService.ts @@ -1,10 +1,12 @@ -import { GET, Path, QueryParam } from "typescript-rest"; +import { GET, Path, POST, QueryParam } from "typescript-rest"; import { ScalarService } from "../scalar/ScalarService"; import config from "../../config"; import { ApiError } from "../ApiError"; import { MatrixLiteClient } from "../../matrix/MatrixLiteClient"; import { CURRENT_VERSION } from "../../version"; import { getFederationConnInfo } from "../../matrix/helpers"; +import UserScalarToken from "../../db/models/UserScalarToken"; +import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache"; interface DimensionVersionResponse { version: string; @@ -20,6 +22,9 @@ interface DimensionConfigResponse { federationHostname: string; clientServerUrl: string; }; + sessionInfo: { + numTokens: number; + }; } /** @@ -82,6 +87,9 @@ export class AdminService { federationHostname: fedInfo.hostname, clientServerUrl: config.homeserver.clientServerUrl, }, + sessionInfo: { + numTokens: await UserScalarToken.count(), + }, }; } @@ -95,4 +103,23 @@ export class AdminService { resolvedServer: await getFederationConnInfo(serverName), }; } + + @POST + @Path("sessions/logout/all") + public async logoutAll(@QueryParam("scalar_token") scalarToken: string): Promise { + await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + // Clear the cache first to hopefully invalidate a bunch of them + Cache.for(CACHE_SCALAR_ACCOUNTS).clear(); + + const tokens = await UserScalarToken.all(); + for (const token of tokens) { + await token.destroy(); + } + + // Clear it again because the delete loop can be slow + Cache.for(CACHE_SCALAR_ACCOUNTS).clear(); + + return {}; + } } \ No newline at end of file diff --git a/src/api/scalar/ScalarService.ts b/src/api/scalar/ScalarService.ts index b7c9b1d..afabeb5 100644 --- a/src/api/scalar/ScalarService.ts +++ b/src/api/scalar/ScalarService.ts @@ -49,7 +49,11 @@ export class ScalarService { @POST @Path("register") - public async register(request: RegisterRequest): Promise { + public async register(request: RegisterRequest, @QueryParam("v") apiVersion: string): Promise { + if (apiVersion !== "1.1") { + throw new ApiError(401, "Invalid API version."); + } + const mxClient = new MatrixOpenIdClient(request); const mxUserId = await mxClient.getUserId(); @@ -95,7 +99,11 @@ export class ScalarService { @GET @Path("account") - public async getAccount(@QueryParam("scalar_token") scalarToken: string): Promise { + public async getAccount(@QueryParam("scalar_token") scalarToken: string, @QueryParam("v") apiVersion: string): Promise { + if (apiVersion !== "1.1") { + throw new ApiError(401, "Invalid API version."); + } + const userId = await ScalarService.getTokenOwner(scalarToken); return {user_id: userId}; } diff --git a/src/scalar/ScalarClient.ts b/src/scalar/ScalarClient.ts index c042a8c..47ed0b2 100644 --- a/src/scalar/ScalarClient.ts +++ b/src/scalar/ScalarClient.ts @@ -3,6 +3,7 @@ import { ScalarRegisterResponse } from "../models/ScalarResponses"; import * as request from "request"; import { LogService } from "matrix-js-snippets"; import Upstream from "../db/models/Upstream"; +import { SCALAR_API_VERSION } from "../utils/common-constants"; export class ScalarClient { constructor(private upstream: Upstream) { @@ -14,6 +15,7 @@ export class ScalarClient { request({ method: "POST", url: this.upstream.scalarUrl + "/register", + qs: {v: SCALAR_API_VERSION}, json: openId, }, (err, res, _body) => { if (err) { diff --git a/src/utils/common-constants.ts b/src/utils/common-constants.ts new file mode 100644 index 0000000..fe62678 --- /dev/null +++ b/src/utils/common-constants.ts @@ -0,0 +1 @@ +export const SCALAR_API_VERSION = "1.1"; \ No newline at end of file diff --git a/web/app/admin/home/home.component.html b/web/app/admin/home/home.component.html index e943a6d..a34d9ee 100644 --- a/web/app/admin/home/home.component.html +++ b/web/app/admin/home/home.component.html @@ -28,6 +28,16 @@ Utility User ID: {{ config.homeserver.userId }} + +
+
+ Sessions
+ Tokens registered: {{ config.sessionInfo.numTokens }}
+ +
+
\ No newline at end of file diff --git a/web/app/admin/home/home.component.ts b/web/app/admin/home/home.component.ts index 4e3d3aa..e47bb15 100644 --- a/web/app/admin/home/home.component.ts +++ b/web/app/admin/home/home.component.ts @@ -1,6 +1,12 @@ import { Component } from "@angular/core"; import { AdminApiService } from "../../shared/services/admin/admin-api.service"; import { FE_DimensionConfig } from "../../shared/models/admin-responses"; +import { ToasterService } from "angular2-toaster"; +import { Modal, overlayConfigFactory } from "ngx-modialog"; +import { + AdminLogoutConfirmationDialogComponent, + LogoutConfirmationDialogContext +} from "./logout-confirmation/logout-confirmation.component"; @Component({ templateUrl: "./home.component.html", @@ -11,10 +17,26 @@ export class AdminHomeComponent { public isLoading = true; public config: FE_DimensionConfig; - constructor(adminApi: AdminApiService) { + constructor(private adminApi: AdminApiService, + private toaster: ToasterService, + private modal: Modal) { adminApi.getConfig().then(config => { this.config = config; this.isLoading = false; }); } + + public logoutAll(): void { + this.modal.open(AdminLogoutConfirmationDialogComponent, overlayConfigFactory({ + isBlocking: true, + }, LogoutConfirmationDialogContext)).result.then(() => { + this.adminApi.logoutAll().then(() => { + this.toaster.pop("success", "Everyone has been logged out"); + this.config.sessionInfo.numTokens = 0; + }).catch(err => { + console.error(err); + this.toaster.pop("error", "Error logging everyone out"); + }); + }); + } } diff --git a/web/app/admin/home/logout-confirmation/logout-confirmation.component.html b/web/app/admin/home/logout-confirmation/logout-confirmation.component.html new file mode 100644 index 0000000..c11da9f --- /dev/null +++ b/web/app/admin/home/logout-confirmation/logout-confirmation.component.html @@ -0,0 +1,20 @@ +
+
+

Logout confirmation

+
+
+

+ Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. + Most clients will automatically re-register for a login token behind the scenes, similar to how a login token + was first acquired. +

+
+ +
\ No newline at end of file diff --git a/web/app/admin/home/logout-confirmation/logout-confirmation.component.scss b/web/app/admin/home/logout-confirmation/logout-confirmation.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/admin/home/logout-confirmation/logout-confirmation.component.ts b/web/app/admin/home/logout-confirmation/logout-confirmation.component.ts new file mode 100644 index 0000000..ce71df1 --- /dev/null +++ b/web/app/admin/home/logout-confirmation/logout-confirmation.component.ts @@ -0,0 +1,15 @@ +import { Component } from "@angular/core"; +import { DialogRef, ModalComponent } from "ngx-modialog"; +import { BSModalContext } from "ngx-modialog/plugins/bootstrap"; + +export class LogoutConfirmationDialogContext extends BSModalContext { +} + +@Component({ + templateUrl: "./logout-confirmation.component.html", + styleUrls: ["./logout-confirmation.component.scss"], +}) +export class AdminLogoutConfirmationDialogComponent implements ModalComponent { + + constructor(public dialog: DialogRef) {} +} diff --git a/web/app/app.module.ts b/web/app/app.module.ts index ef6a4a4..e2a23a0 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -109,6 +109,7 @@ import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge. import { AdminSlackBridgeManageSelfhostedComponent } from "./admin/bridges/slack/manage-selfhosted/manage-selfhosted.component"; import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component"; import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.service"; +import { AdminLogoutConfirmationDialogComponent } from "./admin/home/logout-confirmation/logout-confirmation.component"; import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component"; import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-test/manager-test.component"; @@ -200,6 +201,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes SlackBridgeConfigComponent, AdminSlackBridgeManageSelfhostedComponent, AdminSlackBridgeComponent, + AdminLogoutConfirmationDialogComponent, ReauthExampleWidgetWrapperComponent, ManagerTestWidgetWrapperComponent, @@ -254,6 +256,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes AdminGitterBridgeManageSelfhostedComponent, AdminAddCustomBotComponent, AdminSlackBridgeManageSelfhostedComponent, + AdminLogoutConfirmationDialogComponent, ] }) export class AppModule { diff --git a/web/app/shared/models/admin-responses.ts b/web/app/shared/models/admin-responses.ts index 9d007b9..647f934 100644 --- a/web/app/shared/models/admin-responses.ts +++ b/web/app/shared/models/admin-responses.ts @@ -10,6 +10,9 @@ export interface FE_DimensionConfig { federationHostname: string; clientServerUrl: string; }; + sessionInfo: { + numTokens: number; + }; } export interface FE_DimensionVersion { diff --git a/web/app/shared/services/admin/admin-api.service.ts b/web/app/shared/services/admin/admin-api.service.ts index a0f4b0b..1609ce6 100644 --- a/web/app/shared/services/admin/admin-api.service.ts +++ b/web/app/shared/services/admin/admin-api.service.ts @@ -20,4 +20,8 @@ export class AdminApiService extends AuthedApi { public getVersion(): Promise { return this.authedGet("/api/v1/dimension/admin/version").map(r => r.json()).toPromise(); } + + public logoutAll(): Promise { + return this.authedPost("/api/v1/dimension/admin/sessions/logout/all").map(r => r.json()).toPromise(); + } } 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 69dbac9..7663b6e 100644 --- a/web/app/shared/services/scalar/scalar-server-api.service.ts +++ b/web/app/shared/services/scalar/scalar-server-api.service.ts @@ -6,6 +6,7 @@ import { FE_ScalarRegisterResponse } from "../../models/scalar-server-responses"; import { AuthedApi } from "../authed-api"; +import { SCALAR_API_VERSION } from "../../../../../src/utils/common-constants"; @Injectable() export class ScalarServerApiService extends AuthedApi { @@ -18,10 +19,12 @@ export class ScalarServerApiService extends AuthedApi { } public getAccount(): Promise { - return this.authedGet("/api/v1/scalar/account").map(res => res.json()).toPromise(); + return this.authedGet("/api/v1/scalar/account", {v: SCALAR_API_VERSION}).map(res => res.json()).toPromise(); } public register(openId: FE_ScalarOpenIdRequestBody): Promise { - return this.http.post("/api/v1/scalar/register", openId).map(res => res.json()).toPromise(); + return this.http.post("/api/v1/scalar/register", openId, { + params: {v: SCALAR_API_VERSION}, + }).map(res => res.json()).toPromise(); } }