684 lines
18 KiB
Go
684 lines
18 KiB
Go
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}/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),
|
||
),
|
||
)
|
||
|
||
log.Println("✅ Server çalışıyor: http://localhost:8080")
|
||
log.Fatal(http.ListenAndServe(":8080", 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"
|
||
}
|