335 lines
8.1 KiB
Go
335 lines
8.1 KiB
Go
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/<id>/<session>/ 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)
|
|
}
|
|
}
|