diff --git a/api/src/middlewares/cors.ts b/api/src/middlewares/cors.ts index de1b665..ea94c37 100644 --- a/api/src/middlewares/cors.ts +++ b/api/src/middlewares/cors.ts @@ -3,6 +3,7 @@ import { app, logger } from ".."; app.use('*', (req: Request, res: Response, next: () => void) => { res.header('Access-Control-Allow-Origin', '*'); - res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, x-auth-user, x-auth-token"); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-auth-user, x-auth-token'); + res.header('Access-Control-Allow-Methods', '*'); next(); }); diff --git a/api/src/routes/dash/server.ts b/api/src/routes/dash/server.ts index 8904e86..9793f02 100644 --- a/api/src/routes/dash/server.ts +++ b/api/src/routes/dash/server.ts @@ -1,16 +1,19 @@ -import { app } from '../..'; +import { app, db } from '../..'; import { Request, Response } from 'express'; -import { badRequest, isAuthenticated, unauthorized } from '../../utils'; +import { badRequest, getPermissionLevel, isAuthenticated, unauthorized } from '../../utils'; import { botReq } from '../internal/ws'; +type User = { id: string, username?: string, avatarURL?: string } + type ServerDetails = { id: string, - perms: 0|1|2, + perms: 0|1|2|3, name: string, description?: string, iconURL?: string, bannerURL?: string, serverConfig: any, + users: User[], } app.get('/dash/server/:server', async (req: Request, res: Response) => { @@ -30,3 +33,122 @@ app.get('/dash/server/:server', async (req: Request, res: Response) => { const s: ServerDetails = response.server; res.send({ server: s }); }); + +app.put('/dash/server/:server/:option', async (req: Request, res: Response) => { + try { + const user = await isAuthenticated(req, res, true); + if (!user) return unauthorized(res); + + const { server } = req.params; + const { item } = req.body; + if (!server || typeof server != 'string' || !item || typeof item != 'string') return badRequest(res); + + const permissionLevelRes = await getPermissionLevel(user, server); + if (!permissionLevelRes.success) + return res.status(permissionLevelRes.statusCode || 500).send({ error: permissionLevelRes.error }); + + const servers = db.get('servers'); + const permissionLevel: 0|1|2|3 = permissionLevelRes.level; + const settings = await servers.findOne({ id: server }); + + switch(req.params.option) { + case 'managers': { + if (permissionLevel < 3) return res.status(403).send({ error: 'You are not allowed to add other bot managers.' }); + + const userRes = await botReq('getUser', { user: item }); + if (!userRes.success) { + return res.status(404).send({ error: 'User could not be found' }); + } + + if (settings.botManagers?.includes(userRes.user.id) === true) { + return res.status(400).send({ error: 'This user is already manager' }); + } + + const newManagers = [ ...(settings.botManagers ?? []), userRes.user.id ]; + await servers.update({ id: server }, { $set: { botManagers: newManagers } }); + res.send({ + success: true, + managers: newManagers, + users: [ userRes.user ], + }); + return; + } + + case 'mods': { + if (permissionLevel < 2) return res.status(403).send({ error: 'You are not allowed to add other moderators.' }); + + const userRes = await botReq('getUser', { user: item }); + if (!userRes.success) { + return res.status(404).send({ error: 'User could not be found' }); + } + + if (settings.moderators?.includes(userRes.user.id) === true) { + return res.status(400).send({ error: 'This user is already moderator' }); + } + + const newMods = [ ...(settings.moderators ?? []), userRes.user.id ]; + await servers.update({ id: server }, { $set: { moderators: newMods } }); + res.send({ + success: true, + mods: newMods, + users: [ userRes.user ], + }); + return; + } + + default: return badRequest(res); + } + } catch(e: any) { + res.status(500).send({ error: e }); + } +}); + +app.delete('/dash/server/:server/:option/:target', async (req: Request, res: Response) => { + const user = await isAuthenticated(req, res, true); + if (!user) return unauthorized(res); + + const { server, target, option } = req.params; + if (!server || typeof server != 'string' || !target || typeof target != 'string') return badRequest(res); + + const permissionLevelRes = await getPermissionLevel(user, server); + if (!permissionLevelRes.success) + return res.status(permissionLevelRes.statusCode || 500).send({ error: permissionLevelRes.error }); + + const servers = db.get('servers'); + const permissionLevel: 0|1|2|3 = permissionLevelRes.level; + const settings = await servers.findOne({ id: server }); + + switch(option) { + case 'managers': { + if (permissionLevel < 3) return res.status(403).send({ error: 'You are not allowed to remove bot managers.' }); + + if (!settings.botManagers?.includes(target)) { + return res.status(400).send({ error: 'This user is not manager' }); + } + + const newManagers = (settings.botManagers ?? []).filter((i: string) => i != target); + await servers.update({ id: server }, { $set: { botManagers: newManagers } }); + res.send({ + success: true, + managers: newManagers, + }); + return; + } + case 'mods': { + if (permissionLevel < 2) return res.status(403).send({ error: 'You are not allowed to remove moderators.' }); + + if (!settings.moderators?.includes(target)) { + return res.status(400).send({ error: 'This user is not moderator' }); + } + + const newMods = (settings.moderators ?? []).filter((i: string) => i != target); + await servers.update({ id: server }, { $set: { moderators: newMods } }); + res.send({ + success: true, + mods: newMods, + }); + return; + } + default: return badRequest(res); + } +}); diff --git a/api/src/routes/dash/servers.ts b/api/src/routes/dash/servers.ts index 025d0fe..4504c8c 100644 --- a/api/src/routes/dash/servers.ts +++ b/api/src/routes/dash/servers.ts @@ -3,7 +3,7 @@ import { Request, Response } from 'express'; import { isAuthenticated, unauthorized } from '../../utils'; import { botReq } from '../internal/ws'; -type Server = { id: string, perms: 0|1|2, name: string, iconURL?: string, bannerURL?: string } +type Server = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string } app.get('/dash/servers', async (req: Request, res: Response) => { const user = await isAuthenticated(req, res, true); diff --git a/api/src/utils.ts b/api/src/utils.ts index ea389ae..de42593 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; import { FindOneResult } from "monk"; import { db } from "."; +import { botReq } from "./routes/internal/ws"; class Session { user: string; @@ -44,4 +45,8 @@ function unauthorized(res: Response) { res.status(401).send(JSON.stringify({ "error": "Unauthorized" }, null, 4)); } -export { isAuthenticated, getSessionInfo, badRequest, unauthorized } +async function getPermissionLevel(user: string, server: string) { + return await botReq('getPermissionLevel', { user, server }); +} + +export { isAuthenticated, getSessionInfo, badRequest, unauthorized, getPermissionLevel } diff --git a/bot/src/bot/modules/api/server_details.ts b/bot/src/bot/modules/api/server_details.ts index 965b574..7d2545b 100644 --- a/bot/src/bot/modules/api/server_details.ts +++ b/bot/src/bot/modules/api/server_details.ts @@ -1,21 +1,22 @@ import { Member } from "revolt.js/dist/maps/Members"; import { User } from "revolt.js/dist/maps/Users"; import { client } from "../../.."; -import AutomodSettings from "../../../struct/antispam/AutomodSettings"; import ServerConfig from "../../../struct/ServerConfig"; import { getPermissionLevel } from "../../util"; import { wsEvents, WSResponse } from "../api_communication"; type ReqData = { user: string, server: string } +type APIUser = { id: string, username?: string, avatarURL?: string } type ServerDetails = { id: string, - perms: 0|1|2, + perms: 0|1|2|3, name: string, description?: string, iconURL?: string, bannerURL?: string, serverConfig?: ServerConfig, + users: APIUser[], } wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSResponse) => void) => { @@ -43,6 +44,20 @@ wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSRespo // todo: remove unwanted keys from server config + async function fetchUser(id: string) { + try { + return client.users.get(id) || await client.users.fetch(id); + } catch(e) { + throw id; // this is stupid but idc + } + } + + const users = await Promise.allSettled([ + ...(serverConfig.botManagers?.map(u => fetchUser(u)) ?? []), + ...(serverConfig.moderators?.map(u => fetchUser(u)) ?? []), + fetchUser(user._id), + ]); + const response: ServerDetails = { id: server._id, name: server.name, @@ -51,6 +66,11 @@ wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSRespo bannerURL: server.generateBannerURL(), iconURL: server.generateIconURL(), serverConfig, + users: users.map( + u => u.status == 'fulfilled' + ? { id: u.value._id, avatarURL: u.value.generateAvatarURL(), username: u.value.username } + : { id: u.reason } + ), } cb({ success: true, server: response }); @@ -59,3 +79,5 @@ wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSRespo cb({ success: false, error: `${e}` }); } }); + +export { APIUser } diff --git a/bot/src/bot/modules/api/servers.ts b/bot/src/bot/modules/api/servers.ts index b699e37..84651ed 100644 --- a/bot/src/bot/modules/api/servers.ts +++ b/bot/src/bot/modules/api/servers.ts @@ -17,7 +17,7 @@ wsEvents.on('req:getUserServers', async (data: ReqData, cb: (data: WSResponse) = const mutuals = await user.fetchMutual(); - type ServerResponse = { id: string, perms: 0|1|2, name: string, iconURL?: string, bannerURL?: string } + type ServerResponse = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string } const promises: Promise[] = []; diff --git a/bot/src/bot/modules/api/users.ts b/bot/src/bot/modules/api/users.ts new file mode 100644 index 0000000..4501427 --- /dev/null +++ b/bot/src/bot/modules/api/users.ts @@ -0,0 +1,38 @@ +import { User } from "revolt.js/dist/maps/Users"; +import { client } from "../../.."; +import { getPermissionLevel, parseUser } from "../../util"; +import { wsEvents, WSResponse } from "../api_communication"; +import { APIUser } from "./server_details"; + +wsEvents.on('req:getPermissionLevel', async (data: { user: string, server: string }, cb: (data: WSResponse) => void) => { + try { + const server = client.servers.get(data.server); + if (!server) return cb({ success: false, error: 'The requested server could not be found', statusCode: 404 }); + + let user: User; + try { + user = client.users.get(data.user) || await client.users.fetch(data.user); + } catch(e) { + cb({ success: false, error: 'The requested user could not be found', statusCode: 404 }); + return; + } + + return cb({ success: true, level: await getPermissionLevel(user, server) }) + } catch(e) { + console.error(e); + cb({ success: false, error: `${e}` }); + } +}); + +wsEvents.on('req:getUser', async (data: { user: string }, cb: (data: WSResponse) => void) => { + try { + const user = await parseUser(data.user); + if (!user) + cb({ success: false, statusCode: 404, error: 'User could not be found' }); + else + cb({ success: true, user: { id: user._id, username: user.username, avatarURL: user.generateAvatarURL() } as APIUser }); + } catch(e) { + console.error(e); + cb({ success: false, error: `${e}` }); + } +}); diff --git a/bot/src/bot/modules/api_communication.ts b/bot/src/bot/modules/api_communication.ts index 34a17b0..65e8522 100644 --- a/bot/src/bot/modules/api_communication.ts +++ b/bot/src/bot/modules/api_communication.ts @@ -135,3 +135,4 @@ export { wsEvents, wsSend, WSResponse } import('./api/servers'); import('./api/server_details'); +import('./api/users'); diff --git a/bot/src/bot/util.ts b/bot/src/bot/util.ts index 9995717..3e97c0f 100644 --- a/bot/src/bot/util.ts +++ b/bot/src/bot/util.ts @@ -99,13 +99,13 @@ async function checkSudoPermission(message: Message): Promise { return true; } } -async function getPermissionLevel(user: User|Member, server: Server): Promise<0|1|2> { +async function getPermissionLevel(user: User|Member, server: Server): Promise<0|1|2|3> { if (isSudo(user instanceof User ? user : (user.user || await client.users.fetch(user._id.user)))) return 2; const member = user instanceof User ? await server.fetchMember(user) : user; if (user instanceof Member) user = user.user!; - if (hasPerm(member, 'ManageServer')) return 2; + if (hasPerm(member, 'ManageServer')) return 3; if (hasPerm(member, 'KickMembers')) return 1; const config = (await client.db.get('servers').findOne({ id: server._id }) || {}) as ServerConfig; diff --git a/web/package.json b/web/package.json index 01835ba..43ddd1e 100644 --- a/web/package.json +++ b/web/package.json @@ -7,6 +7,8 @@ "preview": "vite preview" }, "dependencies": { + "@mdi/js": "^6.5.95", + "@mdi/react": "^1.5.0", "@revoltchat/ui": "^1.0.24", "@types/axios": "^0.14.0", "@types/core-js": "^2.5.5", @@ -14,6 +16,7 @@ "axios": "^0.25.0", "core-js": "^3.20.3", "localforage": "^1.10.0", + "prop-types": "^15.8.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^6.2.1", diff --git a/web/src/pages/DashboardHome.tsx b/web/src/pages/DashboardHome.tsx index 4246e7e..bc7c280 100644 --- a/web/src/pages/DashboardHome.tsx +++ b/web/src/pages/DashboardHome.tsx @@ -7,7 +7,7 @@ import { H2 } from '@revoltchat/ui/lib/components/atoms/heading/H2'; import { API_URL } from "../App"; import { getAuthHeaders } from "../utils"; -type Server = { id: string, perms: 0|1|2, name: string, iconURL?: string, bannerURL?: string } +type Server = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string } function permissionName(p: number) { switch(p) { diff --git a/web/src/pages/ServerDashboard.tsx b/web/src/pages/ServerDashboard.tsx index a74cd54..322a21c 100644 --- a/web/src/pages/ServerDashboard.tsx +++ b/web/src/pages/ServerDashboard.tsx @@ -1,17 +1,30 @@ -import localforage from "localforage"; import axios from 'axios'; import { FunctionComponent, useCallback, useEffect, useState } from "react"; import { Button } from '@revoltchat/ui/lib/components/atoms/inputs/Button'; import { InputBox } from '@revoltchat/ui/lib/components/atoms/inputs/InputBox'; import { Checkbox } from '@revoltchat/ui/lib/components/atoms/inputs/Checkbox'; +import { LineDivider } from '@revoltchat/ui/lib/components/atoms/layout/LineDivider'; import { H1 } from '@revoltchat/ui/lib/components/atoms/heading/H1'; import { H3 } from '@revoltchat/ui/lib/components/atoms/heading/H3'; import { H4 } from '@revoltchat/ui/lib/components/atoms/heading/H4'; +import { Icon } from '@mdi/react'; +import { mdiCloseBox } from '@mdi/js'; import { API_URL } from "../App"; import { getAuthHeaders } from "../utils"; import { useParams } from "react-router-dom"; -type Server = { id?: string, perms?: 0|1|2, name?: string, description?: string, iconURL?: string, bannerURL?: string, serverConfig?: any } +type User = { id: string, username?: string, avatarURL?: string } + +type Server = { + id?: string, + perms?: 0|1|2|3, + name?: string, + description?: string, + iconURL?: string, + bannerURL?: string, + serverConfig?: { [key: string]: any }, + users: User[], +} const ServerDashboard: FunctionComponent = () => { const [serverInfo, setServerInfo] = useState({} as Server); @@ -19,6 +32,9 @@ const ServerDashboard: FunctionComponent = () => { const [prefix, setPrefix] = useState('' as string|undefined); const [prefixAllowSpace, setPrefixAllowSpace] = useState(false); + + const [botManagers, setBotManagers] = useState([] as string[]); + const [moderators, setModerators] = useState([] as string[]); const { serverid } = useParams(); @@ -30,11 +46,15 @@ const ServerDashboard: FunctionComponent = () => { try { const res = await axios.get(`${API_URL}/dash/server/${serverid}`, { headers: await getAuthHeaders() }); console.log(res.data); + const server: Server = res.data.server; setServerInfo(server); - setPrefix(server.serverConfig?.prefix || undefined); + setPrefix(server.serverConfig?.prefix || ''); setPrefixAllowSpace(!!server.serverConfig?.spaceAfterPrefix); + + setBotManagers(server.serverConfig?.botManagers ?? []); + setModerators(server.serverConfig?.moderators ?? []); } catch(e: any) { console.error(e); setStatus(`${e?.message ?? e}`); @@ -50,7 +70,7 @@ const ServerDashboard: FunctionComponent = () => {