package main import ( "bssapp-backend/db" "bssapp-backend/internal/auditlog" "bssapp-backend/internal/mailer" "bssapp-backend/middlewares" "bssapp-backend/permissions" "bssapp-backend/repository" "bssapp-backend/routes" "database/sql" "log" "net/http" "os" "path" "path/filepath" "runtime/debug" "strings" "github.com/gorilla/mux" "github.com/joho/godotenv" ) /* =========================================================== ✅ CORS =========================================================== */ func enableCORS(h http.Handler) http.Handler { frontendURL := os.Getenv("APP_FRONTEND_URL") // Default fallback (dev için) if frontendURL == "" { frontendURL = "http://localhost:9000" } log.Println("🌍 CORS Allowed Origin:", frontendURL) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") // Sadece izin verilen origin'e cevap ver if origin == frontendURL { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Vary", "Origin") w.Header().Set("Access-Control-Allow-Credentials", "true") } w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") // Preflight if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } h.ServeHTTP(w, r) }) } /* =========================================================== ✅ V3 — Method-aware Route Auto Register - mk_sys_routes: (path, method, module_code, action) - unique: (path, method) - admin auto-allow: mk_sys_role_permissions (role_id=3) =========================================================== */ func autoRegisterRouteV3( pg *sql.DB, path string, method string, module string, action string, ) { tx, err := pg.Begin() if err != nil { log.Println("❌ TX begin error:", err) return } defer tx.Rollback() // 1) ROUTE REGISTER (path+method) _, err = tx.Exec(` INSERT INTO mk_sys_routes (path, method, module_code, action) VALUES ($1, $2, $3, $4) ON CONFLICT (path, method) DO UPDATE SET module_code = EXCLUDED.module_code, action = EXCLUDED.action `, path, method, module, action, ) if err != nil { log.Printf("❌ Route register error (%s %s): %v", method, path, err) return } // 2) ADMIN AUTO PERMISSION (module+action bazlı) _, err = tx.Exec(` INSERT INTO mk_sys_role_permissions (role_id, module_code, action, allowed) SELECT id, $1, $2, true FROM dfrole WHERE id = 3 -- ADMIN ON CONFLICT DO NOTHING `, module, action, ) if err != nil { log.Printf("❌ Admin perm seed error (%s %s): %v", method, path, err) return } if err := tx.Commit(); err != nil { log.Println("❌ TX commit error:", err) return } log.Printf("✅ Route+Perm registered → %s %s [%s:%s]", method, path, module, action, ) } /* =========================================================== ✅ V3 Route Bind Helper - tek satırda: autoRegister + wrap + Handle =========================================================== */ func bindV3( r *mux.Router, pg *sql.DB, path string, method string, module string, action string, h http.Handler, ) { // main autoRegisterRouteV3(pg, path, method, module, action) r.Handle(path, h).Methods(method, "OPTIONS") } /* =========================================================== InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION =========================================================== */ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router { r := mux.NewRouter() mountSPA(r) /* =========================================================== ✅ wrapV3 (method-aware): - AuthMiddleware (JWT) - panic recover - AuthzGuardByRoute(pgDB) => route(path+method) lookup =========================================================== */ wrapV3 := func(h http.Handler) http.Handler { return middlewares.AuthMiddleware( pgDB, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { log.Printf("🔥 PANIC %s %s\n%v", r.Method, r.URL.Path, rec) debug.PrintStack() http.Error(w, "internal server error", 500) } }() // ✅ method-aware route guard middlewares.AuthzGuardByRoute(pgDB)(h).ServeHTTP(w, r) }), ) } // ============================================================ // PUBLIC (NO AUTHZ) // ============================================================ bindV3(r, pgDB, "/api/auth/login", "POST", "auth", "login", http.HandlerFunc(routes.LoginHandler(pgDB)), ) bindV3(r, pgDB, "/api/auth/refresh", "POST", "auth", "refresh", routes.AuthRefreshHandler(pgDB), ) // ============================================================ // SYSTEM // ============================================================ bindV3(r, pgDB, "/api/password/change", "POST", "auth", "update", wrapV3(http.HandlerFunc(routes.FirstPasswordChangeHandler(pgDB))), ) bindV3(r, pgDB, "/api/activity-logs", "GET", "system", "read", wrapV3(routes.AdminActivityLogsHandler(pgDB)), ) bindV3(r, pgDB, "/api/test-mail", "POST", "system", "update", wrapV3(routes.TestMailHandler(ml)), ) // ============================================================ // PERMISSIONS // ============================================================ rolePerm := "/api/roles/{id}/permissions" bindV3(r, pgDB, rolePerm, "GET", "system", "update", wrapV3(routes.GetRolePermissionMatrix(pgDB)), ) bindV3(r, pgDB, rolePerm, "POST", "system", "update", wrapV3(routes.SaveRolePermissionMatrix(pgDB)), ) userPerm := "/api/users/{id}/permissions" bindV3(r, pgDB, userPerm, "GET", "system", "update", wrapV3(routes.GetUserPermissionsHandler(pgDB)), ) bindV3(r, pgDB, userPerm, "POST", "system", "update", wrapV3(routes.SaveUserPermissionsHandler(pgDB)), ) // ✅ permissions/routes (system:view) bindV3(r, pgDB, "/api/permissions/routes", "GET", "system", "view", wrapV3(routes.GetUserRoutePermissionsHandler(pgDB)), ) // ✅ permissions/effective (system:view) bindV3(r, pgDB, "/api/permissions/effective", "GET", "system", "view", wrapV3(routes.GetMyEffectivePermissions(pgDB)), ) // ✅ my permission matrix (system:view) (Senin tabloda user:view görünüyor; düzeltiyoruz) bindV3(r, pgDB, "/api/permissions/matrix", "GET", "system", "view", wrapV3(routes.GetMyPermissionMatrix(pgDB)), ) // ============================================================ // ROLE + DEPARTMENT PERMISSIONS // ============================================================ rdPerm := "/api/roles/{roleId}/departments/{deptCode}/permissions" rdHandler := routes.NewRoleDepartmentPermissionHandler(pgDB) bindV3(r, pgDB, "/api/role-dept-permissions/list", "GET", "system", "update", wrapV3(http.HandlerFunc(rdHandler.List)), ) bindV3(r, pgDB, rdPerm, "GET", "system", "update", wrapV3(http.HandlerFunc(rdHandler.Get)), ) bindV3(r, pgDB, rdPerm, "POST", "system", "update", wrapV3(http.HandlerFunc(rdHandler.Save)), ) // ============================================================ // USERS // ============================================================ bindV3(r, pgDB, "/api/users/list", "GET", "user", "view", wrapV3(routes.UserListRoute(pgDB)), ) bindV3(r, pgDB, "/api/users", "POST", "user", "insert", wrapV3(routes.UserCreateRoute(pgDB)), ) bindV3(r, pgDB, "/api/users/{id}", "GET", "user", "update", wrapV3(routes.UserDetailRoute(pgDB)), ) bindV3(r, pgDB, "/api/users/{id}", "PUT", "user", "update", wrapV3(routes.UserDetailRoute(pgDB)), ) bindV3(r, pgDB, "/api/users/{id}", "DELETE", "user", "delete", wrapV3(routes.UserDetailRoute(pgDB)), ) bindV3(r, pgDB, "/api/users/{id}/admin-reset-password", "POST", "user", "update", wrapV3(routes.AdminResetPasswordHandler(pgDB)), ) bindV3(r, pgDB, "/api/users/{id}/send-password-mail", "POST", "user", "update", wrapV3(routes.SendPasswordResetMailHandler(pgDB, ml)), ) // ✅ eski kısayol create endpoint (senin eski main.go’da vardı) bindV3(r, pgDB, "/api/users/create", "POST", "user", "insert", wrapV3(routes.UserCreateRoute(pgDB)), ) // ============================================================ // LOOKUPS // ============================================================ lookups := map[string]http.Handler{ "/api/lookups/roles": routes.GetRoleLookupRoute(pgDB), "/api/lookups/departments": routes.GetDepartmentLookupRoute(pgDB), "/api/lookups/nebim-users": routes.GetNebimUserLookupRoute(pgDB), "/api/lookups/piyasalar": routes.GetPiyasaLookupRoute(pgDB), "/api/lookups/users-perm": routes.GetUsersForPermissionSelectRoute(pgDB), "/api/lookups/roles-perm": routes.GetRolesForPermissionSelectRoute(pgDB), "/api/lookups/departments-perm": routes.GetDepartmentsForPermissionSelectRoute(pgDB), "/api/lookups/modules": routes.GetModuleLookupRoute(pgDB), } for path, handler := range lookups { bindV3(r, pgDB, path, "GET", "user", "view", wrapV3(handler), ) } // ============================================================ // CUSTOMER // ============================================================ bindV3(r, pgDB, "/api/accounts", "GET", "customer", "view", wrapV3(http.HandlerFunc(routes.GetAccountsHandler)), ) bindV3(r, pgDB, "/api/customer-list", "GET", "customer", "view", wrapV3(http.HandlerFunc(routes.GetCustomerListHandler)), ) // ============================================================ // FINANCE // ============================================================ bindV3(r, pgDB, "/api/today-currency", "GET", "finance", "view", wrapV3(routes.GetTodayCurrencyV3Handler(mssql)), ) bindV3(r, pgDB, "/api/export-pdf", "GET", "finance", "export", wrapV3(routes.ExportPDFHandler(mssql)), ) bindV3(r, pgDB, "/api/exportstamentheaderreport-pdf", "GET", "finance", "export", wrapV3(routes.ExportStatementHeaderReportPDFHandler(mssql)), ) // ============================================================ // REPORT (STATEMENTS) // ============================================================ bindV3(r, pgDB, "/api/statements", "GET", "finance", "view", wrapV3(http.HandlerFunc(routes.GetStatementHeadersHandler)), ) // ⚠️ Senin handler: GetStatementDetailsHandler vars["accountCode"] bekliyor, // route’da {id} var. Burada, DB route’unu ve path’i bozmayalım diye {id}’yi koruyorum, // ama handler içinde accountCode := mux.Vars(r)["id"] yapman daha doğru. bindV3(r, pgDB, "/api/statements/{id}/details", "GET", "finance", "view", wrapV3(http.HandlerFunc(routes.GetStatementDetailsHandler)), ) // ============================================================ // ORDER // ============================================================ orderRoutes := []struct { Path string Method string Action string Handle http.Handler }{ {"/api/order/create", "POST", "insert", routes.CreateOrderHandler(pgDB, mssql)}, {"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)}, {"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)}, {"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)}, {"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)}, {"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)}, {"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)}, {"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)}, {"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)}, {"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(mssql)}, {"/api/order-inventory", "GET", "view", http.HandlerFunc(routes.GetOrderInventoryHandler)}, {"/api/orderpricelistb2b", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)}, {"/api/min-price", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)}, } for _, rt := range orderRoutes { bindV3(r, pgDB, rt.Path, rt.Method, "order", rt.Action, wrapV3(rt.Handle), ) } // ============================================================ // PRODUCTS (✅ handler mapping fix) // ============================================================ bindV3(r, pgDB, "/api/products", "GET", "order", "view", wrapV3(http.HandlerFunc(routes.GetProductListHandler)), ) bindV3(r, pgDB, "/api/product-detail", "GET", "order", "view", wrapV3(http.HandlerFunc(routes.GetProductDetailHandler)), ) bindV3(r, pgDB, "/api/product-colors", "GET", "order", "view", wrapV3(http.HandlerFunc(routes.GetProductColorsHandler)), ) bindV3(r, pgDB, "/api/product-colorsize", "GET", "order", "view", wrapV3(http.HandlerFunc(routes.GetProductColorSizesHandler)), ) bindV3(r, pgDB, "/api/product-secondcolor", "GET", "order", "view", wrapV3(http.HandlerFunc(routes.GetProductSecondColorsHandler)), ) // ============================================================ // ROLE MANAGEMENT // ============================================================ bindV3(r, pgDB, "/api/roles", "GET", "user", "view", wrapV3(routes.GetRolesHandler(pgDB)), ) bindV3(r, pgDB, "/api/departments", "GET", "user", "view", wrapV3(routes.GetDepartmentsHandler(pgDB)), ) bindV3(r, pgDB, "/api/piyasalar", "GET", "user", "view", wrapV3(routes.GetPiyasalarHandler(pgDB)), ) // ============================================================ // ROLE RELATIONS // ============================================================ bindV3(r, pgDB, "/api/roles/{id}/departments", "POST", "user", "update", wrapV3(routes.UpdateRoleDepartmentsHandler(pgDB)), ) bindV3(r, pgDB, "/api/roles/{id}/piyasalar", "POST", "user", "update", wrapV3(routes.UpdateRolePiyasalarHandler(pgDB)), ) // ============================================================ // USER ↔ ROLE // ============================================================ bindV3(r, pgDB, "/api/users/{id}/roles", "POST", "user", "update", wrapV3(routes.UpdateUserRolesHandler(pgDB)), ) // ============================================================ // ADMIN EXTRA (eski main.go’da vardı, yeni sisteme alındı) // ============================================================ bindV3(r, pgDB, "/api/admin/users/{id}/piyasa-sync", "POST", "admin", "user.update", wrapV3(http.HandlerFunc(routes.AdminSyncUserPiyasaHandler)), ) return r } func main() { log.Println("🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥") // ------------------------------------------------------- // 🔑 ENV // ------------------------------------------------------- if err := godotenv.Load(".env", "mail.env"); err != nil { log.Println("⚠️ .env / mail.env bulunamadı") } jwtSecret := os.Getenv("JWT_SECRET") if len(jwtSecret) < 10 { log.Fatal("❌ JWT_SECRET tanımlı değil veya çok kısa (min 10 karakter)") } log.Println("🔐 JWT_SECRET yüklendi") // ------------------------------------------------------- // 🔗 DATABASE // ------------------------------------------------------- db.ConnectMSSQL() pgDB, err := db.ConnectPostgres() if err != nil { log.Fatalf("❌ PostgreSQL bağlantı hatası: %v", err) } defer pgDB.Close() // ------------------------------------------------------- // 🔐 ADMIN ROLE + DEPARTMENT AUTO SEED // ------------------------------------------------------- if err := permissions.SeedAdminRoleDepartments(pgDB); err != nil { log.Println("❌ Admin dept seed failed:", err) } else { log.Println("✅ Admin dept permissions seeded") } // ------------------------------------------------------- // 🕵️ AUDIT LOG INIT // ------------------------------------------------------- auditlog.Init(pgDB, 1000) log.Println("🕵️ AuditLog sistemi başlatıldı (buffer=1000)") // ------------------------------------------------------- // ✉️ MAILER INIT // ------------------------------------------------------- graphMailer, err := mailer.NewGraphMailer() if err != nil { log.Fatalf("❌ Graph Mailer init error: %v", err) } log.Println("✉️ Graph Mailer hazır") // ------------------------------------------------------- // 👤 DEBUG // ------------------------------------------------------- repository.NewUserRepository(pgDB).DebugListUsers() // ------------------------------------------------------- // 🌍 SERVER // ------------------------------------------------------- router := InitRoutes(pgDB, db.MssqlDB, graphMailer) handler := enableCORS( middlewares.GlobalAuthMiddleware( pgDB, middlewares.RequestLogger(router), ), ) host := strings.TrimSpace(os.Getenv("API_HOST")) port := strings.TrimSpace(os.Getenv("API_PORT")) if host == "" { host = "0.0.0.0" } if port == "" { port = "8080" } addr := host + ":" + port log.Println("🚀 Server running at:", addr) log.Fatal(http.ListenAndServe(addr, handler)) } func mountSPA(r *mux.Router) { r.NotFoundHandler = http.HandlerFunc(spaIndex) r.HandleFunc("/", spaIndex).Methods(http.MethodGet) } func spaIndex(w http.ResponseWriter, r *http.Request) { uiDir := uiRootDir() p := r.URL.Path if r.URL.Path == "/logo.png" { _, err := os.Stat("./logo.png") if err == nil { http.ServeFile(w, r, "./logo.png") return } } if !strings.HasPrefix(p, "/") { p = "/" + p r.URL.Path = p } p = path.Clean(p) if p == "/" { p = "index.html" } if strings.HasPrefix(p, "/api") { http.NotFound(w, r) return } name := path.Join(uiDir, filepath.FromSlash(p)) f, err := os.Stat(name) if err != nil { if os.IsNotExist(err) { http.ServeFile(w, r, filepath.Join(uiDir, "index.html")) return } http.Error(w, "internal server error", http.StatusInternalServerError) return } if f.IsDir() { http.Error(w, "forbidden", http.StatusForbidden) return } http.ServeFile(w, r, name) } func uiRootDir() string { if d := strings.TrimSpace(os.Getenv("UI_DIR")); d != "" { return d } candidates := []string{ "../ui/dist/spa", "./ui/dist/spa", "../ui/dist", "./ui/dist", } for _, d := range candidates { if fi, err := os.Stat(d); err == nil && fi.IsDir() { return d } } return "../ui/dist/spa" }