Add Blacklists

- Add BlacklistedPhrase model to store blacklisted phrases
- Add checking for blacklisted phrases inside of posts content, usernames, groupnames, eventnames
- Add api to create, delete phrases and check if phrases contain blacklisted phrases
pull/4/head
trivernis 5 years ago
parent 81b0aa9657
commit f58dc4a33c

@ -4,7 +4,8 @@ import * as yaml from "js-yaml";
import {Op} from "sequelize"; import {Op} from "sequelize";
import isEmail from "validator/lib/isEmail"; import isEmail from "validator/lib/isEmail";
import dataaccess from "../lib/dataAccess"; import dataaccess from "../lib/dataAccess";
import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; import {BlacklistedError} from "../lib/errors/BlacklistedError";
import {NotAnAdminGqlError, NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
import {InvalidLoginError} from "../lib/errors/InvalidLoginError"; import {InvalidLoginError} from "../lib/errors/InvalidLoginError";
import globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
@ -113,6 +114,16 @@ export function resolver(req: any, res: any): any {
return new GraphQLError("No requestId given."); return new GraphQLError("No requestId given.");
} }
}, },
async blacklisted({phrase}: {phrase: string}) {
const phrases = await dataaccess.checkBlacklisted(phrase);
return {
blacklisted: phrases.length > 0,
phrases: phrases.map((p) => p.phrase),
};
},
async getBlacklistedPhrases({first, offset}: {first: number, offset: number}) {
return (await models.BlacklistedPhrase.findAll({limit: first, offset})).map((p) => p.phrase);
},
acceptCookies() { acceptCookies() {
req.session.cookiesAccepted = true; req.session.cookiesAccepted = true;
return true; return true;
@ -476,6 +487,11 @@ export function resolver(req: any, res: any): any {
const date = new Date(Number(dueDate)); const date = new Date(Number(dueDate));
const group = await models.Group.findByPk(groupId, {include: [{association: "rAdmins"}]}); const group = await models.Group.findByPk(groupId, {include: [{association: "rAdmins"}]});
if (group.rAdmins.find((x) => x.id === req.session.userId)) { if (group.rAdmins.find((x) => x.id === req.session.userId)) {
const blacklisted = await dataaccess.checkBlacklisted(name);
if (blacklisted.length > 0) {
res.status(status.BAD_REQUEST);
return new BlacklistedError(blacklisted.map((p) => p.phrase), "event name").graphqlError;
}
return group.$create<models.Event>("rEvent", {name, dueDate: date}); return group.$create<models.Event>("rEvent", {name, dueDate: date});
} else { } else {
res.status(status.FORBIDDEN); res.status(status.FORBIDDEN);
@ -524,7 +540,45 @@ export function resolver(req: any, res: any): any {
} }
} else { } else {
res.status(status.FORBIDDEN); res.status(status.FORBIDDEN);
return new GraphQLError("You are not an admin."); return new NotAnAdminGqlError();
}
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async addToBlacklist({phrase, languageCode}: {phrase: string, languageCode: string}) {
if (req.session.userId) {
const user = await models.User.findByPk(req.session.userId);
if (user.isAdmin) {
const phraseExists = await models.BlacklistedPhrase.findOne(
{where: {phrase, language: languageCode}});
if (!phraseExists) {
await models.BlacklistedPhrase.create({phrase, language: languageCode});
return true;
} else {
return false;
}
} else {
return new NotAnAdminGqlError();
}
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async removeFromBlacklist({phrase, languageCode}: {phrase: string, languageCode: string}) {
if (req.session.userId) {
const user = await models.User.findByPk(req.session.userId);
if (user.isAdmin) {
const phraseEntry = await models.BlacklistedPhrase.findOne(
{where: {phrase, language: languageCode}});
if (phraseEntry) {
await phraseEntry.destroy();
return true;
} else {
return false;
}
} }
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);

@ -37,6 +37,12 @@ type Query {
"Returns an access token for the user that can be used in requests. To user the token in requests, it has to be set in the HTTP header 'Authorization' with the format Bearer <token>." "Returns an access token for the user that can be used in requests. To user the token in requests, it has to be set in the HTTP header 'Authorization' with the format Bearer <token>."
getToken(email: String!, passwordHash: String!): Token! getToken(email: String!, passwordHash: String!): Token!
"Checks if the input phrase contains blacklisted words"
blacklisted(phrase: String!): BlacklistedResult!
"Returns the blacklist with pagination."
getBlacklistedPhrases(first: Int = 20, offset: Int = 0): [String!]! @complexity(value: 1, multipliers: ["first"])
} }
type Mutation { type Mutation {
@ -111,6 +117,12 @@ type Mutation {
"Creates an activity. Can only be used by admins." "Creates an activity. Can only be used by admins."
createActivity(name: String!, description: String!, points: Int!): Activity createActivity(name: String!, description: String!, points: Int!): Activity
"Adds a phrase to the blacklist. Returns true if the phrase didn't exist and was inserted."
addToBlacklist(phrase: String!, languageCode: String = "en"): Boolean!
"Removes a phrase from the blacklist. Returns true if the phrase could be found and deleted."
removeFromBlacklist(phrase: String!, languageCode: String = "en"): Boolean!
} }
interface UserData { interface UserData {
@ -431,6 +443,22 @@ type Token {
expires: String! expires: String!
} }
"An activity that grants points"
type Activity {
"the id of the activity"
id: ID!
"the name of the activity"
name: String!
"the description of the activity"
description: String!
"the number of points the activity grants"
points: Int!
}
"The result of a search." "The result of a search."
type SearchResult { type SearchResult {
"The users that were found in the search." "The users that were found in the search."
@ -456,21 +484,18 @@ type VoteResult {
post: Post! post: Post!
} }
"An activity that grants points" "The result of checking if a phrase is blacklisted"
type Activity { type BlacklistedResult {
"the id of the activity" "If the phrase contains blacklisted words."
id: ID! blacklisted: Boolean!
"The specific blacklisted phrase."
phrases: [String!]!
}
"the name of the activity"
name: String!
"the description of the activity"
description: String!
"the number of points the activity grants"
points: Int!
}
"represents the type of vote performed on a post" "represents the type of vote performed on a post"
enum VoteType { enum VoteType {

@ -3,6 +3,7 @@ import {GraphQLError} from "graphql";
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {Sequelize} from "sequelize-typescript"; import {Sequelize} from "sequelize-typescript";
import {ActivityNotFoundError} from "./errors/ActivityNotFoundError"; import {ActivityNotFoundError} from "./errors/ActivityNotFoundError";
import {BlacklistedError} from "./errors/BlacklistedError";
import {ChatNotFoundError} from "./errors/ChatNotFoundError"; import {ChatNotFoundError} from "./errors/ChatNotFoundError";
import {DuplicatedRequestError} from "./errors/DuplicatedRequestError"; import {DuplicatedRequestError} from "./errors/DuplicatedRequestError";
import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError"; import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError";
@ -15,7 +16,7 @@ import {UserNotFoundError} from "./errors/UserNotFoundError";
import globals from "./globals"; import globals from "./globals";
import {InternalEvents} from "./InternalEvents"; import {InternalEvents} from "./InternalEvents";
import * as models from "./models"; import * as models from "./models";
import {Activity} from "./models"; import {Activity, BlacklistedPhrase} from "./models";
// tslint:disable:completed-docs // tslint:disable:completed-docs
@ -62,6 +63,7 @@ namespace dataaccess {
models.EventParticipant, models.EventParticipant,
models.Event, models.Event,
models.Activity, models.Activity,
models.BlacklistedPhrase,
]); ]);
} catch (err) { } catch (err) {
globals.logger.error(err.message); globals.logger.error(err.message);
@ -118,6 +120,10 @@ namespace dataaccess {
* @param password * @param password
*/ */
export async function registerUser(username: string, email: string, password: string): Promise<models.User> { export async function registerUser(username: string, email: string, password: string): Promise<models.User> {
const blacklisted = await checkBlacklisted(username);
if (blacklisted.length > 0) {
throw new BlacklistedError(blacklisted.map((p) => p.phrase), "username");
}
const hash = crypto.createHash("sha512"); const hash = crypto.createHash("sha512");
hash.update(password); hash.update(password);
password = hash.digest("hex"); password = hash.digest("hex");
@ -176,6 +182,10 @@ namespace dataaccess {
* @param activityId * @param activityId
*/ */
export async function createPost(content: string, authorId: number, activityId?: number): Promise<models.Post> { export async function createPost(content: string, authorId: number, activityId?: number): Promise<models.Post> {
const blacklisted = await checkBlacklisted(content);
if (blacklisted.length > 0) {
throw new BlacklistedError(blacklisted.map((p) => p.phrase), "content");
}
const activity = await models.Activity.findByPk(activityId); const activity = await models.Activity.findByPk(activityId);
if (!activityId || activity) { if (!activityId || activity) {
const post = await models.Post.create({content, authorId, activityId}); const post = await models.Post.create({content, authorId, activityId});
@ -284,6 +294,10 @@ namespace dataaccess {
* @param members * @param members
*/ */
export async function createGroup(name: string, creator: number, members: number[]): Promise<models.Group> { export async function createGroup(name: string, creator: number, members: number[]): Promise<models.Group> {
const blacklisted = await checkBlacklisted(name);
if (blacklisted.length > 0) {
throw new BlacklistedError(blacklisted.map((p) => p.phrase), "group name");
}
const groupNameExists = !!await models.Group.findOne({where: {name}}); const groupNameExists = !!await models.Group.findOne({where: {name}});
if (!groupNameExists) { if (!groupNameExists) {
members = members || []; members = members || [];
@ -337,6 +351,17 @@ namespace dataaccess {
} }
} }
/**
* Checks if a given phrase is blacklisted.
* @param phrase
* @param language
*/
export async function checkBlacklisted(phrase: string, language: string = "en"): Promise<models.BlacklistedPhrase[]> {
return sequelize.query<BlacklistedPhrase>(`
SELECT * FROM blacklisted_phrases WHERE ? ~* phrase AND language = ?`,
{replacements: [phrase, language], mapToModel: true, model: BlacklistedPhrase});
}
/** /**
* Enum representing the types of votes that can be performed on a post. * Enum representing the types of votes that can be performed on a post.
*/ */

@ -0,0 +1,10 @@
import {BaseError} from "./BaseError";
/**
* Represents an error that is thrown when a blacklisted phrase is used.
*/
export class BlacklistedError extends BaseError {
constructor(public phrases: string[], field: string = "input") {
super(`The ${field} contains the blacklisted words: ${phrases.join(",")}`);
}
}

@ -19,10 +19,19 @@ export class PostNotFoundGqlError extends GraphQLError {
} }
/** /**
* An error for the forntend that is thrown when a group was not found * An error for the frontend that is thrown when a group was not found
*/ */
export class GroupNotFoundGqlError extends GraphQLError { export class GroupNotFoundGqlError extends GraphQLError {
constructor(groupId: number) { constructor(groupId: number) {
super(`Group '${groupId}' not found!`); super(`Group '${groupId}' not found!`);
} }
} }
/**
* An error for the frontend that is thrown when a nonadmin tries to perform an admin operation.
*/
export class NotAnAdminGqlError extends GraphQLError {
constructor() {
super("You are not an admin.");
}
}

@ -0,0 +1,33 @@
import * as sqz from "sequelize";
import {
BelongsTo,
BelongsToMany,
Column,
ForeignKey,
HasMany,
Model,
NotNull,
Table,
Unique,
} from "sequelize-typescript";
/**
* Represents a blacklisted phrase
*/
@Table({underscored: true})
export class BlacklistedPhrase extends Model {
/**
* The phrase that is blacklisted
*/
@NotNull
@Unique
@Column({allowNull: false, unique: true})
public phrase: string;
/**
* An optional language
*/
@Column({type: sqz.STRING(2), defaultValue: "en"})
public language: string;
}

@ -80,7 +80,7 @@ export class User extends Model<User> {
* The auth token for bearer authentication * The auth token for bearer authentication
*/ */
@Unique @Unique
@Column({defaultValue: uuidv4, unique: true}) @Column({defaultValue: uuidv4, unique: true, type: sqz.UUIDV4})
public authToken: string; public authToken: string;
/** /**

@ -12,3 +12,4 @@ export {GroupMember} from "./GroupMember";
export {Event} from "./Event"; export {Event} from "./Event";
export {EventParticipant} from "./EventParticipant"; export {EventParticipant} from "./EventParticipant";
export {Activity} from "./Activity"; export {Activity} from "./Activity";
export {BlacklistedPhrase} from "./BlacklistedPhrase";

Loading…
Cancel
Save