move validationResult to value instead, add create, delete, list, info route for guilds
All checks were successful
Code quality checks / biome (push) Successful in 11s
All checks were successful
Code quality checks / biome (push) Successful in 11s
This commit is contained in:
parent
ca0410f7fb
commit
1e6003079b
30 changed files with 870 additions and 90 deletions
|
@ -3,7 +3,7 @@ function createDefaultGuildData() {
|
|||
verification_level: 0,
|
||||
default_message_notifications: 0,
|
||||
preferred_locale: "en-US",
|
||||
features: new Set(),
|
||||
features: new Array<string>(),
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
};
|
||||
|
|
|
@ -1 +1,25 @@
|
|||
const guildNameRestrictions = {
|
||||
length: {
|
||||
min: 2,
|
||||
max: 50,
|
||||
},
|
||||
regex: /^[a-zA-Z0-9\s._-]+$/, // allow letters, numbers, spaces, and common punctuation
|
||||
};
|
||||
|
||||
const reservedGuildNames = [
|
||||
"admin",
|
||||
"administrator",
|
||||
"mod",
|
||||
"moderator",
|
||||
"system",
|
||||
"official",
|
||||
"staff",
|
||||
"support",
|
||||
"help",
|
||||
"null",
|
||||
"undefined",
|
||||
"anonymous",
|
||||
];
|
||||
|
||||
export { guildNameRestrictions, reservedGuildNames };
|
||||
export * from "./defaults";
|
||||
|
|
|
@ -70,6 +70,7 @@ const forbiddenDisplayNamePatterns = [
|
|||
/[\r\n\t]/,
|
||||
/\s{3,}/,
|
||||
/^\s|\s$/,
|
||||
/@everyone|@here/i,
|
||||
/\p{Cf}/u,
|
||||
/\p{Cc}/u,
|
||||
];
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
CREATE TABLE IF NOT EXISTS guilds_by_owner (
|
||||
owner_id TEXT,
|
||||
name TEXT,
|
||||
guild_id TEXT,
|
||||
created_at TIMESTAMP,
|
||||
PRIMARY KEY (owner_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS guilds_by_owner_chronological (
|
||||
owner_id TEXT,
|
||||
created_at TIMESTAMP,
|
||||
guild_id TEXT,
|
||||
name TEXT,
|
||||
created_at TIMESTAMP,
|
||||
PRIMARY KEY (owner_id, created_at, guild_id)
|
||||
) WITH CLUSTERING ORDER BY (created_at DESC);
|
||||
|
|
|
@ -158,7 +158,6 @@ class SessionManager {
|
|||
});
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
private getSessionKey(userId: string, token: string): string {
|
||||
return `session:${userId}:${token}`;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,11 @@ const pika = new Pika([
|
|||
prefix: "sess",
|
||||
description: "Session ID",
|
||||
},
|
||||
"guild",
|
||||
{
|
||||
prefix: "guild",
|
||||
description: "Guild ID",
|
||||
},
|
||||
]);
|
||||
|
||||
export { pika };
|
||||
|
|
65
src/lib/validation/guild/name.ts
Normal file
65
src/lib/validation/guild/name.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import {
|
||||
forbiddenDisplayNamePatterns,
|
||||
guildNameRestrictions,
|
||||
reservedGuildNames,
|
||||
} from "#environment/constants";
|
||||
|
||||
import type { validationResult } from "#types/lib";
|
||||
|
||||
function isValidGuildName(rawGuildName: string): validationResult {
|
||||
if (typeof rawGuildName !== "string") {
|
||||
return { valid: false, error: "Guild name must be a string" };
|
||||
}
|
||||
|
||||
const guildName = rawGuildName.trim().normalize("NFC");
|
||||
|
||||
if (!guildName) {
|
||||
return { valid: false, error: "Guild name is required" };
|
||||
}
|
||||
|
||||
if (guildName.length < guildNameRestrictions.length.min) {
|
||||
return { valid: false, error: "Guild name is too short" };
|
||||
}
|
||||
|
||||
if (guildName.length > guildNameRestrictions.length.max) {
|
||||
return { valid: false, error: "Guild name is too long" };
|
||||
}
|
||||
|
||||
if (!guildNameRestrictions.regex.test(guildName)) {
|
||||
return { valid: false, error: "Guild name contains invalid characters" };
|
||||
}
|
||||
|
||||
if (/^[._\s-]|[._\s-]$/.test(guildName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Guild name can't start or end with special characters or spaces",
|
||||
};
|
||||
}
|
||||
|
||||
if (/\s{2,}/.test(guildName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Guild name cannot contain multiple consecutive spaces",
|
||||
};
|
||||
}
|
||||
|
||||
if (reservedGuildNames.includes(guildName.toLowerCase())) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Guild name is reserved and cannot be used",
|
||||
};
|
||||
}
|
||||
|
||||
for (const pattern of forbiddenDisplayNamePatterns) {
|
||||
if (pattern.test(guildName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Guild name contains invalid characters or patterns",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, value: guildName };
|
||||
}
|
||||
|
||||
export { isValidGuildName };
|
|
@ -1,5 +1,5 @@
|
|||
import { isValidHostname, isValidPort } from "../general";
|
||||
import { isValidEmail } from "./email";
|
||||
import { isValidHostname, isValidPort } from "./general";
|
||||
import { isValidEmail } from "./user/email";
|
||||
|
||||
import type { MailerConfig } from "#types/config";
|
||||
import type { simpleConfigValidation } from "#types/lib";
|
|
@ -1,4 +1,4 @@
|
|||
export * from "./name";
|
||||
export * from "./password";
|
||||
export * from "./email";
|
||||
export * from "./mailer";
|
||||
export * from "../mailer";
|
||||
|
|
|
@ -39,7 +39,7 @@ function isValidUsername(rawUsername: string): validationResult {
|
|||
};
|
||||
}
|
||||
|
||||
return { valid: true, username };
|
||||
return { valid: true, value: username };
|
||||
}
|
||||
|
||||
function isValidDisplayName(rawDisplayName: string): validationResult {
|
||||
|
@ -84,7 +84,7 @@ function isValidDisplayName(rawDisplayName: string): validationResult {
|
|||
};
|
||||
}
|
||||
|
||||
return { valid: true, name: displayName };
|
||||
return { valid: true, value: displayName };
|
||||
}
|
||||
|
||||
export { isValidUsername, isValidDisplayName };
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
import { echo } from "@atums/echo";
|
||||
import {
|
||||
createDefaultGuildData,
|
||||
errorMessages,
|
||||
httpStatus,
|
||||
} from "#environment/constants";
|
||||
import { cassandra } from "#lib/database";
|
||||
import { pika } from "#lib/utils";
|
||||
import { isValidGuildName } from "#lib/validation/guild/name";
|
||||
|
||||
import type { UserSession } from "#types/config";
|
||||
import type {
|
||||
CreateGuildRequest,
|
||||
CreateGuildResponse,
|
||||
ExtendedRequest,
|
||||
GuildResponse,
|
||||
GuildRow,
|
||||
RouteDef,
|
||||
} from "#types/server";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "POST",
|
||||
accepts: "application/json",
|
||||
returns: "application/json",
|
||||
needsBody: "json",
|
||||
needsAuth: true,
|
||||
};
|
||||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
const session = request.session as UserSession;
|
||||
const { name, description } = request.requestBody as CreateGuildRequest;
|
||||
|
||||
if (!name) {
|
||||
const response: CreateGuildResponse = {
|
||||
code: httpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
error: "Guild name is required",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||
}
|
||||
|
||||
const nameValidation = isValidGuildName(name);
|
||||
if (!nameValidation.valid || !nameValidation.value) {
|
||||
const response: CreateGuildResponse = {
|
||||
code: httpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
error: nameValidation.error || "Invalid guild name",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||
}
|
||||
|
||||
const existingGuildQuery = `
|
||||
SELECT guild_id FROM guilds_by_owner
|
||||
WHERE owner_id = ? AND name = ?
|
||||
LIMIT 1
|
||||
`;
|
||||
const existingGuildResult = (await cassandra.execute(existingGuildQuery, [
|
||||
session.id,
|
||||
nameValidation.value,
|
||||
])) as { rows: Array<{ guild_id: string }> };
|
||||
|
||||
if (existingGuildResult.rows.length > 0) {
|
||||
const response: CreateGuildResponse = {
|
||||
code: httpStatus.CONFLICT,
|
||||
success: false,
|
||||
error: "You already own a guild with this name",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.CONFLICT });
|
||||
}
|
||||
|
||||
const guildId = pika.gen("guild");
|
||||
const defaultData = createDefaultGuildData();
|
||||
const now = new Date();
|
||||
|
||||
const insertGuildQuery = `
|
||||
INSERT INTO guilds (
|
||||
id, name, description, icon_url, banner_url, splash_url,
|
||||
owner_id, verification_level, default_message_notifications,
|
||||
system_channel_id, rules_channel_id, public_updates_channel_id,
|
||||
features, preferred_locale, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const insertParams = [
|
||||
guildId,
|
||||
nameValidation.value,
|
||||
description || null,
|
||||
null, // icon_url
|
||||
null, // banner_url
|
||||
null, // splash_url
|
||||
session.id,
|
||||
Number(defaultData.verification_level),
|
||||
Number(defaultData.default_message_notifications),
|
||||
null, // system_channel_id
|
||||
null, // rules_channel_id
|
||||
null, // public_updates_channel_id
|
||||
defaultData.features,
|
||||
defaultData.preferred_locale,
|
||||
now,
|
||||
now,
|
||||
];
|
||||
|
||||
await cassandra.execute(insertGuildQuery, insertParams, { prepare: true });
|
||||
|
||||
const insertOwnerQuery = `
|
||||
INSERT INTO guilds_by_owner (
|
||||
owner_id, guild_id, name, created_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
await cassandra.execute(
|
||||
insertOwnerQuery,
|
||||
[session.id, guildId, nameValidation.value, now],
|
||||
{ prepare: true },
|
||||
);
|
||||
|
||||
const insertChronologicalQuery = `
|
||||
INSERT INTO guilds_by_owner_chronological (
|
||||
owner_id, created_at, guild_id, name
|
||||
) VALUES (?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
await cassandra.execute(
|
||||
insertChronologicalQuery,
|
||||
[session.id, now, guildId, nameValidation.value],
|
||||
{ prepare: true },
|
||||
);
|
||||
|
||||
const guildQuery = `
|
||||
SELECT id, name, description, icon_url, banner_url, splash_url,
|
||||
owner_id, verification_level, default_message_notifications,
|
||||
system_channel_id, rules_channel_id, public_updates_channel_id,
|
||||
features, preferred_locale, created_at, updated_at
|
||||
FROM guilds WHERE id = ? LIMIT 1
|
||||
`;
|
||||
|
||||
const guildResult = (await cassandra.execute(guildQuery, [guildId], {
|
||||
prepare: true,
|
||||
})) as {
|
||||
rows: Array<GuildRow>;
|
||||
};
|
||||
|
||||
const guild = guildResult.rows[0];
|
||||
if (!guild) {
|
||||
const response: CreateGuildResponse = {
|
||||
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
success: false,
|
||||
error: "Failed to fetch created guild",
|
||||
};
|
||||
return Response.json(response, {
|
||||
status: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
const responseGuild: GuildResponse = {
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
description: guild.description,
|
||||
iconUrl: guild.icon_url,
|
||||
bannerUrl: guild.banner_url,
|
||||
splashUrl: guild.splash_url,
|
||||
ownerId: guild.owner_id,
|
||||
verificationLevel: guild.verification_level,
|
||||
defaultMessageNotifications: guild.default_message_notifications,
|
||||
systemChannelId: guild.system_channel_id,
|
||||
rulesChannelId: guild.rules_channel_id,
|
||||
publicUpdatesChannelId: guild.public_updates_channel_id,
|
||||
features: Array.from(guild.features || []),
|
||||
preferredLocale: guild.preferred_locale,
|
||||
createdAt: guild.created_at.toISOString(),
|
||||
updatedAt: guild.updated_at.toISOString(),
|
||||
};
|
||||
|
||||
const response: CreateGuildResponse = {
|
||||
code: httpStatus.CREATED,
|
||||
success: true,
|
||||
message: "Guild created successfully",
|
||||
guild: responseGuild,
|
||||
};
|
||||
|
||||
return Response.json(response, { status: httpStatus.CREATED });
|
||||
} catch (error) {
|
||||
echo.error({
|
||||
message: "Error creating guild",
|
||||
error,
|
||||
userId: request.session?.id,
|
||||
});
|
||||
|
||||
const response: CreateGuildResponse = {
|
||||
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
success: false,
|
||||
error: errorMessages.INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
return Response.json(response, {
|
||||
status: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
144
src/routes/guild/delete.ts
Normal file
144
src/routes/guild/delete.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
import { echo } from "@atums/echo";
|
||||
import { errorMessages, httpStatus } from "#environment/constants";
|
||||
import { cassandra } from "#lib/database";
|
||||
|
||||
import type { UserSession } from "#types/config";
|
||||
import type {
|
||||
BaseResponse,
|
||||
DeleteGuildRequest,
|
||||
ExtendedRequest,
|
||||
RouteDef,
|
||||
} from "#types/server";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "DELETE",
|
||||
accepts: "application/json",
|
||||
returns: "application/json",
|
||||
needsBody: "json",
|
||||
needsAuth: true,
|
||||
};
|
||||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
const session = request.session as UserSession;
|
||||
const { id, name } = request.requestBody as DeleteGuildRequest;
|
||||
|
||||
if (!id || !name) {
|
||||
const response: BaseResponse = {
|
||||
code: httpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
error: "Both guild ID and name are required for confirmation",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||
}
|
||||
|
||||
if (!id.startsWith("guild_")) {
|
||||
const response: BaseResponse = {
|
||||
code: httpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
error: "Invalid guild ID format",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||
}
|
||||
|
||||
const guildQuery = `
|
||||
SELECT id, name, owner_id, created_at
|
||||
FROM guilds WHERE id = ? LIMIT 1
|
||||
`;
|
||||
|
||||
const guildResult = (await cassandra.execute(guildQuery, [id], {
|
||||
prepare: true,
|
||||
})) as {
|
||||
rows: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
created_at: Date;
|
||||
}>;
|
||||
};
|
||||
|
||||
if (!guildResult?.rows || guildResult.rows.length === 0) {
|
||||
const response: BaseResponse = {
|
||||
code: httpStatus.NOT_FOUND,
|
||||
success: false,
|
||||
error: "Guild not found",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.NOT_FOUND });
|
||||
}
|
||||
|
||||
const guild = guildResult.rows[0];
|
||||
if (!guild) {
|
||||
const response: BaseResponse = {
|
||||
code: httpStatus.NOT_FOUND,
|
||||
success: false,
|
||||
error: "Guild not found",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.NOT_FOUND });
|
||||
}
|
||||
|
||||
if (guild.owner_id !== session.id) {
|
||||
const response: BaseResponse = {
|
||||
code: httpStatus.FORBIDDEN,
|
||||
success: false,
|
||||
error: "You can only delete guilds that you own",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.FORBIDDEN });
|
||||
}
|
||||
|
||||
if (guild.name !== name) {
|
||||
const response: BaseResponse = {
|
||||
code: httpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
error: `Guild name confirmation failed. Expected "${guild.name}" but got "${name}". Please ensure the name matches exactly.`,
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||
}
|
||||
|
||||
const deleteGuildQuery = "DELETE FROM guilds WHERE id = ?";
|
||||
await cassandra.execute(deleteGuildQuery, [id], { prepare: true });
|
||||
|
||||
const deleteOwnerQuery =
|
||||
"DELETE FROM guilds_by_owner WHERE owner_id = ? AND name = ?";
|
||||
await cassandra.execute(deleteOwnerQuery, [session.id, guild.name], {
|
||||
prepare: true,
|
||||
});
|
||||
|
||||
const deleteChronologicalQuery = `
|
||||
DELETE FROM guilds_by_owner_chronological
|
||||
WHERE owner_id = ? AND created_at = ? AND guild_id = ?
|
||||
`;
|
||||
await cassandra.execute(
|
||||
deleteChronologicalQuery,
|
||||
[session.id, guild.created_at, id],
|
||||
{ prepare: true },
|
||||
);
|
||||
|
||||
// TODO: delete related data like channels, members, messages when those features are implemented
|
||||
|
||||
const response: BaseResponse = {
|
||||
code: httpStatus.OK,
|
||||
success: true,
|
||||
message: `Guild "${guild.name}" has been successfully deleted`,
|
||||
};
|
||||
|
||||
return Response.json(response, { status: httpStatus.OK });
|
||||
} catch (error) {
|
||||
echo.error({
|
||||
message: "Error deleting guild",
|
||||
error,
|
||||
userId: request.session?.id,
|
||||
guildId: (request.requestBody as DeleteGuildRequest)?.id,
|
||||
});
|
||||
|
||||
const response: BaseResponse = {
|
||||
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
success: false,
|
||||
error: errorMessages.INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
return Response.json(response, {
|
||||
status: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
120
src/routes/guild/info[id].ts
Normal file
120
src/routes/guild/info[id].ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import { echo } from "@atums/echo";
|
||||
import { errorMessages, httpStatus } from "#environment/constants";
|
||||
import { cassandra } from "#lib/database";
|
||||
|
||||
// import type { UserSession } from "#types/config";
|
||||
import type {
|
||||
BaseResponse,
|
||||
ExtendedRequest,
|
||||
GuildResponse,
|
||||
GuildRow,
|
||||
RouteDef,
|
||||
} from "#types/server";
|
||||
|
||||
interface GuildGetResponse extends BaseResponse {
|
||||
guild?: GuildResponse;
|
||||
}
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "GET",
|
||||
accepts: "*/*",
|
||||
returns: "application/json",
|
||||
};
|
||||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
// const session = request.session as UserSession;
|
||||
const { id: guildId } = request.params;
|
||||
|
||||
if (!guildId) {
|
||||
const response: GuildGetResponse = {
|
||||
code: httpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
error: "Guild ID is required",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||
}
|
||||
|
||||
const guildQuery = `
|
||||
SELECT id, name, description, icon_url, banner_url, splash_url,
|
||||
owner_id, verification_level, default_message_notifications,
|
||||
system_channel_id, rules_channel_id, public_updates_channel_id,
|
||||
features, preferred_locale, created_at, updated_at
|
||||
FROM guilds WHERE id = ? LIMIT 1
|
||||
`;
|
||||
|
||||
const guildResult = (await cassandra.execute(guildQuery, [guildId], {
|
||||
prepare: true,
|
||||
})) as {
|
||||
rows: Array<GuildRow>;
|
||||
};
|
||||
|
||||
if (!guildResult?.rows || guildResult.rows.length === 0) {
|
||||
const response: GuildGetResponse = {
|
||||
code: httpStatus.NOT_FOUND,
|
||||
success: false,
|
||||
error: "Guild not found",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.NOT_FOUND });
|
||||
}
|
||||
|
||||
const guild = guildResult.rows[0];
|
||||
if (!guild) {
|
||||
const response: GuildGetResponse = {
|
||||
code: httpStatus.NOT_FOUND,
|
||||
success: false,
|
||||
error: "Guild not found",
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.NOT_FOUND });
|
||||
}
|
||||
|
||||
// TODO: In the future, check if user has permission to view this guild
|
||||
// const isOwner = session?.id === guild.owner_id;
|
||||
|
||||
const responseGuild: GuildResponse = {
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
description: guild.description,
|
||||
iconUrl: guild.icon_url,
|
||||
bannerUrl: guild.banner_url,
|
||||
splashUrl: guild.splash_url,
|
||||
ownerId: guild.owner_id,
|
||||
verificationLevel: guild.verification_level,
|
||||
defaultMessageNotifications: guild.default_message_notifications,
|
||||
systemChannelId: guild.system_channel_id,
|
||||
rulesChannelId: guild.rules_channel_id,
|
||||
publicUpdatesChannelId: guild.public_updates_channel_id,
|
||||
features: Array.from(guild.features || []),
|
||||
preferredLocale: guild.preferred_locale,
|
||||
createdAt: guild.created_at.toISOString(),
|
||||
updatedAt: guild.updated_at.toISOString(),
|
||||
};
|
||||
|
||||
const response: GuildGetResponse = {
|
||||
code: httpStatus.OK,
|
||||
success: true,
|
||||
message: "Guild retrieved successfully",
|
||||
guild: responseGuild,
|
||||
};
|
||||
|
||||
return Response.json(response, { status: httpStatus.OK });
|
||||
} catch (error) {
|
||||
echo.error({
|
||||
message: "Error fetching guild",
|
||||
error,
|
||||
guildId: request.params.id,
|
||||
userId: request.session?.id,
|
||||
});
|
||||
|
||||
const response: GuildGetResponse = {
|
||||
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
success: false,
|
||||
error: errorMessages.INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
return Response.json(response, {
|
||||
status: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
153
src/routes/guild/list.ts
Normal file
153
src/routes/guild/list.ts
Normal file
|
@ -0,0 +1,153 @@
|
|||
import { echo } from "@atums/echo";
|
||||
import { errorMessages, httpStatus } from "#environment/constants";
|
||||
import { cassandra } from "#lib/database";
|
||||
|
||||
import type { UserSession } from "#types/config";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
GuildListResponse,
|
||||
GuildResponse,
|
||||
GuildRow,
|
||||
RouteDef,
|
||||
} from "#types/server";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "GET",
|
||||
accepts: "*/*",
|
||||
returns: "application/json",
|
||||
needsAuth: true,
|
||||
};
|
||||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
const session = request.session as UserSession;
|
||||
const { limit = "10", offset = "0" } = request.query;
|
||||
|
||||
const limitNum = Math.min(
|
||||
Math.max(Number.parseInt(limit, 10) || 10, 1),
|
||||
50,
|
||||
);
|
||||
const offsetNum = Math.max(Number.parseInt(offset, 10) || 0, 0);
|
||||
|
||||
const guildIdsQuery = `
|
||||
SELECT guild_id, name, created_at
|
||||
FROM guilds_by_owner_chronological
|
||||
WHERE owner_id = ?
|
||||
LIMIT ?
|
||||
`;
|
||||
|
||||
const guildIdsResult = (await cassandra.execute(
|
||||
guildIdsQuery,
|
||||
[session.id, limitNum + offsetNum],
|
||||
{ prepare: true },
|
||||
)) as {
|
||||
rows: Array<{
|
||||
guild_id: string;
|
||||
name: string;
|
||||
created_at: Date;
|
||||
}>;
|
||||
};
|
||||
|
||||
if (!guildIdsResult?.rows || guildIdsResult.rows.length === 0) {
|
||||
const response: GuildListResponse = {
|
||||
code: httpStatus.OK,
|
||||
success: true,
|
||||
message: "No guilds found",
|
||||
guilds: [],
|
||||
count: 0,
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.OK });
|
||||
}
|
||||
|
||||
const relevantGuildIds = guildIdsResult.rows
|
||||
.slice(offsetNum, offsetNum + limitNum)
|
||||
.map((row) => row.guild_id);
|
||||
|
||||
if (relevantGuildIds.length === 0) {
|
||||
const response: GuildListResponse = {
|
||||
code: httpStatus.OK,
|
||||
success: true,
|
||||
message: "No more guilds found",
|
||||
guilds: [],
|
||||
count: 0,
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.OK });
|
||||
}
|
||||
|
||||
const guildDetailsQuery = `
|
||||
SELECT id, name, description, icon_url, banner_url, splash_url,
|
||||
owner_id, verification_level, default_message_notifications,
|
||||
system_channel_id, rules_channel_id, public_updates_channel_id,
|
||||
features, preferred_locale, created_at, updated_at
|
||||
FROM guilds WHERE id IN (${relevantGuildIds.map(() => "?").join(", ")})
|
||||
`;
|
||||
|
||||
const guildsResult = (await cassandra.execute(
|
||||
guildDetailsQuery,
|
||||
relevantGuildIds,
|
||||
{
|
||||
prepare: true,
|
||||
},
|
||||
)) as {
|
||||
rows: Array<GuildRow>;
|
||||
};
|
||||
|
||||
const guildsMap = new Map<string, GuildResponse>();
|
||||
|
||||
for (const guild of guildsResult.rows) {
|
||||
guildsMap.set(guild.id, {
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
description: guild.description,
|
||||
iconUrl: guild.icon_url,
|
||||
bannerUrl: guild.banner_url,
|
||||
splashUrl: guild.splash_url,
|
||||
ownerId: guild.owner_id,
|
||||
verificationLevel: guild.verification_level,
|
||||
defaultMessageNotifications: guild.default_message_notifications,
|
||||
systemChannelId: guild.system_channel_id,
|
||||
rulesChannelId: guild.rules_channel_id,
|
||||
publicUpdatesChannelId: guild.public_updates_channel_id,
|
||||
features: Array.from(guild.features || []),
|
||||
preferredLocale: guild.preferred_locale,
|
||||
createdAt: guild.created_at.toISOString(),
|
||||
updatedAt: guild.updated_at.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const orderedGuilds: GuildResponse[] = [];
|
||||
for (const guildId of relevantGuildIds) {
|
||||
const guild = guildsMap.get(guildId);
|
||||
if (guild) {
|
||||
orderedGuilds.push(guild);
|
||||
}
|
||||
}
|
||||
|
||||
const response: GuildListResponse = {
|
||||
code: httpStatus.OK,
|
||||
success: true,
|
||||
message: `Found ${orderedGuilds.length} guild(s)`,
|
||||
guilds: orderedGuilds,
|
||||
count: orderedGuilds.length,
|
||||
};
|
||||
|
||||
return Response.json(response, { status: httpStatus.OK });
|
||||
} catch (error) {
|
||||
echo.error({
|
||||
message: "Error fetching user guilds",
|
||||
error,
|
||||
userId: request.session?.id,
|
||||
});
|
||||
|
||||
const response: GuildListResponse = {
|
||||
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
success: false,
|
||||
error: errorMessages.INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
return Response.json(response, {
|
||||
status: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
|
@ -2,6 +2,7 @@ import { echo } from "@atums/echo";
|
|||
import { errorMessages, httpStatus } from "#environment/constants";
|
||||
import { cassandra } from "#lib/database";
|
||||
|
||||
import type { UserSession } from "#types/config";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
RouteDef,
|
||||
|
@ -19,7 +20,7 @@ const routeDef: RouteDef = {
|
|||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
const { id: identifier } = request.params;
|
||||
const { session } = request;
|
||||
const session = request.session as UserSession;
|
||||
|
||||
let userQuery: string;
|
||||
let queryParams: string[];
|
||||
|
|
|
@ -16,17 +16,6 @@ const routeDef: RouteDef = {
|
|||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
const { session } = request;
|
||||
|
||||
if (!session) {
|
||||
const response: LogoutResponse = {
|
||||
code: httpStatus.UNAUTHORIZED,
|
||||
success: false,
|
||||
error: errorMessages.NOT_AUTHENTICATED,
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
||||
}
|
||||
|
||||
await sessionManager.invalidateSession(request);
|
||||
const clearCookie = cookieService.clearCookie();
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
}
|
||||
|
||||
const usernameValidation = isValidUsername(username);
|
||||
if (!usernameValidation.valid || !usernameValidation.username) {
|
||||
if (!usernameValidation.valid || !usernameValidation.value) {
|
||||
const response: RegisterResponse = {
|
||||
code: 400,
|
||||
success: false,
|
||||
|
@ -66,7 +66,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
};
|
||||
return Response.json(response, { status: 400 });
|
||||
}
|
||||
validatedDisplayName = displayNameValidation.name || null;
|
||||
validatedDisplayName = displayNameValidation.value || null;
|
||||
}
|
||||
|
||||
const emailValidation = isValidEmail(email);
|
||||
|
@ -93,7 +93,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
"SELECT id FROM users WHERE username = ? LIMIT 1";
|
||||
const existingUsernameResult = (await cassandra.execute(
|
||||
existingUsernameQuery,
|
||||
[usernameValidation.username],
|
||||
[usernameValidation.value],
|
||||
)) as { rows: Array<{ id: string }> };
|
||||
|
||||
if (existingUsernameResult.rows.length > 0) {
|
||||
|
@ -134,7 +134,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
|
||||
await cassandra.execute(insertUserQuery, [
|
||||
userId,
|
||||
usernameValidation.username,
|
||||
usernameValidation.value,
|
||||
validatedDisplayName,
|
||||
email.trim().toLowerCase(),
|
||||
hashedPassword,
|
||||
|
@ -145,7 +145,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
|
||||
const responseUser: RegisterResponse["user"] = {
|
||||
id: userId,
|
||||
username: usernameValidation.username,
|
||||
username: usernameValidation.value,
|
||||
displayName: validatedDisplayName,
|
||||
email: email.trim().toLowerCase(),
|
||||
isVerified: false,
|
||||
|
|
|
@ -30,20 +30,12 @@ const routeDef: RouteDef = {
|
|||
accepts: ["application/json", "*/*"],
|
||||
returns: "application/json",
|
||||
needsBody: "json",
|
||||
needsAuth: true,
|
||||
};
|
||||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
const { session } = request;
|
||||
|
||||
if (!session) {
|
||||
const response: EmailChangeResponse = {
|
||||
code: httpStatus.UNAUTHORIZED,
|
||||
success: false,
|
||||
error: errorMessages.NOT_AUTHENTICATED,
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
||||
}
|
||||
const session = request.session as UserSession;
|
||||
|
||||
if (request.method === "GET") {
|
||||
return await handleEmailVerification(request, session);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { sessionManager } from "#lib/auth";
|
|||
import { cassandra } from "#lib/database";
|
||||
import { isValidDisplayName, isValidUsername } from "#lib/validation";
|
||||
|
||||
import type { UserSession } from "#types/config";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
RouteDef,
|
||||
|
@ -22,21 +23,12 @@ const routeDef: RouteDef = {
|
|||
accepts: "application/json",
|
||||
returns: "application/json",
|
||||
needsBody: "json",
|
||||
needsAuth: true,
|
||||
};
|
||||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
const { session } = request;
|
||||
|
||||
if (!session) {
|
||||
const response: UpdateInfoResponse = {
|
||||
code: httpStatus.UNAUTHORIZED,
|
||||
success: false,
|
||||
error: errorMessages.NOT_AUTHENTICATED,
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
||||
}
|
||||
|
||||
const session = request.session as UserSession;
|
||||
const { username, displayName } = request.requestBody as UpdateInfoRequest;
|
||||
|
||||
if (username === undefined && displayName === undefined) {
|
||||
|
@ -85,7 +77,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
|
||||
if (username !== undefined) {
|
||||
const usernameValidation = isValidUsername(username);
|
||||
if (!usernameValidation.valid || !usernameValidation.username) {
|
||||
if (!usernameValidation.valid || !usernameValidation.value) {
|
||||
const response: UpdateInfoResponse = {
|
||||
code: httpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
|
@ -94,12 +86,12 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||
}
|
||||
|
||||
if (usernameValidation.username !== currentUser.username) {
|
||||
if (usernameValidation.value !== currentUser.username) {
|
||||
const existingUsernameQuery =
|
||||
"SELECT id FROM users WHERE username = ? LIMIT 1";
|
||||
const existingUsernameResult = (await cassandra.execute(
|
||||
existingUsernameQuery,
|
||||
[usernameValidation.username],
|
||||
[usernameValidation.value],
|
||||
)) as { rows: Array<{ id: string }> };
|
||||
|
||||
if (
|
||||
|
@ -114,7 +106,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
return Response.json(response, { status: httpStatus.CONFLICT });
|
||||
}
|
||||
|
||||
updates.username = usernameValidation.username;
|
||||
updates.username = usernameValidation.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,7 +123,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
};
|
||||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||
}
|
||||
updates.displayName = displayNameValidation.name || null;
|
||||
updates.displayName = displayNameValidation.value || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { sessionManager } from "#lib/auth";
|
|||
import { cassandra } from "#lib/database";
|
||||
import { isValidPassword } from "#lib/validation";
|
||||
|
||||
import type { UserSession } from "#types/config";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
RouteDef,
|
||||
|
@ -22,21 +23,12 @@ const routeDef: RouteDef = {
|
|||
accepts: "application/json",
|
||||
returns: "application/json",
|
||||
needsBody: "json",
|
||||
needsAuth: true,
|
||||
};
|
||||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
try {
|
||||
const { session } = request;
|
||||
|
||||
if (!session) {
|
||||
const response: UpdatePasswordResponse = {
|
||||
code: httpStatus.UNAUTHORIZED,
|
||||
success: false,
|
||||
error: errorMessages.NOT_AUTHENTICATED,
|
||||
};
|
||||
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
||||
}
|
||||
|
||||
const session = request.session as UserSession;
|
||||
const { currentPassword, newPassword, logoutAllSessions } =
|
||||
request.requestBody as UpdatePasswordRequest;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { sessionManager } from "#lib/auth";
|
||||
import { cassandra } from "#lib/database";
|
||||
|
||||
import type { UserSession } from "#types/config";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
RouteDef,
|
||||
|
@ -183,7 +184,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
|||
});
|
||||
}
|
||||
|
||||
const { session } = request;
|
||||
const session = request.session as UserSession;
|
||||
let sessionCookie: string | undefined;
|
||||
|
||||
if (session && session.id === user.id) {
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import { resolve } from "node:path";
|
||||
import { type Echo, echo } from "@atums/echo";
|
||||
import { environment } from "#environment/config";
|
||||
import { reqLoggerIgnores } from "#environment/constants";
|
||||
import { noFileLog } from "#index";
|
||||
import { webSocketHandler } from "#websocket";
|
||||
|
||||
import {
|
||||
type BunFile,
|
||||
FileSystemRouter,
|
||||
type MatchedRoute,
|
||||
type Server,
|
||||
} from "bun";
|
||||
|
||||
import { environment } from "#environment/config";
|
||||
import {
|
||||
errorMessages,
|
||||
httpStatus,
|
||||
reqLoggerIgnores,
|
||||
} from "#environment/constants";
|
||||
import { noFileLog } from "#index";
|
||||
import { sessionManager } from "#lib/auth";
|
||||
import { webSocketHandler } from "#websocket";
|
||||
|
||||
import type { ExtendedRequest, RouteModule } from "#types/server";
|
||||
|
||||
class ServerHandler {
|
||||
|
@ -84,14 +87,18 @@ class ServerHandler {
|
|||
});
|
||||
} else {
|
||||
echo.warn(`File not found: ${filePath}`);
|
||||
response = new Response("Not Found", { status: 404 });
|
||||
response = new Response(errorMessages.NOT_FOUND, {
|
||||
status: httpStatus.NOT_FOUND,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error({
|
||||
message: `Error serving static file: ${pathname}`,
|
||||
error: error as Error,
|
||||
});
|
||||
response = new Response("Internal Server Error", { status: 500 });
|
||||
response = new Response(errorMessages.INTERNAL_SERVER_ERROR, {
|
||||
status: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
this.logRequest(request, response, ip);
|
||||
|
@ -146,7 +153,7 @@ class ServerHandler {
|
|||
const customPath = resolve(baseDir, pathname.slice(1));
|
||||
|
||||
if (!customPath.startsWith(baseDir)) {
|
||||
response = new Response("Forbidden", { status: 403 });
|
||||
response = new Response("Forbidden", { status: httpStatus.FORBIDDEN });
|
||||
this.logRequest(extendedRequest, response, ip);
|
||||
return response;
|
||||
}
|
||||
|
@ -247,14 +254,15 @@ class ServerHandler {
|
|||
response = Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 405,
|
||||
error: `Method ${request.method} Not Allowed, expected ${
|
||||
code: httpStatus.METHOD_NOT_ALLOWED,
|
||||
error: errorMessages.METHOD_NOT_ALLOWED,
|
||||
details: `Method ${request.method} Not Allowed, expected ${
|
||||
Array.isArray(routeModule.routeDef.method)
|
||||
? routeModule.routeDef.method.join(", ")
|
||||
: routeModule.routeDef.method
|
||||
}`,
|
||||
},
|
||||
{ status: 405 },
|
||||
{ status: httpStatus.METHOD_NOT_ALLOWED },
|
||||
);
|
||||
} else {
|
||||
const expectedContentType: string | string[] | null =
|
||||
|
@ -276,14 +284,14 @@ class ServerHandler {
|
|||
response = Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 406,
|
||||
code: httpStatus.NOT_ACCEPTABLE,
|
||||
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
|
||||
Array.isArray(expectedContentType)
|
||||
? expectedContentType.join(", ")
|
||||
: expectedContentType
|
||||
}`,
|
||||
},
|
||||
{ status: 406 },
|
||||
{ status: httpStatus.NOT_ACCEPTABLE },
|
||||
);
|
||||
} else {
|
||||
extendedRequest.params = params;
|
||||
|
@ -291,12 +299,23 @@ class ServerHandler {
|
|||
extendedRequest.requestBody = requestBody;
|
||||
extendedRequest.session = await sessionManager.getSession(request);
|
||||
|
||||
response = await routeModule.handler(extendedRequest, server);
|
||||
if (routeModule.routeDef.returns !== "*/*") {
|
||||
response.headers.set(
|
||||
"Content-Type",
|
||||
routeModule.routeDef.returns,
|
||||
if (routeModule.routeDef.needsAuth && !extendedRequest.session) {
|
||||
response = Response.json(
|
||||
{
|
||||
code: httpStatus.UNAUTHORIZED,
|
||||
success: false,
|
||||
error: errorMessages.NOT_AUTHENTICATED,
|
||||
},
|
||||
{ status: httpStatus.UNAUTHORIZED },
|
||||
);
|
||||
} else {
|
||||
response = await routeModule.handler(extendedRequest, server);
|
||||
if (routeModule.routeDef.returns !== "*/*") {
|
||||
response.headers.set(
|
||||
"Content-Type",
|
||||
routeModule.routeDef.returns,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -309,20 +328,20 @@ class ServerHandler {
|
|||
response = Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 500,
|
||||
error: "Internal Server Error",
|
||||
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
error: errorMessages.INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
{ status: 500 },
|
||||
{ status: httpStatus.INTERNAL_SERVER_ERROR },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
response = Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 404,
|
||||
error: "Not Found",
|
||||
code: httpStatus.NOT_FOUND,
|
||||
error: errorMessages.NOT_FOUND,
|
||||
},
|
||||
{ status: 404 },
|
||||
{ status: httpStatus.NOT_FOUND },
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,7 @@ type genericValidation = {
|
|||
type validationResult = {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
username?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
interface UrlValidationOptions {
|
||||
|
|
39
types/server/requests/guild/base.ts
Normal file
39
types/server/requests/guild/base.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
interface GuildResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
iconUrl: string | null;
|
||||
bannerUrl: string | null;
|
||||
splashUrl: string | null;
|
||||
ownerId: string;
|
||||
verificationLevel: number;
|
||||
defaultMessageNotifications: number;
|
||||
systemChannelId: string | null;
|
||||
rulesChannelId: string | null;
|
||||
publicUpdatesChannelId: string | null;
|
||||
features: string[];
|
||||
preferredLocale: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface GuildRow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
icon_url: string | null;
|
||||
banner_url: string | null;
|
||||
splash_url: string | null;
|
||||
owner_id: string;
|
||||
verification_level: number;
|
||||
default_message_notifications: number;
|
||||
system_channel_id: string | null;
|
||||
rules_channel_id: string | null;
|
||||
public_updates_channel_id: string | null;
|
||||
features: Set<string>;
|
||||
preferred_locale: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export type { GuildResponse, GuildRow };
|
15
types/server/requests/guild/create.ts
Normal file
15
types/server/requests/guild/create.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { BaseResponse } from "../base";
|
||||
import type { GuildResponse } from "./base";
|
||||
|
||||
interface CreateGuildRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon_url?: string;
|
||||
banner_url?: string;
|
||||
}
|
||||
|
||||
interface CreateGuildResponse extends BaseResponse {
|
||||
guild?: GuildResponse;
|
||||
}
|
||||
|
||||
export type { CreateGuildRequest, CreateGuildResponse };
|
6
types/server/requests/guild/delete.ts
Normal file
6
types/server/requests/guild/delete.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
interface DeleteGuildRequest {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type { DeleteGuildRequest };
|
4
types/server/requests/guild/index.ts
Normal file
4
types/server/requests/guild/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from "./base";
|
||||
export * from "./create";
|
||||
export * from "./delete";
|
||||
export * from "./list";
|
9
types/server/requests/guild/list.ts
Normal file
9
types/server/requests/guild/list.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import type { BaseResponse } from "../base";
|
||||
import type { GuildResponse } from "./base";
|
||||
|
||||
interface GuildListResponse extends BaseResponse {
|
||||
guilds?: GuildResponse[];
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export type { GuildListResponse };
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./user";
|
||||
export * from "./health";
|
||||
export * from "./base";
|
||||
export * from "./guild";
|
||||
|
|
|
@ -13,6 +13,7 @@ type RouteDef = {
|
|||
| "raw"
|
||||
| "buffer"
|
||||
| "blob";
|
||||
needsAuth?: boolean;
|
||||
};
|
||||
|
||||
type RouteModule = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue