Make terms of service endpoint match MSC

This commit is contained in:
Travis Ralston 2019-07-22 18:09:25 -06:00
parent 9cc1454527
commit e006851465
5 changed files with 87 additions and 38 deletions

View File

@ -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<boolean> {
return Object.keys((await this.getMissingTermsForUser(user)).policies).length > 0;
}
public async getMissingTermsForUser(user: ILoggedInUser): Promise<ITermsNotSignedResponse> {
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,
};
for (const language in missingPolicy.languages) {
policies.policies[missingPolicy.shortcode][language] = {
name: missingPolicy.languages[language].name,
url: missingPolicy.languages[language].url,
};
}
}
// Get upstream terms for the user
// Test upstream terms for the user
const tokensForUser = await UserScalarToken.findAll({where: {userId: user.userId}, include: [Upstream]});
const upstreamTokens = tokensForUser.filter(t => t.upstream);
const urlsToUpstream = {}; // {url: [upstreamId]}
for (const upstreamToken of upstreamTokens) {
try {
const scalarClient = new ScalarClient(upstreamToken.upstream, ScalarClient.KIND_MATRIX_V1);
const upstreamTerms = await scalarClient.getMissingTerms(upstreamToken.scalarToken);
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<ITermsResponse> {
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 termsPolicy.languages) {
policies.policies[termsPolicy.shortcode][language] = {
name: termsPolicy.languages[language].name,
url: termsPolicy.languages[language].url,
};
}
}
// Get upstream terms
const usptreams = await Upstream.findAll();
const urlsToUpstream = {}; // {url: [upstreamId]}
for (const upstream of usptreams) {
try {
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);
}
}
}

View File

@ -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<ITermsNotSignedResponse> {
return this.termsController.getMissingTermsForUser(this.context.request.user);
public async getAllTerms(): Promise<ITermsResponse> {
return this.termsController.getAvailableTerms();
}
@POST

View File

@ -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<ITermsNotSignedResponse> {
return this.termsController.getMissingTermsForUser(this.context.request.user);
public async getTerms(): Promise<ITermsResponse> {
return this.termsController.getAvailableTerms();
}
@POST

View File

@ -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<boolean> {
private static async checkIfUpstreamOnline(upstream: Upstream, scalarKind: string, signTerms = true): Promise<boolean> {
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 (<ILanguagePolicy>p[englishCode]).url;
}).filter(v => !!v);
await client.signTermsUrls(token.scalarToken, urlsToSign);
} catch (e) {
LogService.error("ScalarStore", e);
}
}
private constructor() {
}
}

View File

@ -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<ITermsNotSignedResponse> {
const {scalarUrl, headers, queryString} = this.makeRequestArguments(TERMS_ROUTE, token);
public getAvailableTerms(): Promise<ITermsResponse> {
const {scalarUrl, headers, queryString} = this.makeRequestArguments(TERMS_ROUTE, null);
LogService.info("ScalarClient", "Doing upstream scalar request: GET " + scalarUrl);
return new Promise((resolve, reject) => {
request({