Files
bssapp/svc/main.go
2026-04-14 16:34:25 +03:00

876 lines
23 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)
}),
)
}
// 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)),
)
// ============================================================
// 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)
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"
}