Simple implementation of listing and accepting policies

This commit is contained in:
Travis Ralston 2019-07-06 16:41:07 -06:00
parent 6c6ae5c5ed
commit 147d8a18ae
6 changed files with 114 additions and 4 deletions

View File

@ -2,6 +2,8 @@ import { AutoWired } from "typescript-ioc/es6";
import { IMSCUser } from "../security/MSCSecurity"; import { IMSCUser } from "../security/MSCSecurity";
import TermsRecord from "../../db/models/TermsRecord"; import TermsRecord from "../../db/models/TermsRecord";
import TermsTextRecord from "../../db/models/TermsTextRecord"; import TermsTextRecord from "../../db/models/TermsTextRecord";
import TermsSignedRecord from "../../db/models/TermsSignedRecord";
import { Op } from "sequelize";
export interface ILanguagePolicy { export interface ILanguagePolicy {
name: string; name: string;
@ -45,9 +47,53 @@ export default class TermsController {
return Object.keys((await this.getMissingTermsForUser(user)).policies).length > 0; return Object.keys((await this.getMissingTermsForUser(user)).policies).length > 0;
} }
public async getMissingTermsForUser(_user: IMSCUser): Promise<ITermsNotSignedResponse> { public async getMissingTermsForUser(user: IMSCUser): Promise<ITermsNotSignedResponse> {
// TODO: Abuse a cache for non-draft policies // 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<any> {
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<ITerms[]> { public async getPoliciesForAdmin(): Promise<ITerms[]> {

View File

@ -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 { AutoWired, Inject } from "typescript-ioc/es6";
import { ROLE_MSC_USER } from "../security/MSCSecurity"; import { ROLE_MSC_USER } from "../security/MSCSecurity";
import TermsController, { ITermsNotSignedResponse } from "../controllers/TermsController"; import TermsController, { ITermsNotSignedResponse } from "../controllers/TermsController";
interface SignTermsRequest {
user_accepts: string[];
}
/** /**
* API for account management * API for account management
*/ */
@ -22,4 +26,12 @@ export class MSCTermsService {
public async needsSignatures(): Promise<ITermsNotSignedResponse> { public async needsSignatures(): Promise<ITermsNotSignedResponse> {
return this.termsController.getMissingTermsForUser(this.context.request.user); return this.termsController.getMissingTermsForUser(this.context.request.user);
} }
@POST
@Path("")
@Security(ROLE_MSC_USER)
public async signTerms(request: SignTermsRequest): Promise<any> {
await this.termsController.signTermsMatching(this.context.request.user, request.user_accepts);
return {};
}
} }

View File

@ -28,6 +28,7 @@ import CustomSimpleBotRecord from "./models/CustomSimpleBotRecord";
import SlackBridgeRecord from "./models/SlackBridgeRecord"; import SlackBridgeRecord from "./models/SlackBridgeRecord";
import TermsRecord from "./models/TermsRecord"; import TermsRecord from "./models/TermsRecord";
import TermsTextRecord from "./models/TermsTextRecord"; import TermsTextRecord from "./models/TermsTextRecord";
import TermsSignedRecord from "./models/TermsSignedRecord";
class _DimensionStore { class _DimensionStore {
private sequelize: Sequelize; private sequelize: Sequelize;
@ -67,6 +68,7 @@ class _DimensionStore {
SlackBridgeRecord, SlackBridgeRecord,
TermsRecord, TermsRecord,
TermsTextRecord, TermsTextRecord,
TermsSignedRecord,
]); ]);
} }

View File

@ -13,7 +13,7 @@ export default {
.then(() => queryInterface.createTable("dimension_terms_text", { .then(() => queryInterface.createTable("dimension_terms_text", {
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
"termsId": { "termsId": {
type: DataType.INTEGER, allowNull: true, type: DataType.INTEGER, allowNull: false,
references: {model: "dimension_terms", key: "id"}, references: {model: "dimension_terms", key: "id"},
onUpdate: "cascade", onDelete: "cascade", onUpdate: "cascade", onDelete: "cascade",
}, },

View File

@ -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"));
}
}

View File

@ -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<TermsSignedRecord> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@AllowNull
@Column
@ForeignKey(() => TermsRecord)
termsId?: number;
@AllowNull
@Column
@ForeignKey(() => User)
userId?: string;
}