From aeb57f325a6b7f2a7d14fb4b614e0d408689557b Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sat, 3 Jan 2026 20:21:29 -0300 Subject: [PATCH] Fix profile updates, avatar upload, and settings --- .../internal/api/handlers/core_handlers.go | 45 +++++++- .../internal/api/handlers/storage_handler.go | 11 +- backend/internal/core/domain/entity/user.go | 2 + backend/internal/core/dto/user_auth.go | 9 ++ backend/internal/core/usecases/auth/login.go | 3 + .../core/usecases/user/update_password.go | 55 +++++++++ .../core/usecases/user/update_user.go | 8 ++ backend/internal/dto/auth.go | 3 + .../persistence/postgres/user_repository.go | 94 +++++++++++++-- backend/internal/router/router.go | 3 + backend/internal/services/admin_service.go | 26 ++++- backend/internal/services/storage_service.go | 35 ++++-- frontend/src/app/dashboard/profile/page.tsx | 107 +++++++++++++++++- frontend/src/app/dashboard/settings/page.tsx | 22 +++- frontend/src/components/dashboard-header.tsx | 6 +- frontend/src/lib/api.ts | 14 ++- frontend/src/lib/auth.ts | 3 + frontend/src/lib/types.ts | 1 + 18 files changed, 402 insertions(+), 45 deletions(-) create mode 100644 backend/internal/core/usecases/user/update_password.go diff --git a/backend/internal/api/handlers/core_handlers.go b/backend/internal/api/handlers/core_handlers.go index 881f2a6..048772c 100644 --- a/backend/internal/api/handlers/core_handlers.go +++ b/backend/internal/api/handlers/core_handlers.go @@ -25,6 +25,7 @@ type CoreHandlers struct { listUsersUC *user.ListUsersUseCase deleteUserUC *user.DeleteUserUseCase updateUserUC *user.UpdateUserUseCase + updatePasswordUC *user.UpdatePasswordUseCase listCompaniesUC *tenant.ListCompaniesUseCase auditService *services.AuditService notificationService *services.NotificationService @@ -33,7 +34,7 @@ type CoreHandlers struct { credentialsService *services.CredentialsService } -func NewCoreHandlers(l *auth.LoginUseCase, reg *auth.RegisterCandidateUseCase, c *tenant.CreateCompanyUseCase, u *user.CreateUserUseCase, list *user.ListUsersUseCase, del *user.DeleteUserUseCase, upd *user.UpdateUserUseCase, lc *tenant.ListCompaniesUseCase, auditService *services.AuditService, notificationService *services.NotificationService, ticketService *services.TicketService, adminService *services.AdminService, credentialsService *services.CredentialsService) *CoreHandlers { +func NewCoreHandlers(l *auth.LoginUseCase, reg *auth.RegisterCandidateUseCase, c *tenant.CreateCompanyUseCase, u *user.CreateUserUseCase, list *user.ListUsersUseCase, del *user.DeleteUserUseCase, upd *user.UpdateUserUseCase, updatePasswordUC *user.UpdatePasswordUseCase, lc *tenant.ListCompaniesUseCase, auditService *services.AuditService, notificationService *services.NotificationService, ticketService *services.TicketService, adminService *services.AdminService, credentialsService *services.CredentialsService) *CoreHandlers { return &CoreHandlers{ loginUC: l, registerCandidateUC: reg, @@ -42,6 +43,7 @@ func NewCoreHandlers(l *auth.LoginUseCase, reg *auth.RegisterCandidateUseCase, c listUsersUC: list, deleteUserUC: del, updateUserUC: upd, + updatePasswordUC: updatePasswordUC, listCompaniesUC: lc, auditService: auditService, notificationService: notificationService, @@ -1024,6 +1026,47 @@ func (h *CoreHandlers) UpdateMyProfile(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(resp) } +// UpdateMyPassword updates the authenticated user's password. +// @Summary Update My Password +// @Description Updates the current user's password. +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param password body object true "Password Details" +// @Success 204 {string} string "No Content" +// @Failure 400 {string} string "Invalid Request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal Server Error" +// @Router /api/v1/users/me/password [patch] +func (h *CoreHandlers) UpdateMyPassword(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + userIDVal := ctx.Value(middleware.ContextUserID) + userID, ok := userIDVal.(string) + if !ok || userID == "" { + http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized) + return + } + + var req dto.UpdatePasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid Request", http.StatusBadRequest) + return + } + + if req.CurrentPassword == "" || req.NewPassword == "" { + http.Error(w, "Current and new password are required", http.StatusBadRequest) + return + } + + if err := h.updatePasswordUC.Execute(ctx, userID, req.CurrentPassword, req.NewPassword); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} + // UploadMyAvatar handles profile picture upload. // @Summary Upload Avatar // @Description Uploads a profile picture for the current user. diff --git a/backend/internal/api/handlers/storage_handler.go b/backend/internal/api/handlers/storage_handler.go index 7cef46d..46a6128 100644 --- a/backend/internal/api/handlers/storage_handler.go +++ b/backend/internal/api/handlers/storage_handler.go @@ -72,10 +72,17 @@ func (h *StorageHandler) GetUploadURL(w http.ResponseWriter, r *http.Request) { return } + publicURL, err := h.storageService.GetPublicURL(r.Context(), key) + if err != nil { + http.Error(w, "Failed to generate public URL: "+err.Error(), http.StatusInternalServerError) + return + } + // Return simple JSON resp := map[string]string{ - "url": url, - "key": key, // Client needs key to save to DB profile + "url": url, + "key": key, // Client needs key to save to DB profile + "publicUrl": publicURL, // Public URL for immediate use } w.Header().Set("Content-Type", "application/json") diff --git a/backend/internal/core/domain/entity/user.go b/backend/internal/core/domain/entity/user.go index d60320b..3d533b2 100644 --- a/backend/internal/core/domain/entity/user.go +++ b/backend/internal/core/domain/entity/user.go @@ -27,6 +27,8 @@ type User struct { TenantID string `json:"tenant_id"` // Link to Company Name string `json:"name"` Email string `json:"email"` + Phone *string `json:"phone,omitempty"` + Bio *string `json:"bio,omitempty"` PasswordHash string `json:"-"` AvatarUrl string `json:"avatar_url"` Roles []Role `json:"roles"` diff --git a/backend/internal/core/dto/user_auth.go b/backend/internal/core/dto/user_auth.go index 12a00e5..ee62184 100644 --- a/backend/internal/core/dto/user_auth.go +++ b/backend/internal/core/dto/user_auth.go @@ -25,6 +25,8 @@ type CreateUserRequest struct { type UpdateUserRequest struct { Name *string `json:"name,omitempty"` Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` + Bio *string `json:"bio,omitempty"` Active *bool `json:"active,omitempty"` Status *string `json:"status,omitempty"` Roles *[]string `json:"roles,omitempty"` @@ -38,9 +40,16 @@ type UserResponse struct { Roles []string `json:"roles"` Status string `json:"status"` AvatarUrl string `json:"avatar_url"` + Phone *string `json:"phone,omitempty"` + Bio *string `json:"bio,omitempty"` CreatedAt time.Time `json:"created_at"` } +type UpdatePasswordRequest struct { + CurrentPassword string `json:"currentPassword"` + NewPassword string `json:"newPassword"` +} + type RegisterCandidateRequest struct { Name string `json:"name"` Email string `json:"email"` diff --git a/backend/internal/core/usecases/auth/login.go b/backend/internal/core/usecases/auth/login.go index 3e20e65..873f2e3 100644 --- a/backend/internal/core/usecases/auth/login.go +++ b/backend/internal/core/usecases/auth/login.go @@ -80,6 +80,9 @@ func (uc *LoginUseCase) Execute(ctx context.Context, input dto.LoginRequest) (*d Email: user.Email, Roles: roles, Status: user.Status, + AvatarUrl: user.AvatarUrl, + Phone: user.Phone, + Bio: user.Bio, CreatedAt: user.CreatedAt, }, }, nil diff --git a/backend/internal/core/usecases/user/update_password.go b/backend/internal/core/usecases/user/update_password.go new file mode 100644 index 0000000..2c9b92d --- /dev/null +++ b/backend/internal/core/usecases/user/update_password.go @@ -0,0 +1,55 @@ +package user + +import ( + "context" + "errors" + + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" + "github.com/rede5/gohorsejobs/backend/internal/core/ports" +) + +type UpdatePasswordUseCase struct { + userRepo ports.UserRepository + authService ports.AuthService +} + +func NewUpdatePasswordUseCase(userRepo ports.UserRepository, authService ports.AuthService) *UpdatePasswordUseCase { + return &UpdatePasswordUseCase{ + userRepo: userRepo, + authService: authService, + } +} + +func (uc *UpdatePasswordUseCase) Execute(ctx context.Context, userID, currentPassword, newPassword string) error { + if newPassword == "" { + return errors.New("new password is required") + } + if len(newPassword) < 8 { + return errors.New("new password must be at least 8 characters") + } + + user, err := uc.userRepo.FindByID(ctx, userID) + if err != nil { + return err + } + if user == nil { + return errors.New("user not found") + } + + if !uc.authService.VerifyPassword(user.PasswordHash, currentPassword) { + return errors.New("current password is invalid") + } + + hashed, err := uc.authService.HashPassword(newPassword) + if err != nil { + return err + } + + user.PasswordHash = hashed + if user.Status == entity.UserStatusForceChangePassword { + user.Status = entity.UserStatusActive + } + + _, err = uc.userRepo.Update(ctx, user) + return err +} diff --git a/backend/internal/core/usecases/user/update_user.go b/backend/internal/core/usecases/user/update_user.go index fab42be..0656cee 100644 --- a/backend/internal/core/usecases/user/update_user.go +++ b/backend/internal/core/usecases/user/update_user.go @@ -42,6 +42,12 @@ func (uc *UpdateUserUseCase) Execute(ctx context.Context, id, tenantID string, i if input.Email != nil { user.Email = *input.Email } + if input.Phone != nil { + user.Phone = input.Phone + } + if input.Bio != nil { + user.Bio = input.Bio + } if input.Status != nil { user.Status = *input.Status } else if input.Active != nil { @@ -81,6 +87,8 @@ func (uc *UpdateUserUseCase) Execute(ctx context.Context, id, tenantID string, i Roles: roles, Status: updated.Status, AvatarUrl: updated.AvatarUrl, + Phone: updated.Phone, + Bio: updated.Bio, CreatedAt: updated.CreatedAt, }, nil } diff --git a/backend/internal/dto/auth.go b/backend/internal/dto/auth.go index b9dbba7..f11218d 100755 --- a/backend/internal/dto/auth.go +++ b/backend/internal/dto/auth.go @@ -54,6 +54,9 @@ type User struct { Email string `json:"email"` Role string `json:"role"` Status string `json:"status"` + Phone *string `json:"phone,omitempty"` + Bio *string `json:"bio,omitempty"` + AvatarUrl *string `json:"avatarUrl,omitempty"` CreatedAt time.Time `json:"createdAt"` CompanyID *string `json:"companyId,omitempty"` } diff --git a/backend/internal/infrastructure/persistence/postgres/user_repository.go b/backend/internal/infrastructure/persistence/postgres/user_repository.go index 8857182..c86e470 100644 --- a/backend/internal/infrastructure/persistence/postgres/user_repository.go +++ b/backend/internal/infrastructure/persistence/postgres/user_repository.go @@ -81,14 +81,29 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U } func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) { - query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), - COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, '') + query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), + COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''), + phone, bio FROM users WHERE email = $1 OR identifier = $1` row := r.db.QueryRowContext(ctx, query, email) u := &entity.User{} var dbID string - err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt, &u.AvatarUrl) + var phone sql.NullString + var bio sql.NullString + err := row.Scan( + &dbID, + &u.TenantID, + &u.Name, + &u.Email, + &u.PasswordHash, + &u.Status, + &u.CreatedAt, + &u.UpdatedAt, + &u.AvatarUrl, + &phone, + &bio, + ) if err != nil { if err == sql.ErrNoRows { return nil, nil // Return nil if not found @@ -96,23 +111,42 @@ func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity return nil, err } u.ID = dbID + u.Phone = nullStringPtr(phone) + u.Bio = nullStringPtr(bio) u.Roles, _ = r.getRoles(ctx, dbID) return u, nil } func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) { - query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), - COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, '') + query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), + COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''), + phone, bio FROM users WHERE id = $1` row := r.db.QueryRowContext(ctx, query, id) u := &entity.User{} var dbID string - err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt, &u.AvatarUrl) + var phone sql.NullString + var bio sql.NullString + err := row.Scan( + &dbID, + &u.TenantID, + &u.Name, + &u.Email, + &u.PasswordHash, + &u.Status, + &u.CreatedAt, + &u.UpdatedAt, + &u.AvatarUrl, + &phone, + &bio, + ) if err != nil { return nil, err } u.ID = dbID + u.Phone = nullStringPtr(phone) + u.Bio = nullStringPtr(bio) u.Roles, _ = r.getRoles(ctx, dbID) return u, nil } @@ -124,8 +158,9 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l return nil, 0, err } - query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), - COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, '') + query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), + COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''), + phone, bio FROM users WHERE tenant_id = $1 ORDER BY created_at DESC @@ -140,10 +175,26 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l for rows.Next() { u := &entity.User{} var dbID string - if err := rows.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt, &u.AvatarUrl); err != nil { + var phone sql.NullString + var bio sql.NullString + if err := rows.Scan( + &dbID, + &u.TenantID, + &u.Name, + &u.Email, + &u.PasswordHash, + &u.Status, + &u.CreatedAt, + &u.UpdatedAt, + &u.AvatarUrl, + &phone, + &bio, + ); err != nil { return nil, 0, err } u.ID = dbID + u.Phone = nullStringPtr(phone) + u.Bio = nullStringPtr(bio) u.Roles, _ = r.getRoles(ctx, dbID) users = append(users, u) } @@ -166,8 +217,22 @@ func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity primaryRole = user.Roles[0].Name } - query := `UPDATE users SET name=$1, email=$2, status=$3, role=$4, updated_at=$5, avatar_url=$6 WHERE id=$7` - _, err = tx.ExecContext(ctx, query, user.Name, user.Email, user.Status, primaryRole, user.UpdatedAt, user.AvatarUrl, user.ID) + query := `UPDATE users SET name=$1, full_name=$2, email=$3, status=$4, role=$5, updated_at=$6, avatar_url=$7, phone=$8, bio=$9, password_hash=$10 WHERE id=$11` + _, err = tx.ExecContext( + ctx, + query, + user.Name, + user.Name, + user.Email, + user.Status, + primaryRole, + user.UpdatedAt, + user.AvatarUrl, + user.Phone, + user.Bio, + user.PasswordHash, + user.ID, + ) if err != nil { return nil, err } @@ -225,3 +290,10 @@ func (r *UserRepository) getRoles(ctx context.Context, userID string) ([]entity. } return roles, nil } + +func nullStringPtr(value sql.NullString) *string { + if value.Valid { + return &value.String + } + return nil +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index ba4af33..c57bf1f 100755 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -69,6 +69,7 @@ func NewRouter() http.Handler { listUsersUC := userUC.NewListUsersUseCase(userRepo) deleteUserUC := userUC.NewDeleteUserUseCase(userRepo) updateUserUC := userUC.NewUpdateUserUseCase(userRepo) + updatePasswordUC := userUC.NewUpdatePasswordUseCase(userRepo, authService) auditService := services.NewAuditService(database.DB) notificationService := services.NewNotificationService(database.DB, fcmService) ticketService := services.NewTicketService(database.DB) @@ -87,6 +88,7 @@ func NewRouter() http.Handler { listUsersUC, deleteUserUC, updateUserUC, + updatePasswordUC, listCompaniesUC, auditService, notificationService, // Added @@ -178,6 +180,7 @@ func NewRouter() http.Handler { // Public /api/v1/users/me (Authenticated) mux.Handle("GET /api/v1/users/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.Me))) mux.Handle("PATCH /api/v1/users/me/profile", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMyProfile))) + mux.Handle("PATCH /api/v1/users/me/password", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMyPassword))) // /api/v1/admin/companies -> Handled by coreHandlers.ListCompanies (Smart Branching) // Needs to be wired with Optional Auth to support both Public and Admin. diff --git a/backend/internal/services/admin_service.go b/backend/internal/services/admin_service.go index b2bd595..fb1f56d 100644 --- a/backend/internal/services/admin_service.go +++ b/backend/internal/services/admin_service.go @@ -579,15 +579,37 @@ func (s *AdminService) getTagByID(ctx context.Context, id int) (*models.Tag, err // GetUser fetches a user by ID func (s *AdminService) GetUser(ctx context.Context, id string) (*dto.User, error) { query := ` - SELECT id, full_name, email, role, created_at + SELECT id, full_name, email, role, COALESCE(status, 'active'), created_at, phone, bio, avatar_url FROM users WHERE id = $1 ` var u dto.User var roleStr string - if err := s.DB.QueryRowContext(ctx, query, id).Scan(&u.ID, &u.Name, &u.Email, &roleStr, &u.CreatedAt); err != nil { + var phone sql.NullString + var bio sql.NullString + var avatarURL sql.NullString + if err := s.DB.QueryRowContext(ctx, query, id).Scan( + &u.ID, + &u.Name, + &u.Email, + &roleStr, + &u.Status, + &u.CreatedAt, + &phone, + &bio, + &avatarURL, + ); err != nil { return nil, err } u.Role = roleStr + if phone.Valid { + u.Phone = &phone.String + } + if bio.Valid { + u.Bio = &bio.String + } + if avatarURL.Valid { + u.AvatarUrl = &avatarURL.String + } return &u, nil } diff --git a/backend/internal/services/storage_service.go b/backend/internal/services/storage_service.go index 974d28f..4bcab24 100644 --- a/backend/internal/services/storage_service.go +++ b/backend/internal/services/storage_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -29,26 +30,34 @@ type UploadConfig struct { Region string `json:"region"` } -func (s *StorageService) getClient(ctx context.Context) (*s3.PresignClient, string, error) { - // 1. Fetch Credentials from DB (Encrypted Payload) +func (s *StorageService) getConfig(ctx context.Context) (UploadConfig, error) { payload, err := s.credentialsService.GetDecryptedKey(ctx, "storage") if err != nil { - return nil, "", fmt.Errorf("failed to get storage credentials: %w", err) + return UploadConfig{}, fmt.Errorf("failed to get storage credentials: %w", err) } var uCfg UploadConfig if err := json.Unmarshal([]byte(payload), &uCfg); err != nil { - return nil, "", fmt.Errorf("failed to parse storage credentials: %w", err) + return UploadConfig{}, fmt.Errorf("failed to parse storage credentials: %w", err) } if uCfg.Endpoint == "" || uCfg.AccessKey == "" || uCfg.SecretKey == "" || uCfg.Bucket == "" { - return nil, "", fmt.Errorf("storage credentials incomplete (all fields required)") + return UploadConfig{}, fmt.Errorf("storage credentials incomplete (all fields required)") } if uCfg.Region == "" { uCfg.Region = "auto" } + return uCfg, nil +} + +func (s *StorageService) getClient(ctx context.Context) (*s3.PresignClient, string, error) { + uCfg, err := s.getConfig(ctx) + if err != nil { + return nil, "", err + } + // 2. Setup S3 V2 Client cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(uCfg.Region), @@ -104,14 +113,10 @@ func (s *StorageService) TestConnection(ctx context.Context) error { // For now, let's refactor `getClient` slightly to expose specific logic or just create a one-off checker here. // Refetch raw creds to make a standard client - payload, err := s.credentialsService.GetDecryptedKey(ctx, "storage") + uCfg, err := s.getConfig(ctx) if err != nil { return fmt.Errorf("failed to get storage credentials: %w", err) } - var uCfg UploadConfig - if err := json.Unmarshal([]byte(payload), &uCfg); err != nil { - return fmt.Errorf("failed to parse storage credentials: %w", err) - } cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(uCfg.Region), @@ -140,3 +145,13 @@ func (s *StorageService) TestConnection(ctx context.Context) error { return nil } + +func (s *StorageService) GetPublicURL(ctx context.Context, key string) (string, error) { + uCfg, err := s.getConfig(ctx) + if err != nil { + return "", err + } + + endpoint := strings.TrimRight(uCfg.Endpoint, "/") + return fmt.Sprintf("%s/%s/%s", endpoint, uCfg.Bucket, key), nil +} diff --git a/frontend/src/app/dashboard/profile/page.tsx b/frontend/src/app/dashboard/profile/page.tsx index 28fa780..f36a161 100644 --- a/frontend/src/app/dashboard/profile/page.tsx +++ b/frontend/src/app/dashboard/profile/page.tsx @@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" +import { RichTextEditor } from "@/components/rich-text-editor" import { Save, Loader2, Building2 } from "lucide-react" import { useEffect, useState } from "react" import { profileApi, authApi, adminCompaniesApi, type AdminCompany } from "@/lib/api" @@ -27,6 +28,12 @@ export default function ProfilePage() { bio: "", }) + const [passwordData, setPasswordData] = useState({ + currentPassword: "", + newPassword: "", + confirmPassword: "", + }) + const [companyData, setCompanyData] = useState({ name: "", email: "", @@ -35,6 +42,17 @@ export default function ProfilePage() { description: "", }) + const getInitials = (name: string) => { + const initials = name + .trim() + .split(/\s+/) + .filter(Boolean) + .map((word) => word[0]) + .join("") + .toUpperCase() + return initials.slice(0, 2) || "U" + } + useEffect(() => { const currentUser = getCurrentUser() setIsAdmin(isAdminUser(currentUser)) @@ -89,6 +107,10 @@ export default function ProfilePage() { setCompanyData(prev => ({ ...prev, [field]: value })) } + const handlePasswordChange = (field: string, value: string) => { + setPasswordData(prev => ({ ...prev, [field]: value })) + } + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) @@ -133,6 +155,32 @@ export default function ProfilePage() { } } + const handlePasswordSubmit = async (e: React.FormEvent) => { + e.preventDefault() + console.log("[PROFILE_FLOW] Requesting password reset/update") + + if (passwordData.newPassword !== passwordData.confirmPassword) { + toast.error("Passwords do not match") + return + } + + try { + await profileApi.updatePassword({ + currentPassword: passwordData.currentPassword, + newPassword: passwordData.newPassword, + }) + toast.success("Password updated") + setPasswordData({ + currentPassword: "", + newPassword: "", + confirmPassword: "", + }) + } catch (error) { + console.error("[PROFILE_FLOW] Error updating password:", error) + toast.error("Failed to update password") + } + } + if (loading) { return (
@@ -153,16 +201,18 @@ export default function ProfilePage() {
{ if (file) { try { + console.log("[PROFILE_FLOW] Uploading avatar:", { name: file.name, size: file.size }) await profileApi.uploadAvatar(file) loadProfile() toast.success("Avatar updated") } catch (err) { + console.error("[PROFILE_FLOW] Avatar upload failed:", err) toast.error("Failed to upload avatar") } } @@ -203,11 +253,11 @@ export default function ProfilePage() {
-