From 147d8a18ae36d091f1fc5366e71f3d5ab7386ca6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 6 Jul 2019 16:41:07 -0600 Subject: [PATCH] Simple implementation of listing and accepting policies --- src/api/controllers/TermsController.ts | 50 ++++++++++++++++++- src/api/msc/MSCTermsService.ts | 14 +++++- src/db/DimensionStore.ts | 2 + .../20190630194345-AddTermsOfService.ts | 2 +- .../20190706154345-AddUserSignedTerms.ts | 25 ++++++++++ src/db/models/TermsSignedRecord.ts | 25 ++++++++++ 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 src/db/migrations/20190706154345-AddUserSignedTerms.ts create mode 100644 src/db/models/TermsSignedRecord.ts diff --git a/src/api/controllers/TermsController.ts b/src/api/controllers/TermsController.ts index 6b9a67a..0bfc495 100644 --- a/src/api/controllers/TermsController.ts +++ b/src/api/controllers/TermsController.ts @@ -2,6 +2,8 @@ import { AutoWired } from "typescript-ioc/es6"; import { IMSCUser } from "../security/MSCSecurity"; import TermsRecord from "../../db/models/TermsRecord"; import TermsTextRecord from "../../db/models/TermsTextRecord"; +import TermsSignedRecord from "../../db/models/TermsSignedRecord"; +import { Op } from "sequelize"; export interface ILanguagePolicy { name: string; @@ -45,9 +47,53 @@ export default class TermsController { return Object.keys((await this.getMissingTermsForUser(user)).policies).length > 0; } - public async getMissingTermsForUser(_user: IMSCUser): Promise { + public async getMissingTermsForUser(user: IMSCUser): Promise { // TODO: Abuse a cache for non-draft policies - return {policies: {}}; + // TODO: Upstream policies + + const notDrafts = await TermsRecord.findAll({ + where: {version: {[Op.ne]: VERSION_DRAFT}}, + include: [TermsTextRecord], + }); + const signed = await TermsSignedRecord.findAll({where: {userId: user.userId}}); + + const latest: { [shortcode: string]: TermsRecord } = {}; + for (const record of notDrafts) { + if (!latest[record.shortcode]) { + latest[record.shortcode] = record; + } + if (latest[record.shortcode].id < record.id) { + latest[record.shortcode] = record; + } + } + + const missing = Object.values(latest).filter(d => !signed.find(s => s.termsId === d.id)); + const policies: ITermsNotSignedResponse = {policies: {}}; + + for (const missingPolicy of missing) { + policies.policies[missingPolicy.shortcode] = { + version: missingPolicy.version, + }; + + for (const text of missingPolicy.texts) { + policies.policies[missingPolicy.shortcode][text.language] = { + name: text.name, + url: text.url, + }; + } + } + + return policies; + } + + public async signTermsMatching(user: IMSCUser, urls: string[]): Promise { + const terms = await TermsTextRecord.findAll({where: {url: {[Op.in]: urls}}}); + const signed = await TermsSignedRecord.findAll({where: {userId: user.userId}}); + + const toAdd = terms.filter(t => !signed.find(s => s.termsId === t.termsId)); + for (const termsToSign of toAdd) { + await TermsSignedRecord.create({termsId: termsToSign.id, userId: user.userId}); + } } public async getPoliciesForAdmin(): Promise { diff --git a/src/api/msc/MSCTermsService.ts b/src/api/msc/MSCTermsService.ts index cc9405b..3a9c618 100644 --- a/src/api/msc/MSCTermsService.ts +++ b/src/api/msc/MSCTermsService.ts @@ -1,8 +1,12 @@ -import { Context, GET, Path, Security, ServiceContext } from "typescript-rest"; +import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-rest"; import { AutoWired, Inject } from "typescript-ioc/es6"; import { ROLE_MSC_USER } from "../security/MSCSecurity"; import TermsController, { ITermsNotSignedResponse } from "../controllers/TermsController"; +interface SignTermsRequest { + user_accepts: string[]; +} + /** * API for account management */ @@ -22,4 +26,12 @@ export class MSCTermsService { public async needsSignatures(): Promise { return this.termsController.getMissingTermsForUser(this.context.request.user); } + + @POST + @Path("") + @Security(ROLE_MSC_USER) + public async signTerms(request: SignTermsRequest): Promise { + await this.termsController.signTermsMatching(this.context.request.user, request.user_accepts); + return {}; + } } \ No newline at end of file diff --git a/src/db/DimensionStore.ts b/src/db/DimensionStore.ts index 2d290e1..3551c3e 100644 --- a/src/db/DimensionStore.ts +++ b/src/db/DimensionStore.ts @@ -28,6 +28,7 @@ import CustomSimpleBotRecord from "./models/CustomSimpleBotRecord"; import SlackBridgeRecord from "./models/SlackBridgeRecord"; import TermsRecord from "./models/TermsRecord"; import TermsTextRecord from "./models/TermsTextRecord"; +import TermsSignedRecord from "./models/TermsSignedRecord"; class _DimensionStore { private sequelize: Sequelize; @@ -67,6 +68,7 @@ class _DimensionStore { SlackBridgeRecord, TermsRecord, TermsTextRecord, + TermsSignedRecord, ]); } diff --git a/src/db/migrations/20190630194345-AddTermsOfService.ts b/src/db/migrations/20190630194345-AddTermsOfService.ts index 72bcc9b..e2078ff 100644 --- a/src/db/migrations/20190630194345-AddTermsOfService.ts +++ b/src/db/migrations/20190630194345-AddTermsOfService.ts @@ -13,7 +13,7 @@ export default { .then(() => queryInterface.createTable("dimension_terms_text", { "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, "termsId": { - type: DataType.INTEGER, allowNull: true, + type: DataType.INTEGER, allowNull: false, references: {model: "dimension_terms", key: "id"}, onUpdate: "cascade", onDelete: "cascade", }, diff --git a/src/db/migrations/20190706154345-AddUserSignedTerms.ts b/src/db/migrations/20190706154345-AddUserSignedTerms.ts new file mode 100644 index 0000000..2ee9b5a --- /dev/null +++ b/src/db/migrations/20190706154345-AddUserSignedTerms.ts @@ -0,0 +1,25 @@ +import { QueryInterface } from "sequelize"; +import { DataType } from "sequelize-typescript"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.createTable("dimension_terms_signed", { + "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + "termsId": { + type: DataType.INTEGER, allowNull: false, + references: {model: "dimension_terms", key: "id"}, + onUpdate: "cascade", onDelete: "cascade", + }, + "userId": { + type: DataType.STRING, allowNull: false, + references: {model: "dimension_users", key: "userId"}, + onUpdate: "cascade", onDelete: "cascade", + }, + })); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.dropTable("dimension_terms_signed")); + } +} \ No newline at end of file diff --git a/src/db/models/TermsSignedRecord.ts b/src/db/models/TermsSignedRecord.ts new file mode 100644 index 0000000..0e8fb31 --- /dev/null +++ b/src/db/models/TermsSignedRecord.ts @@ -0,0 +1,25 @@ +import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; +import User from "./User"; +import TermsRecord from "./TermsRecord"; + +@Table({ + tableName: "dimension_terms_signed", + underscored: false, + timestamps: false, +}) +export default class TermsSignedRecord extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @AllowNull + @Column + @ForeignKey(() => TermsRecord) + termsId?: number; + + @AllowNull + @Column + @ForeignKey(() => User) + userId?: string; +} \ No newline at end of file