mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-07-01 00:31:23 +00:00
Implement MSC1961
See https://github.com/matrix-org/matrix-doc/pull/1961
This commit is contained in:
parent
d021974a22
commit
57d585d68a
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -11705,6 +11705,15 @@
|
|||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz",
|
||||
"integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ=="
|
||||
},
|
||||
"typescript-ioc": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript-ioc/-/typescript-ioc-1.2.5.tgz",
|
||||
"integrity": "sha512-HErBOZfOmrJ9N8QZDOHvP56FqvqZJMkaFW9Qm4ExQa93ilUnhQ+S7n80rVfUQPceZWIImsEBU/Kt19W5KXBDEw==",
|
||||
"requires": {
|
||||
"reflect-metadata": "^0.1.10",
|
||||
"require-glob": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"typescript-rest": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-rest/-/typescript-rest-2.0.0.tgz",
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"sqlite3": "^4.0.6",
|
||||
"telegraf": "^3.28.0",
|
||||
"typescript": "^3.4.3",
|
||||
"typescript-ioc": "^1.2.5",
|
||||
"typescript-rest": "^2.0.0",
|
||||
"umzug": "^2.2.0",
|
||||
"url": "^0.11.0"
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Server } from "typescript-rest";
|
|||
import * as _ from "lodash";
|
||||
import config from "../config";
|
||||
import { ApiError } from "./ApiError";
|
||||
import MSCSecurity from "./security/MSCSecurity";
|
||||
|
||||
/**
|
||||
* Web server for Dimension. Handles the API routes for the admin, scalar, dimension, and matrix APIs.
|
||||
|
@ -23,8 +24,12 @@ export default class Webserver {
|
|||
}
|
||||
|
||||
private loadRoutes() {
|
||||
const apis = ["scalar", "dimension", "admin", "matrix"].map(a => path.join(__dirname, a, "*.js"));
|
||||
// TODO: Rename services to controllers, and controllers to services. They're backwards.
|
||||
|
||||
const apis = ["scalar", "dimension", "admin", "matrix", "msc"].map(a => path.join(__dirname, a, "*.js"));
|
||||
const router = express.Router();
|
||||
Server.useIoC();
|
||||
Server.registerAuthenticator(new MSCSecurity());
|
||||
apis.forEach(a => Server.loadServices(router, [a]));
|
||||
const routes = _.uniq(router.stack.map(r => r.route.path));
|
||||
for (const route of routes) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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";
|
||||
|
@ -7,6 +6,7 @@ import { CURRENT_VERSION } from "../../version";
|
|||
import { getFederationConnInfo } from "../../matrix/helpers";
|
||||
import UserScalarToken from "../../db/models/UserScalarToken";
|
||||
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
interface DimensionVersionResponse {
|
||||
version: string;
|
||||
|
@ -50,7 +50,8 @@ export class AdminService {
|
|||
* @throws {ApiError} Thrown with a status code of 401 if the owner is not an administrator
|
||||
*/
|
||||
public static async validateAndGetAdminTokenOwner(scalarToken: string): Promise<string> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken, true);
|
||||
const accountController = new AccountController();
|
||||
const userId = await accountController.getTokenOwner(scalarToken, true);
|
||||
if (!AdminService.isAdmin(userId))
|
||||
throw new ApiError(401, "You must be an administrator to use this API");
|
||||
return userId;
|
||||
|
|
123
src/api/controllers/AccountController.ts
Normal file
123
src/api/controllers/AccountController.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { OpenId } from "../../models/OpenId";
|
||||
import { MatrixOpenIdClient } from "../../matrix/MatrixOpenIdClient";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
import User from "../../db/models/User";
|
||||
import Upstream from "../../db/models/Upstream";
|
||||
import { ScalarStore } from "../../db/ScalarStore";
|
||||
import UserScalarToken from "../../db/models/UserScalarToken";
|
||||
import { ScalarClient } from "../../scalar/ScalarClient";
|
||||
import * as randomString from "random-string";
|
||||
import { AutoWired } from "typescript-ioc/es6";
|
||||
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
|
||||
import { IMSCUser } from "../security/MSCSecurity";
|
||||
|
||||
export interface IAccountRegisteredResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IAccountInfoResponse {
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API controller for account management
|
||||
*/
|
||||
@AutoWired
|
||||
export default class AccountController {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the owner of a given scalar token, throwing an ApiError if the token is invalid.
|
||||
* @param {string} scalarToken The scalar token to validate
|
||||
* @param {boolean} ignoreUpstreams True to consider the token valid if it is missing links to other upstreams
|
||||
* @returns {Promise<string>} Resolves to the owner's user ID if the token is valid.
|
||||
* @throws {ApiError} Thrown with a status code of 401 if the token is invalid.
|
||||
*/
|
||||
public async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<string> {
|
||||
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
|
||||
if (cachedUserId) return cachedUserId;
|
||||
|
||||
try {
|
||||
const user = await ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams);
|
||||
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
|
||||
return user.userId;
|
||||
} catch (err) {
|
||||
LogService.error("ScalarService", err);
|
||||
throw new ApiError(401, "Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an account to use the Integration Manager
|
||||
* @param {OpenId} openId The OpenID request information.
|
||||
* @returns {Promise<IAccountRegisteredResponse>} Resolves when registered.
|
||||
*/
|
||||
public async registerAccount(openId: OpenId): Promise<IAccountRegisteredResponse> {
|
||||
if (!openId || !openId.matrix_server_name || !openId.access_token) {
|
||||
throw new ApiError(400, "Missing OpenID information");
|
||||
}
|
||||
|
||||
const mxClient = new MatrixOpenIdClient(openId);
|
||||
const mxUserId = await mxClient.getUserId();
|
||||
|
||||
if (!mxUserId.endsWith(":" + openId.matrix_server_name)) {
|
||||
LogService.warn("AccountController", `OpenID subject '${mxUserId}' does not belong to the homeserver '${openId.matrix_server_name}'`);
|
||||
throw new ApiError(401, "Invalid token");
|
||||
}
|
||||
|
||||
const user = await User.findByPrimary(mxUserId);
|
||||
if (!user) {
|
||||
// There's a small chance we'll get a validation error because of:
|
||||
// https://github.com/vector-im/riot-web/issues/5846
|
||||
LogService.verbose("AccountController", "User " + mxUserId + " never seen before - creating");
|
||||
await User.create({userId: mxUserId});
|
||||
}
|
||||
|
||||
const upstreams = await Upstream.findAll();
|
||||
await Promise.all(upstreams.map(async upstream => {
|
||||
if (!await ScalarStore.isUpstreamOnline(upstream)) {
|
||||
LogService.warn("AccountController", `Skipping registration for ${mxUserId} on upstream ${upstream.id} (${upstream.name}) because it is offline`);
|
||||
return null;
|
||||
}
|
||||
const tokens = await UserScalarToken.findAll({where: {userId: mxUserId, upstreamId: upstream.id}});
|
||||
if (!tokens || tokens.length === 0) {
|
||||
LogService.info("AccountController", "Registering " + mxUserId + " for a token at upstream " + upstream.id + " (" + upstream.name + ")");
|
||||
const client = new ScalarClient(upstream);
|
||||
const response = await client.register(openId);
|
||||
return UserScalarToken.create({
|
||||
userId: mxUserId,
|
||||
scalarToken: response.scalar_token,
|
||||
isDimensionToken: false,
|
||||
upstreamId: upstream.id,
|
||||
});
|
||||
}
|
||||
}).filter(token => !!token));
|
||||
|
||||
const dimensionToken = randomString({length: 25});
|
||||
const dimensionScalarToken = await UserScalarToken.create({
|
||||
userId: mxUserId,
|
||||
scalarToken: dimensionToken,
|
||||
isDimensionToken: true,
|
||||
});
|
||||
|
||||
LogService.info("AccountController", mxUserId + " has registered for a scalar token successfully");
|
||||
return {token: dimensionScalarToken.scalarToken};
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a user out
|
||||
* @param {IMSCUser} user The user to log out
|
||||
* @returns {Promise<*>} Resolves when complete.
|
||||
*/
|
||||
public async logout(user: IMSCUser): Promise<any> {
|
||||
// TODO: Create a link to upstream tokens to log them out too
|
||||
const tokens = await UserScalarToken.findAll({where: {scalarToken: user.token}});
|
||||
for (const token of tokens) {
|
||||
await token.destroy();
|
||||
}
|
||||
Cache.for(CACHE_SCALAR_ACCOUNTS).clear();
|
||||
return {};
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { ScalarService } from "../scalar/ScalarService";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
interface BridgeRoomRequest {
|
||||
gitterRoomName: string;
|
||||
|
@ -12,12 +13,16 @@ interface BridgeRoomRequest {
|
|||
* API for interacting with the Gitter bridge
|
||||
*/
|
||||
@Path("/api/v1/dimension/gitter")
|
||||
@AutoWired
|
||||
export class DimensionGitterService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@GET
|
||||
@Path("room/:roomId/link")
|
||||
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedRoom> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const gitter = new GitterBridge(userId);
|
||||
|
@ -31,7 +36,7 @@ export class DimensionGitterService {
|
|||
@POST
|
||||
@Path("room/:roomId/link")
|
||||
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedRoom> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const gitter = new GitterBridge(userId);
|
||||
|
@ -46,7 +51,7 @@ export class DimensionGitterService {
|
|||
@DELETE
|
||||
@Path("room/:roomId/link")
|
||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const gitter = new GitterBridge(userId);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { ScalarService } from "../scalar/ScalarService";
|
||||
import { Widget } from "../../integrations/Widget";
|
||||
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||
import { Integration } from "../../integrations/Integration";
|
||||
|
@ -11,6 +10,8 @@ import { ComplexBot } from "../../integrations/ComplexBot";
|
|||
import { Bridge } from "../../integrations/Bridge";
|
||||
import { BridgeStore } from "../../db/BridgeStore";
|
||||
import { BotStore } from "../../db/BotStore";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
|
||||
export interface IntegrationsResponse {
|
||||
widgets: Widget[],
|
||||
|
@ -23,8 +24,12 @@ export interface IntegrationsResponse {
|
|||
* API for managing integrations, primarily for a given room
|
||||
*/
|
||||
@Path("/api/v1/dimension/integrations")
|
||||
@AutoWired
|
||||
export class DimensionIntegrationsService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
/**
|
||||
* Gets a list of widgets
|
||||
* @param {boolean} enabledOnly True to only return the enabled widgets
|
||||
|
@ -86,7 +91,7 @@ export class DimensionIntegrationsService {
|
|||
@GET
|
||||
@Path("room/:roomId")
|
||||
public async getIntegrationsInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<IntegrationsResponse> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
return {
|
||||
widgets: await DimensionIntegrationsService.getWidgets(true),
|
||||
bots: await DimensionIntegrationsService.getSimpleBots(userId),
|
||||
|
@ -110,7 +115,7 @@ export class DimensionIntegrationsService {
|
|||
@POST
|
||||
@Path("room/:roomId/integrations/:category/:type/config")
|
||||
public async setIntegrationConfigurationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string, newConfig: any): Promise<any> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig);
|
||||
else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig);
|
||||
|
@ -123,7 +128,7 @@ export class DimensionIntegrationsService {
|
|||
@DELETE
|
||||
@Path("room/:roomId/integrations/:category/:type")
|
||||
public async removeIntegrationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side");
|
||||
else if (category === "bot") {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ScalarService } from "../scalar/ScalarService";
|
||||
import { IrcBridge } from "../../bridges/IrcBridge";
|
||||
import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
|
||||
import { ApiError } from "../ApiError";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
|
||||
interface RequestLinkRequest {
|
||||
op: string;
|
||||
|
@ -13,12 +14,16 @@ interface RequestLinkRequest {
|
|||
* API for interacting with the IRC bridge
|
||||
*/
|
||||
@Path("/api/v1/dimension/irc")
|
||||
@AutoWired
|
||||
export class DimensionIrcService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@GET
|
||||
@Path(":networkId/channel/:channel/ops")
|
||||
public async getOps(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
||||
|
@ -34,7 +39,7 @@ export class DimensionIrcService {
|
|||
@POST
|
||||
@Path(":networkId/channel/:channel/link/:roomId")
|
||||
public async requestLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string, request: RequestLinkRequest): Promise<any> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
||||
|
@ -50,7 +55,7 @@ export class DimensionIrcService {
|
|||
@POST
|
||||
@Path(":networkId/channel/:channel/unlink/:roomId")
|
||||
public async unlink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { ScalarService } from "../scalar/ScalarService";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge";
|
||||
import { SlackChannel, SlackTeam } from "../../bridges/models/slack";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
interface BridgeRoomRequest {
|
||||
teamId: string;
|
||||
|
@ -14,12 +15,16 @@ interface BridgeRoomRequest {
|
|||
* API for interacting with the Slack bridge
|
||||
*/
|
||||
@Path("/api/v1/dimension/slack")
|
||||
@AutoWired
|
||||
export class DimensionSlackService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@GET
|
||||
@Path("room/:roomId/link")
|
||||
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedChannel> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
|
@ -33,7 +38,7 @@ export class DimensionSlackService {
|
|||
@POST
|
||||
@Path("room/:roomId/link")
|
||||
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedChannel> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
|
@ -48,7 +53,7 @@ export class DimensionSlackService {
|
|||
@DELETE
|
||||
@Path("room/:roomId/link")
|
||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
|
@ -65,7 +70,7 @@ export class DimensionSlackService {
|
|||
@GET
|
||||
@Path("teams")
|
||||
public async getTeams(@QueryParam("scalar_token") scalarToken: string): Promise<SlackTeam[]> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const slack = new SlackBridge(userId);
|
||||
const teams = await slack.getTeams();
|
||||
|
@ -76,7 +81,7 @@ export class DimensionSlackService {
|
|||
@GET
|
||||
@Path("teams/:teamId/channels")
|
||||
public async getChannels(@QueryParam("scalar_token") scalarToken: string, @PathParam("teamId") teamId: string): Promise<SlackChannel[]> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
|
@ -90,7 +95,7 @@ export class DimensionSlackService {
|
|||
@GET
|
||||
@Path("auth")
|
||||
public async getAuthUrl(@QueryParam("scalar_token") scalarToken: string): Promise<{ authUrl: string }> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
|
|
|
@ -2,12 +2,13 @@ import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
|||
import { Cache, CACHE_STICKERS } from "../../MemoryCache";
|
||||
import StickerPack from "../../db/models/StickerPack";
|
||||
import Sticker from "../../db/models/Sticker";
|
||||
import { ScalarService } from "../scalar/ScalarService";
|
||||
import UserStickerPack from "../../db/models/UserStickerPack";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { StickerpackMetadataDownloader } from "../../utils/StickerpackMetadataDownloader";
|
||||
import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
|
||||
import config from "../../config";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
export interface MemoryStickerPack {
|
||||
id: number;
|
||||
|
@ -63,8 +64,12 @@ interface StickerConfig {
|
|||
* API for stickers
|
||||
*/
|
||||
@Path("/api/v1/dimension/stickers")
|
||||
@AutoWired
|
||||
export class DimensionStickerService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
public static async getStickerPacks(enabledOnly = false): Promise<MemoryStickerPack[]> {
|
||||
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs");
|
||||
if (cachedPacks) {
|
||||
|
@ -86,7 +91,7 @@ export class DimensionStickerService {
|
|||
@GET
|
||||
@Path("config")
|
||||
public async getConfig(@QueryParam("scalar_token") scalarToken: string): Promise<StickerConfig> {
|
||||
await ScalarService.getTokenOwner(scalarToken);
|
||||
await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
return {
|
||||
enabled: config.stickers.enabled,
|
||||
|
@ -98,7 +103,7 @@ export class DimensionStickerService {
|
|||
@GET
|
||||
@Path("packs")
|
||||
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs_" + userId);
|
||||
if (cachedPacks) return cachedPacks;
|
||||
|
@ -125,7 +130,7 @@ export class DimensionStickerService {
|
|||
@POST
|
||||
@Path("packs/:packId/selected")
|
||||
public async setPackSelected(@QueryParam("scalar_token") scalarToken: string, @PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const pack = await StickerPack.findByPrimary(packId);
|
||||
if (!pack) throw new ApiError(404, "Sticker pack not found");
|
||||
|
@ -149,7 +154,7 @@ export class DimensionStickerService {
|
|||
@POST
|
||||
@Path("packs/import")
|
||||
public async importPack(@QueryParam("scalar_token") scalarToken: string, request: ImportPackRequest): Promise<MemoryUserStickerPack> {
|
||||
await ScalarService.getTokenOwner(scalarToken);
|
||||
await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
if (!config.stickers.enabled) {
|
||||
throw new ApiError(400, "Custom stickerpacks are disabled on this homeserver");
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { ScalarService } from "../scalar/ScalarService";
|
||||
import { TelegramBridge } from "../../bridges/TelegramBridge";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
interface PortalInfoResponse {
|
||||
bridged: boolean;
|
||||
|
@ -19,12 +20,16 @@ interface BridgeRoomRequest {
|
|||
* API for interacting with the Telegram bridge
|
||||
*/
|
||||
@Path("/api/v1/dimension/telegram")
|
||||
@AutoWired
|
||||
export class DimensionTelegramService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@GET
|
||||
@Path("chat/:chatId")
|
||||
public async getPortalInfo(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @QueryParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const telegram = new TelegramBridge(userId);
|
||||
|
@ -47,7 +52,7 @@ export class DimensionTelegramService {
|
|||
@POST
|
||||
@Path("chat/:chatId/room/:roomId")
|
||||
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<PortalInfoResponse> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const telegram = new TelegramBridge(userId);
|
||||
|
@ -69,7 +74,7 @@ export class DimensionTelegramService {
|
|||
@DELETE
|
||||
@Path("room/:roomId")
|
||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
try {
|
||||
const telegram = new TelegramBridge(userId);
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
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";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
/**
|
||||
* API for interacting with the Webhooks bridge, and for setting up proxies to other
|
||||
* services.
|
||||
*/
|
||||
@Path("/api/v1/dimension/webhooks")
|
||||
@AutoWired
|
||||
export class DimensionWebhooksService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@POST
|
||||
@Path("/travisci/:webhookId")
|
||||
public async postTravisCiWebhook(@PathParam("webhookId") webhookId: string, @FormParam("payload") payload: string, @HeaderParam("Signature") signature: string): Promise<any> {
|
||||
|
@ -43,7 +48,7 @@ export class DimensionWebhooksService {
|
|||
@POST
|
||||
@Path("room/:roomId/webhooks/new")
|
||||
public async newWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const webhooks = new WebhooksBridge(userId);
|
||||
return webhooks.createWebhook(roomId, options);
|
||||
|
@ -52,7 +57,7 @@ export class DimensionWebhooksService {
|
|||
@POST
|
||||
@Path("room/:roomId/webhooks/:hookId")
|
||||
public async updateWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const webhooks = new WebhooksBridge(userId);
|
||||
return webhooks.updateWebhook(roomId, hookId, options);
|
||||
|
@ -61,7 +66,7 @@ export class DimensionWebhooksService {
|
|||
@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 userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const webhooks = new WebhooksBridge(userId);
|
||||
return webhooks.deleteWebhook(roomId, hookId);
|
||||
|
|
41
src/api/msc/MSCAccountService.ts
Normal file
41
src/api/msc/MSCAccountService.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { OpenId } from "../../models/OpenId";
|
||||
import AccountController, { IAccountInfoResponse, IAccountRegisteredResponse } from "../controllers/AccountController";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import { IMSCUser, ROLE_MSC_USER } from "../security/MSCSecurity";
|
||||
|
||||
/**
|
||||
* API for account management
|
||||
*/
|
||||
@Path("/_matrix/integrations/v1/account")
|
||||
@AutoWired
|
||||
export class MSCAccountService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@POST
|
||||
@Path("register")
|
||||
public async register(request: OpenId): Promise<IAccountRegisteredResponse> {
|
||||
return this.accountController.registerAccount(request);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@Security(ROLE_MSC_USER)
|
||||
public async info(): Promise<IAccountInfoResponse> {
|
||||
const user: IMSCUser = this.context.request.user;
|
||||
return {user_id: user.userId};
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("logout")
|
||||
@Security(ROLE_MSC_USER)
|
||||
public async logout(): Promise<any> {
|
||||
await this.accountController.logout(this.context.request.user);
|
||||
return {};
|
||||
}
|
||||
}
|
|
@ -1,104 +1,30 @@
|
|||
import { GET, Path, POST, QueryParam } from "typescript-rest";
|
||||
import { MatrixOpenIdClient } from "../../matrix/MatrixOpenIdClient";
|
||||
import Upstream from "../../db/models/Upstream";
|
||||
import { ScalarClient } from "../../scalar/ScalarClient";
|
||||
import User from "../../db/models/User";
|
||||
import UserScalarToken from "../../db/models/UserScalarToken";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
import * as randomString from "random-string";
|
||||
import { OpenId } from "../../models/OpenId";
|
||||
import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses";
|
||||
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
|
||||
import { ScalarStore } from "../../db/ScalarStore";
|
||||
|
||||
interface RegisterRequest {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
matrix_server_name: string;
|
||||
expires_in: number;
|
||||
}
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
/**
|
||||
* API for the minimum Scalar API we need to implement to be compatible with clients. Used for registration
|
||||
* and general account management.
|
||||
*/
|
||||
@Path("/api/v1/scalar")
|
||||
@AutoWired
|
||||
export class ScalarService {
|
||||
|
||||
/**
|
||||
* Gets the owner of a given scalar token, throwing an ApiError if the token is invalid.
|
||||
* @param {string} scalarToken The scalar token to validate
|
||||
* @param {boolean} ignoreUpstreams True to consider the token valid if it is missing links to other upstreams
|
||||
* @returns {Promise<string>} Resolves to the owner's user ID if the token is valid.
|
||||
* @throws {ApiError} Thrown with a status code of 401 if the token is invalid.
|
||||
*/
|
||||
public static async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<string> {
|
||||
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
|
||||
if (cachedUserId) return cachedUserId;
|
||||
|
||||
try {
|
||||
const user = await ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams);
|
||||
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
|
||||
return user.userId;
|
||||
} catch (err) {
|
||||
LogService.error("ScalarService", err);
|
||||
throw new ApiError(401, "Invalid token");
|
||||
}
|
||||
}
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@POST
|
||||
@Path("register")
|
||||
public async register(request: RegisterRequest, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
|
||||
public async register(request: OpenId, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
|
||||
if (apiVersion !== "1.1") {
|
||||
throw new ApiError(401, "Invalid API version.");
|
||||
}
|
||||
|
||||
const mxClient = new MatrixOpenIdClient(<OpenId>request);
|
||||
const mxUserId = await mxClient.getUserId();
|
||||
|
||||
if (!mxUserId.endsWith(":" + request.matrix_server_name)) {
|
||||
LogService.warn("ScalarService", `OpenID subject '${mxUserId}' does not belong to the homeserver '${request.matrix_server_name}'`);
|
||||
throw new ApiError(401, "Invalid token");
|
||||
}
|
||||
|
||||
const user = await User.findByPrimary(mxUserId);
|
||||
if (!user) {
|
||||
// There's a small chance we'll get a validation error because of:
|
||||
// https://github.com/vector-im/riot-web/issues/5846
|
||||
LogService.verbose("ScalarService", "User " + mxUserId + " never seen before - creating");
|
||||
await User.create({userId: mxUserId});
|
||||
}
|
||||
|
||||
const upstreams = await Upstream.findAll();
|
||||
await Promise.all(upstreams.map(async upstream => {
|
||||
if (!await ScalarStore.isUpstreamOnline(upstream)) {
|
||||
LogService.warn("ScalarService", `Skipping registration for ${mxUserId} on upstream ${upstream.id} (${upstream.name}) because it is offline`);
|
||||
return null;
|
||||
}
|
||||
const tokens = await UserScalarToken.findAll({where: {userId: mxUserId, upstreamId: upstream.id}});
|
||||
if (!tokens || tokens.length === 0) {
|
||||
LogService.info("ScalarService", "Registering " + mxUserId + " for a token at upstream " + upstream.id + " (" + upstream.name + ")");
|
||||
const client = new ScalarClient(upstream);
|
||||
const response = await client.register(<OpenId>request);
|
||||
return UserScalarToken.create({
|
||||
userId: mxUserId,
|
||||
scalarToken: response.scalar_token,
|
||||
isDimensionToken: false,
|
||||
upstreamId: upstream.id,
|
||||
});
|
||||
}
|
||||
}).filter(token => !!token));
|
||||
|
||||
const dimensionToken = randomString({length: 25});
|
||||
const dimensionScalarToken = await UserScalarToken.create({
|
||||
userId: mxUserId,
|
||||
scalarToken: dimensionToken,
|
||||
isDimensionToken: true,
|
||||
});
|
||||
|
||||
LogService.info("ScalarService", mxUserId + " has registered for a scalar token successfully");
|
||||
return {scalar_token: dimensionScalarToken.scalarToken};
|
||||
const response = await this.accountController.registerAccount(request);
|
||||
return {scalar_token: response.token};
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -108,7 +34,7 @@ export class ScalarService {
|
|||
throw new ApiError(401, "Invalid API version.");
|
||||
}
|
||||
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
return {user_id: userId};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@ import { LogService } from "matrix-js-snippets";
|
|||
import { Cache, CACHE_WIDGET_TITLES } from "../../MemoryCache";
|
||||
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
|
||||
import config from "../../config";
|
||||
import { ScalarService } from "./ScalarService";
|
||||
import moment = require("moment");
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
interface UrlPreviewResponse {
|
||||
cached_response: boolean;
|
||||
|
@ -22,12 +23,16 @@ interface UrlPreviewResponse {
|
|||
* API for the minimum Scalar API for widget functionality in clients.
|
||||
*/
|
||||
@Path("/api/v1/scalar/widgets")
|
||||
@AutoWired
|
||||
export class ScalarWidgetService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@GET
|
||||
@Path("title_lookup")
|
||||
public async titleLookup(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
|
||||
await ScalarService.getTokenOwner(scalarToken);
|
||||
await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
const cachedResult = Cache.for(CACHE_WIDGET_TITLES).get(url);
|
||||
if (cachedResult) {
|
||||
|
|
56
src/api/security/MSCSecurity.ts
Normal file
56
src/api/security/MSCSecurity.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { ServiceAuthenticator } from "typescript-rest";
|
||||
import { Request, RequestHandler, Response, Router } from "express";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
export interface IMSCUser {
|
||||
userId: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const ROLE_MSC_USER = "ROLE_MSC_USER";
|
||||
|
||||
export default class MSCSecurity implements ServiceAuthenticator {
|
||||
|
||||
private accountController = new AccountController();
|
||||
|
||||
public getRoles(req: Request): string[] {
|
||||
if (req.user) return [ROLE_MSC_USER];
|
||||
return [];
|
||||
}
|
||||
|
||||
getMiddleware(): RequestHandler {
|
||||
return (async (req: Request, res: Response, next: () => void) => {
|
||||
try {
|
||||
if (req.headers.authorization) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header.startsWith("Bearer ")) {
|
||||
return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
|
||||
}
|
||||
|
||||
const token = header.substring("Bearer ".length);
|
||||
req.user = <IMSCUser>{
|
||||
userId: await this.accountController.getTokenOwner(token),
|
||||
token: token,
|
||||
};
|
||||
return next();
|
||||
}
|
||||
|
||||
console.log(req.query);
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
// TODO: Proper error message
|
||||
res.status(e.statusCode).json({errcode: e.errorCode, error: "Error"});
|
||||
} else {
|
||||
LogService.error("MSCSecurity", e);
|
||||
res.status(500).json({errcode: "M_UNKNOWN", error: "Unknown server error"});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initialize(_router: Router): void {
|
||||
}
|
||||
|
||||
}
|
|
@ -142,7 +142,11 @@ module.exports = function () {
|
|||
'/api': {
|
||||
target: 'http://localhost:8184',
|
||||
secure: false
|
||||
}
|
||||
},
|
||||
'/_matrix': {
|
||||
target: 'http://localhost:8184',
|
||||
secure: false
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user