package routes import ( "bssapp-backend/auth" "bssapp-backend/internal/auditlog" "bssapp-backend/internal/security" "bssapp-backend/models" "bssapp-backend/repository" "database/sql" "encoding/json" "log" "net/http" "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") // -------------------------------------------------- // 1️⃣ JWT CLAIMS // -------------------------------------------------- claims, ok := auth.GetClaimsFromContext(r.Context()) if !ok || claims == nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } // -------------------------------------------------- // 2️⃣ PAYLOAD // -------------------------------------------------- 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, "invalid payload", http.StatusBadRequest) return } if req.CurrentPassword == "" || req.NewPassword == "" { http.Error(w, "password fields required", http.StatusUnprocessableEntity) return } // -------------------------------------------------- // 3️⃣ LOAD USER (mk_dfusr) // -------------------------------------------------- var currentHash string err := db.QueryRow(` SELECT password_hash FROM mk_dfusr WHERE id = $1 `, claims.ID).Scan(¤tHash) if err != nil || currentHash == "" { http.Error(w, "user not found", http.StatusUnauthorized) return } // -------------------------------------------------- // 4️⃣ CURRENT PASSWORD CHECK // -------------------------------------------------- if bcrypt.CompareHashAndPassword( []byte(currentHash), []byte(req.CurrentPassword), ) != nil { http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized) return } // -------------------------------------------------- // 5️⃣ PASSWORD POLICY // -------------------------------------------------- if err := security.ValidatePassword(req.NewPassword); err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } // -------------------------------------------------- // 6️⃣ HASH NEW PASSWORD // -------------------------------------------------- hash, err := bcrypt.GenerateFromPassword( []byte(req.NewPassword), bcrypt.DefaultCost, ) if err != nil { http.Error(w, "password hash error", http.StatusInternalServerError) return } // -------------------------------------------------- // 7️⃣ UPDATE mk_dfusr // -------------------------------------------------- _, err = db.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 { http.Error(w, "password update failed", http.StatusInternalServerError) return } // -------------------------------------------------- // 8️⃣ REFRESH TOKEN REVOKE // -------------------------------------------------- _ = repository. NewRefreshTokenRepository(db). RevokeAllForUser(claims.ID) // -------------------------------------------------- // 9️⃣ NEW JWT (TEK DOĞRU YOL) // -------------------------------------------------- newClaims := auth.BuildClaimsFromUser( &models.MkUser{ ID: claims.ID, Username: claims.Username, RoleCode: claims.RoleCode, 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 generation failed", http.StatusInternalServerError) return } // -------------------------------------------------- // 🔟 AUDIT // -------------------------------------------------- auditlog.ForcePasswordChangeCompleted( r.Context(), claims.ID, "self_change", ) // -------------------------------------------------- // 1️⃣1️⃣ RESPONSE // -------------------------------------------------- _ = json.NewEncoder(w).Encode(map[string]any{ "token": newToken, "user": map[string]any{ "id": claims.ID, "username": claims.Username, "force_password_change": false, }, }) log.Printf("✅ FIRST-PASS claims user=%d role=%s", claims.ID, claims.RoleCode) } }