package main import ( "fmt" "log" "os" "path/filepath" "regexp" "strings" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" ) var safePathTokenRe = regexp.MustCompile(`[^a-zA-Z0-9._-]+`) func safePathToken(s string) string { s = strings.TrimSpace(s) s = strings.ReplaceAll(s, string(filepath.Separator), "_") // In case the client sent URL-like separators. s = strings.ReplaceAll(s, "/", "_") s = strings.ReplaceAll(s, "\\", "_") s = safePathTokenRe.ReplaceAllString(s, "_") s = strings.Trim(s, "._-") if s == "" || s == "." { return "" } if len(s) > 128 { s = s[:128] } return s } func safeFilename(name string) string { name = strings.TrimSpace(name) name = filepath.Base(name) if name == "" || name == "." || name == ".." { return "" } // Disallow any remaining separators just in case. name = strings.ReplaceAll(name, string(filepath.Separator), "_") name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, "\\", "_") if name == "" || name == "." || name == ".." { return "" } if len(name) > 255 { name = name[:255] } return name } func main() { port := os.Getenv("PORT") if port == "" { port = "8082" } app := fiber.New(fiber.Config{ DisableStartupMessage: true, }) app.Use(recover.New()) app.Use(logger.New()) app.Use(cors.New(cors.Config{ AllowOrigins: "*", AllowHeaders: "Origin, Content-Type, Accept, Authorization", AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS", })) // Ensure preflight requests are handled. app.Options("/*", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) }) app.Get("/health", func(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).SendString("ok") }) // Serve built SPA from disk (no embedding). // Expected layout: // server/static/index.html // server/static/assets/... app.Static("/", "./static") app.Post("/upload", func(c *fiber.Ctx) error { form, err := c.MultipartForm() if err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid multipart form") } user := safePathToken(c.FormValue("user")) session := safePathToken(c.FormValue("session")) prop := safePathToken(c.FormValue("prop")) if user == "" { return fiber.NewError(fiber.StatusBadRequest, "missing form field: user") } if session == "" { return fiber.NewError(fiber.StatusBadRequest, "missing form field: session") } files := form.File["documents"] if len(files) == 0 { return fiber.NewError(fiber.StatusBadRequest, "missing files: documents") } dir := filepath.Join("docs", user, session) if prop != "" { dir = filepath.Join(dir, prop) } if err := os.MkdirAll(dir, 0o755); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "cannot create upload directory") } uploaded := make([]string, 0, len(files)) for _, file := range files { name := safeFilename(file.Filename) if name == "" { return fiber.NewError(fiber.StatusBadRequest, "invalid filename") } dst := filepath.Join(dir, name) if err := c.SaveFile(file, dst); err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("cannot save file %s", name)) } uploaded = append(uploaded, name) } return c.JSON(fiber.Map{ "ok": true, "files": uploaded, }) }) app.Post("/loadattachments", func(c *fiber.Ctx) error { // Accept either JSON body { id, session } or form fields. var body struct { ID string `json:"id"` Session string `json:"session"` Prop string `json:"prop"` } _ = c.BodyParser(&body) id := safePathToken(body.ID) if id == "" { id = safePathToken(c.FormValue("id")) } session := safePathToken(body.Session) if session == "" { session = safePathToken(c.FormValue("session")) } prop := safePathToken(body.Prop) if prop == "" { prop = safePathToken(c.FormValue("prop")) } if id == "" { return fiber.NewError(fiber.StatusBadRequest, "missing field: id") } if session == "" { return fiber.NewError(fiber.StatusBadRequest, "missing field: session") } dir := filepath.Join("docs", id, session) if prop != "" { dir = filepath.Join(dir, prop) } entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return c.JSON([]string{}) } return fiber.NewError(fiber.StatusInternalServerError, "cannot read attachments directory") } files := make([]string, 0, len(entries)) for _, e := range entries { if e.IsDir() { continue } name := e.Name() if name == "" || name == "." { continue } files = append(files, name) } return c.JSON(files) }) app.Post("/loadattachmentslist", func(c *fiber.Ctx) error { // Returns a JSON object keyed by the actual subfolder name. // Example: { "salaryCertificate": ["file1.pdf"], "avsCertificate": ["file2.pdf"] } // It scans docs/// and for each subdirectory lists its files. var body struct { ID string `json:"id"` Session string `json:"session"` } _ = c.BodyParser(&body) id := safePathToken(body.ID) if id == "" { id = safePathToken(c.FormValue("id")) } session := safePathToken(body.Session) if session == "" { session = safePathToken(c.FormValue("session")) } if id == "" { return fiber.NewError(fiber.StatusBadRequest, "missing field: id") } if session == "" { return fiber.NewError(fiber.StatusBadRequest, "missing field: session") } baseDir := filepath.Join("docs", id, session) entries, err := os.ReadDir(baseDir) if err != nil { if os.IsNotExist(err) { return c.JSON(fiber.Map{}) } return fiber.NewError(fiber.StatusInternalServerError, "cannot read session directory") } result := make(map[string][]string) for _, entry := range entries { if !entry.IsDir() { continue } folderName := entry.Name() if safePathToken(folderName) == "" { continue } propDir := filepath.Join(baseDir, folderName) propEntries, err := os.ReadDir(propDir) if err != nil { // Ignore unreadable prop folder. continue } files := make([]string, 0, len(propEntries)) for _, pe := range propEntries { if pe.IsDir() { continue } name := pe.Name() if name == "" || name == "." { continue } files = append(files, name) } result[folderName] = files } return c.JSON(result) }) app.Post("/deleteattachment", func(c *fiber.Ctx) error { // Accept either JSON body { id, session, filename } or form fields. var body struct { ID string `json:"id"` Session string `json:"session"` Prop string `json:"prop"` Filename string `json:"filename"` } _ = c.BodyParser(&body) id := safePathToken(body.ID) if id == "" { id = safePathToken(c.FormValue("id")) } session := safePathToken(body.Session) if session == "" { session = safePathToken(c.FormValue("session")) } prop := safePathToken(body.Prop) if prop == "" { prop = safePathToken(c.FormValue("prop")) } filename := safeFilename(body.Filename) if filename == "" { filename = safeFilename(c.FormValue("filename")) } if id == "" { return fiber.NewError(fiber.StatusBadRequest, "missing field: id") } if session == "" { return fiber.NewError(fiber.StatusBadRequest, "missing field: session") } if filename == "" { return fiber.NewError(fiber.StatusBadRequest, "missing field: filename") } path := filepath.Join("docs", id, session) if prop != "" { path = filepath.Join(path, prop) } path = filepath.Join(path, filename) err := os.Remove(path) if err != nil { if os.IsNotExist(err) { return c.JSON(fiber.Map{ "ok": true, "deleted": false, "file": filename, }) } return fiber.NewError(fiber.StatusInternalServerError, "cannot delete attachment") } return c.JSON(fiber.Map{ "ok": true, "deleted": true, "file": filename, }) }) // History API fallback for SPA routes. app.Use(func(c *fiber.Ctx) error { return c.SendFile("./static/index.html") }) addr := ":" + port log.Printf("Starting Fiber server on %s", addr) if err := app.Listen(addr); err != nil { log.Fatalf("server failed: %v", err) } }