From e006851465ad3f14093dd9f01738535bcfc4f802 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jul 2019 18:09:25 -0600 Subject: [PATCH] Make terms of service endpoint match MSC --- src/api/controllers/TermsController.ts | 61 ++++++++++++++++---------- src/api/matrix/MatrixTermsService.ts | 7 ++- src/api/scalar/ScalarService.ts | 7 ++- src/db/ScalarStore.ts | 33 ++++++++++++-- src/scalar/ScalarClient.ts | 17 +++++-- 5 files changed, 87 insertions(+), 38 deletions(-) diff --git a/src/api/controllers/TermsController.ts b/src/api/controllers/TermsController.ts index 0d35441..168dd1a 100644 --- a/src/api/controllers/TermsController.ts +++ b/src/api/controllers/TermsController.ts @@ -24,7 +24,7 @@ export interface IPolicy { [language: string]: string | ILanguagePolicy; } -export interface ITermsNotSignedResponse { +export interface ITermsResponse { policies: { [policyName: string]: IPolicy }; } @@ -87,40 +87,57 @@ export default class TermsController { } public async doesUserNeedToSignTerms(user: ILoggedInUser): Promise { - return Object.keys((await this.getMissingTermsForUser(user)).policies).length > 0; - } - - public async getMissingTermsForUser(user: ILoggedInUser): Promise { const latest = await this.getPublishedTerms(); const signed = await TermsSignedRecord.findAll({where: {userId: user.userId}}); const missing = Object.values(latest).filter(d => !signed.find(s => s.termsId === d.id)); - const policies: ITermsNotSignedResponse = {policies: {}}; + if (missing.length > 0) return true; - for (const missingPolicy of missing) { - policies.policies[missingPolicy.shortcode] = { - version: missingPolicy.version, + // Test upstream terms for the user + const tokensForUser = await UserScalarToken.findAll({where: {userId: user.userId}, include: [Upstream]}); + const upstreamTokens = tokensForUser.filter(t => t.upstream); + for (const upstreamToken of upstreamTokens) { + try { + const scalarClient = new ScalarClient(upstreamToken.upstream, ScalarClient.KIND_MATRIX_V1); + await scalarClient.getAccount(upstreamToken.scalarToken); + // 200 OK means we're fine + } catch (e) { + if (e.statusCode === 403 && e.body && e.body.errcode === 'M_TERMS_NOT_SIGNED') { + return true; + } + } + } + + return false; + } + + public async getAvailableTerms(): Promise { + const latest = await this.getPublishedTerms(); + const policies: ITermsResponse = {policies: {}}; + + for (const termsPolicy of Object.values(latest)) { + policies.policies[termsPolicy.shortcode] = { + version: termsPolicy.version, }; - for (const language in missingPolicy.languages) { - policies.policies[missingPolicy.shortcode][language] = { - name: missingPolicy.languages[language].name, - url: missingPolicy.languages[language].url, + for (const language in termsPolicy.languages) { + policies.policies[termsPolicy.shortcode][language] = { + name: termsPolicy.languages[language].name, + url: termsPolicy.languages[language].url, }; } } - // Get upstream terms for the user - const tokensForUser = await UserScalarToken.findAll({where: {userId: user.userId}, include: [Upstream]}); - const upstreamTokens = tokensForUser.filter(t => t.upstream); + // Get upstream terms + const usptreams = await Upstream.findAll(); const urlsToUpstream = {}; // {url: [upstreamId]} - for (const upstreamToken of upstreamTokens) { + for (const upstream of usptreams) { try { - const scalarClient = new ScalarClient(upstreamToken.upstream, ScalarClient.KIND_MATRIX_V1); - const upstreamTerms = await scalarClient.getMissingTerms(upstreamToken.scalarToken); + const scalarClient = new ScalarClient(upstream, ScalarClient.KIND_MATRIX_V1); + const upstreamTerms = await scalarClient.getAvailableTerms(); // rewrite the shortcodes to avoid conflicts - const shortcodePrefix = `upstream_${md5(`${upstreamToken.id}:${upstreamToken.upstream.apiUrl}`)}`; + const shortcodePrefix = `upstream_${md5(`${upstream.id}:${upstream.apiUrl}`)}`; for (const shortcode of Object.keys(upstreamTerms.policies)) { policies.policies[`${shortcodePrefix}_${shortcode}`] = upstreamTerms.policies[shortcode]; @@ -129,8 +146,8 @@ export default class TermsController { const upstreamUrl = upstreamTerms.policies[shortcode][language]['url']; if (!urlsToUpstream[upstreamUrl]) urlsToUpstream[upstreamUrl] = []; const upstreamsArr = urlsToUpstream[upstreamUrl]; - if (!upstreamsArr.includes(upstreamToken.upstream.id)) { - upstreamsArr.push(upstreamToken.upstream.id); + if (!upstreamsArr.includes(upstream.id)) { + upstreamsArr.push(upstream.id); } } } diff --git a/src/api/matrix/MatrixTermsService.ts b/src/api/matrix/MatrixTermsService.ts index 61b86ea..50d11ee 100644 --- a/src/api/matrix/MatrixTermsService.ts +++ b/src/api/matrix/MatrixTermsService.ts @@ -1,7 +1,7 @@ import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-rest"; import { AutoWired, Inject } from "typescript-ioc/es6"; import { ROLE_USER } from "../security/MatrixSecurity"; -import TermsController, { ITermsNotSignedResponse } from "../controllers/TermsController"; +import TermsController, { ITermsResponse } from "../controllers/TermsController"; export interface SignTermsRequest { user_accepts: string[]; @@ -22,9 +22,8 @@ export class MatrixTermsService { @GET @Path("") - @Security(ROLE_USER) - public async needsSignatures(): Promise { - return this.termsController.getMissingTermsForUser(this.context.request.user); + public async getAllTerms(): Promise { + return this.termsController.getAvailableTerms(); } @POST diff --git a/src/api/scalar/ScalarService.ts b/src/api/scalar/ScalarService.ts index b36ce58..8c8b8ba 100644 --- a/src/api/scalar/ScalarService.ts +++ b/src/api/scalar/ScalarService.ts @@ -5,7 +5,7 @@ import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/Scal import { AutoWired, Inject } from "typescript-ioc/es6"; import AccountController from "../controllers/AccountController"; import { ROLE_USER } from "../security/MatrixSecurity"; -import TermsController, { ITermsNotSignedResponse } from "../controllers/TermsController"; +import TermsController, { ITermsResponse } from "../controllers/TermsController"; import { SignTermsRequest } from "../matrix/MatrixTermsService"; import { ScalarClient } from "../../scalar/ScalarClient"; @@ -50,9 +50,8 @@ export class ScalarService { @GET @Path("terms") - @Security(ROLE_USER) - public async getTerms(): Promise { - return this.termsController.getMissingTermsForUser(this.context.request.user); + public async getTerms(): Promise { + return this.termsController.getAvailableTerms(); } @POST diff --git a/src/db/ScalarStore.ts b/src/db/ScalarStore.ts index a4d0108..c610154 100644 --- a/src/db/ScalarStore.ts +++ b/src/db/ScalarStore.ts @@ -5,6 +5,7 @@ import User from "./models/User"; import { MatrixStickerBot } from "../matrix/MatrixStickerBot"; import { ScalarClient } from "../scalar/ScalarClient"; import { Cache, CACHE_SCALAR_ONLINE_STATE } from "../MemoryCache"; +import { ILanguagePolicy } from "../api/controllers/TermsController"; export class ScalarStore { @@ -56,7 +57,7 @@ export class ScalarStore { return state; } - private static async checkIfUpstreamOnline(upstream: Upstream, scalarKind: string): Promise { + private static async checkIfUpstreamOnline(upstream: Upstream, scalarKind: string, signTerms = true): Promise { try { // The sticker bot can be used for this for now @@ -78,8 +79,14 @@ export class ScalarStore { return true; // it's online } catch (e) { LogService.error("ScalarStore", e); - if (!isNaN(Number(e))) { - if (e === 401 || e === 403) { + if (e && !isNaN(Number(e.statusCode))) { + if (e.statusCode === 403 && e.body) { + if (e.body.errcode === 'M_TERMS_NOT_SIGNED' && signTerms) { + await ScalarStore.signAllTerms(existingTokens[0], scalarKind); + return ScalarStore.checkIfUpstreamOnline(upstream, scalarKind, false); + } + } + if (e.statusCode === 401 || e.statusCode === 403) { LogService.info("ScalarStore", "Test user token expired"); } else { // Assume offline @@ -107,13 +114,16 @@ export class ScalarStore { const openId = await MatrixStickerBot.getOpenId(); const token = await scalarClient.register(openId); - await UserScalarToken.create({ + const scalarToken = await UserScalarToken.create({ userId: testUserId, scalarToken: token.scalar_token, isDimensionToken: false, upstreamId: upstream.id, }); + // Accept all terms of service for the user + await ScalarStore.signAllTerms(scalarToken, scalarKind); + return true; } catch (e) { LogService.error("ScalarStore", e); @@ -121,6 +131,21 @@ export class ScalarStore { } } + private static async signAllTerms(token: UserScalarToken, scalarKind: string) { + try { + const client = new ScalarClient(token.upstream, scalarKind); + const terms = await client.getAvailableTerms(); + const urlsToSign = Object.values(terms.policies).map(p => { + const englishCode = Object.keys(p).find(k => k.toLowerCase() === 'en' || k.toLowerCase().startsWith('en_')); + if (!englishCode) return null; + return (p[englishCode]).url; + }).filter(v => !!v); + await client.signTermsUrls(token.scalarToken, urlsToSign); + } catch (e) { + LogService.error("ScalarStore", e); + } + } + private constructor() { } } \ No newline at end of file diff --git a/src/scalar/ScalarClient.ts b/src/scalar/ScalarClient.ts index de351a1..5c702cc 100644 --- a/src/scalar/ScalarClient.ts +++ b/src/scalar/ScalarClient.ts @@ -4,7 +4,8 @@ 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"; -import { ITermsNotSignedResponse } from "../api/controllers/TermsController"; +import { ITermsResponse } from "../api/controllers/TermsController"; +import { subscriptionLogsToBeFn } from "rxjs/internal/testing/TestScheduler"; const REGISTER_ROUTE = "/register"; const ACCOUNT_INFO_ROUTE = "/account"; @@ -90,8 +91,16 @@ export class ScalarClient { LogService.error("ScalarClient", err); reject(err); } else if (res.statusCode !== 200) { + if (typeof(res.body) === 'string') { + try { + res.body = JSON.parse(res.body); + } catch (e) { + LogService.error("ScalarClient", "Got error parsing error response:"); + LogService.error("ScalarClient", e); + } + } LogService.error("ScalarClient", "Got status code " + res.statusCode + " while getting information for token"); - reject(res.statusCode); + reject(res); } else { resolve(res.body); } @@ -124,8 +133,8 @@ export class ScalarClient { }); } - public getMissingTerms(token: string): Promise { - const {scalarUrl, headers, queryString} = this.makeRequestArguments(TERMS_ROUTE, token); + public getAvailableTerms(): Promise { + const {scalarUrl, headers, queryString} = this.makeRequestArguments(TERMS_ROUTE, null); LogService.info("ScalarClient", "Doing upstream scalar request: GET " + scalarUrl); return new Promise((resolve, reject) => { request({