Add all resolvers and help types to classes

- Add resolvers to Mutation class
- Add reslovers to Query class
- Add helper classes for several types and errors
pull/4/head
trivernis 5 years ago
parent 164ecb77c6
commit 57091e522c

@ -0,0 +1,17 @@
import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors";
/**
* Base resolver class to provide common methods to all resolver classes
*/
export abstract class BaseResolver {
/**
* Checks if the user is logged in and throws an exception if not
* @param request
*/
protected ensureLoggedIn(request: any) {
if (!request.session.userId) {
throw new NotLoggedInGqlError();
}
}
}

@ -0,0 +1,6 @@
export class BlacklistedResult {
constructor(
public blacklisted: boolean,
public phrases: string[],
) {}
}

@ -1,6 +1,467 @@
import {GraphQLError} from "graphql";
import * as yaml from "js-yaml";
import isEmail from "validator/lib/isEmail";
import dataaccess from "../lib/dataAccess";
import {BlacklistedError} from "../lib/errors/BlacklistedError";
import {GroupNotFoundError} from "../lib/errors/GroupNotFoundError";
import {InvalidEmailError} from "../lib/errors/InvalidEmailError";
import {NotAGroupAdminError} from "../lib/errors/NotAGroupAdminError";
import {NotAnAdminError} from "../lib/errors/NotAnAdminError";
import {NotTheGroupCreatorError} from "../lib/errors/NotTheGroupCreatorError";
import {PostNotFoundError} from "../lib/errors/PostNotFoundError";
import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents";
import {Activity, BlacklistedPhrase, ChatMessage, ChatRoom, Event, Group, Post, Request, User} from "../lib/models";
import {BaseResolver} from "./BaseResolver";
const legit = require("legit");
/**
* A class that provides methods to resolve mutations
*/
export class MutationResolver {
export class MutationResolver extends BaseResolver {
/**
* Accepts the usage of cookies and stores the session
* @param args
* @param request
*/
public acceptCookies(args: null, request: any): boolean {
request.session.cookiesAccepted = true;
return true;
}
/**
* Loggs in and appends the user id to the session
* @param email
* @param passwordHash
* @param request
*/
public async login({email, passwordHash}: { email: string, passwordHash: string }, request: any): Promise<User> {
const user = await dataaccess.getUserByLogin(email, passwordHash);
request.session.userId = user.id;
return user;
}
/**
* Loggs out by removing the user from the session
* @param args
* @param request
*/
public logout(args: null, request: any) {
this.ensureLoggedIn(request);
delete request.session.userId;
request.session.save((err: any) => {
if (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
}
});
}
/**
* Registers a new user account
* @param username
* @param email
* @param passwordHash
* @param request
*/
public async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string },
request: any): Promise<User> {
let mailValid = isEmail(email);
if (mailValid) {
try {
mailValid = (await legit(email)).isValid;
} catch (err) {
globals.logger.warn(`Mail legit check returned: ${err.message}`);
globals.logger.debug(err.stack);
mailValid = false;
}
}
if (!mailValid) {
throw new InvalidEmailError(email);
}
const user = await dataaccess.registerUser(username, email, passwordHash);
request.session.userId = user.id;
return user;
}
/**
* Sets the frontend settings for the logged in user
* @param settings
* @param request
*/
public async setUserSettings({settings}: { settings: string }, request: any): Promise<string> {
this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId);
try {
user.frontendSettings = yaml.safeLoad(settings);
await user.save();
return user.settings;
} catch (err) {
throw new GraphQLError("Invalid settings json.");
}
}
/**
* Toggles a vote of a specific type on a post and returns the post and the result
* @param postId
* @param type
* @param request
*/
public async vote({postId, type}: { postId: number, type: dataaccess.VoteType }, request: any):
Promise<{ post: Post, voteType: dataaccess.VoteType }> {
this.ensureLoggedIn(request);
const post = await Post.findByPk(postId);
if (post) {
const voteType = await post.vote(request.session.userId, type);
return {
post,
voteType,
};
} else {
throw new PostNotFoundError(postId);
}
}
/**
* Creates a new post
* @param content
* @param activityId
* @param request
*/
public async createPost({content, activityId}: { content: string, activityId?: number }, request: any):
Promise<Post> {
this.ensureLoggedIn(request);
if (content.length > 2048) {
throw new GraphQLError("Content too long.");
}
const post = await dataaccess.createPost(content, request.session.userId, activityId);
globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post);
return post;
}
/**
* Deletes a post if the user is either the author or a site admin.
* @param postId
* @param request
*/
public async deletePost({postId}: { postId: number }, request: any): Promise<boolean> {
this.ensureLoggedIn(request);
const post = await Post.findByPk(postId, {
include: [{
as: "rAuthor",
model: User,
}],
});
const isAdmin = (await User.findOne({where: {id: request.session.userId}})).isAdmin;
if (post.rAuthor.id === request.session.userId || isAdmin) {
return await dataaccess.deletePost(post.id);
} else {
throw new GraphQLError("User is not author of the post.");
}
}
/**
* Creates a chat with several members
* @param members
* @param request
*/
public async createChat({members}: { members?: number[] }, request: any): Promise<ChatRoom> {
this.ensureLoggedIn(request);
const chatMembers = [request.session.userId];
if (members) {
chatMembers.push(...members);
}
return await dataaccess.createChat(...chatMembers);
}
/**
* Sends a message into a chat the user has joined
* @param chatId
* @param content
* @param request
*/
public async sendMessage({chatId, content}: { chatId: number, content: string }, request: any):
Promise<ChatMessage> {
this.ensureLoggedIn(request);
const message = await dataaccess.sendChatMessage(request.session.userId, chatId, content);
globals.internalEmitter.emit(InternalEvents.GQLCHATMESSAGE, message);
return message;
}
/**
* Sends a request to a specific user
* @param receiver
* @param type
* @param request
*/
public async sendRequest({receiver, type}: { receiver: number, type: dataaccess.RequestType }, request: any):
Promise<Request> {
this.ensureLoggedIn(request);
return dataaccess.createRequest(request.session.userId, receiver, type);
}
/**
* Denies a request
* @param sender
* @param type
* @param request
*/
public async denyRequest({sender, type}: { sender: number, type: dataaccess.RequestType }, request: any) {
this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId);
await user.acceptRequest(sender, type);
return true;
}
/**
* Accepts a request
* @param sender
* @param type
* @param request
*/
public async acceptRequest({sender, type}: { sender: number, type: dataaccess.RequestType }, request: any) {
this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId);
await user.acceptRequest(sender, type);
return true;
}
/**
* Removes a friend
* @param friendId
* @param request
*/
public async removeFriend({friendId}: { friendId: number }, request: any): Promise<boolean> {
this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId);
return user.removeFriend(friendId);
}
/**
* Creates a new group
* @param name
* @param members
* @param request
*/
public async createGroup({name, members}: { name: string, members: number[] }, request: any): Promise<Group> {
this.ensureLoggedIn(request);
return await dataaccess.createGroup(name, request.session.userId, members);
}
/**
* Deletes a group if the user is either the creator or a site admin
* @param groupId
* @param request
*/
public async deleteGroup({groupId}: { groupId: number }, request: any): Promise<boolean> {
this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId);
const group = await Group.findByPk(groupId);
if (group) {
if (user.isAdmin || group.creatorId === user.id) {
await group.destroy();
return true;
}
} else {
throw new GroupNotFoundError(groupId);
}
}
/**
* Joins a group
* @param groupId
* @param request
*/
public async joinGroup({groupId}: { groupId: number }, request: any): Promise<Group> {
this.ensureLoggedIn(request);
return dataaccess.changeGroupMembership(groupId, request.session.userId,
dataaccess.MembershipChangeAction.ADD);
}
/**
* Leaves a group
* @param groupId
* @param request
*/
public async leaveGroup({groupId}: { groupId: number }, request: any): Promise<Group> {
this.ensureLoggedIn(request);
return dataaccess.changeGroupMembership(groupId, request.session.userId,
dataaccess.MembershipChangeAction.REMOVE);
}
/**
* Adds a user to the group admins
* @param groupId
* @param userId
* @param request
*/
public async addGroupAdmin({groupId, userId}: { groupId: number, userId: number }, request: any): Promise<Group> {
this.ensureLoggedIn(request);
const group = await Group.findByPk(groupId);
const user: User = await User.findByPk(request.session.userId);
if (group && !(await group.$has("rAdmins", user)) && (await group.creator()) !== user.id) {
throw new NotAGroupAdminError(groupId);
}
return dataaccess.changeGroupMembership(groupId, userId,
dataaccess.MembershipChangeAction.OP);
}
/**
* Removes an admin from a group
* @param groupId
* @param userId
* @param request
*/
public async removeGroupAdmin({groupId, userId}: { groupId: number, userId: number },
request: any): Promise<Group> {
this.ensureLoggedIn(request);
const group = await Group.findByPk(groupId);
const isCreator = Number(group.creatorId) === Number(request.session.userId);
const userIsCreator = Number(group.creatorId) === Number(userId);
if (group && !isCreator && Number(userId) !== Number(request.session.userId)) {
throw new NotTheGroupCreatorError(groupId);
} else if (userIsCreator) {
throw new GraphQLError(
"You are not allowed to remove a creator as an admin.");
}
return await dataaccess.changeGroupMembership(groupId, userId,
dataaccess.MembershipChangeAction.DEOP);
}
/**
* Creates a new event for a specific group
* @param name
* @param dueDate
* @param groupId
* @param request
*/
public async createEvent({name, dueDate, groupId}: { name: string, dueDate: string, groupId: number },
request: any): Promise<Event> {
this.ensureLoggedIn(request);
const date = new Date(Number(dueDate));
const user: User = await User.findByPk(request.session.userId);
const group = await Group.findByPk(groupId, {include: [{association: "rAdmins"}]});
if (!(await group.$has("rAdmins", user))) {
throw new NotAGroupAdminError(groupId);
}
const blacklisted = await dataaccess.checkBlacklisted(name);
if (blacklisted.length > 0) {
throw new BlacklistedError(blacklisted.map((p) => p.phrase), "event name");
}
return group.$create<Event>("rEvent", {name, dueDate: date});
}
/**
* Deletes an event
* @param eventId
* @param request
*/
public async deleteEvent({eventId}: { eventId: number }, request: any): Promise<boolean> {
this.ensureLoggedIn(request);
const event = await Event.findByPk(eventId, {include: [Group]});
const user = await User.findByPk(request.session.userId);
const group = await event.group();
if (await group.$has("rAdmins", user)) {
await event.destroy();
return true;
} else {
throw new NotAGroupAdminError(group.id);
}
}
/**
* Joins an event
* @param eventId
* @param request
*/
public async joinEvent({eventId}: { eventId: number }, request: any): Promise<Event> {
this.ensureLoggedIn(request);
const event = await Event.findByPk(eventId);
const self = await User.findByPk(request.session.userId);
await event.$add("rParticipants", self);
return event;
}
/**
* Leaves an event
* @param eventId
* @param request
*/
public async leaveEvent({eventId}: { eventId: number }, request: any): Promise<Event> {
this.ensureLoggedIn(request);
const event = await Event.findByPk(eventId);
const self = await User.findByPk(request.session.userId);
await event.$remove("rParticipants", self);
return event;
}
/**
* Creates a new activity or throws an error if the activity already exists
* @param name
* @param description
* @param points
* @param request
*/
public async createActivity({name, description, points}: { name: string, description: string, points: number },
request: any): Promise<Activity> {
this.ensureLoggedIn(request.session.userId);
const user = await User.findByPk(request.session.userId);
if (!user.isAdmin) {
throw new NotAnAdminError();
}
const nameExists = await Activity.findOne({where: {name}});
if (!nameExists) {
return Activity.create({name, description, points});
} else {
throw new GraphQLError(`An activity with the name '${name}' already exists.`);
}
}
/**
* Adds a phrase to the blaclist
* @param phrase
* @param languageCode
* @param request
*/
public async addToBlacklist({phrase, languageCode}: {phrase: string, languageCode?: string}, request: any):
Promise<boolean> {
this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId);
if (!user.isAdmin) {
throw new NotAnAdminError();
}
const phraseExists = await BlacklistedPhrase.findOne(
{where: {phrase, language: languageCode}});
if (!phraseExists) {
await BlacklistedPhrase.create({phrase, language: languageCode});
return true;
} else {
return false;
}
}
/**
* Removes a phrase from the blacklist
* @param phrase
* @param languageCode
* @param request
*/
public async removeFromBlacklist({phrase, languageCode}: {phrase: string, languageCode: string}, request: any):
Promise<boolean> {
this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId);
if (!user.isAdmin) {
throw new NotAnAdminError();
}
const phraseEntry = await BlacklistedPhrase.findOne(
{where: {phrase, language: languageCode}});
if (phraseEntry) {
await phraseEntry.destroy();
return true;
} else {
return false;
}
}
}

@ -1,5 +1,196 @@
import {GraphQLError} from "graphql";
import {Op} from "sequelize";
import dataaccess from "../lib/dataAccess";
import {ChatNotFoundError} from "../lib/errors/ChatNotFoundError";
import {PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
import {GroupNotFoundError} from "../lib/errors/GroupNotFoundError";
import {InvalidLoginError} from "../lib/errors/InvalidLoginError";
import {RequestNotFoundError} from "../lib/errors/RequestNotFoundError";
import {UserNotFoundError} from "../lib/errors/UserNotFoundError";
import {
Activity,
BlacklistedPhrase,
ChatRoom,
Event,
Group,
Post,
Request,
User,
} from "../lib/models";
import {BaseResolver} from "./BaseResolver";
import {BlacklistedResult} from "./BlacklistedResult";
import {SearchResult} from "./SearchResult";
import {Token} from "./Token";
/**
* A class that provides functions to resolve queries
*/
export class QueryResolver {
export class QueryResolver extends BaseResolver {
/**
* Gets a user by id or handle
* @param userId
* @param handle
*/
public async getUser({userId, handle}: {userId?: number, handle?: string}): Promise<User> {
let user: User;
if (userId) {
user = await User.findByPk(userId);
} else if (handle) {
user = await User.findOne({where: {handle}});
} else {
throw new GraphQLError("No handle or userId provided");
}
if (user) {
return user;
} else {
throw new UserNotFoundError(userId ?? handle);
}
}
/**
* Returns the instance of the currently logged in user
* @param args
* @param request
*/
public async getSelf(args: null, request: any): Promise<User> {
this.ensureLoggedIn(request);
return User.findByPk(request.session.userId);
}
/**
* Returns a post for a given post id.
* @param postId
*/
public async getPost({postId}: {postId: number}): Promise<Post> {
const post = await Post.findByPk(postId);
if (post) {
return post;
} else {
throw new PostNotFoundGqlError(postId);
}
}
/**
* Returns a chat for a given chat id
* @param chatId
*/
public async getChat({chatId}: {chatId: number}): Promise<ChatRoom> {
const chat = await ChatRoom.findByPk(chatId);
if (chat) {
return chat;
} else {
throw new ChatNotFoundError(chatId);
}
}
/**
* Returns a group for a given group id.
* @param groupId
*/
public async getGroup({groupId}: {groupId: number}): Promise<Group> {
const group = await Group.findByPk(groupId);
if (group) {
return group;
} else {
throw new GroupNotFoundError(groupId);
}
}
/**
* Returns the request for a given id.
* @param requestId
*/
public async getRequest({requestId}: {requestId: number}): Promise<Request> {
const request = await Request.findByPk(requestId);
if (request) {
return request;
} else {
throw new RequestNotFoundError(requestId);
}
}
/**
* Searches for posts, groups, users, events and returns a search result.
* @param query
* @param first
* @param offset
*/
public async search({query, first, offset}: {query: number, first: number, offset: number}): Promise<SearchResult> {
const limit = first;
const users = await User.findAll({
limit,
offset,
where: {
[Op.or]: [
{handle: {[Op.iRegexp]: query}},
{username: {[Op.iRegexp]: query}},
],
},
});
const groups = await Group.findAll({
limit,
offset,
where: {name: {[Op.iRegexp]: query}},
});
const posts = await Post.findAll({
limit,
offset,
where: {content: {[Op.iRegexp]: query}},
});
const events = await Event.findAll({
limit,
offset,
where: {name: {[Op.iRegexp]: query}},
});
return new SearchResult(users, groups, posts, events);
}
/**
* Returns the posts with a specific sorting
* @param first
* @param offset
* @param sort
*/
public async getPosts({first, offset, sort}: {first: number, offset: number, sort: dataaccess.SortType}):
Promise<Post[]> {
return await dataaccess.getPosts(first, offset, sort);
}
/**
* Returns all activities
*/
public async getActivities(): Promise<Activity[]> {
return Activity.findAll();
}
/**
* Returns the token for a user by login
* @param email
* @param passwordHash
*/
public async getToken({email, passwordHash}: {email: string, passwordHash: string}): Promise<Token> {
const user = await dataaccess.getUserByLogin(email, passwordHash);
return new Token(await user.token(), Number(user.authExpire).toString());
}
/**
* Returns if a input phrase contains blacklisted phrases and which one
* @param phrase
*/
public async blacklisted({phrase}: {phrase: string}): Promise<BlacklistedResult> {
const phrases = await dataaccess.checkBlacklisted(phrase);
return new BlacklistedResult(phrases.length > 0, phrases
.map((p) => p.phrase));
}
/**
* Returns all blacklisted phrases with pagination
* @param first
* @param offset
*/
public async getBlacklistedPhrases({first, offset}: {first: number, offset: number}): Promise<string[]> {
return (await BlacklistedPhrase.findAll({limit: first, offset}))
.map((p) => p.phrase);
}
}

@ -0,0 +1,13 @@
import {Group, Post, User, Event} from "../lib/models";
/**
* A class to wrap search results returned by the search resolver
*/
export class SearchResult {
constructor(
public users: User[],
public groups: Group[],
public posts: Post[],
public events: Event[],
) {}
}

@ -0,0 +1,9 @@
/**
* A class representing a token that can be used with bearer authentication
*/
export class Token {
constructor(
public value: string,
public expires: string,
) {}
}

@ -205,7 +205,7 @@ namespace dataaccess {
* Deletes a post
* @param postId
*/
export async function deletePost(postId: number): Promise<boolean | GraphQLError> {
export async function deletePost(postId: number): Promise<boolean> {
try {
const post = await models.Post.findByPk(postId, {include: [{model: Activity}, {association: "rAuthor"}]});
const activity = await post.activity();

@ -0,0 +1,10 @@
import {BaseError} from "./BaseError";
/**
* An error that is thrown when a user tries to register with an invalid email
*/
export class InvalidEmailError extends BaseError {
constructor(email: string) {
super(`'${email}' is not a valid email address!`);
}
}

@ -0,0 +1,14 @@
import * as status from "http-status";
import {BaseError} from "./BaseError";
/**
* An error that is thrown when a non-admin tries to perform an admin action
*/
export class NotAGroupAdminError extends BaseError {
public readonly statusCode = status.FORBIDDEN;
constructor(groupId: number) {
super(`You are not an admin of '${groupId}'`);
}
}

@ -0,0 +1,13 @@
import {BaseError} from "./BaseError";
/**
* An error that is thrown when a non admin tries to perform an admin action
*/
export class NotAnAdminError extends BaseError {
public readonly statusCode = httpStatus.FORBIDDEN;
constructor() {
super("You are not a site admin!");
}
}

@ -0,0 +1,14 @@
import * as status from "http-status";
import {BaseError} from "./BaseError";
/**
* An error that is thrown when a non-admin tries to perform an admin action
*/
export class NotTheGroupCreatorError extends BaseError {
public readonly statusCode = status.FORBIDDEN;
constructor(groupId: number) {
super(`You are not the creator of '${groupId}'`);
}
}

@ -0,0 +1,10 @@
import {BaseError} from "./BaseError";
/**
* An error that is thrown when a post was not found
*/
export class PostNotFoundError extends BaseError {
constructor(postId: number) {
super(`Post '${postId}' not found!`);
}
}

@ -5,7 +5,11 @@ import {BaseError} from "./BaseError";
* An error that is thrown when a request for a sender, receiver and type was not found
*/
export class RequestNotFoundError extends BaseError {
constructor(sender: number, receiver: number, type: dataaccess.RequestType) {
super(`Request with sender '${sender}' and receiver '${receiver}' of type '${type}' not found.`);
constructor(sender: number, receiver?: number, type?: dataaccess.RequestType) {
if (!receiver) {
super(`Request with id '${sender} not found.'`);
} else {
super(`Request with sender '${sender}' and receiver '${receiver}' of type '${type}' not found.`);
}
}
}

Loading…
Cancel
Save