Skip to content

Commit

Permalink
feat: add members to group
Browse files Browse the repository at this point in the history
  • Loading branch information
aseerkt committed Jul 7, 2024
1 parent 22b2e17 commit ba31168
Show file tree
Hide file tree
Showing 31 changed files with 686 additions and 63 deletions.
2 changes: 1 addition & 1 deletion server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const createApp = async () => {
app.set('io', io)

app.get('/', (_, res) => {
res.send('<h1>Welcome to mChat</h1>')
res.send('<h1>Welcome to mChat API</h1>')
})

app.use('/api/users', routes.userRoutes)
Expand Down
3 changes: 2 additions & 1 deletion server/src/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { verifyToken } from './utils/jwt'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
console.error(err)
res.status(500).json({
message: 'Something went wrong',
error: config.isProd ? undefined : err,
error: config.isProd ? undefined : err.message,
})
}

Expand Down
114 changes: 74 additions & 40 deletions server/src/modules/groups/groups.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { db } from '@/database'
import { withPagination } from '@/database/helpers'
import {
deleteGroupMembersRoles,
getUserSockets,
setMemberRolesForAGroup,
} from '@/redis/handlers'
import { deleteGroupMembersRoles } from '@/redis/handlers'
import { TypedIOServer } from '@/socket/socket.inteface'
import { notFound } from '@/utils/api'
import { eq, getTableColumns, notInArray } from 'drizzle-orm'
import { and, eq, getTableColumns, like, notInArray } from 'drizzle-orm'
import { RequestHandler } from 'express'
import { MemberRole, NewMember, members } from '../members/members.schema'
import { members } from '../members/members.schema'
import { addMembers } from '../members/members.service'
import { messages } from '../messages/messages.schema'
import { users } from '../users/users.schema'
import { groups } from './groups.schema'

// CREATE
Expand All @@ -25,39 +23,12 @@ export const createGroup: RequestHandler = async (req, res, next) => {

const { memberIds = [] } = req.body

const memberValues: NewMember[] = (memberIds as number[]).map(mid => ({
groupId: group.id,
userId: mid,
}))

memberValues.push({
groupId: group.id,
userId: req.user!.id,
role: 'owner',
})

const newMembers = await tx
.insert(members)
.values(memberValues)
.returning({ userId: members.userId, role: members.role })

const userIds: number[] = []
const memberRoles: Record<string, MemberRole> = {}

newMembers.forEach(member => {
if (member.userId !== req.user?.id) {
userIds.push(member.userId)
}
memberRoles[member.userId] = member.role
})

setMemberRolesForAGroup(group.id, memberRoles)

const userSockets = await getUserSockets(userIds)

const io = req.app.get('io') as TypedIOServer

io.to(userSockets).emit('newGroup', group)
await addMembers(
tx,
req.app.get('io'),
group,
memberIds.concat(req.user!.id),
)

return group
})
Expand Down Expand Up @@ -133,6 +104,40 @@ export const listUserGroups: RequestHandler = async (req, res, next) => {
}
}

// UPDATE

export const addGroupMembers: RequestHandler = async (req, res, next) => {
try {
const groupId = Number(req.params.groupId)
const [group] = await db
.select()
.from(groups)
.where(eq(groups.id, groupId))
.limit(1)

if (!group) {
return notFound(res, 'Group')
}

const newMembers = await addMembers(
db,
req.app.get('io'),
group,
req.body.memberIds || [],
)

const io = req.app.get('io') as TypedIOServer

io.to(req.params.groupId).emit('newMembers', newMembers)

// let existing members know new member is joined in member list

res.json(newMembers)
} catch (error) {
next(error)
}
}

// DELETE

export const deleteGroup: RequestHandler = async (req, res, next) => {
Expand All @@ -154,3 +159,32 @@ export const deleteGroup: RequestHandler = async (req, res, next) => {
next(error)
}
}

export const getNonGroupMembers: RequestHandler = async (req, res, next) => {
try {
const groupMembers = await db
.select({ userId: members.userId })
.from(members)
.where(eq(members.groupId, Number(req.params.groupId)))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...columns } = getTableColumns(users)
const rows = await db
.select(columns)
.from(users)
.where(
and(
like(users.username, `%${req.query.query}%`),
notInArray(
users.id,
groupMembers.map(m => m.userId),
),
),
)
.limit(Number(req.query.limit) || 5)
.orderBy(users.username)

res.json(rows)
} catch (error) {
next(error)
}
}
8 changes: 8 additions & 0 deletions server/src/modules/groups/groups.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { Router } from 'express'
import { getGroupMembers } from '../members/members.controller'
import { createMessage, listMessages } from '../messages/messages.controller'
import {
addGroupMembers,
createGroup,
deleteGroup,
getGroup,
getNonGroupMembers,
listGroups,
} from './groups.controller'

Expand All @@ -17,7 +19,13 @@ router.get('/', listGroups)
router.get('/:groupId', hasGroupPermission('member'), getGroup)
router.delete('/:groupId', hasGroupPermission('owner'), deleteGroup)

router.post('/:groupId/members', hasGroupPermission('admin'), addGroupMembers)
router.get('/:groupId/members', hasGroupPermission('member'), getGroupMembers)
router.get(
'/:groupId/non-members',
hasGroupPermission('admin'),
getNonGroupMembers,
)

router.get('/:groupId/messages', hasGroupPermission('member'), listMessages)
router.post('/:groupId/messages', hasGroupPermission('member'), createMessage)
2 changes: 1 addition & 1 deletion server/src/modules/members/members.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { RequestHandler } from 'express'
import { users } from '../users/users.schema'
import { MemberRole, members } from './members.schema'

export const createMembers: RequestHandler = async (req, res, next) => {
export const joinRooms: RequestHandler = async (req, res, next) => {
try {
const { groupIds } = req.body
if (!groupIds?.length) {
Expand Down
4 changes: 2 additions & 2 deletions server/src/modules/members/members.routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from 'express'
import { createMembers } from './members.controller'
import { joinRooms } from './members.controller'

export const router = Router()

router.post('/', createMembers)
router.post('/', joinRooms)
10 changes: 8 additions & 2 deletions server/src/modules/members/members.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { commonSchemaFields } from '@/database/helpers'
import { bigint, index, pgEnum, pgTable, unique } from 'drizzle-orm/pg-core'
import { groups } from '../groups/groups.schema'
import { users } from '../users/users.schema'

// order of roles shows auth precedence
export const memberRoleEnum = pgEnum('member_role', [
Expand All @@ -12,8 +14,12 @@ export const members = pgTable(
'members',
{
...commonSchemaFields,
userId: bigint('user_id', { mode: 'number' }).notNull(),
groupId: bigint('group_id', { mode: 'number' }).notNull(),
userId: bigint('user_id', { mode: 'number' })
.notNull()
.references(() => users.id),
groupId: bigint('group_id', { mode: 'number' })
.notNull()
.references(() => groups.id),
role: memberRoleEnum('role').notNull().default('member'),
},
table => ({
Expand Down
44 changes: 42 additions & 2 deletions server/src/modules/members/members.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { db } from '@/database'
import { getMemberRole, setMemberRolesForAGroup } from '@/redis/handlers'
import {
getMemberRole,
getUserSockets,
setMemberRolesForAGroup,
} from '@/redis/handlers'
import { TypedIOServer } from '@/socket/socket.inteface'
import { and, eq } from 'drizzle-orm'
import { MemberRole, memberRoles, members } from './members.schema'
import { NodePgDatabase } from 'drizzle-orm/node-postgres'
import { Group } from '../groups/groups.schema'
import { MemberRole, NewMember, memberRoles, members } from './members.schema'

export const checkPermission = async (
groupId: number,
Expand All @@ -27,3 +34,36 @@ export const checkPermission = async (

return memberRoles.indexOf(memberRole) >= memberRoles.indexOf(role)
}

export const addMembers = async (
db: NodePgDatabase,
io: TypedIOServer,
group: Group,
memberIds: number[],
) => {
const memberValues: NewMember[] = memberIds.map(mid => ({
groupId: group.id,
userId: mid,
role: mid === group.ownerId ? 'owner' : 'member',
}))

const newMembers = await db.insert(members).values(memberValues).returning()

const userIds: number[] = []
const memberRoles: Record<string, MemberRole> = {}

newMembers.forEach(member => {
if (member.userId !== group.ownerId) {
userIds.push(member.userId)
}
memberRoles[member.userId] = member.role
})

setMemberRolesForAGroup(group.id, memberRoles)

const userSockets = await getUserSockets(userIds)

io.to(userSockets).emit('newGroup', group)

return newMembers
}
24 changes: 23 additions & 1 deletion server/src/modules/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { db } from '@/database'
import { signToken } from '@/utils/jwt'
import { removeAttrFromObject } from '@/utils/object'
import { hash, verify } from 'argon2'
import { eq } from 'drizzle-orm'
import { and, eq, getTableColumns, like, ne } from 'drizzle-orm'
import { RequestHandler } from 'express'
import { users } from './users.schema'

Expand Down Expand Up @@ -73,3 +73,25 @@ export const loginUser: RequestHandler = async (req, res, next) => {
next(error)
}
}

export const getUsers: RequestHandler = async (req, res, next) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...columns } = getTableColumns(users)
const rows = await db
.select(columns)
.from(users)
.where(
and(
like(users.username, `%${req.query.query}%`),
ne(users.id, req.user!.id),
),
)
.limit(Number(req.query.limit) || 5)
.orderBy(users.username)

res.json(rows)
} catch (error) {
next(error)
}
}
5 changes: 3 additions & 2 deletions server/src/modules/users/users.routes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { auth } from '@/middlewares'
import { Router } from 'express'
import { listUserGroups } from '../groups/groups.controller'
import { loginUser, signUpUser } from './users.controller'
import { getUsers, loginUser, signUpUser } from './users.controller'

export const router = Router()

router.post('/', signUpUser)
router.post('/login', loginUser)
router.get('/', auth, getUsers)

router.post('/login', loginUser)
router.get('/:userId/groups', auth, listUserGroups)
1 change: 1 addition & 0 deletions server/src/redis/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,6 @@ export const removeUserSocket = async (userId: number, socketId: string) => {
}

export const getUserSockets = async (userIds: number[]) => {
if (!userIds.length) return []
return redisClient.sunion(userIds.map(uid => redisKeys.SOCKET_MAP(uid)))
}
1 change: 1 addition & 0 deletions server/src/socket/socket.inteface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ServerToClientEvents {
userOffline: (userId: number) => void
newMessage: (message: Message & { username: string }) => void
newMember: (member: Member & { username: string }) => void
newMembers: (member: Member[]) => void
newGroup: (group: Group) => void
typingUsers: (users: { id: number; username: string }[]) => void
}
Expand Down
Loading

0 comments on commit ba31168

Please sign in to comment.