From 2d0a6e3433a2e922f2f900b2681818f91a1b1c95 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 1 Oct 2019 17:45:55 +0200 Subject: [PATCH 1/2] Implemented Socket.io - added socket for chat messages - added socket for new requests - added socket for post creation - added socket for created post --- src/lib/InternalEvents.ts | 8 +++ src/lib/dataaccess/ChatMessage.ts | 13 ++++ src/lib/dataaccess/Chatroom.ts | 4 +- src/lib/dataaccess/Post.ts | 13 ++++ src/lib/dataaccess/Request.ts | 14 ++++- src/lib/dataaccess/index.ts | 31 ++++++++-- src/lib/globals.ts | 2 + src/public/graphql/schema.graphql | 3 + src/routes/home.ts | 98 +++++++++++++++++++++++++------ 9 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 src/lib/InternalEvents.ts diff --git a/src/lib/InternalEvents.ts b/src/lib/InternalEvents.ts new file mode 100644 index 0000000..13cf6ab --- /dev/null +++ b/src/lib/InternalEvents.ts @@ -0,0 +1,8 @@ +export enum InternalEvents { + CHATCREATE = "chatCreate", + CHATMESSAGE = "chatMessage", + GQLCHATMESSAGE = "graphqlChatMessage", + REQUESTCREATE = "requestCreate", + POSTCREATE = "postCreate", + GQLPOSTCREATE = "graphqlPostCreate", +} diff --git a/src/lib/dataaccess/ChatMessage.ts b/src/lib/dataaccess/ChatMessage.ts index bbad8ad..4c91a16 100644 --- a/src/lib/dataaccess/ChatMessage.ts +++ b/src/lib/dataaccess/ChatMessage.ts @@ -15,4 +15,17 @@ export class ChatMessage { public htmlContent(): string { return markdown.renderInline(this.content); } + + /** + * Returns resolved and rendered content of the chat message. + */ + public resolvedContent() { + return { + author: this.author.id, + chat: this.chat.id, + content: this.content, + createdAt: this.createdAt, + htmlContent: this.htmlContent(), + }; + } } diff --git a/src/lib/dataaccess/Chatroom.ts b/src/lib/dataaccess/Chatroom.ts index 0787637..e541f06 100644 --- a/src/lib/dataaccess/Chatroom.ts +++ b/src/lib/dataaccess/Chatroom.ts @@ -5,8 +5,10 @@ import {User} from "./User"; export class Chatroom { - constructor(private readonly id: number) { + public namespace: string; + constructor(public readonly id: number) { this.id = Number(id); + this.namespace = `/chat/${id}`; } /** diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index 7908f62..e4912c7 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -11,6 +11,19 @@ export class Post extends DataObject { private $author: number; private $type: string; + /** + * Returns the resolved data of the post. + */ + public async resolvedData() { + await this.loadDataIfNotExists(); + return { + authorId: this.$author, + content: this.$content, + createdAt: this.$createdAt, + id: this.id, + type: this.$type, + }; + } /** * Returns the upvotes of a post. */ diff --git a/src/lib/dataaccess/Request.ts b/src/lib/dataaccess/Request.ts index f161757..7e8f1bc 100644 --- a/src/lib/dataaccess/Request.ts +++ b/src/lib/dataaccess/Request.ts @@ -8,5 +8,17 @@ export class Request { constructor( public readonly sender: User, public readonly receiver: User, - public readonly type: dataaccess.RequestType) {} + public readonly type: dataaccess.RequestType) { + } + + /** + * Returns the resolved request data. + */ + public resolvedData() { + return { + receiverId: this.receiver.id, + senderId: this.sender.id, + type: this.type, + }; + } } diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 3dccfcd..46a6649 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -2,6 +2,7 @@ import {Pool} from "pg"; import {ChatNotFoundError} from "../errors/ChatNotFoundError"; import {UserNotFoundError} from "../errors/UserNotFoundError"; import globals from "../globals"; +import {InternalEvents} from "../InternalEvents"; import {QueryHelper} from "../QueryHelper"; import {ChatMessage} from "./ChatMessage"; import {Chatroom} from "./Chatroom"; @@ -125,7 +126,9 @@ namespace dataaccess { text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *", values: [content, authorId, type], }); - return new Post(result.id, result); + const post = new Post(result.id, result); + globals.internalEmitter.emit(InternalEvents.POSTCREATE, post); + return post; } /** @@ -167,7 +170,9 @@ namespace dataaccess { } finally { transaction.release(); } - return new Chatroom(id); + const chat = new Chatroom(id); + globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat); + return chat; } /** @@ -183,12 +188,28 @@ namespace dataaccess { text: "INSERT INTO chat_messages (chat, author, content) values ($1, $2, $3) RETURNING *", values: [chatId, authorId, content], }); - return new ChatMessage(new User(result.author), chat, result.created_at, result.content); + const message = new ChatMessage(new User(result.author), chat, result.created_at, result.content); + globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message); + return message; } else { throw new ChatNotFoundError(chatId); } } + /** + * Returns all chats. + */ + export async function getAllChats(): Promise { + const result = await queryHelper.all({ + text: "SELECT id FROM chats;", + }); + const chats = []; + for (const row of result) { + chats.push(new Chatroom(row.id)); + } + return chats; + } + /** * Sends a request to a user. * @param sender @@ -202,7 +223,9 @@ namespace dataaccess { text: "INSERT INTO requests (sender, receiver, type) VALUES ($1, $2, $3) RETURNING *", values: [sender, receiver, type], }); - return new Request(new User(result.sender), new User(result.receiver), result.type); + const request = new Request(new User(result.sender), new User(result.receiver), result.type); + globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request); + return request; } /** diff --git a/src/lib/globals.ts b/src/lib/globals.ts index d3df954..cea8e3c 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -5,6 +5,7 @@ * Partly taken from {@link https://github.com/Trivernis/whooshy} */ +import {EventEmitter} from "events"; import * as fsx from "fs-extra"; import * as yaml from "js-yaml"; import * as winston from "winston"; @@ -42,6 +43,7 @@ namespace globals { }), ], }); + export const internalEmitter = new EventEmitter(); cache.on("set", (key) => logger.debug(`Caching '${key}'.`)); cache.on("miss", (key) => logger.debug(`Cache miss for '${key}'`)); cache.on("hit", (key) => logger.debug(`Cache hit for '${key}'`)); diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index 19a19eb..cefe527 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -189,6 +189,9 @@ type Request { "represents a chatroom" type ChatRoom { + "the socket.io namespace for the chatroom" + namespace: String + "the members of the chatroom" members: [User!] diff --git a/src/routes/home.ts b/src/routes/home.ts index 88ce8b0..614a8f7 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,17 +1,22 @@ import {Router} from "express"; import {GraphQLError} from "graphql"; import * as status from "http-status"; -import {Server} from "socket.io"; +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"; +const chatRooms: Namespace[] = []; + /** * Class for the home route. */ @@ -30,6 +35,33 @@ class HomeRoute extends Route { */ public async init(io: Server) { this.io = io; + + io.on("connection", (socket) => { + socket.on("postCreate", async (content) => { + if (socket.handshake.session.userId) { + const post = await dataaccess.createPost(content, socket.handshake.session.userId); + io.emit("post", await post.resolvedData()); + } else { + socket.emit("error", "Not logged in!"); + } + }); + globals.internalEmitter.on(InternalEvents.REQUESTCREATE, (request: Request) => { + if (request.receiver.id === socket.handshake.session.userId) { + socket.emit("request", request.resolvedData()); + } + }); + globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => { + socket.emit("post", await post.resolvedData()); + }); + }); + + const chats = await dataaccess.getAllChats(); + for (const chat of chats) { + chatRooms[chat.id] = this.getChatSocketNamespace(chat.id); + } + globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: Chatroom) => { + chatRooms[chat.id] = this.getChatSocketNamespace(chat.id); + }); } /** @@ -55,7 +87,7 @@ class HomeRoute extends Route { return new NotLoggedInGqlError(); } }, - async getUser({userId, handle}: {userId: number, handle: string}) { + async getUser({userId, handle}: { userId: number, handle: string }) { if (handle) { return await dataaccess.getUserByHandle(handle); } else if (userId) { @@ -65,7 +97,7 @@ class HomeRoute extends Route { return new GraphQLError("No userId or handle provided."); } }, - async getPost({postId}: {postId: number}) { + async getPost({postId}: { postId: number }) { if (postId) { return await dataaccess.getPost(postId); } else { @@ -73,7 +105,7 @@ class HomeRoute extends Route { return new GraphQLError("No postId given."); } }, - async getChat({chatId}: {chatId: number}) { + async getChat({chatId}: { chatId: number }) { if (chatId) { return new Chatroom(chatId); } else { @@ -85,7 +117,7 @@ class HomeRoute extends Route { req.session.cookiesAccepted = true; return true; }, - async login({email, passwordHash}: {email: string, passwordHash: string}) { + async login({email, passwordHash}: { email: string, passwordHash: string }) { if (email && passwordHash) { try { const user = await dataaccess.getUserByLogin(email, passwordHash); @@ -110,7 +142,7 @@ class HomeRoute extends Route { return new NotLoggedInGqlError(); } }, - async register({username, email, passwordHash}: {username: string, email: string, passwordHash: string}) { + async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) { if (username && email && passwordHash) { if (!is.email(email)) { res.status(status.BAD_REQUEST); @@ -129,7 +161,7 @@ class HomeRoute extends Route { return new GraphQLError("No username, email or password given."); } }, - async vote({postId, type}: {postId: number, type: dataaccess.VoteType}) { + 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); @@ -142,10 +174,12 @@ class HomeRoute extends Route { return new GraphQLError("No postId or type given."); } }, - async createPost({content}: {content: string}) { + async createPost({content}: { content: string }) { if (content) { if (req.session.userId) { - return await dataaccess.createPost(content, 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(); @@ -155,7 +189,7 @@ class HomeRoute extends Route { return new GraphQLError("Can't create empty post."); } }, - async deletePost({postId}: {postId: number}) { + async deletePost({postId}: { postId: number }) { if (postId) { const post = new Post(postId); if ((await post.author()).id === req.session.userId) { @@ -168,27 +202,28 @@ class HomeRoute extends Route { return new GraphQLError("No postId given."); } }, - async createChat({members}: {members: number[]}) { + 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}) { + async sendMessage({chatId, content}: { chatId: number, content: string }) { if (!req.session.userId) { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); } if (chatId && content) { try { - return await dataaccess.sendChatMessage(req.session.userId, chatId, content); + 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; @@ -198,7 +233,7 @@ class HomeRoute extends Route { return new GraphQLError("No chatId or content given."); } }, - async sendRequest({receiver, type}: {receiver: number, type: dataaccess.RequestType}) { + async sendRequest({receiver, type}: { receiver: number, type: dataaccess.RequestType }) { if (!req.session.userId) { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); @@ -210,7 +245,7 @@ class HomeRoute extends Route { return new GraphQLError("No receiver or type given."); } }, - async denyRequest({sender, type}: {sender: number, type: dataaccess.RequestType}) { + async denyRequest({sender, type}: { sender: number, type: dataaccess.RequestType }) { if (!req.session.userId) { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); @@ -224,7 +259,7 @@ class HomeRoute extends Route { return new GraphQLError("No sender or type given."); } }, - async acceptRequest({sender, type}: {sender: number, type: dataaccess.RequestType}) { + async acceptRequest({sender, type}: { sender: number, type: dataaccess.RequestType }) { if (!req.session.userId) { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); @@ -245,6 +280,35 @@ class HomeRoute extends Route { }, }; } + + /** + * Returns the namespace socket for a chat socket. + * @param chatId + */ + private getChatSocketNamespace(chatId: number) { + if (chatRooms[chatId]) { + return chatRooms[chatId]; + } + const chatNs = this.io.of(`/chat/${chatId}`); + chatNs.on("connection", (socket) => { + socket.on("chatMessage", async (content) => { + if (socket.handshake.session.userId) { + const userId = socket.handshake.session.userId; + const message = await dataaccess.sendChatMessage(userId, chatId, content); + socket.broadcast.emit("chatMessage", message.resolvedContent()); + socket.emit("chatMessageSent", message.resolvedContent()); + } else { + socket.emit("error", "Not logged in!"); + } + }); + globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, (message: ChatMessage) => { + if (message.chat.id === chatId) { + socket.emit("chatMessage", message.resolvedContent()); + } + }); + }); + return chatNs; + } } export default HomeRoute; From bc8455f84b37094a606f7e6f6fac35d3e9dedd2a Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 1 Oct 2019 18:11:01 +0200 Subject: [PATCH 2/2] 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