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,
|
verification_level: 0,
|
||||||
default_message_notifications: 0,
|
default_message_notifications: 0,
|
||||||
preferred_locale: "en-US",
|
preferred_locale: "en-US",
|
||||||
features: new Set(),
|
features: new Array<string>(),
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
updated_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";
|
export * from "./defaults";
|
||||||
|
|
|
@ -70,6 +70,7 @@ const forbiddenDisplayNamePatterns = [
|
||||||
/[\r\n\t]/,
|
/[\r\n\t]/,
|
||||||
/\s{3,}/,
|
/\s{3,}/,
|
||||||
/^\s|\s$/,
|
/^\s|\s$/,
|
||||||
|
/@everyone|@here/i,
|
||||||
/\p{Cf}/u,
|
/\p{Cf}/u,
|
||||||
/\p{Cc}/u,
|
/\p{Cc}/u,
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
CREATE TABLE IF NOT EXISTS guilds_by_owner (
|
CREATE TABLE IF NOT EXISTS guilds_by_owner (
|
||||||
owner_id TEXT,
|
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,
|
guild_id TEXT,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
created_at TIMESTAMP,
|
|
||||||
PRIMARY KEY (owner_id, created_at, guild_id)
|
PRIMARY KEY (owner_id, created_at, guild_id)
|
||||||
) WITH CLUSTERING ORDER BY (created_at DESC);
|
) WITH CLUSTERING ORDER BY (created_at DESC);
|
||||||
|
|
|
@ -158,7 +158,6 @@ class SessionManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private helper methods
|
|
||||||
private getSessionKey(userId: string, token: string): string {
|
private getSessionKey(userId: string, token: string): string {
|
||||||
return `session:${userId}:${token}`;
|
return `session:${userId}:${token}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,11 @@ const pika = new Pika([
|
||||||
prefix: "sess",
|
prefix: "sess",
|
||||||
description: "Session ID",
|
description: "Session ID",
|
||||||
},
|
},
|
||||||
|
"guild",
|
||||||
|
{
|
||||||
|
prefix: "guild",
|
||||||
|
description: "Guild ID",
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export { pika };
|
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 { isValidHostname, isValidPort } from "./general";
|
||||||
import { isValidEmail } from "./email";
|
import { isValidEmail } from "./user/email";
|
||||||
|
|
||||||
import type { MailerConfig } from "#types/config";
|
import type { MailerConfig } from "#types/config";
|
||||||
import type { simpleConfigValidation } from "#types/lib";
|
import type { simpleConfigValidation } from "#types/lib";
|
|
@ -1,4 +1,4 @@
|
||||||
export * from "./name";
|
export * from "./name";
|
||||||
export * from "./password";
|
export * from "./password";
|
||||||
export * from "./email";
|
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 {
|
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 };
|
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 { errorMessages, httpStatus } from "#environment/constants";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
|
|
||||||
|
import type { UserSession } from "#types/config";
|
||||||
import type {
|
import type {
|
||||||
ExtendedRequest,
|
ExtendedRequest,
|
||||||
RouteDef,
|
RouteDef,
|
||||||
|
@ -19,7 +20,7 @@ const routeDef: RouteDef = {
|
||||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const { id: identifier } = request.params;
|
const { id: identifier } = request.params;
|
||||||
const { session } = request;
|
const session = request.session as UserSession;
|
||||||
|
|
||||||
let userQuery: string;
|
let userQuery: string;
|
||||||
let queryParams: string[];
|
let queryParams: string[];
|
||||||
|
|
|
@ -16,17 +16,6 @@ const routeDef: RouteDef = {
|
||||||
|
|
||||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
try {
|
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);
|
await sessionManager.invalidateSession(request);
|
||||||
const clearCookie = cookieService.clearCookie();
|
const clearCookie = cookieService.clearCookie();
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const usernameValidation = isValidUsername(username);
|
const usernameValidation = isValidUsername(username);
|
||||||
if (!usernameValidation.valid || !usernameValidation.username) {
|
if (!usernameValidation.valid || !usernameValidation.value) {
|
||||||
const response: RegisterResponse = {
|
const response: RegisterResponse = {
|
||||||
code: 400,
|
code: 400,
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -66,7 +66,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: 400 });
|
||||||
}
|
}
|
||||||
validatedDisplayName = displayNameValidation.name || null;
|
validatedDisplayName = displayNameValidation.value || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailValidation = isValidEmail(email);
|
const emailValidation = isValidEmail(email);
|
||||||
|
@ -93,7 +93,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
"SELECT id FROM users WHERE username = ? LIMIT 1";
|
"SELECT id FROM users WHERE username = ? LIMIT 1";
|
||||||
const existingUsernameResult = (await cassandra.execute(
|
const existingUsernameResult = (await cassandra.execute(
|
||||||
existingUsernameQuery,
|
existingUsernameQuery,
|
||||||
[usernameValidation.username],
|
[usernameValidation.value],
|
||||||
)) as { rows: Array<{ id: string }> };
|
)) as { rows: Array<{ id: string }> };
|
||||||
|
|
||||||
if (existingUsernameResult.rows.length > 0) {
|
if (existingUsernameResult.rows.length > 0) {
|
||||||
|
@ -134,7 +134,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
|
|
||||||
await cassandra.execute(insertUserQuery, [
|
await cassandra.execute(insertUserQuery, [
|
||||||
userId,
|
userId,
|
||||||
usernameValidation.username,
|
usernameValidation.value,
|
||||||
validatedDisplayName,
|
validatedDisplayName,
|
||||||
email.trim().toLowerCase(),
|
email.trim().toLowerCase(),
|
||||||
hashedPassword,
|
hashedPassword,
|
||||||
|
@ -145,7 +145,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
|
|
||||||
const responseUser: RegisterResponse["user"] = {
|
const responseUser: RegisterResponse["user"] = {
|
||||||
id: userId,
|
id: userId,
|
||||||
username: usernameValidation.username,
|
username: usernameValidation.value,
|
||||||
displayName: validatedDisplayName,
|
displayName: validatedDisplayName,
|
||||||
email: email.trim().toLowerCase(),
|
email: email.trim().toLowerCase(),
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
|
|
|
@ -30,20 +30,12 @@ const routeDef: RouteDef = {
|
||||||
accepts: ["application/json", "*/*"],
|
accepts: ["application/json", "*/*"],
|
||||||
returns: "application/json",
|
returns: "application/json",
|
||||||
needsBody: "json",
|
needsBody: "json",
|
||||||
|
needsAuth: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const { session } = request;
|
const session = request.session as UserSession;
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
const response: EmailChangeResponse = {
|
|
||||||
code: httpStatus.UNAUTHORIZED,
|
|
||||||
success: false,
|
|
||||||
error: errorMessages.NOT_AUTHENTICATED,
|
|
||||||
};
|
|
||||||
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.method === "GET") {
|
if (request.method === "GET") {
|
||||||
return await handleEmailVerification(request, session);
|
return await handleEmailVerification(request, session);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { sessionManager } from "#lib/auth";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
import { isValidDisplayName, isValidUsername } from "#lib/validation";
|
import { isValidDisplayName, isValidUsername } from "#lib/validation";
|
||||||
|
|
||||||
|
import type { UserSession } from "#types/config";
|
||||||
import type {
|
import type {
|
||||||
ExtendedRequest,
|
ExtendedRequest,
|
||||||
RouteDef,
|
RouteDef,
|
||||||
|
@ -22,21 +23,12 @@ const routeDef: RouteDef = {
|
||||||
accepts: "application/json",
|
accepts: "application/json",
|
||||||
returns: "application/json",
|
returns: "application/json",
|
||||||
needsBody: "json",
|
needsBody: "json",
|
||||||
|
needsAuth: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const { session } = request;
|
const session = request.session as UserSession;
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
const response: UpdateInfoResponse = {
|
|
||||||
code: httpStatus.UNAUTHORIZED,
|
|
||||||
success: false,
|
|
||||||
error: errorMessages.NOT_AUTHENTICATED,
|
|
||||||
};
|
|
||||||
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { username, displayName } = request.requestBody as UpdateInfoRequest;
|
const { username, displayName } = request.requestBody as UpdateInfoRequest;
|
||||||
|
|
||||||
if (username === undefined && displayName === undefined) {
|
if (username === undefined && displayName === undefined) {
|
||||||
|
@ -85,7 +77,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
|
|
||||||
if (username !== undefined) {
|
if (username !== undefined) {
|
||||||
const usernameValidation = isValidUsername(username);
|
const usernameValidation = isValidUsername(username);
|
||||||
if (!usernameValidation.valid || !usernameValidation.username) {
|
if (!usernameValidation.valid || !usernameValidation.value) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: httpStatus.BAD_REQUEST,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -94,12 +86,12 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usernameValidation.username !== currentUser.username) {
|
if (usernameValidation.value !== currentUser.username) {
|
||||||
const existingUsernameQuery =
|
const existingUsernameQuery =
|
||||||
"SELECT id FROM users WHERE username = ? LIMIT 1";
|
"SELECT id FROM users WHERE username = ? LIMIT 1";
|
||||||
const existingUsernameResult = (await cassandra.execute(
|
const existingUsernameResult = (await cassandra.execute(
|
||||||
existingUsernameQuery,
|
existingUsernameQuery,
|
||||||
[usernameValidation.username],
|
[usernameValidation.value],
|
||||||
)) as { rows: Array<{ id: string }> };
|
)) as { rows: Array<{ id: string }> };
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -114,7 +106,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
return Response.json(response, { status: httpStatus.CONFLICT });
|
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 });
|
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 { cassandra } from "#lib/database";
|
||||||
import { isValidPassword } from "#lib/validation";
|
import { isValidPassword } from "#lib/validation";
|
||||||
|
|
||||||
|
import type { UserSession } from "#types/config";
|
||||||
import type {
|
import type {
|
||||||
ExtendedRequest,
|
ExtendedRequest,
|
||||||
RouteDef,
|
RouteDef,
|
||||||
|
@ -22,21 +23,12 @@ const routeDef: RouteDef = {
|
||||||
accepts: "application/json",
|
accepts: "application/json",
|
||||||
returns: "application/json",
|
returns: "application/json",
|
||||||
needsBody: "json",
|
needsBody: "json",
|
||||||
|
needsAuth: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const { session } = request;
|
const session = request.session as UserSession;
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
const response: UpdatePasswordResponse = {
|
|
||||||
code: httpStatus.UNAUTHORIZED,
|
|
||||||
success: false,
|
|
||||||
error: errorMessages.NOT_AUTHENTICATED,
|
|
||||||
};
|
|
||||||
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentPassword, newPassword, logoutAllSessions } =
|
const { currentPassword, newPassword, logoutAllSessions } =
|
||||||
request.requestBody as UpdatePasswordRequest;
|
request.requestBody as UpdatePasswordRequest;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import { sessionManager } from "#lib/auth";
|
import { sessionManager } from "#lib/auth";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
|
|
||||||
|
import type { UserSession } from "#types/config";
|
||||||
import type {
|
import type {
|
||||||
ExtendedRequest,
|
ExtendedRequest,
|
||||||
RouteDef,
|
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;
|
let sessionCookie: string | undefined;
|
||||||
|
|
||||||
if (session && session.id === user.id) {
|
if (session && session.id === user.id) {
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { type Echo, echo } from "@atums/echo";
|
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 {
|
import {
|
||||||
type BunFile,
|
type BunFile,
|
||||||
FileSystemRouter,
|
FileSystemRouter,
|
||||||
type MatchedRoute,
|
type MatchedRoute,
|
||||||
type Server,
|
type Server,
|
||||||
} from "bun";
|
} from "bun";
|
||||||
|
import { environment } from "#environment/config";
|
||||||
|
import {
|
||||||
|
errorMessages,
|
||||||
|
httpStatus,
|
||||||
|
reqLoggerIgnores,
|
||||||
|
} from "#environment/constants";
|
||||||
|
import { noFileLog } from "#index";
|
||||||
import { sessionManager } from "#lib/auth";
|
import { sessionManager } from "#lib/auth";
|
||||||
|
import { webSocketHandler } from "#websocket";
|
||||||
|
|
||||||
import type { ExtendedRequest, RouteModule } from "#types/server";
|
import type { ExtendedRequest, RouteModule } from "#types/server";
|
||||||
|
|
||||||
class ServerHandler {
|
class ServerHandler {
|
||||||
|
@ -84,14 +87,18 @@ class ServerHandler {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
echo.warn(`File not found: ${filePath}`);
|
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) {
|
} catch (error) {
|
||||||
echo.error({
|
echo.error({
|
||||||
message: `Error serving static file: ${pathname}`,
|
message: `Error serving static file: ${pathname}`,
|
||||||
error: error as Error,
|
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);
|
this.logRequest(request, response, ip);
|
||||||
|
@ -146,7 +153,7 @@ class ServerHandler {
|
||||||
const customPath = resolve(baseDir, pathname.slice(1));
|
const customPath = resolve(baseDir, pathname.slice(1));
|
||||||
|
|
||||||
if (!customPath.startsWith(baseDir)) {
|
if (!customPath.startsWith(baseDir)) {
|
||||||
response = new Response("Forbidden", { status: 403 });
|
response = new Response("Forbidden", { status: httpStatus.FORBIDDEN });
|
||||||
this.logRequest(extendedRequest, response, ip);
|
this.logRequest(extendedRequest, response, ip);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -247,14 +254,15 @@ class ServerHandler {
|
||||||
response = Response.json(
|
response = Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
code: 405,
|
code: httpStatus.METHOD_NOT_ALLOWED,
|
||||||
error: `Method ${request.method} Not Allowed, expected ${
|
error: errorMessages.METHOD_NOT_ALLOWED,
|
||||||
|
details: `Method ${request.method} Not Allowed, expected ${
|
||||||
Array.isArray(routeModule.routeDef.method)
|
Array.isArray(routeModule.routeDef.method)
|
||||||
? routeModule.routeDef.method.join(", ")
|
? routeModule.routeDef.method.join(", ")
|
||||||
: routeModule.routeDef.method
|
: routeModule.routeDef.method
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{ status: 405 },
|
{ status: httpStatus.METHOD_NOT_ALLOWED },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const expectedContentType: string | string[] | null =
|
const expectedContentType: string | string[] | null =
|
||||||
|
@ -276,14 +284,14 @@ class ServerHandler {
|
||||||
response = Response.json(
|
response = Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
code: 406,
|
code: httpStatus.NOT_ACCEPTABLE,
|
||||||
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
|
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
|
||||||
Array.isArray(expectedContentType)
|
Array.isArray(expectedContentType)
|
||||||
? expectedContentType.join(", ")
|
? expectedContentType.join(", ")
|
||||||
: expectedContentType
|
: expectedContentType
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{ status: 406 },
|
{ status: httpStatus.NOT_ACCEPTABLE },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
extendedRequest.params = params;
|
extendedRequest.params = params;
|
||||||
|
@ -291,6 +299,16 @@ class ServerHandler {
|
||||||
extendedRequest.requestBody = requestBody;
|
extendedRequest.requestBody = requestBody;
|
||||||
extendedRequest.session = await sessionManager.getSession(request);
|
extendedRequest.session = await sessionManager.getSession(request);
|
||||||
|
|
||||||
|
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);
|
response = await routeModule.handler(extendedRequest, server);
|
||||||
if (routeModule.routeDef.returns !== "*/*") {
|
if (routeModule.routeDef.returns !== "*/*") {
|
||||||
response.headers.set(
|
response.headers.set(
|
||||||
|
@ -300,6 +318,7 @@ class ServerHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
echo.error({
|
echo.error({
|
||||||
message: `Error handling route ${request.url}`,
|
message: `Error handling route ${request.url}`,
|
||||||
|
@ -309,20 +328,20 @@ class ServerHandler {
|
||||||
response = Response.json(
|
response = Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
error: "Internal Server Error",
|
error: errorMessages.INTERNAL_SERVER_ERROR,
|
||||||
},
|
},
|
||||||
{ status: 500 },
|
{ status: httpStatus.INTERNAL_SERVER_ERROR },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response = Response.json(
|
response = Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
error: "Not Found",
|
error: errorMessages.NOT_FOUND,
|
||||||
},
|
},
|
||||||
{ status: 404 },
|
{ status: httpStatus.NOT_FOUND },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,7 @@ type genericValidation = {
|
||||||
type validationResult = {
|
type validationResult = {
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
username?: string;
|
value?: string;
|
||||||
name?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface UrlValidationOptions {
|
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 "./user";
|
||||||
export * from "./health";
|
export * from "./health";
|
||||||
export * from "./base";
|
export * from "./base";
|
||||||
|
export * from "./guild";
|
||||||
|
|
|
@ -13,6 +13,7 @@ type RouteDef = {
|
||||||
| "raw"
|
| "raw"
|
||||||
| "buffer"
|
| "buffer"
|
||||||
| "blob";
|
| "blob";
|
||||||
|
needsAuth?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RouteModule = {
|
type RouteModule = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue