diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index a764a9c..5b5a40b 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -1,8 +1,10 @@ import {GraphQLError} from "graphql"; import * as status from "http-status"; import dataaccess from "../lib/dataaccess"; +import {UserNotFoundError} from "../lib/errors/UserNotFoundError"; +import {Group} from "../lib/models"; import * as models from "../lib/models"; -import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; +import {GroupNotFoundGqlError, NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; import {is} from "../lib/regex"; @@ -106,7 +108,7 @@ export function resolver(req: any, res: any): any { if (post) { return await post.vote(req.session.userId, type); } else { - res.status(400); + res.status(status.BAD_REQUEST); return new PostNotFoundGqlError(postId); } } else { @@ -229,5 +231,99 @@ export function resolver(req: any, res: any): any { 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) { + return await dataaccess.createGroup(name, req.session.userId, members); + } else { + return new NotLoggedInGqlError(); + } + }, + async joinGroup({id}: {id: number}) { + if (req.session.userId) { + const group = await models.Group.findByPk(id); + if (group) { + const user = await models.User.findByPk(req.session.userId); + await group.$add("rMembers", user); + await (await group.chat()).$add("rMembers", user); + return group; + } else { + res.status(status.BAD_REQUEST); + return new GroupNotFoundGqlError(id); + } + } else { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + }, + async leaveGroup({id}: {id: number}) { + if (req.session.userId) { + const group = await models.Group.findByPk(id); + if (group) { + const user = await models.User.findByPk(req.session.userId); + await group.$remove("rMembers", user); + await (await group.chat()).$remove("rMembers", user); + return group; + } else { + res.status(status.BAD_REQUEST); + return new GroupNotFoundGqlError(id); + } + } 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 user = await models.User.findByPk(userId); + const self = await models.User.findByPk(req.session.userId); + if (!group) { + res.status(status.BAD_REQUEST); + return new GroupNotFoundGqlError(groupId); + } + if (!user) { + res.status(status.BAD_REQUEST); + return new UserNotFoundError(userId.toString()).graphqlError; + } + if (!(await group.$has("rAdmins", self)) && (await group.creator()) !== self.id) { + res.status(status.FORBIDDEN); + return new GraphQLError("You are not a group admin!"); + } + await group.$add("rAdmins", user); + return group; + + } 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 user = await models.User.findByPk(userId); + if (!group) { + res.status(status.BAD_REQUEST); + return new GroupNotFoundGqlError(groupId); + } + if (!user) { + res.status(status.BAD_REQUEST); + return new UserNotFoundError(userId.toString()).graphqlError; + } + if ((await group.creator()).id === userId) { + res.status(status.FORBIDDEN); + return new GraphQLError("You can't remove the creator of a group."); + } + if ((await group.creator()).id !== req.session.userId) { + res.status(status.FORBIDDEN); + return new GraphQLError("You are not a group admin!"); + } + await group.$remove("rAdmins", user); + return group; + + } else { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + }, }; } diff --git a/src/graphql/schema.graphql b/src/graphql/schema.graphql index 681b0f4..f81050c 100644 --- a/src/graphql/schema.graphql +++ b/src/graphql/schema.graphql @@ -60,6 +60,21 @@ type Mutation { "Creates a chat between the user (and optional an other user)" createChat(members: [ID!]): ChatRoom + + "Creates a new group with a given name and additional members" + createGroup(name: String!, members: [ID!]): Group + + "Joins a group with the given id" + joinGroup(id: ID!): Group + + "leaves the group with the given id" + leaveGroup(id: ID!): Group + + "adds an admin to the group" + addGroupAdmin(groupId: ID!, userId: ID!): Group + + "removes an admin from the group" + removeGroupAdmin(groupId: ID!, userId: ID!): Group } interface UserData { @@ -148,6 +163,16 @@ type Profile implements UserData { "all received request for groupChats/friends/events" receivedRequests: [Request] + + "all groups the user is an admin of" + administratedGroups: [Group] + + "all groups the user has created" + createdGroups: [Group] + + "all groups the user has joined" + groups: [Group] + } "represents a single user post" @@ -225,6 +250,26 @@ type ChatMessage { htmlContent: String } +type Group { + "ID of the group" + id: ID! + + "name of the group" + name: String! + + "the creator of the group" + creator: User + + "all admins of the group" + admins: [User]! + + "the members of the group with pagination" + members(first: Int = 10, offset: Int = 0): [User]! + + "the groups chat" + chat: ChatRoom +} + "represents the type of vote performed on a post" enum VoteType { UPVOTE diff --git a/src/lib/dataaccess.ts b/src/lib/dataaccess.ts index e13faca..ec753e4 100644 --- a/src/lib/dataaccess.ts +++ b/src/lib/dataaccess.ts @@ -1,3 +1,4 @@ +import * as crypto from "crypto"; import {Sequelize} from "sequelize-typescript"; import {ChatNotFoundError} from "./errors/ChatNotFoundError"; import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError"; @@ -36,6 +37,9 @@ namespace dataaccess { models.PostVote, models.Request, models.User, + models.Group, + models.GroupAdmin, + models.GroupMember, ]); } catch (err) { globals.logger.error(err.message); @@ -62,6 +66,9 @@ namespace dataaccess { * @param password */ export async function getUserByLogin(email: string, password: string): Promise { + const hash = crypto.createHash("sha512"); + hash.update(password); + password = hash.digest("hex"); const user = await models.User.findOne({where: {email, password}}); if (user) { return user; @@ -77,6 +84,9 @@ namespace dataaccess { * @param password */ export async function registerUser(username: string, email: string, password: string): Promise { + const hash = crypto.createHash("sha512"); + hash.update(password); + password = hash.digest("hex"); const existResult = !!(await models.User.findOne({where: {username, email, password}})); const handle = generateHandle(username); if (!existResult) { @@ -201,6 +211,27 @@ namespace dataaccess { return request; } + /** + * Create a new group. + * @param name + * @param creator + * @param members + */ + export async function createGroup(name: string, creator: number, members: number[]): Promise { + return sequelize.transaction(async (t) => { + members.push(creator); + const groupChat = await createChat(...members); + const group = await models.Group.create({name, creatorId: creator, chatId: groupChat.id}, {transaction: t}); + const creatorUser = await models.User.findByPk(creator, {transaction: t}); + await group.$add("rAdmins", creatorUser, {transaction: t}); + for (const member of members) { + const user = await models.User.findByPk(member, {transaction: t}); + await group.$add("rMembers", user, {transaction: t}); + } + return group; + }); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/lib/errors/graphqlErrors.ts b/src/lib/errors/graphqlErrors.ts index b33a883..c809fa3 100644 --- a/src/lib/errors/graphqlErrors.ts +++ b/src/lib/errors/graphqlErrors.ts @@ -11,3 +11,9 @@ export class PostNotFoundGqlError extends GraphQLError { super(`Post '${postId}' not found!`); } } + +export class GroupNotFoundGqlError extends GraphQLError { + constructor(groupId: number) { + super(`Group '${groupId}' not found!`); + } +} diff --git a/src/lib/models/Group.ts b/src/lib/models/Group.ts new file mode 100644 index 0000000..222a971 --- /dev/null +++ b/src/lib/models/Group.ts @@ -0,0 +1,65 @@ +import * as sqz from "sequelize"; +import { + BelongsTo, + BelongsToMany, + Column, + CreatedAt, ForeignKey, + HasMany, + Model, + NotNull, + Table, + Unique, + UpdatedAt, +} from "sequelize-typescript"; +import {ChatMessage} from "./ChatMessage"; +import {ChatRoom} from "./ChatRoom"; +import {GroupAdmin} from "./GroupAdmin"; +import {GroupMember} from "./GroupMember"; +import {User} from "./User"; + +@Table({underscored: true}) +export class Group extends Model { + @NotNull + @Column( {allowNull: false}) + public name: string; + + @NotNull + @ForeignKey(() => User) + @Column({allowNull: false}) + public creatorId: number; + + @NotNull + @ForeignKey(() => ChatRoom) + @Column({allowNull: false}) + public chatId: number; + + @BelongsTo(() => User, "creatorId") + public rCreator: User; + + @BelongsToMany(() => User, () => GroupAdmin) + public rAdmins: User[]; + + @BelongsToMany(() => User, () => GroupMember) + public rMembers: User[]; + + @BelongsTo(() => ChatRoom) + public rChat: ChatRoom; + + public async creator(): Promise { + return await this.$get("rCreator") as User; + } + + public async admins(): Promise { + return await this.$get("rAdmins") as User[]; + } + + public async members({first, offset}: {first: number, offset: number}): Promise { + const limit = first || 10; + offset = offset || 0; + return await this.$get("rMembers", {limit, offset}) as User[]; + } + + public async chat(): Promise { + return await this.$get("rChat") as ChatRoom; + } +} diff --git a/src/lib/models/GroupAdmin.ts b/src/lib/models/GroupAdmin.ts new file mode 100644 index 0000000..c04c48c --- /dev/null +++ b/src/lib/models/GroupAdmin.ts @@ -0,0 +1,28 @@ +import * as sqz from "sequelize"; +import { + BelongsTo, + BelongsToMany, + Column, + CreatedAt, ForeignKey, + HasMany, + Model, Not, + NotNull, + Table, + Unique, + UpdatedAt, +} from "sequelize-typescript"; +import {Group} from "./Group"; +import {User} from "./User"; + +@Table({underscored: true}) +export class GroupAdmin extends Model { + @NotNull + @ForeignKey(() => User) + @Column({allowNull: false}) + public userId: number; + + @NotNull + @ForeignKey(() => Group) + @Column({allowNull: false}) + public groupId: number; +} diff --git a/src/lib/models/GroupMember.ts b/src/lib/models/GroupMember.ts new file mode 100644 index 0000000..f9ba6c4 --- /dev/null +++ b/src/lib/models/GroupMember.ts @@ -0,0 +1,28 @@ +import * as sqz from "sequelize"; +import { + BelongsTo, + BelongsToMany, + Column, + CreatedAt, ForeignKey, + HasMany, + Model, Not, + NotNull, + Table, + Unique, + UpdatedAt, +} from "sequelize-typescript"; +import {Group} from "./Group"; +import {User} from "./User"; + +@Table({underscored: true}) +export class GroupMember extends Model { + @NotNull + @ForeignKey(() => User) + @Column({allowNull: false}) + public userId: number; + + @NotNull + @ForeignKey(() => Group) + @Column({allowNull: false}) + public groupId: number; +} diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index e9920ef..c79cd5a 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -15,6 +15,9 @@ import {ChatMember} from "./ChatMember"; import {ChatMessage} from "./ChatMessage"; import {ChatRoom} from "./ChatRoom"; import {Friendship} from "./Friendship"; +import {Group} from "./Group"; +import {GroupAdmin} from "./GroupAdmin"; +import {GroupMember} from "./GroupMember"; import {Post} from "./Post"; import {PostVote} from "./PostVote"; import {Request, RequestType} from "./Request"; @@ -52,6 +55,12 @@ export class User extends Model { @BelongsToMany(() => ChatRoom, () => ChatMember) public rChats: ChatRoom[]; + @BelongsToMany(() => Group, () => GroupAdmin) + public rAdministratedGroups: Group[]; + + @BelongsToMany(() => Group, () => GroupMember) + public rGroups: Group[]; + @HasMany(() => Post, "authorId") public rPosts: Post[]; @@ -64,6 +73,9 @@ export class User extends Model { @HasMany(() => ChatMessage, "authorId") public messages: ChatMessage[]; + @HasMany(() => Group, "creatorId") + public rCreatedGroups: Group[]; + @CreatedAt public readonly createdAt!: Date; @@ -102,6 +114,18 @@ export class User extends Model { return this.$count("rPosts"); } + public async administratedGroups(): Promise { + return await this.$get("rAdministratedGroups") as Group[]; + } + + public async createdGroups(): Promise { + return await this.$get("rCreatedGroups") as Group[]; + } + + public async groups(): Promise { + return await this.$get("rGroups") as Group[]; + } + public async denyRequest(sender: number, type: RequestType) { const request = await this.$get("rReceivedRequests", {where: {senderId: sender, requestType: type}}) as Request[]; diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts index 9e47059..0388fbc 100644 --- a/src/lib/models/index.ts +++ b/src/lib/models/index.ts @@ -6,3 +6,6 @@ export {Post} from "./Post"; export {PostVote} from "./PostVote"; export {Request} from "./Request"; export {User} from "./User"; +export {Group} from "./Group"; +export {GroupAdmin} from "./GroupAdmin"; +export {GroupMember} from "./GroupMember";