Merge branch 'julius-dev' of Software_Engineering_I/greenvironment-server into develop

pull/1/head
Trivernis 5 years ago committed by Gitea
commit 8aaac775bf

@ -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"))),
};
}));
}

@ -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.");
}
},
};
}

@ -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!]

@ -0,0 +1,8 @@
export enum InternalEvents {
CHATCREATE = "chatCreate",
CHATMESSAGE = "chatMessage",
GQLCHATMESSAGE = "graphqlChatMessage",
REQUESTCREATE = "requestCreate",
POSTCREATE = "postCreate",
GQLPOSTCREATE = "graphqlPostCreate",
}

@ -21,7 +21,6 @@ abstract class Route {
public abstract async init(...params: any): Promise<any>;
public abstract async destroy(...params: any): Promise<any>;
public abstract async resolver(request: any, response: any): Promise<object>;
}
export default Route;

@ -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(),
};
}
}

@ -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}`;
}
/**

@ -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.
*/

@ -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,
};
}
}

@ -1,7 +1,9 @@
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";
import {QueryHelper} from "../QueryHelper";
import {ChatMessage} from "./ChatMessage";
import {Chatroom} from "./Chatroom";
@ -90,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);
}
}
/**
@ -125,7 +135,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 +179,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 +197,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<Chatroom[]> {
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 +232,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;
}
/**

@ -0,0 +1,8 @@
import {BaseError} from "./BaseError";
export class EmailAlreadyRegisteredError extends BaseError {
constructor(email: string) {
super(`A user for '${email}' does already exist.`);
}
}

@ -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}'`));

@ -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);
}
}

@ -1,17 +1,19 @@
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 {User} from "../lib/dataaccess/User";
import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors";
import {Request} from "../lib/dataaccess/Request";
import globals from "../lib/globals";
import {is} from "../lib/regex";
import {InternalEvents} from "../lib/InternalEvents";
import Route from "../lib/Route";
/**
* list of chatroom socket namespaces.
*/
const chatRooms: Namespace[] = [];
/**
* Class for the home route.
*/
@ -30,6 +32,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);
});
}
/**
@ -37,213 +66,35 @@ class HomeRoute extends Route {
*/
public async destroy(): Promise<void> {
this.router = null;
this.resolver = null;
}
/**
* Returns the resolvers for the graphql api.
* @param req - the request object
* @param res - the response object
* Returns the namespace socket for a chat socket.
* @param chatId
*/
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) {
return await dataaccess.createPost(content, req.session.userId);
} 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.");
}
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 {
return new GraphQLError("No postId given.");
socket.emit("error", "Not logged in!");
}
},
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 {
return await dataaccess.sendChatMessage(req.session.userId, chatId, content);
} 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.");
});
globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, (message: ChatMessage) => {
if (message.chat.id === chatId) {
socket.emit("chatMessage", message.resolvedContent());
}
},
};
});
});
return chatNs;
}
}

@ -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<object> {
return homeRoute.resolver(request, response);
}
/**
* Assigns the io listeners or namespaces to the routes
* @param io

Loading…
Cancel
Save