From eaa222e7f6bb725ca8ca010efdca3eaf1b36a63a Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Wed, 5 Mar 2025 19:45:24 -0500 Subject: [PATCH 01/16] backend support for avatar upload --- backend/src/chat/chat.module.ts | 2 + backend/src/user/dto/upload-avatar.input.ts | 8 ++++ backend/src/user/user.model.ts | 4 ++ backend/src/user/user.module.ts | 2 + backend/src/user/user.resolver.ts | 47 +++++++++++++++++++++ backend/src/user/user.service.ts | 30 +++++++++++++ 6 files changed, 93 insertions(+) create mode 100644 backend/src/user/dto/upload-avatar.input.ts diff --git a/backend/src/chat/chat.module.ts b/backend/src/chat/chat.module.ts index 00df9643..00f3a53a 100644 --- a/backend/src/chat/chat.module.ts +++ b/backend/src/chat/chat.module.ts @@ -10,12 +10,14 @@ import { AuthModule } from '../auth/auth.module'; import { UserService } from 'src/user/user.service'; import { PubSub } from 'graphql-subscriptions'; import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module'; +import { UploadModule } from 'src/upload/upload.module'; @Module({ imports: [ TypeOrmModule.forFeature([Chat, User, Message]), AuthModule, JwtCacheModule, + UploadModule, ], providers: [ ChatResolver, diff --git a/backend/src/user/dto/upload-avatar.input.ts b/backend/src/user/dto/upload-avatar.input.ts new file mode 100644 index 00000000..1cf18ba9 --- /dev/null +++ b/backend/src/user/dto/upload-avatar.input.ts @@ -0,0 +1,8 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { FileUpload, GraphQLUpload } from 'graphql-upload-minimal'; + +@InputType() +export class UploadAvatarInput { + @Field(() => GraphQLUpload) + file: Promise; +} diff --git a/backend/src/user/user.model.ts b/backend/src/user/user.model.ts index 5cd35dfd..f4822201 100644 --- a/backend/src/user/user.model.ts +++ b/backend/src/user/user.model.ts @@ -28,6 +28,10 @@ export class User extends SystemBaseModel { @Column() password: string; + @Field() + @Column({ nullable: true }) + avartarUrl?: string; + @Field() @Column({ unique: true }) @IsEmail() diff --git a/backend/src/user/user.module.ts b/backend/src/user/user.module.ts index 9f01e50e..20f3b5c0 100644 --- a/backend/src/user/user.module.ts +++ b/backend/src/user/user.module.ts @@ -7,6 +7,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtModule } from '@nestjs/jwt'; import { AuthModule } from 'src/auth/auth.module'; import { MailModule } from 'src/mail/mail.module'; +import { UploadModule } from 'src/upload/upload.module'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { MailModule } from 'src/mail/mail.module'; JwtModule, AuthModule, MailModule, + UploadModule, ], providers: [UserResolver, UserService, DateScalar], exports: [UserService], diff --git a/backend/src/user/user.resolver.ts b/backend/src/user/user.resolver.ts index 37617be9..17843a70 100644 --- a/backend/src/user/user.resolver.ts +++ b/backend/src/user/user.resolver.ts @@ -18,6 +18,7 @@ import { import { Logger } from '@nestjs/common'; import { EmailConfirmationResponse } from 'src/auth/auth.resolver'; import { ResendEmailInput } from './dto/resend-email.input'; +import { FileUpload, GraphQLUpload } from 'graphql-upload-minimal'; @ObjectType() class LoginResponse { @@ -28,6 +29,15 @@ class LoginResponse { refreshToken: string; } +@ObjectType() +class AvatarUploadResponse { + @Field() + success: boolean; + + @Field() + avatarUrl: string; +} + @Resolver(() => User) export class UserResolver { constructor( @@ -73,4 +83,41 @@ export class UserResolver { Logger.log('me id:', id); return this.userService.getUser(id); } + + /** + * Upload a new avatar for the authenticated user + * Uses validateAndBufferFile to ensure the image meets requirements + */ + @Mutation(() => AvatarUploadResponse) + async uploadAvatar( + @GetUserIdFromToken() userId: string, + @Args('file', { type: () => GraphQLUpload }) file: Promise, + ): Promise { + try { + const updatedUser = await this.userService.updateAvatar(userId, file); + return { + success: true, + avatarUrl: updatedUser.avartarUrl, + }; + } catch (error) { + // Log the error + Logger.error( + `Avatar upload failed: ${error.message}`, + error.stack, + 'UserResolver', + ); + + // Rethrow the exception to be handled by the GraphQL error handler + throw error; + } + } + + /** + * Get the avatar URL for a user + */ + @Query(() => String, { nullable: true }) + async getUserAvatar(@Args('userId') userId: string): Promise { + const user = await this.userService.getUser(userId); + return user ? user.avartarUrl : null; + } } diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index d067a058..e3c484e9 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -2,12 +2,16 @@ import { Injectable } from '@nestjs/common'; import { User } from './user.model'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; +import { FileUpload } from 'graphql-upload-minimal'; +import { UploadService } from '../upload/upload.service'; +import { validateAndBufferFile } from 'src/common/security/file_check'; @Injectable() export class UserService { constructor( @InjectRepository(User) private userRepository: Repository, + private readonly uploadService: UploadService, ) {} // Method to get all chats of a user @@ -25,9 +29,35 @@ export class UserService { return user; } + async getUser(id: string): Promise | null { return await this.userRepository.findOneBy({ id, }); } + + /** + * Updates the user's avatar + * @param userId User ID + * @param file File upload + * @returns Updated user object + */ + async updateAvatar(userId: string, file: Promise): Promise { + // Get the user + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + throw new Error('User not found'); + } + + // Validate and convert file to buffer + const uploadedFile = await file; + const { buffer, mimetype } = await validateAndBufferFile(uploadedFile); + + // Upload the validated buffer to storage + const result = await this.uploadService.upload(buffer, mimetype, 'avatars'); + + // Update the user's avatar URL + user.avartarUrl = result.url; + return this.userRepository.save(user); + } } From 12ec7d175d9d53955f8c2cd6ecfebb9df7191ec4 Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Wed, 5 Mar 2025 19:45:45 -0500 Subject: [PATCH 02/16] add windows support --- frontend/src/app/api/runProject/route.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/api/runProject/route.ts b/frontend/src/app/api/runProject/route.ts index 73b44b67..97ec700b 100644 --- a/frontend/src/app/api/runProject/route.ts +++ b/frontend/src/app/api/runProject/route.ts @@ -8,6 +8,9 @@ import { useMutation } from '@apollo/client/react/hooks/useMutation'; import { toast } from 'sonner'; import { UPDATE_PROJECT_PHOTO_URL } from '@/graphql/request'; import { TLS } from '@/utils/const'; +import os from 'os'; + +const isWindows = os.platform() === 'win32'; const runningContainers = new Map< string, @@ -81,10 +84,9 @@ async function checkExistingContainer( async function removeNodeModulesAndLockFiles(directory: string) { return new Promise((resolve, reject) => { // Linux/macOS command. On Windows, you might need a different approach. - const removeCmd = `rm -rf "${path.join(directory, 'node_modules')}" \ - "${path.join(directory, 'yarn.lock')}" \ - "${path.join(directory, 'package-lock.json')}" \ - "${path.join(directory, 'pnpm-lock.yaml')}"`; + const removeCmd = isWindows + ? `rd /s /q "${path.join(directory, 'node_modules')}" && del /f /q "${path.join(directory, 'yarn.lock')}" "${path.join(directory, 'package-lock.json')}" "${path.join(directory, 'pnpm-lock.yaml')}"` + : `rm -rf "${path.join(directory, 'node_modules')}" "${path.join(directory, 'yarn.lock')}" "${path.join(directory, 'package-lock.json')}" "${path.join(directory, 'pnpm-lock.yaml')}"`; console.log(`Cleaning up node_modules and lock files in: ${directory}`); exec(removeCmd, (err, stdout, stderr) => { From ba2832be4ec8a4d486f614dc4b112e166295b6ef Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Wed, 5 Mar 2025 19:46:21 -0500 Subject: [PATCH 03/16] fix user setting not pop up when at root --- frontend/src/components/user-settings.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/user-settings.tsx b/frontend/src/components/user-settings.tsx index c0b3169f..ecf6b481 100644 --- a/frontend/src/components/user-settings.tsx +++ b/frontend/src/components/user-settings.tsx @@ -16,7 +16,7 @@ import { import { GearIcon } from '@radix-ui/react-icons'; import { Button } from '@/components/ui/button'; import { useRouter } from 'next/navigation'; -import { useMemo, useState, memo } from 'react'; +import { useMemo, useState, memo, useEffect } from 'react'; import { EventEnum } from '../const/EventEnum'; import { useAuthContext } from '@/providers/AuthProvider'; @@ -47,6 +47,17 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => { return user?.username || 'Anonymous'; }, [isLoading, user?.username]); + const handleSettingsClick = () => { + // First navigate using Next.js router + router.push('/chat?id=setting'); + + // Then dispatch the event + setTimeout(() => { + const event = new Event(EventEnum.SETTING); + window.dispatchEvent(event); + }, 0); + }; + const avatarButton = useMemo(() => { return ( + + ); +}; diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index d0a4f770..25990f10 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -250,3 +250,20 @@ export const GET_SUBSCRIBED_PROJECTS = gql` } } `; + +// mutation to upload a user avatar +export const UPLOAD_AVATAR = gql` + mutation UploadAvatar($file: Upload!) { + uploadAvatar(file: $file) { + success + avatarUrl + } + } +`; + +//query to get user avatar +export const GET_USER_AVATAR = gql` + query GetUserAvatar($userId: String!) { + getUserAvatar(userId: $userId) + } +`; From 55fb4453269b04c790dc26bd00ca45930a1ae6f7 Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Wed, 5 Mar 2025 21:08:10 -0500 Subject: [PATCH 05/16] update for user avatar --- backend/src/user/user.model.ts | 2 +- backend/src/user/user.resolver.ts | 4 +- backend/src/user/user.service.ts | 2 +- .../src/components/edit-username-form.tsx | 37 +++++++++++++------ frontend/src/components/user-settings.tsx | 5 ++- frontend/src/graphql/request.ts | 1 + 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/backend/src/user/user.model.ts b/backend/src/user/user.model.ts index f4822201..d458dc9a 100644 --- a/backend/src/user/user.model.ts +++ b/backend/src/user/user.model.ts @@ -30,7 +30,7 @@ export class User extends SystemBaseModel { @Field() @Column({ nullable: true }) - avartarUrl?: string; + avatarUrl?: string; @Field() @Column({ unique: true }) diff --git a/backend/src/user/user.resolver.ts b/backend/src/user/user.resolver.ts index 17843a70..22a52ded 100644 --- a/backend/src/user/user.resolver.ts +++ b/backend/src/user/user.resolver.ts @@ -97,7 +97,7 @@ export class UserResolver { const updatedUser = await this.userService.updateAvatar(userId, file); return { success: true, - avatarUrl: updatedUser.avartarUrl, + avatarUrl: updatedUser.avatarUrl, }; } catch (error) { // Log the error @@ -118,6 +118,6 @@ export class UserResolver { @Query(() => String, { nullable: true }) async getUserAvatar(@Args('userId') userId: string): Promise { const user = await this.userService.getUser(userId); - return user ? user.avartarUrl : null; + return user ? user.avatarUrl : null; } } diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index e3c484e9..e4476b2e 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -57,7 +57,7 @@ export class UserService { const result = await this.uploadService.upload(buffer, mimetype, 'avatars'); // Update the user's avatar URL - user.avartarUrl = result.url; + user.avatarUrl = result.url; return this.userRepository.save(user); } } diff --git a/frontend/src/components/edit-username-form.tsx b/frontend/src/components/edit-username-form.tsx index 128a06dd..1eacd450 100644 --- a/frontend/src/components/edit-username-form.tsx +++ b/frontend/src/components/edit-username-form.tsx @@ -15,10 +15,11 @@ import { zodResolver } from '@hookform/resolvers/zod'; import React, { useEffect, useMemo, useState } from 'react'; import { ModeToggle } from './mode-toggle'; import { toast } from 'sonner'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import Image from 'next/image'; import { ActivityCalendar } from 'react-activity-calendar'; import { TeamSelector } from './team-selector'; +import { useQuery } from '@apollo/client'; +import { AvatarUploader } from './avatar-uploader'; +import { useAuthContext } from '@/providers/AuthProvider'; const data = [ { @@ -56,6 +57,8 @@ const formSchema = z.object({ export default function EditUsernameForm() { const [name, setName] = useState(''); + const { user, isLoading } = useAuthContext(); + const [avatarUrl, setAvatarUrl] = useState(''); const avatarFallback = useMemo(() => { if (!name) return 'US'; @@ -63,8 +66,11 @@ export default function EditUsernameForm() { }, [name]); useEffect(() => { - setName(localStorage.getItem('ollama_user') || 'Anonymous'); - }, []); + if (user) { + setName(user.username || 'Anonymous'); + setAvatarUrl(user.avatarUrl || ''); + } + }, [user]); const form = useForm>({ resolver: zodResolver(formSchema), @@ -84,22 +90,29 @@ export default function EditUsernameForm() { form.setValue('username', e.currentTarget.value); setName(e.currentTarget.value); }; + + const handleAvatarChange = (newUrl: string) => { + setAvatarUrl(newUrl); + }; + return ( -
-

User Settings

+
+

User Settings

{/* Profile Picture Section */} -
+

Profile Picture

-
+

You look good today!

- - - {avatarFallback} - + +
diff --git a/frontend/src/components/user-settings.tsx b/frontend/src/components/user-settings.tsx index ecf6b481..7a753db9 100644 --- a/frontend/src/components/user-settings.tsx +++ b/frontend/src/components/user-settings.tsx @@ -68,13 +68,14 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => { }`} > - + {/* Use empty string fallback instead of undefined to avoid React warnings */} + {avatarFallback} {!isSimple && {displayUsername}} ); - }, [avatarFallback, displayUsername, isSimple]); + }, [avatarFallback, displayUsername, isSimple, user?.avatarUrl]); return ( diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index 25990f10..e794e7dd 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -101,6 +101,7 @@ export const GET_USER_INFO = gql` me { username email + avatarUrl } } `; From fa5c56b3541eb82efb743765386cc2e241630e3c Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Wed, 5 Mar 2025 21:08:25 -0500 Subject: [PATCH 06/16] add avatar --- frontend/src/graphql/type.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/graphql/type.tsx b/frontend/src/graphql/type.tsx index eaa9c3b3..9aa2ba4f 100644 --- a/frontend/src/graphql/type.tsx +++ b/frontend/src/graphql/type.tsx @@ -364,6 +364,7 @@ export type UpdateProjectPhotoInput = { }; export type User = { + avatarUrl: string; __typename: 'User'; chats: Array; createdAt: Scalars['Date']['output']; From bbb1fb0666681b67c885ee97968e48548e4ef717 Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Wed, 5 Mar 2025 22:17:35 -0500 Subject: [PATCH 07/16] fix avatar is not render after up0load --- frontend/src/components/avatar-uploader.tsx | 5 +++++ frontend/src/components/user-settings.tsx | 6 +++++- frontend/src/providers/AuthProvider.tsx | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/avatar-uploader.tsx b/frontend/src/components/avatar-uploader.tsx index 1a2ecf99..0d400c0a 100644 --- a/frontend/src/components/avatar-uploader.tsx +++ b/frontend/src/components/avatar-uploader.tsx @@ -6,6 +6,7 @@ import { UPLOAD_AVATAR } from '../graphql/request'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { toast } from 'sonner'; +import { useAuthContext } from '@/providers/AuthProvider'; interface AvatarUploaderProps { currentAvatarUrl: string; @@ -21,6 +22,7 @@ export const AvatarUploader: React.FC = ({ const [uploadAvatar, { loading }] = useMutation(UPLOAD_AVATAR); const fileInputRef = useRef(null); const [previewUrl, setPreviewUrl] = useState(null); + const { refreshUserInfo } = useAuthContext(); const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -60,6 +62,9 @@ export const AvatarUploader: React.FC = ({ if (data?.uploadAvatar?.success) { onAvatarChange(data.uploadAvatar.avatarUrl); toast.success('Avatar updated successfully'); + + // Refresh the user information in the auth context + await refreshUserInfo(); } } catch (error) { console.error('Error uploading avatar:', error); diff --git a/frontend/src/components/user-settings.tsx b/frontend/src/components/user-settings.tsx index 7a753db9..85678e7d 100644 --- a/frontend/src/components/user-settings.tsx +++ b/frontend/src/components/user-settings.tsx @@ -69,7 +69,11 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => { > {/* Use empty string fallback instead of undefined to avoid React warnings */} - + {avatarFallback} {!isSimple && {displayUsername}} diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index f4239347..95130c43 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -23,6 +23,7 @@ interface AuthContextValue { logout: () => void; refreshAccessToken: () => Promise; validateToken: () => Promise; + refreshUserInfo: () => Promise; } const AuthContext = createContext({ @@ -34,6 +35,7 @@ const AuthContext = createContext({ logout: () => {}, refreshAccessToken: async () => {}, validateToken: async () => false, + refreshUserInfo: async () => false, }); export function AuthProvider({ children }: { children: React.ReactNode }) { @@ -82,6 +84,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }, [getUserInfo]); + const refreshUserInfo = useCallback(async () => { + return await fetchUserInfo(); + }, [fetchUserInfo]); + const refreshAccessToken = useCallback(async () => { try { const refreshToken = localStorage.getItem(LocalStore.refreshToken); @@ -187,6 +193,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { logout, refreshAccessToken, validateToken, + refreshUserInfo, }} > {children} From cc0e6b4101dfe3d8665ec72a9567a79319b0671d Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Thu, 6 Mar 2025 23:48:36 -0500 Subject: [PATCH 08/16] Support local media --- codefox-common/src/common-path.ts | 8 +++ frontend/src/app/api/media/[...path]/route.ts | 70 +++++++++++++++++++ frontend/src/components/avatar-uploader.tsx | 33 ++++++++- frontend/src/components/user-settings.tsx | 42 ++++++++++- 4 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 frontend/src/app/api/media/[...path]/route.ts diff --git a/codefox-common/src/common-path.ts b/codefox-common/src/common-path.ts index b81aeaa6..553dbccb 100644 --- a/codefox-common/src/common-path.ts +++ b/codefox-common/src/common-path.ts @@ -44,6 +44,14 @@ export const getModelStatusPath = (): string => { return modelStatusPath; }; +//Media Directory +export const getMediaDir = (): string => + ensureDir(path.join(getRootDir(), 'media')); +export const getMediaPath = (modelName: string): string => + path.join(getModelsDir(), modelName); +export const getMediaAvatarsDir = (): string => + ensureDir(path.join(getMediaDir(), 'avatars')); + // Models Directory export const getModelsDir = (): string => ensureDir(path.join(getRootDir(), 'models')); diff --git a/frontend/src/app/api/media/[...path]/route.ts b/frontend/src/app/api/media/[...path]/route.ts new file mode 100644 index 00000000..a2c1aeb6 --- /dev/null +++ b/frontend/src/app/api/media/[...path]/route.ts @@ -0,0 +1,70 @@ +// For App Router: app/api/media/[...path]/route.ts +import { NextRequest } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getMediaDir } from 'codefox-common'; + +export async function GET( + request: NextRequest, + { params }: { params: { path: string[] } } +) { + try { + // Get the media directory path + const mediaDir = getMediaDir(); + + // Construct the full path to the requested file + const filePath = path.join(mediaDir, ...params.path); + + // Check if the file exists + if (!fs.existsSync(filePath)) { + // Log directory contents for debugging + try { + if (fs.existsSync(mediaDir)) { + const avatarsDir = path.join(mediaDir, 'avatars'); + if (fs.existsSync(avatarsDir)) { + console.log( + 'Avatars directory contents:', + fs.readdirSync(avatarsDir) + ); + } else { + console.log('Avatars directory does not exist'); + } + } else { + console.log('Media directory does not exist'); + } + } catch (err) { + console.error('Error reading directory:', err); + } + + return new Response('File not found', { status: 404 }); + } + + // Read the file + const fileBuffer = fs.readFileSync(filePath); + + // Determine content type based on file extension + const ext = path.extname(filePath).toLowerCase(); + const contentTypeMap: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.webp': 'image/webp', + '.gif': 'image/gif', + }; + + const contentType = contentTypeMap[ext] || 'application/octet-stream'; + + // Return the file with appropriate headers + return new Response(fileBuffer, { + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=31536000', + }, + }); + } catch (error) { + console.error('Error serving media file:', error); + return new Response(`Error serving file: ${error.message}`, { + status: 500, + }); + } +} diff --git a/frontend/src/components/avatar-uploader.tsx b/frontend/src/components/avatar-uploader.tsx index 0d400c0a..455cae6d 100644 --- a/frontend/src/components/avatar-uploader.tsx +++ b/frontend/src/components/avatar-uploader.tsx @@ -8,6 +8,31 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { toast } from 'sonner'; import { useAuthContext } from '@/providers/AuthProvider'; +// Avatar URL normalization helper +function normalizeAvatarUrl(avatarUrl: string | null | undefined): string { + if (!avatarUrl) return ''; + + // Check if it's already an absolute URL (S3 case) + if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) { + return avatarUrl; + } + + // Check if it's a relative media path + if (avatarUrl.startsWith('media/')) { + // Convert to API route path + return `/api/${avatarUrl}`; + } + + // Handle paths that might not have the media/ prefix + if (avatarUrl.includes('avatars/')) { + const parts = avatarUrl.split('avatars/'); + return `/api/media/avatars/${parts[parts.length - 1]}`; + } + + // Return as is for other cases + return avatarUrl; +} + interface AvatarUploaderProps { currentAvatarUrl: string; avatarFallback: string; @@ -60,7 +85,9 @@ export const AvatarUploader: React.FC = ({ }); if (data?.uploadAvatar?.success) { - onAvatarChange(data.uploadAvatar.avatarUrl); + // Store the original URL from backend + const avatarUrl = data.uploadAvatar.avatarUrl; + onAvatarChange(avatarUrl); toast.success('Avatar updated successfully'); // Refresh the user information in the auth context @@ -84,8 +111,8 @@ export const AvatarUploader: React.FC = ({ fileInputRef.current?.click(); }; - // Use preview URL if available, otherwise use the current avatar URL - const displayUrl = previewUrl || currentAvatarUrl; + // Use preview URL if available, otherwise use the normalized current avatar URL + const displayUrl = previewUrl || normalizeAvatarUrl(currentAvatarUrl); return (
diff --git a/frontend/src/components/user-settings.tsx b/frontend/src/components/user-settings.tsx index 85678e7d..4628a262 100644 --- a/frontend/src/components/user-settings.tsx +++ b/frontend/src/components/user-settings.tsx @@ -20,6 +20,31 @@ import { useMemo, useState, memo, useEffect } from 'react'; import { EventEnum } from '../const/EventEnum'; import { useAuthContext } from '@/providers/AuthProvider'; +// Avatar URL normalization helper +function normalizeAvatarUrl(avatarUrl: string | null | undefined): string { + if (!avatarUrl) return ''; + + // Check if it's already an absolute URL (S3 case) + if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) { + return avatarUrl; + } + + // Check if it's a relative media path + if (avatarUrl.startsWith('media/')) { + // Convert to API route path + return `/api/${avatarUrl}`; + } + + // Handle paths that might not have the media/ prefix + if (avatarUrl.includes('avatars/')) { + const parts = avatarUrl.split('avatars/'); + return `/api/media/avatars/${parts[parts.length - 1]}`; + } + + // Return as is for other cases + return avatarUrl; +} + interface UserSettingsProps { isSimple: boolean; } @@ -47,6 +72,11 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => { return user?.username || 'Anonymous'; }, [isLoading, user?.username]); + // Normalize the avatar URL + const normalizedAvatarUrl = useMemo(() => { + return normalizeAvatarUrl(user?.avatarUrl); + }, [user?.avatarUrl]); + const handleSettingsClick = () => { // First navigate using Next.js router router.push('/chat?id=setting'); @@ -68,9 +98,9 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => { }`} > - {/* Use empty string fallback instead of undefined to avoid React warnings */} + {/* Use normalized avatar URL */} @@ -79,7 +109,13 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => { {!isSimple && {displayUsername}} ); - }, [avatarFallback, displayUsername, isSimple, user?.avatarUrl]); + }, [ + avatarFallback, + displayUsername, + isSimple, + normalizedAvatarUrl, + user?.avatarUrl, + ]); return ( From 238d3a4ae9cb2bb0d6a63c168f5ff0e244d26d63 Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Fri, 7 Mar 2025 10:24:01 -0500 Subject: [PATCH 09/16] Fix fetch fail --- backend/src/user/user.model.ts | 2 +- frontend/src/app/api/media/[...path]/route.ts | 72 +++++++++---------- frontend/src/graphql/schema.gql | 8 +++ frontend/src/graphql/type.tsx | 48 ++++++++++++- 4 files changed, 89 insertions(+), 41 deletions(-) diff --git a/backend/src/user/user.model.ts b/backend/src/user/user.model.ts index d458dc9a..1655392e 100644 --- a/backend/src/user/user.model.ts +++ b/backend/src/user/user.model.ts @@ -28,7 +28,7 @@ export class User extends SystemBaseModel { @Column() password: string; - @Field() + @Field({ nullable: true }) @Column({ nullable: true }) avatarUrl?: string; diff --git a/frontend/src/app/api/media/[...path]/route.ts b/frontend/src/app/api/media/[...path]/route.ts index a2c1aeb6..1371dc58 100644 --- a/frontend/src/app/api/media/[...path]/route.ts +++ b/frontend/src/app/api/media/[...path]/route.ts @@ -1,6 +1,5 @@ -// For App Router: app/api/media/[...path]/route.ts import { NextRequest } from 'next/server'; -import fs from 'fs'; +import fs from 'fs/promises'; // Use promises API import path from 'path'; import { getMediaDir } from 'codefox-common'; @@ -9,62 +8,57 @@ export async function GET( { params }: { params: { path: string[] } } ) { try { - // Get the media directory path const mediaDir = getMediaDir(); - - // Construct the full path to the requested file const filePath = path.join(mediaDir, ...params.path); + const normalizedPath = path.normalize(filePath); - // Check if the file exists - if (!fs.existsSync(filePath)) { - // Log directory contents for debugging - try { - if (fs.existsSync(mediaDir)) { - const avatarsDir = path.join(mediaDir, 'avatars'); - if (fs.existsSync(avatarsDir)) { - console.log( - 'Avatars directory contents:', - fs.readdirSync(avatarsDir) - ); - } else { - console.log('Avatars directory does not exist'); - } - } else { - console.log('Media directory does not exist'); - } - } catch (err) { - console.error('Error reading directory:', err); - } - - return new Response('File not found', { status: 404 }); + if (!normalizedPath.startsWith(mediaDir)) { + console.error('Possible directory traversal attempt:', filePath); + return new Response('Access denied', { status: 403 }); } - // Read the file - const fileBuffer = fs.readFileSync(filePath); - - // Determine content type based on file extension - const ext = path.extname(filePath).toLowerCase(); + // File extension allowlist const contentTypeMap: Record = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', - '.gif': 'image/gif', }; - const contentType = contentTypeMap[ext] || 'application/octet-stream'; + const ext = path.extname(filePath).toLowerCase(); + if (!contentTypeMap[ext]) { + return new Response('Forbidden file type', { status: 403 }); + } + + // File existence and size check + let fileStat; + try { + fileStat = await fs.stat(filePath); + } catch (err) { + return new Response('File not found', { status: 404 }); + } - // Return the file with appropriate headers + if (fileStat.size > 10 * 1024 * 1024) { + // 10MB limit + return new Response('File too large', { status: 413 }); + } + + // Read and return the file + const fileBuffer = await fs.readFile(filePath); return new Response(fileBuffer, { headers: { - 'Content-Type': contentType, + 'Content-Type': contentTypeMap[ext], + 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'public, max-age=31536000', }, }); } catch (error) { console.error('Error serving media file:', error); - return new Response(`Error serving file: ${error.message}`, { - status: 500, - }); + const errorMessage = + process.env.NODE_ENV === 'development' + ? `Error serving file: ${error.message}` + : 'An error occurred while serving the file'; + + return new Response(errorMessage, { status: 500 }); } } diff --git a/frontend/src/graphql/schema.gql b/frontend/src/graphql/schema.gql index 55d76b75..6c419a01 100644 --- a/frontend/src/graphql/schema.gql +++ b/frontend/src/graphql/schema.gql @@ -2,6 +2,11 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ +type AvatarUploadResponse { + avatarUrl: String! + success: Boolean! +} + type Chat { createdAt: Date! id: ID! @@ -122,6 +127,7 @@ type Mutation { updateChatTitle(updateChatTitleInput: UpdateChatTitleInput!): Chat updateProjectPhoto(input: UpdateProjectPhotoInput!): Project! updateProjectPublicStatus(isPublic: Boolean!, projectId: ID!): Project! + uploadAvatar(file: Upload!): AvatarUploadResponse! } input NewChatInput { @@ -178,6 +184,7 @@ type Query { getProject(projectId: String!): Project! getRemainingProjectLimit: Int! getSubscribedProjects: [Project!]! + getUserAvatar(userId: String!): String getUserChats: [Chat!] getUserProjects: [Project!]! isValidateProject(isValidProject: IsValidProjectInput!): Boolean! @@ -229,6 +236,7 @@ input UpdateProjectPhotoInput { scalar Upload type User { + avatarUrl: String chats: [Chat!]! createdAt: Date! email: String! diff --git a/frontend/src/graphql/type.tsx b/frontend/src/graphql/type.tsx index 9aa2ba4f..3def174b 100644 --- a/frontend/src/graphql/type.tsx +++ b/frontend/src/graphql/type.tsx @@ -40,6 +40,12 @@ export type Scalars = { Upload: { input: any; output: any }; }; +export type AvatarUploadResponse = { + __typename: 'AvatarUploadResponse'; + avatarUrl: Scalars['String']['output']; + success: Scalars['Boolean']['output']; +}; + export type Chat = { __typename: 'Chat'; createdAt: Scalars['Date']['output']; @@ -166,6 +172,7 @@ export type Mutation = { updateChatTitle?: Maybe; updateProjectPhoto: Project; updateProjectPublicStatus: Project; + uploadAvatar: AvatarUploadResponse; }; export type MutationClearChatHistoryArgs = { @@ -237,6 +244,10 @@ export type MutationUpdateProjectPublicStatusArgs = { projectId: Scalars['ID']['input']; }; +export type MutationUploadAvatarArgs = { + file: Scalars['Upload']['input']; +}; + export type NewChatInput = { title?: InputMaybe; }; @@ -293,6 +304,7 @@ export type Query = { getProject: Project; getRemainingProjectLimit: Scalars['Int']['output']; getSubscribedProjects: Array; + getUserAvatar?: Maybe; getUserChats?: Maybe>; getUserProjects: Array; isValidateProject: Scalars['Boolean']['output']; @@ -320,6 +332,10 @@ export type QueryGetProjectArgs = { projectId: Scalars['String']['input']; }; +export type QueryGetUserAvatarArgs = { + userId: Scalars['String']['input']; +}; + export type QueryIsValidateProjectArgs = { isValidProject: IsValidProjectInput; }; @@ -364,8 +380,8 @@ export type UpdateProjectPhotoInput = { }; export type User = { - avatarUrl: string; __typename: 'User'; + avatarUrl?: Maybe; chats: Array; createdAt: Scalars['Date']['output']; email: Scalars['String']['output']; @@ -491,6 +507,7 @@ export type DirectiveResolverFn< /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = ResolversObject<{ + AvatarUploadResponse: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; Chat: ResolverTypeWrapper; ChatCompletionChoiceType: ResolverTypeWrapper; @@ -531,6 +548,7 @@ export type ResolversTypes = ResolversObject<{ /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = ResolversObject<{ + AvatarUploadResponse: AvatarUploadResponse; Boolean: Scalars['Boolean']['output']; Chat: Chat; ChatCompletionChoiceType: ChatCompletionChoiceType; @@ -567,6 +585,16 @@ export type ResolversParentTypes = ResolversObject<{ User: User; }>; +export type AvatarUploadResponseResolvers< + ContextType = any, + ParentType extends + ResolversParentTypes['AvatarUploadResponse'] = ResolversParentTypes['AvatarUploadResponse'], +> = ResolversObject<{ + avatarUrl?: Resolver; + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}>; + export type ChatResolvers< ContextType = any, ParentType extends @@ -807,6 +835,12 @@ export type MutationResolvers< 'isPublic' | 'projectId' > >; + uploadAvatar?: Resolver< + ResolversTypes['AvatarUploadResponse'], + ParentType, + ContextType, + RequireFields + >; }>; export type ProjectResolvers< @@ -923,6 +957,12 @@ export type QueryResolvers< ParentType, ContextType >; + getUserAvatar?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + >; getUserChats?: Resolver< Maybe>, ParentType, @@ -977,6 +1017,11 @@ export type UserResolvers< ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], > = ResolversObject<{ + avatarUrl?: Resolver< + Maybe, + ParentType, + ContextType + >; chats?: Resolver, ParentType, ContextType>; createdAt?: Resolver; email?: Resolver; @@ -1005,6 +1050,7 @@ export type UserResolvers< }>; export type Resolvers = ResolversObject<{ + AvatarUploadResponse?: AvatarUploadResponseResolvers; Chat?: ChatResolvers; ChatCompletionChoiceType?: ChatCompletionChoiceTypeResolvers; ChatCompletionChunkType?: ChatCompletionChunkTypeResolvers; From 39f04c6c856362e9a0703fb8934611db31145d0e Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Fri, 7 Mar 2025 17:38:26 -0500 Subject: [PATCH 10/16] fix some ui problem but still have problem --- frontend/src/components/chat/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/chat/index.tsx b/frontend/src/components/chat/index.tsx index b17f102f..9dee314b 100644 --- a/frontend/src/components/chat/index.tsx +++ b/frontend/src/components/chat/index.tsx @@ -96,7 +96,7 @@ export default function Chat() { // Render the settings view if chatId indicates settings mode if (chatId === EventEnum.SETTING) { return ( -
+
); From ccb923f26f2af8da2b33a3f2f33de5a92faa9556 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Sun, 9 Mar 2025 03:58:58 -0500 Subject: [PATCH 11/16] refactor: rename UserSettings component and update imports --- frontend/src/components/avatar-uploader.tsx | 37 ++++++++++++------- frontend/src/components/chat/index.tsx | 4 +- frontend/src/components/detail-settings.tsx | 4 +- frontend/src/components/file-sidebar.tsx | 2 +- .../settings.tsx} | 12 +++--- frontend/src/components/sidebar.tsx | 2 +- ...ser-settings.tsx => user-settings-bar.tsx} | 6 +++ 7 files changed, 40 insertions(+), 27 deletions(-) rename frontend/src/components/{edit-username-form.tsx => settings/settings.tsx} (93%) rename frontend/src/components/{user-settings.tsx => user-settings-bar.tsx} (95%) diff --git a/frontend/src/components/avatar-uploader.tsx b/frontend/src/components/avatar-uploader.tsx index 455cae6d..e8883197 100644 --- a/frontend/src/components/avatar-uploader.tsx +++ b/frontend/src/components/avatar-uploader.tsx @@ -115,7 +115,11 @@ export const AvatarUploader: React.FC = ({ const displayUrl = previewUrl || normalizeAvatarUrl(currentAvatarUrl); return ( -
+ -
+
+ + + {avatarFallback} + +
+ Upload +
+ {loading && ( +
+
+
+ )} +
+ ); }; diff --git a/frontend/src/components/chat/index.tsx b/frontend/src/components/chat/index.tsx index 9dee314b..ae18b13a 100644 --- a/frontend/src/components/chat/index.tsx +++ b/frontend/src/components/chat/index.tsx @@ -9,7 +9,7 @@ import { GET_CHAT_HISTORY } from '@/graphql/request'; import { useQuery } from '@apollo/client'; import { toast } from 'sonner'; import { EventEnum } from '@/const/EventEnum'; -import EditUsernameForm from '@/components/edit-username-form'; +import UserSetting from '@/components/settings/settings'; import ChatContent from '@/components/chat/chat-panel'; import { useModels } from '@/hooks/useModels'; import { useChatList } from '@/hooks/useChatList'; @@ -97,7 +97,7 @@ export default function Chat() { if (chatId === EventEnum.SETTING) { return (
- +
); } diff --git a/frontend/src/components/detail-settings.tsx b/frontend/src/components/detail-settings.tsx index 6bd461d4..d36ccc29 100644 --- a/frontend/src/components/detail-settings.tsx +++ b/frontend/src/components/detail-settings.tsx @@ -18,7 +18,7 @@ import { import { DownloadIcon, GearIcon } from '@radix-ui/react-icons'; import PullModelForm from './pull-model-form'; -import EditUsernameForm from './edit-username-form'; +import UserSetting from './settings/settings'; export default function DetailSettings() { const [isOpen, setIsOpen] = React.useState(false); @@ -33,7 +33,7 @@ export default function DetailSettings() { Settings - + diff --git a/frontend/src/components/file-sidebar.tsx b/frontend/src/components/file-sidebar.tsx index deefbac5..e0a57e1d 100644 --- a/frontend/src/components/file-sidebar.tsx +++ b/frontend/src/components/file-sidebar.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation'; import { memo, useCallback, useEffect, useState } from 'react'; import { SquarePen } from 'lucide-react'; import SidebarSkeleton from './sidebar-skeleton'; -import UserSettings from './user-settings'; +import UserSettings from './user-settings-bar'; import { SideBarItem } from './sidebar-item'; import { EventEnum } from '../const/EventEnum'; import { diff --git a/frontend/src/components/edit-username-form.tsx b/frontend/src/components/settings/settings.tsx similarity index 93% rename from frontend/src/components/edit-username-form.tsx rename to frontend/src/components/settings/settings.tsx index 1eacd450..68437b8e 100644 --- a/frontend/src/components/edit-username-form.tsx +++ b/frontend/src/components/settings/settings.tsx @@ -13,12 +13,12 @@ import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import React, { useEffect, useMemo, useState } from 'react'; -import { ModeToggle } from './mode-toggle'; +import { ModeToggle } from '../mode-toggle'; import { toast } from 'sonner'; import { ActivityCalendar } from 'react-activity-calendar'; -import { TeamSelector } from './team-selector'; +import { TeamSelector } from '../team-selector'; import { useQuery } from '@apollo/client'; -import { AvatarUploader } from './avatar-uploader'; +import { AvatarUploader } from '../avatar-uploader'; import { useAuthContext } from '@/providers/AuthProvider'; const data = [ @@ -55,7 +55,7 @@ const formSchema = z.object({ }), }); -export default function EditUsernameForm() { +export default function UserSetting() { const [name, setName] = useState(''); const { user, isLoading } = useAuthContext(); const [avatarUrl, setAvatarUrl] = useState(''); @@ -105,9 +105,7 @@ export default function EditUsernameForm() { {/* Profile Picture Section */}

Profile Picture

-
-

You look good today!

- +
{ const { user, isLoading, logout } = useAuthContext(); const [open, setOpen] = useState(false); From 51dfd577669cf0a7ab7aacc874c5fbcec7df8b11 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Sun, 9 Mar 2025 04:13:36 -0500 Subject: [PATCH 12/16] feat: create settings page and refactor UserSettings component to UserSettingsBar --- frontend/src/app/(main)/settings/page.tsx | 6 +++++ frontend/src/components/chat/chat-panel.tsx | 15 ------------- frontend/src/components/chat/index.tsx | 11 ++-------- frontend/src/components/file-sidebar.tsx | 4 ++-- frontend/src/components/sidebar.tsx | 4 ++-- frontend/src/components/user-settings-bar.tsx | 22 +++++++++++-------- 6 files changed, 25 insertions(+), 37 deletions(-) create mode 100644 frontend/src/app/(main)/settings/page.tsx diff --git a/frontend/src/app/(main)/settings/page.tsx b/frontend/src/app/(main)/settings/page.tsx new file mode 100644 index 00000000..b64cb5e1 --- /dev/null +++ b/frontend/src/app/(main)/settings/page.tsx @@ -0,0 +1,6 @@ +import UserSetting from '@/components/settings/settings'; +import { UserSettingsBar } from '@/components/user-settings-bar'; + +export default function Page() { + return ; +} diff --git a/frontend/src/components/chat/chat-panel.tsx b/frontend/src/components/chat/chat-panel.tsx index 6d713513..6753516d 100644 --- a/frontend/src/components/chat/chat-panel.tsx +++ b/frontend/src/components/chat/chat-panel.tsx @@ -36,21 +36,6 @@ export default function ChatContent({ setInput, setMessages, }: ChatProps) { - // TODO(Sma1lboy): on message edit - // onMessageEdit?: (messageId: string, newContent: string) => void; - // const [editingMessageId, setEditingMessageId] = React.useState(null); - // const [editContent, setEditContent] = React.useState(''); - // const handleEditStart = (message: Message) => { - // setEditingMessageId(message.id); - // setEditContent(message.content); - // }; - // const handleEditSubmit = (messageId: string) => { - // if (onMessageEdit) { - // onMessageEdit(messageId, editContent); - // } - // setEditingMessageId(null); - // setEditContent(''); - // }; return (
diff --git a/frontend/src/components/chat/index.tsx b/frontend/src/components/chat/index.tsx index ae18b13a..6e0d717e 100644 --- a/frontend/src/components/chat/index.tsx +++ b/frontend/src/components/chat/index.tsx @@ -18,6 +18,7 @@ import { CodeEngine } from './code-engine/code-engine'; import { useProjectStatusMonitor } from '@/hooks/useProjectStatusMonitor'; import { Loader2 } from 'lucide-react'; import { useAuthContext } from '@/providers/AuthProvider'; +import { useRouter } from 'next/navigation'; export default function Chat() { // Initialize state, refs, and custom hooks @@ -30,6 +31,7 @@ export default function Chat() { const { models } = useModels(); const [selectedModel, setSelectedModel] = useState(models[0] || 'gpt-4o'); const { refetchChats } = useChatList(); + const route = useRouter(); // Project status monitoring for the current chat const { isReady, projectId, projectName, error } = @@ -93,15 +95,6 @@ export default function Chat() { }; }, [updateChatId]); - // Render the settings view if chatId indicates settings mode - if (chatId === EventEnum.SETTING) { - return ( -
- -
- ); - } - // Render the main layout return chatId ? ( - + diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index e4eab19a..5937b1f7 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'; import Image from 'next/image'; import { memo, useCallback, useContext, useState } from 'react'; import SidebarSkeleton from './sidebar-skeleton'; -import UserSettings from './user-settings-bar'; +import UserSettingsBar from './user-settings-bar'; import { SideBarItem } from './sidebar-item'; import { Chat } from '@/graphql/type'; import { EventEnum } from '../const/EventEnum'; @@ -231,7 +231,7 @@ function ChatSideBarComponent({ - + { +export const UserSettingsBar = ({ isSimple }: UserSettingsProps) => { const { user, isLoading, logout } = useAuthContext(); const [open, setOpen] = useState(false); const router = useRouter(); @@ -85,7 +86,7 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => { const handleSettingsClick = () => { // First navigate using Next.js router - router.push('/chat?id=setting'); + router.push('/settings'); // Then dispatch the event setTimeout(() => { @@ -126,7 +127,7 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => { return ( {avatarButton} - + e.preventDefault()}>
{
- - Logout + +
+ + Logout +
); }; -export default memo(UserSettings); +export default memo(UserSettingsBar); From 791fdf0f1e9bbe2cf78a721139352fb6ffd9da07 Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Sun, 9 Mar 2025 17:40:35 -0400 Subject: [PATCH 13/16] Fix avatar not showing when on mac with s3 --- frontend/src/components/avatar-uploader.tsx | 5 ++-- frontend/src/components/user-settings-bar.tsx | 26 +------------------ 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/avatar-uploader.tsx b/frontend/src/components/avatar-uploader.tsx index e8883197..f6c19bd3 100644 --- a/frontend/src/components/avatar-uploader.tsx +++ b/frontend/src/components/avatar-uploader.tsx @@ -9,11 +9,12 @@ import { toast } from 'sonner'; import { useAuthContext } from '@/providers/AuthProvider'; // Avatar URL normalization helper -function normalizeAvatarUrl(avatarUrl: string | null | undefined): string { +export function normalizeAvatarUrl(avatarUrl: string | null | undefined): string { if (!avatarUrl) return ''; + console.log("Avatar URL " + avatarUrl); // Check if it's already an absolute URL (S3 case) - if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) { + if (avatarUrl.startsWith('https:') || avatarUrl.startsWith('http:')) { return avatarUrl; } diff --git a/frontend/src/components/user-settings-bar.tsx b/frontend/src/components/user-settings-bar.tsx index 2adf946e..a72a4bdd 100644 --- a/frontend/src/components/user-settings-bar.tsx +++ b/frontend/src/components/user-settings-bar.tsx @@ -20,31 +20,7 @@ import { useMemo, useState, memo, useEffect } from 'react'; import { EventEnum } from '../const/EventEnum'; import { useAuthContext } from '@/providers/AuthProvider'; import { LogOut } from 'lucide-react'; - -// Avatar URL normalization helper -function normalizeAvatarUrl(avatarUrl: string | null | undefined): string { - if (!avatarUrl) return ''; - - // Check if it's already an absolute URL (S3 case) - if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) { - return avatarUrl; - } - - // Check if it's a relative media path - if (avatarUrl.startsWith('media/')) { - // Convert to API route path - return `/api/${avatarUrl}`; - } - - // Handle paths that might not have the media/ prefix - if (avatarUrl.includes('avatars/')) { - const parts = avatarUrl.split('avatars/'); - return `/api/media/avatars/${parts[parts.length - 1]}`; - } - - // Return as is for other cases - return avatarUrl; -} +import { normalizeAvatarUrl } from './avatar-uploader'; interface UserSettingsProps { isSimple: boolean; From 90f9d529cd6d3c8a6522d5f08744bc8573d68365 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:42:00 +0000 Subject: [PATCH 14/16] [autofix.ci] apply automated fixes --- frontend/src/components/avatar-uploader.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/avatar-uploader.tsx b/frontend/src/components/avatar-uploader.tsx index f6c19bd3..622cbf8f 100644 --- a/frontend/src/components/avatar-uploader.tsx +++ b/frontend/src/components/avatar-uploader.tsx @@ -9,10 +9,12 @@ import { toast } from 'sonner'; import { useAuthContext } from '@/providers/AuthProvider'; // Avatar URL normalization helper -export function normalizeAvatarUrl(avatarUrl: string | null | undefined): string { +export function normalizeAvatarUrl( + avatarUrl: string | null | undefined +): string { if (!avatarUrl) return ''; - console.log("Avatar URL " + avatarUrl); + console.log('Avatar URL ' + avatarUrl); // Check if it's already an absolute URL (S3 case) if (avatarUrl.startsWith('https:') || avatarUrl.startsWith('http:')) { return avatarUrl; From 4c3501452127b11dc7bf304f2cf57b9c37ca175b Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Sun, 9 Mar 2025 17:42:43 -0400 Subject: [PATCH 15/16] delete log --- frontend/src/components/avatar-uploader.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/avatar-uploader.tsx b/frontend/src/components/avatar-uploader.tsx index f6c19bd3..c877210a 100644 --- a/frontend/src/components/avatar-uploader.tsx +++ b/frontend/src/components/avatar-uploader.tsx @@ -12,7 +12,6 @@ import { useAuthContext } from '@/providers/AuthProvider'; export function normalizeAvatarUrl(avatarUrl: string | null | undefined): string { if (!avatarUrl) return ''; - console.log("Avatar URL " + avatarUrl); // Check if it's already an absolute URL (S3 case) if (avatarUrl.startsWith('https:') || avatarUrl.startsWith('http:')) { return avatarUrl; From 20b460d79065a721a73cb3972b08849d1a208004 Mon Sep 17 00:00:00 2001 From: ZHallen122 Date: Sun, 9 Mar 2025 17:45:46 -0400 Subject: [PATCH 16/16] fix error --- frontend/src/components/avatar-uploader.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/components/avatar-uploader.tsx b/frontend/src/components/avatar-uploader.tsx index 59e29917..4fcdccf2 100644 --- a/frontend/src/components/avatar-uploader.tsx +++ b/frontend/src/components/avatar-uploader.tsx @@ -14,10 +14,6 @@ export function normalizeAvatarUrl( ): string { if (!avatarUrl) return ''; -<<<<<<< HEAD -======= - console.log('Avatar URL ' + avatarUrl); ->>>>>>> 90f9d529cd6d3c8a6522d5f08744bc8573d68365 // Check if it's already an absolute URL (S3 case) if (avatarUrl.startsWith('https:') || avatarUrl.startsWith('http:')) { return avatarUrl;