package routes import ( "bssapp-backend/auth" "bssapp-backend/internal/auditlog" "bssapp-backend/internal/security" "bssapp-backend/models" "bssapp-backend/queries" "bssapp-backend/repository" "bssapp-backend/services" "database/sql" "encoding/json" "fmt" "log" "net/http" "strconv" "strings" "time" "github.com/gorilla/mux" "golang.org/x/crypto/bcrypt" ) /* ====================================================== 🔐 LOGIN ====================================================== */ type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` } func LoginHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") // -------------------------------------------------- // 0️⃣ REQUEST // -------------------------------------------------- var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Geçersiz JSON", http.StatusBadRequest) return } login := strings.TrimSpace(req.Username) pass := req.Password // ⚠️ TRIM YAPMA if login == "" || pass == "" { http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) return } mkRepo := repository.NewMkUserRepository(db) // ================================================== // 1️⃣ mk_dfusr ÖNCELİKLİ // ================================================== mkUser, err := mkRepo.GetByUsername(login) if err == nil { log.Println("🧪 MK USER FROM DB") log.Printf("🧪 ID=%d role_id=%d role_code='%s' depts=%v", mkUser.ID, mkUser.RoleID, mkUser.RoleCode, mkUser.DepartmentCodes, ) } log.Printf( "🔎 LOGIN DEBUG | mk_user_found=%v err=%v hash_len=%d", err == nil, err, func() int { if err == nil { return len(strings.TrimSpace(mkUser.PasswordHash)) } return 0 }(), ) if err == nil { // mk_dfusr authoritative if strings.TrimSpace(mkUser.PasswordHash) != "" { if bcrypt.CompareHashAndPassword( []byte(mkUser.PasswordHash), []byte(pass), ) != nil { http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) return } _ = mkRepo.TouchLastLogin(mkUser.ID) writeLoginResponse(w, db, mkUser) return } // password_hash boşsa legacy fallback } else if err != repository.ErrMkUserNotFound { log.Println("❌ mk_dfusr lookup error:", err) http.Error(w, "Giriş yapılamadı", http.StatusInternalServerError) return } // ================================================== // 2️⃣ dfusr FALLBACK (LEGACY) // ================================================== log.Println("🟡 LEGACY LOGIN PATH:", login) legacyRepo := repository.NewUserRepository(db) legacyUser, err := legacyRepo.GetLegacyUserForLogin(login) if err != nil || !legacyUser.IsActive { http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) return } log.Printf( "🔎 LOGIN DEBUG | legacy_upass_len=%d prefix=%s", len(strings.TrimSpace(legacyUser.Upass)), func() string { if len(legacyUser.Upass) >= 4 { return legacyUser.Upass[:4] } return legacyUser.Upass }(), ) if !services.CheckPasswordWithLegacy(legacyUser, pass) { http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) return } // ================================================== // 3️⃣ LEGACY SESSION (PENDING MIGRATION) // - mk_dfusr migration is completed in /api/password/change // ================================================== 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, } auditlog.Write(auditlog.ActivityLog{ ActionType: "LEGACY_USER_LOGIN_PENDING_MIGRATION", ActionCategory: "security", Description: "legacy login ok, first password change required", IsSuccess: true, }) // ================================================== // 4️⃣ SUCCESS // ================================================== writeLoginResponse(w, db, mkUser) } } // ====================================================== // 🔑 LOGIN RESPONSE // ====================================================== func writeLoginResponse(w http.ResponseWriter, db *sql.DB, user *models.MkUser) { // 🔥 ROLE GARANTİSİ if user.RoleID == 0 { _ = db.QueryRow(` SELECT dfrole_id FROM dfrole_usr WHERE dfusr_id = $1 LIMIT 1 `, user.ID).Scan(&user.RoleID) } if user.RoleCode == "" && user.RoleID > 0 { _ = db.QueryRow(` SELECT code FROM dfrole WHERE id = $1 `, user.RoleID).Scan(&user.RoleCode) } log.Println("🧪 LOGIN RESPONSE USER DEBUG") log.Printf("🧪 user.ID = %d", user.ID) log.Printf("🧪 user.Username = %s", user.Username) log.Printf("🧪 user.RoleID = %d", user.RoleID) log.Printf("🧪 user.RoleCode = '%s'", user.RoleCode) log.Printf("🧪 user.IsActive = %v", user.IsActive) permRepo := repository.NewPermissionRepository(db) perms, _ := permRepo.GetPermissionsByRoleID(user.RoleID) // ✅ CLAIMS BUILD claims := auth.BuildClaimsFromUser(user, 15*time.Minute) token, err := auth.GenerateToken( claims, user.Username, user.ForcePasswordChange, ) if err != nil { http.Error(w, "Token üretilemedi", http.StatusInternalServerError) return } refreshPlain, refreshHash, err := security.GenerateRefreshToken() if err != nil { http.Error(w, "Refresh token üretilemedi", http.StatusInternalServerError) return } refreshExp := time.Now().Add(14 * 24 * time.Hour) rtRepo := repository.NewRefreshTokenRepository(db) if err := rtRepo.IssueRefreshToken(user.ID, refreshHash, refreshExp); err != nil { log.Printf("refresh token store failed user=%d err=%v", user.ID, err) http.Error(w, "Session başlatılamadı", http.StatusInternalServerError) return } setRefreshCookie(w, refreshPlain, refreshExp) _ = json.NewEncoder(w).Encode(map[string]any{ "token": token, "user": map[string]any{ "id": user.ID, "username": user.Username, "email": user.Email, "is_active": user.IsActive, "role_id": user.RoleID, "role_code": user.RoleCode, "force_password_change": user.ForcePasswordChange, "v3_username": user.V3Username, "v3_usergroup": user.V3UserGroup, }, "permissions": perms, }) } /* ====================================================== 🔎 LOOKUPS (aynen korunuyor) ====================================================== */ type LookupOption struct { Value string `json:"value"` Label string `json:"label"` } func GetRoleLookupRoute(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rows, err := db.Query(queries.GetRoleLookup) if err != nil { http.Error(w, "role lookup error", http.StatusInternalServerError) return } defer rows.Close() var list []LookupOption for rows.Next() { var o LookupOption if err := rows.Scan(&o.Value, &o.Label); err == nil { list = append(list, o) } } _ = json.NewEncoder(w).Encode(list) } } func GetDepartmentLookupRoute(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rows, err := db.Query(queries.GetDepartmentLookup) if err != nil { http.Error(w, "department lookup error", http.StatusInternalServerError) return } defer rows.Close() var list []LookupOption for rows.Next() { var o LookupOption if err := rows.Scan(&o.Value, &o.Label); err == nil { list = append(list, o) } } _ = json.NewEncoder(w).Encode(list) } } func GetPiyasaLookupRoute(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rows, err := db.Query(queries.GetPiyasaLookup) if err != nil { http.Error(w, "piyasa lookup error", http.StatusInternalServerError) return } defer rows.Close() var list []LookupOption for rows.Next() { var o LookupOption if err := rows.Scan(&o.Value, &o.Label); err == nil { list = append(list, o) } } _ = json.NewEncoder(w).Encode(list) } } // ====================================================== // 🧾 NEBIM USER LOOKUP // GET /api/lookups/nebim-users // ====================================================== func GetNebimUserLookupRoute(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rows, err := db.Query(queries.GetNebimUserLookup) if err != nil { http.Error(w, "nebim lookup error", http.StatusInternalServerError) return } defer rows.Close() var list []map[string]string for rows.Next() { var v, l, g string if err := rows.Scan(&v, &l, &g); err == nil { list = append(list, map[string]string{ "value": v, "label": l, "user_group_code": g, }) } } _ = json.NewEncoder(w).Encode(list) } } func UserCreateRoute(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") var payload models.UserWrite if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Geçersiz payload", http.StatusBadRequest) return } if payload.Code == "" { http.Error(w, "Kullanıcı kodu zorunludur", http.StatusUnprocessableEntity) return } tx, err := db.Begin() if err != nil { http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) return } defer tx.Rollback() var newID int64 err = tx.QueryRow(` INSERT INTO mk_dfusr ( code, is_active, full_name, email, mobile, address, force_password_change, last_updated_date ) VALUES ($1,$2,$3,$4,$5,$6,true,NOW()) RETURNING id `, payload.Code, payload.IsActive, payload.FullName, payload.Email, payload.Mobile, payload.Address, ).Scan(&newID) if err != nil { log.Println("USER INSERT ERROR:", err) http.Error(w, "Kullanıcı oluşturulamadı", http.StatusInternalServerError) return } // ROLES for _, role := range payload.Roles { _, _ = tx.Exec(queries.InsertUserRole, newID, role) } // DEPARTMENTS for _, d := range payload.Departments { _, _ = tx.Exec(queries.InsertUserDepartment, newID, d.Code) } // PIYASALAR for _, p := range payload.Piyasalar { _, _ = tx.Exec(queries.InsertUserPiyasa, newID, p.Code) } // NEBIM for _, n := range payload.NebimUsers { _, _ = tx.Exec(queries.InsertUserNebim, newID, n.Username) } if err := tx.Commit(); err != nil { http.Error(w, "Commit başarısız", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(map[string]any{ "success": true, "id": newID, }) } } // ====================================================== // 🔐 ROLES LIST // GET /api/roles // ====================================================== func GetRolesHandler(db *sql.DB) http.HandlerFunc { type RoleRow struct { ID int64 `json:"id"` Code string `json:"code"` Title string `json:"title"` } return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") rows, err := db.Query(queries.GetRoles) if err != nil { http.Error(w, "roles query error", http.StatusInternalServerError) return } defer rows.Close() var list []RoleRow for rows.Next() { var x RoleRow if err := rows.Scan(&x.ID, &x.Code, &x.Title); err == nil { list = append(list, x) } } _ = json.NewEncoder(w).Encode(list) } } // ====================================================== // 🏢 DEPARTMENTS LIST // GET /api/departments // ====================================================== func GetDepartmentsHandler(db *sql.DB) http.HandlerFunc { type DeptRow struct { ID int64 `json:"id"` Code string `json:"code"` Title string `json:"title"` } return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") rows, err := db.Query(queries.GetDepartments) if err != nil { http.Error(w, "departments query error", http.StatusInternalServerError) return } defer rows.Close() var list []DeptRow for rows.Next() { var x DeptRow if err := rows.Scan(&x.ID, &x.Code, &x.Title); err == nil { list = append(list, x) } } _ = json.NewEncoder(w).Encode(list) } } // ====================================================== // 🌍 PIYASALAR LIST // GET /api/piyasalar // ====================================================== func GetPiyasalarHandler(db *sql.DB) http.HandlerFunc { type PiyRow struct { Code string `json:"code"` Title string `json:"title"` } return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") rows, err := db.Query(queries.GetPiyasalar) if err != nil { http.Error(w, "piyasalar query error", http.StatusInternalServerError) return } defer rows.Close() var list []PiyRow for rows.Next() { var x PiyRow if err := rows.Scan(&x.Code, &x.Title); err == nil { list = append(list, x) } } _ = json.NewEncoder(w).Encode(list) } } // ====================================================== // 🔗 ROLE → DEPARTMENTS UPDATE // POST /api/roles/{id}/departments // body: { "codes": ["D01","D02"] } // ====================================================== type CodesPayload struct { Codes []string `json:"codes"` } func UpdateRoleDepartmentsHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") roleIDStr := mux.Vars(r)["id"] roleID, err := strconv.ParseInt(roleIDStr, 10, 64) if err != nil || roleID <= 0 { http.Error(w, "Geçersiz role id", http.StatusBadRequest) return } var payload CodesPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Geçersiz payload", http.StatusBadRequest) return } tx, err := db.Begin() if err != nil { http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) return } defer tx.Rollback() // reset if _, err := tx.Exec(queries.DeleteRoleDepartments, roleID); err != nil { http.Error(w, "Role departments silinemedi", http.StatusInternalServerError) return } // insert new for _, code := range payload.Codes { code = strings.TrimSpace(code) if code == "" { continue } if _, err := tx.Exec(queries.InsertRoleDepartment, roleID, code); err != nil { http.Error(w, "Role department eklenemedi", http.StatusInternalServerError) return } } if err := tx.Commit(); err != nil { http.Error(w, "Commit başarısız", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(map[string]any{"success": true}) } } // ====================================================== // 🔗 ROLE → PIYASALAR UPDATE // POST /api/roles/{id}/piyasalar // body: { "codes": ["TR","EU"] } (piyasa_code list) // ====================================================== func UpdateRolePiyasalarHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") roleIDStr := mux.Vars(r)["id"] roleID, err := strconv.ParseInt(roleIDStr, 10, 64) if err != nil || roleID <= 0 { http.Error(w, "Geçersiz role id", http.StatusBadRequest) return } var payload CodesPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Geçersiz payload", http.StatusBadRequest) return } tx, err := db.Begin() if err != nil { http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) return } defer tx.Rollback() // reset if _, err := tx.Exec(queries.DeleteRolePiyasalar, roleID); err != nil { http.Error(w, "Role piyasalar silinemedi", http.StatusInternalServerError) return } // insert new for _, code := range payload.Codes { code = strings.TrimSpace(code) if code == "" { continue } if _, err := tx.Exec(queries.InsertRolePiyasa, roleID, code); err != nil { http.Error(w, "Role piyasa eklenemedi", http.StatusInternalServerError) return } } if err := tx.Commit(); err != nil { http.Error(w, "Commit başarısız", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(map[string]any{"success": true}) } } // ====================================================== // 👤 USER → ROLES UPDATE // POST /api/users/{id}/roles // body: { "codes": ["admin","user"] } (dfrole.code list) // ====================================================== func UpdateUserRolesHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") // ---------- CLAIMS ---------- claims, ok := auth.GetClaimsFromContext(r.Context()) if !ok || !claims.IsAdmin() { http.Error(w, "forbidden", http.StatusForbidden) return } // ---------- TARGET USER ---------- userIDStr := mux.Vars(r)["id"] userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil || userID <= 0 { http.Error(w, "Geçersiz user id", http.StatusBadRequest) return } // ---------- PAYLOAD ---------- var payload CodesPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Geçersiz payload", http.StatusBadRequest) return } // ---------- BEFORE (ROLE LIST) ---------- oldRoles, _ := repository.GetUserRolesByUserID(db, userID) // ---------- TX ---------- tx, err := db.Begin() if err != nil { http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) return } defer tx.Rollback() // reset if _, err := tx.Exec(queries.DeleteUserRoles, userID); err != nil { http.Error(w, "User roles silinemedi", http.StatusInternalServerError) return } // insert new for _, roleCode := range payload.Codes { roleCode = strings.TrimSpace(roleCode) if roleCode == "" { continue } if _, err := tx.Exec(queries.InsertUserRole, userID, roleCode); err != nil { http.Error(w, "User role eklenemedi", http.StatusInternalServerError) return } } if err := tx.Commit(); err != nil { http.Error(w, "Commit başarısız", http.StatusInternalServerError) return } // ---------- AFTER (ROLE LIST) ---------- newRoles, _ := repository.GetUserRolesByUserID(db, userID) // ---------- AUDIT ---------- auditlog.Enqueue(r.Context(), auditlog.ActivityLog{ ActionType: "role_change", ActionCategory: "user_admin", ActionTarget: fmt.Sprintf("/api/users/%d/roles", userID), Description: "user roles updated", Username: claims.Username, RoleCode: claims.RoleCode, DfUsrID: int64(claims.ID), TargetDfUsrID: userID, ChangeBefore: map[string]any{ "roles": oldRoles, }, ChangeAfter: map[string]any{ "roles": newRoles, }, IsSuccess: true, }) _ = json.NewEncoder(w).Encode(map[string]any{ "success": true, }) } }