Skip to content

Commit

Permalink
feat: more global access management
Browse files Browse the repository at this point in the history
this feature allows setting two new config values: moderators and
allowGlobalEdits. moderators can basically do anything in the plugin
while allowGlobalEdits allows users to edit each others questions and
answers.

closes #81
closes #75
  • Loading branch information
drodil committed Aug 2, 2023
1 parent d6561ef commit f078b34
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 160 deletions.
5 changes: 4 additions & 1 deletion app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ qeta:
# allowedFilesTypes: ['image/png', 'image/jpg', 'image/jpeg', 'image/gif']
allowAnonymous: true
allowMetadataInput: false
#allowGlobalEdits: true
entities:
max: 2
tags:
Expand All @@ -54,7 +55,9 @@ qeta:
#- test
#- another_tag
max: 3
entityKinds:
# moderators:
# - 'user:default/guest'
entityKinds:
- Component
- Resource
- Group
Expand Down
6 changes: 6 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The following configuration options are available for your app-config.yaml:
qeta:
allowAnonymous: true
allowMetadataInput: false
allowGlobalEdits: false
entities:
kinds: ['Component']
max: 3
Expand All @@ -15,6 +16,9 @@ qeta:
- test
- tag2
max: 5
moderators:
- group:default/my-group
- user:default/my-user
storage:
disabled: true
type: database
Expand All @@ -25,11 +29,13 @@ The configuration values are:
- allowAnonymous, boolean, allows anonymous users to post questions and answers. If enabled all users without authentication will be named after guest user. Required for local development.
- allowMetadataInput, boolean, allows `created` and `user` fields to be passed when creating questions, answers, or comments. Useful if migrating information into the system. Default is `false`
- allowGlobalEdits, boolean, allows editing other users' questions and answers by any user. Default is `false`.
- entities.kinds, string array, what kind of catalog entities can be attached to a question. Default is ['Component']
- entities.max, integer, maximum number of entities to attach to a question. Default is `3`
- tags.allowCreation, boolean, determines whether it's possible to add new tags when creating question. Default is `true`
- tags.allowedTags, string array, list of allowed tags to be attached to questions. Only used if `tags.allowCreation` is set to `false`.
- tags.max, integer, maximum number of tags to be attached to a question. Default is `5`.
- moderators, string array, list of moderator groups or users who can edit, delete questions/answers/comments and mark answers correct for any question
- storage.type, string, what kind of storage is used to upload images used in questions. Default is `database`
- storage.maxSizeImage, number, the maximum allowed size of upload files in bytes. Default is `2500000`
- storage.folder, string, what folder is used to storage temporarily images to convert and send to frontend. Default is `/tmp/backstage-qeta-images`
Expand Down
2 changes: 2 additions & 0 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ The default permissions are hard-coded. These include:

- Only author can edit and delete own questions
- Only author can edit and delete own answers
- Only author can delete their own comments
- User cannot vote their own questions or answers
- Only question author can mark answer as correct
- Except for moderators, who can do these for any questions/answer/comment

## Set up

Expand Down
12 changes: 12 additions & 0 deletions plugins/qeta-backend/configSchema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export interface Config {
* @visibility backend
*/
allowMetadataInput?: boolean;
/**
* Allow all users to edit other users questions and answers
*
* @visibility backend
*/
allowGlobalEdits?: boolean;
/**
* Entities configuration for questions.
*
Expand Down Expand Up @@ -51,6 +57,12 @@ export interface Config {
*/
max?: number;
};
/**
* List of users/groups that can moderate questions and answers
*
* @visibility backend
*/
moderators: string[];
/**
* Configuration about images attachments storage
*
Expand Down
120 changes: 85 additions & 35 deletions plugins/qeta-backend/src/database/DatabaseQetaStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import {
TagResponse,
} from './QetaStore';
import {
Answer,
Attachment,
Question,
Statistic,
StatisticsRequestParameters,
Vote,
Question,
Answer,
Attachment,
} from '@drodil/backstage-plugin-qeta-common';

const migrationsDir = resolvePackagePath(
Expand Down Expand Up @@ -335,12 +335,15 @@ export class DatabaseQetaStore implements QetaStore {
question_id: number,
id: number,
user_ref: string,
moderator?: boolean,
): Promise<MaybeQuestion> {
await this.db('question_comments')
const query = this.db('question_comments')
.where('id', '=', id)
.where('author', '=', user_ref)
.where('questionId', '=', question_id)
.delete();
.where('questionId', '=', question_id);
if (!moderator) {
query.where('author', '=', user_ref);
}
await query.delete();
return this.getQuestion(user_ref, question_id, false);
}

Expand All @@ -365,12 +368,16 @@ export class DatabaseQetaStore implements QetaStore {
answer_id: number,
id: number,
user_ref: string,
moderator?: boolean,
): Promise<MaybeAnswer> {
await this.db('answer_comments')
const query = this.db('answer_comments')
.where('id', '=', id)
.where('author', '=', user_ref)
.where('answerId', '=', answer_id)
.delete();
.where('answerId', '=', answer_id);

if (!moderator) {
query.where('author', '=', user_ref);
}
await query.delete();
return this.getAnswer(answer_id);
}

Expand All @@ -382,11 +389,18 @@ export class DatabaseQetaStore implements QetaStore {
tags?: string[],
entities?: string[],
images?: number[],
moderator?: boolean,
): Promise<MaybeQuestion> {
const rows = await this.db('questions')
.where('questions.id', '=', id)
.where('questions.author', '=', user_ref)
.update({ title, content, updatedBy: user_ref, updated: new Date() });
const query = this.db('questions').where('questions.id', '=', id);
if (!moderator) {
query.where('questions.author', '=', user_ref);
}
const rows = await query.update({
title,
content,
updatedBy: user_ref,
updated: new Date(),
});

if (!rows) {
return null;
Expand All @@ -406,11 +420,16 @@ export class DatabaseQetaStore implements QetaStore {
return await this.getQuestion(user_ref, id, false);
}

async deleteQuestion(user_ref: string, id: number): Promise<boolean> {
return !!(await this.db('questions')
.where('id', '=', id)
.where('author', '=', user_ref)
.delete());
async deleteQuestion(
user_ref: string,
id: number,
moderator?: boolean,
): Promise<boolean> {
const query = this.db('questions').where('id', '=', id);
if (!moderator) {
query.where('author', '=', user_ref);
}
return !!(await query.delete());
}

async answerQuestion(
Expand Down Expand Up @@ -446,12 +465,20 @@ export class DatabaseQetaStore implements QetaStore {
answerId: number,
answer: string,
images?: number[],
moderator?: boolean,
): Promise<MaybeAnswer> {
const rows = await this.db('answers')
const query = this.db('answers')
.where('answers.id', '=', answerId)
.where('answers.questionId', '=', questionId)
.where('answers.author', '=', user_ref)
.update({ content: answer, updatedBy: user_ref, updated: new Date() });
.where('answers.questionId', '=', questionId);
if (!moderator) {
query.where('answers.author', '=', user_ref);
}

const rows = await query.update({
content: answer,
updatedBy: user_ref,
updated: new Date(),
});

if (!rows) {
return null;
Expand All @@ -471,11 +498,17 @@ export class DatabaseQetaStore implements QetaStore {
return this.mapAnswer(answers[0], true, true);
}

async deleteAnswer(user_ref: string, id: number): Promise<boolean> {
return !!(await this.db('answers')
.where('id', '=', id)
.where('author', '=', user_ref)
.delete());
async deleteAnswer(
user_ref: string,
id: number,
moderator?: boolean,
): Promise<boolean> {
const query = this.db('answers').where('id', '=', id);
if (!moderator) {
query.where('author', '=', user_ref);
}

return !!(await query.delete());
}

async voteQuestion(
Expand Down Expand Up @@ -562,16 +595,30 @@ export class DatabaseQetaStore implements QetaStore {
user_ref: string,
questionId: number,
answerId: number,
moderator?: boolean,
): Promise<boolean> {
return await this.markAnswer(user_ref, questionId, answerId, true);
return await this.markAnswer(
user_ref,
questionId,
answerId,
true,
moderator,
);
}

async markAnswerIncorrect(
user_ref: string,
questionId: number,
answerId: number,
moderator?: boolean,
): Promise<boolean> {
return await this.markAnswer(user_ref, questionId, answerId, false);
return await this.markAnswer(
user_ref,
questionId,
answerId,
false,
moderator,
);
}

async getTags(): Promise<TagResponse[]> {
Expand Down Expand Up @@ -1120,6 +1167,7 @@ export class DatabaseQetaStore implements QetaStore {
questionId: number,
answerId: number,
correct: boolean,
moderator?: boolean,
): Promise<boolean> {
// There can be only one correct answer
if (correct) {
Expand All @@ -1132,22 +1180,24 @@ export class DatabaseQetaStore implements QetaStore {
}
}

const ret = await this.db('answers')
.update({ correct }, ['id'])
const query = this.db('answers')
.onConflict()
.ignore()
.where('answers.id', '=', answerId)
.where('questionId', '=', questionId)
.where('questionId', '=', questionId);
if (!moderator) {
// Need to do with subquery as missing join functionality for update in knex.
// See: https://github.com/knex/knex/issues/2796
// eslint-disable-next-line
.whereIn('questionId', function () {
query.whereIn('questionId', function () {
this.from('questions')
.select('id')
.where('id', '=', questionId)
.where('author', '=', user_ref);
});
}

const ret = await query.update({ correct }, ['id']);
return ret !== undefined && ret?.length > 0;
}
}
Loading

0 comments on commit f078b34

Please sign in to comment.