Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -10,29 +10,49 @@ import (
|
|||||||
|
|
||||||
func AuthMiddleware(db *sql.DB, next http.Handler) http.Handler {
|
func AuthMiddleware(db *sql.DB, next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"AUTH_MIDDLEWARE 401 reason=missing_authorization_header method=%s path=%s",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
)
|
||||||
|
http.Error(w, "unauthorized: authorization header missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.SplitN(authHeader, " ", 2)
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"AUTH_MIDDLEWARE 401 reason=invalid_authorization_format method=%s path=%s raw=%q",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
authHeader,
|
||||||
|
)
|
||||||
|
http.Error(w, "unauthorized: invalid authorization format", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := auth.ValidateToken(parts[1])
|
claims, err := auth.ValidateToken(parts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"AUTH_MIDDLEWARE 401 reason=token_validation_failed method=%s path=%s err=%v",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
http.Error(w, "unauthorized: token validation failed", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 BU SATIR ŞART
|
|
||||||
ctx := auth.WithClaims(r.Context(), claims)
|
ctx := auth.WithClaims(r.Context(), claims)
|
||||||
|
log.Printf(
|
||||||
log.Printf("🔐 AUTH CTX SET user=%d role=%s", claims.ID, claims.RoleCode)
|
"AUTH_MIDDLEWARE PASS user=%d role=%s method=%s path=%s",
|
||||||
|
claims.ID,
|
||||||
|
claims.RoleCode,
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
)
|
||||||
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -859,7 +859,12 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
claims, ok := auth.GetClaimsFromContext(r.Context())
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
if !ok || claims == nil {
|
if !ok || claims == nil {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"AUTHZ_BY_ROUTE 401 reason=claims_missing method=%s path=%s",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
)
|
||||||
|
http.Error(w, "unauthorized: token missing or invalid", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +878,7 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
r.Method, r.URL.Path,
|
r.Method, r.URL.Path,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "route not resolved", 403)
|
http.Error(w, "route not resolved", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,7 +886,14 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ AUTHZ: path template error: %v", err)
|
log.Printf("❌ AUTHZ: path template error: %v", err)
|
||||||
|
|
||||||
http.Error(w, "route template error", 403)
|
http.Error(w, "route template error", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password change must be reachable for every authenticated user.
|
||||||
|
// This avoids permission deadlocks during forced first-password flow.
|
||||||
|
if pathTemplate == "/api/password/change" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,7 +920,12 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
pathTemplate,
|
pathTemplate,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "route permission not found", 403)
|
if pathTemplate == "/api/password/change" {
|
||||||
|
http.Error(w, "password change route permission not found", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "route permission not found", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,7 +952,12 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "forbidden", 403)
|
if pathTemplate == "/api/password/change" {
|
||||||
|
http.Error(w, "password change permission check failed", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,7 +970,12 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
action,
|
action,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "forbidden", 403)
|
if pathTemplate == "/api/password/change" {
|
||||||
|
http.Error(w, "password change forbidden: permission denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ import (
|
|||||||
"bssapp-backend/internal/security"
|
"bssapp-backend/internal/security"
|
||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"bssapp-backend/repository"
|
"bssapp-backend/repository"
|
||||||
|
"bssapp-backend/services"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -17,21 +20,19 @@ import (
|
|||||||
|
|
||||||
func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 1️⃣ JWT CLAIMS
|
|
||||||
// --------------------------------------------------
|
|
||||||
claims, ok := auth.GetClaimsFromContext(r.Context())
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
if !ok || claims == nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 2️⃣ PAYLOAD
|
|
||||||
// --------------------------------------------------
|
|
||||||
var req struct {
|
var req struct {
|
||||||
CurrentPassword string `json:"current_password"`
|
CurrentPassword string `json:"current_password"`
|
||||||
NewPassword string `json:"new_password"`
|
NewPassword string `json:"new_password"`
|
||||||
@@ -42,48 +43,75 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.CurrentPassword = strings.TrimSpace(req.CurrentPassword)
|
||||||
|
req.NewPassword = strings.TrimSpace(req.NewPassword)
|
||||||
if req.CurrentPassword == "" || req.NewPassword == "" {
|
if req.CurrentPassword == "" || req.NewPassword == "" {
|
||||||
http.Error(w, "password fields required", http.StatusUnprocessableEntity)
|
http.Error(w, "password fields required", http.StatusUnprocessableEntity)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
mkRepo := repository.NewMkUserRepository(db)
|
||||||
// 3️⃣ LOAD USER (mk_dfusr)
|
legacyRepo := repository.NewUserRepository(db)
|
||||||
// --------------------------------------------------
|
|
||||||
var currentHash string
|
|
||||||
err := db.QueryRow(`
|
|
||||||
SELECT password_hash
|
|
||||||
FROM mk_dfusr
|
|
||||||
WHERE id = $1
|
|
||||||
`, claims.ID).Scan(¤tHash)
|
|
||||||
|
|
||||||
if err != nil || currentHash == "" {
|
mkUser, mkErr := mkRepo.GetByID(claims.ID)
|
||||||
http.Error(w, "user not found", http.StatusUnauthorized)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
var legacyUser *models.User
|
||||||
// 4️⃣ CURRENT PASSWORD CHECK
|
|
||||||
// --------------------------------------------------
|
// If user already exists in mk_dfusr with hash, verify against mk hash.
|
||||||
if bcrypt.CompareHashAndPassword(
|
// Otherwise verify against legacy dfusr password before migration.
|
||||||
[]byte(currentHash),
|
if hasMkUser && strings.TrimSpace(mkUser.PasswordHash) != "" {
|
||||||
[]byte(req.CurrentPassword),
|
if bcrypt.CompareHashAndPassword(
|
||||||
) != nil {
|
[]byte(mkUser.PasswordHash),
|
||||||
http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
|
[]byte(req.CurrentPassword),
|
||||||
return
|
) != 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 {
|
if err := security.ValidatePassword(req.NewPassword); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 6️⃣ HASH NEW PASSWORD
|
|
||||||
// --------------------------------------------------
|
|
||||||
hash, err := bcrypt.GenerateFromPassword(
|
hash, err := bcrypt.GenerateFromPassword(
|
||||||
[]byte(req.NewPassword),
|
[]byte(req.NewPassword),
|
||||||
bcrypt.DefaultCost,
|
bcrypt.DefaultCost,
|
||||||
@@ -93,39 +121,133 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
tx, err := db.Begin()
|
||||||
// 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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
_ = repository.NewRefreshTokenRepository(db).RevokeAllForUser(claims.ID)
|
||||||
// 8️⃣ REFRESH TOKEN REVOKE
|
|
||||||
// --------------------------------------------------
|
|
||||||
_ = repository.
|
|
||||||
NewRefreshTokenRepository(db).
|
|
||||||
RevokeAllForUser(claims.ID)
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 9️⃣ NEW JWT (TEK DOĞRU YOL)
|
|
||||||
// --------------------------------------------------
|
|
||||||
newClaims := auth.BuildClaimsFromUser(
|
newClaims := auth.BuildClaimsFromUser(
|
||||||
&models.MkUser{
|
&models.MkUser{
|
||||||
ID: claims.ID,
|
ID: claims.ID,
|
||||||
Username: claims.Username,
|
Username: claims.Username,
|
||||||
|
RoleID: claims.RoleID,
|
||||||
RoleCode: claims.RoleCode,
|
RoleCode: claims.RoleCode,
|
||||||
|
DepartmentCodes: claims.DepartmentCodes,
|
||||||
V3Username: claims.V3Username,
|
V3Username: claims.V3Username,
|
||||||
V3UserGroup: claims.V3UserGroup,
|
V3UserGroup: claims.V3UserGroup,
|
||||||
SessionID: claims.SessionID,
|
SessionID: claims.SessionID,
|
||||||
@@ -144,18 +266,16 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
source := "mk_password_update"
|
||||||
// 🔟 AUDIT
|
if migratedFromLegacy {
|
||||||
// --------------------------------------------------
|
source = "legacy_migration_completed"
|
||||||
|
}
|
||||||
auditlog.ForcePasswordChangeCompleted(
|
auditlog.ForcePasswordChangeCompleted(
|
||||||
r.Context(),
|
r.Context(),
|
||||||
claims.ID,
|
claims.ID,
|
||||||
"self_change",
|
source,
|
||||||
)
|
)
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 1️⃣1️⃣ RESPONSE
|
|
||||||
// --------------------------------------------------
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"token": newToken,
|
"token": newToken,
|
||||||
"user": map[string]any{
|
"user": map[string]any{
|
||||||
@@ -163,7 +283,14 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
"username": claims.Username,
|
"username": claims.Username,
|
||||||
"force_password_change": false,
|
"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(
|
mkUser = &models.MkUser{
|
||||||
[]byte(pass),
|
ID: int64(legacyUser.ID),
|
||||||
bcrypt.DefaultCost,
|
Username: legacyUser.Username,
|
||||||
)
|
Email: legacyUser.Email,
|
||||||
if err != nil {
|
IsActive: legacyUser.IsActive,
|
||||||
http.Error(w, "Şifre üretilemedi", http.StatusInternalServerError)
|
RoleID: int64(legacyUser.RoleID),
|
||||||
return
|
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{
|
auditlog.Write(auditlog.ActivityLog{
|
||||||
ActionType: "LEGACY_USER_MIGRATED",
|
ActionType: "LEGACY_USER_LOGIN_PENDING_MIGRATION",
|
||||||
ActionCategory: "security",
|
ActionCategory: "security",
|
||||||
Description: "dfusr -> mk_dfusr on login",
|
Description: "legacy login ok, first password change required",
|
||||||
IsSuccess: true,
|
IsSuccess: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import api from 'src/services/api'
|
import api, { extractApiErrorDetail } from 'src/services/api'
|
||||||
import { useAuthStore } from 'stores/authStore.js'
|
import { useAuthStore } from 'stores/authStore.js'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -69,6 +69,33 @@ const newPassword2 = ref('')
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
|
function resolveFirstPasswordError(status, detail) {
|
||||||
|
const text = String(detail || '').trim()
|
||||||
|
const lower = text.toLowerCase()
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
if (lower.includes('mevcut sifre') || lower.includes('current password')) {
|
||||||
|
return 'Mevcut şifreyi yanlış girdiniz.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.includes('token') || lower.includes('authorization')) {
|
||||||
|
return 'Oturum doğrulanamadı. Lütfen tekrar giriş yapın.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || 'Kimlik doğrulama hatası (401). Lütfen tekrar giriş yapın.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 403) {
|
||||||
|
if (lower.includes('permission')) {
|
||||||
|
return 'Şifre değiştirme yetkiniz yok (403). Sistem yöneticinize başvurun.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || 'Bu işlem için yetkiniz yok (403).'
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || 'Şifre güncellenemedi'
|
||||||
|
}
|
||||||
|
|
||||||
async function submit () {
|
async function submit () {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
@@ -84,25 +111,25 @@ async function submit () {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 🔐 TOKEN interceptor ile otomatik
|
|
||||||
await api.post('/password/change', {
|
await api.post('/password/change', {
|
||||||
current_password: currentPassword.value,
|
current_password: currentPassword.value,
|
||||||
new_password: newPassword.value
|
new_password: newPassword.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// Şifre değişimi sonrası tekrar giriş zorunlu
|
|
||||||
auth.clearSession()
|
auth.clearSession()
|
||||||
router.replace('/login')
|
router.replace('/login')
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value =
|
const status = e?.response?.status
|
||||||
e?.data?.message ||
|
const detail = await extractApiErrorDetail(e)
|
||||||
e?.message ||
|
|
||||||
'Şifre güncellenemedi'
|
console.error('FIRST_PASSWORD_CHANGE failed', {
|
||||||
|
status,
|
||||||
|
detail
|
||||||
|
})
|
||||||
|
|
||||||
|
error.value = resolveFirstPasswordError(status, detail)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
// src/services/api.js
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import qs from 'qs'
|
import qs from 'qs'
|
||||||
import { useAuthStore } from 'stores/authStore'
|
import { useAuthStore } from 'stores/authStore'
|
||||||
|
|
||||||
// 🔥 ENV YOK
|
|
||||||
export const API_BASE_URL = '/api'
|
export const API_BASE_URL = '/api'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
@@ -38,17 +36,22 @@ api.interceptors.response.use(
|
|||||||
r => r,
|
r => r,
|
||||||
async (error) => {
|
async (error) => {
|
||||||
const status = error?.response?.status
|
const status = error?.response?.status
|
||||||
|
const requestUrl = String(error?.config?.url || '')
|
||||||
const hasBlob = typeof Blob !== 'undefined' && error?.response?.data instanceof Blob
|
const hasBlob = typeof Blob !== 'undefined' && error?.response?.data instanceof Blob
|
||||||
|
const isPasswordChangeRequest =
|
||||||
|
requestUrl.startsWith('/password/change') ||
|
||||||
|
requestUrl.startsWith('/me/password')
|
||||||
|
|
||||||
if ((status >= 500 || hasBlob) && error) {
|
if ((status >= 500 || hasBlob) && error) {
|
||||||
const method = String(error?.config?.method || 'GET').toUpperCase()
|
const method = String(error?.config?.method || 'GET').toUpperCase()
|
||||||
const url = error?.config?.url || ''
|
|
||||||
const detail = await extractApiErrorDetail(error)
|
const detail = await extractApiErrorDetail(error)
|
||||||
error.parsedMessage = detail
|
error.parsedMessage = detail
|
||||||
console.error(`❌ API ${status || '-'} ${method} ${url}: ${detail}`)
|
console.error(`API ${status || '-'} ${method} ${requestUrl}: ${detail}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error?.response?.status === 401 && !isLoggingOut) {
|
// Password change endpoints may return 401 for business reasons
|
||||||
|
// (for example current password mismatch). Keep session in that case.
|
||||||
|
if (status === 401 && !isPasswordChangeRequest && !isLoggingOut) {
|
||||||
isLoggingOut = true
|
isLoggingOut = true
|
||||||
try {
|
try {
|
||||||
useAuthStore().clearSession()
|
useAuthStore().clearSession()
|
||||||
@@ -56,6 +59,7 @@ api.interceptors.response.use(
|
|||||||
isLoggingOut = false
|
isLoggingOut = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user