mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-10-01 01:05:53 -04:00
Early terms management UI
This commit is contained in:
parent
d9637b1d3d
commit
a11e57db31
42
src/api/admin/AdminTermsService.ts
Normal file
42
src/api/admin/AdminTermsService.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { GET, Path, PathParam, POST, Security } from "typescript-rest";
|
||||
import TermsController, { ITerms } from "../controllers/TermsController";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
|
||||
|
||||
interface CreatePolicyObject {
|
||||
name: string;
|
||||
text?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Administrative API for configuring terms of service.
|
||||
*/
|
||||
@Path("/api/v1/dimension/admin/terms")
|
||||
@AutoWired
|
||||
export class AdminTermsService {
|
||||
|
||||
@Inject
|
||||
private termsController: TermsController;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
@Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
|
||||
public async getPolicies(): Promise<ITerms[]> {
|
||||
return this.termsController.getPoliciesForAdmin();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(":shortcode/:version")
|
||||
@Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
|
||||
public async getPolicy(@PathParam("shortcode") shortcode: string, @PathParam("version") version: string): Promise<ITerms> {
|
||||
return this.termsController.getPolicyForAdmin(shortcode, version);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path(":shortcode/draft")
|
||||
@Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
|
||||
public async createDraftPolicy(@PathParam("shortcode") shortcode: string, request: CreatePolicyObject): Promise<ITerms> {
|
||||
return this.termsController.createDraftPolicy(request.name, shortcode, request.text, request.url);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { AutoWired } from "typescript-ioc/es6";
|
||||
import { IMSCUser } from "../security/MSCSecurity";
|
||||
import TermsRecord from "../../db/models/TermsRecord";
|
||||
import TermsTextRecord from "../../db/models/TermsTextRecord";
|
||||
|
||||
export interface ILanguagePolicy {
|
||||
name: string;
|
||||
@ -17,6 +19,20 @@ export interface ITermsNotSignedResponse {
|
||||
policies: { [policyName: string]: IPolicy };
|
||||
}
|
||||
|
||||
export interface ITerms {
|
||||
shortcode: string;
|
||||
version: string;
|
||||
languages: {
|
||||
[lang: string]: {
|
||||
name: string;
|
||||
url: string;
|
||||
text?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const VERSION_DRAFT = "draft";
|
||||
|
||||
/**
|
||||
* API controller for terms of service management
|
||||
*/
|
||||
@ -30,6 +46,39 @@ export default class TermsController {
|
||||
}
|
||||
|
||||
public async getMissingTermsForUser(_user: IMSCUser): Promise<ITermsNotSignedResponse> {
|
||||
// TODO: Abuse a cache for non-draft policies
|
||||
return {policies: {}};
|
||||
}
|
||||
|
||||
public async getPoliciesForAdmin(): Promise<ITerms[]> {
|
||||
const terms = await TermsRecord.findAll({include: [TermsTextRecord]});
|
||||
return terms.map(this.mapPolicy.bind(this, false));
|
||||
}
|
||||
|
||||
public async getPolicyForAdmin(shortcode: string, version: string): Promise<ITerms> {
|
||||
const terms = await TermsRecord.findOne({where: {shortcode, version}, include: [TermsTextRecord]});
|
||||
return this.mapPolicy(true, terms);
|
||||
}
|
||||
|
||||
public async createDraftPolicy(name: string, shortcode: string, text: string, url: string): Promise<ITerms> {
|
||||
const terms = await TermsRecord.create({shortcode, version: VERSION_DRAFT});
|
||||
const termsText = await TermsTextRecord.create({termsId: terms.id, language: "en", name, text, url});
|
||||
|
||||
terms.texts = [termsText];
|
||||
return this.mapPolicy(true, terms);
|
||||
}
|
||||
|
||||
private mapPolicy(withText: boolean, policy: TermsRecord): ITerms {
|
||||
const languages = {};
|
||||
policy.texts.forEach(pt => languages[pt.language] = {
|
||||
name: pt.name,
|
||||
url: pt.url,
|
||||
text: withText ? pt.text : null,
|
||||
});
|
||||
return {
|
||||
shortcode: policy.shortcode,
|
||||
version: policy.version,
|
||||
languages: languages,
|
||||
};
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import { ApiError } from "../ApiError";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import TermsController from "../controllers/TermsController";
|
||||
import config from "../../config";
|
||||
|
||||
export interface IMSCUser {
|
||||
userId: string;
|
||||
@ -11,6 +12,7 @@ export interface IMSCUser {
|
||||
}
|
||||
|
||||
export const ROLE_MSC_USER = "ROLE_MSC_USER";
|
||||
export const ROLE_MSC_ADMIN = "ROLE_MSC_ADMIN";
|
||||
|
||||
const TERMS_IGNORED_ROUTES = [
|
||||
{method: "GET", path: "/_matrix/integrations/v1/terms"},
|
||||
@ -25,7 +27,13 @@ export default class MSCSecurity implements ServiceAuthenticator {
|
||||
private termsController = new TermsController();
|
||||
|
||||
public getRoles(req: Request): string[] {
|
||||
if (req.user) return [ROLE_MSC_USER];
|
||||
if (req.user) {
|
||||
const roles = [ROLE_MSC_USER];
|
||||
if (config.admins.includes(req.user.userId)) {
|
||||
roles.push(ROLE_MSC_ADMIN);
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,8 @@ import WebhookBridgeRecord from "./models/WebhookBridgeRecord";
|
||||
import GitterBridgeRecord from "./models/GitterBridgeRecord";
|
||||
import CustomSimpleBotRecord from "./models/CustomSimpleBotRecord";
|
||||
import SlackBridgeRecord from "./models/SlackBridgeRecord";
|
||||
import TermsRecord from "./models/TermsRecord";
|
||||
import TermsTextRecord from "./models/TermsTextRecord";
|
||||
|
||||
class _DimensionStore {
|
||||
private sequelize: Sequelize;
|
||||
@ -63,6 +65,8 @@ class _DimensionStore {
|
||||
GitterBridgeRecord,
|
||||
CustomSimpleBotRecord,
|
||||
SlackBridgeRecord,
|
||||
TermsRecord,
|
||||
TermsTextRecord,
|
||||
]);
|
||||
}
|
||||
|
||||
|
31
src/db/migrations/20190630194345-AddTermsOfService.ts
Normal file
31
src/db/migrations/20190630194345-AddTermsOfService.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { QueryInterface } from "sequelize";
|
||||
import { DataType } from "sequelize-typescript";
|
||||
|
||||
export default {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.createTable("dimension_terms", {
|
||||
// Ideally we'd use a composite primary key here, but that's not really possible with our libraries.
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"shortcode": {type: DataType.STRING, allowNull: false},
|
||||
"version": {type: DataType.STRING, allowNull: false},
|
||||
}))
|
||||
.then(() => queryInterface.createTable("dimension_terms_text", {
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"termsId": {
|
||||
type: DataType.INTEGER, allowNull: true,
|
||||
references: {model: "dimension_terms", key: "id"},
|
||||
onUpdate: "cascade", onDelete: "cascade",
|
||||
},
|
||||
"language": {type: DataType.STRING, allowNull: false},
|
||||
"name": {type: DataType.STRING, allowNull: false},
|
||||
"text": {type: DataType.STRING, allowNull: true},
|
||||
"url": {type: DataType.STRING, allowNull: false},
|
||||
}));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.dropTable("dimension_terms"))
|
||||
.then(() => queryInterface.dropTable("dimension_terms_text"));
|
||||
}
|
||||
}
|
23
src/db/models/TermsRecord.ts
Normal file
23
src/db/models/TermsRecord.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { AutoIncrement, Column, HasMany, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||
import TermsTextRecord from "./TermsTextRecord";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_terms",
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class TermsRecord extends Model<TermsRecord> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
shortcode: string;
|
||||
|
||||
@Column
|
||||
version: string;
|
||||
|
||||
@HasMany(() => TermsTextRecord)
|
||||
texts: TermsTextRecord[];
|
||||
}
|
44
src/db/models/TermsTextRecord.ts
Normal file
44
src/db/models/TermsTextRecord.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
AllowNull,
|
||||
AutoIncrement,
|
||||
BelongsTo,
|
||||
Column,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table
|
||||
} from "sequelize-typescript";
|
||||
import TermsRecord from "./TermsRecord";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_terms_text",
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class TermsTextRecord extends Model<TermsTextRecord> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
@ForeignKey(() => TermsRecord)
|
||||
termsId?: number;
|
||||
|
||||
@BelongsTo(() => TermsRecord)
|
||||
terms: TermsRecord;
|
||||
|
||||
@Column
|
||||
language: string;
|
||||
|
||||
@Column
|
||||
url: string;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
text?: string;
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
<li (click)="goto('custom-bots')" [ngClass]="[isActive('custom-bots') ? 'active' : '']">Custom Bots</li>
|
||||
<li (click)="goto('bridges')" [ngClass]="[isActive('bridges') ? 'active' : '']">Bridges</li>
|
||||
<li (click)="goto('stickerpacks')" [ngClass]="[isActive('stickerpacks') ? 'active' : '']">Sticker Packs</li>
|
||||
<li (click)="goto('terms')" [ngClass]="[isActive('terms') ? 'active' : '']">Terms of Service</li>
|
||||
</ul>
|
||||
<span class="version">{{ version }}</span>
|
||||
|
||||
|
40
web/app/admin/terms/terms.component.html
Normal file
40
web/app/admin/terms/terms.component.html
Normal file
@ -0,0 +1,40 @@
|
||||
<div *ngIf="isLoading">
|
||||
<my-spinner></my-spinner>
|
||||
</div>
|
||||
<div *ngIf="!isLoading">
|
||||
<my-ibox boxTitle="Terms of Service">
|
||||
<div class="my-ibox-content">
|
||||
<p>
|
||||
Before users can use Dimension they must agree to the terms of service for using your
|
||||
instance. If you're using any matrix.org bridges, users will be required to accept
|
||||
the terms of service for your upstream integration managers (scalar.vector.im usually)
|
||||
in addition to the terms you add here.
|
||||
</p>
|
||||
|
||||
<table class="table table-striped table-condensed table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Policy Name</th>
|
||||
<th>Published Version</th>
|
||||
<th class="text-center" style="width: 120px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngIf="!policies || policies.length === 0">
|
||||
<td colspan="3"><i>No policies written.</i></td>
|
||||
</tr>
|
||||
<tr *ngFor="let policy of policies trackById">
|
||||
<td>{{ policy.languages['en'].name }}</td>
|
||||
<td>{{ policy.version }}</td>
|
||||
|
||||
<td class="text-center">
|
||||
<span class="previewButton" title="edit draft" *ngIf="policy.version == 'draft'">
|
||||
<i class="fa fa-pencil-alt"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</my-ibox>
|
||||
</div>
|
0
web/app/admin/terms/terms.component.scss
Normal file
0
web/app/admin/terms/terms.component.scss
Normal file
29
web/app/admin/terms/terms.component.ts
Normal file
29
web/app/admin/terms/terms.component.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { FE_TermsEditable } from "../../shared/models/terms";
|
||||
import { AdminTermsApiService } from "../../shared/services/admin/admin-terms-api.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./terms.component.html",
|
||||
styleUrls: ["./terms.component.scss"],
|
||||
})
|
||||
export class AdminTermsComponent implements OnInit {
|
||||
|
||||
public isLoading = true;
|
||||
public policies: FE_TermsEditable[];
|
||||
|
||||
constructor(private adminTerms: AdminTermsApiService,
|
||||
private toaster: ToasterService) {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.adminTerms.getAllPolicies().then(policies => {
|
||||
this.policies = policies;
|
||||
this.isLoading = false;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.toaster.pop("error", "Failed to load policies");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -112,6 +112,8 @@ import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.se
|
||||
import { AdminLogoutConfirmationDialogComponent } from "./admin/home/logout-confirmation/logout-confirmation.component";
|
||||
import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component";
|
||||
import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-test/manager-test.component";
|
||||
import { AdminTermsApiService } from "./shared/services/admin/admin-terms-api.service";
|
||||
import { AdminTermsComponent } from "./admin/terms/terms.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -204,6 +206,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes
|
||||
AdminLogoutConfirmationDialogComponent,
|
||||
ReauthExampleWidgetWrapperComponent,
|
||||
ManagerTestWidgetWrapperComponent,
|
||||
AdminTermsComponent,
|
||||
|
||||
// Vendor
|
||||
],
|
||||
@ -233,6 +236,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes
|
||||
SlackApiService,
|
||||
AdminSlackApiService,
|
||||
ToasterService,
|
||||
AdminTermsApiService,
|
||||
{provide: Window, useValue: window},
|
||||
|
||||
// Vendor
|
||||
|
@ -44,6 +44,7 @@ import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component
|
||||
import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge.component";
|
||||
import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component";
|
||||
import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-test/manager-test.component";
|
||||
import { AdminTermsComponent } from "./admin/terms/terms.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{path: "", component: HomeComponent},
|
||||
@ -145,7 +146,17 @@ const routes: Routes = [
|
||||
component: AdminStickerPacksComponent,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "terms",
|
||||
data: {breadcrumb: "Terms of Service", name: "Terms of Service"},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: AdminTermsComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
11
web/app/shared/models/terms.ts
Normal file
11
web/app/shared/models/terms.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface FE_TermsEditable {
|
||||
shortcode: string;
|
||||
version: string;
|
||||
languages: {
|
||||
[lang: string]: {
|
||||
name: string;
|
||||
url: string;
|
||||
text?: string;
|
||||
};
|
||||
};
|
||||
}
|
15
web/app/shared/services/admin/admin-terms-api.service.ts
Normal file
15
web/app/shared/services/admin/admin-terms-api.service.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { AuthedApi } from "../authed-api";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { FE_TermsEditable } from "../../models/terms";
|
||||
|
||||
@Injectable()
|
||||
export class AdminTermsApiService extends AuthedApi {
|
||||
constructor(http: HttpClient) {
|
||||
super(http, true);
|
||||
}
|
||||
|
||||
public getAllPolicies(): Promise<FE_TermsEditable[]> {
|
||||
return this.authedGet<FE_TermsEditable[]>("/api/v1/dimension/admin/terms/all").toPromise();
|
||||
}
|
||||
}
|
@ -3,24 +3,34 @@ import { SessionStorage } from "../SessionStorage";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
|
||||
export class AuthedApi {
|
||||
constructor(protected http: HttpClient) {
|
||||
constructor(protected http: HttpClient, private mscAuth = false) {
|
||||
}
|
||||
|
||||
protected authedGet<T>(url: string, qs?: any): Observable<T> {
|
||||
if (!qs) qs = {};
|
||||
qs["scalar_token"] = SessionStorage.scalarToken;
|
||||
return this.http.get<T>(url, {params: qs});
|
||||
const opts = this.fillAuthOptions(null, qs, null);
|
||||
return this.http.get<T>(url, opts);
|
||||
}
|
||||
|
||||
protected authedPost<T>(url: string, body?: any): Observable<T> {
|
||||
if (!body) body = {};
|
||||
const qs = {scalar_token: SessionStorage.scalarToken};
|
||||
return this.http.post<T>(url, body, {params: qs});
|
||||
const opts = this.fillAuthOptions(null, null, null);
|
||||
return this.http.post<T>(url, body, opts);
|
||||
}
|
||||
|
||||
protected authedDelete<T>(url: string, qs?: any): Observable<T> {
|
||||
const opts = this.fillAuthOptions(null, qs, null);
|
||||
return this.http.delete<T>(url, opts);
|
||||
}
|
||||
|
||||
private fillAuthOptions(opts: any, qs: any, headers: any): { headers: any, params: any } {
|
||||
if (!opts) opts = {};
|
||||
if (!qs) qs = {};
|
||||
qs["scalar_token"] = SessionStorage.scalarToken;
|
||||
return this.http.delete<T>(url, {params: qs});
|
||||
if (!headers) headers = {};
|
||||
if (this.mscAuth) {
|
||||
headers["Authorization"] = `Bearer ${SessionStorage.scalarToken}`;
|
||||
} else {
|
||||
qs["scalar_token"] = SessionStorage.scalarToken;
|
||||
}
|
||||
return Object.assign({}, opts, {params: qs, headers: headers});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user