From bc8455f84b37094a606f7e6f6fac35d3e9dedd2a Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 1 Oct 2019 18:11:01 +0200 Subject: [PATCH] Errors and Structure - moved graphql stuff to a seperate directory - added error for already existing email in database --- src/app.ts | 5 +- src/graphql/resolvers.ts | 227 ++++++++++++++++++ src/{public => }/graphql/schema.graphql | 0 src/lib/Route.ts | 1 - src/lib/dataaccess/index.ts | 17 +- src/lib/errors/EmailAlreadyRegisteredError.ts | 8 + src/lib/regex.ts | 4 +- src/routes/home.ts | 219 +---------------- src/routes/index.ts | 10 - 9 files changed, 256 insertions(+), 235 deletions(-) create mode 100644 src/graphql/resolvers.ts rename src/{public => }/graphql/schema.graphql (100%) create mode 100644 src/lib/errors/EmailAlreadyRegisteredError.ts diff --git a/src/app.ts b/src/app.ts index 5801771..d7c3641 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,6 +10,7 @@ import {importSchema} from "graphql-import"; import * as http from "http"; import * as path from "path"; import * as socketIo from "socket.io"; +import {resolver} from "./graphql/resolvers"; import dataaccess, {queryHelper} from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; @@ -72,8 +73,8 @@ class App { // @ts-ignore all context: {session: request.session}, graphiql: true, - rootValue: routes.resolvers(request, response), - schema: buildSchema(importSchema(path.join(__dirname, "./public/graphql/schema.graphql"))), + rootValue: resolver(request, response), + schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))), }; })); } diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts new file mode 100644 index 0000000..943da91 --- /dev/null +++ b/src/graphql/resolvers.ts @@ -0,0 +1,227 @@ +import {GraphQLError} from "graphql"; +import * as status from "http-status"; +import dataaccess from "../lib/dataaccess"; +import {Chatroom} from "../lib/dataaccess/Chatroom"; +import {Post} from "../lib/dataaccess/Post"; +import {Profile} from "../lib/dataaccess/Profile"; +import {User} from "../lib/dataaccess/User"; +import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors"; +import globals from "../lib/globals"; +import {InternalEvents} from "../lib/InternalEvents"; +import {is} from "../lib/regex"; + +/** + * Returns the resolvers for the graphql api. + * @param req - the request object + * @param res - the response object + */ +export function resolver(req: any, res: any): any { + return { + getSelf() { + if (req.session.userId) { + return new Profile(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 new User(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 new Chatroom(chatId); + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No chatId given."); + } + }, + 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 || err.message; + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No email or password given."); + } + }, + logout() { + if (req.session.user) { + delete req.session.user; + return true; + } else { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + }, + async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) { + if (username && email && passwordHash) { + if (!is.email(email)) { + 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 || err.message; + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No username, email or password given."); + } + }, + async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) { + if (postId && type) { + if (req.session.userId) { + return await (new Post(postId)).vote(req.session.userId, type); + } 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}: { content: string }) { + if (content) { + if (req.session.userId) { + const post = await dataaccess.createPost(content, req.session.userId); + globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); + return post; + } 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 = new Post(postId); + if ((await post.author()).id === req.session.userId) { + return await dataaccess.deletePost(post.id); + } 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 || 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) { + return await dataaccess.createRequest(req.session.userId, receiver, type); + } 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 profile = new Profile(req.session.userId); + await profile.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 profile = new Profile(req.session.userId); + await profile.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 || err.message; + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No sender or type given."); + } + }, + }; +} diff --git a/src/public/graphql/schema.graphql b/src/graphql/schema.graphql similarity index 100% rename from src/public/graphql/schema.graphql rename to src/graphql/schema.graphql diff --git a/src/lib/Route.ts b/src/lib/Route.ts index 63c25ac..37bed62 100644 --- a/src/lib/Route.ts +++ b/src/lib/Route.ts @@ -21,7 +21,6 @@ abstract class Route { public abstract async init(...params: any): Promise; public abstract async destroy(...params: any): Promise; - public abstract async resolver(request: any, response: any): Promise; } export default Route; diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 46a6649..b160283 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,5 +1,6 @@ import {Pool} from "pg"; import {ChatNotFoundError} from "../errors/ChatNotFoundError"; +import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError"; import {UserNotFoundError} from "../errors/UserNotFoundError"; import globals from "../globals"; import {InternalEvents} from "../InternalEvents"; @@ -91,11 +92,19 @@ namespace dataaccess { * @param password */ export async function registerUser(username: string, email: string, password: string) { - const result = await queryHelper.first({ - text: "INSERT INTO users (name, handle, password, email) VALUES ($1, $2, $3, $4) RETURNING *", - values: [username, generateHandle(username), password, email], + const existResult = await queryHelper.first({ + text: "SELECT email FROM users WHERE email = $1;", + values: [email], }); - return new Profile(result.id, result); + if (!existResult || !existResult.email) { + const result = await queryHelper.first({ + text: "INSERT INTO users (name, handle, password, email) VALUES ($1, $2, $3, $4) RETURNING *", + values: [username, generateHandle(username), password, email], + }); + return new Profile(result.id, result); + } else { + throw new EmailAlreadyRegisteredError(email); + } } /** diff --git a/src/lib/errors/EmailAlreadyRegisteredError.ts b/src/lib/errors/EmailAlreadyRegisteredError.ts new file mode 100644 index 0000000..4a6fa09 --- /dev/null +++ b/src/lib/errors/EmailAlreadyRegisteredError.ts @@ -0,0 +1,8 @@ +import {BaseError} from "./BaseError"; + +export class EmailAlreadyRegisteredError extends BaseError { + constructor(email: string) { + super(`A user for '${email}' does already exist.`); + } + +} diff --git a/src/lib/regex.ts b/src/lib/regex.ts index 1ed6c11..281da4a 100644 --- a/src/lib/regex.ts +++ b/src/lib/regex.ts @@ -1,11 +1,11 @@ export namespace is { - const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g + const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g; /** * Tests if a string is a valid email. * @param testString */ export function email(testString: string) { - return emailRegex.test(testString) + return emailRegex.test(testString); } } diff --git a/src/routes/home.ts b/src/routes/home.ts index 614a8f7..726b890 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,20 +1,17 @@ import {Router} from "express"; -import {GraphQLError} from "graphql"; -import * as status from "http-status"; import {Namespace, Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; import {ChatMessage} from "../lib/dataaccess/ChatMessage"; import {Chatroom} from "../lib/dataaccess/Chatroom"; import {Post} from "../lib/dataaccess/Post"; -import {Profile} from "../lib/dataaccess/Profile"; import {Request} from "../lib/dataaccess/Request"; -import {User} from "../lib/dataaccess/User"; -import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; -import {is} from "../lib/regex"; import Route from "../lib/Route"; +/** + * list of chatroom socket namespaces. + */ const chatRooms: Namespace[] = []; /** @@ -69,216 +66,6 @@ class HomeRoute extends Route { */ public async destroy(): Promise { this.router = null; - this.resolver = null; - } - - /** - * Returns the resolvers for the graphql api. - * @param req - the request object - * @param res - the response object - */ - public resolver(req: any, res: any): any { - return { - getSelf() { - if (req.session.userId) { - return new Profile(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 new User(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 new Chatroom(chatId); - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No chatId given."); - } - }, - 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.verbose(`Failed to login user '${email}'`); - res.status(status.BAD_REQUEST); - return err.graphqlError; - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No email or password given."); - } - }, - logout() { - if (req.session.user) { - delete req.session.user; - return true; - } else { - res.status(status.UNAUTHORIZED); - return new NotLoggedInGqlError(); - } - }, - async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) { - if (username && email && passwordHash) { - if (!is.email(email)) { - res.status(status.BAD_REQUEST); - return new GraphQLError(`'${email}' is not a valid email address!`); - } - const user = await dataaccess.registerUser(username, email, passwordHash); - if (user) { - req.session.userId = user.id; - return user; - } else { - res.status(status.INTERNAL_SERVER_ERROR); - return new GraphQLError("Failed to create account."); - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No username, email or password given."); - } - }, - async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) { - if (postId && type) { - if (req.session.userId) { - return await (new Post(postId)).vote(req.session.userId, type); - } 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}: { content: string }) { - if (content) { - if (req.session.userId) { - const post = await dataaccess.createPost(content, req.session.userId); - globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); - return post; - } 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 = new Post(postId); - if ((await post.author()).id === req.session.userId) { - return await dataaccess.deletePost(post.id); - } 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) { - res.status(status.BAD_REQUEST); - return err.graphqlError; - } - } 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) { - return await dataaccess.createRequest(req.session.userId, receiver, type); - } 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 profile = new Profile(req.session.userId); - await profile.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 profile = new Profile(req.session.userId); - await profile.acceptRequest(sender, type); - return true; - } catch (err) { - res.status(status.BAD_REQUEST); - return err.graphqlError; - } - } else { - res.status(status.BAD_REQUEST); - return new GraphQLError("No sender or type given."); - } - }, - }; } /** diff --git a/src/routes/index.ts b/src/routes/index.ts index eb65750..201d8ea 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -22,16 +22,6 @@ namespace routes { router.use("/", homeRoute.router); - /** - * Asnyc function to create a graphql resolver that takes the request and response - * of express.js as arguments. - * @param request - * @param response - */ - export function resolvers(request: any, response: any): Promise { - return homeRoute.resolver(request, response); - } - /** * Assigns the io listeners or namespaces to the routes * @param io