Files
bssapp/svc/main.go
2026-04-16 15:18:44 +03:00

965 lines
26 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) MODULE LOOKUP AUTO SEED (permission ekranları için)
moduleLabel := strings.TrimSpace(strings.ReplaceAll(module, "_", " "))
if moduleLabel == "" {
moduleLabel = module
}
_, err = tx.Exec(`
INSERT INTO mk_sys_modules (code, name)
VALUES ($1::text, $2::text)
ON CONFLICT (code) DO UPDATE
SET name = COALESCE(NULLIF(EXCLUDED.name, ''), mk_sys_modules.name)
`,
module,
moduleLabel,
)
if err != nil {
log.Printf("❌ Module seed error (%s %s): %v", method, path, err)
return
}
// 3) ROLE PERMISSION AUTO SEED (admin=true, diğer roller=false)
_, err = tx.Exec(`
INSERT INTO mk_sys_role_permissions
(role_id, module_code, action, allowed)
SELECT
id,
$1,
$2,
CASE
WHEN id = 3 OR LOWER(code) = 'admin' THEN true
ELSE false
END
FROM dfrole
ON CONFLICT DO NOTHING
`,
module,
action,
)
if err != nil {
log.Printf("❌ Role perm seed error (%s %s): %v", method, path, err)
return
}
// 4) ROLE+DEPARTMENT PERMISSION AUTO SEED
// Existing role+department kombinasyonlarına yeni module+action satırıılır.
_, err = tx.Exec(`
WITH role_dept_scope AS (
SELECT DISTINCT role_id, department_code
FROM mk_sys_role_department_permissions
UNION
SELECT 3 AS role_id, d.code AS department_code
FROM mk_dprt d
)
INSERT INTO mk_sys_role_department_permissions
(role_id, department_code, module_code, action, allowed)
SELECT
rds.role_id,
rds.department_code,
$1,
$2,
CASE
WHEN rds.role_id = 3 THEN true
ELSE false
END
FROM role_dept_scope rds
ON CONFLICT DO NOTHING
`,
module,
action,
)
if err != nil {
log.Printf("❌ Role+Dept 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)
}),
)
}
// Sadece JWT doğrulaması; route-level yetki kontrolü yok.
wrapAuthOnly := func(h http.Handler) http.Handler {
return middlewares.AuthMiddleware(pgDB, h)
}
// ============================================================
// 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),
)
// Password reset flow (public)
bindV3(r, pgDB,
"/api/password/forgot", "POST",
"auth", "update",
routes.ForgotPasswordHandler(pgDB, ml),
)
bindV3(r, pgDB,
"/api/password/reset/validate/{token}", "GET",
"auth", "view",
routes.ValidatePasswordResetTokenHandler(pgDB),
)
bindV3(r, pgDB,
"/api/password/reset", "POST",
"auth", "update",
routes.CompletePasswordResetHandler(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)),
)
bindV3(r, pgDB,
"/api/system/market-mail-mappings/lookups", "GET",
"system", "update",
wrapV3(routes.GetMarketMailMappingLookupsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/system/market-mail-mappings", "GET",
"system", "update",
wrapV3(routes.GetMarketMailMappingsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/system/market-mail-mappings/{marketId}", "PUT",
"system", "update",
wrapV3(routes.SaveMarketMailMappingHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/language/translations", "GET",
"language", "update",
wrapV3(routes.GetTranslationRowsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/language/translations/{id}", "PUT",
"language", "update",
wrapV3(routes.UpdateTranslationRowHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/language/translations/upsert-missing", "POST",
"language", "update",
wrapV3(routes.UpsertMissingTranslationsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/language/translations/sync-sources", "POST",
"language", "update",
wrapV3(routes.SyncTranslationSourcesHandler(pgDB, mssql)),
)
bindV3(r, pgDB,
"/api/language/translations/translate-selected", "POST",
"language", "update",
wrapV3(routes.TranslateSelectedTranslationsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/language/translations/bulk-approve", "POST",
"language", "update",
wrapV3(routes.BulkApproveTranslationsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/language/translations/bulk-update", "POST",
"language", "update",
wrapV3(routes.BulkUpdateTranslationsHandler(pgDB)),
)
// ============================================================
// 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.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)),
)
bindV3(r, pgDB,
"/api/finance/customer-balances", "GET",
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetCustomerBalanceListHandler)),
)
bindV3(r, pgDB,
"/api/finance/customer-balances/export-pdf", "GET",
"finance", "export",
wrapV3(routes.ExportCustomerBalancePDFHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/customer-balances/export-excel", "GET",
"finance", "export",
wrapV3(routes.ExportCustomerBalanceExcelHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement", "GET",
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetStatementAgingHandler)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement/export-pdf", "GET",
"finance", "export",
wrapV3(routes.ExportStatementAgingPDFHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement/export-screen-pdf", "GET",
"finance", "export",
wrapV3(routes.ExportStatementAgingScreenPDFHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement/export-excel", "GET",
"finance", "export",
wrapV3(routes.ExportStatementAgingExcelHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/aged-customer-balance-list", "GET",
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetAgedCustomerBalanceListHandler)),
)
// ============================================================
// 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/{id}/bulk-due-date", "POST", "update", routes.BulkUpdateOrderLineDueDateHandler(mssql)},
{"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)},
{"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)},
{"/api/orders/production-list", "GET", "update", routes.OrderProductionListRoute(mssql)},
{"/api/orders/production-items/cditem-lookups", "GET", "view", routes.OrderProductionCdItemLookupsRoute(mssql)},
{"/api/orders/production-items/{id}", "GET", "view", routes.OrderProductionItemsRoute(mssql)},
{"/api/orders/production-items/{id}/insert-missing", "POST", "update", routes.OrderProductionInsertMissingRoute(mssql)},
{"/api/orders/production-items/{id}/validate", "POST", "update", routes.OrderProductionValidateRoute(mssql)},
{"/api/orders/production-items/{id}/apply", "POST", "update", routes.OrderProductionApplyRoute(mssql, ml)},
{"/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, pgDB)},
{"/api/order/send-market-mail", "POST", "read", routes.SendOrderMarketMailHandler(pgDB, mssql, ml)},
{"/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 {
if rt.Path == "/api/order/send-market-mail" {
bindV3(r, pgDB,
rt.Path, rt.Method,
"order", rt.Action,
wrapAuthOnly(rt.Handle),
)
continue
}
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-cditem", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductCdItemHandler)),
)
bindV3(r, pgDB,
"/api/product-colors", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductColorsHandler)),
)
bindV3(r, pgDB,
"/api/product-newcolors", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductNewColorsHandler)),
)
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)),
)
bindV3(r, pgDB,
"/api/product-newsecondcolor", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductNewSecondColorsHandler)),
)
bindV3(r, pgDB,
"/api/product-attributes", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductAttributesHandler)),
)
bindV3(r, pgDB,
"/api/product-item-attributes", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductItemAttributesHandler)),
)
bindV3(r, pgDB,
"/api/product-stock-query", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductStockQueryHandler)),
)
bindV3(r, pgDB,
"/api/product-stock-attribute-options", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductStockAttributeOptionsHandler)),
)
bindV3(r, pgDB,
"/api/product-stock-query-by-attributes", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductStockQueryByAttributesHandler)),
)
bindV3(r, pgDB,
"/api/product-images", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductImagesHandler(pgDB))),
)
bindV3(r, pgDB,
"/api/product-images/{id}/content", "GET",
"order", "view",
http.HandlerFunc(routes.GetProductImageContentHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/product-size-match/rules", "GET",
"order", "view",
wrapV3(routes.GetProductSizeMatchRulesHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/products", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductPricingListHandler)),
)
// ============================================================
// 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
// -------------------------------------------------------
// Önce .env + mail.env yükle. MSSQL başarısızsa .env.local dene.
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
// -------------------------------------------------------
if err := db.ConnectMSSQL(); err != nil {
log.Println("⚠️ MSSQL ilk deneme başarısız:", err)
if err2 := godotenv.Overload(".env.local"); err2 != nil {
log.Println("⚠️ .env.local bulunamadı")
}
if err3 := db.ConnectMSSQL(); err3 != nil {
log.Fatal(err3)
}
}
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)
startTranslationSyncScheduler(pgDB, db.MssqlDB)
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"
}