Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -6,10 +6,13 @@ import (
|
||||
"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"
|
||||
@@ -17,21 +20,19 @@ import (
|
||||
|
||||
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)
|
||||
log.Printf(
|
||||
"FIRST_PASSWORD_CHANGE 401 reason=claims_missing method=%s path=%s",
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
)
|
||||
http.Error(w, "unauthorized: token missing or invalid", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// 2️⃣ PAYLOAD
|
||||
// --------------------------------------------------
|
||||
var req struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
@@ -42,48 +43,75 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
req.CurrentPassword = strings.TrimSpace(req.CurrentPassword)
|
||||
req.NewPassword = strings.TrimSpace(req.NewPassword)
|
||||
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)
|
||||
mkRepo := repository.NewMkUserRepository(db)
|
||||
legacyRepo := repository.NewUserRepository(db)
|
||||
|
||||
if err != nil || currentHash == "" {
|
||||
http.Error(w, "user not found", http.StatusUnauthorized)
|
||||
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, "user lookup failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// 4️⃣ CURRENT PASSWORD CHECK
|
||||
// --------------------------------------------------
|
||||
if bcrypt.CompareHashAndPassword(
|
||||
[]byte(currentHash),
|
||||
[]byte(req.CurrentPassword),
|
||||
) != nil {
|
||||
http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
|
||||
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 sifre hatali", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
|
||||
if err != nil || legacyUser == nil || !legacyUser.IsActive || int64(legacyUser.ID) != claims.ID {
|
||||
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, "unauthorized: user not found", 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 sifre hatali", 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,
|
||||
@@ -93,39 +121,133 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
||||
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)
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
http.Error(w, "password update failed", http.StatusInternalServerError)
|
||||
http.Error(w, "transaction error", 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, "password update failed", 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, "password update failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if legacyUser == nil {
|
||||
// Defensive fallback, should not happen.
|
||||
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
|
||||
if err != nil || legacyUser == nil || int64(legacyUser.ID) != claims.ID {
|
||||
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 user reload failed", 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 migration failed", 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, "commit failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// 8️⃣ REFRESH TOKEN REVOKE
|
||||
// --------------------------------------------------
|
||||
_ = repository.
|
||||
NewRefreshTokenRepository(db).
|
||||
RevokeAllForUser(claims.ID)
|
||||
_ = repository.NewRefreshTokenRepository(db).RevokeAllForUser(claims.ID)
|
||||
|
||||
// --------------------------------------------------
|
||||
// 9️⃣ NEW JWT (TEK DOĞRU YOL)
|
||||
// --------------------------------------------------
|
||||
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,
|
||||
@@ -144,18 +266,16 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// 🔟 AUDIT
|
||||
// --------------------------------------------------
|
||||
source := "mk_password_update"
|
||||
if migratedFromLegacy {
|
||||
source = "legacy_migration_completed"
|
||||
}
|
||||
auditlog.ForcePasswordChangeCompleted(
|
||||
r.Context(),
|
||||
claims.ID,
|
||||
"self_change",
|
||||
source,
|
||||
)
|
||||
|
||||
// --------------------------------------------------
|
||||
// 1️⃣1️⃣ RESPONSE
|
||||
// --------------------------------------------------
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"token": newToken,
|
||||
"user": map[string]any{
|
||||
@@ -163,7 +283,14 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
||||
"username": claims.Username,
|
||||
"force_password_change": false,
|
||||
},
|
||||
"migrated_from_legacy": migratedFromLegacy,
|
||||
})
|
||||
log.Printf("✅ FIRST-PASS claims user=%d role=%s", claims.ID, claims.RoleCode)
|
||||
|
||||
log.Printf(
|
||||
"FIRST_PASSWORD_CHANGE 200 user=%d role=%s migrated_from_legacy=%v",
|
||||
claims.ID,
|
||||
claims.RoleCode,
|
||||
migratedFromLegacy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,31 +133,23 @@ func LoginHandler(db *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// 3️⃣ MIGRATION (dfusr → mk_dfusr)
|
||||
// 3️⃣ LEGACY SESSION (PENDING MIGRATION)
|
||||
// - mk_dfusr migration is completed in /api/password/change
|
||||
// ==================================================
|
||||
newHash, err := bcrypt.GenerateFromPassword(
|
||||
[]byte(pass),
|
||||
bcrypt.DefaultCost,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "Şifre üretilemedi", http.StatusInternalServerError)
|
||||
return
|
||||
mkUser = &models.MkUser{
|
||||
ID: int64(legacyUser.ID),
|
||||
Username: legacyUser.Username,
|
||||
Email: legacyUser.Email,
|
||||
IsActive: legacyUser.IsActive,
|
||||
RoleID: int64(legacyUser.RoleID),
|
||||
RoleCode: legacyUser.RoleCode,
|
||||
ForcePasswordChange: true,
|
||||
}
|
||||
|
||||
mkUser, err = mkRepo.CreateFromLegacy(legacyUser, string(newHash))
|
||||
if err != nil {
|
||||
log.Println("❌ CREATE_FROM_LEGACY FAILED:", err)
|
||||
http.Error(w, "Kullanıcı migrate edilemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 🔥 KRİTİK: TOKEN GUARD İÇİN GARANTİ
|
||||
mkUser.ForcePasswordChange = true
|
||||
|
||||
auditlog.Write(auditlog.ActivityLog{
|
||||
ActionType: "LEGACY_USER_MIGRATED",
|
||||
ActionType: "LEGACY_USER_LOGIN_PENDING_MIGRATION",
|
||||
ActionCategory: "security",
|
||||
Description: "dfusr -> mk_dfusr on login",
|
||||
Description: "legacy login ok, first password change required",
|
||||
IsSuccess: true,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user