Repository: BrunoAseff/flaira
Pull request #96: feat: add route for upload profile picture and s3 instance
Author: login: BrunoAseff
Created: 2025-05-22T20:07:31Z | Merged: 2025-05-23T02:48:33Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for uploading, retrieving, and deleting user avatars via new API endpoints.
  - Integrated AWS S3 for secure avatar management using pre-signed URLs.
  - Introduced user authentication session handling and a new user information endpoint.
  - Added a user profile tab displaying avatar, name, and email in the settings dialog.
  - Improved session management with a unified session hook for better state handling.
  - Enhanced avatar display in sidebar and profile with presigned URL fetching and upload UI.

- **Chores**
  - Updated environment configuration and documentation to include AWS S3 settings.
  - Enhanced CI/CD workflow to provide AWS credentials for backend operations.
  - Added necessary AWS SDK dependencies to the backend application.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 308bb08..15dd91c 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -52,6 +52,10 @@ jobs:
         BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
         FRONTEND_URL: ${{ secrets.FRONTEND_URL }}
         RESEND_API: ${{ secrets.RESEND_API }}
+        S3_BUCKET: ${{ secrets.S3_BUCKET }}
+        AWS_REGION: ${{ secrets.AWS_REGION }}
+        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
     steps:
       - uses: actions/checkout@v3
 
@@ -112,6 +116,10 @@ jobs:
           BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
           FRONTEND_URL: ${{ secrets.FRONTEND_URL }}
           RESEND_API: ${{ secrets.RESEND_API }}
+          S3_BUCKET: ${{ secrets.S3_BUCKET }}
+          AWS_REGION: ${{ secrets.AWS_REGION }}
+          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
         run: |
           pnpm install --no-frozen-lockfile
           cd apps/backend
diff --git a/apps/backend/.env.example b/apps/backend/.env.example
index 8272f55..cc74e00 100644
--- a/apps/backend/.env.example
+++ b/apps/backend/.env.example
@@ -2,4 +2,8 @@ BETTER_AUTH_SECRET=
 RESEND_API=
 BETTER_AUTH_URL=http://localhost:3001
 DATABASE_URL=postgres://local_user:local_password@localhost:5432/local_db
-FRONTEND_URL=http://localhost:3000
\ No newline at end of file
+FRONTEND_URL=http://localhost:3000
+S3_BUCKET=
+AWS_REGION=
+AWS_SECRET_ACCESS_KEY=
+AWS_ACCESS_KEY_ID=
\ No newline at end of file
diff --git a/apps/backend/package.json b/apps/backend/package.json
index e70c80a..77a4e5f 100644
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -17,13 +17,18 @@
     "drizzle:studio": "drizzle-kit studio"
   },
   "dependencies": {
+    "@aws-sdk/client-s3": "3.816.0",
+    "@aws-sdk/s3-request-presigner": "3.816.0",
     "@hono/node-server": "1.2.0",
+    "@hono/zod-validator": "0.5.0",
+    "@types/uuid": "10.0.0",
     "better-auth": "1.2.7",
     "dotenv": "16.4.7",
     "drizzle-orm": "0.40.0",
     "hono": "4.7.4",
     "pg": "8.14.1",
     "resend": "4.5.0",
+    "uuid": "11.1.0",
     "zod": "3.24.2"
   },
   "devDependencies": {
diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts
index c1b3a1d..af87f5e 100644
--- a/apps/backend/src/app.ts
+++ b/apps/backend/src/app.ts
@@ -1,15 +1,39 @@
 import { Hono } from "hono";
 import { cors } from "hono/cors";
-import { status } from "@/modules/status/route";
 import { auth } from "./utils/auth";
 import { corsConfig } from "./utils/http";
+import { user } from "@/modules/user/route";
+import { status } from "@/modules/status/route";
 
-const app = new Hono();
+const app = new Hono<{
+  Variables: {
+    user: typeof auth.$Infer.Session.user | null;
+    session: typeof auth.$Infer.Session.session | null;
+  };
+}>();
 
 app.use("*", cors(corsConfig));
 
-app.on(["POST", "GET"], "/auth/**", (c) => auth.handler(c.req.raw));
+app.use("*", async (c, next) => {
+  const session = await auth.api.getSession({ headers: c.req.raw.headers });
+
+  if (!session) {
+    c.set("user", null);
+    c.set("session", null);
+    return next();
+  }
+
+  c.set("user", session.user);
+  c.set("session", session.session);
+  return next();
+});
+
+app.on(["POST", "GET"], "/auth/*", (c) => {
+  return auth.handler(c.req.raw);
+});
 
 app.route("/status", status);
 
+app.route("/user", user);
+
 export { app };
diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts
index df04a89..64e31f6 100644
--- a/apps/backend/src/env.ts
+++ b/apps/backend/src/env.ts
@@ -9,6 +9,10 @@ const envSchema = z.object({
   BETTER_AUTH_URL: z.string(),
   FRONTEND_URL: z.string(),
   RESEND_API: z.string(),
+  S3_BUCKET: z.string(),
+  AWS_REGION: z.string(),
+  AWS_SECRET_ACCESS_KEY: z.string(),
+  AWS_ACCESS_KEY_ID: z.string(),
 });
 
 const _env = envSchema.safeParse(process.env);
diff --git a/apps/backend/src/modules/user/controller.ts b/apps/backend/src/modules/user/controller.ts
new file mode 100644
index 0000000..0c28048
--- /dev/null
+++ b/apps/backend/src/modules/user/controller.ts
@@ -0,0 +1,66 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar, getUserAvatar, deleteUserAvatar } from "./service";
+
+export const uploadAvatar = async (context: Context) => {
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to upload profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const getAvatar = async (context: Context) => {
+  try {
+    const key = context.req.query("key");
+    if (!key) {
+      const headers = getHeaders();
+      const body = getBody(400, null);
+
+      return getResponse(context, 400, headers, body);
+    }
+    const result = await getUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const deleteAvatar = async (context: Context) => {
+  try {
+    const { key } = await context.req.json();
+    const result = await deleteUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
diff --git a/apps/backend/src/modules/user/route.ts b/apps/backend/src/modules/user/route.ts
new file mode 100644
index 0000000..7a6e6df
--- /dev/null
+++ b/apps/backend/src/modules/user/route.ts
@@ -0,0 +1,21 @@
+import { Hono } from "hono";
+import { deleteAvatar, getAvatar, uploadAvatar } from "./controller";
+import { zValidator } from "@hono/zod-validator";
+import { uploadAvatarSchema } from "./validator";
+import { middleware } from "@/utils/middleware";
+
+const user = new Hono();
+
+user.use(middleware);
+
+user.post(
+  "/upload-avatar",
+  zValidator("json", uploadAvatarSchema, (result, c) => {
+    if (!result.success) return c.text("Invalid format", 422);
+  }),
+  uploadAvatar,
+);
+user.get("/get-avatar", getAvatar);
+user.delete("/delete-avatar", deleteAvatar);
+
+export { user };
diff --git a/apps/backend/src/modules/user/service.ts b/apps/backend/src/modules/user/service.ts
new file mode 100644
index 0000000..6e3c8ce
--- /dev/null
+++ b/apps/backend/src/modules/user/service.ts
@@ -0,0 +1,22 @@
+import { deleteObject, getUrl, uploadUrl } from "@/utils/s3";
+import { v4 as uuidv4 } from "uuid";
+
+export const uploadUserAvatar = async ({
+  fileName,
+  type,
+}: { fileName: string; type: string }) => {
+  const id = uuidv4();
+  const key = `avatar/${id}/${fileName}`;
+  const url = await uploadUrl({ key, type });
+  return { url, key };
+};
+
+export const getUserAvatar = async ({ key }: { key: string }) => {
+  const url = await getUrl({ key });
+  return url;
+};
+
+export const deleteUserAvatar = async ({ key }: { key: string }) => {
+  const result = await deleteObject({ key });
+  return result;
+};
diff --git a/apps/backend/src/modules/user/validator.ts b/apps/backend/src/modules/user/validator.ts
new file mode 100644
index 0000000..8c728fa
--- /dev/null
+++ b/apps/backend/src/modules/user/validator.ts
@@ -0,0 +1,11 @@
+import { z } from "zod";
+
+export const uploadAvatarSchema = z.object({
+  fileName: z
+    .string()
+    .min(1)
+    .refine(
+      (val) => !val.includes("..") && !val.includes("/") && !val.includes("\\"),
+    ),
+  type: z.string().regex(/^image\/(jpeg|jpg|png|webp)$/),
+});
diff --git a/apps/backend/src/utils/checkUser.ts b/apps/backend/src/utils/checkUser.ts
new file mode 100644
index 0000000..3f55614
--- /dev/null
+++ b/apps/backend/src/utils/checkUser.ts
@@ -0,0 +1,10 @@
+import type { Context } from "hono";
+
+export const checkUser = ({ context }: { context: Context }) => {
+  const session = context.get("session");
+  const user = context.get("user");
+
+  if (!session || !user) return false;
+
+  return true;
+};
diff --git a/apps/backend/src/utils/middleware.ts b/apps/backend/src/utils/middleware.ts
new file mode 100644
index 0000000..98ba0b8
--- /dev/null
+++ b/apps/backend/src/utils/middleware.ts
@@ -0,0 +1,14 @@
+import { getHeaders, getBody, getResponse } from "@/utils/http";
+import { checkUser } from "@/utils/checkUser";
+import type { Context } from "hono";
+
+export const middleware = async (c: Context, next: () => Promise<void>) => {
+  const user = checkUser({ context: c });
+  if (!user) {
+    const headers = getHeaders();
+    const body = getBody(401, null);
+    return getResponse(c, 401, headers, body);
+  }
+  c.set("user", user);
+  await next();
+};
diff --git a/apps/backend/src/utils/s3.ts b/apps/backend/src/utils/s3.ts
new file mode 100644
index 0000000..1d1218d
--- /dev/null
+++ b/apps/backend/src/utils/s3.ts
@@ -0,0 +1,50 @@
+import { env } from "@/env";
+import {
+  S3Client,
+  PutObjectCommand,
+  GetObjectCommand,
+  DeleteObjectCommand,
+} from "@aws-sdk/client-s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
+
+const s3 = new S3Client({
+  region: env.AWS_REGION,
+  credentials: {
+    accessKeyId: env.AWS_ACCESS_KEY_ID,
+    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
+  },
+});
+
+export const uploadUrl = async ({
+  key,
+  type,
+}: { key: string; type: string }) => {
+  const command = new PutObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+    ContentType: type,
+  });
+
+  const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
+  return url;
+};
+
+export const getUrl = async ({ key }: { key: string }) => {
+  const command = new GetObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+  });
+
+  const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
+  return url;
+};
+
+export const deleteObject = async ({ key }: { key: string }) => {
+  const command = new DeleteObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+  });
+
+  await s3.send(command);
+  return true;
+};
diff --git a/apps/frontend/src/components/settings/ProfileTab.tsx b/apps/frontend/src/components/settings/ProfileTab.tsx
new file mode 100644
index 0000000..0d39692
--- /dev/null
+++ b/apps/frontend/src/components/settings/ProfileTab.tsx
@@ -0,0 +1,252 @@
+"use client";
+
+import { useState, type ChangeEvent } from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Separator } from "../ui/separator";
+import { ScrollArea } from "../ui/scroll-area";
+import type { User } from "better-auth/types";
+import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
+import { HugeiconsIcon } from "@hugeicons/react";
+import {
+  User03Icon,
+  Loading02Icon,
+  PaintBrush01Icon,
+} from "@hugeicons/core-free-icons";
+import { Banner } from "../ui/banner";
+import { Input } from "../ui/input";
+import { auth } from "@/auth/client";
+
+interface ProfileTabProps {
+  user: User | null;
+  error: Error | null;
+}
+
+export default function ProfileTab({ user, error }: ProfileTabProps) {
+  const queryClient = useQueryClient();
+  const [clientSideUploadError, setClientSideUploadError] = useState<
+    string | null
+  >(null);
+  const [optimisticAvatarUrl, setOptimisticAvatarUrl] = useState<string | null>(
+    null,
+  );
+
+  const { data: imageUrl } = useQuery({
+    queryKey: ["image-url", user?.image],
+    queryFn: async () => {
+      if (!user?.image) {
+        return null;
+      }
+
+      const url = new URL(`${process.env.NEXT_PUBLIC_API_URL}/user/get-avatar`);
+      url.searchParams.append("key", user.image);
+
+      const presignedUrlResponse = await fetch(url.toString(), {
+        method: "GET",
+        headers: { "Content-Type": "application/json" },
+        credentials: "include",
+      });
+
+      if (!presignedUrlResponse.ok) {
+        throw new Error(
+          `Failed to fetch avatar URL: ${presignedUrlResponse.status}`,
+        );
+      }
+
+      const presignedUrlData = await presignedUrlResponse.json();
+      return presignedUrlData.data || null;
+    },
+    enabled: !!user && !!user.image,
+    staleTime: 5 * 60 * 1000,
+    retry: 2,
+  });
+
+  const uploadAvatarMutation = useMutation({
+    mutationFn: async (file: File) => {
+      setClientSideUploadError(null);
+      const fileName = encodeURIComponent(file.name);
+      const type = file.type;
+
+      const presignedUrlResponse = await fetch(
+        `${process.env.NEXT_PUBLIC_API_URL}/user/upload-avatar`,
+        {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          credentials: "include",
+          body: JSON.stringify({ fileName, type }),
+        },
+      );
+
+      if (!presignedUrlResponse.ok) {
+        const errorData = await presignedUrlResponse.json().catch(() => ({}));
+        throw new Error(errorData.message || "Upload failed.");
+      }
+      const presignedUrlData = await presignedUrlResponse.json();
+      const { url, key } = presignedUrlData.data;
+
+      const uploadResponse = await fetch(url, {
+        method: "PUT",
+        headers: { "Content-Type": type },
+        body: file,
+      });
+
+      if (!uploadResponse.ok) {
+        throw new Error("File upload to storage failed.");
+      }
+
+      const updateUserResponse = await auth.updateUser({ image: key });
+      if (updateUserResponse.error) {
+        throw new Error(
+          updateUserResponse.error.message || "Failed to update user profile.",
+        );
+      }
+      return updateUserResponse.data;
+    },
+    onMutate: async (file: File) => {
+      const localUrl = URL.createObjectURL(file);
+      setOptimisticAvatarUrl(localUrl);
+      return { localUrl };
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ["currentSession"] });
+    },
+    onError: (
+      _error: Error,
+      _variables,
+      _context?: { localUrl?: string },
+    ) => {},
+    onSettled: (_data, _error, _variables, context?: { localUrl?: string }) => {
+      if (context?.localUrl) {
+        URL.revokeObjectURL(context.localUrl);
+      }
+      if (_error) {
+        setOptimisticAvatarUrl(null);
+      }
+    },
+  });
+
+  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
+    const file = event.target.files?.[0];
+    if (file) {
+      if (file.size > 5 * 1024 * 1024) {
+        setClientSideUploadError("File is too large (max 5MB).");
+        uploadAvatarMutation.reset();
+        return;
+      }
+      if (
+        !["image/jpeg", "image/png", "image/webp", "image/jpg"].includes(
+          file.type,
+        )
+      ) {
+        setClientSideUploadError(
+          "Invalid file type. Please use JPG, PNG or WEBP.",
+        );
+        uploadAvatarMutation.reset();
+        return;
+      }
+      setClientSideUploadError(null);
+      uploadAvatarMutation.mutate(file);
+    }
+    if (event.target) {
+      event.target.value = "";
+    }
+  };
+
+  if (error) {
+    return (
+      <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
+        <Banner variant="error">
+          We could not load your profile information.
+        </Banner>
+      </div>
+    );
+  }
+
+  const imageSrc = optimisticAvatarUrl || imageUrl || "";
+  console.log(imageUrl);
+
+  return (
+    <div className="w-full flex flex-col h-full">
+      <ScrollArea className="flex-1 pb-16 max-h-[85vh] scrollbar-gutter-stable overflow-y-auto">
+        <div className="w-full py-0 px-4 pb-10">
+          <div className="w-full flex flex-col gap-6">
+            <div className="flex flex-col items-center gap-3 pt-6">
+              <div className="relative group">
+                <Avatar className="size-24 border-2 border-muted">
+                  <AvatarImage
+                    src={imageSrc}
+                    alt={`${user?.name || "User"}'s profile picture`}
+                  />
+                  <AvatarFallback className="bg-background text-foreground size-full">
+                    <HugeiconsIcon
+                      icon={User03Icon}
+                      color="currentColor"
+                      strokeWidth={1.5}
+                      size={48}
+                    />
+                  </AvatarFallback>
+                </Avatar>
+                {!uploadAvatarMutation.isPending && (
+                  <label
+                    htmlFor="avatarUpload"
+                    className="absolute inset-0 flex items-center justify-center bg-muted text-primary text-xs font-medium rounded-full cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
+                  >
+                    <HugeiconsIcon
+                      icon={PaintBrush01Icon}
+                      className="text-primary"
+                      size={32}
+                    />
+                  </label>
+                )}
+                {uploadAvatarMutation.isPending && (
+                  <div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-60 rounded-full">
+                    <HugeiconsIcon
+                      icon={Loading02Icon}
+                      className="animate-spin text-foreground"
+                      size={32}
+                    />
+                  </div>
+                )}
+              </div>
+              <Input
+                id="avatarUpload"
+                type="file"
+                className="hidden"
+                accept="image/png, image/jpeg, image/webp, image/gif"
+                onChange={handleFileChange}
+                disabled={uploadAvatarMutation.isPending}
+              />
+              {(uploadAvatarMutation.isError || clientSideUploadError) && (
+                <Banner
+                  variant="error"
+                  className="w-full max-w-sm text-sm text-center"
+                >
+                  {clientSideUploadError ||
+                    uploadAvatarMutation.error?.message ||
+                    "Avatar upload failed."}
+                </Banner>
+              )}
+            </div>
+
+            <Separator />
+
+            <div className="flex w-full justify-between items-center">
+              <h1 className="text-base font-bold text-foreground/90">Name</h1>
+              <span className="text-sm text-foreground/80">
+                {user?.name || "N/A"}
+              </span>
+            </div>
+
+            <Separator />
+
+            <div className="flex w-full justify-between items-center">
+              <h1 className="text-base font-bold text-foreground/90">Email</h1>
+              <span className="text-sm text-foreground/80">
+                {user?.email || "N/A"}
+              </span>
+            </div>
+          </div>
+        </div>
+      </ScrollArea>
+    </div>
+  );
+}
diff --git a/apps/frontend/src/components/settings/SecurityTab.tsx b/apps/frontend/src/components/settings/SecurityTab.tsx
index 96a8ef5..70c9ea4 100644
--- a/apps/frontend/src/components/settings/SecurityTab.tsx
+++ b/apps/frontend/src/components/settings/SecurityTab.tsx
@@ -81,7 +81,6 @@ export default function SecurityTab({
     },
     onSuccess: () => {
       query.invalidateQueries({ queryKey: ["sessions"] });
-      query.invalidateQueries({ queryKey: ["currentSession"] });
     },
     onSettled: (_data, error, variables, context) => {
       const sessionIdToClear =
diff --git a/apps/frontend/src/components/settings/SettingsDialog.tsx b/apps/frontend/src/components/settings/SettingsDialog.tsx
index 98f06dd..72d419e 100644
--- a/apps/frontend/src/components/settings/SettingsDialog.tsx
+++ b/apps/frontend/src/components/settings/SettingsDialog.tsx
@@ -16,6 +16,7 @@ import { HugeiconsIcon } from "@hugeicons/react";
 import { LockKeyIcon, UserSquareIcon } from "@hugeicons/core-free-icons";
 import SecurityTab from "./SecurityTab";
 import { Separator } from "@/components/ui/separator";
+import ProfileTab from "./ProfileTab";
 
 export function SettingsDialog({
   isOpen,
@@ -24,21 +25,7 @@ export function SettingsDialog({
   isOpen: boolean;
   setIsOpen: (open: boolean) => void;
 }) {
-  const {
-    data: currentSessionData,
-    isLoading: isLoadingCurrentSession,
-    error: currentSessionError,
-  } = useQuery({
-    queryKey: ["currentSession"],
-    queryFn: async () => {
-      const response = await auth.getSession();
-      if (response.error) throw response.error;
-      return response.data?.session ?? null;
-    },
-    enabled: isOpen,
-  });
-
-  const currentSessionId = currentSessionData?.id ?? null;
+  const { data: session, isPending, error } = auth.useSession();
 
   const {
     data: sessionsData,
@@ -57,14 +44,14 @@ export function SettingsDialog({
   const sortedSessions = useMemo(() => {
     if (!sessionsData) return null;
     return sessionsData.slice().sort((a, b) => {
-      if (a.id === currentSessionId) return -1;
-      if (b.id === currentSessionId) return 1;
+      if (a.id === session?.session.id) return -1;
+      if (b.id === session?.session.id) return 1;
       return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
     });
-  }, [sessionsData, currentSessionId]);
+  }, [sessionsData, session?.session.id]);
 
-  const isLoading = isLoadingCurrentSession || isLoadingSessions;
-  const fetchError = currentSessionError || sessionsError;
+  const isLoading = isPending || isLoadingSessions;
+  const fetchError = error || sessionsError;
 
   return (
     <Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -104,23 +91,22 @@ export function SettingsDialog({
             <Separator orientation="vertical" className="h-auto" />
 
             <TabsContent
-              tabIndex={-1}
               className="w-full flex-1 h-full overflow-hidden pl-4"
               value="profile"
             >
-              <div className="flex flex-col min-h-[80vh] items-center justify-center">
-                Profile Tab Content
-              </div>
+              <ProfileTab
+                user={session?.user ?? null}
+                error={error as Error | null}
+              />
             </TabsContent>
 
             <TabsContent
-              tabIndex={-1}
               className="w-full flex-1 h-full overflow-hidden pl-4"
               value="security"
             >
               <SecurityTab
                 sessionList={sortedSessions}
-                currentSession={currentSessionData ?? null}
+                currentSession={session?.session ?? null}
                 isLoading={isLoading}
                 error={fetchError as Error | null}
               />
diff --git a/apps/frontend/src/components/sidebar/UserButton.tsx b/apps/frontend/src/components/sidebar/UserButton.tsx
index 0204458..8502fb0 100644
--- a/apps/frontend/src/components/sidebar/UserButton.tsx
+++ b/apps/frontend/src/components/sidebar/UserButton.tsx
@@ -18,6 +18,7 @@ import {
 } from "@hugeicons/core-free-icons";
 import { useState } from "react";
 import { SettingsDialog } from "../settings/SettingsDialog";
+import { useQuery } from "@tanstack/react-query";
 
 export default function UserButton() {
   const { data: session } = auth.useSession();
@@ -28,6 +29,36 @@ export default function UserButton() {
     await auth.signOut();
   }
 
+  const { data: imageUrl } = useQuery({
+    queryKey: ["image-url", session?.user.image],
+    queryFn: async () => {
+      if (!session?.user.image) {
+        return null;
+      }
+
+      const url = new URL(`${process.env.NEXT_PUBLIC_API_URL}/user/get-avatar`);
+      url.searchParams.append("key", session.user.image);
+
+      const presignedUrlResponse = await fetch(url.toString(), {
+        method: "GET",
+        headers: { "Content-Type": "application/json" },
+        credentials: "include",
+      });
+
+      if (!presignedUrlResponse.ok) {
+        throw new Error(
+          `Failed to fetch avatar URL: ${presignedUrlResponse.status}`,
+        );
+      }
+
+      const presignedUrlData = await presignedUrlResponse.json();
+      return presignedUrlData.data || null;
+    },
+    enabled: !!session && !!session.user.image,
+    staleTime: 5 * 60 * 1000,
+    retry: 2,
+  });
+
   return (
     <>
       <DropdownMenu>
@@ -35,7 +66,7 @@ export default function UserButton() {
           <SidebarMenuButton className="py-6 hover:bg-background">
             <Avatar className="size-8 rounded-lg">
               <AvatarImage
-                src={session?.user.image ?? ""}
+                src={imageUrl ?? ""}
                 alt={`${session?.user.name} profile picture`}
               />
               <AvatarFallback className="text-foreground/70">
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e9e672e..6bd7a9d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -39,9 +39,21 @@ importers:
 
   apps/backend:
     dependencies:
+      '@aws-sdk/client-s3':
+        specifier: 3.816.0
+        version: 3.816.0
+      '@aws-sdk/s3-request-presigner':
+        specifier: 3.816.0
+        version: 3.816.0
       '@hono/node-server':
         specifier: 1.2.0
         version: 1.2.0
+      '@hono/zod-validator':
+        specifier: 0.5.0
+        version: 0.5.0(hono@4.7.4)(zod@3.24.2)
+      '@types/uuid':
+        specifier: 10.0.0
+        version: 10.0.0
       better-auth:
         specifier: 1.2.7
         version: 1.2.7
@@ -60,6 +72,9 @@ importers:
       resend:
         specifier: 4.5.0
         version: 4.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      uuid:
+        specifier: 11.1.0
+        version: 11.1.0
       zod:
         specifier: 3.24.2
         version: 3.24.2
@@ -192,6 +207,165 @@ packages:
     resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
     engines: {node: '>=10'}
 
+  '@aws-crypto/crc32@5.2.0':
+    resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-crypto/crc32c@5.2.0':
+    resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==}
+
+  '@aws-crypto/sha1-browser@5.2.0':
+    resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==}
+
+  '@aws-crypto/sha256-browser@5.2.0':
+    resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
+
+  '@aws-crypto/sha256-js@5.2.0':
+    resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-crypto/supports-web-crypto@5.2.0':
+    resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==}
+
+  '@aws-crypto/util@5.2.0':
+    resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
+
+  '@aws-sdk/client-s3@3.816.0':
+    resolution: {integrity: sha512-WsVDoXrQ6YWyse04EJyP2vg7dGcHbb5cYtTUZTDwbldsNBu8IH67pTx61lDh07cXeav4UwdzJvSD86zTT7nHPg==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/client-sso@3.816.0':
+    resolution: {integrity: sha512-2D2bc6wVDgGxwHjqSyxGYXUHa3ni0R21Isiq0OzJk6nBc8Vi6HpYoimok6UaUfPJsG6OEWfQ9vubcR30BCBmqQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/core@3.816.0':
+    resolution: {integrity: sha512-Lx50wjtyarzKpMFV6V+gjbSZDgsA/71iyifbClGUSiNPoIQ4OCV0KVOmAAj7mQRVvGJqUMWKVM+WzK79CjbjWA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/credential-provider-env@3.816.0':
+    resolution: {integrity: sha512-wUJZwRLe+SxPxRV9AENYBLrJZRrNIo+fva7ZzejsC83iz7hdfq6Rv6B/aHEdPwG/nQC4+q7UUvcRPlomyrpsBA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/credential-provider-http@3.816.0':
+    resolution: {integrity: sha512-gcWGzMQ7yRIF+ljTkR8Vzp7727UY6cmeaPrFQrvcFB8PhOqWpf7g0JsgOf5BSaP8CkkSQcTQHc0C5ZYAzUFwPg==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/credential-provider-ini@3.816.0':
+    resolution: {integrity: sha512-60FiF4PFgrnIQ7vizAMN4imrjFzlXPEvXKk05cs47W7qtA54MudRvTITN9Gji4vD5j6CDp9fOb0HS0+N83YEAA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/credential-provider-node@3.816.0':
+    resolution: {integrity: sha512-NsjMzCHWXpFXNdJYliY4GM2vtsmP8w9OyjrNcHq4mmdp7lAzM3BvRKavVuhBXrSHQFzLkGWaTGd+fkkTZtYHXQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/credential-provider-process@3.816.0':
+    resolution: {integrity: sha512-9Tm+AxMoV2Izvl5b9tyMQRbBwaex8JP06HN7ZeCXgC5sAsSN+o8dsThnEhf8jKN+uBpT6CLWKN1TXuUMrAmW1A==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/credential-provider-sso@3.816.0':
+    resolution: {integrity: sha512-wxNqvPrRpKwkNQ41uGE/y3Y95TsWnJnMpcTIMhGEub0Jq5ifnUEev3gJ60857uaut133mwpGfupE2fk8V68e6A==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/credential-provider-web-identity@3.816.0':
+    resolution: {integrity: sha512-guKv76eRTMQkGOHsv9tY8XP9y1TZ0U4w6RD5kHQYNuzoBf5HOpZcib9OGmWJA2n+BC+byCOOftl6mMmYtRfcpQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-bucket-endpoint@3.808.0':
+    resolution: {integrity: sha512-wEPlNcs8dir9lXbuviEGtSzYSxG/NRKQrJk5ybOc7OpPGHovsN+QhDOdY3lcjOFdwMTiMIG9foUkPz3zBpLB1A==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-expect-continue@3.804.0':
+    resolution: {integrity: sha512-YW1hySBolALMII6C8y7Z0CRG2UX1dGJjLEBNFeefhO/xP7ZuE1dvnmfJGaEuBMnvc3wkRS63VZ3aqX6sevM1CA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-flexible-checksums@3.816.0':
+    resolution: {integrity: sha512-kftcwDxB/VoCBsUiRgkm5CIuKbTfCN1WLPbis9LRwX3kQhKgGVxG2gG78SHk4TBB0qviWVAd/t+i/KaUgwiAcA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-host-header@3.804.0':
+    resolution: {integrity: sha512-bum1hLVBrn2lJCi423Z2fMUYtsbkGI2s4N+2RI2WSjvbaVyMSv/WcejIrjkqiiMR+2Y7m5exgoKeg4/TODLDPQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-location-constraint@3.804.0':
+    resolution: {integrity: sha512-AMtKnllIWKgoo7hiJfphLYotEwTERfjVMO2+cKAncz9w1g+bnYhHxiVhJJoR94y047c06X4PU5MsTxvdQ73Znw==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-logger@3.804.0':
+    resolution: {integrity: sha512-w/qLwL3iq0KOPQNat0Kb7sKndl9BtceigINwBU7SpkYWX9L/Lem6f8NPEKrC9Tl4wDBht3Yztub4oRTy/horJA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-recursion-detection@3.804.0':
+    resolution: {integrity: sha512-zqHOrvLRdsUdN/ehYfZ9Tf8svhbiLLz5VaWUz22YndFv6m9qaAcijkpAOlKexsv3nLBMJdSdJ6GUTAeIy3BZzw==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-sdk-s3@3.816.0':
+    resolution: {integrity: sha512-jJ+EAXM7gnOwiCM6rrl4AUNY5urmtIsX7roTkxtb4DevJxcS+wFYRRg3/j33fQbuxQZrvk21HqxyZYx5UH70PA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-ssec@3.804.0':
+    resolution: {integrity: sha512-Tk8jK0gOIUBvEPTz/wwSlP1V70zVQ3QYqsLPAjQRMO6zfOK9ax31dln3MgKvFDJxBydS2tS3wsn53v+brxDxTA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/middleware-user-agent@3.816.0':
+    resolution: {integrity: sha512-bHRSlWZ0xDsFR8E2FwDb//0Ff6wMkVx4O+UKsfyNlAbtqCiiHRt5ANNfKPafr95cN2CCxLxiPvFTFVblQM5TsQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/nested-clients@3.816.0':
+    resolution: {integrity: sha512-N1jSEa53QHLvIH37Rrm8cD9MlC6ucTEpDhbO+fN9UHBimRnpt9kqk0iiBCu5FK7GEKWYM5kiawfUtbB3PN65CQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/region-config-resolver@3.808.0':
+    resolution: {integrity: sha512-9x2QWfphkARZY5OGkl9dJxZlSlYM2l5inFeo2bKntGuwg4A4YUe5h7d5yJ6sZbam9h43eBrkOdumx03DAkQF9A==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/s3-request-presigner@3.816.0':
+    resolution: {integrity: sha512-zxHLYmGGd2uaBITm4+PYSKCu1QL9DT65vyJnn/OuJjAdXc9FPD5JZ33cCu8R++ThPYLOqEV/OUJkWBDZZv3NDQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/signature-v4-multi-region@3.816.0':
+    resolution: {integrity: sha512-idcr9NW86sSIXASSej3423Selu6fxlhhJJtMgpAqoCH/HJh1eQrONJwNKuI9huiruPE8+02pwxuePvLW46X2mw==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/token-providers@3.816.0':
+    resolution: {integrity: sha512-OFeeq7j3JsI5OrUW4vw/Lckwq8I+HNffi7IXenfqzE83vhgXQF9BfCg+TGHDfxiDt6NUaS6F0QzhfkW6/AORlA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/types@3.804.0':
+    resolution: {integrity: sha512-A9qnsy9zQ8G89vrPPlNG9d1d8QcKRGqJKqwyGgS0dclJpwy6d1EWgQLIolKPl6vcFpLoe6avLOLxr+h8ur5wpg==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/util-arn-parser@3.804.0':
+    resolution: {integrity: sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/util-endpoints@3.808.0':
+    resolution: {integrity: sha512-N6Lic98uc4ADB7fLWlzx+1uVnq04VgVjngZvwHoujcRg9YDhIg9dUDiTzD5VZv13g1BrPYmvYP1HhsildpGV6w==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/util-format-url@3.804.0':
+    resolution: {integrity: sha512-1nOwSg7B0bj5LFGor0udF/HSdvDuSCxP+NC0IuSOJ5RgJ2AphFo03pLtK2UwArHY5WWZaejAEz5VBND6xxOEhA==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/util-locate-window@3.804.0':
+    resolution: {integrity: sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==}
+    engines: {node: '>=18.0.0'}
+
+  '@aws-sdk/util-user-agent-browser@3.804.0':
+    resolution: {integrity: sha512-KfW6T6nQHHM/vZBBdGn6fMyG/MgX5lq82TDdX4HRQRRuHKLgBWGpKXqqvBwqIaCdXwWHgDrg2VQups6GqOWW2A==}
+
+  '@aws-sdk/util-user-agent-node@3.816.0':
+    resolution: {integrity: sha512-Q6dxmuj4hL7pudhrneWEQ7yVHIQRBFr0wqKLF1opwOi1cIePuoEbPyJ2jkel6PDEv1YMfvsAKaRshp6eNA8VHg==}
+    engines: {node: '>=18.0.0'}
+    peerDependencies:
+      aws-crt: '>=1.0.0'
+    peerDependenciesMeta:
+      aws-crt:
+        optional: true
+
+  '@aws-sdk/xml-builder@3.804.0':
+    resolution: {integrity: sha512-JbGWp36IG9dgxtvC6+YXwt5WDZYfuamWFtVfK6fQpnmL96dx+GUPOXPKRWdw67WLKf2comHY28iX2d3z35I53Q==}
+    engines: {node: '>=18.0.0'}
+
   '@babel/code-frame@7.26.2':
     resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
     engines: {node: '>=6.9.0'}
@@ -738,6 +912,12 @@ packages:
     resolution: {integrity: sha512-aHT8lDMLpd7ioXJ1/057+h+oE/k7rCOWmjklYDsE0jE4CoNB9XzG4f8dRHvw4s5HJFocaYDiGgYM/V0kYbQ0ww==}
     engines: {node: '>=18.0.0'}
 
+  '@hono/zod-validator@0.5.0':
+    resolution: {integrity: sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg==}
+    peerDependencies:
+      hono: '>=3.9.0'
+      zod: ^3.19.1
+
   '@hugeicons/core-free-icons@1.0.14':
     resolution: {integrity: sha512-0vY3lyMDZVgA+wBJHjuvOAx6y0Mg0cBgYNsHt8Rw5eAXZ0LqM3T3xwIrovEJ1TtusUwFPb7QUBi1Ubux4VhQJg==}
 
@@ -1466,6 +1646,218 @@ packages:
     resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==}
     engines: {node: '>=20.0.0'}
 
+  '@smithy/abort-controller@4.0.3':
+    resolution: {integrity: sha512-AqXFf6DXnuRBXy4SoK/n1mfgHaKaq36bmkphmD1KO0nHq6xK/g9KHSW4HEsPQUBCGdIEfuJifGHwxFXPIFay9Q==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/chunked-blob-reader-native@4.0.0':
+    resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/chunked-blob-reader@5.0.0':
+    resolution: {integrity: sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/config-resolver@4.1.3':
+    resolution: {integrity: sha512-N5e7ofiyYDmHxnPnqF8L4KtsbSDwyxFRfDK9bp1d9OyPO4ytRLd0/XxCqi5xVaaqB65v4woW8uey6jND6zxzxQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/core@3.4.0':
+    resolution: {integrity: sha512-dDYISQo7k0Ml/rXlFIjkTmTcQze/LxhtIRAEmZ6HJ/EI0inVxVEVnrUXJ7jPx6ZP0GHUhFm40iQcCgS5apXIXA==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/credential-provider-imds@4.0.5':
+    resolution: {integrity: sha512-saEAGwrIlkb9XxX/m5S5hOtzjoJPEK6Qw2f9pYTbIsMPOFyGSXBBTw95WbOyru8A1vIS2jVCCU1Qhz50QWG3IA==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/eventstream-codec@4.0.3':
+    resolution: {integrity: sha512-V22KIPXZsE2mc4zEgYGANM/7UbL9jWlOACEolyGyMuTY+jjHJ2PQ0FdopOTS1CS7u6PlAkALmypkv2oQ4aftcg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/eventstream-serde-browser@4.0.3':
+    resolution: {integrity: sha512-oe1d/tfCGVZBMX8O6HApaM4G+fF9JNdyLP7tWXt00epuL/kLOdp/4o9VqheLFeJaXgao+9IaBgs/q/oM48hxzg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/eventstream-serde-config-resolver@4.1.1':
+    resolution: {integrity: sha512-XXCPGjRNwpFWHKQJMKIjGLfFKYULYckFnxGcWmBC2mBf3NsrvUKgqHax4NCqc0TfbDAimPDHOc6HOKtzsXK9Gw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/eventstream-serde-node@4.0.3':
+    resolution: {integrity: sha512-HOEbRmm9TrikCoFrypYu0J/gC4Lsk8gl5LtOz1G3laD2Jy44+ht2Pd2E9qjNQfhMJIzKDZ/gbuUH0s0v4kWQ0A==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/eventstream-serde-universal@4.0.3':
+    resolution: {integrity: sha512-ShOP512CZrYI9n+h64PJ84udzoNHUQtPddyh1j175KNTKsSnMEDNscOWJWyEoLQiuhWWw51lSa+k6ea9ZGXcRg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/fetch-http-handler@5.0.3':
+    resolution: {integrity: sha512-yBZwavI31roqTndNI7ONHqesfH01JmjJK6L3uUpZAhyAmr86LN5QiPzfyZGIxQmed8VEK2NRSQT3/JX5V1njfQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/hash-blob-browser@4.0.3':
+    resolution: {integrity: sha512-37wZYU/XI2cOF4hgNDNMzZNAuNtJTkZFWxcpagQrnf6PYU/6sJ6y5Ey9Bp4vzi9nteex/ImxAugfsF3XGLrqWA==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/hash-node@4.0.3':
+    resolution: {integrity: sha512-W5Uhy6v/aYrgtjh9y0YP332gIQcwccQ+EcfWhllL0B9rPae42JngTTUpb8W6wuxaNFzqps4xq5klHckSSOy5fw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/hash-stream-node@4.0.3':
+    resolution: {integrity: sha512-CAwAvztwGYHHZGGcXtbinNxytaj5FNZChz8V+o7eNUAi5BgVqnF91Z3cJSmaE9O7FYUQVrIzGAB25Aok9T5KHQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/invalid-dependency@4.0.3':
+    resolution: {integrity: sha512-1Bo8Ur1ZGqxvwTqBmv6DZEn0rXtwJGeqiiO2/JFcCtz3nBakOqeXbJBElXJMMzd0ghe8+eB6Dkw98nMYctgizg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/is-array-buffer@2.2.0':
+    resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==}
+    engines: {node: '>=14.0.0'}
+
+  '@smithy/is-array-buffer@4.0.0':
+    resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/md5-js@4.0.3':
+    resolution: {integrity: sha512-m95Z+1UJFPq4cv/R6TPMLYkoau7cNJYA5GLuuUJjfmF+Zrad4yaupIWeGGzIinf8pD1L+CIAxjh8eowPvyL7Dw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/middleware-content-length@4.0.3':
+    resolution: {integrity: sha512-NE/Zph4BP5u16bzYq2csq9qD0T6UBLeg4AuNrwNJ7Gv9uLYaGEgelZUOdRndGdMGcUfSGvNlXGb2aA2hPCwJ6g==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/middleware-endpoint@4.1.7':
+    resolution: {integrity: sha512-KDzM7Iajo6K7eIWNNtukykRT4eWwlHjCEsULZUaSfi/SRSBK8BPRqG5FsVfp58lUxcvre8GT8AIPIqndA0ERKw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/middleware-retry@4.1.8':
+    resolution: {integrity: sha512-e2OtQgFzzlSG0uCjcJmi02QuFSRTrpT11Eh2EcqqDFy7DYriteHZJkkf+4AsxsrGDugAtPFcWBz1aq06sSX5fQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/middleware-serde@4.0.6':
+    resolution: {integrity: sha512-YECyl7uNII+jCr/9qEmCu8xYL79cU0fqjo0qxpcVIU18dAPHam/iYwcknAu4Jiyw1uN+sAx7/SMf/Kmef/Jjsg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/middleware-stack@4.0.3':
+    resolution: {integrity: sha512-baeV7t4jQfQtFxBADFmnhmqBmqR38dNU5cvEgHcMK/Kp3D3bEI0CouoX2Sr/rGuntR+Eg0IjXdxnGGTc6SbIkw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/node-config-provider@4.1.2':
+    resolution: {integrity: sha512-SUvNup8iU1v7fmM8XPk+27m36udmGCfSz+VZP5Gb0aJ3Ne0X28K/25gnsrg3X1rWlhcnhzNUUysKW/Ied46ivQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/node-http-handler@4.0.5':
+    resolution: {integrity: sha512-T7QglZC1vS7SPT44/1qSIAQEx5bFKb3LfO6zw/o4Xzt1eC5HNoH1TkS4lMYA9cWFbacUhx4hRl/blLun4EOCkg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/property-provider@4.0.3':
+    resolution: {integrity: sha512-Wcn17QNdawJZcZZPBuMuzyBENVi1AXl4TdE0jvzo4vWX2x5df/oMlmr/9M5XAAC6+yae4kWZlOYIsNsgDrMU9A==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/protocol-http@5.1.1':
+    resolution: {integrity: sha512-Vsay2mzq05DwNi9jK01yCFtfvu9HimmgC7a4HTs7lhX12Sx8aWsH0mfz6q/02yspSp+lOB+Q2HJwi4IV2GKz7A==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/querystring-builder@4.0.3':
+    resolution: {integrity: sha512-UUzIWMVfPmDZcOutk2/r1vURZqavvQW0OHvgsyNV0cKupChvqg+/NKPRMaMEe+i8tP96IthMFeZOZWpV+E4RAw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/querystring-parser@4.0.3':
+    resolution: {integrity: sha512-K5M4ZJQpFCblOJ5Oyw7diICpFg1qhhR47m2/5Ef1PhGE19RaIZf50tjYFrxa6usqcuXyTiFPGo4d1geZdH4YcQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/service-error-classification@4.0.4':
+    resolution: {integrity: sha512-W5ScbQ1bTzgH91kNEE2CvOzM4gXlDOqdow4m8vMFSIXCel2scbHwjflpVNnC60Y3F1m5i7w2gQg9lSnR+JsJAA==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/shared-ini-file-loader@4.0.3':
+    resolution: {integrity: sha512-vHwlrqhZGIoLwaH8vvIjpHnloShqdJ7SUPNM2EQtEox+yEDFTVQ7E+DLZ+6OhnYEgFUwPByJyz6UZaOu2tny6A==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/signature-v4@5.1.1':
+    resolution: {integrity: sha512-zy8Repr5zvT0ja+Tf5wjV/Ba6vRrhdiDcp/ww6cvqYbSEudIkziDe3uppNRlFoCViyJXdPnLcwyZdDLA4CHzSg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/smithy-client@4.3.0':
+    resolution: {integrity: sha512-DNsRA38pN6tYHUjebmwD9e4KcgqTLldYQb2gC6K+oxXYdCTxPn6wV9+FvOa6wrU2FQEnGJoi+3GULzOTKck/tg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/types@4.3.0':
+    resolution: {integrity: sha512-+1iaIQHthDh9yaLhRzaoQxRk+l9xlk+JjMFxGRhNLz+m9vKOkjNeU8QuB4w3xvzHyVR/BVlp/4AXDHjoRIkfgQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/url-parser@4.0.3':
+    resolution: {integrity: sha512-n5/DnosDu/tweOqUUNtUbu7eRIR4J/Wz9nL7V5kFYQQVb8VYdj7a4G5NJHCw6o21ul7CvZoJkOpdTnsQDLT0tQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-base64@4.0.0':
+    resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-body-length-browser@4.0.0':
+    resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-body-length-node@4.0.0':
+    resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-buffer-from@2.2.0':
+    resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==}
+    engines: {node: '>=14.0.0'}
+
+  '@smithy/util-buffer-from@4.0.0':
+    resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-config-provider@4.0.0':
+    resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-defaults-mode-browser@4.0.15':
+    resolution: {integrity: sha512-bJJ/B8owQbHAflatSq92f9OcV8858DJBQF1Y3GRjB8psLyUjbISywszYPFw16beREHO/C3I3taW4VGH+tOuwrQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-defaults-mode-node@4.0.15':
+    resolution: {integrity: sha512-8CUrEW2Ni5q+NmYkj8wsgkfqoP7l4ZquptFbq92yQE66xevc4SxqP2zH6tMtN158kgBqBDsZ+qlrRwXWOjCR8A==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-endpoints@3.0.5':
+    resolution: {integrity: sha512-PjDpqLk24/vAl340tmtCA++Q01GRRNH9cwL9qh46NspAX9S+IQVcK+GOzPt0GLJ6KYGyn8uOgo2kvJhiThclJw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-hex-encoding@4.0.0':
+    resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-middleware@4.0.3':
+    resolution: {integrity: sha512-iIsC6qZXxkD7V3BzTw3b1uK8RVC1M8WvwNxK1PKrH9FnxntCd30CSunXjL/8iJBE8Z0J14r2P69njwIpRG4FBQ==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-retry@4.0.4':
+    resolution: {integrity: sha512-Aoqr9W2jDYGrI6OxljN8VmLDQIGO4VdMAUKMf9RGqLG8hn6or+K41NEy1Y5dtum9q8F7e0obYAuKl2mt/GnpZg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-stream@4.2.1':
+    resolution: {integrity: sha512-W3IR0x5DY6iVtjj5p902oNhD+Bz7vs5S+p6tppbPa509rV9BdeXZjGuRSCtVEad9FA0Mba+tNUtUmtnSI1nwUw==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-uri-escape@4.0.0':
+    resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-utf8@2.3.0':
+    resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==}
+    engines: {node: '>=14.0.0'}
+
+  '@smithy/util-utf8@4.0.0':
+    resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==}
+    engines: {node: '>=18.0.0'}
+
+  '@smithy/util-waiter@4.0.4':
+    resolution: {integrity: sha512-73aeIvHjtSB6fd9I08iFaQIGTICKpLrI3EtlWAkStVENGo1ARMq9qdoD4QwkY0RUp6A409xlgbD9NCCfCF5ieg==}
+    engines: {node: '>=18.0.0'}
+
   '@swc/counter@0.1.3':
     resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
 
@@ -1606,6 +1998,9 @@ packages:
   '@types/react@19.0.12':
     resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==}
 
+  '@types/uuid@10.0.0':
+    resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
+
   '@vitest/expect@3.1.1':
     resolution: {integrity: sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==}
 
@@ -1680,6 +2075,9 @@ packages:
     resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
     engines: {node: '>=8'}
 
+  bowser@2.11.0:
+    resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==}
+
   brace-expansion@1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
 
@@ -2019,6 +2417,10 @@ packages:
   fast-safe-stringify@2.1.1:
     resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
 
+  fast-xml-parser@4.4.1:
+    resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
+    hasBin: true
+
   fastq@1.19.1:
     resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
 
@@ -2668,6 +3070,9 @@ packages:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
 
+  strnum@1.1.2:
+    resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
+
   styled-jsx@5.1.6:
     resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
     engines: {node: '>= 12.0.0'}
@@ -2825,6 +3230,14 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
 
+  uuid@11.1.0:
+    resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
+    hasBin: true
+
+  uuid@9.0.1:
+    resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
+    hasBin: true
+
   vite-node@3.1.1:
     resolution: {integrity: sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -2947,6 +3360,483 @@ snapshots:
 
   '@alloc/quick-lru@5.2.0': {}
 
+  '@aws-crypto/crc32@5.2.0':
+    dependencies:
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.804.0
+      tslib: 2.8.1
+
+  '@aws-crypto/crc32c@5.2.0':
+    dependencies:
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.804.0
+      tslib: 2.8.1
+
+  '@aws-crypto/sha1-browser@5.2.0':
+    dependencies:
+      '@aws-crypto/supports-web-crypto': 5.2.0
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-locate-window': 3.804.0
+      '@smithy/util-utf8': 2.3.0
+      tslib: 2.8.1
+
+  '@aws-crypto/sha256-browser@5.2.0':
+    dependencies:
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-crypto/supports-web-crypto': 5.2.0
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-locate-window': 3.804.0
+      '@smithy/util-utf8': 2.3.0
+      tslib: 2.8.1
+
+  '@aws-crypto/sha256-js@5.2.0':
+    dependencies:
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.804.0
+      tslib: 2.8.1
+
+  '@aws-crypto/supports-web-crypto@5.2.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@aws-crypto/util@5.2.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/util-utf8': 2.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/client-s3@3.816.0':
+    dependencies:
+      '@aws-crypto/sha1-browser': 5.2.0
+      '@aws-crypto/sha256-browser': 5.2.0
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/credential-provider-node': 3.816.0
+      '@aws-sdk/middleware-bucket-endpoint': 3.808.0
+      '@aws-sdk/middleware-expect-continue': 3.804.0
+      '@aws-sdk/middleware-flexible-checksums': 3.816.0
+      '@aws-sdk/middleware-host-header': 3.804.0
+      '@aws-sdk/middleware-location-constraint': 3.804.0
+      '@aws-sdk/middleware-logger': 3.804.0
+      '@aws-sdk/middleware-recursion-detection': 3.804.0
+      '@aws-sdk/middleware-sdk-s3': 3.816.0
+      '@aws-sdk/middleware-ssec': 3.804.0
+      '@aws-sdk/middleware-user-agent': 3.816.0
+      '@aws-sdk/region-config-resolver': 3.808.0
+      '@aws-sdk/signature-v4-multi-region': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-endpoints': 3.808.0
+      '@aws-sdk/util-user-agent-browser': 3.804.0
+      '@aws-sdk/util-user-agent-node': 3.816.0
+      '@aws-sdk/xml-builder': 3.804.0
+      '@smithy/config-resolver': 4.1.3
+      '@smithy/core': 3.4.0
+      '@smithy/eventstream-serde-browser': 4.0.3
+      '@smithy/eventstream-serde-config-resolver': 4.1.1
+      '@smithy/eventstream-serde-node': 4.0.3
+      '@smithy/fetch-http-handler': 5.0.3
+      '@smithy/hash-blob-browser': 4.0.3
+      '@smithy/hash-node': 4.0.3
+      '@smithy/hash-stream-node': 4.0.3
+      '@smithy/invalid-dependency': 4.0.3
+      '@smithy/md5-js': 4.0.3
+      '@smithy/middleware-content-length': 4.0.3
+      '@smithy/middleware-endpoint': 4.1.7
+      '@smithy/middleware-retry': 4.1.8
+      '@smithy/middleware-serde': 4.0.6
+      '@smithy/middleware-stack': 4.0.3
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/node-http-handler': 4.0.5
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      '@smithy/url-parser': 4.0.3
+      '@smithy/util-base64': 4.0.0
+      '@smithy/util-body-length-browser': 4.0.0
+      '@smithy/util-body-length-node': 4.0.0
+      '@smithy/util-defaults-mode-browser': 4.0.15
+      '@smithy/util-defaults-mode-node': 4.0.15
+      '@smithy/util-endpoints': 3.0.5
+      '@smithy/util-middleware': 4.0.3
+      '@smithy/util-retry': 4.0.4
+      '@smithy/util-stream': 4.2.1
+      '@smithy/util-utf8': 4.0.0
+      '@smithy/util-waiter': 4.0.4
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/client-sso@3.816.0':
+    dependencies:
+      '@aws-crypto/sha256-browser': 5.2.0
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/middleware-host-header': 3.804.0
+      '@aws-sdk/middleware-logger': 3.804.0
+      '@aws-sdk/middleware-recursion-detection': 3.804.0
+      '@aws-sdk/middleware-user-agent': 3.816.0
+      '@aws-sdk/region-config-resolver': 3.808.0
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-endpoints': 3.808.0
+      '@aws-sdk/util-user-agent-browser': 3.804.0
+      '@aws-sdk/util-user-agent-node': 3.816.0
+      '@smithy/config-resolver': 4.1.3
+      '@smithy/core': 3.4.0
+      '@smithy/fetch-http-handler': 5.0.3
+      '@smithy/hash-node': 4.0.3
+      '@smithy/invalid-dependency': 4.0.3
+      '@smithy/middleware-content-length': 4.0.3
+      '@smithy/middleware-endpoint': 4.1.7
+      '@smithy/middleware-retry': 4.1.8
+      '@smithy/middleware-serde': 4.0.6
+      '@smithy/middleware-stack': 4.0.3
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/node-http-handler': 4.0.5
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      '@smithy/url-parser': 4.0.3
+      '@smithy/util-base64': 4.0.0
+      '@smithy/util-body-length-browser': 4.0.0
+      '@smithy/util-body-length-node': 4.0.0
+      '@smithy/util-defaults-mode-browser': 4.0.15
+      '@smithy/util-defaults-mode-node': 4.0.15
+      '@smithy/util-endpoints': 3.0.5
+      '@smithy/util-middleware': 4.0.3
+      '@smithy/util-retry': 4.0.4
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/core@3.816.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/core': 3.4.0
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/property-provider': 4.0.3
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/signature-v4': 5.1.1
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      '@smithy/util-middleware': 4.0.3
+      fast-xml-parser: 4.4.1
+      tslib: 2.8.1
+
+  '@aws-sdk/credential-provider-env@3.816.0':
+    dependencies:
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/property-provider': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/credential-provider-http@3.816.0':
+    dependencies:
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/fetch-http-handler': 5.0.3
+      '@smithy/node-http-handler': 4.0.5
+      '@smithy/property-provider': 4.0.3
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      '@smithy/util-stream': 4.2.1
+      tslib: 2.8.1
+
+  '@aws-sdk/credential-provider-ini@3.816.0':
+    dependencies:
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/credential-provider-env': 3.816.0
+      '@aws-sdk/credential-provider-http': 3.816.0
+      '@aws-sdk/credential-provider-process': 3.816.0
+      '@aws-sdk/credential-provider-sso': 3.816.0
+      '@aws-sdk/credential-provider-web-identity': 3.816.0
+      '@aws-sdk/nested-clients': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/credential-provider-imds': 4.0.5
+      '@smithy/property-provider': 4.0.3
+      '@smithy/shared-ini-file-loader': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/credential-provider-node@3.816.0':
+    dependencies:
+      '@aws-sdk/credential-provider-env': 3.816.0
+      '@aws-sdk/credential-provider-http': 3.816.0
+      '@aws-sdk/credential-provider-ini': 3.816.0
+      '@aws-sdk/credential-provider-process': 3.816.0
+      '@aws-sdk/credential-provider-sso': 3.816.0
+      '@aws-sdk/credential-provider-web-identity': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/credential-provider-imds': 4.0.5
+      '@smithy/property-provider': 4.0.3
+      '@smithy/shared-ini-file-loader': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/credential-provider-process@3.816.0':
+    dependencies:
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/property-provider': 4.0.3
+      '@smithy/shared-ini-file-loader': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/credential-provider-sso@3.816.0':
+    dependencies:
+      '@aws-sdk/client-sso': 3.816.0
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/token-providers': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/property-provider': 4.0.3
+      '@smithy/shared-ini-file-loader': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/credential-provider-web-identity@3.816.0':
+    dependencies:
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/nested-clients': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/property-provider': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/middleware-bucket-endpoint@3.808.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-arn-parser': 3.804.0
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      '@smithy/util-config-provider': 4.0.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-expect-continue@3.804.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-flexible-checksums@3.816.0':
+    dependencies:
+      '@aws-crypto/crc32': 5.2.0
+      '@aws-crypto/crc32c': 5.2.0
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/is-array-buffer': 4.0.0
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      '@smithy/util-middleware': 4.0.3
+      '@smithy/util-stream': 4.2.1
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-host-header@3.804.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-location-constraint@3.804.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-logger@3.804.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-recursion-detection@3.804.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-sdk-s3@3.816.0':
+    dependencies:
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-arn-parser': 3.804.0
+      '@smithy/core': 3.4.0
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/signature-v4': 5.1.1
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      '@smithy/util-config-provider': 4.0.0
+      '@smithy/util-middleware': 4.0.3
+      '@smithy/util-stream': 4.2.1
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-ssec@3.804.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/middleware-user-agent@3.816.0':
+    dependencies:
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-endpoints': 3.808.0
+      '@smithy/core': 3.4.0
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/nested-clients@3.816.0':
+    dependencies:
+      '@aws-crypto/sha256-browser': 5.2.0
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/middleware-host-header': 3.804.0
+      '@aws-sdk/middleware-logger': 3.804.0
+      '@aws-sdk/middleware-recursion-detection': 3.804.0
+      '@aws-sdk/middleware-user-agent': 3.816.0
+      '@aws-sdk/region-config-resolver': 3.808.0
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-endpoints': 3.808.0
+      '@aws-sdk/util-user-agent-browser': 3.804.0
+      '@aws-sdk/util-user-agent-node': 3.816.0
+      '@smithy/config-resolver': 4.1.3
+      '@smithy/core': 3.4.0
+      '@smithy/fetch-http-handler': 5.0.3
+      '@smithy/hash-node': 4.0.3
+      '@smithy/invalid-dependency': 4.0.3
+      '@smithy/middleware-content-length': 4.0.3
+      '@smithy/middleware-endpoint': 4.1.7
+      '@smithy/middleware-retry': 4.1.8
+      '@smithy/middleware-serde': 4.0.6
+      '@smithy/middleware-stack': 4.0.3
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/node-http-handler': 4.0.5
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      '@smithy/url-parser': 4.0.3
+      '@smithy/util-base64': 4.0.0
+      '@smithy/util-body-length-browser': 4.0.0
+      '@smithy/util-body-length-node': 4.0.0
+      '@smithy/util-defaults-mode-browser': 4.0.15
+      '@smithy/util-defaults-mode-node': 4.0.15
+      '@smithy/util-endpoints': 3.0.5
+      '@smithy/util-middleware': 4.0.3
+      '@smithy/util-retry': 4.0.4
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/region-config-resolver@3.808.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/types': 4.3.0
+      '@smithy/util-config-provider': 4.0.0
+      '@smithy/util-middleware': 4.0.3
+      tslib: 2.8.1
+
+  '@aws-sdk/s3-request-presigner@3.816.0':
+    dependencies:
+      '@aws-sdk/signature-v4-multi-region': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@aws-sdk/util-format-url': 3.804.0
+      '@smithy/middleware-endpoint': 4.1.7
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/signature-v4-multi-region@3.816.0':
+    dependencies:
+      '@aws-sdk/middleware-sdk-s3': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/signature-v4': 5.1.1
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/token-providers@3.816.0':
+    dependencies:
+      '@aws-sdk/core': 3.816.0
+      '@aws-sdk/nested-clients': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/property-provider': 4.0.3
+      '@smithy/shared-ini-file-loader': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/types@3.804.0':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/util-arn-parser@3.804.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@aws-sdk/util-endpoints@3.808.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/types': 4.3.0
+      '@smithy/util-endpoints': 3.0.5
+      tslib: 2.8.1
+
+  '@aws-sdk/util-format-url@3.804.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/querystring-builder': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/util-locate-window@3.804.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@aws-sdk/util-user-agent-browser@3.804.0':
+    dependencies:
+      '@aws-sdk/types': 3.804.0
+      '@smithy/types': 4.3.0
+      bowser: 2.11.0
+      tslib: 2.8.1
+
+  '@aws-sdk/util-user-agent-node@3.816.0':
+    dependencies:
+      '@aws-sdk/middleware-user-agent': 3.816.0
+      '@aws-sdk/types': 3.804.0
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@aws-sdk/xml-builder@3.804.0':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
   '@babel/code-frame@7.26.2':
     dependencies:
       '@babel/helper-validator-identifier': 7.25.9
@@ -3281,6 +4171,11 @@ snapshots:
 
   '@hono/node-server@1.2.0': {}
 
+  '@hono/zod-validator@0.5.0(hono@4.7.4)(zod@3.24.2)':
+    dependencies:
+      hono: 4.7.4
+      zod: 3.24.2
+
   '@hugeicons/core-free-icons@1.0.14': {}
 
   '@hugeicons/react@1.0.5(react@19.0.0)':
@@ -3908,6 +4803,338 @@ snapshots:
       '@peculiar/asn1-schema': 2.3.15
       '@peculiar/asn1-x509': 2.3.15
 
+  '@smithy/abort-controller@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/chunked-blob-reader-native@4.0.0':
+    dependencies:
+      '@smithy/util-base64': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/chunked-blob-reader@5.0.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/config-resolver@4.1.3':
+    dependencies:
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/types': 4.3.0
+      '@smithy/util-config-provider': 4.0.0
+      '@smithy/util-middleware': 4.0.3
+      tslib: 2.8.1
+
+  '@smithy/core@3.4.0':
+    dependencies:
+      '@smithy/middleware-serde': 4.0.6
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      '@smithy/util-body-length-browser': 4.0.0
+      '@smithy/util-middleware': 4.0.3
+      '@smithy/util-stream': 4.2.1
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/credential-provider-imds@4.0.5':
+    dependencies:
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/property-provider': 4.0.3
+      '@smithy/types': 4.3.0
+      '@smithy/url-parser': 4.0.3
+      tslib: 2.8.1
+
+  '@smithy/eventstream-codec@4.0.3':
+    dependencies:
+      '@aws-crypto/crc32': 5.2.0
+      '@smithy/types': 4.3.0
+      '@smithy/util-hex-encoding': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/eventstream-serde-browser@4.0.3':
+    dependencies:
+      '@smithy/eventstream-serde-universal': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/eventstream-serde-config-resolver@4.1.1':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/eventstream-serde-node@4.0.3':
+    dependencies:
+      '@smithy/eventstream-serde-universal': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/eventstream-serde-universal@4.0.3':
+    dependencies:
+      '@smithy/eventstream-codec': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/fetch-http-handler@5.0.3':
+    dependencies:
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/querystring-builder': 4.0.3
+      '@smithy/types': 4.3.0
+      '@smithy/util-base64': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/hash-blob-browser@4.0.3':
+    dependencies:
+      '@smithy/chunked-blob-reader': 5.0.0
+      '@smithy/chunked-blob-reader-native': 4.0.0
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/hash-node@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      '@smithy/util-buffer-from': 4.0.0
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/hash-stream-node@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/invalid-dependency@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/is-array-buffer@2.2.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/is-array-buffer@4.0.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/md5-js@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/middleware-content-length@4.0.3':
+    dependencies:
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/middleware-endpoint@4.1.7':
+    dependencies:
+      '@smithy/core': 3.4.0
+      '@smithy/middleware-serde': 4.0.6
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/shared-ini-file-loader': 4.0.3
+      '@smithy/types': 4.3.0
+      '@smithy/url-parser': 4.0.3
+      '@smithy/util-middleware': 4.0.3
+      tslib: 2.8.1
+
+  '@smithy/middleware-retry@4.1.8':
+    dependencies:
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/service-error-classification': 4.0.4
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      '@smithy/util-middleware': 4.0.3
+      '@smithy/util-retry': 4.0.4
+      tslib: 2.8.1
+      uuid: 9.0.1
+
+  '@smithy/middleware-serde@4.0.6':
+    dependencies:
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/middleware-stack@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/node-config-provider@4.1.2':
+    dependencies:
+      '@smithy/property-provider': 4.0.3
+      '@smithy/shared-ini-file-loader': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/node-http-handler@4.0.5':
+    dependencies:
+      '@smithy/abort-controller': 4.0.3
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/querystring-builder': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/property-provider@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/protocol-http@5.1.1':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/querystring-builder@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      '@smithy/util-uri-escape': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/querystring-parser@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/service-error-classification@4.0.4':
+    dependencies:
+      '@smithy/types': 4.3.0
+
+  '@smithy/shared-ini-file-loader@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/signature-v4@5.1.1':
+    dependencies:
+      '@smithy/is-array-buffer': 4.0.0
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      '@smithy/util-hex-encoding': 4.0.0
+      '@smithy/util-middleware': 4.0.3
+      '@smithy/util-uri-escape': 4.0.0
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/smithy-client@4.3.0':
+    dependencies:
+      '@smithy/core': 3.4.0
+      '@smithy/middleware-endpoint': 4.1.7
+      '@smithy/middleware-stack': 4.0.3
+      '@smithy/protocol-http': 5.1.1
+      '@smithy/types': 4.3.0
+      '@smithy/util-stream': 4.2.1
+      tslib: 2.8.1
+
+  '@smithy/types@4.3.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/url-parser@4.0.3':
+    dependencies:
+      '@smithy/querystring-parser': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/util-base64@4.0.0':
+    dependencies:
+      '@smithy/util-buffer-from': 4.0.0
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/util-body-length-browser@4.0.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/util-body-length-node@4.0.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/util-buffer-from@2.2.0':
+    dependencies:
+      '@smithy/is-array-buffer': 2.2.0
+      tslib: 2.8.1
+
+  '@smithy/util-buffer-from@4.0.0':
+    dependencies:
+      '@smithy/is-array-buffer': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/util-config-provider@4.0.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/util-defaults-mode-browser@4.0.15':
+    dependencies:
+      '@smithy/property-provider': 4.0.3
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      bowser: 2.11.0
+      tslib: 2.8.1
+
+  '@smithy/util-defaults-mode-node@4.0.15':
+    dependencies:
+      '@smithy/config-resolver': 4.1.3
+      '@smithy/credential-provider-imds': 4.0.5
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/property-provider': 4.0.3
+      '@smithy/smithy-client': 4.3.0
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/util-endpoints@3.0.5':
+    dependencies:
+      '@smithy/node-config-provider': 4.1.2
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/util-hex-encoding@4.0.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/util-middleware@4.0.3':
+    dependencies:
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/util-retry@4.0.4':
+    dependencies:
+      '@smithy/service-error-classification': 4.0.4
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
+  '@smithy/util-stream@4.2.1':
+    dependencies:
+      '@smithy/fetch-http-handler': 5.0.3
+      '@smithy/node-http-handler': 4.0.5
+      '@smithy/types': 4.3.0
+      '@smithy/util-base64': 4.0.0
+      '@smithy/util-buffer-from': 4.0.0
+      '@smithy/util-hex-encoding': 4.0.0
+      '@smithy/util-utf8': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/util-uri-escape@4.0.0':
+    dependencies:
+      tslib: 2.8.1
+
+  '@smithy/util-utf8@2.3.0':
+    dependencies:
+      '@smithy/util-buffer-from': 2.2.0
+      tslib: 2.8.1
+
+  '@smithy/util-utf8@4.0.0':
+    dependencies:
+      '@smithy/util-buffer-from': 4.0.0
+      tslib: 2.8.1
+
+  '@smithy/util-waiter@4.0.4':
+    dependencies:
+      '@smithy/abort-controller': 4.0.3
+      '@smithy/types': 4.3.0
+      tslib: 2.8.1
+
   '@swc/counter@0.1.3': {}
 
   '@swc/helpers@0.5.15':
@@ -4040,6 +5267,8 @@ snapshots:
     dependencies:
       csstype: 3.1.3
 
+  '@types/uuid@10.0.0': {}
+
   '@vitest/expect@3.1.1':
     dependencies:
       '@vitest/spy': 3.1.1
@@ -4133,6 +5362,8 @@ snapshots:
 
   binary-extensions@2.3.0: {}
 
+  bowser@2.11.0: {}
+
   brace-expansion@1.1.11:
     dependencies:
       balanced-match: 1.0.2
@@ -4456,6 +5687,10 @@ snapshots:
 
   fast-safe-stringify@2.1.1: {}
 
+  fast-xml-parser@4.4.1:
+    dependencies:
+      strnum: 1.1.2
+
   fastq@1.19.1:
     dependencies:
       reusify: 1.1.0
@@ -5071,6 +6306,8 @@ snapshots:
 
   strip-json-comments@3.1.1: {}
 
+  strnum@1.1.2: {}
+
   styled-jsx@5.1.6(react@19.0.0):
     dependencies:
       client-only: 0.0.1
@@ -5197,6 +6434,10 @@ snapshots:
     dependencies:
       react: 19.0.0
 
+  uuid@11.1.0: {}
+
+  uuid@9.0.1: {}
+
   vite-node@3.1.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2):
     dependencies:
       cac: 6.7.14


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 15**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 13a6d11c29e80de9375b542022ae28273588c854 and 29776bb148eec63a0b29e087ef90f37a879d1eb2.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `.github/workflows/ci-cd.yml` (2 hunks)
* `apps/backend/.env.example` (1 hunks)
* `apps/backend/package.json` (1 hunks)
* `apps/backend/src/app.ts` (2 hunks)
* `apps/backend/src/env.ts` (1 hunks)
* `apps/backend/src/modules/user/controller.ts` (1 hunks)
* `apps/backend/src/modules/user/route.ts` (1 hunks)
* `apps/backend/src/modules/user/service.ts` (1 hunks)
* `apps/backend/src/utils/s3.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>apps/backend/src/modules/user/service.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/s3.ts (1)</summary>

* `getS3Url` (13-25)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/route.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/controller.ts (1)</summary>

* `uploadAvatar` (5-22)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/utils/s3.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/env.ts (1)</summary>

* `env` (26-26)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (2)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/service.ts (1)</summary>

* `uploadUserAvatar` (3-8)

</details>
<details>
<summary>apps/backend/src/utils/http.ts (3)</summary>

* `getHeaders` (5-7)
* `getBody` (9-28)
* `getResponse` (30-37)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>apps/backend/src/app.ts (1)</summary>

`16-16`: **LGTM: Route registration follows existing pattern**

The user route registration follows the same pattern as the existing routes, which is good for consistency.

</details>
<details>
<summary>.github/workflows/ci-cd.yml (1)</summary>

`55-58`: **AWS credentials added appropriately to CI/CD workflow**

The AWS environment variables have been correctly added to both the `backend-tests` and `push-db-migrations` jobs, sourced securely from GitHub secrets.



Also applies to: 119-122

</details>
<details>
<summary>apps/backend/src/env.ts (1)</summary>

`12-15`: **Properly enforced AWS environment variables**

The environment schema correctly validates the required AWS credentials and configuration for S3 integration.

</details>
<details>
<summary>apps/backend/src/utils/s3.ts (2)</summary>

`5-11`: **S3 client configuration is correct**

The S3 client is properly initialized with credentials from environment variables.

---

`1-25`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify S3 bucket CORS configuration**

Ensure that the S3 bucket has appropriate CORS configuration to allow uploads from your frontend domain.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the S3 bucket exists and get its CORS configuration
aws s3api get-bucket-cors --bucket $S3_BUCKET || echo "CORS not configured or bucket doesn't exist"
```

Length of output: 230

---



</details>

**Ensure S3 Bucket CORS Configuration Allows Frontend Uploads**

Please verify that your S3 bucket’s CORS settings permit uploads from your frontend domain:

- AllowedOrigins includes your app’s URL (e.g., https://app.example.com)  
- AllowedMethods includes PUT (and any other methods you use)  
- AllowedHeaders includes Content-Type (and any custom headers)  
- ExposeHeaders and MaxAgeSeconds are set if needed  

You can fetch the live CORS configuration locally via AWS CLI:  
```bash
aws s3api get-bucket-cors --bucket <YOUR_S3_BUCKET>
```  
Or use this Node.js snippet with your AWS credentials:  
```js
import { S3Client, GetBucketCorsCommand } from "@aws-sdk/client-s3";

const client = new S3Client({ region: process.env.AWS_REGION });
(async () => {
  try {
    const { CORSRules } = await client.send(
      new GetBucketCorsCommand({ Bucket: process.env.S3_BUCKET })
    );
    console.log(JSON.stringify(CORSRules, null, 2));
  } catch (err) {
    console.error("Failed to fetch CORS config:", err);
  }
})();
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T20:12:15Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 11**

<details>
<summary>♻️ Duplicate comments (6)</summary><blockquote>

<details>
<summary>apps/backend/package.json (1)</summary><blockquote>

`20-21`: **LGTM: Appropriate AWS SDK dependencies added with exact versions.**

The AWS SDK dependencies for S3 integration have been added with exact version pinning, which is good for reproducible builds.

</blockquote></details>
<details>
<summary>apps/backend/src/utils/s3.ts (2)</summary><blockquote>

`18-30`: _🛠️ Refactor suggestion_

**Add error handling to uploadUrl function**

The uploadUrl function lacks error handling for potential S3 operation failures. This could lead to unhandled exceptions propagating to callers.
 

```diff
export const uploadUrl = async ({
  key,
  type,
}: { key: string; type: string }) => {
+  try {
    const command = new PutObjectCommand({
      Bucket: env.S3_BUCKET,
      Key: key,
      ContentType: type,
    });

    const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
    return url;
+  } catch (error) {
+    console.error('Error generating S3 upload URL:', error);
+    throw new Error('Failed to generate S3 upload URL');
+  }
};
```

---

`22-26`: _🧹 Nitpick (assertive)_

**Consider limiting file size for avatar uploads**

There's no specification of maximum file size for avatar uploads, which could lead to unnecessarily large uploads.
 

```diff
export const uploadUrl = async ({
  key,
  type,
}: { key: string; type: string }) => {
  const command = new PutObjectCommand({
    Bucket: env.S3_BUCKET,
    Key: key,
    ContentType: type,
+   // Set maximum file size for avatar uploads (e.g., 5MB)
+   ContentLength: 5 * 1024 * 1024,
  });

  const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
  return url;
};
```

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/service.ts (1)</summary><blockquote>

`4-11`: _🛠️ Refactor suggestion_

**Add error handling and input validation to uploadUserAvatar**

The function lacks error handling and validation for the filename parameter, which could lead to runtime errors if invalid filenames are provided.


```diff
export const uploadUserAvatar = async ({
  fileName,
  type,
}: { fileName: string; type: string }) => {
+  try {
+    // Basic validation for filename
+    if (!fileName || typeof fileName !== 'string') {
+      throw new Error('Invalid filename');
+    }
+    
+    // Prevent path traversal
+    if (fileName.includes('/') || fileName.includes('\\')) {
+      throw new Error('Invalid characters in filename');
+    }
+    
    const id = uuidv4();
    const url = await uploadUrl({ key: `avatar/${id}/${fileName}`, type });
    return url;
+  } catch (error) {
+    console.error('Error generating upload URL:', error);
+    throw new Error('Failed to generate upload URL');
+  }
};
```

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (2)</summary><blockquote>

`16-17`: _🛠️ Refactor suggestion_

**Sanitize filename to prevent path traversal**

The fileName parameter is used directly without sanitization, which could lead to path traversal if a malicious user provides a filename with directory traversal characters.
 

```diff
-    const { fileName, type } = await context.req.json();
+    let { fileName, type } = await context.req.json();
+    
+    // Sanitize filename to prevent path traversal
+    fileName = fileName.replace(/[/\\?%*:|"<>]/g, '-');
+    // Ensure filename doesn't start with dots or slashes
+    fileName = fileName.replace(/^[.\/\\]+/, '');
+    
     const result = await uploadUserAvatar({ fileName, type });
```

---

`24-24`: _🧹 Nitpick (assertive)_

**Improve error logging for better diagnostics**

The current error logging is basic and might not provide enough information for debugging in production.
 

```diff
-    console.error("Failed to upload profile picture", error);
+    console.error("Failed to upload profile picture", {
+      error: error instanceof Error ? error.message : String(error),
+      stack: error instanceof Error ? error.stack : undefined,
+      fileName,
+      type
+    });
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 29776bb148eec63a0b29e087ef90f37a879d1eb2 and 00976d1bf435892986ff40780431121bcf81344d.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `apps/backend/package.json` (1 hunks)
* `apps/backend/src/app.ts` (1 hunks)
* `apps/backend/src/modules/user/controller.ts` (1 hunks)
* `apps/backend/src/modules/user/route.ts` (1 hunks)
* `apps/backend/src/modules/user/service.ts` (1 hunks)
* `apps/backend/src/modules/user/validator.ts` (1 hunks)
* `apps/backend/src/utils/checkUser.ts` (1 hunks)
* `apps/backend/src/utils/s3.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (5)</summary>

<details>
<summary>apps/backend/src/utils/checkUser.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/db/schema/auth.ts (1)</summary>

* `session` (13-24)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/route.ts (2)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/validator.ts (1)</summary>

* `uploadAvatarSchema` (3-14)

</details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (3)</summary>

* `uploadAvatar` (6-31)
* `getAvatar` (33-58)
* `deleteAvatar` (60-85)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (3)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/checkUser.ts (1)</summary>

* `checkUser` (3-10)

</details>
<details>
<summary>apps/backend/src/utils/http.ts (3)</summary>

* `getHeaders` (5-7)
* `getBody` (9-28)
* `getResponse` (30-37)

</details>
<details>
<summary>apps/backend/src/modules/user/service.ts (3)</summary>

* `uploadUserAvatar` (4-11)
* `getUserAvatar` (13-16)
* `deleteUserAvatar` (18-20)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/service.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/s3.ts (3)</summary>

* `uploadUrl` (18-30)
* `getUrl` (32-40)
* `deleteObject` (42-50)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/utils/s3.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/env.ts (1)</summary>

* `env` (26-26)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/checkUser.ts (1)</summary>

`3-10`: **Function implementation looks good.**

The utility function properly checks for both session and user presence in the context, which is necessary for authentication verification. This approach aligns well with Hono's context-based middleware pattern.

</details>
<details>
<summary>apps/backend/src/modules/user/validator.ts (1)</summary>

`3-14`: **Good security measures in the validation schema.**

The validation schema effectively prevents directory traversal attacks by checking for problematic characters and limits uploads to common image formats. The minimum length check and custom error message are also good additions.

</details>
<details>
<summary>apps/backend/package.json (1)</summary>

`23-25`: **Appropriate supporting dependencies added.**

The additional dependencies for Zod validation, UUID generation, and type definitions are correctly added and will support the new avatar functionality.



Also applies to: 31-31

</details>
<details>
<summary>apps/backend/src/modules/user/route.ts (1)</summary>

`8-14`: **Good implementation of validation middleware for upload route.**

The zValidator middleware is properly integrated to validate the request body against the uploadAvatarSchema, with appropriate error handling for invalid formats.

</details>
<details>
<summary>apps/backend/src/app.ts (3)</summary>

`8-13`: **Good type declaration for Hono context variables**

The explicit type parameters for user and session context variables improve type safety and provide better developer experience with autocompletion.

---

`17-29`: **LGTM: Robust authentication middleware implementation**

The middleware properly retrieves authentication session and sets user and session context variables accordingly. Good handling of both authenticated and unauthenticated cases.

---

`37-37`: **LGTM: Good route organization**

The user route is properly registered, which helps maintain a clean separation of concerns.

</details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (1)</summary>

`6-13`: **LGTM: Good authentication check**

The controller properly checks for user authentication before proceeding with the avatar upload.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T21:41:17Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/route.ts (1)</summary><blockquote>

`1-22`: _🧹 Nitpick (assertive)_

**Consider using more RESTful route naming.**

The current route names (`/upload-avatar`, `/get-avatar`, `/delete-avatar`) are verb-based. Consider adopting a more RESTful approach with a resource-based naming convention and HTTP methods.


```diff
 import { Hono } from "hono";
 import { deleteAvatar, getAvatar, uploadAvatar } from "./controller";
 import { zValidator } from "@hono/zod-validator";
 import { uploadAvatarSchema } from "./validator";
 import { middleware } from "@/utils/middleware";

 const user = new Hono();

 user.use(middleware);

-user.post(
-  "/upload-avatar",
+user.post(
+  "/avatar",
   zValidator("json", uploadAvatarSchema, (result, c) => {
     if (!result.success) return c.text("Invalid format", 422);
   }),
   uploadAvatar,
 );
-user.get("/get-avatar", getAvatar);
-user.delete("/delete-avatar", deleteAvatar);
+user.get("/avatar", getAvatar);
+user.delete("/avatar", deleteAvatar);

 export { user };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (4)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/route.ts (1)</summary><blockquote>

`18-19`: _🛠️ Refactor suggestion_

**Add validation middleware to get and delete avatar routes.**

While the upload route includes validation, the get and delete routes are missing similar validation for the `key` parameter they expect in the request body.



```diff
 import { Hono } from "hono";
 import { deleteAvatar, getAvatar, uploadAvatar } from "./controller";
 import { zValidator } from "@hono/zod-validator";
-import { uploadAvatarSchema } from "./validator";
+import { uploadAvatarSchema, avatarKeySchema } from "./validator";

 const user = new Hono();

 user.use(middleware);

 user.post(
   "/upload-avatar",
   zValidator("json", uploadAvatarSchema, (result, c) => {
     if (!result.success) return c.text("Invalid format", 422);
   }),
   uploadAvatar,
 );
-user.get("/get-avatar", getAvatar);
-user.delete("/delete-avatar", deleteAvatar);
+user.get("/get-avatar", 
+  zValidator("json", avatarKeySchema, (result, c) => {
+    if (!result.success) return c.text("Invalid format", 422);
+  }),
+  getAvatar
+);
+user.delete("/delete-avatar", 
+  zValidator("json", avatarKeySchema, (result, c) => {
+    if (!result.success) return c.text("Invalid format", 422);
+  }),
+  deleteAvatar
+);
```

You'll need to create an `avatarKeySchema` in your validator file:

```ts
// In validator.ts
export const avatarKeySchema = z.object({
  key: z.string().min(1)
});
```

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/service.ts (2)</summary><blockquote>

`13-16`: _🛠️ Refactor suggestion_

**Add error handling to getUserAvatar function.**

The function should include error handling for potential failures when retrieving the avatar URL.



```diff
export const getUserAvatar = async ({ key }: { key: string }) => {
+  try {
+    if (!key || typeof key !== 'string') {
+      throw new Error('Invalid key');
+    }
    const url = await getUrl({ key });
    return url;
+  } catch (error) {
+    console.error('Error retrieving avatar URL:', error);
+    throw new Error('Failed to retrieve avatar URL');
+  }
};
```

---

`18-21`: _🛠️ Refactor suggestion_

**Add error handling to deleteUserAvatar function.**

Similar to the getUserAvatar function, deleteUserAvatar should also have proper error handling.


```diff
export const deleteUserAvatar = async ({ key }: { key: string }) => {
+  try {
+    if (!key || typeof key !== 'string') {
+      throw new Error('Invalid key');
+    }
    const result = await deleteObject({ key });
    return result;
+  } catch (error) {
+    console.error('Error deleting avatar:', error);
+    throw new Error('Failed to delete avatar');
+  }
};
```

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (1)</summary><blockquote>

`53-53`: _⚠️ Potential issue_

**Fix error logging message in deleteAvatar function.**

The error message incorrectly refers to getting a profile picture instead of deleting it.



```diff
-    console.error("Failed to get profile picture", error);
+    console.error("Failed to delete profile picture", error);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 00976d1bf435892986ff40780431121bcf81344d and 40cc584246fa159bd5de54e533f5a9cfb8d9b41f.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/backend/src/modules/user/controller.ts` (1 hunks)
* `apps/backend/src/modules/user/route.ts` (1 hunks)
* `apps/backend/src/modules/user/service.ts` (1 hunks)
* `apps/backend/src/utils/middleware.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>apps/backend/src/utils/middleware.ts (2)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/checkUser.ts (1)</summary>

* `checkUser` (3-10)

</details>
<details>
<summary>apps/backend/src/utils/http.ts (3)</summary>

* `getHeaders` (5-7)
* `getBody` (9-28)
* `getResponse` (30-37)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/service.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/s3.ts (3)</summary>

* `uploadUrl` (18-30)
* `getUrl` (32-40)
* `deleteObject` (42-50)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/route.ts (3)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/middleware.ts (1)</summary>

* `middleware` (5-14)

</details>
<details>
<summary>apps/backend/src/modules/user/validator.ts (1)</summary>

* `uploadAvatarSchema` (3-14)

</details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (3)</summary>

* `uploadAvatar` (5-22)
* `getAvatar` (24-41)
* `deleteAvatar` (43-60)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (2)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/service.ts (3)</summary>

* `uploadUserAvatar` (4-11)
* `getUserAvatar` (13-16)
* `deleteUserAvatar` (18-21)

</details>
<details>
<summary>apps/backend/src/utils/http.ts (3)</summary>

* `getHeaders` (5-7)
* `getBody` (9-28)
* `getResponse` (30-37)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/route.ts (2)</summary>

`9-10`: **Authentication middleware is properly applied.**

Good implementation of global authentication middleware to protect all user routes. This ensures only authenticated users can access the avatar upload, retrieval, and deletion endpoints.

---

`11-17`: **Good use of validation middleware for upload endpoint.**

The upload endpoint correctly uses zValidator to validate the request body against the uploadAvatarSchema, with a clear error response for invalid inputs.

</details>
<details>
<summary>apps/backend/src/utils/middleware.ts (1)</summary>

`5-14`: **Authentication middleware implementation is correct.**

The middleware properly checks for authenticated users and returns an appropriate 401 response when authentication fails. This ensures that routes using this middleware are protected.

</details>
<details>
<summary>apps/backend/src/modules/user/service.ts (1)</summary>

`4-11`: **Good UUID implementation for unique avatar paths.**

The use of UUID for generating unique keys helps prevent filename collisions and ensures each avatar is stored separately in S3, even if multiple users upload files with the same name.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T21:53:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 40cc584246fa159bd5de54e533f5a9cfb8d9b41f and 465f8f8b5b207088e16f650c25f04e5f9fb42c6e.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `apps/frontend/src/components/settings/ProfileTab.tsx` (1 hunks)
* `apps/frontend/src/components/settings/SecurityTab.tsx` (0 hunks)
* `apps/frontend/src/components/settings/SettingsDialog.tsx` (5 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* apps/frontend/src/components/settings/SecurityTab.tsx

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>apps/frontend/src/components/settings/ProfileTab.tsx (1)</summary>

`1-59`: **Nice implementation of the ProfileTab component!**

The component structure is clean and the UI looks well-organized. I appreciate the proper error handling and fallback for the avatar.

However, I don't see any functionality for uploading or changing the profile picture, which is mentioned in the PR title. Is this planned to be implemented in a future PR, or will it be added to this component?

</details>
<details>
<summary>apps/frontend/src/components/settings/SettingsDialog.tsx (4)</summary>

`28-28`: **Good refactoring to use the auth.useSession() hook**

The change from manual React Query to a unified hook simplifies the code.

---

`98-101`: **Good integration of the new ProfileTab component**

The ProfileTab is properly integrated with the appropriate props.

---

`1-122`: **Missing upload functionality in the UI**

The PR title mentions adding a route for uploading profile pictures and an S3 instance, but I don't see any UI elements for uploading or changing the avatar in the settings dialog. Is this functionality planned to be added elsewhere or in a future PR?

---

`111-111`: _🧹 Nitpick (assertive)_

**Add null check for currentSession prop**

Similar to the user prop, ensure the currentSession prop is properly handled.


```diff
<SecurityTab
  sessionList={sortedSessions}
- currentSession={session?.session ?? null}
+ currentSession={session?.session || null}
  isLoading={isLoading}
  error={fetchError as Error | null}
/>
```



> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T23:32:44Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 7**

<details>
<summary>♻️ Duplicate comments (11)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/validator.ts (1)</summary><blockquote>

`3-11`: **Consider adding a key validation schema for retrieval and deletion operations.**



The current schema covers the upload operation, but the get and delete avatar endpoints also require validation for the `key` parameter, as mentioned in previous reviews.

</blockquote></details>
<details>
<summary>apps/frontend/src/components/settings/SettingsDialog.tsx (2)</summary><blockquote>

`99-99`: **Avoid type assertions when possible.**



The type assertion `error as Error | null` might be problematic if the actual error is of a different type, as noted in previous reviews.

---

`47-51`: **Add type safety for session id access.**



The session structure has changed, and accessing nested properties requires careful handling to avoid TypeScript errors, as noted in previous reviews.

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/service.ts (3)</summary><blockquote>

`14-17`: _🛠️ Refactor suggestion_

**Add error handling and input validation to getUserAvatar.**

The function should validate inputs and handle potential failures when retrieving the avatar URL.



```diff
export const getUserAvatar = async ({ key }: { key: string }) => {
+ try {
+   if (!key || typeof key !== 'string' || key.trim() === '') {
+     throw new Error('Invalid key provided');
+   }
    const url = await getUrl({ key });
    return url;
+ } catch (error) {
+   console.error('Error retrieving avatar URL:', error);
+   throw new Error('Failed to retrieve avatar URL');
+ }
};
```

---

`19-22`: _🛠️ Refactor suggestion_

**Add input validation to deleteUserAvatar.**

The function should validate the key parameter before attempting deletion.



```diff
export const deleteUserAvatar = async ({ key }: { key: string }) => {
+ try {
+   if (!key || typeof key !== 'string' || key.trim() === '') {
+     throw new Error('Invalid key provided');
+   }
    const result = await deleteObject({ key });
    return result;
+ } catch (error) {
+   console.error('Error deleting avatar:', error);
+   throw new Error('Failed to delete avatar');
+ }
};
```

---

`4-12`: _🛠️ Refactor suggestion_

**Add comprehensive error handling and input validation.**

The function lacks proper error handling and input validation, which could lead to runtime errors with invalid inputs.



```diff
export const uploadUserAvatar = async ({
  fileName,
  type,
}: { fileName: string; type: string }) => {
+ try {
+   // Validate inputs
+   if (!fileName || typeof fileName !== 'string' || fileName.trim() === '') {
+     throw new Error('Invalid filename provided');
+   }
+   if (!type || typeof type !== 'string' || !type.startsWith('image/')) {
+     throw new Error('Invalid content type provided');
+   }
+   
    const id = uuidv4();
    const key = `avatar/${id}/${fileName}`;
    const url = await uploadUrl({ key, type });
    return { url, key };
+ } catch (error) {
+   console.error('Error generating avatar upload URL:', error);
+   throw new Error('Failed to generate avatar upload URL');
+ }
};
```

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (3)</summary><blockquote>

`7-7`: **Use validated data from the zValidator middleware.**

The controller should use the validated data from the middleware instead of extracting values directly from the request JSON.

```diff
-    const { fileName, type } = await context.req.json();
+    const { fileName, type } = context.req.valid('json');
```

---

`59-59`: **Fix incorrect error message in deleteAvatar function.**

The error message incorrectly refers to getting a profile picture instead of deleting it.

```diff
-    console.error("Failed to get profile picture", error);
+    console.error("Failed to delete profile picture", error);
```

---

`7-8`: _⚠️ Potential issue_

**Sanitize filename and generate unique filenames to prevent security issues and overwrites.**

The filename needs sanitization to prevent path traversal attacks, and unique filename generation to prevent users from overwriting each other's avatars.


```diff
-    const { fileName, type } = await context.req.json();
-    const result = await uploadUserAvatar({ fileName, type });
+    const { fileName, type } = context.req.valid('json');
+    
+    // Sanitize filename to prevent path traversal
+    let sanitizedFileName = fileName.replace(/[/\\?%*:|"<>]/g, '-');
+    sanitizedFileName = sanitizedFileName.replace(/^[.\/\\]+/, '');
+    
+    // Generate unique filename to prevent overwrites
+    const timestamp = Date.now();
+    const randomString = Math.random().toString(36).substring(2, 15);
+    const uniqueFileName = `${timestamp}-${randomString}-${sanitizedFileName}`;
+    
+    const result = await uploadUserAvatar({ fileName: uniqueFileName, type });
```

</blockquote></details>
<details>
<summary>apps/frontend/src/components/settings/ProfileTab.tsx (2)</summary><blockquote>

`19-22`: **Consider adding loading state to improve user experience.**

The component handles error states but doesn't explicitly handle loading states during data fetching.

```diff
 interface ProfileTabProps {
   user: User | null;
   error: Error | null;
+  isLoading?: boolean;
 }
```

---

`175-178`: **Optimize avatar image handling to avoid unnecessary network requests.**

The current implementation sets an empty string as src when no image is available, which can cause unnecessary network requests.

```diff
                 <Avatar className="size-24 border-2 border-muted">
-                  <AvatarImage
-                    src={imageSrc}
-                    alt={`${user?.name || "User"}'s profile picture`}
-                  />
+                  {imageSrc ? (
+                    <AvatarImage
+                      src={imageSrc}
+                      alt={`${user?.name || "User"}'s profile picture`}
+                    />
+                  ) : null}
                   <AvatarFallback className="bg-background text-foreground size-full">
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 465f8f8b5b207088e16f650c25f04e5f9fb42c6e and 9370ad3406c88a07593518ea1349983463d10179.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `apps/backend/src/modules/user/controller.ts` (1 hunks)
* `apps/backend/src/modules/user/service.ts` (1 hunks)
* `apps/backend/src/modules/user/validator.ts` (1 hunks)
* `apps/frontend/src/components/settings/ProfileTab.tsx` (1 hunks)
* `apps/frontend/src/components/settings/SettingsDialog.tsx` (4 hunks)
* `apps/frontend/src/components/sidebar/UserButton.tsx` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>apps/frontend/src/components/sidebar/UserButton.tsx (3)</summary><blockquote>

<details>
<summary>apps/frontend/src/components/ui/dropdown-menu.tsx (2)</summary>

* `DropdownMenu` (243-243)
* `DropdownMenuTrigger` (245-245)

</details>
<details>
<summary>apps/frontend/src/components/ui/sidebar.tsx (1)</summary>

* `SidebarMenuButton` (695-695)

</details>
<details>
<summary>apps/frontend/src/components/ui/avatar.tsx (2)</summary>

* `Avatar` (53-53)
* `AvatarImage` (53-53)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/service.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/s3.ts (3)</summary>

* `uploadUrl` (18-30)
* `getUrl` (32-40)
* `deleteObject` (42-50)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/modules/user/controller.ts (2)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/service.ts (3)</summary>

* `uploadUserAvatar` (4-12)
* `getUserAvatar` (14-17)
* `deleteUserAvatar` (19-22)

</details>
<details>
<summary>apps/backend/src/utils/http.ts (3)</summary>

* `getHeaders` (5-7)
* `getBody` (9-28)
* `getResponse` (30-37)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/user/controller.ts (1)</summary>

`24-47`: **Well-implemented avatar retrieval with proper validation.**

The implementation correctly validates the query parameter and returns appropriate error responses. Good error handling structure.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:33:10Z

Review comments:
diff_hunk: @@ -3,6 +3,7 @@ import { cors } from "hono/cors";
 import { status } from "@/modules/status/route";
 import { auth } from "./utils/auth";
 import { corsConfig } from "./utils/http";
+import { user } from "./modules/user/route";
path: apps/backend/src/app.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider using consistent import aliasing**

The import for the user route uses a relative path `"./modules/user/route"`, while the status module uses the alias pattern `"@/modules/status/route"`. Consider using the alias pattern consistently across imports.

```diff
-import { user } from "./modules/user/route";
+import { user } from "@/modules/user/route";
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
-import { user } from "./modules/user/route";
+import { user } from "@/modules/user/route";
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/app.ts at line 6, the import for the user route uses a
relative path instead of the alias pattern used elsewhere. Change the import
statement to use the alias pattern "@/modules/user/route" to maintain consistent
import aliasing across the file.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits e59d5cc to 00976d1
created_at: 2025-05-22T20:12:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,4 +2,8 @@ BETTER_AUTH_SECRET=
 RESEND_API=
 BETTER_AUTH_URL=http://localhost:3001
 DATABASE_URL=postgres://local_user:local_password@localhost:5432/local_db
-FRONTEND_URL=http://localhost:3000
\ No newline at end of file
+FRONTEND_URL=http://localhost:3000
+S3_BUCKET=
+AWS_REGION=
+AWS_SECRET_ACCESS_KEY=
+AWS_ACCESS_KEY_ID=
path: apps/backend/.env.example
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add comments and example values for clarity**

The new environment variables for AWS S3 integration are correctly added, but consider enhancing the file with comments explaining each variable's purpose and format. For AWS_REGION, an example value like "us-east-1" would be helpful.

```diff
-FRONTEND_URL=http://localhost:3000
-S3_BUCKET=
-AWS_REGION=
-AWS_SECRET_ACCESS_KEY=
-AWS_ACCESS_KEY_ID=
+# URL of the frontend application
+FRONTEND_URL=http://localhost:3000
+# Name of your S3 bucket for storing user avatars
+S3_BUCKET=
+# AWS region where your S3 bucket is located (e.g., us-east-1)
+AWS_REGION=
+# Your AWS secret access key (keep this secure)
+AWS_SECRET_ACCESS_KEY=
+# Your AWS access key ID
+AWS_ACCESS_KEY_ID=
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
# URL of the frontend application
FRONTEND_URL=http://localhost:3000
# Name of your S3 bucket for storing user avatars
S3_BUCKET=
# AWS region where your S3 bucket is located (e.g., us-east-1)
AWS_REGION=
# Your AWS secret access key (keep this secure)
AWS_SECRET_ACCESS_KEY=
# Your AWS access key ID
AWS_ACCESS_KEY_ID=
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/.env.example around lines 5 to 9, the AWS-related environment
variables lack explanatory comments and example values. Add brief comments above
each variable describing its purpose and expected format, and include example
values such as "us-east-1" for AWS_REGION to improve clarity and usability for
developers.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T20:12:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+import { Hono } from "hono";
+import { uploadAvatar } from "./controller";
+
+const user = new Hono();
+
+user.post("/upload-avatar", uploadAvatar);
+
+export { user };
path: apps/backend/src/modules/user/route.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add authentication middleware to protect user uploads**

This route for uploading avatars should be protected to ensure only authenticated users can upload their profile pictures. Consider adding authentication middleware.

```diff
import { Hono } from "hono";
import { uploadAvatar } from "./controller";
+import { auth } from "../../utils/auth";

const user = new Hono();

-user.post("/upload-avatar", uploadAvatar);
+// Route to generate a pre-signed S3 URL for user avatar uploads
+// Expects: { fileName: string, type: string } in request body
+user.post("/upload-avatar", auth.middleware(), uploadAvatar);

export { user };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
import { Hono } from "hono";
import { uploadAvatar } from "./controller";
import { auth } from "../../utils/auth";

const user = new Hono();

// Route to generate a pre-signed S3 URL for user avatar uploads
// Expects: { fileName: string, type: string } in request body
user.post("/upload-avatar", auth.middleware(), uploadAvatar);

export { user };
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/route.ts lines 1 to 8, the POST route for
uploading avatars lacks authentication protection. Add an authentication
middleware to the route handler chain before the uploadAvatar controller to
ensure only authenticated users can access this endpoint. Import the appropriate
auth middleware and apply it to the user.post call for "/upload-avatar".
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T20:12:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+import { Hono } from "hono";
+import { uploadAvatar } from "./controller";
+
+const user = new Hono();
+
+user.post("/upload-avatar", uploadAvatar);
path: apps/backend/src/modules/user/route.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider adding input validation middleware**

The endpoint accepts `fileName` and `type` from the request body but doesn't appear to validate these inputs at the route level. Consider adding validation middleware to ensure proper data format and prevent security issues like path traversal attacks.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there's any validation in the controller or service layer
echo "Checking for validation in controller.ts:"
rg -A 10 "uploadAvatar" apps/backend/src/modules/user/controller.ts

echo "Checking for validation in service.ts:"
rg -A 10 "uploadUserAvatar" apps/backend/src/modules/user/service.ts

echo "Checking for zod or other validation schemes:"
rg "fileName|type" apps/backend/src/modules/user/
```

Length of output: 1600

---



</details>

**Add input validation middleware to `/upload-avatar` route**

The `upload-avatar` endpoint currently reads `fileName` and `type` from `context.req.json()` without any runtime checks, which can lead to malformed inputs or security issues (e.g. path traversal in S3 keys). We searched both the controller and service layers and found no existing validation.

Please add schema validation at the route level—using Zod, Hono’s validator, or a similar library—to enforce required formats before calling `uploadAvatar`. For example:

• apps/backend/src/modules/user/route.ts  
  - Wrap `uploadAvatar` with a validation middleware.  
  - Import and apply a schema that checks `fileName` (e.g. non-empty, allowed chars) and `type` (e.g. valid MIME type).

Optional diff example:
```diff
-import { uploadAvatar } from "./controller";
+import { uploadAvatar } from "./controller";
+import { validateBody } from "hono/zod-validator";
+import { uploadAvatarSchema } from "./validation";

-user.post("/upload-avatar", uploadAvatar);
+user.post(
+  "/upload-avatar",
+  validateBody(uploadAvatarSchema),
+  uploadAvatar
+);
```

• apps/backend/src/modules/user/validation.ts  
  ```ts
  import { z } from "zod";

  export const uploadAvatarSchema = z.object({
    fileName: z.string().min(1),
    type: z.string().regex(/^image\/(jpeg|png|gif)$/),
  });
  ```

This ensures only well-formed data reaches your controller and S3 utility.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/route.ts at line 6, the /upload-avatar route
lacks input validation for fileName and type from the request body, risking
malformed inputs and security issues. Fix this by creating a validation schema
(e.g., using Zod) that enforces fileName as a non-empty string and type as a
valid image MIME type, then import and apply this validation middleware to the
route before calling uploadAvatar, ensuring only validated data proceeds to the
controller.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits e59d5cc to 00976d1
created_at: 2025-05-22T20:12:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,8 @@
     "drizzle:studio": "drizzle-kit studio"
   },
   "dependencies": {
+    "@aws-sdk/client-s3": "^3.816.0",
+    "@aws-sdk/s3-request-presigner": "^3.816.0",
path: apps/backend/package.json
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**LGTM: Appropriate AWS SDK dependencies added**

The AWS SDK dependencies needed for S3 integration have been added correctly. Using the same version for both packages is good for consistency.


Consider using exact version pinning (removing the caret ^) for more predictable builds, though this depends on your team's versioning strategy:
```diff
-    "@aws-sdk/client-s3": "^3.816.0",
-    "@aws-sdk/s3-request-presigner": "^3.816.0",
+    "@aws-sdk/client-s3": "3.816.0",
+    "@aws-sdk/s3-request-presigner": "3.816.0",
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    "@aws-sdk/client-s3": "3.816.0",
    "@aws-sdk/s3-request-presigner": "3.816.0",
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/package.json around lines 20 to 21, the AWS SDK dependencies use
caret (^) version ranges which allow minor and patch updates. To ensure more
predictable and consistent builds, remove the caret from the version strings to
pin exact versions. Update the versions to "3.816.0" without the caret prefix
for both "@aws-sdk/client-s3" and "@aws-sdk/s3-request-presigner".
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits e59d5cc to 00976d1
created_at: 2025-05-22T20:12:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+import { getS3Url } from "@/utils/s3";
+
+export const uploadUserAvatar = async ({
+  fileName,
+  type,
+}: { fileName: string; type: string }) => {
+  await getS3Url({ key: `avatar/${fileName}`, type });
+};
path: apps/backend/src/modules/user/service.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing return value for pre-signed URL**

The function calls `getS3Url` but doesn't return the generated URL to the caller. This likely breaks the upload flow since the client needs this URL to upload directly to S3.

```diff
export const uploadUserAvatar = async ({
  fileName,
  type,
}: { fileName: string; type: string }) => {
-  await getS3Url({ key: `avatar/${fileName}`, type });
+  return await getS3Url({ key: `avatar/${fileName}`, type });
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const uploadUserAvatar = async ({
  fileName,
  type,
}: { fileName: string; type: string }) => {
  return await getS3Url({ key: `avatar/${fileName}`, type });
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/service.ts between lines 3 and 8, the function
uploadUserAvatar calls getS3Url but does not return its result. To fix this, add
a return statement before the getS3Url call so that the generated pre-signed URL
is returned to the caller, enabling the client to use it for direct S3 uploads.
```

</details>

---

_🛠️ Refactor suggestion_

**Add error handling and input validation**

The function lacks error handling and validation for the filename parameter, which could lead to runtime errors if invalid filenames are provided.

```diff
export const uploadUserAvatar = async ({
  fileName,
  type,
}: { fileName: string; type: string }) => {
-  await getS3Url({ key: `avatar/${fileName}`, type });
+  try {
+    // Basic validation for filename (avoid path traversal)
+    if (fileName.includes('/') || fileName.includes('\\')) {
+      throw new Error('Invalid filename');
+    }
+    return await getS3Url({ key: `avatar/${fileName}`, type });
+  } catch (error) {
+    console.error('Error generating upload URL:', error);
+    throw new Error('Failed to generate upload URL');
+  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const uploadUserAvatar = async ({
  fileName,
  type,
}: { fileName: string; type: string }) => {
  try {
    // Basic validation for filename (avoid path traversal)
    if (fileName.includes('/') || fileName.includes('\\')) {
      throw new Error('Invalid filename');
    }
    return await getS3Url({ key: `avatar/${fileName}`, type });
  } catch (error) {
    console.error('Error generating upload URL:', error);
    throw new Error('Failed to generate upload URL');
  }
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/service.ts between lines 3 and 8, the
uploadUserAvatar function does not validate the fileName input or handle
potential errors from the getS3Url call. Add validation to ensure fileName is a
non-empty string and conforms to expected filename patterns. Wrap the getS3Url
call in a try-catch block to catch and handle any errors, logging or rethrowing
them as appropriate to prevent unhandled exceptions.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits e59d5cc to 00976d1
created_at: 2025-05-22T20:12:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+import { env } from "@/env";
+import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
+
+const s3 = new S3Client({
+  region: env.AWS_REGION,
+  credentials: {
+    accessKeyId: env.AWS_ACCESS_KEY_ID,
+    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
+  },
+});
+
+export const getS3Url = async ({
+  key,
+  type,
+}: { key: string; type: string }) => {
+  const command = new PutObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+    ContentType: type,
+  });
+
+  const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
+  return url;
+};
path: apps/backend/src/utils/s3.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to getS3Url function**

The function should include error handling to provide meaningful errors when S3 operations fail.

```diff
export const getS3Url = async ({
  key,
  type,
}: { key: string; type: string }) => {
+  try {
    const command = new PutObjectCommand({
      Bucket: env.S3_BUCKET,
      Key: key,
      ContentType: type,
    });

    const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
    return url;
+  } catch (error) {
+    console.error('Error generating S3 signed URL:', error);
+    throw new Error('Failed to generate S3 upload URL');
+  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const getS3Url = async ({
  key,
  type,
}: { key: string; type: string }) => {
  try {
    const command = new PutObjectCommand({
      Bucket: env.S3_BUCKET,
      Key: key,
      ContentType: type,
    });

    const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
    return url;
  } catch (error) {
    console.error('Error generating S3 signed URL:', error);
    throw new Error('Failed to generate S3 upload URL');
  }
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/utils/s3.ts around lines 13 to 25, the getS3Url function
lacks error handling for potential failures during the S3 operation. Wrap the
existing code in a try-catch block, and in the catch block, throw or log a
meaningful error message that includes context about the failure to generate the
signed URL. This will help callers understand what went wrong when the function
fails.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T20:12:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+import { env } from "@/env";
+import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
+
+const s3 = new S3Client({
+  region: env.AWS_REGION,
+  credentials: {
+    accessKeyId: env.AWS_ACCESS_KEY_ID,
+    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
+  },
+});
+
+export const getS3Url = async ({
+  key,
+  type,
+}: { key: string; type: string }) => {
+  const command = new PutObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+    ContentType: type,
+  });
path: apps/backend/src/utils/s3.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider limiting file size for avatar uploads**

There's no specification of maximum file size for avatar uploads, which could lead to unnecessarily large uploads.

```diff
export const getS3Url = async ({
  key,
  type,
}: { key: string; type: string }) => {
  const command = new PutObjectCommand({
    Bucket: env.S3_BUCKET,
    Key: key,
    ContentType: type,
+   // Set maximum file size for avatar uploads (e.g., 5MB)
+   ContentLength: 5 * 1024 * 1024,
  });

  const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
  return url;
};
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/utils/s3.ts around lines 17 to 21, the PutObjectCommand for
uploading avatars does not enforce a maximum file size limit. To fix this, add a
check before creating the command to validate the file size against a defined
maximum limit for avatar uploads, and reject or handle files exceeding this size
to prevent large uploads.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T20:12:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar } from "./service";
+
+export const uploadAvatar = async (context: Context) => {
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle return value from uploadUserAvatar service.**

The `uploadUserAvatar` service function doesn't seem to return anything based on the relevant code snippets, yet the controller is using its return value.


Ensure the service function returns a proper value or adjust the controller to handle the case when no value is returned:

```diff
-    const result = await uploadUserAvatar({ fileName, type });
+    const url = await uploadUserAvatar({ fileName, type });
+    const result = { url };
```

Also update the service function to return the generated URL:

```typescript
// In service.ts
export const uploadUserAvatar = async ({
  fileName,
  type,
}: { fileName: string; type: string }) => {
  return await getS3Url({ key: `avatar/${fileName}`, type });
};
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts at line 8, the controller expects
a return value from uploadUserAvatar, but the service function currently returns
nothing. To fix this, update the uploadUserAvatar service function to return the
generated URL by returning the result of getS3Url with the appropriate key and
type. Then, ensure the controller uses this returned URL properly instead of
assuming no return value.
```

</details>

---

_🛠️ Refactor suggestion_

**Ensure avatars have unique filenames to prevent overwrites.**

The current implementation may allow users to overwrite each other's avatars if they use the same filename.


Generate unique filenames for avatar uploads:

```diff
-    const result = await uploadUserAvatar({ fileName, type });
+    // Generate a unique filename with a timestamp and random string
+    const timestamp = Date.now();
+    const randomString = Math.random().toString(36).substring(2, 15);
+    const uniqueFileName = `${timestamp}-${randomString}-${fileName}`;
+    
+    const result = await uploadUserAvatar({ fileName: uniqueFileName, type });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    // Generate a unique filename with a timestamp and random string
    const timestamp = Date.now();
    const randomString = Math.random().toString(36).substring(2, 15);
    const uniqueFileName = `${timestamp}-${randomString}-${fileName}`;
    
    const result = await uploadUserAvatar({ fileName: uniqueFileName, type });
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts at line 8, the current avatar
upload uses the original filename, risking overwrites if multiple users upload
files with the same name. Modify the code to generate a unique filename for each
avatar upload, for example by appending a timestamp or a UUID to the original
filename before calling uploadUserAvatar. This ensures each avatar is stored
uniquely and prevents accidental overwrites.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T20:12:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar } from "./service";
+
+export const uploadAvatar = async (context: Context) => {
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to upload profile picture", error);
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Improve error logging for better diagnostics.**

The current error logging is basic and might not provide enough information for debugging in production.


Consider enhancing the error logging with more context:

```diff
-    console.error("Failed to upload profile picture", error);
+    console.error("Failed to upload profile picture", {
+      error: error instanceof Error ? error.message : String(error),
+      stack: error instanceof Error ? error.stack : undefined,
+      fileName,
+      type
+    });
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts at line 15, the error logging for
profile picture upload failure is too basic. Enhance the logging by including
additional context such as user ID or request details along with the error
object. This will provide better diagnostics and make debugging easier in
production environments.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T20:12:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar } from "./service";
+
+export const uploadAvatar = async (context: Context) => {
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Sanitize filename to prevent path traversal.**

The `fileName` parameter is used directly without sanitization, which could lead to path traversal if a malicious user provides a filename with directory traversal characters.


Sanitize the filename before passing it to the service:

```diff
-    const { fileName, type } = await context.req.json();
+    let { fileName, type } = await context.req.json();
+    
+    // Sanitize filename to prevent path traversal
+    fileName = fileName.replace(/[/\\?%*:|"<>]/g, '-');
+    // Ensure filename doesn't start with dots or slashes
+    fileName = fileName.replace(/^[.\/\\]+/, '');
+    
     const result = await uploadUserAvatar({ fileName, type });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    let { fileName, type } = await context.req.json();
    
    // Sanitize filename to prevent path traversal
    fileName = fileName.replace(/[/\\?%*:|"<>]/g, '-');
    // Ensure filename doesn't start with dots or slashes
    fileName = fileName.replace(/^[.\/\\]+/, '');
    
    const result = await uploadUserAvatar({ fileName, type });
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts around lines 7 to 8, the fileName
is used directly from the request without sanitization, which risks path
traversal attacks. To fix this, sanitize the fileName by removing or
neutralizing any directory traversal characters (like ../) before passing it to
uploadUserAvatar. Use a safe method or library to clean the filename to ensure
it cannot escape the intended directory.
```

</details>

---

_🛠️ Refactor suggestion_

**Add input validation for file parameters.**

The controller extracts `fileName` and `type` from the request body without any validation. This could be a security risk if malicious input is provided.


Consider adding validation for these parameters:

```diff
-    const { fileName, type } = await context.req.json();
+    const body = await context.req.json();
+    const fileName = body.fileName;
+    const type = body.type;
+    
+    // Validate required fields
+    if (!fileName || !type) {
+      const headers = getHeaders();
+      const body = getBody(400, null, new Error('Missing required fields: fileName and type'));
+      return getResponse(context, 400, headers, body);
+    }
+    
+    // Validate file type (allowlist approach)
+    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
+    if (!allowedTypes.includes(type)) {
+      const headers = getHeaders();
+      const body = getBody(400, null, new Error('Unsupported file type'));
+      return getResponse(context, 400, headers, body);
+    }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    const body = await context.req.json();
    const fileName = body.fileName;
    const type = body.type;
    
    // Validate required fields
    if (!fileName || !type) {
      const headers = getHeaders();
      const body = getBody(400, null, new Error('Missing required fields: fileName and type'));
      return getResponse(context, 400, headers, body);
    }
    
    // Validate file type (allowlist approach)
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!allowedTypes.includes(type)) {
      const headers = getHeaders();
      const body = getBody(400, null, new Error('Unsupported file type'));
      return getResponse(context, 400, headers, body);
    }

    const result = await uploadUserAvatar({ fileName, type });
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts around lines 7 to 8, the code
extracts fileName and type from the request body without validating them, which
poses a security risk. Add input validation to check that fileName and type are
present, are strings, and conform to expected formats or allowed values before
calling uploadUserAvatar. If validation fails, respond with an appropriate error
to prevent processing invalid or malicious input.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T20:12:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+import { z } from "zod";
+
+export const uploadAvatarSchema = z.object({
+  fileName: z
+    .string()
+    .min(1)
+    .refine(
+      (val) => !val.includes("..") && !val.includes("/") && !val.includes("\\"),
+      {
+        message: "Invalid filename",
+      },
+    ),
+  type: z.string().regex(/^image\/(jpeg|png|gif)$/),
+});
path: apps/backend/src/modules/user/validator.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider adding a key validation schema for retrieval and deletion operations.**

The current schema covers the upload operation, but the get and delete avatar endpoints also require validation for the `key` parameter.


```diff
 export const uploadAvatarSchema = z.object({
   fileName: z
     .string()
     .min(1)
     .refine(
       (val) => !val.includes("..") && !val.includes("/") && !val.includes("\\"),
       {
         message: "Invalid filename",
       },
     ),
   type: z.string().regex(/^image\/(jpeg|png|gif)$/),
 });
+
+export const avatarKeySchema = z.object({
+  key: z.string().min(1)
+});
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const uploadAvatarSchema = z.object({
  fileName: z
    .string()
    .min(1)
    .refine(
      (val) => !val.includes("..") && !val.includes("/") && !val.includes("\\"),
      {
        message: "Invalid filename",
      },
    ),
  type: z.string().regex(/^image\/(jpeg|png|gif)$/),
});

export const avatarKeySchema = z.object({
  key: z.string().min(1)
});
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/validator.ts around lines 3 to 14, the current
schema only validates the upload avatar operation but lacks validation for the
`key` parameter used in get and delete avatar endpoints. Add a new Zod schema
specifically for the `key` parameter that enforces appropriate constraints
(e.g., string type, no path traversal characters) to ensure safe and consistent
validation for retrieval and deletion operations.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import { Hono } from "hono";
+import { deleteAvatar, getAvatar, uploadAvatar } from "./controller";
+import { zValidator } from "@hono/zod-validator";
+import { uploadAvatarSchema } from "./validator";
+
+const user = new Hono();
+
+user.post(
+  "/upload-avatar",
+  zValidator("json", uploadAvatarSchema, (result, c) => {
+    if (!result.success) return c.text("Invalid format", 422);
+  }),
+  uploadAvatar,
+);
+user.get("/get-avatar", getAvatar);
+user.delete("/delete-avatar", deleteAvatar);
path: apps/backend/src/modules/user/route.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation middleware to get and delete avatar routes.**

While the upload route includes validation, the get and delete routes are missing similar validation for the `key` parameter they expect in the request body.


```diff
 import { Hono } from "hono";
 import { deleteAvatar, getAvatar, uploadAvatar } from "./controller";
 import { zValidator } from "@hono/zod-validator";
-import { uploadAvatarSchema } from "./validator";
+import { uploadAvatarSchema, avatarKeySchema } from "./validator";

 const user = new Hono();

 user.post(
   "/upload-avatar",
   zValidator("json", uploadAvatarSchema, (result, c) => {
     if (!result.success) return c.text("Invalid format", 422);
   }),
   uploadAvatar,
 );
-user.get("/get-avatar", getAvatar);
-user.delete("/delete-avatar", deleteAvatar);
+user.get("/get-avatar", 
+  zValidator("json", avatarKeySchema, (result, c) => {
+    if (!result.success) return c.text("Invalid format", 422);
+  }),
+  getAvatar
+);
+user.delete("/delete-avatar", 
+  zValidator("json", avatarKeySchema, (result, c) => {
+    if (!result.success) return c.text("Invalid format", 422);
+  }),
+  deleteAvatar
+);
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/route.ts around lines 15 to 16, the getAvatar
and deleteAvatar routes lack validation middleware for the 'key' parameter in
the request body. Add appropriate validation middleware to these routes to
ensure the 'key' parameter is present and valid before processing the request,
similar to the validation used in the upload route.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import { Hono } from "hono";
+import { deleteAvatar, getAvatar, uploadAvatar } from "./controller";
+import { zValidator } from "@hono/zod-validator";
+import { uploadAvatarSchema } from "./validator";
+
+const user = new Hono();
+
+user.post(
+  "/upload-avatar",
+  zValidator("json", uploadAvatarSchema, (result, c) => {
+    if (!result.success) return c.text("Invalid format", 422);
+  }),
+  uploadAvatar,
+);
+user.get("/get-avatar", getAvatar);
+user.delete("/delete-avatar", deleteAvatar);
+
+export { user };
path: apps/backend/src/modules/user/route.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider adding authentication middleware to the routes.**

While the controller functions check for user authentication, it would be more maintainable to add an authentication middleware at the route level to avoid repeating the authentication check in each controller.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there's an auth middleware available in the codebase

# Look for auth middleware or utilities
echo "Looking for auth middleware or utilities:"
fd -t f "auth" --exec grep -l "middleware" {} \;

# Check for middleware usage patterns in other routes
echo "Checking middleware usage patterns in routes:"
fd -t f "route.ts" --exec grep -A 3 "middleware" {} \;
```

Length of output: 299

---

🏁 Script executed:

```shell
#!/bin/bash
set -e

echo "1. Searching for any middleware usage via '.use(' occurrences in the backend module:"
grep -R ".use(" -n apps/backend/src || echo "No '.use(' found in apps/backend/src"

echo
echo "2. Searching for 'auth' references (case-insensitive) in the backend module:"
grep -R -i "auth" -n apps/backend/src || echo "No 'auth' references found in apps/backend/src"

echo
echo "3. Locating the user controller file(s) defining uploadAvatar, getAvatar, deleteAvatar:"
CONTROLLER_FILES=$(grep -R -lE "uploadAvatar|getAvatar|deleteAvatar" -n apps/backend/src)
if [ -n "$CONTROLLER_FILES" ]; then
  for file in $CONTROLLER_FILES; do
    echo "---- $file ----"
    sed -n '1,200p' "$file"
    echo
  done
else
  echo "No controller definitions found for uploadAvatar/getAvatar/deleteAvatar in apps/backend/src"
fi
```

Length of output: 5945

---



</details>

**Add authentication middleware to the user routes**

To avoid repeating the `checkUser` call in every controller, extract it into a route‐level middleware and apply it once in `apps/backend/src/modules/user/route.ts`. For example:

• Create a middleware (e.g. `apps/backend/src/modules/user/middleware.ts`):
```ts
import { Context } from "hono";
import { getHeaders, getBody, getResponse } from "@/utils/http";
import { checkUser } from "@/utils/checkUser";

export const requireAuth = async (c: Context, next: () => Promise<void>) => {
  const user = checkUser({ context: c });
  if (!user) {
    const headers = getHeaders();
    const body = getBody(401, null);
    return getResponse(c, 401, headers, body);
  }
  // Optionally attach user to context
  c.set("user", user);
  await next();
};
```

• Update `route.ts` to use it:
```diff
 import { Hono } from "hono";
-import { deleteAvatar, getAvatar, uploadAvatar } from "./controller";
+import { deleteAvatar, getAvatar, uploadAvatar } from "./controller";
+import { requireAuth } from "./middleware";
 import { zValidator } from "@hono/zod-validator";
 import { uploadAvatarSchema } from "./validator";

 const user = new Hono();
+user.use(requireAuth);

 user.post(
   "/upload-avatar",
   zValidator("json", uploadAvatarSchema, (result, c) => {
     if (!result.success) return c.text("Invalid format", 422);
   }),
   uploadAvatar,
 );
 user.get("/get-avatar", getAvatar);
 user.delete("/delete-avatar", deleteAvatar);
```

This centralizes the authentication check and lets controllers assume a valid user.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/route.ts lines 1 to 18, the user routes
currently do not have authentication middleware applied, causing repeated user
checks in each controller. To fix this, create an authentication middleware
(e.g., requireAuth) in apps/backend/src/modules/user/middleware.ts that verifies
the user and returns a 401 response if unauthorized. Then, import and apply this
middleware to the user router using user.use(requireAuth) before defining the
routes, so all routes require authentication centrally and controllers can
assume a valid user.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+import { env } from "@/env";
+import {
+  S3Client,
+  PutObjectCommand,
+  GetObjectCommand,
+  DeleteObjectCommand,
+} from "@aws-sdk/client-s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
+
+const s3 = new S3Client({
+  region: env.AWS_REGION,
+  credentials: {
+    accessKeyId: env.AWS_ACCESS_KEY_ID,
+    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
+  },
+});
+
+export const uploadUrl = async ({
+  key,
+  type,
+}: { key: string; type: string }) => {
+  const command = new PutObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+    ContentType: type,
+  });
+
+  const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
+  return url;
+};
+
+export const getUrl = async ({ key }: { key: string }) => {
+  const command = new GetObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+  });
+
+  const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
+  return url;
+};
+
+export const deleteObject = async ({ key }: { key: string }) => {
+  const command = new DeleteObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+  });
+
+  await s3.send(command);
+  return true;
+};
path: apps/backend/src/utils/s3.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to deleteObject function and return appropriate result**

The deleteObject function lacks error handling and always returns true regardless of the operation's success or failure.


```diff
export const deleteObject = async ({ key }: { key: string }) => {
+  try {
    const command = new DeleteObjectCommand({
      Bucket: env.S3_BUCKET,
      Key: key,
    });

    await s3.send(command);
    return true;
+  } catch (error) {
+    console.error('Error deleting S3 object:', error);
+    throw new Error('Failed to delete S3 object');
+  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const deleteObject = async ({ key }: { key: string }) => {
  try {
    const command = new DeleteObjectCommand({
      Bucket: env.S3_BUCKET,
      Key: key,
    });

    await s3.send(command);
    return true;
  } catch (error) {
    console.error('Error deleting S3 object:', error);
    throw new Error('Failed to delete S3 object');
  }
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/utils/s3.ts around lines 42 to 50, the deleteObject function
currently lacks error handling and always returns true, which can mislead about
the operation's success. Wrap the s3.send(command) call in a try-catch block,
return true if the deletion succeeds, and return false or handle the error
appropriately if an exception occurs, ensuring the function accurately reflects
the outcome.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+import { env } from "@/env";
+import {
+  S3Client,
+  PutObjectCommand,
+  GetObjectCommand,
+  DeleteObjectCommand,
+} from "@aws-sdk/client-s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
+
+const s3 = new S3Client({
+  region: env.AWS_REGION,
+  credentials: {
+    accessKeyId: env.AWS_ACCESS_KEY_ID,
+    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
+  },
+});
+
+export const uploadUrl = async ({
+  key,
+  type,
+}: { key: string; type: string }) => {
+  const command = new PutObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+    ContentType: type,
+  });
+
+  const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
+  return url;
+};
+
+export const getUrl = async ({ key }: { key: string }) => {
+  const command = new GetObjectCommand({
+    Bucket: env.S3_BUCKET,
+    Key: key,
+  });
+
+  const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
+  return url;
+};
path: apps/backend/src/utils/s3.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to getUrl function**

Similar to the uploadUrl function, the getUrl function lacks error handling for potential S3 operation failures.


```diff
export const getUrl = async ({ key }: { key: string }) => {
+  try {
    const command = new GetObjectCommand({
      Bucket: env.S3_BUCKET,
      Key: key,
    });

    const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
    return url;
+  } catch (error) {
+    console.error('Error generating S3 get URL:', error);
+    throw new Error('Failed to generate S3 retrieval URL');
+  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const getUrl = async ({ key }: { key: string }) => {
  try {
    const command = new GetObjectCommand({
      Bucket: env.S3_BUCKET,
      Key: key,
    });

    const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });
    return url;
  } catch (error) {
    console.error('Error generating S3 get URL:', error);
    throw new Error('Failed to generate S3 retrieval URL');
  }
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/utils/s3.ts around lines 32 to 40, the getUrl function lacks
error handling for potential failures when generating the signed URL. Wrap the
existing code in a try-catch block, and in the catch block, log or handle the
error appropriately, then rethrow or return a meaningful error response to
ensure failures are managed gracefully.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+import { env } from "@/env";
+import {
+  S3Client,
+  PutObjectCommand,
+  GetObjectCommand,
+  DeleteObjectCommand,
+} from "@aws-sdk/client-s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
+
+const s3 = new S3Client({
+  region: env.AWS_REGION,
+  credentials: {
+    accessKeyId: env.AWS_ACCESS_KEY_ID,
+    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
+  },
+});
path: apps/backend/src/utils/s3.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation for AWS credentials**

The S3 client is initialized using AWS credentials from environment variables without validation. If these credentials are missing or invalid, the application might fail at runtime with unclear error messages.


```diff
const s3 = new S3Client({
  region: env.AWS_REGION,
  credentials: {
    accessKeyId: env.AWS_ACCESS_KEY_ID,
    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
  },
+  // Setting this to false will cause the client to validate credentials earlier
+  credentialDefaultProvider: () => async () => {
+    if (!env.AWS_ACCESS_KEY_ID || !env.AWS_SECRET_ACCESS_KEY) {
+      throw new Error('AWS credentials are missing or invalid');
+    }
+    return {
+      accessKeyId: env.AWS_ACCESS_KEY_ID,
+      secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
+    };
+  },
});
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/utils/s3.ts around lines 10 to 16, the AWS credentials used
to initialize the S3 client are not validated. Add checks to verify that
env.AWS_ACCESS_KEY_ID and env.AWS_SECRET_ACCESS_KEY are present and non-empty
before creating the S3Client instance. If either credential is missing, throw a
clear error or handle it gracefully to prevent runtime failures with unclear
messages.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,15 +1,39 @@
 import { Hono } from "hono";
 import { cors } from "hono/cors";
-import { status } from "@/modules/status/route";
 import { auth } from "./utils/auth";
 import { corsConfig } from "./utils/http";
+import { user } from "@/modules/user/route";
+import { status } from "@/modules/status/route";
 
-const app = new Hono();
+const app = new Hono<{
+  Variables: {
+    user: typeof auth.$Infer.Session.user | null;
+    session: typeof auth.$Infer.Session.session | null;
+  };
+}>();
 
 app.use("*", cors(corsConfig));
 
-app.on(["POST", "GET"], "/auth/**", (c) => auth.handler(c.req.raw));
+app.use("*", async (c, next) => {
+  const session = await auth.api.getSession({ headers: c.req.raw.headers });
+
+  if (!session) {
+    c.set("user", null);
+    c.set("session", null);
+    return next();
+  }
+
+  c.set("user", session.user);
+  c.set("session", session.session);
+  return next();
+});
+
+app.on(["POST", "GET"], "/auth/*", (c) => {
+  return auth.handler(c.req.raw);
+});
path: apps/backend/src/app.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider adding more specific route handlers**

The current implementation uses a wildcard pattern for auth routes. Consider more specific route handlers for better clarity and control.


```diff
-app.on(["POST", "GET"], "/auth/*", (c) => {
+app.on(["POST"], "/auth/login", (c) => {
+  return auth.handler(c.req.raw);
+});
+
+app.on(["GET"], "/auth/session", (c) => {
  return auth.handler(c.req.raw);
});
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
app.on(["POST"], "/auth/login", (c) => {
  return auth.handler(c.req.raw);
});

app.on(["GET"], "/auth/session", (c) => {
  return auth.handler(c.req.raw);
});
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/app.ts around lines 31 to 33, the current route handler uses
a wildcard pattern "/auth/*" for both POST and GET methods, which reduces
clarity and control over specific auth routes. Refactor this by defining more
specific route handlers for each distinct auth endpoint needed, such as
"/auth/login", "/auth/logout", or "/auth/register", and assign appropriate
handlers to each. This will improve route clarity and maintainability.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import { deleteObject, getUrl, uploadUrl } from "@/utils/s3";
+import { v4 as uuidv4 } from "uuid";
+
+export const uploadUserAvatar = async ({
+  fileName,
+  type,
+}: { fileName: string; type: string }) => {
+  const id = uuidv4();
+  const url = await uploadUrl({ key: `avatar/${id}/${fileName}`, type });
+  return url;
+};
+
+export const getUserAvatar = async ({ key }: { key: string }) => {
+  const url = await getUrl({ key });
+  return url;
+};
+
+export const deleteUserAvatar = async ({ key }: { key: string }) => {
+  await deleteObject({ key });
+};
path: apps/backend/src/modules/user/service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Return success status from deleteUserAvatar function**

The function doesn't return any value, but the deleteObject function returns a boolean indicating success. This return value should be propagated to callers.


```diff
export const deleteUserAvatar = async ({ key }: { key: string }) => {
+  try {
+    if (!key || typeof key !== 'string') {
+      throw new Error('Invalid key');
+    }
-  await deleteObject({ key });
+    const result = await deleteObject({ key });
+    return result;
+  } catch (error) {
+    console.error('Error deleting avatar:', error);
+    throw new Error('Failed to delete avatar');
+  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const deleteUserAvatar = async ({ key }: { key: string }) => {
  try {
    if (!key || typeof key !== 'string') {
      throw new Error('Invalid key');
    }
    const result = await deleteObject({ key });
    return result;
  } catch (error) {
    console.error('Error deleting avatar:', error);
    throw new Error('Failed to delete avatar');
  }
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/service.ts around lines 18 to 20, the
deleteUserAvatar function currently does not return any value, but since
deleteObject returns a boolean indicating success, modify deleteUserAvatar to
return the result of the deleteObject call so that the success status is
propagated to the callers.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import { deleteObject, getUrl, uploadUrl } from "@/utils/s3";
+import { v4 as uuidv4 } from "uuid";
+
+export const uploadUserAvatar = async ({
+  fileName,
+  type,
+}: { fileName: string; type: string }) => {
+  const id = uuidv4();
+  const url = await uploadUrl({ key: `avatar/${id}/${fileName}`, type });
+  return url;
+};
+
+export const getUserAvatar = async ({ key }: { key: string }) => {
+  const url = await getUrl({ key });
+  return url;
+};
path: apps/backend/src/modules/user/service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to getUserAvatar function**

The function should include error handling for potential failures when retrieving the avatar URL.


```diff
export const getUserAvatar = async ({ key }: { key: string }) => {
+  try {
+    if (!key || typeof key !== 'string') {
+      throw new Error('Invalid key');
+    }
    const url = await getUrl({ key });
    return url;
+  } catch (error) {
+    console.error('Error retrieving avatar URL:', error);
+    throw new Error('Failed to retrieve avatar URL');
+  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const getUserAvatar = async ({ key }: { key: string }) => {
  try {
    if (!key || typeof key !== 'string') {
      throw new Error('Invalid key');
    }
    const url = await getUrl({ key });
    return url;
  } catch (error) {
    console.error('Error retrieving avatar URL:', error);
    throw new Error('Failed to retrieve avatar URL');
  }
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/service.ts around lines 13 to 16, the
getUserAvatar function lacks error handling for potential failures when calling
getUrl. Wrap the call to getUrl in a try-catch block, and in the catch block,
handle or log the error appropriately, then rethrow or return a fallback value
to ensure the function handles errors gracefully.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar, getUserAvatar, deleteUserAvatar } from "./service";
+import { checkUser } from "@/utils/checkUser";
+
+export const uploadAvatar = async (context: Context) => {
+  const user = checkUser({ context });
+
+  if (!user) {
+    const headers = getHeaders();
+    const body = getBody(401, null);
+    return getResponse(context, 401, headers, body);
+  }
+
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to upload profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const getAvatar = async (context: Context) => {
+  const user = checkUser({ context });
+
+  if (!user) {
+    const headers = getHeaders();
+    const body = getBody(401, null);
+    return getResponse(context, 401, headers, body);
+  }
+
+  try {
+    const { key } = await context.req.json();
+    const result = await getUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const deleteAvatar = async (context: Context) => {
+  const user = checkUser({ context });
+
+  if (!user) {
+    const headers = getHeaders();
+    const body = getBody(401, null);
+    return getResponse(context, 401, headers, body);
+  }
+
+  try {
+    const { key } = await context.req.json();
+    const result = await deleteUserAvatar({ key });
+
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix return value handling for deleteUserAvatar**

The deleteUserAvatar service function doesn't return anything, but the controller is using its return value. Ensure the service function returns a proper value.


```diff
    const { key } = await context.req.json();
-    const result = await deleteUserAvatar({ key });
+    await deleteUserAvatar({ key });
+    const result = { success: true };
```

Alternatively, update the service function to return a success status:

```typescript
// In service.ts
export const deleteUserAvatar = async ({ key }: { key: string }) => {
  const result = await deleteObject({ key });
  return { success: result };
};
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts around lines 70 to 72, the
controller expects a return value from deleteUserAvatar, but the service
function currently returns nothing. To fix this, update the deleteUserAvatar
service function to return a success status object indicating the result of the
delete operation, or alternatively, modify the controller to not rely on a
return value if no meaningful data is returned.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar, getUserAvatar, deleteUserAvatar } from "./service";
+import { checkUser } from "@/utils/checkUser";
+
+export const uploadAvatar = async (context: Context) => {
+  const user = checkUser({ context });
+
+  if (!user) {
+    const headers = getHeaders();
+    const body = getBody(401, null);
+    return getResponse(context, 401, headers, body);
+  }
+
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to upload profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const getAvatar = async (context: Context) => {
+  const user = checkUser({ context });
+
+  if (!user) {
+    const headers = getHeaders();
+    const body = getBody(401, null);
+    return getResponse(context, 401, headers, body);
+  }
+
+  try {
+    const { key } = await context.req.json();
+    const result = await getUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const deleteAvatar = async (context: Context) => {
+  const user = checkUser({ context });
+
+  if (!user) {
+    const headers = getHeaders();
+    const body = getBody(401, null);
+    return getResponse(context, 401, headers, body);
+  }
+
+  try {
+    const { key } = await context.req.json();
+    const result = await deleteUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Fix error logging message in deleteAvatar function**

The error message incorrectly refers to getting a profile picture instead of deleting it.


```diff
-    console.error("Failed to get profile picture", error);
+    console.error("Failed to delete profile picture", error);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    console.error("Failed to delete profile picture", error);
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts at line 78, the error logging
message in the deleteAvatar function incorrectly states "Failed to get profile
picture". Update this message to accurately reflect the operation by changing it
to "Failed to delete profile picture" to correctly describe the error context.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+import { getHeaders, getBody, getResponse } from "@/utils/http";
+import { checkUser } from "@/utils/checkUser";
+import type { Context } from "hono";
+
+export const middleware = async (c: Context, next: () => Promise<void>) => {
+  const user = checkUser({ context: c });
+  if (!user) {
+    const headers = getHeaders();
+    const body = getBody(401, null);
+    return getResponse(c, 401, headers, body);
+  }
+  c.set("user", user);
path: apps/backend/src/utils/middleware.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Review user object setting in context.**

Based on the `checkUser` implementation, it appears to only return a boolean (true/false) rather than the actual user object. Setting this boolean value to the user context key may be misleading.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the implementation of checkUser to understand what it returns
echo "checkUser implementation:"
cat apps/backend/src/utils/checkUser.ts

# Check if user is actually used somewhere in the codebase
echo -e "\nUsage of context.get('user'):"
rg "context.get\(['\"]user['\"]" apps/backend/src/
```

Length of output: 553

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the contents of the middleware file to confirm usage of checkUser and context.set
echo "apps/backend/src/utils/middleware.ts:"
sed -n '1,200p' apps/backend/src/utils/middleware.ts
```

Length of output: 583

---



</details>

**Return a real user object instead of a boolean**  

The current checkUser implementation returns only true/false, so c.set("user", user) stores a boolean under `"user"`. You likely intend to store the actual user data.  

• apps/backend/src/utils/checkUser.ts  
• apps/backend/src/utils/middleware.ts  

Suggested change:

```diff
--- a/apps/backend/src/utils/checkUser.ts
+++ b/apps/backend/src/utils/checkUser.ts
-export const checkUser = ({ context }: { context: Context }) => {
-  const session = context.get("session");
-  const user = context.get("user");
-
-  if (!session || !user) return false;
-
-  return true;
-};
+/** Returns the actual user or null if unauthenticated */
+export const getUser = ({ context }: { context: Context }) => {
+  const session = context.get("session");
+  const user = context.get("user");
+  if (!session || !user) return null;
+  return user;
+};
```

```diff
--- a/apps/backend/src/utils/middleware.ts
+++ b/apps/backend/src/utils/middleware.ts
-import { checkUser } from "@/utils/checkUser";
+import { getUser } from "@/utils/checkUser";
 
 export const middleware = async (c: Context, next: () => Promise<void>) => {
-  const user = checkUser({ context: c });
+  const user = getUser({ context: c });
   if (!user) {
     const headers = getHeaders();
     const body = getBody(401, null);
     return getResponse(c, 401, headers, body);
   }
   c.set("user", user);
   await next();
 };
```

If you only need a boolean guard, rename the function to `isAuthenticated` and avoid setting it as the user.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/utils/middleware.ts at line 12, the code sets the result of
checkUser, which returns a boolean, as the "user" in context. This is misleading
because "user" should hold the actual user object, not a boolean. To fix this,
modify the middleware to retrieve the real user object (e.g., from a token or
session) and set that in context instead of the boolean. Alternatively, if only
a boolean is needed, rename checkUser to isAuthenticated and avoid setting it as
"user" in context.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:53:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar, getUserAvatar, deleteUserAvatar } from "./service";
+
+export const uploadAvatar = async (context: Context) => {
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to upload profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Ensure uploadAvatar handler uses the validation results.**

The controller currently extracts values directly from the request JSON, but since you're using zValidator middleware, you should use the validated data from the middleware.


```diff
export const uploadAvatar = async (context: Context) => {
  try {
-    const { fileName, type } = await context.req.json();
+    // Get the validated data from the validator middleware
+    const { fileName, type } = context.req.valid('json');
    const result = await uploadUserAvatar({ fileName, type });

    const headers = getHeaders();
    const body = getBody(200, result);

    return getResponse(context, 200, headers, body);
  } catch (error) {
    console.error("Failed to upload profile picture", error);

    const headers = getHeaders();
    const body = getBody(500, null, error);

    return getResponse(context, 500, headers, body);
  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const uploadAvatar = async (context: Context) => {
  try {
    // Get the validated data from the validator middleware
    const { fileName, type } = context.req.valid('json');
    const result = await uploadUserAvatar({ fileName, type });

    const headers = getHeaders();
    const body = getBody(200, result);

    return getResponse(context, 200, headers, body);
  } catch (error) {
    console.error("Failed to upload profile picture", error);

    const headers = getHeaders();
    const body = getBody(500, null, error);

    return getResponse(context, 500, headers, body);
  }
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts around lines 5 to 22, the
uploadAvatar handler extracts fileName and type directly from the request JSON
instead of using the validated data from the zValidator middleware. Modify the
handler to access the validated data provided by the middleware, typically
available on the context or request object, and use that data for the
uploadUserAvatar call to ensure only validated inputs are processed.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:53:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar, getUserAvatar, deleteUserAvatar } from "./service";
+
+export const uploadAvatar = async (context: Context) => {
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to upload profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const getAvatar = async (context: Context) => {
+  try {
+    const { key } = await context.req.json();
+    const result = await getUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const deleteAvatar = async (context: Context) => {
+  try {
+    const { key } = await context.req.json();
+    const result = await deleteUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Enhance error logging with request context.**

Consider adding more context to error logs to help with debugging.


```diff
  try {
    const { key } = await context.req.json();
    const result = await deleteUserAvatar({ key });

    const headers = getHeaders();
    const body = getBody(200, result);

    return getResponse(context, 200, headers, body);
  } catch (error) {
-    console.error("Failed to get profile picture", error);
+    console.error("Failed to delete profile picture", {
+      error: error instanceof Error ? error.message : String(error),
+      stack: error instanceof Error ? error.stack : undefined,
+      key, // Include the key that failed
+      url: context.req.url,
+    });

    const headers = getHeaders();
    const body = getBody(500, null, error);

    return getResponse(context, 500, headers, body);
  }
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts around lines 43 to 60, the error
logging in the catch block only outputs a generic message and the error object.
Enhance the error logging by including relevant request context such as the
request key or other identifying information from the input to provide better
debugging insights. Modify the console.error call to include this additional
context along with the error details.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:53:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar, getUserAvatar, deleteUserAvatar } from "./service";
+
+export const uploadAvatar = async (context: Context) => {
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to upload profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const getAvatar = async (context: Context) => {
+  try {
+    const { key } = await context.req.json();
+    const result = await getUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Improve error handling in getAvatar controller.**

Consider adding more specific error responses based on the type of error encountered.


```diff
export const getAvatar = async (context: Context) => {
  try {
    const { key } = await context.req.json();
+    
+    if (!key) {
+      const headers = getHeaders();
+      const body = getBody(400, null, new Error('Missing required key parameter'));
+      return getResponse(context, 400, headers, body);
+    }
+    
    const result = await getUserAvatar({ key });

    const headers = getHeaders();
    const body = getBody(200, result);

    return getResponse(context, 200, headers, body);
  } catch (error) {
    console.error("Failed to get profile picture", error);
+    
+    // Determine appropriate status code based on error
+    const statusCode = error.message?.includes('Invalid key') ? 400 : 500;

    const headers = getHeaders();
-    const body = getBody(500, null, error);
+    const body = getBody(statusCode, null, error);

-    return getResponse(context, 500, headers, body);
+    return getResponse(context, statusCode, headers, body);
  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const getAvatar = async (context: Context) => {
  try {
    const { key } = await context.req.json();

    if (!key) {
      const headers = getHeaders();
      const body = getBody(400, null, new Error('Missing required key parameter'));
      return getResponse(context, 400, headers, body);
    }

    const result = await getUserAvatar({ key });

    const headers = getHeaders();
    const body = getBody(200, result);

    return getResponse(context, 200, headers, body);
  } catch (error) {
    console.error("Failed to get profile picture", error);

    // Determine appropriate status code based on error
    const statusCode = error.message?.includes('Invalid key') ? 400 : 500;

    const headers = getHeaders();
    const body = getBody(statusCode, null, error);

    return getResponse(context, statusCode, headers, body);
  }
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts around lines 24 to 41, the
getAvatar function currently returns a generic 500 error for all exceptions.
Improve error handling by inspecting the error type or properties and returning
more specific HTTP status codes and messages accordingly. For example, handle
validation errors with a 400 status, not found errors with 404, and keep 500 for
unexpected errors. Adjust the response body to reflect these specific error
details to provide clearer feedback to the client.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T21:53:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,59 @@
+"use client";
+
+import { Separator } from "../ui/separator";
+import { ScrollArea } from "../ui/scroll-area";
+import type { User } from "better-auth/types";
+import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { User03Icon } from "@hugeicons/core-free-icons";
+import { Banner } from "../ui/banner";
+
+interface ProfileTabProps {
+  user: User | null;
+  error: Error | null;
+}
+
+export default function ProfileTab({ user, error }: ProfileTabProps) {
+  if (error) {
+    return (
+      <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
+        <Banner variant="error">We could not load the content</Banner>
+      </div>
+    );
+  }
+
+  return (
+    <div className="w-full flex flex-col h-full">
+      <ScrollArea className="flex-1 pb-16 max-h-[85vh] scrollbar-gutter-stable overflow-y-auto">
+        <div className="w-full py-0 px-4 pb-10">
+          <div className="w-full flex flex-col gap-6">
+            <div className="flex w-full justify-between">
+              <Avatar className="size-8 rounded-lg">
+                <AvatarImage
+                  src={user?.image ?? ""}
+                  alt={`${user?.name} profile picture`}
+                />
+                <AvatarFallback className="text-foreground/70">
+                  <HugeiconsIcon
+                    icon={User03Icon}
+                    color="currentColor"
+                    strokeWidth={2}
+                  />
+                </AvatarFallback>
+              </Avatar>{" "}
path: apps/frontend/src/components/settings/ProfileTab.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Remove unintentional whitespace**

There's a `{" "}` after the Avatar component which appears to be unintentional.


```diff
-</Avatar>{" "}
+</Avatar>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
              </Avatar>
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/settings/ProfileTab.tsx at line 43, remove the
unintentional whitespace represented by {" "} immediately following the Avatar
component to clean up the JSX and avoid unnecessary spacing.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 9370ad3
created_at: 2025-05-22T23:32:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,59 @@
+"use client";
+
+import { Separator } from "../ui/separator";
+import { ScrollArea } from "../ui/scroll-area";
+import type { User } from "better-auth/types";
+import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { User03Icon } from "@hugeicons/core-free-icons";
+import { Banner } from "../ui/banner";
+
+interface ProfileTabProps {
+  user: User | null;
+  error: Error | null;
+}
+
+export default function ProfileTab({ user, error }: ProfileTabProps) {
+  if (error) {
+    return (
+      <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
+        <Banner variant="error">We could not load the content</Banner>
+      </div>
+    );
+  }
path: apps/frontend/src/components/settings/ProfileTab.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider handling loading state**

The component handles error states but doesn't explicitly handle loading states. Consider adding a loading indicator when data is being fetched.


```diff
interface ProfileTabProps {
  user: User | null;
  error: Error | null;
+ isLoading?: boolean;
}

export default function ProfileTab({ user, error, isLoading }: ProfileTabProps) {
+ if (isLoading) {
+   return (
+     <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
+       <p>Loading profile information...</p>
+     </div>
+   );
+ }
  
  if (error) {
    return (
      <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
        <Banner variant="error">We could not load the content</Banner>
      </div>
    );
  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
interface ProfileTabProps {
  user: User | null;
  error: Error | null;
  isLoading?: boolean;
}

export default function ProfileTab({ user, error, isLoading }: ProfileTabProps) {
  if (isLoading) {
    return (
      <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
        <p>Loading profile information...</p>
      </div>
    );
  }

  if (error) {
    return (
      <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
        <Banner variant="error">We could not load the content</Banner>
      </div>
    );
  }

  // ...rest of the component rendering when user is available
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/settings/ProfileTab.tsx around lines 16 to 23,
the component currently handles error states but lacks handling for loading
states. Add a loading state check before rendering the content or error message,
and display a loading indicator (such as a spinner or a loading banner) when the
data is being fetched to improve user experience during asynchronous operations.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T23:32:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,59 @@
+"use client";
+
+import { Separator } from "../ui/separator";
+import { ScrollArea } from "../ui/scroll-area";
+import type { User } from "better-auth/types";
+import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { User03Icon } from "@hugeicons/core-free-icons";
+import { Banner } from "../ui/banner";
+
+interface ProfileTabProps {
+  user: User | null;
+  error: Error | null;
+}
+
+export default function ProfileTab({ user, error }: ProfileTabProps) {
+  if (error) {
+    return (
+      <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
+        <Banner variant="error">We could not load the content</Banner>
+      </div>
+    );
+  }
+
+  return (
+    <div className="w-full flex flex-col h-full">
+      <ScrollArea className="flex-1 pb-16 max-h-[85vh] scrollbar-gutter-stable overflow-y-auto">
+        <div className="w-full py-0 px-4 pb-10">
+          <div className="w-full flex flex-col gap-6">
+            <div className="flex w-full justify-between">
+              <Avatar className="size-8 rounded-lg">
+                <AvatarImage
+                  src={user?.image ?? ""}
+                  alt={`${user?.name} profile picture`}
+                />
+                <AvatarFallback className="text-foreground/70">
+                  <HugeiconsIcon
+                    icon={User03Icon}
+                    color="currentColor"
+                    strokeWidth={2}
+                  />
+                </AvatarFallback>
+              </Avatar>{" "}
path: apps/frontend/src/components/settings/ProfileTab.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Optimize avatar image handling**

The current implementation for the avatar source could lead to unnecessary network requests when `user?.image` is empty.


```diff
<Avatar className="size-8 rounded-lg">
-  <AvatarImage
-    src={user?.image ?? ""}
-    alt={`${user?.name} profile picture`}
-  />
+  {user?.image ? (
+    <AvatarImage
+      src={user?.image}
+      alt={`${user?.name} profile picture`}
+    />
+  ) : null}
  <AvatarFallback className="text-foreground/70">
    <HugeiconsIcon
      icon={User03Icon}
      color="currentColor"
      strokeWidth={2}
    />
  </AvatarFallback>
</Avatar>{" "}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
              <Avatar className="size-8 rounded-lg">
-                <AvatarImage
-                  src={user?.image ?? ""}
-                  alt={`${user?.name} profile picture`}
-                />
+                {user?.image ? (
+                  <AvatarImage
+                    src={user?.image}
+                    alt={`${user?.name} profile picture`}
+                  />
+                ) : null}
                <AvatarFallback className="text-foreground/70">
                  <HugeiconsIcon
                    icon={User03Icon}
                    color="currentColor"
                    strokeWidth={2}
                  />
                </AvatarFallback>
              </Avatar>{" "}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/settings/ProfileTab.tsx around lines 31 to 43,
the AvatarImage src is set to an empty string when user?.image is not available,
which can cause unnecessary network requests. To fix this, conditionally render
the AvatarImage component only if user?.image exists; otherwise, rely on the
AvatarFallback to display the placeholder icon. This avoids loading an empty or
invalid image source.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T23:32:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,9 +95,10 @@ export function SettingsDialog({
               className="w-full flex-1 h-full overflow-hidden pl-4"
               value="profile"
             >
-              <div className="flex flex-col min-h-[80vh] items-center justify-center">
-                Profile Tab Content
-              </div>
+              <ProfileTab
+                user={session?.user ?? null}
+                error={error as Error | null}
path: apps/frontend/src/components/settings/SettingsDialog.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Avoid type assertions when possible**

The type assertion `error as Error | null` might be problematic if the actual error is of a different type.


```diff
<ProfileTab
  user={session?.user ?? null}
- error={error as Error | null}
+ error={error ? (error instanceof Error ? error : new Error(String(error))) : null}
/>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
<ProfileTab
  user={session?.user ?? null}
  error={error ? (error instanceof Error ? error : new Error(String(error))) : null}
/>
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/settings/SettingsDialog.tsx at line 100, avoid
using the type assertion 'error as Error | null' because the actual error might
not be of type Error. Instead, ensure that the 'error' variable is properly
typed or validated before passing it, or use a type guard or conditional check
to safely handle different error types without forcing a cast.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T23:32:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,14 +44,14 @@ export function SettingsDialog({
   const sortedSessions = useMemo(() => {
     if (!sessionsData) return null;
     return sessionsData.slice().sort((a, b) => {
-      if (a.id === currentSessionId) return -1;
-      if (b.id === currentSessionId) return 1;
+      if (a.id === session?.session.id) return -1;
+      if (b.id === session?.session.id) return 1;
       return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
     });
-  }, [sessionsData, currentSessionId]);
+  }, [sessionsData, session?.session.id]);
path: apps/frontend/src/components/settings/SettingsDialog.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add type safety for session id access**

The session structure has changed, and accessing nested properties requires careful handling to avoid TypeScript errors.


```diff
return sessionsData.slice().sort((a, b) => {
-  if (a.id === session?.session.id) return -1;
-  if (b.id === session?.session.id) return 1;
+  const currentSessionId = session?.session?.id;
+  if (currentSessionId && a.id === currentSessionId) return -1;
+  if (currentSessionId && b.id === currentSessionId) return 1;
  return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
}, [sessionsData, session?.session.id]);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
      // Add type-safe lookup for the current session ID
      const currentSessionId = session?.session?.id;
      if (currentSessionId && a.id === currentSessionId) return -1;
      if (currentSessionId && b.id === currentSessionId) return 1;
      return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
    });
  }, [sessionsData, session?.session.id]);
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/settings/SettingsDialog.tsx around lines 47 to
51, the code accesses session?.session.id without type safety, which can cause
TypeScript errors due to changes in the session structure. Update the code to
safely access the session id by adding proper type checks or optional chaining
that matches the new session structure, ensuring no runtime errors or type
violations occur when accessing nested properties.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T23:32:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+import { z } from "zod";
+
+export const uploadAvatarSchema = z.object({
+  fileName: z
+    .string()
+    .min(1)
+    .refine(
+      (val) => !val.includes("..") && !val.includes("/") && !val.includes("\\"),
+    ),
+  type: z.string().regex(/^image\/(jpeg|jpg|png|webp)$/),
path: apps/backend/src/modules/user/validator.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix incorrect MIME type in regex pattern.**

The regex includes `image/jpg` which is not a valid MIME type. JPEG images use the MIME type `image/jpeg`.



```diff
-  type: z.string().regex(/^image\/(jpeg|jpg|png|webp)$/),
+  type: z.string().regex(/^image\/(jpeg|png|webp)$/),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  type: z.string().regex(/^image\/(jpeg|png|webp)$/),
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/validator.ts at line 10, the regex pattern
incorrectly includes "image/jpg" as a MIME type, which is invalid. Update the
regex to replace "jpg" with "jpeg" so it correctly matches "image/jpeg" instead
of "image/jpg".
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T02:33:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+import { z } from "zod";
+
+export const uploadAvatarSchema = z.object({
+  fileName: z
+    .string()
+    .min(1)
+    .refine(
+      (val) => !val.includes("..") && !val.includes("/") && !val.includes("\\"),
+    ),
path: apps/backend/src/modules/user/validator.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error message for filename validation.**

The `refine` function should include a descriptive error message to help with debugging and user feedback.



```diff
  .refine(
    (val) => !val.includes("..") && !val.includes("/") && !val.includes("\\"),
+   {
+     message: "Filename cannot contain path separators or relative path indicators",
+   },
  ),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    .refine(
      (val) => !val.includes("..") && !val.includes("/") && !val.includes("\\"),
      {
        message: "Filename cannot contain path separators or relative path indicators",
      },
    ),
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/validator.ts around lines 7 to 9, the refine
function used for filename validation lacks an error message. Update the refine
call to include a descriptive error message string that clearly explains the
validation failure, so it provides helpful feedback during debugging and to
users.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T02:33:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,14 +29,44 @@ export default function UserButton() {
     await auth.signOut();
   }
 
+  const { data: imageUrl } = useQuery({
+    queryKey: ["image-url", session?.user.image],
+    queryFn: async () => {
+      if (!session?.user.image) {
+        return null;
+      }
+
+      const url = new URL(`${process.env.NEXT_PUBLIC_API_URL}/user/get-avatar`);
+      url.searchParams.append("key", session.user.image);
path: apps/frontend/src/components/sidebar/UserButton.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Improve URL construction robustness.**

The current URL construction concatenates strings directly, which could be error-prone if the API URL doesn't end with a slash or contains query parameters.



```diff
- const url = new URL(`${process.env.NEXT_PUBLIC_API_URL}/user/get-avatar`);
+ const url = new URL("/user/get-avatar", process.env.NEXT_PUBLIC_API_URL);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
      const url = new URL("/user/get-avatar", process.env.NEXT_PUBLIC_API_URL);
      url.searchParams.append("key", session.user.image);
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/sidebar/UserButton.tsx around lines 39 to 40,
the URL is constructed by directly concatenating the base API URL and the path,
which can cause issues if the base URL lacks a trailing slash or already
contains query parameters. To fix this, use the URL constructor properly by
passing the path as the first argument and the base URL as the second argument,
ensuring correct URL resolution regardless of the base URL format. Then append
the search parameters as needed.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T02:33:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,14 +29,44 @@ export default function UserButton() {
     await auth.signOut();
   }
 
+  const { data: imageUrl } = useQuery({
+    queryKey: ["image-url", session?.user.image],
+    queryFn: async () => {
+      if (!session?.user.image) {
+        return null;
+      }
+
+      const url = new URL(`${process.env.NEXT_PUBLIC_API_URL}/user/get-avatar`);
+      url.searchParams.append("key", session.user.image);
+
+      const presignedUrlResponse = await fetch(url.toString(), {
+        method: "GET",
+        headers: { "Content-Type": "application/json" },
+        credentials: "include",
+      });
+
+      if (!presignedUrlResponse.ok) {
+        throw new Error(
+          `Failed to fetch avatar URL: ${presignedUrlResponse.status}`,
+        );
+      }
+
+      const presignedUrlData = await presignedUrlResponse.json();
+      return presignedUrlData.data || null;
+    },
+    enabled: !!session && !!session.user.image,
+    staleTime: 5 * 60 * 1000,
+    retry: 2,
+  });
path: apps/frontend/src/components/sidebar/UserButton.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add loading state handling for avatar fetch.**

The avatar query doesn't handle loading states, which could result in a brief flash of the fallback avatar while the presigned URL is being fetched.



Consider updating the implementation to show a loading indicator or skeleton:

```diff
- const { data: imageUrl } = useQuery({
+ const { data: imageUrl, isPending: isLoadingAvatar } = useQuery({
    queryKey: ["image-url", session?.user.image],
    // ... rest of query config
  });

  // In the Avatar component:
  <AvatarImage
-   src={imageUrl ?? ""}
+   src={isPending ? undefined : (imageUrl ?? "")}
    alt={`${session?.user.name} profile picture`}
  />
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/sidebar/UserButton.tsx around lines 32 to 60,
the avatar fetch query lacks loading state handling, causing a fallback avatar
flash during fetch. Update the component to check the loading state from
useQuery and render a loading indicator or skeleton avatar while the presigned
URL is being fetched, ensuring a smooth user experience without abrupt avatar
changes.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T02:33:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+import type { Context } from "hono";
+import { getBody, getHeaders, getResponse } from "@/utils/http";
+import { uploadUserAvatar, getUserAvatar, deleteUserAvatar } from "./service";
+
+export const uploadAvatar = async (context: Context) => {
+  try {
+    const { fileName, type } = await context.req.json();
+    const result = await uploadUserAvatar({ fileName, type });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to upload profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const getAvatar = async (context: Context) => {
+  try {
+    const key = context.req.query("key");
+    if (!key) {
+      const headers = getHeaders();
+      const body = getBody(400, null);
+
+      return getResponse(context, 400, headers, body);
+    }
+    const result = await getUserAvatar({ key });
+
+    const headers = getHeaders();
+    const body = getBody(200, result);
+
+    return getResponse(context, 200, headers, body);
+  } catch (error) {
+    console.error("Failed to get profile picture", error);
+
+    const headers = getHeaders();
+    const body = getBody(500, null, error);
+
+    return getResponse(context, 500, headers, body);
+  }
+};
+
+export const deleteAvatar = async (context: Context) => {
+  try {
+    const { key } = await context.req.json();
+    const result = await deleteUserAvatar({ key });
path: apps/backend/src/modules/user/controller.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add input validation for the key parameter.**

The function should validate that the key parameter is present before attempting deletion.

```diff
     const { key } = await context.req.json();
+    
+    if (!key) {
+      const headers = getHeaders();
+      const body = getBody(400, null, new Error('Missing required field: key'));
+      return getResponse(context, 400, headers, body);
+    }
+    
     const result = await deleteUserAvatar({ key });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    const { key } = await context.req.json();
    
    if (!key) {
      const headers = getHeaders();
      const body = getBody(400, null, new Error('Missing required field: key'));
      return getResponse(context, 400, headers, body);
    }
    
    const result = await deleteUserAvatar({ key });
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/backend/src/modules/user/controller.ts around lines 51 to 52, the key
parameter extracted from the request body is used without validation. Add a
check to ensure the key parameter is present and valid before calling
deleteUserAvatar. If the key is missing or invalid, return an appropriate error
response to prevent unintended behavior.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T02:33:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,252 @@
+"use client";
+
+import { useState, type ChangeEvent } from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Separator } from "../ui/separator";
+import { ScrollArea } from "../ui/scroll-area";
+import type { User } from "better-auth/types";
+import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
+import { HugeiconsIcon } from "@hugeicons/react";
+import {
+  User03Icon,
+  Loading02Icon,
+  PaintBrush01Icon,
+} from "@hugeicons/core-free-icons";
+import { Banner } from "../ui/banner";
+import { Input } from "../ui/input";
+import { auth } from "@/auth/client";
+
+interface ProfileTabProps {
+  user: User | null;
+  error: Error | null;
+}
+
+export default function ProfileTab({ user, error }: ProfileTabProps) {
+  const queryClient = useQueryClient();
+  const [clientSideUploadError, setClientSideUploadError] = useState<
+    string | null
+  >(null);
+  const [optimisticAvatarUrl, setOptimisticAvatarUrl] = useState<string | null>(
+    null,
+  );
+
+  const { data: imageUrl } = useQuery({
+    queryKey: ["image-url", user?.image],
+    queryFn: async () => {
+      if (!user?.image) {
+        return null;
+      }
+
+      const url = new URL(`${process.env.NEXT_PUBLIC_API_URL}/user/get-avatar`);
+      url.searchParams.append("key", user.image);
+
+      const presignedUrlResponse = await fetch(url.toString(), {
+        method: "GET",
+        headers: { "Content-Type": "application/json" },
+        credentials: "include",
+      });
+
+      if (!presignedUrlResponse.ok) {
+        throw new Error(
+          `Failed to fetch avatar URL: ${presignedUrlResponse.status}`,
+        );
+      }
+
+      const presignedUrlData = await presignedUrlResponse.json();
+      return presignedUrlData.data || null;
+    },
+    enabled: !!user && !!user.image,
+    staleTime: 5 * 60 * 1000,
+    retry: 2,
+  });
+
+  const uploadAvatarMutation = useMutation({
+    mutationFn: async (file: File) => {
+      setClientSideUploadError(null);
+      const fileName = encodeURIComponent(file.name);
+      const type = file.type;
+
+      const presignedUrlResponse = await fetch(
+        `${process.env.NEXT_PUBLIC_API_URL}/user/upload-avatar`,
+        {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          credentials: "include",
+          body: JSON.stringify({ fileName, type }),
+        },
+      );
+
+      if (!presignedUrlResponse.ok) {
+        const errorData = await presignedUrlResponse.json().catch(() => ({}));
+        throw new Error(errorData.message || "Upload failed.");
+      }
+      const presignedUrlData = await presignedUrlResponse.json();
+      const { url, key } = presignedUrlData.data;
+
+      const uploadResponse = await fetch(url, {
+        method: "PUT",
+        headers: { "Content-Type": type },
+        body: file,
+      });
+
+      if (!uploadResponse.ok) {
+        throw new Error("File upload to storage failed.");
+      }
+
+      const updateUserResponse = await auth.updateUser({ image: key });
+      if (updateUserResponse.error) {
+        throw new Error(
+          updateUserResponse.error.message || "Failed to update user profile.",
+        );
+      }
+      return updateUserResponse.data;
+    },
+    onMutate: async (file: File) => {
+      const localUrl = URL.createObjectURL(file);
+      setOptimisticAvatarUrl(localUrl);
+      return { localUrl };
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ["currentSession"] });
+    },
+    onError: (
+      _error: Error,
+      _variables,
+      _context?: { localUrl?: string },
+    ) => {},
+    onSettled: (_data, _error, _variables, context?: { localUrl?: string }) => {
+      if (context?.localUrl) {
+        URL.revokeObjectURL(context.localUrl);
+      }
+      if (_error) {
+        setOptimisticAvatarUrl(null);
+      }
+    },
+  });
+
+  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
+    const file = event.target.files?.[0];
+    if (file) {
+      if (file.size > 5 * 1024 * 1024) {
+        setClientSideUploadError("File is too large (max 5MB).");
+        uploadAvatarMutation.reset();
+        return;
+      }
+      if (
+        !["image/jpeg", "image/png", "image/webp", "image/jpg"].includes(
+          file.type,
+        )
path: apps/frontend/src/components/settings/ProfileTab.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix inconsistency between file type validation and accept attribute.**

The validation logic checks for JPEG, PNG, and WEBP files, but the accept attribute on the input element includes GIF.

```diff
-                accept="image/png, image/jpeg, image/webp, image/gif"
+                accept="image/png, image/jpeg, image/webp, image/jpg"
```

Alternatively, if GIF support is intended:
```diff
       if (
-        !["image/jpeg", "image/png", "image/webp", "image/jpg"].includes(
+        !["image/jpeg", "image/png", "image/webp", "image/jpg", "image/gif"].includes(
           file.type,
         )
       ) {
         setClientSideUploadError(
-          "Invalid file type. Please use JPG, PNG or WEBP.",
+          "Invalid file type. Please use JPG, PNG, WEBP, or GIF.",
         );
```


Also applies to: 214-214

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/settings/ProfileTab.tsx around lines 136 to 138
and line 214, the file type validation excludes GIF while the input's accept
attribute includes it, causing inconsistency. To fix this, either add
"image/gif" to the validation array to match the accept attribute if GIF support
is intended, or remove "image/gif" from the accept attribute to align with the
current validation logic.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T02:33:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,252 @@
+"use client";
+
+import { useState, type ChangeEvent } from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Separator } from "../ui/separator";
+import { ScrollArea } from "../ui/scroll-area";
+import type { User } from "better-auth/types";
+import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
+import { HugeiconsIcon } from "@hugeicons/react";
+import {
+  User03Icon,
+  Loading02Icon,
+  PaintBrush01Icon,
+} from "@hugeicons/core-free-icons";
+import { Banner } from "../ui/banner";
+import { Input } from "../ui/input";
+import { auth } from "@/auth/client";
+
+interface ProfileTabProps {
+  user: User | null;
+  error: Error | null;
+}
+
+export default function ProfileTab({ user, error }: ProfileTabProps) {
+  const queryClient = useQueryClient();
+  const [clientSideUploadError, setClientSideUploadError] = useState<
+    string | null
+  >(null);
+  const [optimisticAvatarUrl, setOptimisticAvatarUrl] = useState<string | null>(
+    null,
+  );
+
+  const { data: imageUrl } = useQuery({
+    queryKey: ["image-url", user?.image],
+    queryFn: async () => {
+      if (!user?.image) {
+        return null;
+      }
+
+      const url = new URL(`${process.env.NEXT_PUBLIC_API_URL}/user/get-avatar`);
+      url.searchParams.append("key", user.image);
+
+      const presignedUrlResponse = await fetch(url.toString(), {
+        method: "GET",
+        headers: { "Content-Type": "application/json" },
+        credentials: "include",
+      });
+
+      if (!presignedUrlResponse.ok) {
+        throw new Error(
+          `Failed to fetch avatar URL: ${presignedUrlResponse.status}`,
+        );
+      }
+
+      const presignedUrlData = await presignedUrlResponse.json();
+      return presignedUrlData.data || null;
+    },
+    enabled: !!user && !!user.image,
+    staleTime: 5 * 60 * 1000,
+    retry: 2,
+  });
+
+  const uploadAvatarMutation = useMutation({
+    mutationFn: async (file: File) => {
+      setClientSideUploadError(null);
+      const fileName = encodeURIComponent(file.name);
+      const type = file.type;
+
+      const presignedUrlResponse = await fetch(
+        `${process.env.NEXT_PUBLIC_API_URL}/user/upload-avatar`,
+        {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          credentials: "include",
+          body: JSON.stringify({ fileName, type }),
+        },
+      );
+
+      if (!presignedUrlResponse.ok) {
+        const errorData = await presignedUrlResponse.json().catch(() => ({}));
+        throw new Error(errorData.message || "Upload failed.");
+      }
+      const presignedUrlData = await presignedUrlResponse.json();
+      const { url, key } = presignedUrlData.data;
+
+      const uploadResponse = await fetch(url, {
+        method: "PUT",
+        headers: { "Content-Type": type },
+        body: file,
+      });
+
+      if (!uploadResponse.ok) {
+        throw new Error("File upload to storage failed.");
+      }
+
+      const updateUserResponse = await auth.updateUser({ image: key });
+      if (updateUserResponse.error) {
+        throw new Error(
+          updateUserResponse.error.message || "Failed to update user profile.",
+        );
+      }
+      return updateUserResponse.data;
+    },
+    onMutate: async (file: File) => {
+      const localUrl = URL.createObjectURL(file);
+      setOptimisticAvatarUrl(localUrl);
+      return { localUrl };
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ["currentSession"] });
+    },
+    onError: (
+      _error: Error,
+      _variables,
+      _context?: { localUrl?: string },
+    ) => {},
+    onSettled: (_data, _error, _variables, context?: { localUrl?: string }) => {
+      if (context?.localUrl) {
+        URL.revokeObjectURL(context.localUrl);
+      }
+      if (_error) {
+        setOptimisticAvatarUrl(null);
+      }
+    },
+  });
+
+  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
+    const file = event.target.files?.[0];
+    if (file) {
+      if (file.size > 5 * 1024 * 1024) {
+        setClientSideUploadError("File is too large (max 5MB).");
+        uploadAvatarMutation.reset();
+        return;
+      }
+      if (
+        !["image/jpeg", "image/png", "image/webp", "image/jpg"].includes(
+          file.type,
+        )
+      ) {
+        setClientSideUploadError(
+          "Invalid file type. Please use JPG, PNG or WEBP.",
+        );
+        uploadAvatarMutation.reset();
+        return;
+      }
+      setClientSideUploadError(null);
+      uploadAvatarMutation.mutate(file);
+    }
+    if (event.target) {
+      event.target.value = "";
+    }
+  };
+
+  if (error) {
+    return (
+      <div className="w-full mt-12 flex flex-col h-full items-center justify-center">
+        <Banner variant="error">
+          We could not load your profile information.
+        </Banner>
+      </div>
+    );
+  }
+
+  const imageSrc = optimisticAvatarUrl || imageUrl || "";
+  console.log(imageUrl);
path: apps/frontend/src/components/settings/ProfileTab.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove debug console.log statement.**

Debug logging should be removed from production code.

```diff
   const imageSrc = optimisticAvatarUrl || imageUrl || "";
-  console.log(imageUrl);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  const imageSrc = optimisticAvatarUrl || imageUrl || "";
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/frontend/src/components/settings/ProfileTab.tsx at line 165, remove the
console.log statement used for debugging the imageUrl variable to clean up the
production code and avoid unnecessary logging.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T02:33:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-22T20:07:35Z
body: [vc]: #pu9m5s9NWYh7ChLNpiVNbPNemTbZT+9aijSjxnsD0mg=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJmbGFpcmEtZnJvbnRlbmQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZmxhaXJhLWZyb250ZW5kLWdpdC1kZXZlbG9wLWJydW5vLWFzZWZmcy1wcm9qZWN0cy52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9icnVuby1hc2VmZnMtcHJvamVjdHMvZmxhaXJhLWZyb250ZW5kLzNyU0JXbXlvalVrWGFzS3dBRWM1OGZ2QnZyU04iLCJwcmV2aWV3VXJsIjoiZmxhaXJhLWZyb250ZW5kLWdpdC1kZXZlbG9wLWJydW5vLWFzZWZmcy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvZnJvbnRlbmQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **flaira-frontend** | ✅ Ready ([Inspect](https://vercel.com/bruno-aseffs-projects/flaira-frontend/3rSBWmyojUkXasKwAEc58fvBvrSN)) | [Visit Preview](https://flaira-frontend-git-develop-bruno-aseffs-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/flaira-frontend-git-develop-bruno-aseffs-projects.vercel.app?via=pr-comment-feedback-link) | May 23, 2025 2:30am |


author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-22T20:07:37Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes integrate AWS S3 support into the backend by adding environment variables, dependencies, and validation for AWS credentials. New utilities for S3 operations and user avatar management are introduced. The backend app registers a new user route with controllers and services handling avatar upload, retrieval, and deletion via pre-signed S3 URLs. CI/CD workflows are updated to provide AWS secrets during testing and migrations. Frontend components are enhanced to fetch and upload avatars using the new backend APIs, including a new profile tab component and updates to user avatar display.

## Changes

| File(s)                                                                                     | Change Summary                                                                                                                          |
|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
| .github/workflows/ci-cd.yml                                                                 | Injected AWS-related environment variables (`S3_BUCKET`, `AWS_REGION`, `AWS_SECRET_ACCESS_KEY`, `AWS_ACCESS_KEY_ID`) into backend jobs. |
| apps/backend/.env.example                                                                   | Added example placeholders for AWS S3 configuration environment variables.                                                             |
| apps/backend/package.json                                                                   | Added dependencies: `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner`, `@hono/zod-validator`, `uuid`, and `@types/uuid`.            |
| apps/backend/src/env.ts                                                                     | Extended environment schema with required AWS S3 variables: `S3_BUCKET`, `AWS_REGION`, `AWS_SECRET_ACCESS_KEY`, `AWS_ACCESS_KEY_ID`.     |
| apps/backend/src/app.ts                                                                     | Added typed context for user and session; registered new `/user` route; replaced `/auth/**` with `/auth/*` route handler; added session middleware. |
| apps/backend/src/modules/user/controller.ts                                                 | Added `uploadAvatar`, `getAvatar`, and `deleteAvatar` controllers with authentication checks and error handling for avatar operations.  |
| apps/backend/src/modules/user/route.ts                                                      | Created user router with endpoints: POST `/upload-avatar` (with Zod validation), GET `/get-avatar`, DELETE `/delete-avatar`.            |
| apps/backend/src/modules/user/service.ts                                                    | Added services: `uploadUserAvatar`, `getUserAvatar`, and `deleteUserAvatar` to manage avatar files using S3 pre-signed URLs.            |
| apps/backend/src/modules/user/validator.ts                                                  | Added `uploadAvatarSchema` using Zod to validate avatar upload request body.                                                            |
| apps/backend/src/utils/s3.ts                                                                | Introduced S3 client and functions to generate pre-signed upload and download URLs, and to delete objects from S3 bucket.               |
| apps/backend/src/utils/checkUser.ts                                                        | Added utility function `checkUser` to verify presence of authenticated user and session in context.                                      |
| apps/backend/src/utils/middleware.ts                                                       | Added authentication middleware to verify user presence and return 401 Unauthorized if absent.                                          |
| apps/frontend/src/components/settings/ProfileTab.tsx                                       | Added new `ProfileTab` React component for displaying and uploading user avatars with client-side validation and optimistic UI updates. |
| apps/frontend/src/components/settings/SecurityTab.tsx                                     | Removed invalidation of "currentSession" query after session revocation.                                                                 |
| apps/frontend/src/components/settings/SettingsDialog.tsx                                  | Replaced manual session query with `auth.useSession()` hook; integrated `ProfileTab` component in "Profile" tab; updated session props. |
| apps/frontend/src/components/sidebar/UserButton.tsx                                       | Updated avatar image fetching to use presigned URL via react-query instead of direct image URL from session.                             |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant App
    participant UserRoute
    participant UserController
    participant UserService
    participant S3Util
    participant AWS_S3

    Client->>App: POST /user/upload-avatar (fileName, type)
    App->>UserRoute: Route request
    UserRoute->>UserController: uploadAvatar(context)
    UserController->>UserService: uploadUserAvatar({fileName, type})
    UserService->>S3Util: uploadUrl({key, type})
    S3Util->>AWS_S3: Generate pre-signed upload URL
    AWS_S3-->>S3Util: Return pre-signed URL
    S3Util-->>UserService: Return URL
    UserService-->>UserController: Return URL
    UserController-->>Client: Respond with pre-signed URL
```

## Possibly related PRs

- BrunoAseff/flaira#84: Modifies `apps/frontend/src/components/settings/SecurityTab.tsx` with session revocation improvements; related by touching the same component but addressing different aspects of session management.

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=BrunoAseff/flaira&utm_content=96):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR