package routes import ( "bssapp-backend/auth" "bssapp-backend/internal/auditlog" "bssapp-backend/internal/security" "bssapp-backend/models" "bssapp-backend/repository" "bssapp-backend/services" "database/sql" "encoding/json" "errors" "log" "net/http" "strings" "time" "golang.org/x/crypto/bcrypt" ) func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") claims, ok := auth.GetClaimsFromContext(r.Context()) if !ok || claims == nil { log.Printf( "FIRST_PASSWORD_CHANGE 401 reason=claims_missing method=%s path=%s", r.Method, r.URL.Path, ) http.Error(w, "yetkisiz: token eksik veya geçersiz", http.StatusUnauthorized) return } var req struct { CurrentPassword string `json:"current_password"` NewPassword string `json:"new_password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "geçersiz istek gövdesi", http.StatusBadRequest) return } req.CurrentPassword = strings.TrimSpace(req.CurrentPassword) req.NewPassword = strings.TrimSpace(req.NewPassword) if req.CurrentPassword == "" || req.NewPassword == "" { http.Error(w, "şifre alanları zorunludur", http.StatusUnprocessableEntity) return } mkRepo := repository.NewMkUserRepository(db) legacyRepo := repository.NewUserRepository(db) mkUser, mkErr := mkRepo.GetByID(claims.ID) hasMkUser := mkErr == nil if mkErr != nil && !errors.Is(mkErr, repository.ErrMkUserNotFound) { log.Printf( "FIRST_PASSWORD_CHANGE 500 reason=mk_lookup_failed user_id=%d err=%v", claims.ID, mkErr, ) http.Error(w, "kullanıcı sorgulama hatası", http.StatusInternalServerError) return } var legacyUser *models.User // If user already exists in mk_dfusr with hash, verify against mk hash. // Otherwise verify against legacy dfusr password before migration. if hasMkUser && strings.TrimSpace(mkUser.PasswordHash) != "" { if bcrypt.CompareHashAndPassword( []byte(mkUser.PasswordHash), []byte(req.CurrentPassword), ) != nil { log.Printf( "FIRST_PASSWORD_CHANGE 401 reason=current_password_mismatch_mk user_id=%d username=%s", claims.ID, claims.Username, ) http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized) return } } else { var err error legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username) if err != nil || legacyUser == nil || !legacyUser.IsActive { log.Printf( "FIRST_PASSWORD_CHANGE 401 reason=legacy_user_not_found user_id=%d username=%s err=%v", claims.ID, claims.Username, err, ) http.Error(w, "yetkisiz: kullanıcı bulunamadı", http.StatusUnauthorized) return } if !hasMkUser && int64(legacyUser.ID) != claims.ID { log.Printf( "FIRST_PASSWORD_CHANGE 401 reason=legacy_id_mismatch user_id=%d legacy_id=%d username=%s", claims.ID, legacyUser.ID, claims.Username, ) http.Error(w, "yetkisiz: kullanıcı bulunamadı", http.StatusUnauthorized) return } if !services.CheckPasswordWithLegacy(legacyUser, req.CurrentPassword) { log.Printf( "FIRST_PASSWORD_CHANGE 401 reason=current_password_mismatch_legacy user_id=%d username=%s", claims.ID, claims.Username, ) http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized) return } } if err := security.ValidatePassword(req.NewPassword); err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } hash, err := bcrypt.GenerateFromPassword( []byte(req.NewPassword), bcrypt.DefaultCost, ) if err != nil { http.Error(w, "şifre hash hatası", http.StatusInternalServerError) return } tx, err := db.Begin() if err != nil { http.Error(w, "işlem başlatılamadı", http.StatusInternalServerError) return } defer tx.Rollback() migratedFromLegacy := false if hasMkUser { res, err := tx.Exec(` UPDATE mk_dfusr SET password_hash = $1, force_password_change = false, password_updated_at = NOW(), updated_at = NOW() WHERE id = $2 `, string(hash), claims.ID) if err != nil { log.Printf( "FIRST_PASSWORD_CHANGE 500 reason=password_update_failed user_id=%d err=%v", claims.ID, err, ) http.Error(w, "şifre güncellenemedi", http.StatusInternalServerError) return } affected, _ := res.RowsAffected() if affected == 0 { log.Printf( "FIRST_PASSWORD_CHANGE 500 reason=password_update_no_rows user_id=%d", claims.ID, ) http.Error(w, "şifre güncellenemedi", http.StatusInternalServerError) return } } else { if legacyUser == nil { // Defensive fallback, should not happen. legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username) if err != nil || legacyUser == nil { log.Printf( "FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_failed user_id=%d username=%s err=%v", claims.ID, claims.Username, err, ) http.Error(w, "legacy kullanıcı yeniden yüklenemedi", http.StatusInternalServerError) return } if !hasMkUser && int64(legacyUser.ID) != claims.ID { log.Printf( "FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_id_mismatch user_id=%d legacy_id=%d username=%s", claims.ID, legacyUser.ID, claims.Username, ) http.Error(w, "legacy kullanıcı yeniden yüklenemedi", http.StatusInternalServerError) return } } _, err = tx.Exec(` INSERT INTO mk_dfusr ( id, username, email, full_name, mobile, address, is_active, password_hash, force_password_change, password_updated_at, created_at, updated_at ) VALUES ( $1,$2,$3,$4,$5,$6,$7,$8,false,NOW(),NOW(),NOW() ) ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, email = EXCLUDED.email, full_name = EXCLUDED.full_name, mobile = EXCLUDED.mobile, address = EXCLUDED.address, is_active = EXCLUDED.is_active, password_hash = EXCLUDED.password_hash, force_password_change = false, password_updated_at = NOW(), updated_at = NOW() `, int64(legacyUser.ID), strings.TrimSpace(legacyUser.Username), strings.TrimSpace(legacyUser.Email), strings.TrimSpace(legacyUser.FullName), strings.TrimSpace(legacyUser.Mobile), strings.TrimSpace(legacyUser.Address), legacyUser.IsActive, string(hash), ) if err != nil { log.Printf( "FIRST_PASSWORD_CHANGE 500 reason=legacy_migration_failed user_id=%d username=%s err=%v", claims.ID, claims.Username, err, ) http.Error(w, "legacy geçişi başarısız", http.StatusInternalServerError) return } migratedFromLegacy = true } if err := tx.Commit(); err != nil { log.Printf( "FIRST_PASSWORD_CHANGE 500 reason=tx_commit_failed user_id=%d err=%v", claims.ID, err, ) http.Error(w, "işlem tamamlanamadı", http.StatusInternalServerError) return } _ = repository.NewRefreshTokenRepository(db).RevokeAllForUser(claims.ID) newClaims := auth.BuildClaimsFromUser( &models.MkUser{ ID: claims.ID, Username: claims.Username, RoleID: claims.RoleID, RoleCode: claims.RoleCode, DepartmentCodes: claims.DepartmentCodes, V3Username: claims.V3Username, V3UserGroup: claims.V3UserGroup, SessionID: claims.SessionID, ForcePasswordChange: false, }, 15*time.Minute, ) newToken, err := auth.GenerateToken( newClaims, claims.Username, false, ) if err != nil { http.Error(w, "token üretilemedi", http.StatusInternalServerError) return } source := "mk_password_update" if migratedFromLegacy { source = "legacy_migration_completed" } auditlog.ForcePasswordChangeCompleted( r.Context(), claims.ID, source, ) _ = json.NewEncoder(w).Encode(map[string]any{ "token": newToken, "user": map[string]any{ "id": claims.ID, "username": claims.Username, "force_password_change": false, }, "migrated_from_legacy": migratedFromLegacy, }) log.Printf( "FIRST_PASSWORD_CHANGE 200 user=%d role=%s migrated_from_legacy=%v", claims.ID, claims.RoleCode, migratedFromLegacy, ) } }