refactor(services): validate UUID parameters with Zod and add error handling to prevent DB errors

This commit is contained in:
Chneemann 2026-08-29 17:36:39 +02:00
parent 4dc68e92a7
commit b8ed4a5b66
No known key found for this signature in database
4 changed files with 101 additions and 46 deletions

View file

@ -3,7 +3,7 @@
* @description Page component for viewing a specific channel and its messages. * @description Page component for viewing a specific channel and its messages.
*/ */
import { notFound } from "next/navigation"; import { redirect } from "next/navigation";
import { AppHeader } from "@/components/layout/AppHeader"; import { AppHeader } from "@/components/layout/AppHeader";
import { ChatInput } from "@/components/chat/ChatInput"; import { ChatInput } from "@/components/chat/ChatInput";
import { ChatMessages } from "@/components/chat/ChatMessages"; import { ChatMessages } from "@/components/chat/ChatMessages";
@ -24,13 +24,13 @@ export default async function ChannelPage({
}) { }) {
const { channelId } = await params; const { channelId } = await params;
// Paralleles Laden von Kanal-Daten und Nachrichten // Parallel loading of channel data and messages
const [channel, channelMessages] = await Promise.all([ const [channel, channelMessages] = await Promise.all([
getChannelById(channelId), getChannelById(channelId),
getChannelMessages(channelId), getChannelMessages(channelId),
]); ]);
if (!channel) return notFound(); if (!channel) redirect("/");
return ( return (
<div className="flex p-4 flex-col h-full bg-background"> <div className="flex p-4 flex-col h-full bg-background">

View file

@ -1,20 +1,34 @@
/** /**
* @file lib/services/channel.service.ts * @file lib/services/channel.service.ts
* @description Service module providing database access methods for channel management. * @description Service module providing database access methods for channel management and validation.
*/ */
import { db } from "@/db"; import { db } from "@/db";
import { channels } from "@/db/schema"; import { channels } from "@/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { z } from "zod";
const uuidSchema = z.uuid();
/** /**
* Retrieves a single channel record from the database by its unique identifier. * Retrieves a single channel record from the database by its unique identifier.
* Validates the UUID format before executing the database query.
* *
* @param {string} channelId - The unique identifier of the channel to fetch. * @param {string} channelId - The unique identifier of the channel to fetch.
* @returns {Promise<Object | undefined>} The channel object if found, or undefined if no matching channel exists. * @returns {Promise<Object | null>} The channel object if found, or null if no matching channel exists or an invalid ID was provided.
*/ */
export async function getChannelById(channelId: string) { export async function getChannelById(channelId: string) {
return await db.query.channels.findFirst({ if (!uuidSchema.safeParse(channelId).success) {
where: eq(channels.id, channelId), return null;
}); }
try {
const channel = await db.query.channels.findFirst({
where: eq(channels.id, channelId),
});
return channel ?? null;
} catch (error) {
console.error(`Error fetching channel ${channelId}:`, error);
return null;
}
} }

View file

@ -1,29 +1,43 @@
/** /**
* @file lib/services/message.service.ts * @file lib/services/message.service.ts
* @description Data access service for retrieving and managing message entities from the database. * @description Service module providing database access methods for fetching and managing channel messages and associated member profiles.
*/ */
import { db } from "@/db"; import { db } from "@/db";
import { messages } from "@/db/schema"; import { messages } from "@/db/schema";
import { eq } from "drizzle-orm"; import { eq, asc } from "drizzle-orm";
import { z } from "zod";
const uuidSchema = z.uuid();
/** /**
* Retrieves all messages belonging to a specific channel, ordered chronologically ascending, * Retrieves all messages for a specific channel sorted chronologically, including member and user details.
* including associated member and user details. * Validates the UUID format before querying the database and handles potential runtime errors gracefully.
* *
* @param {string} channelId - The unique identifier of the target channel. * @param {string} channelId - The unique identifier of the channel whose messages are to be fetched.
* @returns {Promise<Array<Object>>} A promise resolving to a list of message objects populated with nested member and user data. * @returns {Promise<Array<Object>>} An array of message objects with nested member and user data, or an empty array if invalid or failed.
*/ */
export async function getChannelMessages(channelId: string) { export async function getChannelMessages(channelId: string) {
return await db.query.messages.findMany({ if (!uuidSchema.safeParse(channelId).success) {
where: eq(messages.channelId, channelId), return [];
with: { }
member: {
with: { try {
user: true, const channelMessages = await db.query.messages.findMany({
where: eq(messages.channelId, channelId),
orderBy: [asc(messages.createdAt)],
with: {
member: {
with: {
user: true,
},
}, },
}, },
}, });
orderBy: (messages, { asc }) => [asc(messages.createdAt)],
}); return channelMessages;
} catch (error) {
console.error(`Error fetching messages for channel ${channelId}:`, error);
return [];
}
} }

View file

@ -6,31 +6,47 @@
import { db } from "@/db"; import { db } from "@/db";
import { members, servers } from "@/db/schema"; import { members, servers } from "@/db/schema";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { z } from "zod";
const uuidSchema = z.uuid();
/** /**
* Retrieves a server along with its channels sorted chronologically if the specified user is a verified member. * Retrieves a server along with its channels sorted chronologically if the specified user is a verified member.
* Validates UUID formats prior to database execution to prevent database errors.
* *
* @param {string} serverId - The unique identifier of the server to retrieve. * @param {string} serverId - The unique identifier of the server to retrieve.
* @param {string} userId - The unique identifier of the requesting user. * @param {string} userId - The unique identifier of the requesting user.
* @returns {Promise<Object | null>} The server record with nested channels array, or null if the user is not a member or the server is not found. * @returns {Promise<Object | null>} The server record with nested channels array, or null if the user is not a member, the server does not exist, or an invalid ID was provided.
*/ */
export async function getServerWithChannels(serverId: string, userId: string) { export async function getServerWithChannels(serverId: string, userId: string) {
const isMember = await db.query.members.findFirst({ if (
where: and(eq(members.serverId, serverId), eq(members.userId, userId)), !uuidSchema.safeParse(serverId).success ||
}); !uuidSchema.safeParse(userId).success
) {
return null;
}
if (!isMember) return null; try {
const isMember = await db.query.members.findFirst({
where: and(eq(members.serverId, serverId), eq(members.userId, userId)),
});
const server = await db.query.servers.findFirst({ if (!isMember) return null;
where: eq(servers.id, serverId),
with: { const server = await db.query.servers.findFirst({
channels: { where: eq(servers.id, serverId),
orderBy: (channels, { asc }) => [asc(channels.createdAt)], with: {
channels: {
orderBy: (channels, { asc }) => [asc(channels.createdAt)],
},
}, },
}, });
});
return server; return server ?? null;
} catch (error) {
console.error(`Error fetching server ${serverId}:`, error);
return null;
}
} }
/** /**
@ -40,18 +56,29 @@ export async function getServerWithChannels(serverId: string, userId: string) {
* @returns {Promise<Array<Object>>} An array of server objects associated with the user. * @returns {Promise<Array<Object>>} An array of server objects associated with the user.
*/ */
export async function getUserServers(userId: string) { export async function getUserServers(userId: string) {
const userMemberships = await db.query.members.findMany({ if (!uuidSchema.safeParse(userId).success) {
where: eq(members.userId, userId), return [];
with: { }
server: {
with: { try {
channels: { const userMemberships = await db.query.members.findMany({
orderBy: (channels, { asc }) => [asc(channels.createdAt)], where: eq(members.userId, userId),
with: {
server: {
with: {
channels: {
orderBy: (channels, { asc }) => [asc(channels.createdAt)],
},
}, },
}, },
}, },
}, });
});
return userMemberships.map((membership) => membership.server).filter(Boolean); return userMemberships
.map((membership) => membership.server)
.filter(Boolean);
} catch (error) {
console.error(`Error fetching servers for user ${userId}:`, error);
return [];
}
} }