317 lines
8.3 KiB
Go
317 lines
8.3 KiB
Go
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,
|
||
)
|
||
}
|
||
}
|