Files
bssapp/svc/routes/login.go
2026-02-18 13:51:34 +03:00

743 lines
19 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})
}
}