1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-09 12:12:35 +01:00

initial progress for admin api

This commit is contained in:
Puyodead1 2023-12-20 03:33:28 -05:00
parent 7e60f8b998
commit 57fb5d7b30
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
23 changed files with 5274 additions and 43 deletions

View File

@ -5219,6 +5219,9 @@
}, },
"rules_channel_id": { "rules_channel_id": {
"type": "string" "type": "string"
},
"owner_id": {
"type": "string"
} }
} }
}, },
@ -5295,6 +5298,9 @@
"type": "string", "type": "string",
"nullable": true "nullable": true
}, },
"owner_id": {
"type": "string"
},
"region": { "region": {
"type": "string" "type": "string"
}, },
@ -7040,6 +7046,12 @@
} }
] ]
}, },
"AdminGuildsResponse": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Guild"
}
},
"BackupCodesChallengeResponse": { "BackupCodesChallengeResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7859,6 +7871,9 @@
}, },
"disabled": { "disabled": {
"type": "boolean" "type": "boolean"
},
"rights": {
"type": "string"
} }
}, },
"required": [ "required": [
@ -7876,6 +7891,7 @@
"premium_usage_flags", "premium_usage_flags",
"public_flags", "public_flags",
"purchased_flags", "purchased_flags",
"rights",
"username", "username",
"verified" "verified"
] ]
@ -7982,6 +7998,9 @@
}, },
"disabled": { "disabled": {
"type": "boolean" "type": "boolean"
},
"rights": {
"type": "string"
} }
}, },
"required": [ "required": [
@ -7999,6 +8018,7 @@
"premium_usage_flags", "premium_usage_flags",
"public_flags", "public_flags",
"purchased_flags", "purchased_flags",
"rights",
"username", "username",
"verified" "verified"
] ]
@ -9008,6 +9028,71 @@
] ]
} }
}, },
"/users/": {
"get": {
"x-right-required": "OPERATOR",
"security": [
{
"bearer": []
}
],
"description": "Get a list of users",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUsersResponse"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
}
},
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "number"
},
"description": "max number of users to return (1-1000). default 100"
},
{
"name": "after",
"in": "query",
"required": false,
"schema": {
"type": "number"
},
"description": "The amount of users to skip"
},
{
"name": "query",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "The search query"
}
],
"tags": [
"users"
]
}
},
"/users/@me/settings/": { "/users/@me/settings/": {
"get": { "get": {
"security": [ "security": [
@ -10945,6 +11030,12 @@
}, },
"tags": [ "tags": [
"scheduled-maintenances" "scheduled-maintenances"
],
"x-badges": [
{
"label": "Spacebar-only",
"color": "red"
}
] ]
} }
}, },
@ -11577,8 +11668,70 @@
} }
}, },
"/guilds/": { "/guilds/": {
"get": {
"x-right-required": "OPERATOR",
"security": [
{
"bearer": []
}
],
"description": "Get a list of guilds",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminGuildsResponse"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
}
},
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "number"
},
"description": "max number of guilds to return (1-1000). default 100"
},
{
"name": "after",
"in": "query",
"required": false,
"schema": {
"type": "number"
},
"description": "The amount of guilds to skip"
},
{
"name": "query",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "The search query"
}
],
"tags": [
"guilds"
]
},
"post": { "post": {
"x-right-required": "CREATE_GUILDS",
"security": [ "security": [
{ {
"bearer": [] "bearer": []
@ -12787,6 +12940,7 @@
] ]
}, },
"post": { "post": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_ROLES", "x-permission-required": "MANAGE_ROLES",
"security": [ "security": [
{ {
@ -12917,6 +13071,7 @@
}, },
"/guilds/{guild_id}/roles/{role_id}/members/": { "/guilds/{guild_id}/roles/{role_id}/members/": {
"patch": { "patch": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_ROLES", "x-permission-required": "MANAGE_ROLES",
"security": [ "security": [
{ {
@ -13054,6 +13209,7 @@
] ]
}, },
"delete": { "delete": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_ROLES", "x-permission-required": "MANAGE_ROLES",
"security": [ "security": [
{ {
@ -13120,6 +13276,7 @@
] ]
}, },
"patch": { "patch": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_ROLES", "x-permission-required": "MANAGE_ROLES",
"security": [ "security": [
{ {
@ -13547,6 +13704,7 @@
}, },
"/guilds/{guild_id}/members/{member_id}/roles/{role_id}/": { "/guilds/{guild_id}/members/{member_id}/roles/{role_id}/": {
"delete": { "delete": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_ROLES", "x-permission-required": "MANAGE_ROLES",
"security": [ "security": [
{ {
@ -13602,6 +13760,7 @@
] ]
}, },
"put": { "put": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_ROLES", "x-permission-required": "MANAGE_ROLES",
"security": [ "security": [
{ {
@ -14124,6 +14283,7 @@
] ]
}, },
"patch": { "patch": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_GUILD", "x-permission-required": "MANAGE_GUILD",
"security": [ "security": [
{ {
@ -14598,6 +14758,7 @@
] ]
}, },
"post": { "post": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_CHANNELS", "x-permission-required": "MANAGE_CHANNELS",
"security": [ "security": [
{ {
@ -14662,6 +14823,7 @@
] ]
}, },
"patch": { "patch": {
"x-right-required": "OPERATOR",
"x-permission-required": "MANAGE_CHANNELS", "x-permission-required": "MANAGE_CHANNELS",
"security": [ "security": [
{ {
@ -14790,6 +14952,7 @@
}, },
"/guilds/{guild_id}/bans/": { "/guilds/{guild_id}/bans/": {
"get": { "get": {
"x-right-required": "OPERATOR",
"x-permission-required": "BAN_MEMBERS", "x-permission-required": "BAN_MEMBERS",
"security": [ "security": [
{ {
@ -14836,6 +14999,7 @@
}, },
"/guilds/{guild_id}/bans/{user_id}": { "/guilds/{guild_id}/bans/{user_id}": {
"get": { "get": {
"x-right-required": "OPERATOR",
"x-permission-required": "BAN_MEMBERS", "x-permission-required": "BAN_MEMBERS",
"security": [ "security": [
{ {
@ -14899,6 +15063,7 @@
] ]
}, },
"put": { "put": {
"x-right-required": "OPERATOR",
"x-permission-required": "BAN_MEMBERS", "x-permission-required": "BAN_MEMBERS",
"security": [ "security": [
{ {
@ -14972,6 +15137,7 @@
] ]
}, },
"delete": { "delete": {
"x-right-required": "OPERATOR",
"x-permission-required": "BAN_MEMBERS", "x-permission-required": "BAN_MEMBERS",
"security": [ "security": [
{ {

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,8 @@ declare global {
user_bot: boolean; user_bot: boolean;
token: { id: string; iat: number }; token: { id: string; iat: number };
rights: Rights; rights: Rights;
has_permission?: boolean;
has_right?: boolean;
} }
} }
} }

View File

@ -38,6 +38,7 @@ router.get(
"/", "/",
route({ route({
permission: "BAN_MEMBERS", permission: "BAN_MEMBERS",
right: "OPERATOR",
responses: { responses: {
200: { 200: {
body: "GuildBansResponse", body: "GuildBansResponse",
@ -84,6 +85,7 @@ router.get(
"/:user_id", "/:user_id",
route({ route({
permission: "BAN_MEMBERS", permission: "BAN_MEMBERS",
right: "OPERATOR",
responses: { responses: {
200: { 200: {
body: "BanModeratorSchema", body: "BanModeratorSchema",
@ -120,6 +122,7 @@ router.put(
route({ route({
requestBody: "BanCreateSchema", requestBody: "BanCreateSchema",
permission: "BAN_MEMBERS", permission: "BAN_MEMBERS",
right: "OPERATOR",
responses: { responses: {
200: { 200: {
body: "Ban", body: "Ban",
@ -185,6 +188,7 @@ router.delete(
"/:user_id", "/:user_id",
route({ route({
permission: "BAN_MEMBERS", permission: "BAN_MEMBERS",
right: "OPERATOR",
responses: { responses: {
204: {}, 204: {},
403: { 403: {
@ -198,13 +202,12 @@ router.delete(
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, user_id } = req.params; const { guild_id, user_id } = req.params;
await Ban.findOneOrFail({
where: { guild_id: guild_id, user_id: user_id },
});
const banned_user = await User.getPublicUser(user_id); const banned_user = await User.getPublicUser(user_id);
await Promise.all([ await Promise.all([
Ban.findOneOrFail({
where: { guild_id: guild_id, user_id: user_id },
}),
Ban.delete({ Ban.delete({
user_id: user_id, user_id: user_id,
guild_id, guild_id,

View File

@ -59,6 +59,7 @@ router.post(
route({ route({
requestBody: "ChannelModifySchema", requestBody: "ChannelModifySchema",
permission: "MANAGE_CHANNELS", permission: "MANAGE_CHANNELS",
right: "OPERATOR",
responses: { responses: {
201: { 201: {
body: "Channel", body: "Channel",
@ -95,6 +96,7 @@ router.patch(
route({ route({
requestBody: "ChannelReorderSchema", requestBody: "ChannelReorderSchema",
permission: "MANAGE_CHANNELS", permission: "MANAGE_CHANNELS",
right: "OPERATOR",
responses: { responses: {
204: {}, 204: {},
400: { 400: {

View File

@ -17,7 +17,7 @@
*/ */
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Guild, GuildDeleteEvent, emitEvent } from "@spacebar/util"; import { Guild, GuildDeleteEvent, emitEvent, getRights } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -40,12 +40,13 @@ router.post(
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const rights = await getRights(req.user_id);
const guild = await Guild.findOneOrFail({ const guild = await Guild.findOneOrFail({
where: { id: guild_id }, where: { id: guild_id },
select: ["owner_id"], select: ["owner_id"],
}); });
if (guild.owner_id !== req.user_id) if (!rights.has("OPERATOR") || guild.owner_id !== req.user_id)
throw new HTTPError("You are not the owner of this guild", 401); throw new HTTPError("You are not the owner of this guild", 401);
await Promise.all([ await Promise.all([

View File

@ -19,7 +19,6 @@
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
Channel, Channel,
DiscordApiErrors,
Guild, Guild,
GuildUpdateEvent, GuildUpdateEvent,
GuildUpdateSchema, GuildUpdateSchema,
@ -27,7 +26,6 @@ import {
Permissions, Permissions,
SpacebarApiErrors, SpacebarApiErrors,
emitEvent, emitEvent,
getPermission,
getRights, getRights,
handleFile, handleFile,
} from "@spacebar/util"; } from "@spacebar/util";
@ -53,12 +51,13 @@ router.get(
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const rights = await getRights(req.user_id);
const [guild, member] = await Promise.all([ const [guild, member] = await Promise.all([
Guild.findOneOrFail({ where: { id: guild_id } }), Guild.findOneOrFail({ where: { id: guild_id } }),
Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }), Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
]); ]);
if (!member) if (!rights.has("OPERATOR") || !member)
throw new HTTPError( throw new HTTPError(
"You are not a member of the guild you are trying to access", "You are not a member of the guild you are trying to access",
401, 401,
@ -76,6 +75,7 @@ router.patch(
route({ route({
requestBody: "GuildUpdateSchema", requestBody: "GuildUpdateSchema",
permission: "MANAGE_GUILD", permission: "MANAGE_GUILD",
right: "OPERATOR",
responses: { responses: {
200: { 200: {
body: "GuildCreateResponse", body: "GuildCreateResponse",
@ -95,14 +95,6 @@ router.patch(
const body = req.body as GuildUpdateSchema; const body = req.body as GuildUpdateSchema;
const { guild_id } = req.params; const { guild_id } = req.params;
const rights = await getRights(req.user_id);
const permission = await getPermission(req.user_id, guild_id);
if (!rights.has("MANAGE_GUILDS") && !permission.has("MANAGE_GUILD"))
throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(
"MANAGE_GUILDS",
);
const guild = await Guild.findOneOrFail({ const guild = await Guild.findOneOrFail({
where: { id: guild_id }, where: { id: guild_id },
relations: ["emojis", "roles", "stickers"], relations: ["emojis", "roles", "stickers"],

View File

@ -17,7 +17,12 @@
*/ */
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { getPermission, Member, PermissionResolvable } from "@spacebar/util"; import {
getPermission,
getRights,
Member,
PermissionResolvable,
} from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
@ -38,14 +43,18 @@ router.patch(
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const rights = await getRights(req.user_id);
let permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; let permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
const member_id = const member_id =
req.params.member_id === "@me" req.params.member_id === "@me"
? ((permissionString = "CHANGE_NICKNAME"), req.user_id) ? ((permissionString = "CHANGE_NICKNAME"), req.user_id)
: req.params.member_id; : req.params.member_id;
const perms = await getPermission(req.user_id, guild_id); // admins dont need to be in the guild
perms.hasThrow(permissionString); if (member_id !== "@me" && !rights.has("OPERATOR")) {
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow(permissionString);
}
await Member.changeNickname(member_id, guild_id, req.body.nick); await Member.changeNickname(member_id, guild_id, req.body.nick);
res.status(200).send(); res.status(200).send();

View File

@ -26,6 +26,7 @@ router.delete(
"/", "/",
route({ route({
permission: "MANAGE_ROLES", permission: "MANAGE_ROLES",
right: "OPERATOR",
responses: { responses: {
204: {}, 204: {},
403: { 403: {
@ -45,6 +46,7 @@ router.put(
"/", "/",
route({ route({
permission: "MANAGE_ROLES", permission: "MANAGE_ROLES",
right: "OPERATOR",
responses: { responses: {
204: {}, 204: {},
403: {}, 403: {},

View File

@ -17,7 +17,7 @@
*/ */
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Member, PublicMemberProjection } from "@spacebar/util"; import { Member, PublicMemberProjection, getRights } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { MoreThan } from "typeorm"; import { MoreThan } from "typeorm";
@ -51,13 +51,15 @@ router.get(
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const rights = await getRights(req.user_id);
const limit = Number(req.query.limit) || 1; const limit = Number(req.query.limit) || 1;
if (limit > 1000 || limit < 1) if (limit > 1000 || limit < 1)
throw new HTTPError("Limit must be between 1 and 1000"); throw new HTTPError("Limit must be between 1 and 1000");
const after = `${req.query.after}`; const after = `${req.query.after}`;
const query = after ? { id: MoreThan(after) } : {}; const query = after ? { id: MoreThan(after) } : {};
await Member.IsInGuildOrFail(req.user_id, guild_id); if (!rights.has("OPERATOR"))
await Member.IsInGuildOrFail(req.user_id, guild_id);
const members = await Member.find({ const members = await Member.find({
where: { guild_id, ...query }, where: { guild_id, ...query },

View File

@ -19,7 +19,13 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Channel, FieldErrors, Message, getPermission } from "@spacebar/util"; import {
Channel,
FieldErrors,
Message,
getPermission,
getRights,
} from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { FindManyOptions, In, Like } from "typeorm"; import { FindManyOptions, In, Like } from "typeorm";
@ -53,6 +59,7 @@ router.get(
author_id, author_id,
} = req.query; } = req.query;
const rights = await getRights(req.user_id);
const parsedLimit = Number(limit) || 50; const parsedLimit = Number(limit) || 50;
if (parsedLimit < 1 || parsedLimit > 100) if (parsedLimit < 1 || parsedLimit > 100)
throw new HTTPError("limit must be between 1 and 100", 422); throw new HTTPError("limit must be between 1 and 100", 422);
@ -75,7 +82,7 @@ router.get(
req.params.guild_id, req.params.guild_id,
channel_id as string | undefined, channel_id as string | undefined,
); );
permissions.hasThrow("VIEW_CHANNEL"); if (!rights.has("OPERATOR")) permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) if (!permissions.has("READ_MESSAGE_HISTORY"))
return res.json({ messages: [], total_results: 0 }); return res.json({ messages: [], total_results: 0 });
@ -120,6 +127,7 @@ router.get(
channel.id, channel.id,
); );
if ( if (
!rights.has("OPERATOR") ||
!perm.has("VIEW_CHANNEL") || !perm.has("VIEW_CHANNEL") ||
!perm.has("READ_MESSAGE_HISTORY") !perm.has("READ_MESSAGE_HISTORY")
) )

View File

@ -19,6 +19,7 @@
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
emitEvent, emitEvent,
getRights,
GuildRoleDeleteEvent, GuildRoleDeleteEvent,
GuildRoleUpdateEvent, GuildRoleUpdateEvent,
handleFile, handleFile,
@ -48,7 +49,10 @@ router.get(
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, role_id } = req.params; const { guild_id, role_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id); const rights = await getRights(req.user_id);
// admins dont need to be in the guild
if (!rights.has("OPERATOR"))
await Member.IsInGuildOrFail(req.user_id, guild_id);
const role = await Role.findOneOrFail({ const role = await Role.findOneOrFail({
where: { guild_id, id: role_id }, where: { guild_id, id: role_id },
}); });
@ -59,6 +63,7 @@ router.get(
router.delete( router.delete(
"/", "/",
route({ route({
right: "OPERATOR",
permission: "MANAGE_ROLES", permission: "MANAGE_ROLES",
responses: { responses: {
204: {}, 204: {},
@ -103,6 +108,7 @@ router.patch(
"/", "/",
route({ route({
requestBody: "RoleModifySchema", requestBody: "RoleModifySchema",
right: "OPERATOR",
permission: "MANAGE_ROLES", permission: "MANAGE_ROLES",
responses: { responses: {
200: { 200: {

View File

@ -16,15 +16,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { DiscordApiErrors, Member, partition } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { DiscordApiErrors, Member, partition } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.patch( router.patch(
"/", "/",
route({ permission: "MANAGE_ROLES" }), route({ permission: "MANAGE_ROLES", right: "OPERATOR" }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
// Payload is JSON containing a list of member_ids, the new list of members to have the role // Payload is JSON containing a list of member_ids, the new list of members to have the role
const { guild_id, role_id } = req.params; const { guild_id, role_id } = req.params;

View File

@ -49,6 +49,7 @@ router.post(
route({ route({
requestBody: "RoleModifySchema", requestBody: "RoleModifySchema",
permission: "MANAGE_ROLES", permission: "MANAGE_ROLES",
right: "OPERATOR",
responses: { responses: {
200: { 200: {
body: "Role", body: "Role",
@ -65,11 +66,14 @@ router.post(
const guild_id = req.params.guild_id; const guild_id = req.params.guild_id;
const body = req.body as RoleModifySchema; const body = req.body as RoleModifySchema;
const role_count = await Role.count({ where: { guild_id } }); // admins can bypass this
const { maxRoles } = Config.get().limits.guild; if (!req.has_right) {
const role_count = await Role.count({ where: { guild_id } });
const { maxRoles } = Config.get().limits.guild;
if (role_count > maxRoles) if (role_count > maxRoles)
throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles); throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles);
}
const role = Role.create({ const role = Role.create({
// values before ...body are default and can be overriden // values before ...body are default and can be overriden

View File

@ -16,16 +16,19 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { Role, Member } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Member, Role, getRights } from "@spacebar/util";
import { Request, Response, Router } from "express";
import {} from "typeorm"; import {} from "typeorm";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id); const rights = await getRights(req.user_id);
// admins dont need to be in the guild
if (!rights.has("OPERATOR"))
await Member.IsInGuildOrFail(req.user_id, guild_id);
const role_ids = await Role.find({ where: { guild_id }, select: ["id"] }); const role_ids = await Role.find({ where: { guild_id }, select: ["id"] });
const counts: { [id: string]: number } = {}; const counts: { [id: string]: number } = {};

View File

@ -26,16 +26,71 @@ import {
getRights, getRights,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { ILike, MoreThan } from "typeorm";
const router: Router = Router(); const router: Router = Router();
router.get(
"/",
route({
description: "Get a list of guilds",
right: "OPERATOR",
query: {
limit: {
description:
"max number of guilds to return (1-1000). default 100",
type: "number",
required: false,
},
after: {
description: "The amount of guilds to skip",
type: "number",
required: false,
},
query: {
description: "The search query",
type: "string",
required: false,
},
},
responses: {
200: {
body: "AdminGuildsResponse",
},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { after, query } = req.query as {
after?: number;
query?: string;
};
const limit = Number(req.query.limit) || 100;
if (limit > 1000 || limit < 1)
throw new HTTPError("Limit must be between 1 and 1000");
const guilds = await Guild.find({
where: {
...(after ? { id: MoreThan(`${after}`) } : {}),
...(query ? { name: ILike(`%${query}%`) } : {}),
},
take: limit,
});
res.send(guilds);
},
);
//TODO: create default channel //TODO: create default channel
router.post( router.post(
"/", "/",
route({ route({
requestBody: "GuildCreateSchema", requestBody: "GuildCreateSchema",
right: "CREATE_GUILDS",
responses: { responses: {
201: { 201: {
body: "GuildCreateResponse", body: "GuildCreateResponse",
@ -50,17 +105,31 @@ router.post(
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as GuildCreateSchema; const body = req.body as GuildCreateSchema;
const rights = await getRights(req.user_id);
if (!rights.has("CREATE_GUILDS") && !rights.has("OPERATOR")) {
throw new HTTPError(
`You are missing the following rights CREATE_GUILDS or OPERATOR`,
403,
);
}
const { maxGuilds } = Config.get().limits.user; const { maxGuilds } = Config.get().limits.user;
const guild_count = await Member.count({ where: { id: req.user_id } }); const guild_count = await Member.count({ where: { id: req.user_id } });
const rights = await getRights(req.user_id); // allow admins to bypass guild limits
if (guild_count >= maxGuilds && !rights.has("MANAGE_GUILDS")) { if (guild_count >= maxGuilds && !rights.has("OPERATOR")) {
throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds);
} }
let owner_id = req.user_id;
// only admins can do this, is ignored otherwise
if (body.owner_id && rights.has("OPERATOR")) {
owner_id = body.owner_id;
}
const guild = await Guild.createGuild({ const guild = await Guild.createGuild({
...body, ...body,
owner_id: req.user_id, owner_id,
}); });
const { autoJoin } = Config.get().guild; const { autoJoin } = Config.get().guild;

View File

@ -17,7 +17,7 @@
*/ */
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { User } from "@spacebar/util"; import { User, getRights } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
@ -33,8 +33,15 @@ router.get(
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { id } = req.params; const { id } = req.params;
const rights = await getRights(req.user_id);
res.json(await User.getPublicUser(id)); const user = await User.findOneOrFail({ where: { id } });
res.json(
rights.has("OPERATOR")
? await user.toPrivateUser()
: await user.toPublicUser(),
);
}, },
); );

View File

@ -0,0 +1,82 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { route } from "@spacebar/api";
import { PrivateUserProjection, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { ILike, MoreThan } from "typeorm";
const router = Router();
router.get(
"/",
route({
right: "OPERATOR",
description: "Get a list of users",
query: {
limit: {
description:
"max number of users to return (1-1000). default 100",
type: "number",
required: false,
},
after: {
description: "The amount of users to skip",
type: "number",
required: false,
},
query: {
description: "The search query",
type: "string",
required: false,
},
},
responses: {
200: {
body: "AdminUsersResponse",
},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { after, query } = req.query as {
after?: number;
query?: string;
};
const limit = Number(req.query.limit) || 100;
if (limit > 1000 || limit < 1)
throw new HTTPError("Limit must be between 1 and 1000");
const users = await User.find({
where: {
...(after ? { id: MoreThan(`${after}`) } : {}),
...(query ? { username: ILike(`%${query}%`) } : {}),
},
take: limit,
select: PrivateUserProjection,
order: { id: "ASC" },
});
res.send(users);
},
);
export default router;

View File

@ -89,7 +89,7 @@ export function route(opts: RouteOptions) {
} }
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
if (opts.permission) { if (opts.permission && !opts.right) {
req.permission = await getPermission( req.permission = await getPermission(
req.user_id, req.user_id,
req.params.guild_id, req.params.guild_id,
@ -118,6 +118,7 @@ export function route(opts: RouteOptions) {
opts.right as string, opts.right as string,
); );
} }
req.has_right = true;
} }
if (validate) { if (validate) {

View File

@ -66,6 +66,7 @@ export enum PrivateUserEnum {
purchased_flags, purchased_flags,
premium_usage_flags, premium_usage_flags,
disabled, disabled,
rights,
// settings, // now a relation // settings, // now a relation
// locale // locale
} }

View File

@ -28,4 +28,5 @@ export interface GuildCreateSchema {
channels?: ChannelModifySchema[]; channels?: ChannelModifySchema[];
system_channel_id?: string; system_channel_id?: string;
rules_channel_id?: string; rules_channel_id?: string;
owner_id?: string; // used by admins to create a guild for someone else
} }

View File

@ -0,0 +1,21 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Guild } from "../../entities";
export type AdminGuildsResponse = Guild[];

View File

@ -18,6 +18,7 @@
export * from "./APIErrorOrCaptchaResponse"; export * from "./APIErrorOrCaptchaResponse";
export * from "./APIErrorResponse"; export * from "./APIErrorResponse";
export * from "./AdminGuildsResponse";
export * from "./BackupCodesChallengeResponse"; export * from "./BackupCodesChallengeResponse";
export * from "./CaptchaRequiredResponse"; export * from "./CaptchaRequiredResponse";
export * from "./DiscoverableGuildsResponse"; export * from "./DiscoverableGuildsResponse";