diff --git a/package.json b/package.json index ffd79fe..e575457 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ }, "dependencies": { "@types/body-parser": "^1.17.1", + "@types/lodash": "^4.14.149", "body-parser": "^1.19.0", "compression": "^1.7.4", "config": "^3.2.4", @@ -79,6 +80,7 @@ "http-status": "^1.3.2", "js-yaml": "^3.13.1", "legit": "^1.0.7", + "lodash": "^4.17.15", "markdown-it": "^10.0.0", "markdown-it-emoji": "^1.4.0", "markdown-it-html5-media": "^0.6.0", diff --git a/src/app.ts b/src/app.ts index c6b1c54..eeebd33 100644 --- a/src/app.ts +++ b/src/app.ts @@ -200,7 +200,11 @@ class App { formatError: (err: GraphQLError | any) => { if (err.statusCode) { response.status(err.statusCode); + } else { + response.status(400); } + logger.debug(err.message); + logger.silly(err.stack); return err.graphqlError ?? err; }, graphiql: config.get("api.graphiql"), diff --git a/src/graphql/QueryResolver.ts b/src/graphql/QueryResolver.ts index 91b94b6..04d7cd8 100644 --- a/src/graphql/QueryResolver.ts +++ b/src/graphql/QueryResolver.ts @@ -4,7 +4,6 @@ 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 { @@ -17,15 +16,15 @@ import { Request, User, } from "../lib/models"; -import {BaseResolver} from "./BaseResolver"; import {BlacklistedResult} from "./BlacklistedResult"; +import {MutationResolver} from "./MutationResolver"; import {SearchResult} from "./SearchResult"; import {Token} from "./Token"; /** * A class that provides functions to resolve queries */ -export class QueryResolver extends BaseResolver { +export class QueryResolver extends MutationResolver { /** * Gets a user by id or handle diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 04d4cb3..0199d29 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -1,21 +1,4 @@ -import {GraphQLError} from "graphql"; -import * as status from "http-status"; -import * as yaml from "js-yaml"; -import {Op} from "sequelize"; -import isEmail from "validator/lib/isEmail"; -import dataaccess from "../lib/dataAccess"; -import {BlacklistedError} from "../lib/errors/BlacklistedError"; -import {NotAnAdminGqlError, NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; -import {GroupNotFoundError} from "../lib/errors/GroupNotFoundError"; -import {InvalidLoginError} from "../lib/errors/InvalidLoginError"; -import globals from "../lib/globals"; -import {InternalEvents} from "../lib/InternalEvents"; -import * as models from "../lib/models"; -import {is} from "../lib/regex"; - -const legit = require("legit"); - -// tslint:disable:completed-docs +import {QueryResolver} from "./QueryResolver"; /** * Returns the resolvers for the graphql api. @@ -23,608 +6,5 @@ const legit = require("legit"); * @param res - the response object */ export function resolver(req: any, res: any): any { - return { - async search({first, offset, query}: { first: number, offset: number, query: string }) { - const limit = first; - const users = await models.User.findAll({ - limit, - offset, - where: { - [Op.or]: [ - {handle: {[Op.iRegexp]: query}}, - {username: {[Op.iRegexp]: query}}, - ], - }, - }); - const groups = await models.Group.findAll({ - limit, - offset, - where: {name: {[Op.iRegexp]: query}}, - }); - const posts = await models.Post.findAll({ - limit, - offset, - where: {content: {[Op.iRegexp]: query}}, - }); - const events = await models.Event.findAll({ - limit, - offset, - where: {name: {[Op.iRegexp]: query}}, - }); - return {users, posts, groups, events}; - }, - async findUser({first, offset, name, handle}: - { first: number, offset: number, name: string, handle: string }) { - res.status(status.MOVED_PERMANENTLY); - if (name) { - return models.User.findAll({where: {username: {[Op.like]: `%${name}%`}}, offset, limit: first}); - } else if (handle) { - return models.User.findAll({where: {handle: {[Op.like]: `%${handle}%`}}, offset, limit: first}); - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No search parameters provided."); - } - }, - async getSelf() { - if (req.session.userId) { - return models.User.findByPk(req.session.userId); - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async getUser({userId, handle}: { userId: number, handle: string }) { - if (handle) { - return await dataaccess.getUserByHandle(handle); - } else if (userId) { - return models.User.findByPk(userId); - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No userId or handle provided."); - } - }, - async getPost({postId}: { postId: number }) { - if (postId) { - return await dataaccess.getPost(postId); - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No postId given."); - } - }, - async getChat({chatId}: { chatId: number }) { - if (chatId) { - return models.ChatRoom.findByPk(chatId); - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No chatId given."); - } - }, - async getGroup({groupId}: { groupId: number }) { - if (groupId) { - return models.Group.findByPk(groupId); - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No group id given."); - } - }, - async getRequest({requestId}: { requestId: number }) { - if (requestId) { - return models.Request.findByPk(requestId); - } else { - res.status(status.BAD_REQUEST); - 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() { - req.session.cookiesAccepted = true; - return true; - }, - async login({email, passwordHash}: { email: string, passwordHash: string }) { - if (email && passwordHash) { - try { - const user = await dataaccess.getUserByLogin(email, passwordHash); - req.session.userId = user.id; - return user; - } catch (err) { - globals.logger.warn(err.message); - globals.logger.debug(err.stack); - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No email or password given."); - } - }, - logout() { - if (req.session.userId) { - delete req.session.userId; - req.session.save((err: any) => { - if (err) { - globals.logger.error(err.message); - globals.logger.debug(err.stack); - } - }); - return true; - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async getToken({email, passwordHash}: { email: string, passwordHash: string }) { - if (email && passwordHash) { - try { - const user = await dataaccess.getUserByLogin(email, passwordHash); - if (!user) { - res.status(status.BAD_REQUEST); - return new InvalidLoginError(email); - } else { - return { - expires: Number(user.authExpire), - value: user.token(), - }; - } - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No email or password specified."); - } - }, - async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) { - if (username && email && passwordHash) { - 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) { - res.status(status.BAD_REQUEST); - return new GraphQLError(`'${email}' is not a valid email address!`); - } - try { - const user = await dataaccess.registerUser(username, email, passwordHash); - req.session.userId = user.id; - return user; - } catch (err) { - globals.logger.warn(err.message); - globals.logger.debug(err.stack); - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No username, email or password given."); - } - }, - async setUserSettings({settings}: { settings: string }) { - if (req.session.userId) { - const user = await models.User.findByPk(req.session.userId); - try { - user.frontendSettings = yaml.safeLoad(settings); - await user.save(); - return user.settings; - } catch (err) { - res.status(status.BAD_REQUEST); - return new GraphQLError("Invalid settings json."); - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) { - if (postId && type) { - if (req.session.userId) { - const post = await models.Post.findByPk(postId); - if (post) { - const voteType = await post.vote(req.session.userId, type); - return { - post, - voteType, - }; - } else { - res.status(status.BAD_REQUEST); - return new PostNotFoundGqlError(postId); - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No postId or type given."); - } - }, - async createPost({content, activityId}: { content: string, activityId: number }) { - if (content) { - if (req.session.userId) { - if (content.length > 2048) { - return new GraphQLError("Content too long."); - } else { - try { - const post = await dataaccess.createPost(content, req.session.userId, activityId); - globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); - return post; - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("Can't create empty post."); - } - }, - async deletePost({postId}: { postId: number }) { - if (postId) { - const post = await models.Post.findByPk(postId, { - include: [{ - as: "rAuthor", - model: models.User, - }], - }); - const isAdmin = (await models.User.findOne({where: {id: req.session.userId}})).isAdmin; - if (post.rAuthor.id === req.session.userId || isAdmin) { - try { - return await dataaccess.deletePost(post.id); - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.FORBIDDEN); - return new GraphQLError("User is not author of the post."); - } - } else { - return new GraphQLError("No postId given."); - } - }, - async createChat({members}: { members: number[] }) { - if (req.session.userId) { - const chatMembers = [req.session.userId]; - if (members) { - chatMembers.push(...members); - } - return await dataaccess.createChat(...chatMembers); - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async sendMessage({chatId, content}: { chatId: number, content: string }) { - if (!req.session.userId) { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - if (chatId && content) { - try { - const message = await dataaccess.sendChatMessage(req.session.userId, chatId, content); - globals.internalEmitter.emit(InternalEvents.GQLCHATMESSAGE, message); - return message; - } catch (err) { - globals.logger.warn(err.message); - globals.logger.debug(err.stack); - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No chatId or content given."); - } - }, - async sendRequest({receiver, type}: { receiver: number, type: dataaccess.RequestType }) { - if (!req.session.userId) { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - if (receiver && type) { - try { - return await dataaccess.createRequest(req.session.userId, receiver, type); - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No receiver or type given."); - } - }, - async denyRequest({sender, type}: { sender: number, type: dataaccess.RequestType }) { - if (!req.session.userId) { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - if (sender && type) { - const user = await models.User.findByPk(req.session.userId); - await user.denyRequest(sender, type); - return true; - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No sender or type given."); - } - }, - async acceptRequest({sender, type}: { sender: number, type: dataaccess.RequestType }) { - if (!req.session.userId) { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - if (sender && type) { - try { - const user = await models.User.findByPk(req.session.userId); - await user.acceptRequest(sender, type); - return true; - } catch (err) { - globals.logger.warn(err.message); - globals.logger.debug(err.stack); - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No sender or type given."); - } - }, - async removeFriend({friendId}: { friendId: number }) { - if (req.session.userId) { - const self = await models.User.findByPk(req.session.userId); - return await self.removeFriend(friendId); - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async getPosts({first, offset, sort}: { first: number, offset: number, sort: dataaccess.SortType }) { - return await dataaccess.getPosts(first, offset, sort); - }, - async createGroup({name, members}: { name: string, members: number[] }) { - if (req.session.userId) { - try { - return await dataaccess.createGroup(name, req.session.userId, members); - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - return new NotLoggedInGqlError(); - } - }, - async deleteGroup({groupId}: {groupId: number}) { - if (req.session.userId) { - const group = await models.Group.findByPk(groupId); - if (!group) { - res.status(status.BAD_REQUEST); - return new GroupNotFoundError(groupId).graphqlError; - } - if (group.creatorId === req.session.userId) { - await group.destroy(); - return true; - } else { - res.status(status.FORBIDDEN); - return new GraphQLError("You are not the group admin."); - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async joinGroup({id}: { id: number }) { - if (req.session.userId) { - try { - return await dataaccess - .changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.ADD); - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async leaveGroup({id}: { id: number }) { - if (req.session.userId) { - try { - return await dataaccess - .changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.REMOVE); - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async addGroupAdmin({groupId, userId}: { groupId: number, userId: number }) { - if (req.session.userId) { - const group = await models.Group.findByPk(groupId); - const self = await models.User.findByPk(req.session.userId); - if (group && !(await group.$has("rAdmins", self)) && (await group.creator()) !== self.id) { - res.status(status.FORBIDDEN); - return new GraphQLError("You are not a group admin!"); - } - try { - return await dataaccess - .changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.OP); - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async removeGroupAdmin({groupId, userId}: { groupId: number, userId: number }) { - if (req.session.userId) { - const group = await models.Group.findByPk(groupId); - const isCreator = Number(group.creatorId) === Number(req.session.userId); - const userIsCreator = Number(group.creatorId) === Number(userId); - if (group && !isCreator && Number(userId) !== Number(req.session.userId)) { - res.status(status.FORBIDDEN); - return new GraphQLError("You are not the group creator!"); - } else if (userIsCreator) { - res.status(status.FORBIDDEN); - return new GraphQLError("You are not allowed to remove a creator as an admin."); - } - try { - return await dataaccess - .changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.DEOP); - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError ?? new GraphQLError(err.message); - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async createEvent({name, dueDate, groupId}: { name: string, dueDate: string, groupId: number }) { - if (req.session.userId) { - const date = new Date(Number(dueDate)); - const group = await models.Group.findByPk(groupId, {include: [{association: "rAdmins"}]}); - 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("rEvent", {name, dueDate: date}); - } else { - res.status(status.FORBIDDEN); - return new GraphQLError("You are not a group admin!"); - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async deleteEvent({eventId}: {eventId: number}) { - if (req.session.userId) { - const event = await models.Event.findByPk(eventId, {include: [models.Group]}); - const user = await models.User.findByPk(req.session.userId); - if (!event) { - res.status(status.BAD_REQUEST); - return new GraphQLError(`No event with id '${eventId}' found.`); - } - const group = await event.group(); - if (await group.$has("rAdmins", user)) { - await event.destroy(); - return true; - } else { - res.status(status.FORBIDDEN); - return new NotAnAdminGqlError(); - } - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async joinEvent({eventId}: { eventId: number }) { - if (req.session.userId) { - const event = await models.Event.findByPk(eventId); - const self = await models.User.findByPk(req.session.userId); - await event.$add("rParticipants", self); - return event; - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async leaveEvent({eventId}: { eventId: number }) { - if (req.session.userId) { - const event = await models.Event.findByPk(eventId); - const self = await models.User.findByPk(req.session.userId); - await event.$remove("rParticipants", self); - return event; - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async getActivities() { - return models.Activity.findAll(); - }, - async createActivity({name, description, points}: - { name: string, description: string, points: number }) { - if (req.session.userId) { - const user = await models.User.findByPk(req.session.userId); - if (user.isAdmin) { - const nameExists = await models.Activity.findOne({where: {name}}); - if (!nameExists) { - return models.Activity.create({name, description, points}); - } else { - return new GraphQLError(`An activity with the name '${name}' already exists.`); - } - } else { - res.status(status.FORBIDDEN); - 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 { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - }; + return new QueryResolver(); } diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index 76d22f4..ad15e01 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -80,7 +80,7 @@ export class User extends Model { * The auth token for bearer authentication */ @Unique - @Column({defaultValue: uuidv4, unique: true, type: sqz.UUIDV4}) + @Column({defaultValue: uuidv4, unique: true, type: sqz.UUID}) public authToken: string; /** diff --git a/yarn.lock b/yarn.lock index 8c6b810..33c2acc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -179,6 +179,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.148.tgz#ffa2786721707b335c6aa1465e6d3d74016fbd3e" integrity sha512-05+sIGPev6pwpHF7NZKfP3jcXhXsIVFnYyVRT4WOB0me62E8OlWfTN+sKyt2/rqN+ETxuHAtgTSK1v71F0yncg== +"@types/lodash@^4.14.149": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== + "@types/markdown-it@0.0.9": version "0.0.9" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.9.tgz#a5d552f95216c478e0a27a5acc1b28dcffd989ce"