Files
bssapp/svc/main.go
2026-02-17 13:30:04 +03:00

697 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
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 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",
"system", "update",
wrapV3(http.HandlerFunc(routes.FirstPasswordChangeHandler(pgDB))),
)
bindV3(r, pgDB,
"/api/activity-logs", "GET",
"user", "view",
wrapV3(routes.AdminActivityLogsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/test-mail", "POST",
"user", "insert",
wrapV3(routes.TestMailHandler(ml)),
)
// ============================================================
// PERMISSIONS
// ============================================================
rolePerm := "/api/roles/{id}/permissions"
bindV3(r, pgDB,
rolePerm, "GET",
"user", "update",
wrapV3(routes.GetRolePermissionMatrix(pgDB)),
)
bindV3(r, pgDB,
rolePerm, "POST",
"user", "update",
wrapV3(routes.SaveRolePermissionMatrix(pgDB)),
)
userPerm := "/api/users/{id}/permissions"
bindV3(r, pgDB,
userPerm, "GET",
"user", "update",
wrapV3(routes.GetUserPermissionsHandler(pgDB)),
)
bindV3(r, pgDB,
userPerm, "POST",
"user", "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",
"user", "update",
wrapV3(http.HandlerFunc(rdHandler.List)),
)
bindV3(r, pgDB,
rdPerm, "GET",
"user", "update",
wrapV3(http.HandlerFunc(rdHandler.Get)),
)
bindV3(r, pgDB,
rdPerm, "POST",
"user", "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.goda 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,
// routeda {id} var. Burada, DB routeunu ve pathi 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.goda 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),
),
)
port := strings.TrimSpace(os.Getenv("HTTPPORT"))
if port == "" {
port = "8080"
}
if !strings.HasPrefix(port, ":") {
port = ":" + port
}
log.Println("Server calisiyor: http://localhost" + port)
log.Fatal(http.ListenAndServe(port, 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", "./ui/dist"}
for _, d := range candidates {
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
return d
}
}
return "../ui/dist"
}