init push
This commit is contained in:
219
internal/app/api/audit_middleware.go
Normal file
219
internal/app/api/audit_middleware.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
sharedconst "wucher/internal/shared/const"
|
||||
)
|
||||
|
||||
func AuditMiddleware(auditSvc *service.AuditLogService) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if auditSvc == nil {
|
||||
return c.Next()
|
||||
}
|
||||
start := time.Now()
|
||||
err := c.Next()
|
||||
status := c.Response().StatusCode()
|
||||
if status == 0 {
|
||||
status = fiber.StatusOK
|
||||
}
|
||||
|
||||
requestID := c.GetRespHeader(fiber.HeaderXRequestID)
|
||||
if requestID == "" {
|
||||
requestID = localString(c.Locals("requestid"))
|
||||
}
|
||||
|
||||
meta := map[string]any{
|
||||
"latency_ms": time.Since(start).Milliseconds(),
|
||||
}
|
||||
if err != nil {
|
||||
meta["error"] = err.Error()
|
||||
}
|
||||
|
||||
module, action := resolveAuditModuleAction(c)
|
||||
if !shouldPersistAuditByMethod(c.Method()) {
|
||||
return err
|
||||
}
|
||||
|
||||
auditSvc.Log(c.UserContext(), service.AuditLogInput{
|
||||
RequestID: requestID,
|
||||
ActorUserID: localUserID(c.Locals("user_id")),
|
||||
Layer: "api",
|
||||
Module: module,
|
||||
Action: action,
|
||||
Method: c.Method(),
|
||||
Path: c.Path(),
|
||||
StatusCode: status,
|
||||
Success: status >= 200 && status < 400 && err == nil,
|
||||
IP: c.IP(),
|
||||
UserAgent: c.Get(fiber.HeaderUserAgent),
|
||||
Message: "HTTP " + strconv.Itoa(status),
|
||||
Metadata: meta,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func shouldPersistAuditByMethod(method string) bool {
|
||||
switch strings.ToUpper(strings.TrimSpace(method)) {
|
||||
case fiber.MethodPost, fiber.MethodPut, fiber.MethodPatch, fiber.MethodDelete:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func resolveAuditModuleAction(c *fiber.Ctx) (string, string) {
|
||||
method := c.Method()
|
||||
path := c.Path()
|
||||
normalized := strings.ToLower(strings.TrimSpace(path))
|
||||
parts := strings.Split(strings.Trim(normalized, "/"), "/")
|
||||
if len(parts) < 3 || parts[0] != "api" || parts[1] != "v1" {
|
||||
return sharedconst.AuditModuleUnknown, actionFromMethod(method)
|
||||
}
|
||||
moduleSegment := strings.TrimSpace(parts[2])
|
||||
module := moduleFromPathSegment(moduleSegment)
|
||||
if moduleSegment == "bases" {
|
||||
module = resolveBaseModuleVariant(c, module)
|
||||
}
|
||||
suffix := ""
|
||||
if len(parts) > 3 {
|
||||
suffix = strings.TrimSpace(parts[len(parts)-1])
|
||||
}
|
||||
switch suffix {
|
||||
case "get-all", "dt":
|
||||
return module, sharedconst.AuditActionList
|
||||
case "get":
|
||||
return module, sharedconst.AuditActionGet
|
||||
case "create":
|
||||
return module, sharedconst.AuditActionCreate
|
||||
case "update":
|
||||
return module, sharedconst.AuditActionUpdate
|
||||
case "delete":
|
||||
return module, sharedconst.AuditActionDelete
|
||||
case "export":
|
||||
return module, sharedconst.AuditActionExport
|
||||
}
|
||||
return module, actionFromMethod(method)
|
||||
}
|
||||
|
||||
func resolveBaseModuleVariant(c *fiber.Ctx, fallback string) string {
|
||||
raw := strings.TrimSpace(c.Query("category_type"))
|
||||
if raw == "" {
|
||||
raw = strings.TrimSpace(c.Query("base_category"))
|
||||
}
|
||||
if raw == "" {
|
||||
raw = strings.TrimSpace(c.Query("category"))
|
||||
}
|
||||
switch strings.ToLower(raw) {
|
||||
case "regular":
|
||||
return sharedconst.AuditModuleBaseRegular
|
||||
case "hems":
|
||||
return sharedconst.AuditModuleBaseHEMS
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func actionFromMethod(method string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(method)) {
|
||||
case fiber.MethodGet:
|
||||
return sharedconst.AuditActionGet
|
||||
case fiber.MethodPost:
|
||||
return sharedconst.AuditActionCreate
|
||||
case fiber.MethodPut, fiber.MethodPatch:
|
||||
return sharedconst.AuditActionUpdate
|
||||
case fiber.MethodDelete:
|
||||
return sharedconst.AuditActionDelete
|
||||
default:
|
||||
return sharedconst.AuditActionCustom
|
||||
}
|
||||
}
|
||||
|
||||
func moduleFromPathSegment(segment string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(segment)) {
|
||||
case "air-rescuer-checklist":
|
||||
return sharedconst.AuditModuleAirRescuerChecklist
|
||||
case "audit-logs":
|
||||
return sharedconst.AuditModuleAuditLog
|
||||
case "auth":
|
||||
return sharedconst.AuditModuleAuth
|
||||
case "bases":
|
||||
return sharedconst.AuditModuleBase
|
||||
case "branding", "public":
|
||||
return sharedconst.AuditModuleBranding
|
||||
case "contacts":
|
||||
return sharedconst.AuditModuleContact
|
||||
case "duty-roster":
|
||||
return sharedconst.AuditModuleDutyRoster
|
||||
case "dul":
|
||||
return sharedconst.AuditModuleDUL
|
||||
case "facilities":
|
||||
return sharedconst.AuditModuleFacility
|
||||
case "federal-state":
|
||||
return sharedconst.AuditModuleFederalState
|
||||
case "file-manager":
|
||||
return sharedconst.AuditModuleFileManager
|
||||
case "flight-data":
|
||||
return sharedconst.AuditModuleFlightData
|
||||
case "flights":
|
||||
return sharedconst.AuditModuleFlight
|
||||
case "forces-present":
|
||||
return sharedconst.AuditModuleForcesPresent
|
||||
case "health-insurance-companies":
|
||||
return sharedconst.AuditModuleHealthInsuranceCompany
|
||||
case "helicopter-files":
|
||||
return sharedconst.AuditModuleHelicopterFile
|
||||
case "helicopters":
|
||||
return sharedconst.AuditModuleHelicopter
|
||||
case "hems-operation", "hems-operation-category", "hems-operational-data":
|
||||
return sharedconst.AuditModuleUnknown
|
||||
case "hospital":
|
||||
return sharedconst.AuditModuleHospital
|
||||
case "icao":
|
||||
return sharedconst.AuditModuleICAO
|
||||
case "insurance-patient-data":
|
||||
return sharedconst.AuditModuleInsurancePatientData
|
||||
case "land":
|
||||
return sharedconst.AuditModuleLand
|
||||
case "master-settings":
|
||||
return sharedconst.AuditModuleMasterSetting
|
||||
case "medicine":
|
||||
return sharedconst.AuditModuleMedicine
|
||||
case "mission":
|
||||
return sharedconst.AuditModuleMission
|
||||
case "opc":
|
||||
return sharedconst.AuditModuleOPC
|
||||
case "patient-data":
|
||||
return sharedconst.AuditModulePatientData
|
||||
case "reserve-acs":
|
||||
return sharedconst.AuditModuleReserveAC
|
||||
case "roles":
|
||||
return sharedconst.AuditModuleRole
|
||||
case "users":
|
||||
return sharedconst.AuditModuleUser
|
||||
case "vocation":
|
||||
return sharedconst.AuditModuleVocation
|
||||
default:
|
||||
return sharedconst.AuditModuleUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func localString(v any) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func localUserID(v any) []byte {
|
||||
id, _ := v.([]byte)
|
||||
if len(id) != 16 {
|
||||
return nil
|
||||
}
|
||||
return id
|
||||
}
|
||||
208
internal/app/api/audit_middleware_test.go
Normal file
208
internal/app/api/audit_middleware_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/domain/audit"
|
||||
"wucher/internal/service"
|
||||
sharedconst "wucher/internal/shared/const"
|
||||
)
|
||||
|
||||
type mockAuditMiddlewareRepo struct {
|
||||
mu sync.Mutex
|
||||
entries []*audit.AuditLog
|
||||
}
|
||||
|
||||
func (m *mockAuditMiddlewareRepo) Create(_ context.Context, entry *audit.AuditLog) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
cp := *entry
|
||||
m.entries = append(m.entries, &cp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAuditMiddleware_SkipsGETRequests(t *testing.T) {
|
||||
repo := &mockAuditMiddlewareRepo{}
|
||||
auditSvc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 16, Workers: 1})
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(AuditMiddleware(auditSvc))
|
||||
app.Get("/api/v1/ping", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusCreated)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/ping", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
repo.mu.Lock()
|
||||
n := len(repo.entries)
|
||||
repo.mu.Unlock()
|
||||
if n > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
if len(repo.entries) != 0 {
|
||||
t.Fatalf("expected no audit log entry for GET, got=%d", len(repo.entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditMiddleware_LogsMutationRequests(t *testing.T) {
|
||||
repo := &mockAuditMiddlewareRepo{}
|
||||
auditSvc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 16, Workers: 1})
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(AuditMiddleware(auditSvc))
|
||||
app.Post("/api/v1/helicopters/create", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusCreated)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
repo.mu.Lock()
|
||||
n := len(repo.entries)
|
||||
repo.mu.Unlock()
|
||||
if n > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
if len(repo.entries) == 0 {
|
||||
t.Fatalf("expected audit log entry for mutation request")
|
||||
}
|
||||
got := repo.entries[0]
|
||||
if got.Layer != "api" || got.Module != sharedconst.AuditModuleHelicopter || got.Action != sharedconst.AuditActionCreate {
|
||||
t.Fatalf("unexpected audit entry: layer=%s module=%s action=%s", got.Layer, got.Module, got.Action)
|
||||
}
|
||||
if got.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("expected status 201, got %d", got.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleFromPathSegment(t *testing.T) {
|
||||
cases := []struct {
|
||||
segment string
|
||||
want string
|
||||
}{
|
||||
{segment: "helicopters", want: sharedconst.AuditModuleHelicopter},
|
||||
{segment: "bases", want: sharedconst.AuditModuleBase},
|
||||
{segment: "health-insurance-companies", want: sharedconst.AuditModuleHealthInsuranceCompany},
|
||||
{segment: "hems-roster", want: sharedconst.AuditModuleUnknown},
|
||||
{segment: "dul", want: sharedconst.AuditModuleDUL},
|
||||
{segment: "reserve-acs", want: sharedconst.AuditModuleReserveAC},
|
||||
{segment: "not-registered", want: sharedconst.AuditModuleUnknown},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.segment, func(t *testing.T) {
|
||||
got := moduleFromPathSegment(tc.segment)
|
||||
if got != tc.want {
|
||||
t.Fatalf("moduleFromPathSegment(%q) = %q, want %q", tc.segment, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAuditModuleAction(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
method string
|
||||
target string
|
||||
wantModule string
|
||||
wantAction string
|
||||
}{
|
||||
{
|
||||
name: "datatable list",
|
||||
method: http.MethodGet,
|
||||
target: "/api/v1/helicopters/get-all/dt",
|
||||
wantModule: sharedconst.AuditModuleHelicopter,
|
||||
wantAction: sharedconst.AuditActionList,
|
||||
},
|
||||
{
|
||||
name: "explicit create",
|
||||
method: http.MethodPost,
|
||||
target: "/api/v1/helicopters/create",
|
||||
wantModule: sharedconst.AuditModuleHelicopter,
|
||||
wantAction: sharedconst.AuditActionCreate,
|
||||
},
|
||||
{
|
||||
name: "fallback method update",
|
||||
method: http.MethodPatch,
|
||||
target: "/api/v1/helicopters/019dbeb6-4c37-7495-b0d0-e02ed134f87d",
|
||||
wantModule: sharedconst.AuditModuleHelicopter,
|
||||
wantAction: sharedconst.AuditActionUpdate,
|
||||
},
|
||||
{
|
||||
name: "base regular variant from query",
|
||||
method: http.MethodGet,
|
||||
target: "/api/v1/bases/get-all?category_type=regular",
|
||||
wantModule: sharedconst.AuditModuleBaseRegular,
|
||||
wantAction: sharedconst.AuditActionList,
|
||||
},
|
||||
{
|
||||
name: "base hems variant from query alias",
|
||||
method: http.MethodGet,
|
||||
target: "/api/v1/bases/get-all?base_category=hems",
|
||||
wantModule: sharedconst.AuditModuleBaseHEMS,
|
||||
wantAction: sharedconst.AuditActionList,
|
||||
},
|
||||
{
|
||||
name: "unknown module",
|
||||
method: http.MethodGet,
|
||||
target: "/api/v1/ping",
|
||||
wantModule: sharedconst.AuditModuleUnknown,
|
||||
wantAction: sharedconst.AuditActionGet,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.All("/*", func(c *fiber.Ctx) error {
|
||||
gotModule, gotAction := resolveAuditModuleAction(c)
|
||||
if gotModule != tc.wantModule || gotAction != tc.wantAction {
|
||||
t.Fatalf("resolveAuditModuleAction(%q, %q) = (%q, %q), want (%q, %q)",
|
||||
tc.method, tc.target, gotModule, gotAction, tc.wantModule, tc.wantAction)
|
||||
}
|
||||
return c.SendStatus(http.StatusNoContent)
|
||||
})
|
||||
req, _ := http.NewRequest(tc.method, tc.target, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
131
internal/app/api/compress_wopi_test.go
Normal file
131
internal/app/api/compress_wopi_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
)
|
||||
|
||||
// buildTinyXLSX returns a minimal but structurally valid .xlsx (ZIP) payload.
|
||||
func buildTinyXLSX(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
entries := map[string]string{
|
||||
"[Content_Types].xml": `<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/></Types>`,
|
||||
"_rels/.rels": `<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`,
|
||||
"xl/workbook.xml": `<?xml version="1.0"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets></workbook>`,
|
||||
}
|
||||
for name, content := range entries {
|
||||
w, err := zw.Create(name)
|
||||
if err != nil {
|
||||
t.Fatalf("zip create %s: %v", name, err)
|
||||
}
|
||||
if _, err := io.WriteString(w, content); err != nil {
|
||||
t.Fatalf("zip write %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatalf("zip close: %v", err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func assertValidZip(t *testing.T, b []byte) {
|
||||
t.Helper()
|
||||
if _, err := zip.NewReader(bytes.NewReader(b), int64(len(b))); err != nil {
|
||||
t.Fatalf("payload is not a valid zip: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsWOPIRequest(t *testing.T) {
|
||||
app := fiber.New()
|
||||
cases := []struct {
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{"/wopi/files/123/contents", true},
|
||||
{"/wopi/files/123", true},
|
||||
{"/api/file-manager/files", false},
|
||||
{"/wopimirror/x", false},
|
||||
{"/", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
app.Get(tc.path, func(c *fiber.Ctx) error {
|
||||
if got := isWOPIRequest(c); got != tc.want {
|
||||
t.Errorf("isWOPIRequest(%q) = %v, want %v", tc.path, got, tc.want)
|
||||
}
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
resp, err := app.Test(httptest.NewRequest("GET", tc.path, nil), -1)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test(%q): %v", tc.path, err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Regression for the "template .xlsx always corrupt via WOPI" bug: the global
|
||||
// gzip compress middleware must NOT compress WOPI file-content responses.
|
||||
// Collabora saves the response body verbatim as the document, so a gzip stream
|
||||
// there is a guaranteed-corrupt file.
|
||||
func TestCompressMiddleware_SkipsWOPIContents(t *testing.T) {
|
||||
src := buildTinyXLSX(t)
|
||||
assertValidZip(t, src)
|
||||
|
||||
app := fiber.New()
|
||||
// Mirror the production compress wiring (UseDefaultMiddlewares).
|
||||
app.Use(compress.New(compress.Config{
|
||||
Level: compress.LevelBestSpeed,
|
||||
Next: func(c *fiber.Ctx) bool {
|
||||
return isEventStreamRequest(c) || isWOPIRequest(c)
|
||||
},
|
||||
}))
|
||||
|
||||
streamXLSX := func(c *fiber.Ctx) error {
|
||||
c.Set(fiber.HeaderContentType, "application/octet-stream")
|
||||
c.Status(fiber.StatusOK)
|
||||
c.Context().Response.SetBodyStream(io.NopCloser(bytes.NewReader(src)), len(src))
|
||||
return nil
|
||||
}
|
||||
app.Get("/wopi/files/:id/contents", streamXLSX)
|
||||
// Negative control: a non-WOPI route with a compressible body must still gzip.
|
||||
app.Get("/api/echo", func(c *fiber.Ctx) error {
|
||||
c.Set(fiber.HeaderContentType, "application/octet-stream")
|
||||
return c.Send(bytes.Repeat([]byte("A"), 4096))
|
||||
})
|
||||
|
||||
// WOPI content download with gzip offered must come back raw + valid.
|
||||
req := httptest.NewRequest("GET", "/wopi/files/abc/contents", nil)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
resp, err := app.Test(req, -1)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test wopi: %v", err)
|
||||
}
|
||||
if enc := resp.Header.Get("Content-Encoding"); enc != "" {
|
||||
t.Fatalf("WOPI content unexpectedly encoded: Content-Encoding=%q", enc)
|
||||
}
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
assertValidZip(t, got)
|
||||
if !bytes.Equal(got, src) {
|
||||
t.Fatalf("WOPI content body mutated: got %d bytes, want %d", len(got), len(src))
|
||||
}
|
||||
|
||||
// Sanity: prove compression is actually active for non-excluded routes.
|
||||
req2 := httptest.NewRequest("GET", "/api/echo", nil)
|
||||
req2.Header.Set("Accept-Encoding", "gzip")
|
||||
resp2, err := app.Test(req2, -1)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test echo: %v", err)
|
||||
}
|
||||
if enc := resp2.Header.Get("Content-Encoding"); enc != "gzip" {
|
||||
t.Fatalf("expected gzip on non-WOPI route, got Content-Encoding=%q (compress not active - test invalid)", enc)
|
||||
}
|
||||
_ = resp2.Body.Close()
|
||||
}
|
||||
471
internal/app/api/middleware.go
Normal file
471
internal/app/api/middleware.go
Normal file
@@ -0,0 +1,471 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/helmet"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/gofiber/fiber/v2/middleware/timeout"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/shared/pkg/apperrorsx"
|
||||
)
|
||||
|
||||
var (
|
||||
httpMetricsRegisterOnce = sync.Once{}
|
||||
httpRequestsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "http_requests_total",
|
||||
Help: "Total number of HTTP requests.",
|
||||
}, []string{"method", "path", "status"})
|
||||
httpRequestDurationSeconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request duration in seconds.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}, []string{"method", "path", "status"})
|
||||
)
|
||||
|
||||
// UseDefaultMiddlewares applies baseline best-practice middleware.
|
||||
func UseDefaultMiddlewares(app *fiber.App, cfg config.HTTPConfig) {
|
||||
registerHTTPMetrics()
|
||||
|
||||
app.Use(recover.New(recover.Config{
|
||||
EnableStackTrace: true,
|
||||
StackTraceHandler: func(c *fiber.Ctx, err any) {
|
||||
slog.Error("panic recovered", "method", c.Method(), "path", c.Path(), "error", err, "stack", string(debug.Stack()))
|
||||
},
|
||||
}))
|
||||
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Header: fiber.HeaderXRequestID,
|
||||
ContextKey: "requestid",
|
||||
Generator: utils.UUIDv4,
|
||||
}))
|
||||
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
// Use a request-scoped context that keeps request values but is not canceled
|
||||
// as soon as fasthttp starts draining on server shutdown.
|
||||
c.SetUserContext(context.WithoutCancel(c.Context()))
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
app.Use(httpAccessLogMiddleware())
|
||||
|
||||
app.Use(compress.New(compress.Config{
|
||||
Level: compress.LevelBestSpeed,
|
||||
Next: func(c *fiber.Ctx) bool {
|
||||
return isEventStreamRequest(c) || isWOPIRequest(c)
|
||||
},
|
||||
}))
|
||||
app.Use(requestTimeoutMiddleware(cfg))
|
||||
app.Use(httpMetricsMiddleware())
|
||||
|
||||
app.Use(helmet.New(helmet.Config{
|
||||
XFrameOptions: "DENY",
|
||||
ReferrerPolicy: "no-referrer",
|
||||
PermissionPolicy: "geolocation=(), microphone=(), camera=(), payment=(), usb=()",
|
||||
HSTSMaxAge: cfg.HSTSMaxAge,
|
||||
HSTSPreloadEnabled: cfg.HSTSMaxAge > 0,
|
||||
}))
|
||||
|
||||
app.Use(disallowUnsafeMethods())
|
||||
|
||||
corsAllowCredentials := cfg.CORSAllowCredentials && !containsWildcardOrigin(cfg.CORSAllowOrigins)
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: cfg.CORSAllowOrigins,
|
||||
AllowMethods: cfg.CORSAllowMethods,
|
||||
AllowHeaders: cfg.CORSAllowHeaders,
|
||||
ExposeHeaders: cfg.CORSExposeHeaders,
|
||||
AllowCredentials: corsAllowCredentials,
|
||||
MaxAge: cfg.CORSMaxAge,
|
||||
}))
|
||||
|
||||
app.Use(limiter.New(limiter.Config{
|
||||
Max: cfg.RateLimitMax,
|
||||
Expiration: cfg.RateLimitWindow,
|
||||
Next: func(c *fiber.Ctx) bool {
|
||||
path := c.Path()
|
||||
return c.Method() == fiber.MethodOptions ||
|
||||
path == "/health" ||
|
||||
strings.HasPrefix(path, "/swagger")
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
retryAfter := int(cfg.RateLimitWindow.Seconds())
|
||||
if retryAfter < 1 {
|
||||
retryAfter = 1
|
||||
}
|
||||
c.Set(fiber.HeaderRetryAfter, strconv.Itoa(retryAfter))
|
||||
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{
|
||||
"status": "429",
|
||||
"title": "Too Many Requests",
|
||||
"detail": "rate limit exceeded",
|
||||
}},
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
// Per-endpoint limiter (exact method+path match), configured from env/config.
|
||||
// Applied in addition to the global limiter above.
|
||||
endpointKeys := make([]string, 0, len(cfg.RateLimitByEndpoint))
|
||||
for key := range cfg.RateLimitByEndpoint {
|
||||
endpointKeys = append(endpointKeys, key)
|
||||
}
|
||||
sort.Strings(endpointKeys)
|
||||
for _, endpointKey := range endpointKeys {
|
||||
rule := cfg.RateLimitByEndpoint[endpointKey]
|
||||
currentEndpointKey := endpointKey
|
||||
currentRule := rule
|
||||
|
||||
app.Use(limiter.New(limiter.Config{
|
||||
Max: currentRule.Max,
|
||||
Expiration: currentRule.Window,
|
||||
Next: func(c *fiber.Ctx) bool {
|
||||
return methodPathKey(c.Method(), c.Path()) != currentEndpointKey
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
retryAfter := int(currentRule.Window.Seconds())
|
||||
if retryAfter < 1 {
|
||||
retryAfter = 1
|
||||
}
|
||||
c.Set(fiber.HeaderRetryAfter, strconv.Itoa(retryAfter))
|
||||
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{
|
||||
"status": "429",
|
||||
"title": "Too Many Requests",
|
||||
"detail": fmt.Sprintf("rate limit exceeded for %s", currentEndpointKey),
|
||||
}},
|
||||
})
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func httpAccessLogMiddleware() fiber.Handler {
|
||||
requestLogger := slog.Default()
|
||||
|
||||
return func(c *fiber.Ctx) error {
|
||||
started := time.Now()
|
||||
err := c.Next()
|
||||
|
||||
if c.Path() == "/health" {
|
||||
return err
|
||||
}
|
||||
|
||||
statusCode := c.Response().StatusCode()
|
||||
level := slog.LevelInfo
|
||||
if statusCode >= fiber.StatusInternalServerError || err != nil {
|
||||
level = slog.LevelError
|
||||
} else if statusCode >= fiber.StatusBadRequest {
|
||||
level = slog.LevelWarn
|
||||
}
|
||||
|
||||
requestID := strings.TrimSpace(fmt.Sprint(c.Locals("requestid")))
|
||||
if requestID == "" || requestID == "<nil>" {
|
||||
requestID = strings.TrimSpace(c.GetRespHeader(fiber.HeaderXRequestID))
|
||||
}
|
||||
|
||||
args := []any{
|
||||
slog.String("request_id", requestID),
|
||||
slog.String("method", c.Method()),
|
||||
slog.String("path", c.Path()),
|
||||
slog.Int("status", statusCode),
|
||||
slog.Duration("latency", time.Since(started)),
|
||||
slog.String("ip", c.IP()),
|
||||
}
|
||||
if detail := strings.TrimSpace(fmt.Sprint(c.Locals(apperrorsx.ContextKeyHTTPErrorInternalDetail))); detail != "" && detail != "<nil>" {
|
||||
args = append(args, slog.String("internal_detail", detail))
|
||||
}
|
||||
if detail := strings.TrimSpace(fmt.Sprint(c.Locals(apperrorsx.ContextKeyHTTPErrorDetail))); detail != "" && detail != "<nil>" {
|
||||
args = append(args, slog.String("detail", detail))
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
args = append(args, slog.Any("error_chain", buildErrorChain(err)))
|
||||
}
|
||||
if statusCode >= fiber.StatusInternalServerError {
|
||||
if body := strings.TrimSpace(string(c.Response().Body())); body != "" {
|
||||
args = append(args, slog.String("response_body", truncateForLog(body, 1800)))
|
||||
}
|
||||
}
|
||||
|
||||
requestLogger.Log(c.UserContext(), level, "http request", args...)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func buildErrorChain(err error) []string {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
chain := make([]string, 0, 8)
|
||||
seen := map[string]struct{}{}
|
||||
for current := err; current != nil; current = errors.Unwrap(current) {
|
||||
msg := current.Error()
|
||||
if msg == "" {
|
||||
msg = fmt.Sprintf("%T", current)
|
||||
}
|
||||
key := fmt.Sprintf("%T:%s", current, msg)
|
||||
if _, ok := seen[key]; ok {
|
||||
break
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
chain = append(chain, fmt.Sprintf("%T: %s", current, msg))
|
||||
}
|
||||
return chain
|
||||
}
|
||||
|
||||
func truncateForLog(v string, max int) string {
|
||||
if max <= 0 || len(v) <= max {
|
||||
return v
|
||||
}
|
||||
if max <= 3 {
|
||||
return v[:max]
|
||||
}
|
||||
return v[:max-3] + "..."
|
||||
}
|
||||
|
||||
func httpMetricsMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
started := time.Now()
|
||||
method := normalizeHTTPMethod(c.Method())
|
||||
err := c.Next()
|
||||
|
||||
path := metricsPathLabel(c)
|
||||
if shouldSkipHTTPMetrics(path) {
|
||||
return err
|
||||
}
|
||||
status := strconv.Itoa(c.Response().StatusCode())
|
||||
|
||||
httpRequestsTotal.WithLabelValues(method, path, status).Inc()
|
||||
httpRequestDurationSeconds.WithLabelValues(method, path, status).Observe(time.Since(started).Seconds())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func registerHTTPMetrics() {
|
||||
httpMetricsRegisterOnce.Do(func() {
|
||||
registerHTTPCollector(httpRequestsTotal)
|
||||
registerHTTPCollector(httpRequestDurationSeconds)
|
||||
})
|
||||
}
|
||||
|
||||
func registerHTTPCollector(collector prometheus.Collector) {
|
||||
if err := prometheus.Register(collector); err != nil {
|
||||
var alreadyRegistered prometheus.AlreadyRegisteredError
|
||||
if errors.As(err, &alreadyRegistered) {
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldSkipHTTPMetrics(path string) bool {
|
||||
return path == "/metrics" || path == "/health"
|
||||
}
|
||||
|
||||
func metricsPathLabel(c *fiber.Ctx) string {
|
||||
route := c.Route()
|
||||
if route != nil {
|
||||
if routePath := strings.TrimSpace(utils.CopyString(route.Path)); routePath != "" {
|
||||
return routePath
|
||||
}
|
||||
}
|
||||
return "unmatched"
|
||||
}
|
||||
|
||||
func normalizeHTTPMethod(method string) string {
|
||||
method = strings.ToUpper(strings.TrimSpace(utils.CopyString(method)))
|
||||
if method == "" {
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
switch method {
|
||||
case fiber.MethodGet,
|
||||
fiber.MethodHead,
|
||||
fiber.MethodPost,
|
||||
fiber.MethodPut,
|
||||
fiber.MethodPatch,
|
||||
fiber.MethodDelete,
|
||||
fiber.MethodOptions,
|
||||
fiber.MethodTrace,
|
||||
fiber.MethodConnect:
|
||||
return method
|
||||
default:
|
||||
return "OTHER"
|
||||
}
|
||||
}
|
||||
|
||||
func requestTimeoutMiddleware(cfg config.HTTPConfig) fiber.Handler {
|
||||
defaultTimeout := cfg.RequestTimeout
|
||||
overrides := cfg.RequestTimeoutByPath
|
||||
|
||||
if defaultTimeout <= 0 && len(overrides) == 0 {
|
||||
return func(c *fiber.Ctx) error { return c.Next() }
|
||||
}
|
||||
|
||||
return func(c *fiber.Ctx) error {
|
||||
if isEventStreamRequest(c) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
requestTimeout := defaultTimeout
|
||||
if override, ok := timeoutForRequest(overrides, c.Method(), c.Path(), c.Route()); ok {
|
||||
requestTimeout = override
|
||||
}
|
||||
if requestTimeout <= 0 {
|
||||
return c.Next()
|
||||
}
|
||||
return timeout.NewWithContext(func(c *fiber.Ctx) error {
|
||||
return c.Next()
|
||||
}, requestTimeout)(c)
|
||||
}
|
||||
}
|
||||
|
||||
func isEventStreamRequest(c *fiber.Ctx) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
path := strings.TrimSpace(c.Path())
|
||||
if strings.HasSuffix(path, "/file-manager/events") {
|
||||
return true
|
||||
}
|
||||
accept := strings.ToLower(strings.TrimSpace(c.Get(fiber.HeaderAccept)))
|
||||
return strings.Contains(accept, "text/event-stream")
|
||||
}
|
||||
|
||||
// isWOPIRequest reports whether the request targets the Collabora/WOPI
|
||||
// endpoints. WOPI file-content responses carry raw office bytes (an .xlsx is a
|
||||
// ZIP container that is already compressed); gzipping them makes Collabora
|
||||
// store the compressed stream verbatim as the document, producing a file that
|
||||
// is consistently corrupt. Office/JSON WOPI payloads gain nothing from gzip, so
|
||||
// the whole group is excluded from the compress middleware.
|
||||
func isWOPIRequest(c *fiber.Ctx) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(strings.TrimSpace(c.Path()), "/wopi/")
|
||||
}
|
||||
|
||||
func timeoutForRequest(overrides map[string]time.Duration, method, path string, route *fiber.Route) (time.Duration, bool) {
|
||||
if len(overrides) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
normalizedMethod := strings.ToUpper(strings.TrimSpace(method))
|
||||
trimmedPath := strings.TrimSpace(path)
|
||||
|
||||
if route != nil {
|
||||
registeredPath := strings.TrimSpace(route.Path)
|
||||
if registeredPath != "" && registeredPath != "/" {
|
||||
key := timeoutOverrideKey(normalizedMethod, registeredPath)
|
||||
if v, ok := overrides[key]; ok {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := overrides[timeoutOverrideKey(normalizedMethod, trimmedPath)]; ok {
|
||||
return v, true
|
||||
}
|
||||
|
||||
for key, duration := range overrides {
|
||||
parts := strings.SplitN(key, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
if strings.ToUpper(strings.TrimSpace(parts[0])) != normalizedMethod {
|
||||
continue
|
||||
}
|
||||
if routePatternMatches(strings.TrimSpace(parts[1]), trimmedPath) {
|
||||
return duration, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func methodPathKey(method, path string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(method)) + " " + strings.TrimSpace(path)
|
||||
}
|
||||
|
||||
func timeoutOverrideKey(method, path string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(method)) + " " + strings.TrimSpace(path)
|
||||
}
|
||||
|
||||
func routePatternMatches(pattern, path string) bool {
|
||||
if pattern == "" {
|
||||
return false
|
||||
}
|
||||
if pattern == path {
|
||||
return true
|
||||
}
|
||||
|
||||
pSeg := splitPathSegments(pattern)
|
||||
rSeg := splitPathSegments(path)
|
||||
|
||||
if len(pSeg) == 0 && len(rSeg) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
i := 0
|
||||
for ; i < len(pSeg); i++ {
|
||||
segment := pSeg[i]
|
||||
if segment == "*" || strings.HasPrefix(segment, "*") {
|
||||
return true
|
||||
}
|
||||
if i >= len(rSeg) {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(segment, ":") || strings.HasPrefix(segment, "+") {
|
||||
continue
|
||||
}
|
||||
if segment != rSeg[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return i == len(rSeg)
|
||||
}
|
||||
|
||||
func splitPathSegments(path string) []string {
|
||||
trimmed := strings.TrimSpace(path)
|
||||
trimmed = strings.TrimPrefix(trimmed, "/")
|
||||
trimmed = strings.TrimSuffix(trimmed, "/")
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(trimmed, "/")
|
||||
}
|
||||
|
||||
func disallowUnsafeMethods() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if c.Method() == fiber.MethodTrace || c.Method() == "TRACK" {
|
||||
return c.SendStatus(fiber.StatusMethodNotAllowed)
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func containsWildcardOrigin(origins string) bool {
|
||||
for _, origin := range strings.Split(origins, ",") {
|
||||
if strings.TrimSpace(origin) == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
586
internal/app/api/middleware_test.go
Normal file
586
internal/app/api/middleware_test.go
Normal file
@@ -0,0 +1,586 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/shared/pkg/apperrorsx"
|
||||
"wucher/internal/transport/http/jsonapi"
|
||||
"wucher/internal/transport/http/response"
|
||||
)
|
||||
|
||||
func testHTTPConfig() config.HTTPConfig {
|
||||
return config.HTTPConfig{
|
||||
CORSAllowOrigins: "http://localhost:3000",
|
||||
CORSAllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
||||
CORSAllowHeaders: "Origin,Content-Type,Accept,Authorization",
|
||||
CORSExposeHeaders: "X-Request-ID",
|
||||
CORSAllowCredentials: true,
|
||||
CORSMaxAge: 300,
|
||||
RateLimitMax: 100,
|
||||
RateLimitWindow: time.Minute,
|
||||
RequestTimeout: 0,
|
||||
RequestTimeoutByPath: map[string]time.Duration{},
|
||||
HSTSMaxAge: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_CORSHeaders(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
||||
req.Header.Set("Origin", "http://localhost:3000")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "http://localhost:3000" {
|
||||
t.Fatalf("expected allow-origin http://localhost:3000, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Credentials"); got != "true" {
|
||||
t.Fatalf("expected allow-credentials true, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("X-Frame-Options"); got != "DENY" {
|
||||
t.Fatalf("expected X-Frame-Options DENY, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("X-Request-Id"); got == "" {
|
||||
t.Fatalf("expected request id header to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_BlockTrace(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.All("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
req, _ := http.NewRequest(http.MethodTrace, "/ping", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected 405, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RateLimiter(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RateLimitMax = 1
|
||||
cfg.RateLimitWindow = time.Minute
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/limited", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
firstReq, _ := http.NewRequest(http.MethodGet, "/limited", nil)
|
||||
firstResp, err := app.Test(firstReq)
|
||||
if err != nil {
|
||||
t.Fatalf("first request app.Test: %v", err)
|
||||
}
|
||||
if firstResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected first request 200, got %d", firstResp.StatusCode)
|
||||
}
|
||||
|
||||
secondReq, _ := http.NewRequest(http.MethodGet, "/limited", nil)
|
||||
secondResp, err := app.Test(secondReq)
|
||||
if err != nil {
|
||||
t.Fatalf("second request app.Test: %v", err)
|
||||
}
|
||||
if secondResp.StatusCode != http.StatusTooManyRequests {
|
||||
t.Fatalf("expected second request 429, got %d", secondResp.StatusCode)
|
||||
}
|
||||
if got := secondResp.Header.Get("Retry-After"); got == "" {
|
||||
t.Fatalf("expected Retry-After header on 429 response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_EndpointRateLimiter(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RateLimitMax = 100
|
||||
cfg.RateLimitByEndpoint = map[string]config.RateLimitRule{
|
||||
"POST /auth/login": {
|
||||
Max: 1,
|
||||
Window: time.Minute,
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Post("/auth/login", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
app.Post("/auth/refresh", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
req1, _ := http.NewRequest(http.MethodPost, "/auth/login", nil)
|
||||
resp1, err := app.Test(req1)
|
||||
if err != nil {
|
||||
t.Fatalf("first login request app.Test: %v", err)
|
||||
}
|
||||
if resp1.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected first login request 200, got %d", resp1.StatusCode)
|
||||
}
|
||||
|
||||
req2, _ := http.NewRequest(http.MethodPost, "/auth/login", nil)
|
||||
resp2, err := app.Test(req2)
|
||||
if err != nil {
|
||||
t.Fatalf("second login request app.Test: %v", err)
|
||||
}
|
||||
if resp2.StatusCode != http.StatusTooManyRequests {
|
||||
t.Fatalf("expected second login request 429, got %d", resp2.StatusCode)
|
||||
}
|
||||
|
||||
otherReq, _ := http.NewRequest(http.MethodPost, "/auth/refresh", nil)
|
||||
otherResp, err := app.Test(otherReq)
|
||||
if err != nil {
|
||||
t.Fatalf("other endpoint request app.Test: %v", err)
|
||||
}
|
||||
if otherResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected other endpoint request 200, got %d", otherResp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_WildcardOriginDisablesCredentials(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.CORSAllowOrigins = "*"
|
||||
cfg.CORSAllowCredentials = true
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
||||
req.Header.Set("Origin", "http://localhost:3000")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "*" {
|
||||
t.Fatalf("expected allow-origin *, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Fatalf("expected allow-credentials to be omitted, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestContextDetachedFromShutdown(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/ctx", func(c *fiber.Ctx) error {
|
||||
if c.UserContext() == nil {
|
||||
return c.SendStatus(http.StatusInternalServerError)
|
||||
}
|
||||
if c.UserContext().Done() != nil {
|
||||
return c.SendStatus(http.StatusInternalServerError)
|
||||
}
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/ctx", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_CompressesLargeJSONWhenGzipAccepted(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
|
||||
largePayload := map[string]string{
|
||||
"data": strings.Repeat("x", 8*1024),
|
||||
}
|
||||
app.Get("/large-json", func(c *fiber.Ctx) error {
|
||||
return c.JSON(largePayload)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/large-json", nil)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Content-Encoding"); got != "gzip" {
|
||||
t.Fatalf("expected Content-Encoding gzip, got %q", got)
|
||||
}
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("gzip reader: %v", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
var payload map[string]string
|
||||
if err := json.NewDecoder(gz).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode json: %v", err)
|
||||
}
|
||||
if payload["data"] != largePayload["data"] {
|
||||
t.Fatalf("unexpected payload body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestTimeoutExceeded(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RequestTimeout = 10 * time.Millisecond
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/slow", func(c *fiber.Ctx) error {
|
||||
select {
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
return c.SendStatus(http.StatusOK)
|
||||
case <-c.UserContext().Done():
|
||||
return c.SendStatus(http.StatusRequestTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/slow", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusRequestTimeout {
|
||||
t.Fatalf("expected 408, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestTimeoutNormalRequestUnaffected(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RequestTimeout = 200 * time.Millisecond
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/fast", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/fast", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestTimeoutPerEndpointOverride(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RequestTimeout = 10 * time.Millisecond
|
||||
cfg.RequestTimeoutByPath = map[string]time.Duration{
|
||||
"GET /slow": 120 * time.Millisecond,
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/slow", func(c *fiber.Ctx) error {
|
||||
select {
|
||||
case <-time.After(40 * time.Millisecond):
|
||||
return c.SendStatus(http.StatusOK)
|
||||
case <-c.UserContext().Done():
|
||||
return c.SendStatus(http.StatusRequestTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/slow", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200 with override timeout, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestTimeoutPerEndpointOverrideDynamicRoute(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RequestTimeout = 10 * time.Millisecond
|
||||
cfg.RequestTimeoutByPath = map[string]time.Duration{
|
||||
"GET /users/:id": 120 * time.Millisecond,
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/users/:id", func(c *fiber.Ctx) error {
|
||||
select {
|
||||
case <-time.After(40 * time.Millisecond):
|
||||
return c.SendStatus(http.StatusOK)
|
||||
case <-c.UserContext().Done():
|
||||
return c.SendStatus(http.StatusRequestTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/users/123", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200 with dynamic route override timeout, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPAccessLogMiddlewareIncludesErrorDetail(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
|
||||
t.Cleanup(func() {
|
||||
slog.SetDefault(prev)
|
||||
})
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(httpAccessLogMiddleware())
|
||||
app.Get("/bad", func(c *fiber.Ctx) error {
|
||||
return response.WriteErrors(c, http.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
||||
Title: "Validation error",
|
||||
Detail: "name is required",
|
||||
}})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/bad", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if strings.Contains(string(body), "name is required") {
|
||||
t.Fatalf("expected sanitized response detail, got %s", string(body))
|
||||
}
|
||||
if !strings.Contains(string(body), `"detail":"validation failed"`) {
|
||||
t.Fatalf("expected generic response detail, got %s", string(body))
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "name is required") {
|
||||
t.Fatalf("expected access log to include original detail, got %s", logBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPAccessLogMiddlewareIncludesResponseBodyOnServerError(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
|
||||
t.Cleanup(func() {
|
||||
slog.SetDefault(prev)
|
||||
})
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(httpAccessLogMiddleware())
|
||||
app.Get("/boom", func(c *fiber.Ctx) error {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{
|
||||
"status": "500",
|
||||
"title": "Update failed",
|
||||
"detail": "db deadlock on takeover update",
|
||||
}},
|
||||
})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/boom", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("expected 500, got %d", resp.StatusCode)
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "db deadlock on takeover update") {
|
||||
t.Fatalf("expected access log to include response body detail, got %s", logBuf.String())
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "response_body") {
|
||||
t.Fatalf("expected access log to include response_body field, got %s", logBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPAccessLogMiddlewareIncludesInternalErrorDetail(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
|
||||
t.Cleanup(func() {
|
||||
slog.SetDefault(prev)
|
||||
})
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(httpAccessLogMiddleware())
|
||||
app.Get("/boom", func(c *fiber.Ctx) error {
|
||||
c.Locals(apperrorsx.ContextKeyHTTPErrorInternalDetail, "action=UpdateTakeover | error=reserve update failed | chain=*errors.errorString: reserve update failed")
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{
|
||||
"status": "500",
|
||||
"title": "Update failed",
|
||||
"detail": "internal server error",
|
||||
}},
|
||||
})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/boom", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("expected 500, got %d", resp.StatusCode)
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "internal_detail") {
|
||||
t.Fatalf("expected access log to include internal_detail field, got %s", logBuf.String())
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "reserve update failed") {
|
||||
t.Fatalf("expected access log to include internal error detail, got %s", logBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_HTTPMetricsUsesRoutePatternLabels(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/users/:id", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusCreated)
|
||||
})
|
||||
app.Get("/metrics", func(c *fiber.Ctx) error {
|
||||
metricsHandler(c.Context())
|
||||
return nil
|
||||
})
|
||||
|
||||
beforeCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/users/:id", "201")
|
||||
beforeHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/users/:id", "201")
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/users/123", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
afterCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/users/:id", "201")
|
||||
afterHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/users/:id", "201")
|
||||
if afterCounter <= beforeCounter {
|
||||
t.Fatalf("expected counter to increase, before=%v after=%v", beforeCounter, afterCounter)
|
||||
}
|
||||
if afterHistogram <= beforeHistogram {
|
||||
t.Fatalf("expected histogram count to increase, before=%v after=%v", beforeHistogram, afterHistogram)
|
||||
}
|
||||
|
||||
metricsReq, _ := http.NewRequest(http.MethodGet, "/metrics", nil)
|
||||
metricsResp, err := app.Test(metricsReq)
|
||||
if err != nil {
|
||||
t.Fatalf("metrics app.Test: %v", err)
|
||||
}
|
||||
if metricsResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected /metrics status 200, got %d", metricsResp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(metricsResp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read /metrics body: %v", err)
|
||||
}
|
||||
metricsBody := string(body)
|
||||
if !strings.Contains(metricsBody, "http_requests_total") {
|
||||
t.Fatalf("expected http_requests_total in /metrics output")
|
||||
}
|
||||
if !strings.Contains(metricsBody, "http_request_duration_seconds") {
|
||||
t.Fatalf("expected http_request_duration_seconds in /metrics output")
|
||||
}
|
||||
if !strings.Contains(metricsBody, "path=\"/users/:id\"") {
|
||||
t.Fatalf("expected route-pattern path label in /metrics output")
|
||||
}
|
||||
|
||||
beforeMetricsCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/metrics", "200")
|
||||
beforeMetricsHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/metrics", "200")
|
||||
metricsReq2, _ := http.NewRequest(http.MethodGet, "/metrics", nil)
|
||||
metricsResp2, err := app.Test(metricsReq2)
|
||||
if err != nil {
|
||||
t.Fatalf("metrics second app.Test: %v", err)
|
||||
}
|
||||
if metricsResp2.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected second /metrics status 200, got %d", metricsResp2.StatusCode)
|
||||
}
|
||||
afterMetricsCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/metrics", "200")
|
||||
afterMetricsHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/metrics", "200")
|
||||
if afterMetricsCounter != beforeMetricsCounter {
|
||||
t.Fatalf("expected /metrics to be excluded from counter, before=%v after=%v", beforeMetricsCounter, afterMetricsCounter)
|
||||
}
|
||||
if afterMetricsHistogram != beforeMetricsHistogram {
|
||||
t.Fatalf("expected /metrics to be excluded from histogram, before=%v after=%v", beforeMetricsHistogram, afterMetricsHistogram)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeHTTPMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{name: "get", input: "GET", output: "GET"},
|
||||
{name: "lowercase", input: "post", output: "POST"},
|
||||
{name: "trimmed", input: " put ", output: "PUT"},
|
||||
{name: "empty", input: "", output: "UNKNOWN"},
|
||||
{name: "custom", input: "GETT", output: "OTHER"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizeHTTPMethod(tt.input); got != tt.output {
|
||||
t.Fatalf("expected %q, got %q", tt.output, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func metricCounterValue(t *testing.T, vec *prometheus.CounterVec, labels ...string) float64 {
|
||||
t.Helper()
|
||||
m := &dto.Metric{}
|
||||
if err := vec.WithLabelValues(labels...).Write(m); err != nil {
|
||||
t.Fatalf("read counter metric: %v", err)
|
||||
}
|
||||
return m.GetCounter().GetValue()
|
||||
}
|
||||
|
||||
func metricHistogramCount(t *testing.T, vec *prometheus.HistogramVec, labels ...string) uint64 {
|
||||
t.Helper()
|
||||
observer, err := vec.GetMetricWithLabelValues(labels...)
|
||||
if err != nil {
|
||||
t.Fatalf("load histogram metric: %v", err)
|
||||
}
|
||||
metric, ok := observer.(prometheus.Metric)
|
||||
if !ok {
|
||||
t.Fatalf("histogram observer does not implement prometheus.Metric")
|
||||
}
|
||||
m := &dto.Metric{}
|
||||
if err := metric.Write(m); err != nil {
|
||||
t.Fatalf("read histogram metric: %v", err)
|
||||
}
|
||||
if m.GetHistogram() == nil {
|
||||
t.Fatalf("histogram metric not available for labels %v", labels)
|
||||
}
|
||||
return m.GetHistogram().GetSampleCount()
|
||||
}
|
||||
9
internal/app/api/pin_actions.go
Normal file
9
internal/app/api/pin_actions.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package api
|
||||
|
||||
type PinProtectedEndpoint struct {
|
||||
Method string
|
||||
Path string
|
||||
Action string
|
||||
}
|
||||
|
||||
var PinProtectedEndpoints = []PinProtectedEndpoint{}
|
||||
64
internal/app/api/pin_actions_test.go
Normal file
64
internal/app/api/pin_actions_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestPinProtectedEndpoints_Definition(t *testing.T) {
|
||||
seen := map[string]struct{}{}
|
||||
for _, ep := range PinProtectedEndpoints {
|
||||
if ep.Method == "" || ep.Path == "" || ep.Action == "" {
|
||||
t.Fatalf("invalid pin-protected endpoint: %#v", ep)
|
||||
}
|
||||
key := ep.Method + " " + ep.Path
|
||||
if _, ok := seen[key]; ok {
|
||||
t.Fatalf("duplicate pin-protected endpoint %s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinProtectedEndpoints_RoutesHavePinMiddleware(t *testing.T) {
|
||||
if len(PinProtectedEndpoints) == 0 {
|
||||
return
|
||||
}
|
||||
app := fiber.New()
|
||||
RegisterRoutes(app, buildRouteDependenciesForTest())
|
||||
|
||||
routes := app.GetRoutes(true)
|
||||
for _, ep := range PinProtectedEndpoints {
|
||||
route := findRoute(routes, ep.Method, ep.Path)
|
||||
if route == nil {
|
||||
t.Fatalf("pin-protected route %s %s is not registered", ep.Method, ep.Path)
|
||||
}
|
||||
minHandlers := 3 // pin + permission + handler
|
||||
if ep.Path == "/api/v1/auth/invite" {
|
||||
minHandlers = 4 // auth + pin + permission + handler (no group auth middleware)
|
||||
}
|
||||
if len(route.Handlers) < minHandlers {
|
||||
t.Fatalf("route %s %s expected at least %d handlers, got %d", ep.Method, ep.Path, minHandlers, len(route.Handlers))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func findRoute(routes []fiber.Route, method, path string) *fiber.Route {
|
||||
for i := range routes {
|
||||
if routes[i].Method == method && routes[i].Path == path {
|
||||
return &routes[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPinProtectedEndpoints_ExpectedMethods(t *testing.T) {
|
||||
for _, ep := range PinProtectedEndpoints {
|
||||
switch ep.Method {
|
||||
case http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete:
|
||||
default:
|
||||
t.Fatalf("unexpected method %q on pin-protected endpoint %s", ep.Method, ep.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
140
internal/app/api/routes.go
Normal file
140
internal/app/api/routes.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||||
|
||||
samlauth "wucher/internal/auth"
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/shared/pkg/metrics"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
var metricsHandler = fasthttpadaptor.NewFastHTTPHandler(metrics.PromHTTPHandler())
|
||||
|
||||
type RouteServices struct {
|
||||
Auth *service.AuthService
|
||||
Audit *service.AuditLogService
|
||||
WOPI *service.WOPIService
|
||||
}
|
||||
|
||||
type RouteHandlers struct {
|
||||
User *handlers.UserHandler
|
||||
Contact *handlers.ContactHandler
|
||||
Health *handlers.HealthHandler
|
||||
BuildInfo *handlers.BuildInfoHandler
|
||||
Auth *handlers.AuthHandler
|
||||
Role *handlers.RoleHandler
|
||||
Helicopter *handlers.HelicopterHandler
|
||||
HelicopterUsage *handlers.HelicopterUsageHandler
|
||||
HelicopterFile *handlers.HelicopterFileHandler
|
||||
Flight *handlers.FlightHandler
|
||||
FlightData *handlers.FlightDataHandler
|
||||
Mission *handlers.MissionHandler
|
||||
FMReport *handlers.FMReportHandler
|
||||
ReserveAc *handlers.ReserveAcHandler
|
||||
Takeover *handlers.TakeoverHandler
|
||||
OtherPerson *handlers.OtherPersonHandler
|
||||
Facility *handlers.FacilityHandler
|
||||
Base *handlers.BaseHandler
|
||||
Medicine *handlers.MedicineHandler
|
||||
Vocation *handlers.VocationHandler
|
||||
Opc *handlers.OpcHandler
|
||||
ForcesPresent *handlers.ForcesPresentHandler
|
||||
Hospital *handlers.HospitalHandler
|
||||
HealthInsuranceCompanies *handlers.HealthInsuranceCompaniesHandler
|
||||
FederalState *handlers.FederalStateHandler
|
||||
ICAO *handlers.ICAOHandler
|
||||
Land *handlers.LandHandler
|
||||
MasterSettings *handlers.MasterSettingsHandler
|
||||
Branding *handlers.BrandingHandler
|
||||
FileManagerFolder *handlers.FileManagerFolderHandler
|
||||
FileManagerFile *handlers.FileManagerFileHandler
|
||||
FileManagerAttachment *handlers.FileManagerAttachmentHandler
|
||||
FileManagerEvents *handlers.FileManagerEventsHandler
|
||||
HelicopterFileEvents *handlers.HelicopterFileEventsHandler
|
||||
DutyRoster *handlers.DutyRosterHandler
|
||||
AirRescuerChecklist *handlers.AirRescuerChecklistHandler
|
||||
InsurancePatientData *handlers.InsurancePatientDataHandler
|
||||
PatientData *handlers.PatientDataHandler
|
||||
HEMSOperationalData *handlers.HEMSOperationalDataHandler
|
||||
HEMSOperationCategory *handlers.HEMSOperationCategoryHandler
|
||||
HEMSOperation *handlers.HEMSOperationHandler
|
||||
Complaint *handlers.ComplaintHandler
|
||||
EASARelease *handlers.EASAReleaseHandler
|
||||
ActionSignoff *handlers.ActionSignoffHandler
|
||||
MCF *handlers.MCFHandler
|
||||
DUL *handlers.DULHandler
|
||||
FleetStatus *handlers.FleetStatusHandler
|
||||
SAMLAuth *samlauth.Handler
|
||||
}
|
||||
|
||||
type RouteDependencies struct {
|
||||
Handlers RouteHandlers
|
||||
Services RouteServices
|
||||
}
|
||||
|
||||
func RegisterRoutes(app *fiber.App, deps RouteDependencies) {
|
||||
h := deps.Handlers
|
||||
s := deps.Services
|
||||
|
||||
app.Get("/health", h.Health.Handle)
|
||||
app.Get("/version", h.BuildInfo.Handle)
|
||||
app.Get("/api/version", h.BuildInfo.Handle)
|
||||
app.Get("/api/build-info", h.BuildInfo.Handle)
|
||||
app.Get("/metrics", func(c *fiber.Ctx) error {
|
||||
metricsHandler(c.Context())
|
||||
return nil
|
||||
})
|
||||
|
||||
api := app.Group("/api")
|
||||
api.Use(AuditMiddleware(s.Audit))
|
||||
v1 := api.Group("/v1")
|
||||
|
||||
registerAuthRoutes(v1, h.Auth, s.Auth, h.SAMLAuth)
|
||||
registerUserRoutes(v1, h.User, h.FileManagerEvents, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerContactRoutes(v1, h.Contact, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerRoleRoutes(v1, h.Role, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerHelicopterRoutes(v1, h.Helicopter, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerHelicopterUsageRoutes(v1, h.HelicopterUsage, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerHelicopterFileRoutes(v1, h.HelicopterFile, h.HelicopterFileEvents, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerFlightRoutes(v1, h.Flight, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerFlightDataRoutes(v1, h.FlightData, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerMissionRoutes(v1, h.Mission, h.FileManagerEvents, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerFMReportRoutes(v1, h.FMReport, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerReserveAcRoutes(v1, h.ReserveAc, h.Takeover, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerTakeoverRoutes(v1, h.Takeover, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerOtherPersonRoutes(v1, h.OtherPerson, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerLastFlightsInspectionRoutes(v1, h.ReserveAc, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerFacilityRoutes(v1, h.Facility, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerBaseRoutes(v1, h.Base, h.FileManagerEvents, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerMedicineRoutes(v1, h.Medicine, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerVocationRoutes(v1, h.Vocation, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerOpcRoutes(v1, h.Opc, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerForcesPresentRoutes(v1, h.ForcesPresent, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerHospitalRoutes(v1, h.Hospital, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerHealthInsuranceCompaniesRoutes(v1, h.HealthInsuranceCompanies, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerFederalStateRoutes(v1, h.FederalState, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerICAORoutes(v1, h.ICAO, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerLandRoutes(v1, h.Land, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerMasterSettingsRoutes(v1, h.MasterSettings, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerBrandingRoutes(v1, h.Branding)
|
||||
registerFileManagerRoutes(v1, h.FileManagerFolder, h.FileManagerFile, h.FileManagerAttachment, h.FileManagerEvents, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerWOPIRoutes(app, h.Takeover, s.WOPI)
|
||||
registerDutyRosterRoutes(v1, h.DutyRoster, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerAirRescuerChecklistRoutes(v1, h.AirRescuerChecklist, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerInsurancePatientDataRoutes(v1, h.InsurancePatientData, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerPatientDataRoutes(v1, h.PatientData, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerHEMSOperationalDataRoutes(v1, h.HEMSOperationalData, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerHEMSOperationCategoryRoutes(v1, h.HEMSOperationCategory, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerHEMSOperationRoutes(v1, h.HEMSOperation, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerComplaintRoutes(v1, h.Complaint, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerEASAReleaseRoutes(v1, h.EASARelease, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerActionSignoffRoutes(v1, h.ActionSignoff, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerMCFRoutes(v1, h.MCF, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerDULRoutes(v1, h.DUL, h.FileManagerEvents, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerFleetStatusRoutes(v1, h.FleetStatus, middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerAuditRoutes(v1, handlers.NewAuditHandler(s.Audit), middleware.AuthRequired(s.Auth), s.Auth)
|
||||
registerSwaggerRoutes(app)
|
||||
}
|
||||
15
internal/app/api/routes_action_signoff.go
Normal file
15
internal/app/api/routes_action_signoff.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerActionSignoffRoutes(v1 fiber.Router, h *handlers.ActionSignoffHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
g := v1.Group("/action-signoffs", authMiddleware)
|
||||
g.Get("/get-by-flight/:flight_id", middleware.PermissionRequired(authSvc, "action_signoff.read"), h.GetByFlight)
|
||||
g.Post("/sign/:helicopter_id", middleware.PermissionRequired(authSvc, "action_signoff.sign"), h.Sign)
|
||||
}
|
||||
23
internal/app/api/routes_air_rescuer_checklist.go
Normal file
23
internal/app/api/routes_air_rescuer_checklist.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerAirRescuerChecklistRoutes(v1 fiber.Router, checklistHandler *handlers.AirRescuerChecklistHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
checklists := v1.Group("/air-rescuer-checklist", authMiddleware)
|
||||
checklists.Post("/create", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.create"), checklistHandler.Create)
|
||||
checklists.Get("/get-all", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.read"), checklistHandler.List)
|
||||
checklists.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.read"), checklistHandler.ListDatatable)
|
||||
checklists.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.read"), checklistHandler.Get)
|
||||
checklists.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.update"), checklistHandler.Update)
|
||||
checklists.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.delete"), checklistHandler.Delete)
|
||||
checklists.Post("/:uuid/items/create", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.update"), checklistHandler.CreateItem)
|
||||
checklists.Patch("/:uuid/items/reorder", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.update"), checklistHandler.ReorderItems)
|
||||
checklists.Patch("/items/update/:item_uuid", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.update"), checklistHandler.UpdateItem)
|
||||
checklists.Delete("/items/delete/:item_uuid", middleware.PermissionRequired(authSvc, "air_rescuer_checklist.delete"), checklistHandler.DeleteItem)
|
||||
}
|
||||
14
internal/app/api/routes_audit.go
Normal file
14
internal/app/api/routes_audit.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerAuditRoutes(v1 fiber.Router, auditHandler *handlers.AuditHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
audits := v1.Group("/audit-logs", authMiddleware)
|
||||
audits.Get("/get-all", middleware.PermissionRequiredReusable(authSvc, "audit.read"), auditHandler.List)
|
||||
}
|
||||
142
internal/app/api/routes_auth.go
Normal file
142
internal/app/api/routes_auth.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
|
||||
samlauth "wucher/internal/auth"
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerAuthRoutes(v1 fiber.Router, authHandler *handlers.AuthHandler, authSvc *service.AuthService, samlHandler *samlauth.Handler) {
|
||||
auth := v1.Group("/auth")
|
||||
auth.Post("/register", authHandler.Register)
|
||||
auth.Post("/invite", middleware.AuthRequired(authSvc), middleware.PermissionRequired(authSvc, "user.create"), authHandler.Invite)
|
||||
auth.Post("/invite/resend-set-password", middleware.AuthRequired(authSvc), middleware.PermissionRequired(authSvc, "user.update"), forgotPasswordIPLimiter(authSvc), authHandler.ResendSetPasswordInvite)
|
||||
auth.Post("/set-password", authHandler.SetPassword)
|
||||
auth.Post("/forgot-password", forgotPasswordIPLimiter(authSvc), authHandler.ForgotPassword)
|
||||
auth.Post("/reset-password", authHandler.ResetPassword)
|
||||
auth.Post("/pin/setup", middleware.AuthRequired(authSvc), authHandler.SetupSecurityPIN)
|
||||
auth.Post("/pin/verify", middleware.AuthRequired(authSvc), authHandler.VerifySecurityPIN)
|
||||
auth.Post("/pin/change", middleware.AuthRequired(authSvc), authHandler.ChangeSecurityPIN)
|
||||
auth.Post("/pin/request-reset", middleware.AuthRequired(authSvc), forgotPasswordIPLimiter(authSvc), authHandler.RequestSecurityPINReset)
|
||||
auth.Post("/pin/forgot", forgotPasswordIPLimiter(authSvc), authHandler.ForgotSecurityPIN)
|
||||
auth.Post("/pin/reset", authHandler.ResetSecurityPIN)
|
||||
auth.Post("/login", loginIPLimiter(authSvc), authHandler.Login)
|
||||
auth.Post("/exchange", authHandler.Exchange)
|
||||
auth.Post("/webauthn/register/begin", middleware.AuthRequired(authSvc), authHandler.WebAuthnRegisterBegin)
|
||||
auth.Post("/webauthn/register/finish", middleware.AuthRequired(authSvc), authHandler.WebAuthnRegisterFinish)
|
||||
auth.Post("/webauthn/login/begin", authHandler.WebAuthnLoginBegin)
|
||||
auth.Post("/webauthn/login/finish", authHandler.WebAuthnLoginFinish)
|
||||
auth.Post("/webauthn/reset-setup", middleware.AuthRequired(authSvc), authHandler.WebAuthnResetSetup)
|
||||
auth.Post("/refresh", authHandler.Refresh)
|
||||
auth.Post("/logout", authHandler.Logout)
|
||||
auth.Get("/sessions", middleware.AuthRequired(authSvc), authHandler.Sessions)
|
||||
auth.Delete("/sessions/:session_id", middleware.AuthRequired(authSvc), authHandler.DeleteSession)
|
||||
auth.Get("/me", middleware.AuthRequired(authSvc), authHandler.Me)
|
||||
auth.Patch("/me/timezone", middleware.AuthRequired(authSvc), authHandler.UpdateTimezone)
|
||||
auth.Get("/verify-email", authHandler.VerifyEmail)
|
||||
if samlHandler != nil {
|
||||
auth.Get("/microsoft/login", samlHandler.HandleLogin)
|
||||
auth.Get("/microsoft/metadata", samlHandler.HandleMetadata)
|
||||
} else {
|
||||
auth.Get("/microsoft/login", authHandler.MicrosoftLogin)
|
||||
}
|
||||
auth.Get("/microsoft/login-check", authHandler.MicrosoftLoginCheck)
|
||||
auth.Get("/microsoft/link", middleware.AuthRequired(authSvc), authHandler.MicrosoftLink)
|
||||
auth.Get("/microsoft/reauth", middleware.AuthRequired(authSvc), authHandler.MicrosoftReauth)
|
||||
if samlHandler != nil {
|
||||
auth.Get("/microsoft/callback", samlHandler.HandleSAMLCallback)
|
||||
auth.Post("/microsoft/callback", samlHandler.HandleSAMLCallback)
|
||||
auth.Get("/microsoft/logout", func(c *fiber.Ctx) error {
|
||||
if c.Query("SAMLRequest") != "" {
|
||||
return samlHandler.HandleSAMLLogoutRequest(c)
|
||||
}
|
||||
if c.Query("SAMLResponse") != "" {
|
||||
return samlHandler.HandleSAMLLogoutResponse(c)
|
||||
}
|
||||
return samlHandler.HandleBrowserLocalLogout(c)
|
||||
})
|
||||
auth.Post("/microsoft/logout", func(c *fiber.Ctx) error {
|
||||
if c.FormValue("SAMLRequest") != "" {
|
||||
return samlHandler.HandleSAMLLogoutRequest(c)
|
||||
}
|
||||
if c.FormValue("SAMLResponse") != "" {
|
||||
return samlHandler.HandleSAMLLogoutResponse(c)
|
||||
}
|
||||
return samlHandler.HandleBrowserLocalLogout(c)
|
||||
})
|
||||
} else {
|
||||
auth.Get("/microsoft/callback", authHandler.MicrosoftCallback)
|
||||
auth.Post("/microsoft/logout", middleware.AuthRequired(authSvc), authHandler.MicrosoftLogout)
|
||||
}
|
||||
// OIDC front-channel logout: invoked by Microsoft Entra in a hidden iframe when the
|
||||
// user signs out at the IdP. Must be public (the IdP has no app credentials) and
|
||||
// available regardless of the active Microsoft auth mode.
|
||||
auth.Get("/microsoft/front-channel-logout", authHandler.MicrosoftFrontChannelLogout)
|
||||
auth.Post("/microsoft/front-channel-logout", authHandler.MicrosoftFrontChannelLogout)
|
||||
auth.Post("/microsoft/refresh", middleware.AuthRequired(authSvc), authHandler.MicrosoftRefresh)
|
||||
auth.Post("/sso/link", middleware.AuthRequired(authSvc), authHandler.SSOLink)
|
||||
auth.Delete("/sso/unlink", middleware.AuthRequired(authSvc), authHandler.SSOUnlink)
|
||||
auth.Post("/totp/setup", middleware.AuthRequired(authSvc), authHandler.TOTPSetup)
|
||||
auth.Post("/totp/confirm", middleware.AuthRequired(authSvc), authHandler.TOTPConfirm)
|
||||
auth.Post("/totp/verify", authHandler.TOTPVerify)
|
||||
auth.Post("/email-otp/send", authHandler.SendEmailOTP)
|
||||
auth.Post("/email-otp/verify", authHandler.VerifyEmailOTP)
|
||||
auth.Post("/totp/disable", middleware.AuthRequired(authSvc), authHandler.TOTPDisable)
|
||||
auth.Post("/totp/reset-setup", middleware.AuthRequired(authSvc), authHandler.TOTPResetSetup)
|
||||
}
|
||||
|
||||
func forgotPasswordIPLimiter(authSvc *service.AuthService) fiber.Handler {
|
||||
window := authSvc.ForgotPasswordIPRateWindow()
|
||||
return limiter.New(limiter.Config{
|
||||
Max: authSvc.ForgotPasswordIPRateMax(),
|
||||
Expiration: window,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
retryAfter := int(window.Seconds())
|
||||
if retryAfter < 1 {
|
||||
retryAfter = 1
|
||||
}
|
||||
c.Set(fiber.HeaderRetryAfter, strconv.Itoa(retryAfter))
|
||||
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{
|
||||
"status": "429",
|
||||
"title": "Too Many Requests",
|
||||
"detail": "rate limit exceeded",
|
||||
}},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func loginIPLimiter(authSvc *service.AuthService) fiber.Handler {
|
||||
window := authSvc.LoginIPRateWindow()
|
||||
return limiter.New(limiter.Config{
|
||||
Max: authSvc.LoginIPRateMax(),
|
||||
Expiration: window,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
retryAfter := int(window.Seconds())
|
||||
if retryAfter < 1 {
|
||||
retryAfter = 1
|
||||
}
|
||||
c.Set(fiber.HeaderRetryAfter, strconv.Itoa(retryAfter))
|
||||
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{
|
||||
"status": "429",
|
||||
"title": "Too Many Requests",
|
||||
"detail": "rate limit exceeded",
|
||||
}},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
154
internal/app/api/routes_auth_test.go
Normal file
154
internal/app/api/routes_auth_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
)
|
||||
|
||||
func TestForgotPasswordIPLimiter(t *testing.T) {
|
||||
cfg := config.AuthConfig{
|
||||
ForgotPasswordIPRateMax: 1,
|
||||
ForgotPasswordIPRateWindow: time.Minute,
|
||||
}
|
||||
svc := service.NewAuthService(nil, nil, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: cfg})
|
||||
|
||||
app := fiber.New()
|
||||
app.Post("/forgot", forgotPasswordIPLimiter(svc), func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
req1, _ := http.NewRequest(http.MethodPost, "/forgot", nil)
|
||||
resp1, err := app.Test(req1)
|
||||
if err != nil {
|
||||
t.Fatalf("first request failed: %v", err)
|
||||
}
|
||||
if resp1.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected first request 200, got %d", resp1.StatusCode)
|
||||
}
|
||||
|
||||
req2, _ := http.NewRequest(http.MethodPost, "/forgot", nil)
|
||||
resp2, err := app.Test(req2)
|
||||
if err != nil {
|
||||
t.Fatalf("second request failed: %v", err)
|
||||
}
|
||||
if resp2.StatusCode != http.StatusTooManyRequests {
|
||||
t.Fatalf("expected second request 429, got %d", resp2.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginIPLimiter(t *testing.T) {
|
||||
cfg := config.AuthConfig{
|
||||
LoginIPRateMax: 1,
|
||||
LoginIPRateWindow: time.Minute,
|
||||
}
|
||||
svc := service.NewAuthService(nil, nil, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: cfg})
|
||||
|
||||
app := fiber.New()
|
||||
app.Post("/login", loginIPLimiter(svc), func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
req1, _ := http.NewRequest(http.MethodPost, "/login", nil)
|
||||
resp1, err := app.Test(req1)
|
||||
if err != nil {
|
||||
t.Fatalf("first request failed: %v", err)
|
||||
}
|
||||
if resp1.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected first request 200, got %d", resp1.StatusCode)
|
||||
}
|
||||
|
||||
req2, _ := http.NewRequest(http.MethodPost, "/login", nil)
|
||||
resp2, err := app.Test(req2)
|
||||
if err != nil {
|
||||
t.Fatalf("second request failed: %v", err)
|
||||
}
|
||||
if resp2.StatusCode != http.StatusTooManyRequests {
|
||||
t.Fatalf("expected second request 429, got %d", resp2.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterAuthRoutes(t *testing.T) {
|
||||
app := fiber.New()
|
||||
v1 := app.Group("/api/v1")
|
||||
|
||||
svc := service.NewAuthService(nil, nil, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
|
||||
authHandler := handlers.NewAuthHandler(svc)
|
||||
registerAuthRoutes(v1, authHandler, svc, nil)
|
||||
|
||||
routes := app.GetRoutes(true)
|
||||
required := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{method: http.MethodPost, path: "/api/v1/auth/register"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/invite"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/invite/resend-set-password"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/forgot-password"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/reset-password"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/pin/setup"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/pin/verify"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/pin/change"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/pin/request-reset"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/pin/forgot"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/pin/reset"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/login"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/exchange"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/webauthn/register/begin"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/webauthn/register/finish"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/webauthn/login/begin"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/webauthn/login/finish"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/webauthn/reset-setup"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/refresh"},
|
||||
{method: http.MethodGet, path: "/api/v1/auth/sessions"},
|
||||
{method: http.MethodDelete, path: "/api/v1/auth/sessions/:session_id"},
|
||||
{method: http.MethodGet, path: "/api/v1/auth/me"},
|
||||
{method: http.MethodGet, path: "/api/v1/auth/microsoft/login"},
|
||||
{method: http.MethodGet, path: "/api/v1/auth/microsoft/login-check"},
|
||||
{method: http.MethodGet, path: "/api/v1/auth/microsoft/link"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/sso/link"},
|
||||
{method: http.MethodDelete, path: "/api/v1/auth/sso/unlink"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/totp/verify"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/totp/reset-setup"},
|
||||
}
|
||||
|
||||
for _, tc := range required {
|
||||
if !hasRoute(routes, tc.method, tc.path) {
|
||||
t.Fatalf("route %s %s not registered", tc.method, tc.path)
|
||||
}
|
||||
}
|
||||
|
||||
var forgotRoute *fiber.Route
|
||||
for i := range routes {
|
||||
if routes[i].Method == http.MethodPost && routes[i].Path == "/api/v1/auth/forgot-password" {
|
||||
forgotRoute = &routes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if forgotRoute == nil {
|
||||
t.Fatalf("forgot password route not found")
|
||||
}
|
||||
if len(forgotRoute.Handlers) < 2 {
|
||||
t.Fatalf("expected forgot password route to have limiter and handler, got %d handlers", len(forgotRoute.Handlers))
|
||||
}
|
||||
|
||||
var loginRoute *fiber.Route
|
||||
for i := range routes {
|
||||
if routes[i].Method == http.MethodPost && routes[i].Path == "/api/v1/auth/login" {
|
||||
loginRoute = &routes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if loginRoute == nil {
|
||||
t.Fatalf("login route not found")
|
||||
}
|
||||
if len(loginRoute.Handlers) < 2 {
|
||||
t.Fatalf("expected login route to have limiter and handler, got %d handlers", len(loginRoute.Handlers))
|
||||
}
|
||||
}
|
||||
35
internal/app/api/routes_base.go
Normal file
35
internal/app/api/routes_base.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerBaseRoutes(
|
||||
v1 fiber.Router,
|
||||
baseHandler *handlers.BaseHandler,
|
||||
eventsHandler *handlers.FileManagerEventsHandler,
|
||||
authMiddleware fiber.Handler,
|
||||
authSvc *service.AuthService,
|
||||
) {
|
||||
bases := v1.Group("/bases", authMiddleware)
|
||||
bases.Get("/events", middleware.PermissionRequired(authSvc, "base.read"), eventsHandler.Stream)
|
||||
bases.Post("/create",
|
||||
middleware.PermissionRequired(authSvc, "base.create"),
|
||||
baseHandler.CreateBase,
|
||||
)
|
||||
bases.Get("/get-all", middleware.PermissionRequired(authSvc, "base.read"), baseHandler.ListBases)
|
||||
bases.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "base.read"), baseHandler.ListBasesDatatable)
|
||||
bases.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "base.read"), baseHandler.GetBase)
|
||||
bases.Patch("/update/:uuid",
|
||||
middleware.PermissionRequired(authSvc, "base.update"),
|
||||
baseHandler.UpdateBase,
|
||||
)
|
||||
bases.Delete("/delete/:uuid",
|
||||
middleware.PermissionRequired(authSvc, "base.delete"),
|
||||
baseHandler.DeleteBase,
|
||||
)
|
||||
}
|
||||
20
internal/app/api/routes_branding.go
Normal file
20
internal/app/api/routes_branding.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/transport/http/handlers"
|
||||
)
|
||||
|
||||
func registerBrandingRoutes(v1 fiber.Router, brandingHandler *handlers.BrandingHandler) {
|
||||
if brandingHandler == nil {
|
||||
return
|
||||
}
|
||||
branding := v1.Group("/branding")
|
||||
branding.Get("/logo", brandingHandler.GetLogo)
|
||||
branding.Get("/login-cover", brandingHandler.GetLoginCover)
|
||||
branding.Get("/login-content", brandingHandler.GetLoginContent)
|
||||
|
||||
publicBranding := v1.Group("/public/branding")
|
||||
publicBranding.Get("/email-logo", brandingHandler.GetEmailLogo)
|
||||
}
|
||||
26
internal/app/api/routes_complaint.go
Normal file
26
internal/app/api/routes_complaint.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerComplaintRoutes(v1 fiber.Router, h *handlers.ComplaintHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
complaints := v1.Group("/complaints", authMiddleware)
|
||||
complaints.Post("/create", middleware.PermissionRequired(authSvc, "complaint.create"), h.Create)
|
||||
complaints.Get("/get-all", middleware.PermissionRequired(authSvc, "complaint.read"), h.List)
|
||||
complaints.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "complaint.read"), h.ListDatatable)
|
||||
complaints.Get("/get-by-helicopter/:helicopter_id", middleware.PermissionRequired(authSvc, "complaint.read"), h.ListByHelicopter)
|
||||
complaints.Get("/hold-items/get-all", middleware.PermissionRequired(authSvc, "complaint.read"), h.ListHoldItems)
|
||||
complaints.Get("/hold-items/get-all/dt", middleware.PermissionRequired(authSvc, "complaint.read"), h.ListHoldItemsDatatable)
|
||||
complaints.Get("/get/:id", middleware.PermissionRequired(authSvc, "complaint.read"), h.GetByID)
|
||||
complaints.Patch("/update/:id", middleware.PermissionRequired(authSvc, "complaint.update"), h.Update)
|
||||
complaints.Post("/action-taken/create/:id", middleware.PermissionRequired(authSvc, "complaint.update"), h.ActionTaken)
|
||||
complaints.Patch("/action-taken/update/:id", middleware.PermissionRequired(authSvc, "complaint.update"), h.UpdateActionTaken)
|
||||
complaints.Post("/nsr/sign/:id", middleware.PermissionRequired(authSvc, "complaint.nsr"), h.NSRSign)
|
||||
complaints.Post("/nsr/unsign/:id", middleware.PermissionRequired(authSvc, "complaint.nsr"), h.NSRUnsign)
|
||||
complaints.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "complaint.delete"), h.Delete)
|
||||
}
|
||||
26
internal/app/api/routes_contacts.go
Normal file
26
internal/app/api/routes_contacts.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerContactRoutes(v1 fiber.Router, contactHandler *handlers.ContactHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
contacts := v1.Group("/contacts", authMiddleware)
|
||||
contacts.Post("/create", middleware.PermissionRequired(authSvc, "user.create"), contactHandler.Create)
|
||||
contacts.Get("/get-all", middleware.PermissionRequired(authSvc, "user.read"), contactHandler.List)
|
||||
contacts.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "user.read"), contactHandler.ListDatatable)
|
||||
contacts.Get("/:user_id", middleware.PermissionRequired(authSvc, "user.read"), contactHandler.Get)
|
||||
contacts.Patch("/:user_id", middleware.PermissionRequired(authSvc, "user.update"), contactHandler.Update)
|
||||
contacts.Delete("/:user_id", middleware.PermissionRequired(authSvc, "user.delete"), contactHandler.Delete)
|
||||
|
||||
contacts.Patch("/:user_id/status", middleware.PermissionRequired(authSvc, "user.update"), contactHandler.UpdateContactStatus)
|
||||
contacts.Patch("/:user_id/role", middleware.PermissionRequired(authSvc, "user.update"), contactHandler.UpdateContactRole)
|
||||
contacts.Patch("/:user_id/pilot-category", middleware.PermissionRequired(authSvc, "user.update"), contactHandler.UpdatePilotCategory)
|
||||
contacts.Patch("/:user_id/license", middleware.PermissionRequired(authSvc, "user.update"), contactHandler.UpdateLicense)
|
||||
contacts.Patch("/:user_id/chief-flags", middleware.PermissionRequired(authSvc, "user.update"), contactHandler.UpdateChiefFlags)
|
||||
contacts.Post("/bulk-update", middleware.PermissionRequired(authSvc, "user.update"), contactHandler.BulkUpdate)
|
||||
}
|
||||
26
internal/app/api/routes_dul.go
Normal file
26
internal/app/api/routes_dul.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerDULRoutes(
|
||||
v1 fiber.Router,
|
||||
dulHandler *handlers.DULHandler,
|
||||
eventsHandler *handlers.FileManagerEventsHandler,
|
||||
authMiddleware fiber.Handler,
|
||||
authSvc *service.AuthService,
|
||||
) {
|
||||
dul := v1.Group("/dul", authMiddleware)
|
||||
dul.Get("/events", middleware.PermissionRequired(authSvc, "dul.read"), eventsHandler.Stream)
|
||||
dul.Post("/create", middleware.PermissionRequired(authSvc, "dul.create"), dulHandler.Create)
|
||||
dul.Get("/get-all", middleware.PermissionRequired(authSvc, "dul.read"), dulHandler.List)
|
||||
dul.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "dul.read"), dulHandler.ListDatatable)
|
||||
dul.Get("/get/:id", middleware.PermissionRequired(authSvc, "dul.read"), dulHandler.Get)
|
||||
dul.Patch("/update/:id", middleware.PermissionRequired(authSvc, "dul.update"), dulHandler.Update)
|
||||
dul.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "dul.delete"), dulHandler.Delete)
|
||||
}
|
||||
19
internal/app/api/routes_duty_roster.go
Normal file
19
internal/app/api/routes_duty_roster.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerDutyRosterRoutes(v1 fiber.Router, rosterHandler *handlers.DutyRosterHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
dutyRosters := v1.Group("/duty-roster", authMiddleware)
|
||||
dutyRosters.Post("/create", middleware.PermissionRequired(authSvc, "duty_roster.create"), rosterHandler.CreateAny)
|
||||
dutyRosters.Get("/get-all", middleware.PermissionRequired(authSvc, "duty_roster.read"), rosterHandler.GetAllRosters)
|
||||
dutyRosters.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "duty_roster.read"), rosterHandler.ListAnyDatatable)
|
||||
dutyRosters.Patch("/update", middleware.PermissionRequired(authSvc, "duty_roster.update"), rosterHandler.UpdateAny)
|
||||
dutyRosters.Delete("/delete", middleware.PermissionRequired(authSvc, "duty_roster.delete"), rosterHandler.DeleteAny)
|
||||
dutyRosters.Delete("/crew", middleware.PermissionRequired(authSvc, "duty_roster.delete"), rosterHandler.DeleteAnyCrew)
|
||||
}
|
||||
21
internal/app/api/routes_easa_release.go
Normal file
21
internal/app/api/routes_easa_release.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerEASAReleaseRoutes(v1 fiber.Router, h *handlers.EASAReleaseHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
g := v1.Group("/easa-releases", authMiddleware)
|
||||
g.Post("/create", middleware.PermissionRequired(authSvc, "easa_release.create"), h.Create)
|
||||
g.Get("/get-by-helicopter/:helicopter_id", middleware.PermissionRequired(authSvc, "easa_release.read"), h.ListByHelicopter)
|
||||
g.Get("/get-by-flight/:flight_id", middleware.PermissionRequired(authSvc, "easa_release.read"), h.GetByFlight)
|
||||
g.Get("/get-by-complaint/:complaint_id", middleware.PermissionRequired(authSvc, "easa_release.read"), h.GetByComplaint)
|
||||
g.Get("/get/:id", middleware.PermissionRequired(authSvc, "easa_release.read"), h.GetByID)
|
||||
g.Patch("/update/:id", middleware.PermissionRequired(authSvc, "easa_release.update"), h.Update)
|
||||
g.Post("/sign/:id", middleware.PermissionRequired(authSvc, "easa_release.sign"), h.Sign)
|
||||
g.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "easa_release.delete"), h.Delete)
|
||||
}
|
||||
19
internal/app/api/routes_facility.go
Normal file
19
internal/app/api/routes_facility.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerFacilityRoutes(v1 fiber.Router, facilityHandler *handlers.FacilityHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
facilities := v1.Group("/facilities", authMiddleware)
|
||||
facilities.Post("/create", middleware.PermissionRequired(authSvc, "facility.create"), facilityHandler.Create)
|
||||
facilities.Get("/get-all", middleware.PermissionRequired(authSvc, "facility.read"), facilityHandler.List)
|
||||
facilities.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "facility.read"), facilityHandler.ListDatatable)
|
||||
facilities.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "facility.read"), facilityHandler.Get)
|
||||
facilities.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "facility.update"), facilityHandler.Update)
|
||||
facilities.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "facility.delete"), facilityHandler.Delete)
|
||||
}
|
||||
18
internal/app/api/routes_federal_state.go
Normal file
18
internal/app/api/routes_federal_state.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerFederalStateRoutes(v1 fiber.Router, federalStateHandler *handlers.FederalStateHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
federalState := v1.Group("/federal-state", authMiddleware)
|
||||
federalState.Post("/create", middleware.PermissionRequired(authSvc, "federal_state.create"), federalStateHandler.Create)
|
||||
federalState.Get("/get-all", middleware.PermissionRequired(authSvc, "federal_state.read"), federalStateHandler.List)
|
||||
federalState.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "federal_state.read"), federalStateHandler.ListDatatable)
|
||||
federalState.Patch("/update/:id", middleware.PermissionRequired(authSvc, "federal_state.update"), federalStateHandler.Update)
|
||||
federalState.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "federal_state.delete"), federalStateHandler.Delete)
|
||||
}
|
||||
62
internal/app/api/routes_file_manager.go
Normal file
62
internal/app/api/routes_file_manager.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerFileManagerRoutes(
|
||||
v1 fiber.Router,
|
||||
folderHandler *handlers.FileManagerFolderHandler,
|
||||
fileHandler *handlers.FileManagerFileHandler,
|
||||
attachmentHandler *handlers.FileManagerAttachmentHandler,
|
||||
eventsHandler *handlers.FileManagerEventsHandler,
|
||||
authMiddleware fiber.Handler,
|
||||
authSvc *service.AuthService,
|
||||
) {
|
||||
fm := v1.Group("/file-manager", authMiddleware)
|
||||
fm.Get("/get-all", middleware.PermissionRequired(authSvc, "file_manager.read"), folderHandler.ListAll)
|
||||
fm.Get("/events", middleware.PermissionRequired(authSvc, "file_manager.read"), eventsHandler.Stream)
|
||||
fm.Get("/trash/get-all", middleware.PermissionRequired(authSvc, "file_manager.read"), folderHandler.ListTrashItems)
|
||||
fm.Patch("/move", middleware.PermissionRequired(authSvc, "file_manager.update"), folderHandler.MoveNodes)
|
||||
fm.Delete("/delete", middleware.PermissionRequired(authSvc, "file_manager.delete"), folderHandler.DeleteNodesBulk)
|
||||
fm.Patch("/restore", middleware.PermissionRequired(authSvc, "file_manager.update"), folderHandler.RestoreNodesBulk)
|
||||
fm.Delete("/purge-delete", middleware.PermissionRequired(authSvc, "file_manager.delete"), folderHandler.PurgeNodesBulk)
|
||||
|
||||
folders := fm.Group("/folders")
|
||||
folders.Post("/create", middleware.PermissionRequired(authSvc, "file_manager.create"), folderHandler.CreateFolder)
|
||||
folders.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "file_manager.read"), folderHandler.GetFolder)
|
||||
folders.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "file_manager.update"), folderHandler.UpdateFolder)
|
||||
|
||||
files := fm.Group("/files")
|
||||
files.Post("/create", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateFile)
|
||||
files.Post("/create-from-template", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateFromTemplate)
|
||||
files.Post("/create-aircraft-image", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateAircraftImage)
|
||||
files.Post("/create-base-image", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateBaseImage)
|
||||
files.Post("/create-dul-image", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateDULImage)
|
||||
files.Post("/create-user-image", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateUserImage)
|
||||
files.Post("/create-takeover-file", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateTakeoverFile)
|
||||
files.Get("/:fileId/edit-session", middleware.PermissionRequired(authSvc, "file_manager.read"), fileHandler.EditSession)
|
||||
files.Post("/create-mission-file", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateMissionFile)
|
||||
files.Post("/upload", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.UploadFile)
|
||||
files.Post("/upload-aircraft-image", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.UploadAircraftImage)
|
||||
files.Post("/upload-base-image", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.UploadBaseImage)
|
||||
files.Post("/upload-dul-image", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.UploadDULImage)
|
||||
files.Post("/upload-user-image", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.UploadUserImage)
|
||||
files.Post("/upload-takeover-file", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.UploadTakeoverFile)
|
||||
files.Post("/upload-mission-file", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.UploadMissionFile)
|
||||
files.Post("/upload-fleet-status-file", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.UploadFleetStatusFile)
|
||||
files.Post("/create-fleet-status-file", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CreateFleetStatusFile)
|
||||
files.Delete("/cancel-upload", middleware.PermissionRequired(authSvc, "file_manager.create"), fileHandler.CancelUpload)
|
||||
files.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "file_manager.read"), fileHandler.GetFile)
|
||||
files.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "file_manager.update"), fileHandler.UpdateFile)
|
||||
|
||||
attachments := fm.Group("/attachments")
|
||||
attachments.Post("/create", middleware.PermissionRequired(authSvc, "file_manager.create"), attachmentHandler.CreateAttachment)
|
||||
attachments.Get("/get-all", middleware.PermissionRequired(authSvc, "file_manager.read"), attachmentHandler.ListAttachments)
|
||||
attachments.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "file_manager.read"), attachmentHandler.GetAttachment)
|
||||
attachments.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "file_manager.delete"), attachmentHandler.DeleteAttachment)
|
||||
}
|
||||
22
internal/app/api/routes_fleet_status.go
Normal file
22
internal/app/api/routes_fleet_status.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerFleetStatusRoutes(v1 fiber.Router, h *handlers.FleetStatusHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
g := v1.Group("/fleet-status", authMiddleware)
|
||||
g.Post("/create", middleware.PermissionRequired(authSvc, "flight.create"), h.Create)
|
||||
g.Get("/get/:id", middleware.PermissionRequired(authSvc, "flight.read"), h.GetByID)
|
||||
g.Get("/get-by-helicopter/:helicopter_id", middleware.PermissionRequired(authSvc, "flight.read"), h.GetByHelicopterID)
|
||||
g.Get("/get-all", middleware.PermissionRequired(authSvc, "flight.read"), h.List)
|
||||
g.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "flight.read"), h.ListDatatable)
|
||||
g.Get("/history/:helicopter_id", middleware.PermissionRequired(authSvc, "flight.read"), h.ListHistoryByHelicopter)
|
||||
g.Post("/mark-serviced/:id", middleware.PermissionRequired(authSvc, "flight.update"), h.MarkServiced)
|
||||
g.Patch("/update/:id", middleware.PermissionRequired(authSvc, "flight.update"), h.Update)
|
||||
g.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "flight.delete"), h.Delete)
|
||||
}
|
||||
15
internal/app/api/routes_flight.go
Normal file
15
internal/app/api/routes_flight.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerFlightRoutes(v1 fiber.Router, flightHandler *handlers.FlightHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
flights := v1.Group("/flights", authMiddleware)
|
||||
flights.Get("/get-all", middleware.PermissionRequired(authSvc, "flight.read"), flightHandler.List)
|
||||
flights.Get("/get", middleware.PermissionRequired(authSvc, "flight.read"), flightHandler.GetByAuthUser)
|
||||
}
|
||||
24
internal/app/api/routes_flight_data.go
Normal file
24
internal/app/api/routes_flight_data.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerFlightDataRoutes(v1 fiber.Router, flightDataHandler *handlers.FlightDataHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
flightData := v1.Group("/flight-data", authMiddleware)
|
||||
flightData.Post("/create", middleware.PermissionRequired(authSvc, "flight.create"), flightDataHandler.Create)
|
||||
flightData.Get("/get/:id", middleware.PermissionRequired(authSvc, "flight.read"), flightDataHandler.Get)
|
||||
flightData.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "flight.read"), flightDataHandler.Get)
|
||||
flightData.Get("/get-all", middleware.PermissionRequired(authSvc, "flight.read"), flightDataHandler.List)
|
||||
flightData.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "flight.read"), flightDataHandler.ListDatatable)
|
||||
flightData.Patch("/update/:id", middleware.PermissionRequired(authSvc, "flight.update"), flightDataHandler.Update)
|
||||
flightData.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "flight.delete"), flightDataHandler.Delete)
|
||||
|
||||
flightsData := v1.Group("/flights-data", authMiddleware)
|
||||
flightsData.Get("/get-all", middleware.PermissionRequired(authSvc, "flight.read"), flightDataHandler.List)
|
||||
flightsData.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "flight.read"), flightDataHandler.ListDatatable)
|
||||
}
|
||||
22
internal/app/api/routes_fm_report.go
Normal file
22
internal/app/api/routes_fm_report.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
sharedconst "wucher/internal/shared/const"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerFMReportRoutes(v1 fiber.Router, fmReportHandler *handlers.FMReportHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
fmReports := v1.Group("/fm-reports", authMiddleware)
|
||||
fmReports.Get("/get-all", middleware.PermissionRequired(authSvc, "flight.read"), fmReportHandler.List)
|
||||
fmReports.Get("/get/:flight_id", middleware.PermissionRequired(authSvc, "flight.read"), fmReportHandler.GetByFlightID)
|
||||
fmReports.Patch("/update/:flight_id", middleware.PermissionRequired(authSvc, "flight.update"), fmReportHandler.UpdateByFlightID)
|
||||
fmReports.Post("/complete/:flight_id", middleware.PermissionRequired(authSvc, "flight.complete"), fmReportHandler.Complete)
|
||||
fmReports.Post("/:id/attachments/upload-intent", middleware.PermissionRequired(authSvc, "flight.update"), fmReportHandler.RequestAttachmentUploadIntent)
|
||||
fmReports.Post("/:id/attachments", middleware.PermissionRequired(authSvc, "flight.update"), fmReportHandler.UploadAttachment)
|
||||
fmReports.Delete("/:id/attachments/:att_id", middleware.PermissionRequired(authSvc, "flight.update"), fmReportHandler.DeleteAttachment)
|
||||
fmReports.Delete("/:id", middleware.PermissionRequired(authSvc, "flight.delete"), middleware.PinRequired(authSvc, sharedconst.PinActionFmReportDelete), fmReportHandler.Delete)
|
||||
}
|
||||
18
internal/app/api/routes_forces_present.go
Normal file
18
internal/app/api/routes_forces_present.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerForcesPresentRoutes(v1 fiber.Router, forcesPresentHandler *handlers.ForcesPresentHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
forcesPresent := v1.Group("/forces-present", authMiddleware)
|
||||
forcesPresent.Post("/create", middleware.PermissionRequired(authSvc, "forces_present.create"), forcesPresentHandler.Create)
|
||||
forcesPresent.Get("/get-all", middleware.PermissionRequired(authSvc, "forces_present.read"), forcesPresentHandler.List)
|
||||
forcesPresent.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "forces_present.read"), forcesPresentHandler.ListDatatable)
|
||||
forcesPresent.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "forces_present.update"), forcesPresentHandler.Update)
|
||||
forcesPresent.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "forces_present.delete"), forcesPresentHandler.Delete)
|
||||
}
|
||||
18
internal/app/api/routes_health_insurance_companies.go
Normal file
18
internal/app/api/routes_health_insurance_companies.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerHealthInsuranceCompaniesRoutes(v1 fiber.Router, healthInsuranceCompaniesHandler *handlers.HealthInsuranceCompaniesHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
hic := v1.Group("/health-insurance-companies", authMiddleware)
|
||||
hic.Post("/create", middleware.PermissionRequired(authSvc, "health_insurance_companies.create"), healthInsuranceCompaniesHandler.Create)
|
||||
hic.Get("/get-all", middleware.PermissionRequired(authSvc, "health_insurance_companies.read"), healthInsuranceCompaniesHandler.List)
|
||||
hic.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "health_insurance_companies.read"), healthInsuranceCompaniesHandler.ListDatatable)
|
||||
hic.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "health_insurance_companies.update"), healthInsuranceCompaniesHandler.Update)
|
||||
hic.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "health_insurance_companies.delete"), healthInsuranceCompaniesHandler.Delete)
|
||||
}
|
||||
20
internal/app/api/routes_helicopter.go
Normal file
20
internal/app/api/routes_helicopter.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerHelicopterRoutes(v1 fiber.Router, helicopterHandler *handlers.HelicopterHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
helicopters := v1.Group("/helicopters", authMiddleware)
|
||||
helicopters.Post("/create", middleware.PermissionRequired(authSvc, "helicopter.create"), helicopterHandler.Create)
|
||||
helicopters.Post("/:id/reports/next-number", middleware.PermissionRequired(authSvc, "helicopter.report.generate"), helicopterHandler.NextReportNumber)
|
||||
helicopters.Get("/get-all", middleware.PermissionRequired(authSvc, "helicopter.read"), helicopterHandler.List)
|
||||
helicopters.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "helicopter.read"), helicopterHandler.ListDatatable)
|
||||
helicopters.Get("/get/:id", middleware.PermissionRequired(authSvc, "helicopter.read"), helicopterHandler.Get)
|
||||
helicopters.Patch("/update/:id", middleware.PermissionRequired(authSvc, "helicopter.update"), helicopterHandler.Update)
|
||||
helicopters.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "helicopter.delete"), helicopterHandler.Delete)
|
||||
}
|
||||
27
internal/app/api/routes_helicopter_file.go
Normal file
27
internal/app/api/routes_helicopter_file.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerHelicopterFileRoutes(
|
||||
v1 fiber.Router,
|
||||
handler *handlers.HelicopterFileHandler,
|
||||
eventsHandler *handlers.HelicopterFileEventsHandler,
|
||||
authMiddleware fiber.Handler,
|
||||
authSvc *service.AuthService,
|
||||
) {
|
||||
files := v1.Group("/helicopter-files", authMiddleware)
|
||||
files.Get("/events", middleware.PermissionRequired(authSvc, "helicopter.read"), eventsHandler.Stream)
|
||||
files.Post("/upload", middleware.PermissionRequired(authSvc, "helicopter.update"), handler.Upload)
|
||||
files.Post("/cancel-upload", middleware.PermissionRequired(authSvc, "helicopter.update"), handler.CancelUpload)
|
||||
files.Post("/create", middleware.PermissionRequired(authSvc, "helicopter.update"), handler.Create)
|
||||
files.Get("/get-all", middleware.PermissionRequired(authSvc, "helicopter.read"), handler.List)
|
||||
files.Get("/get/:helicopter_id", middleware.PermissionRequired(authSvc, "helicopter.read"), handler.Get)
|
||||
files.Patch("/update", middleware.PermissionRequired(authSvc, "helicopter.update"), handler.Update)
|
||||
files.Delete("/delete", middleware.PermissionRequired(authSvc, "helicopter.delete"), handler.Delete)
|
||||
}
|
||||
19
internal/app/api/routes_helicopter_usage.go
Normal file
19
internal/app/api/routes_helicopter_usage.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerHelicopterUsageRoutes(v1 fiber.Router, helicopterUsageHandler *handlers.HelicopterUsageHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
helicopterUsage := v1.Group("/helicopter-usage", authMiddleware)
|
||||
helicopterUsage.Post("/create", middleware.PermissionRequired(authSvc, "helicopter_usage.create"), helicopterUsageHandler.Create)
|
||||
helicopterUsage.Get("/get-all", middleware.PermissionRequired(authSvc, "helicopter_usage.read"), helicopterUsageHandler.List)
|
||||
helicopterUsage.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "helicopter_usage.read"), helicopterUsageHandler.ListDatatable)
|
||||
helicopterUsage.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "helicopter_usage.read"), helicopterUsageHandler.Get)
|
||||
helicopterUsage.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "helicopter_usage.update"), helicopterUsageHandler.Update)
|
||||
helicopterUsage.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "helicopter_usage.delete"), helicopterUsageHandler.Delete)
|
||||
}
|
||||
19
internal/app/api/routes_hems_operation.go
Normal file
19
internal/app/api/routes_hems_operation.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerHEMSOperationRoutes(v1 fiber.Router, h *handlers.HEMSOperationHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
r := v1.Group("/hems-operation", authMiddleware)
|
||||
r.Post("/create", middleware.PermissionRequired(authSvc, "hems_operation.create"), h.Create)
|
||||
r.Get("/get-all", middleware.PermissionRequired(authSvc, "hems_operation.read"), h.List)
|
||||
r.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "hems_operation.read"), h.ListDatatable)
|
||||
r.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "hems_operation.read"), h.Get)
|
||||
r.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "hems_operation.update"), h.Update)
|
||||
r.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "hems_operation.delete"), h.Delete)
|
||||
}
|
||||
19
internal/app/api/routes_hems_operation_category.go
Normal file
19
internal/app/api/routes_hems_operation_category.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerHEMSOperationCategoryRoutes(v1 fiber.Router, h *handlers.HEMSOperationCategoryHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
r := v1.Group("/hems-operation-category", authMiddleware)
|
||||
r.Post("/create", middleware.PermissionRequired(authSvc, "operation_category.create"), h.Create)
|
||||
r.Get("/get-all", middleware.PermissionRequired(authSvc, "operation_category.read"), h.List)
|
||||
r.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "operation_category.read"), h.ListDatatable)
|
||||
r.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "operation_category.read"), h.Get)
|
||||
r.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "operation_category.update"), h.Update)
|
||||
r.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "operation_category.delete"), h.Delete)
|
||||
}
|
||||
19
internal/app/api/routes_hems_operational_data.go
Normal file
19
internal/app/api/routes_hems_operational_data.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerHEMSOperationalDataRoutes(v1 fiber.Router, h *handlers.HEMSOperationalDataHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
r := v1.Group("/hems-operational-data", authMiddleware)
|
||||
r.Post("/create", middleware.PermissionRequired(authSvc, "operation.create"), h.Create)
|
||||
r.Get("/get-all", middleware.PermissionRequired(authSvc, "operation.read"), h.List)
|
||||
r.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "operation.read"), h.ListDatatable)
|
||||
r.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "operation.read"), h.Get)
|
||||
r.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "operation.update"), h.Update)
|
||||
r.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "operation.delete"), h.Delete)
|
||||
}
|
||||
18
internal/app/api/routes_hospital.go
Normal file
18
internal/app/api/routes_hospital.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerHospitalRoutes(v1 fiber.Router, hospitalHandler *handlers.HospitalHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
hospital := v1.Group("/hospital", authMiddleware)
|
||||
hospital.Post("/create", middleware.PermissionRequired(authSvc, "no_icao_code.create"), hospitalHandler.Create)
|
||||
hospital.Get("/get-all", middleware.PermissionRequired(authSvc, "no_icao_code.read"), hospitalHandler.List)
|
||||
hospital.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "no_icao_code.read"), hospitalHandler.ListDatatable)
|
||||
hospital.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "no_icao_code.update"), hospitalHandler.Update)
|
||||
hospital.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "no_icao_code.delete"), hospitalHandler.Delete)
|
||||
}
|
||||
18
internal/app/api/routes_icao.go
Normal file
18
internal/app/api/routes_icao.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerICAORoutes(v1 fiber.Router, icaoHandler *handlers.ICAOHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
icao := v1.Group("/icao", authMiddleware)
|
||||
icao.Post("/create", middleware.PermissionRequired(authSvc, "icao.create"), icaoHandler.Create)
|
||||
icao.Get("/get-all", middleware.PermissionRequired(authSvc, "icao.read"), icaoHandler.List)
|
||||
icao.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "icao.read"), icaoHandler.ListDatatable)
|
||||
icao.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "icao.update"), icaoHandler.Update)
|
||||
icao.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "icao.delete"), icaoHandler.Delete)
|
||||
}
|
||||
18
internal/app/api/routes_land.go
Normal file
18
internal/app/api/routes_land.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerLandRoutes(v1 fiber.Router, landHandler *handlers.LandHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
land := v1.Group("/land", authMiddleware)
|
||||
land.Post("/create", middleware.PermissionRequired(authSvc, "country.create"), landHandler.Create)
|
||||
land.Get("/get-all", middleware.PermissionRequired(authSvc, "country.read"), landHandler.List)
|
||||
land.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "country.read"), landHandler.ListDatatable)
|
||||
land.Patch("/update/:id", middleware.PermissionRequired(authSvc, "country.update"), landHandler.Update)
|
||||
land.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "country.delete"), landHandler.Delete)
|
||||
}
|
||||
17
internal/app/api/routes_last_flights_inspection.go
Normal file
17
internal/app/api/routes_last_flights_inspection.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerLastFlightsInspectionRoutes(v1 fiber.Router, reserveAcHandler *handlers.ReserveAcHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
lastFlightsInspection := v1.Group("/after-flight-inspection", authMiddleware)
|
||||
lastFlightsInspection.Get("/get", middleware.PermissionRequired(authSvc, "after_flight.read"), reserveAcHandler.GetLastFlightsInspection)
|
||||
lastFlightsInspection.Post("/create", middleware.PermissionRequired(authSvc, "after_flight.create"), reserveAcHandler.CreateLastFlightsInspection)
|
||||
lastFlightsInspection.Patch("/update", middleware.PermissionRequired(authSvc, "after_flight.update"), reserveAcHandler.UpdateLastFlightsInspection)
|
||||
lastFlightsInspection.Delete("/delete", middleware.PermissionRequired(authSvc, "after_flight.delete"), reserveAcHandler.DeleteLastFlightsInspection)
|
||||
}
|
||||
21
internal/app/api/routes_master_settings.go
Normal file
21
internal/app/api/routes_master_settings.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerMasterSettingsRoutes(v1 fiber.Router, masterSettingsHandler *handlers.MasterSettingsHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
masterSettingsPublic := v1.Group("/master-settings")
|
||||
masterSettingsPublic.Get("/get", masterSettingsHandler.Get)
|
||||
|
||||
masterSettings := v1.Group("/master-settings", authMiddleware)
|
||||
masterSettings.Post("/create", middleware.PermissionRequired(authSvc, "master_settings.create"), masterSettingsHandler.Create)
|
||||
masterSettings.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "master_settings.update"), masterSettingsHandler.Update)
|
||||
masterSettings.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "master_settings.delete"), masterSettingsHandler.Delete)
|
||||
masterSettings.Get("/sso/microsoft", middleware.PermissionRequired(authSvc, "master_settings.update"), masterSettingsHandler.GetMicrosoftEntraSSO)
|
||||
masterSettings.Put("/sso/microsoft", middleware.PermissionRequired(authSvc, "master_settings.update"), masterSettingsHandler.UpsertMicrosoftEntraSSO)
|
||||
}
|
||||
17
internal/app/api/routes_mcf.go
Normal file
17
internal/app/api/routes_mcf.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerMCFRoutes(v1 fiber.Router, h *handlers.MCFHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
g := v1.Group("/mcf", authMiddleware)
|
||||
g.Post("/set/:helicopter_id", middleware.PermissionRequired(authSvc, "mcf.create"), h.Set)
|
||||
g.Get("/get-by-helicopter/:helicopter_id", middleware.PermissionRequired(authSvc, "mcf.read"), h.ListByHelicopter)
|
||||
g.Get("/get/:id", middleware.PermissionRequired(authSvc, "mcf.read"), h.GetByID)
|
||||
g.Post("/sign/:id", middleware.PermissionRequired(authSvc, "mcf.complete"), h.Sign)
|
||||
}
|
||||
32
internal/app/api/routes_medicine.go
Normal file
32
internal/app/api/routes_medicine.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerMedicineRoutes(v1 fiber.Router, medicineHandler *handlers.MedicineHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
medicine := v1.Group("/medicine", authMiddleware)
|
||||
medicine.Post("/create", middleware.PermissionRequired(authSvc, "medicine.create"), medicineHandler.Create)
|
||||
medicine.Get("/get-all", middleware.PermissionRequired(authSvc, "medicine.read"), medicineHandler.List)
|
||||
medicine.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "medicine.read"), medicineHandler.ListDatatable)
|
||||
medicine.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "medicine.update"), medicineHandler.Update)
|
||||
medicine.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "medicine.delete"), medicineHandler.Delete)
|
||||
|
||||
medicineGroup := v1.Group("/medicine-group", authMiddleware)
|
||||
medicineGroup.Post("/create", middleware.PermissionRequired(authSvc, "medicine.create"), medicineHandler.CreateGroup)
|
||||
medicineGroup.Get("/get-all", middleware.PermissionRequired(authSvc, "medicine.read"), medicineHandler.ListGroups)
|
||||
medicineGroup.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "medicine.read"), medicineHandler.ListGroupsDatatable)
|
||||
medicineGroup.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "medicine.update"), medicineHandler.UpdateGroup)
|
||||
medicineGroup.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "medicine.delete"), medicineHandler.DeleteGroup)
|
||||
|
||||
motorReaction := v1.Group("/motor-reaction", authMiddleware)
|
||||
motorReaction.Post("/create", middleware.PermissionRequired(authSvc, "medicine.create"), medicineHandler.CreateReaction)
|
||||
motorReaction.Get("/get-all", middleware.PermissionRequired(authSvc, "medicine.read"), medicineHandler.ListReactions)
|
||||
motorReaction.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "medicine.read"), medicineHandler.ListReactionsDatatable)
|
||||
motorReaction.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "medicine.update"), medicineHandler.UpdateReaction)
|
||||
motorReaction.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "medicine.delete"), medicineHandler.DeleteReaction)
|
||||
}
|
||||
26
internal/app/api/routes_mission.go
Normal file
26
internal/app/api/routes_mission.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerMissionRoutes(v1 fiber.Router, missionHandler *handlers.MissionHandler, eventsHandler *handlers.FileManagerEventsHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
mission := v1.Group("/mission", authMiddleware)
|
||||
mission.Get("/events", middleware.PermissionRequired(authSvc, "flight.read"), eventsHandler.Stream)
|
||||
mission.Post("/create", middleware.PermissionRequired(authSvc, "flight.create"), missionHandler.Create)
|
||||
mission.Patch("/update/:mission_id", middleware.PermissionRequired(authSvc, "flight.update"), missionHandler.Update)
|
||||
mission.Delete("/file/:mission_id/:attachment_id", middleware.PermissionRequired(authSvc, "flight.update"), missionHandler.RemoveFile)
|
||||
mission.Delete("/delete/:flight_id", middleware.PermissionRequired(authSvc, "flight.delete"), missionHandler.Delete)
|
||||
mission.Delete("/delete-by-id/:mission_id", middleware.PermissionRequired(authSvc, "flight.delete"), missionHandler.DeleteByID)
|
||||
mission.Get("/get/:flight_id", middleware.PermissionRequired(authSvc, "flight.read"), missionHandler.GetByFlightID)
|
||||
mission.Get("/get-all", middleware.PermissionRequired(authSvc, "flight.read"), missionHandler.List)
|
||||
mission.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "flight.read"), missionHandler.ListDatatable)
|
||||
|
||||
missionType := v1.Group("/mission-type", authMiddleware)
|
||||
missionType.Post("/create", middleware.PermissionRequired(authSvc, "flight.create"), missionHandler.CreateType)
|
||||
missionType.Get("/get-all", middleware.PermissionRequired(authSvc, "flight.read"), missionHandler.ListTypes)
|
||||
}
|
||||
18
internal/app/api/routes_opc.go
Normal file
18
internal/app/api/routes_opc.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerOpcRoutes(v1 fiber.Router, opcHandler *handlers.OpcHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
opc := v1.Group("/opc", authMiddleware)
|
||||
opc.Post("/create", middleware.PermissionRequired(authSvc, "opc.create"), opcHandler.Create)
|
||||
opc.Get("/get-all", middleware.PermissionRequired(authSvc, "opc.read"), opcHandler.List)
|
||||
opc.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "opc.read"), opcHandler.ListDatatable)
|
||||
opc.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "opc.update"), opcHandler.Update)
|
||||
opc.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "opc.delete"), opcHandler.Delete)
|
||||
}
|
||||
20
internal/app/api/routes_other_person.go
Normal file
20
internal/app/api/routes_other_person.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
// registerOtherPersonRoutes exposes the reusable, NON-STAFF "other person" master under
|
||||
// /contact/other. It is kept separate from /contacts (staff) and /takeovers on purpose.
|
||||
func registerOtherPersonRoutes(v1 fiber.Router, otherPersonHandler *handlers.OtherPersonHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
other := v1.Group("/contact/other", authMiddleware)
|
||||
other.Get("/", middleware.PermissionRequired(authSvc, "reserve_ac.read"), otherPersonHandler.Search)
|
||||
other.Get("/get/:id", middleware.PermissionRequired(authSvc, "reserve_ac.read"), otherPersonHandler.Get)
|
||||
other.Post("/create", middleware.PermissionRequired(authSvc, "reserve_ac.create"), otherPersonHandler.Create)
|
||||
other.Patch("/update/:id", middleware.PermissionRequired(authSvc, "reserve_ac.update"), otherPersonHandler.Update)
|
||||
other.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "reserve_ac.delete"), otherPersonHandler.Delete)
|
||||
}
|
||||
29
internal/app/api/routes_patient_data.go
Normal file
29
internal/app/api/routes_patient_data.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerInsurancePatientDataRoutes(v1 fiber.Router, h *handlers.InsurancePatientDataHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
g := v1.Group("/insurance-patient-data", authMiddleware)
|
||||
g.Post("/create", middleware.PermissionRequired(authSvc, "insurance_patient_data.create"), h.Create)
|
||||
g.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "insurance_patient_data.read"), h.Get)
|
||||
g.Get("/get-all", middleware.PermissionRequired(authSvc, "insurance_patient_data.read"), h.List)
|
||||
g.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "insurance_patient_data.read"), h.ListDatatable)
|
||||
g.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "insurance_patient_data.update"), h.Update)
|
||||
g.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "insurance_patient_data.delete"), h.Delete)
|
||||
}
|
||||
|
||||
func registerPatientDataRoutes(v1 fiber.Router, h *handlers.PatientDataHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
g := v1.Group("/patient-data", authMiddleware)
|
||||
g.Post("/create", middleware.PermissionRequired(authSvc, "patient_data.create"), h.Create)
|
||||
g.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "patient_data.read"), h.Get)
|
||||
g.Get("/get-all", middleware.PermissionRequired(authSvc, "patient_data.read"), h.List)
|
||||
g.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "patient_data.read"), h.ListDatatable)
|
||||
g.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "patient_data.update"), h.Update)
|
||||
g.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "patient_data.delete"), h.Delete)
|
||||
}
|
||||
20
internal/app/api/routes_reserve_ac.go
Normal file
20
internal/app/api/routes_reserve_ac.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerReserveAcRoutes(v1 fiber.Router, reserveAcHandler *handlers.ReserveAcHandler, takeoverHandler *handlers.TakeoverHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
reserveAcs := v1.Group("/reserve-acs", authMiddleware)
|
||||
reserveAcs.Post("/takeovers/create", middleware.PermissionRequired(authSvc, "reserve_ac.create"), takeoverHandler.CreateTakeover)
|
||||
reserveAcs.Post("/create", middleware.PermissionRequired(authSvc, "reserve_ac.create"), reserveAcHandler.Create)
|
||||
reserveAcs.Get("/get-all", middleware.PermissionRequired(authSvc, "reserve_ac.read"), reserveAcHandler.List)
|
||||
reserveAcs.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "reserve_ac.read"), reserveAcHandler.ListDatatable)
|
||||
reserveAcs.Get("/get/:id", middleware.PermissionRequired(authSvc, "reserve_ac.read"), reserveAcHandler.Get)
|
||||
reserveAcs.Patch("/update", middleware.PermissionRequired(authSvc, "reserve_ac.update"), reserveAcHandler.Update)
|
||||
reserveAcs.Delete("/delete", middleware.PermissionRequired(authSvc, "reserve_ac.delete"), reserveAcHandler.Delete)
|
||||
}
|
||||
26
internal/app/api/routes_role.go
Normal file
26
internal/app/api/routes_role.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerRoleRoutes(v1 fiber.Router, roleHandler *handlers.RoleHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
roles := v1.Group("/roles", authMiddleware)
|
||||
roles.Post("/create", middleware.PermissionRequired(authSvc, "role.create"), roleHandler.Create)
|
||||
roles.Get("/get-all", middleware.PermissionRequired(authSvc, "role.read"), roleHandler.List)
|
||||
roles.Get("/permissions/get-all", middleware.PermissionRequired(authSvc, "role.permission.read"), roleHandler.ListPermissions)
|
||||
roles.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "role.read"), roleHandler.ListDatatable)
|
||||
roles.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "role.read"), roleHandler.GetByID)
|
||||
roles.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "role.update"), roleHandler.Update)
|
||||
roles.Patch("/permissions/:permission_uuid", middleware.PermissionRequired(authSvc, "role.permission.update"), roleHandler.UpdatePermission)
|
||||
roles.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "role.delete"), roleHandler.Delete)
|
||||
roles.Get("/:uuid/permissions", middleware.PermissionRequired(authSvc, "role.permission.read"), roleHandler.ListRolePermissions)
|
||||
roles.Post("/:uuid/permissions", middleware.PermissionRequired(authSvc, "role.permission.assign"), roleHandler.AssignPermission)
|
||||
roles.Delete("/:uuid/permissions/:permission_uuid", middleware.PermissionRequired(authSvc, "role.permission.remove"), roleHandler.RemovePermission)
|
||||
roles.Post("/:uuid/permissions/assign-bulk", middleware.PermissionRequired(authSvc, "role.permission.assign"), roleHandler.AssignPermissionsBulk)
|
||||
roles.Post("/:uuid/permissions/remove-bulk", middleware.PermissionRequired(authSvc, "role.permission.remove"), roleHandler.RemovePermissionsBulk)
|
||||
}
|
||||
21
internal/app/api/routes_saml_auth.go
Normal file
21
internal/app/api/routes_saml_auth.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
|
||||
func registerSAMLAuthRoutes(v1 fiber.Router, samlHandler interface {
|
||||
HandleLogin(*fiber.Ctx) error
|
||||
HandleSAMLCallback(*fiber.Ctx) error
|
||||
HandleSLO(*fiber.Ctx) error
|
||||
HandleMetadata(*fiber.Ctx) error
|
||||
HandleLogout(*fiber.Ctx) error
|
||||
}) {
|
||||
if samlHandler == nil {
|
||||
return
|
||||
}
|
||||
auth := v1.Group("/auth")
|
||||
auth.Get("/login", samlHandler.HandleLogin)
|
||||
auth.Post("/saml/callback", samlHandler.HandleSAMLCallback)
|
||||
auth.Post("/saml/logout", samlHandler.HandleSLO)
|
||||
auth.Get("/saml/metadata", samlHandler.HandleMetadata)
|
||||
auth.Post("/saml/app-logout", samlHandler.HandleLogout)
|
||||
}
|
||||
10
internal/app/api/routes_swagger.go
Normal file
10
internal/app/api/routes_swagger.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/swagger"
|
||||
)
|
||||
|
||||
func registerSwaggerRoutes(app *fiber.App) {
|
||||
app.Get("/swagger/*", swagger.HandlerDefault)
|
||||
}
|
||||
18
internal/app/api/routes_takeover.go
Normal file
18
internal/app/api/routes_takeover.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerTakeoverRoutes(v1 fiber.Router, takeoverHandler *handlers.TakeoverHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
takeovers := v1.Group("/takeovers", authMiddleware)
|
||||
takeovers.Post("/create", middleware.PermissionRequired(authSvc, "reserve_ac.create"), takeoverHandler.CreateTakeover)
|
||||
takeovers.Get("/get-all", middleware.PermissionRequired(authSvc, "reserve_ac.read"), takeoverHandler.ListTakeovers)
|
||||
takeovers.Get("/get/:id", middleware.PermissionRequired(authSvc, "reserve_ac.read"), takeoverHandler.GetTakeover)
|
||||
takeovers.Patch("/update/:id", middleware.PermissionRequired(authSvc, "reserve_ac.update"), takeoverHandler.UpdateTakeover)
|
||||
takeovers.Delete("/delete/:id", middleware.PermissionRequired(authSvc, "reserve_ac.delete"), takeoverHandler.DeleteTakeover)
|
||||
}
|
||||
243
internal/app/api/routes_test.go
Normal file
243
internal/app/api/routes_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestRegisterRoutes(t *testing.T) {
|
||||
app := fiber.New()
|
||||
RegisterRoutes(app, buildRouteDependenciesForTest())
|
||||
|
||||
routes := app.GetRoutes(true)
|
||||
required := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{method: http.MethodGet, path: "/health"},
|
||||
{method: http.MethodGet, path: "/version"},
|
||||
{method: http.MethodGet, path: "/api/version"},
|
||||
{method: http.MethodGet, path: "/api/build-info"},
|
||||
{method: http.MethodGet, path: "/metrics"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/register"},
|
||||
{method: http.MethodPost, path: "/api/v1/auth/forgot-password"},
|
||||
{method: http.MethodPost, path: "/api/v1/users/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/users/events"},
|
||||
{method: http.MethodPost, path: "/api/v1/contacts/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/contacts/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/contacts/get-all/dt"},
|
||||
{method: http.MethodPost, path: "/api/v1/roles/create"},
|
||||
{method: http.MethodPost, path: "/api/v1/helicopters/create"},
|
||||
{method: http.MethodPost, path: "/api/v1/helicopter-usage/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/helicopter-usage/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/helicopter-usage/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/helicopter-usage/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/helicopter-usage/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/helicopter-usage/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/helicopters/:id/reports/next-number"},
|
||||
{method: http.MethodPost, path: "/api/v1/helicopter-files/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/helicopter-files/events"},
|
||||
{method: http.MethodPost, path: "/api/v1/helicopter-files/upload"},
|
||||
{method: http.MethodPost, path: "/api/v1/helicopter-files/cancel-upload"},
|
||||
{method: http.MethodGet, path: "/api/v1/helicopter-files/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/helicopter-files/get/:helicopter_id"},
|
||||
{method: http.MethodPatch, path: "/api/v1/helicopter-files/update"},
|
||||
{method: http.MethodDelete, path: "/api/v1/helicopter-files/delete"},
|
||||
{method: http.MethodGet, path: "/api/v1/flights/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/flights/get"},
|
||||
{method: http.MethodGet, path: "/api/v1/fm-reports/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/fm-reports/get/:flight_id"},
|
||||
{method: http.MethodPatch, path: "/api/v1/fm-reports/update/:flight_id"},
|
||||
{method: http.MethodPost, path: "/api/v1/fm-reports/complete/:flight_id"},
|
||||
{method: http.MethodPost, path: "/api/v1/flight-data/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/flight-data/get/:uuid"},
|
||||
{method: http.MethodGet, path: "/api/v1/flight-data/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/flight-data/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/flights-data/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/flights-data/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/mission-type/get-all"},
|
||||
{method: http.MethodDelete, path: "/api/v1/mission/delete-by-id/:mission_id"},
|
||||
{method: http.MethodPost, path: "/api/v1/reserve-acs/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/reserve-acs/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/reserve-acs/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/reserve-acs/get/:id"},
|
||||
{method: http.MethodPatch, path: "/api/v1/reserve-acs/update"},
|
||||
{method: http.MethodDelete, path: "/api/v1/reserve-acs/delete"},
|
||||
{method: http.MethodGet, path: "/api/v1/after-flight-inspection/get"},
|
||||
{method: http.MethodPost, path: "/api/v1/after-flight-inspection/create"},
|
||||
{method: http.MethodPatch, path: "/api/v1/after-flight-inspection/update"},
|
||||
{method: http.MethodDelete, path: "/api/v1/after-flight-inspection/delete"},
|
||||
{method: http.MethodPost, path: "/api/v1/facilities/create"},
|
||||
{method: http.MethodPost, path: "/api/v1/bases/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/bases/events"},
|
||||
{method: http.MethodPost, path: "/api/v1/medicine/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/medicine/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/medicine/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/medicine/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/medicine/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/medicine-group/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/medicine-group/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/medicine-group/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/medicine-group/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/medicine-group/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/motor-reaction/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/motor-reaction/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/motor-reaction/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/motor-reaction/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/motor-reaction/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/vocation/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/vocation/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/vocation/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/vocation/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/vocation/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/opc/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/opc/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/opc/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/opc/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/opc/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/forces-present/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/forces-present/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/forces-present/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/forces-present/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/forces-present/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/hospital/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/hospital/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/hospital/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/hospital/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/hospital/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/health-insurance-companies/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/health-insurance-companies/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/health-insurance-companies/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/health-insurance-companies/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/health-insurance-companies/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/federal-state/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/federal-state/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/federal-state/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/federal-state/update/:id"},
|
||||
{method: http.MethodDelete, path: "/api/v1/federal-state/delete/:id"},
|
||||
{method: http.MethodPost, path: "/api/v1/icao/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/icao/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/icao/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/icao/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/icao/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/land/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/land/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/land/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/land/update/:id"},
|
||||
{method: http.MethodDelete, path: "/api/v1/land/delete/:id"},
|
||||
{method: http.MethodPost, path: "/api/v1/master-settings/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/master-settings/get"},
|
||||
{method: http.MethodPatch, path: "/api/v1/master-settings/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/master-settings/delete/:uuid"},
|
||||
{method: http.MethodGet, path: "/api/v1/branding/logo"},
|
||||
{method: http.MethodGet, path: "/api/v1/branding/login-cover"},
|
||||
{method: http.MethodGet, path: "/api/v1/branding/login-content"},
|
||||
{method: http.MethodGet, path: "/api/v1/file-manager/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/file-manager/events"},
|
||||
{method: http.MethodGet, path: "/api/v1/file-manager/trash/get-all"},
|
||||
{method: http.MethodPatch, path: "/api/v1/file-manager/move"},
|
||||
{method: http.MethodDelete, path: "/api/v1/file-manager/delete"},
|
||||
{method: http.MethodPatch, path: "/api/v1/file-manager/restore"},
|
||||
{method: http.MethodDelete, path: "/api/v1/file-manager/purge-delete"},
|
||||
{method: http.MethodPost, path: "/api/v1/file-manager/folders/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/file-manager/folders/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/file-manager/folders/update/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/file-manager/files/create"},
|
||||
{method: http.MethodPost, path: "/api/v1/file-manager/files/upload"},
|
||||
{method: http.MethodDelete, path: "/api/v1/file-manager/files/cancel-upload"},
|
||||
{method: http.MethodGet, path: "/api/v1/file-manager/files/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/file-manager/files/update/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/file-manager/attachments/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/file-manager/attachments/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/file-manager/attachments/get/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/file-manager/attachments/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/duty-roster/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/duty-roster/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/duty-roster/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/duty-roster/update"},
|
||||
{method: http.MethodDelete, path: "/api/v1/duty-roster/delete"},
|
||||
{method: http.MethodDelete, path: "/api/v1/duty-roster/crew"},
|
||||
{method: http.MethodPost, path: "/api/v1/air-rescuer-checklist/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/air-rescuer-checklist/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/air-rescuer-checklist/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/air-rescuer-checklist/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/air-rescuer-checklist/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/air-rescuer-checklist/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/air-rescuer-checklist/:uuid/items/create"},
|
||||
{method: http.MethodPatch, path: "/api/v1/air-rescuer-checklist/:uuid/items/reorder"},
|
||||
{method: http.MethodPatch, path: "/api/v1/air-rescuer-checklist/items/update/:item_uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/air-rescuer-checklist/items/delete/:item_uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/insurance-patient-data/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/insurance-patient-data/get/:uuid"},
|
||||
{method: http.MethodGet, path: "/api/v1/insurance-patient-data/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/insurance-patient-data/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/insurance-patient-data/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/insurance-patient-data/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/patient-data/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/patient-data/get/:uuid"},
|
||||
{method: http.MethodGet, path: "/api/v1/patient-data/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/patient-data/get-all/dt"},
|
||||
{method: http.MethodPatch, path: "/api/v1/patient-data/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/patient-data/delete/:uuid"},
|
||||
{method: http.MethodGet, path: "/api/v1/dul/events"},
|
||||
{method: http.MethodPost, path: "/api/v1/hems-operational-data/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operational-data/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operational-data/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operational-data/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/hems-operational-data/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/hems-operational-data/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/hems-operation-category/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operation-category/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operation-category/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operation-category/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/hems-operation-category/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/hems-operation-category/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/hems-operation/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operation/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operation/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-operation/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/hems-operation/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/hems-operation/delete/:uuid"},
|
||||
{method: http.MethodGet, path: "/swagger/*"},
|
||||
}
|
||||
|
||||
for _, tc := range required {
|
||||
if !hasRoute(routes, tc.method, tc.path) {
|
||||
t.Fatalf("route %s %s not registered", tc.method, tc.path)
|
||||
}
|
||||
}
|
||||
|
||||
legacy := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{method: http.MethodPost, path: "/api/v1/crews/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/crews/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/crews/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/crews/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/crews/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/crews/delete/:uuid"},
|
||||
{method: http.MethodPost, path: "/api/v1/hems-bases/create"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-bases/get-all"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-bases/get-all/dt"},
|
||||
{method: http.MethodGet, path: "/api/v1/hems-bases/get/:uuid"},
|
||||
{method: http.MethodPatch, path: "/api/v1/hems-bases/update/:uuid"},
|
||||
{method: http.MethodDelete, path: "/api/v1/hems-bases/delete/:uuid"},
|
||||
}
|
||||
for _, tc := range legacy {
|
||||
if hasRoute(routes, tc.method, tc.path) {
|
||||
t.Fatalf("legacy route %s %s should not be registered", tc.method, tc.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasRoute(routes []fiber.Route, method, path string) bool {
|
||||
for _, route := range routes {
|
||||
if route.Method == method && route.Path == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
58
internal/app/api/routes_test_helpers_test.go
Normal file
58
internal/app/api/routes_test_helpers_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
)
|
||||
|
||||
func buildRouteDependenciesForTest() RouteDependencies {
|
||||
authSvc := service.NewAuthService(nil, nil, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
|
||||
return RouteDependencies{
|
||||
Handlers: RouteHandlers{
|
||||
User: handlers.NewUserHandler(service.NewUserService(nil)),
|
||||
Contact: handlers.NewContactHandler(service.NewContactService(nil, nil), nil),
|
||||
Health: handlers.NewHealthHandler(),
|
||||
BuildInfo: handlers.NewBuildInfoHandler(),
|
||||
Auth: handlers.NewAuthHandler(authSvc),
|
||||
Role: handlers.NewRoleHandler(service.NewRoleService(nil)),
|
||||
Helicopter: handlers.NewHelicopterHandler(service.NewHelicopterService(nil)),
|
||||
HelicopterUsage: handlers.NewHelicopterUsageHandler(service.NewHelicopterUsageService(nil)),
|
||||
HelicopterFile: handlers.NewHelicopterFileHandler(service.NewHelicopterFileService(nil)),
|
||||
Flight: handlers.NewFlightHandler(service.NewFlightService(nil)).WithMissionService(service.NewMissionService(nil)),
|
||||
FMReport: handlers.NewFMReportHandler(service.NewFMReportService(nil)).WithTakeoverService(service.NewTakeoverService(nil)).WithHelicopterUsageService(service.NewHelicopterUsageService(nil)),
|
||||
ReserveAc: handlers.NewReserveAcHandler(service.NewReserveAcService(nil)),
|
||||
Facility: handlers.NewFacilityHandler(service.NewFacilityService(nil)),
|
||||
Base: handlers.NewBaseHandler(service.NewBaseService(nil)),
|
||||
Medicine: handlers.NewMedicineHandler(service.NewMedicineService(nil)),
|
||||
Vocation: handlers.NewVocationHandler(service.NewVocationService(nil)),
|
||||
Opc: handlers.NewOpcHandler(service.NewOpcService(nil)),
|
||||
ForcesPresent: handlers.NewForcesPresentHandler(service.NewForcesPresentService(nil)),
|
||||
Hospital: handlers.NewHospitalHandler(service.NewHospitalService(nil)),
|
||||
HealthInsuranceCompanies: handlers.NewHealthInsuranceCompaniesHandler(service.NewHealthInsuranceCompaniesService(nil)),
|
||||
FederalState: handlers.NewFederalStateHandler(service.NewFederalStateService(nil)),
|
||||
ICAO: handlers.NewICAOHandler(service.NewICAOService(nil)),
|
||||
Land: handlers.NewLandHandler(service.NewLandService(nil)),
|
||||
MasterSettings: handlers.NewMasterSettingsHandler(service.NewMasterSettingsService(nil, service.MasterSettingsServiceDependencies{}), service.NewFileManagerService(nil, nil, nil, service.FileManagerServiceDependencies{}), nil),
|
||||
Branding: handlers.NewBrandingHandler(service.NewMasterSettingsService(nil, service.MasterSettingsServiceDependencies{}), service.NewFileManagerService(nil, nil, nil, service.FileManagerServiceDependencies{}), nil),
|
||||
FileManagerFolder: handlers.NewFileManagerFolderHandler(service.NewFileManagerService(nil, nil, nil, service.FileManagerServiceDependencies{}), nil),
|
||||
FileManagerFile: handlers.NewFileManagerFileHandler(service.NewFileManagerService(nil, nil, nil, service.FileManagerServiceDependencies{}), nil).
|
||||
WithAircraftService(service.NewHelicopterService(nil)).
|
||||
WithBaseService(service.NewBaseService(nil)).
|
||||
WithTakeoverService(service.NewTakeoverService(nil)),
|
||||
FileManagerAttachment: handlers.NewFileManagerAttachmentHandler(service.NewFileManagerService(nil, nil, nil, service.FileManagerServiceDependencies{})),
|
||||
FileManagerEvents: handlers.NewFileManagerEventsHandler(nil),
|
||||
DutyRoster: handlers.NewDutyRosterHandler(service.NewDutyRosterService(nil)),
|
||||
AirRescuerChecklist: handlers.NewAirRescuerChecklistHandler(service.NewAirRescuerChecklistService(nil)),
|
||||
InsurancePatientData: handlers.NewInsurancePatientDataHandler(service.NewInsurancePatientDataService(nil)),
|
||||
PatientData: handlers.NewPatientDataHandler(service.NewPatientDataService(nil)),
|
||||
HEMSOperationalData: handlers.NewHEMSOperationalDataHandler(service.NewHEMSOperationalDataService(nil)),
|
||||
HEMSOperationCategory: handlers.NewHEMSOperationCategoryHandler(service.NewHEMSOperationCategoryService(nil)),
|
||||
HEMSOperation: handlers.NewHEMSOperationHandler(service.NewHEMSOperationService(nil)),
|
||||
},
|
||||
Services: RouteServices{
|
||||
Auth: authSvc,
|
||||
Audit: service.NewAuditLogService(nil, service.AuditLogServiceDependencies{QueueSize: 0, Workers: 0, IPEncryptionKey: ""}),
|
||||
},
|
||||
}
|
||||
}
|
||||
26
internal/app/api/routes_user.go
Normal file
26
internal/app/api/routes_user.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerUserRoutes(
|
||||
v1 fiber.Router,
|
||||
userHandler *handlers.UserHandler,
|
||||
eventsHandler *handlers.FileManagerEventsHandler,
|
||||
authMiddleware fiber.Handler,
|
||||
authSvc *service.AuthService,
|
||||
) {
|
||||
users := v1.Group("/users", authMiddleware)
|
||||
users.Get("/events", middleware.PermissionRequired(authSvc, "user.read"), eventsHandler.Stream)
|
||||
users.Post("/create", middleware.PermissionRequired(authSvc, "user.create"), userHandler.Create)
|
||||
users.Get("/get-all", middleware.PermissionRequired(authSvc, "user.read"), userHandler.List)
|
||||
users.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "user.read"), userHandler.ListDatatable)
|
||||
users.Get("/get/:uuid", middleware.PermissionRequired(authSvc, "user.read"), userHandler.Get)
|
||||
users.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "user.update"), userHandler.Update)
|
||||
users.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "user.delete"), userHandler.Delete)
|
||||
}
|
||||
18
internal/app/api/routes_vocation.go
Normal file
18
internal/app/api/routes_vocation.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerVocationRoutes(v1 fiber.Router, vocationHandler *handlers.VocationHandler, authMiddleware fiber.Handler, authSvc *service.AuthService) {
|
||||
vocation := v1.Group("/vocation", authMiddleware)
|
||||
vocation.Post("/create", middleware.PermissionRequired(authSvc, "vocation.create"), vocationHandler.Create)
|
||||
vocation.Get("/get-all", middleware.PermissionRequired(authSvc, "vocation.read"), vocationHandler.List)
|
||||
vocation.Get("/get-all/dt", middleware.PermissionRequired(authSvc, "vocation.read"), vocationHandler.ListDatatable)
|
||||
vocation.Patch("/update/:uuid", middleware.PermissionRequired(authSvc, "vocation.update"), vocationHandler.Update)
|
||||
vocation.Delete("/delete/:uuid", middleware.PermissionRequired(authSvc, "vocation.delete"), vocationHandler.Delete)
|
||||
}
|
||||
23
internal/app/api/routes_wopi.go
Normal file
23
internal/app/api/routes_wopi.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/transport/http/handlers"
|
||||
"wucher/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
func registerWOPIRoutes(app fiber.Router, takeoverHandler *handlers.TakeoverHandler, wopiSvc *service.WOPIService) {
|
||||
wopi := app.Group("/wopi", middleware.WOPIRequired(wopiSvc))
|
||||
if wopiSvc != nil {
|
||||
wopi.Use(middleware.WOPIRequestBodyLimit(wopiSvc.MaxBodyBytes()))
|
||||
}
|
||||
readOnly := wopi.Group("", middleware.WOPIPermissionRequired(service.WOPIPermissionRead))
|
||||
writeOnly := wopi.Group("", middleware.WOPIPermissionRequired(service.WOPIPermissionWrite))
|
||||
|
||||
readOnly.Get("/files/:fileId", takeoverHandler.CheckWOPIFileInfo)
|
||||
readOnly.Get("/files/:fileId/contents", takeoverHandler.GetWOPIContents)
|
||||
writeOnly.Post("/files/:fileId/contents", takeoverHandler.PutWOPIContents)
|
||||
writeOnly.Post("/files/:fileId", takeoverHandler.PostWOPIFile)
|
||||
}
|
||||
423
internal/app/api/seed.go
Normal file
423
internal/app/api/seed.go
Normal file
@@ -0,0 +1,423 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"wucher/internal/domain/auth"
|
||||
"wucher/internal/domain/base"
|
||||
)
|
||||
|
||||
// SeedRoles inserts default roles if they don't exist.
|
||||
func SeedRoles(db *gorm.DB) error {
|
||||
roles := []auth.Role{
|
||||
{Code: "admin", Name: "admin", Description: "Full access to all resources."},
|
||||
{Code: auth.RoleCodeStaff, Name: auth.RoleCodeStaff, Description: "Operational access with limited admin rights."},
|
||||
{Code: "user", Name: "user", Description: "Regular end-user access."},
|
||||
{Code: auth.RoleCodePilot, Name: "pilot", Description: "HEMS pilot contact role."},
|
||||
{Code: auth.RoleCodeDoctor, Name: "doctor", Description: "HEMS doctor contact role."},
|
||||
{Code: auth.RoleCodeAirRescuer, Name: "air_rescuer", Description: "HEMS air rescuer contact role."},
|
||||
{Code: auth.RoleCodeTechnician, Name: "technician", Description: "HEMS technician contact role."},
|
||||
{Code: auth.RoleCodeFlightAssistant, Name: "flight_assistant", Description: "HEMS flight assistant contact role."},
|
||||
}
|
||||
|
||||
return db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "name"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"code", "description"}),
|
||||
}).Create(&roles).Error
|
||||
}
|
||||
|
||||
// DefaultPermissions returns the canonical catalog of permissions seeded into the system.
|
||||
func DefaultPermissions() []auth.Permission {
|
||||
return []auth.Permission{
|
||||
{Name: "Create User", Key: "user.create", Description: "Create users"},
|
||||
{Name: "Read User", Key: "user.read", Description: "Read users"},
|
||||
{Name: "Update User", Key: "user.update", Description: "Update users"},
|
||||
{Name: "Delete User", Key: "user.delete", Description: "Delete users"},
|
||||
{Name: "Create Role", Key: "role.create", Description: "Create roles"},
|
||||
{Name: "Read Role", Key: "role.read", Description: "Read roles"},
|
||||
{Name: "Update Role", Key: "role.update", Description: "Update roles"},
|
||||
{Name: "Delete Role", Key: "role.delete", Description: "Delete roles"},
|
||||
{Name: "Read Permission", Key: "role.permission.read", Description: "Read permissions"},
|
||||
{Name: "Assign Permission", Key: "role.permission.assign", Description: "Assign permissions to role"},
|
||||
{Name: "Remove Permission", Key: "role.permission.remove", Description: "Remove permissions from role"},
|
||||
{Name: "Update Permission", Key: "role.permission.update", Description: "Update permission configuration"},
|
||||
{Name: "Read Filesystem", Key: "filesystem.read", Description: "Read filesystem structure"},
|
||||
{Name: "Read File Manager", Key: "file_manager.read", Description: "Read file manager"},
|
||||
{Name: "Create File Manager", Key: "file_manager.create", Description: "Create in file manager"},
|
||||
{Name: "Update File Manager", Key: "file_manager.update", Description: "Update in file manager"},
|
||||
{Name: "Delete File Manager", Key: "file_manager.delete", Description: "Delete in file manager"},
|
||||
{Name: "Read Folders", Key: "file_manager.folder.read", Description: "Read folders"},
|
||||
{Name: "Create Folders", Key: "file_manager.folder.create", Description: "Create folders"},
|
||||
{Name: "Update Folders", Key: "file_manager.folder.update", Description: "Update folders"},
|
||||
{Name: "Delete Folders", Key: "file_manager.folder.delete", Description: "Delete folders"},
|
||||
{Name: "Read Files", Key: "file_manager.file.read", Description: "Read files"},
|
||||
{Name: "Create Files", Key: "file_manager.file.create", Description: "Create files"},
|
||||
{Name: "Update Files", Key: "file_manager.file.update", Description: "Update files"},
|
||||
{Name: "Delete Files", Key: "file_manager.file.delete", Description: "Delete files"},
|
||||
{Name: "Create Helicopter", Key: "helicopter.create", Description: "Create helicopter"},
|
||||
{Name: "Generate Helicopter Report Number", Key: "helicopter.report.generate", Description: "Generate next helicopter report number"},
|
||||
{Name: "Read Helicopter", Key: "helicopter.read", Description: "Read helicopter"},
|
||||
{Name: "Update Helicopter", Key: "helicopter.update", Description: "Update helicopter"},
|
||||
{Name: "Delete Helicopter", Key: "helicopter.delete", Description: "Delete helicopter"},
|
||||
{Name: "Create Helicopter Usage", Key: "helicopter_usage.create", Description: "Create helicopter usage"},
|
||||
{Name: "Read Helicopter Usage", Key: "helicopter_usage.read", Description: "Read helicopter usage"},
|
||||
{Name: "Update Helicopter Usage", Key: "helicopter_usage.update", Description: "Update helicopter usage"},
|
||||
{Name: "Delete Helicopter Usage", Key: "helicopter_usage.delete", Description: "Delete helicopter usage"},
|
||||
{Name: "Create Flight Inspection", Key: "flight_inspection.create", Description: "Create flight inspection"},
|
||||
{Name: "Read Flight Inspection", Key: "flight_inspection.read", Description: "Read flight inspection"},
|
||||
{Name: "Update Flight Inspection", Key: "flight_inspection.update", Description: "Update flight inspection"},
|
||||
{Name: "Create After Flight Inspection", Key: "after_flight.create", Description: "Create after flight inspection"},
|
||||
{Name: "Read After Flight Inspection", Key: "after_flight.read", Description: "Read after flight inspection"},
|
||||
{Name: "Update After Flight Inspection", Key: "after_flight.update", Description: "Update after flight inspection"},
|
||||
{Name: "Delete After Flight Inspection", Key: "after_flight.delete", Description: "Delete after flight inspection"},
|
||||
{Name: "Create Flight", Key: "flight.create", Description: "Create flight"},
|
||||
{Name: "Read Flight", Key: "flight.read", Description: "Read flight"},
|
||||
{Name: "Update Flight", Key: "flight.update", Description: "Update flight"},
|
||||
{Name: "Complete Flight Report", Key: "flight.complete", Description: "Complete (finalize) flight report"},
|
||||
{Name: "Delete Flight", Key: "flight.delete", Description: "Delete flight"},
|
||||
{Name: "Create Reserve AC", Key: "reserve_ac.create", Description: "Create reserve AC"},
|
||||
{Name: "Read Reserve AC", Key: "reserve_ac.read", Description: "Read reserve AC"},
|
||||
{Name: "Update Reserve AC", Key: "reserve_ac.update", Description: "Update reserve AC"},
|
||||
{Name: "Delete Reserve AC", Key: "reserve_ac.delete", Description: "Delete reserve AC"},
|
||||
{Name: "Create Facility", Key: "facility.create", Description: "Create facility"},
|
||||
{Name: "Read Facility", Key: "facility.read", Description: "Read facility"},
|
||||
{Name: "Update Facility", Key: "facility.update", Description: "Update facility"},
|
||||
{Name: "Delete Facility", Key: "facility.delete", Description: "Delete facility"},
|
||||
{Name: "Create Base", Key: "base.create", Description: "Create base"},
|
||||
{Name: "Read Base", Key: "base.read", Description: "Read base"},
|
||||
{Name: "Update Base", Key: "base.update", Description: "Update base"},
|
||||
{Name: "Delete Base", Key: "base.delete", Description: "Delete base"},
|
||||
{Name: "Create HEMS Base", Key: "hems_base.create", Description: "Create HEMS base"},
|
||||
{Name: "Read HEMS Base", Key: "hems_base.read", Description: "Read HEMS base"},
|
||||
{Name: "Update HEMS Base", Key: "hems_base.update", Description: "Update HEMS base"},
|
||||
{Name: "Delete HEMS Base", Key: "hems_base.delete", Description: "Delete HEMS base"},
|
||||
{Name: "Create Medicine", Key: "medicine.create", Description: "Create medicine"},
|
||||
{Name: "Read Medicine", Key: "medicine.read", Description: "Read medicine"},
|
||||
{Name: "Update Medicine", Key: "medicine.update", Description: "Update medicine"},
|
||||
{Name: "Delete Medicine", Key: "medicine.delete", Description: "Delete medicine"},
|
||||
{Name: "Create Vocation", Key: "vocation.create", Description: "Create vocation"},
|
||||
{Name: "Read Vocation", Key: "vocation.read", Description: "Read vocation"},
|
||||
{Name: "Update Vocation", Key: "vocation.update", Description: "Update vocation"},
|
||||
{Name: "Delete Vocation", Key: "vocation.delete", Description: "Delete vocation"},
|
||||
{Name: "Create OPC", Key: "opc.create", Description: "Create opc"},
|
||||
{Name: "Read OPC", Key: "opc.read", Description: "Read opc"},
|
||||
{Name: "Update OPC", Key: "opc.update", Description: "Update opc"},
|
||||
{Name: "Delete OPC", Key: "opc.delete", Description: "Delete opc"},
|
||||
{Name: "Create Forces Present", Key: "forces_present.create", Description: "Create forces present"},
|
||||
{Name: "Read Forces Present", Key: "forces_present.read", Description: "Read forces present"},
|
||||
{Name: "Update Forces Present", Key: "forces_present.update", Description: "Update forces present"},
|
||||
{Name: "Delete Forces Present", Key: "forces_present.delete", Description: "Delete forces present"},
|
||||
{Name: "Create No ICAO Code", Key: "no_icao_code.create", Description: "Create no icao code"},
|
||||
{Name: "Read No ICAO Code", Key: "no_icao_code.read", Description: "Read no icao code"},
|
||||
{Name: "Update No ICAO Code", Key: "no_icao_code.update", Description: "Update no icao code"},
|
||||
{Name: "Delete No ICAO Code", Key: "no_icao_code.delete", Description: "Delete no icao code"},
|
||||
{Name: "Create Health Insurance Companies", Key: "health_insurance_companies.create", Description: "Create health insurance companies"},
|
||||
{Name: "Read Health Insurance Companies", Key: "health_insurance_companies.read", Description: "Read health insurance companies"},
|
||||
{Name: "Update Health Insurance Companies", Key: "health_insurance_companies.update", Description: "Update health insurance companies"},
|
||||
{Name: "Delete Health Insurance Companies", Key: "health_insurance_companies.delete", Description: "Delete health insurance companies"},
|
||||
{Name: "Create Federal State", Key: "federal_state.create", Description: "Create federal state"},
|
||||
{Name: "Read Federal State", Key: "federal_state.read", Description: "Read federal state"},
|
||||
{Name: "Update Federal State", Key: "federal_state.update", Description: "Update federal state"},
|
||||
{Name: "Delete Federal State", Key: "federal_state.delete", Description: "Delete federal state"},
|
||||
{Name: "Create ICAO", Key: "icao.create", Description: "Create ICAO"},
|
||||
{Name: "Read ICAO", Key: "icao.read", Description: "Read ICAO"},
|
||||
{Name: "Update ICAO", Key: "icao.update", Description: "Update ICAO"},
|
||||
{Name: "Delete ICAO", Key: "icao.delete", Description: "Delete ICAO"},
|
||||
{Name: "Create Country", Key: "country.create", Description: "Create country"},
|
||||
{Name: "Read Country", Key: "country.read", Description: "Read country"},
|
||||
{Name: "Update Country", Key: "country.update", Description: "Update country"},
|
||||
{Name: "Delete Country", Key: "country.delete", Description: "Delete country"},
|
||||
{Name: "Create Master Settings", Key: "master_settings.create", Description: "Create master settings"},
|
||||
{Name: "Read Master Settings", Key: "master_settings.read", Description: "Read master settings"},
|
||||
{Name: "Update Master Settings", Key: "master_settings.update", Description: "Update master settings"},
|
||||
{Name: "Delete Master Settings", Key: "master_settings.delete", Description: "Delete master settings"},
|
||||
{Name: "Create Duty Roster", Key: "duty_roster.create", Description: "Create duty roster"},
|
||||
{Name: "Read Duty Roster", Key: "duty_roster.read", Description: "Read duty roster"},
|
||||
{Name: "Update Duty Roster", Key: "duty_roster.update", Description: "Update duty roster"},
|
||||
{Name: "Delete Duty Roster", Key: "duty_roster.delete", Description: "Delete duty roster"},
|
||||
{Name: "Create Air Rescuer Checklist", Key: "air_rescuer_checklist.create", Description: "Create air rescuer checklist"},
|
||||
{Name: "Read Air Rescuer Checklist", Key: "air_rescuer_checklist.read", Description: "Read air rescuer checklist"},
|
||||
{Name: "Update Air Rescuer Checklist", Key: "air_rescuer_checklist.update", Description: "Update air rescuer checklist"},
|
||||
{Name: "Delete Air Rescuer Checklist", Key: "air_rescuer_checklist.delete", Description: "Delete air rescuer checklist"},
|
||||
{Name: "Create Flight Prep Check", Key: "flight_prep_check.create", Description: "Create flight prep check"},
|
||||
{Name: "Read Flight Prep Check", Key: "flight_prep_check.read", Description: "Read flight prep check"},
|
||||
{Name: "Update Flight Prep Check", Key: "flight_prep_check.update", Description: "Update flight prep check"},
|
||||
{Name: "Read Audit Log", Key: "audit.read", Description: "Read audit logs"},
|
||||
{Name: "Create Insurance Patient Data", Key: "insurance_patient_data.create", Description: "Create insurance patient data"},
|
||||
{Name: "Read Insurance Patient Data", Key: "insurance_patient_data.read", Description: "Read insurance patient data"},
|
||||
{Name: "Update Insurance Patient Data", Key: "insurance_patient_data.update", Description: "Update insurance patient data"},
|
||||
{Name: "Delete Insurance Patient Data", Key: "insurance_patient_data.delete", Description: "Delete insurance patient data"},
|
||||
{Name: "Create Patient Data", Key: "patient_data.create", Description: "Create patient data"},
|
||||
{Name: "Read Patient Data", Key: "patient_data.read", Description: "Read patient data"},
|
||||
{Name: "Update Patient Data", Key: "patient_data.update", Description: "Update patient data"},
|
||||
{Name: "Delete Patient Data", Key: "patient_data.delete", Description: "Delete patient data"},
|
||||
{Name: "Create Operation", Key: "operation.create", Description: "Create HEMS operational data"},
|
||||
{Name: "Read Operation", Key: "operation.read", Description: "Read HEMS operational data"},
|
||||
{Name: "Update Operation", Key: "operation.update", Description: "Update HEMS operational data"},
|
||||
{Name: "Delete Operation", Key: "operation.delete", Description: "Delete HEMS operational data"},
|
||||
{Name: "Create Operation Category", Key: "operation_category.create", Description: "Create HEMS operation category"},
|
||||
{Name: "Read Operation Category", Key: "operation_category.read", Description: "Read HEMS operation category"},
|
||||
{Name: "Update Operation Category", Key: "operation_category.update", Description: "Update HEMS operation category"},
|
||||
{Name: "Delete Operation Category", Key: "operation_category.delete", Description: "Delete HEMS operation category"},
|
||||
{Name: "Create HEMS Operation", Key: "hems_operation.create", Description: "Create HEMS operation"},
|
||||
{Name: "Read HEMS Operation", Key: "hems_operation.read", Description: "Read HEMS operation"},
|
||||
{Name: "Update HEMS Operation", Key: "hems_operation.update", Description: "Update HEMS operation"},
|
||||
{Name: "Delete HEMS Operation", Key: "hems_operation.delete", Description: "Delete HEMS operation"},
|
||||
{Name: "Create DUL", Key: "dul.create", Description: "Create DUL"},
|
||||
{Name: "Read DUL", Key: "dul.read", Description: "Read DUL"},
|
||||
{Name: "Update DUL", Key: "dul.update", Description: "Update DUL"},
|
||||
{Name: "Delete DUL", Key: "dul.delete", Description: "Delete DUL"},
|
||||
{Name: "Create Complaint", Key: "complaint.create", Description: "Create complaint"},
|
||||
{Name: "Read Complaint", Key: "complaint.read", Description: "Read complaint"},
|
||||
{Name: "Update Complaint", Key: "complaint.update", Description: "Update complaint"},
|
||||
{Name: "Delete Complaint", Key: "complaint.delete", Description: "Delete complaint"},
|
||||
{Name: "Decide Complaint NSR", Key: "complaint.nsr", Description: "Set or cancel complaint NSR (non-safety release)"},
|
||||
{Name: "Create EASA Release", Key: "easa_release.create", Description: "Create EASA maintenance release"},
|
||||
{Name: "Read EASA Release", Key: "easa_release.read", Description: "Read EASA maintenance release"},
|
||||
{Name: "Update EASA Release", Key: "easa_release.update", Description: "Update EASA maintenance release"},
|
||||
{Name: "Delete EASA Release", Key: "easa_release.delete", Description: "Delete EASA maintenance release"},
|
||||
{Name: "Sign EASA Release", Key: "easa_release.sign", Description: "Sign EASA maintenance release (return to service)"},
|
||||
{Name: "Read Action Sign-off", Key: "action_signoff.read", Description: "Read corrective-action sign-off"},
|
||||
{Name: "Sign Action Sign-off", Key: "action_signoff.sign", Description: "Toggle (sign/unsign) corrective-action sign-off, per complaint or fleet-wide"},
|
||||
{Name: "Create MCF", Key: "mcf.create", Description: "Create maintenance check flight"},
|
||||
{Name: "Read MCF", Key: "mcf.read", Description: "Read maintenance check flight"},
|
||||
{Name: "Complete MCF", Key: "mcf.complete", Description: "Complete (release) a helicopter's open maintenance check flight"},
|
||||
}
|
||||
}
|
||||
|
||||
// SeedPermissions inserts default permissions and keeps existing rows in sync.
|
||||
func SeedPermissions(db *gorm.DB) error {
|
||||
perms := DefaultPermissions()
|
||||
|
||||
for i := range perms {
|
||||
perms[i].Module = auth.ModuleForKey(perms[i].Key)
|
||||
perms[i].RequiresPIN = auth.DefaultPermissionRequiresPIN(perms[i].Key)
|
||||
}
|
||||
|
||||
return db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"name", "description", "module"}),
|
||||
}).Create(&perms).Error
|
||||
}
|
||||
|
||||
// SeedRolePermissions inserts default role-permission mappings and ensures admin has all permissions.
|
||||
func SeedRolePermissions(db *gorm.DB) error {
|
||||
type rolePerm struct {
|
||||
RoleName string
|
||||
PermKey string
|
||||
}
|
||||
|
||||
mappings := []rolePerm{
|
||||
// staff: read-only for users/roles/permissions
|
||||
{RoleName: "staff", PermKey: "user.read"},
|
||||
{RoleName: "staff", PermKey: "role.read"},
|
||||
{RoleName: "staff", PermKey: "role.permission.read"},
|
||||
{RoleName: "staff", PermKey: "helicopter.read"},
|
||||
{RoleName: "staff", PermKey: "flight_inspection.read"},
|
||||
{RoleName: "staff", PermKey: "flight.read"},
|
||||
{RoleName: "staff", PermKey: "reserve_ac.read"},
|
||||
{RoleName: "staff", PermKey: "facility.read"},
|
||||
{RoleName: "staff", PermKey: "base.read"},
|
||||
{RoleName: "staff", PermKey: "hems_base.read"},
|
||||
{RoleName: "staff", PermKey: "medicine.read"},
|
||||
{RoleName: "staff", PermKey: "vocation.read"},
|
||||
{RoleName: "staff", PermKey: "opc.read"},
|
||||
{RoleName: "staff", PermKey: "forces_present.read"},
|
||||
{RoleName: "staff", PermKey: "no_icao_code.read"},
|
||||
{RoleName: "staff", PermKey: "health_insurance_companies.read"},
|
||||
{RoleName: "staff", PermKey: "federal_state.read"},
|
||||
{RoleName: "staff", PermKey: "icao.read"},
|
||||
{RoleName: "staff", PermKey: "country.read"},
|
||||
{RoleName: "staff", PermKey: "master_settings.read"},
|
||||
{RoleName: "staff", PermKey: "file_manager.read"},
|
||||
{RoleName: "staff", PermKey: "duty_roster.read"},
|
||||
{RoleName: "staff", PermKey: "air_rescuer_checklist.read"},
|
||||
{RoleName: "staff", PermKey: "flight_prep_check.create"},
|
||||
{RoleName: "staff", PermKey: "flight_prep_check.read"},
|
||||
{RoleName: "staff", PermKey: "flight_prep_check.update"},
|
||||
{RoleName: "staff", PermKey: "helicopter_usage.read"},
|
||||
{RoleName: "staff", PermKey: "after_flight.read"},
|
||||
{RoleName: "staff", PermKey: "audit.read"},
|
||||
{RoleName: "staff", PermKey: "insurance_patient_data.read"},
|
||||
{RoleName: "staff", PermKey: "patient_data.read"},
|
||||
{RoleName: "staff", PermKey: "operation.read"},
|
||||
{RoleName: "staff", PermKey: "operation_category.read"},
|
||||
{RoleName: "staff", PermKey: "hems_operation.read"},
|
||||
{RoleName: "pilot", PermKey: "flight.read"},
|
||||
{RoleName: "pilot", PermKey: "duty_roster.read"},
|
||||
}
|
||||
|
||||
if err := assignAllPermissionsToRole(db, "admin"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, m := range mappings {
|
||||
if err := assignPermissionToRole(db, m.RoleName, m.PermKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func assignAllPermissionsToRole(db *gorm.DB, roleName string) error {
|
||||
var role auth.Role
|
||||
if err := db.Where("name = ?", roleName).First(&role).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var perms []auth.Permission
|
||||
if err := db.Find(&perms).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, perm := range perms {
|
||||
rp := auth.RolePermission{RoleID: role.ID, PermissionID: perm.ID}
|
||||
if err := db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "role_id"}, {Name: "permission_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(&rp).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func assignPermissionToRole(db *gorm.DB, roleName, permKey string) error {
|
||||
var role auth.Role
|
||||
if err := db.Where("name = ?", roleName).First(&role).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var perm auth.Permission
|
||||
if err := db.Where("`key` = ?", permKey).First(&perm).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
rp := auth.RolePermission{RoleID: role.ID, PermissionID: perm.ID}
|
||||
return db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "role_id"}, {Name: "permission_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(&rp).Error
|
||||
}
|
||||
|
||||
// SeedBaseCategories inserts default base categories if they don't exist.
|
||||
func SeedBaseCategories(db *gorm.DB) error {
|
||||
categories := []base.BaseCategory{
|
||||
{Key: base.CategoryKeyRegular, Name: "Regular Base"},
|
||||
{Key: base.CategoryKeyHEMS, Name: "HEMS Base"},
|
||||
}
|
||||
|
||||
return db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"name"}),
|
||||
}).Create(&categories).Error
|
||||
}
|
||||
|
||||
// SeedRosterUsers inserts sample users for duty roster assignment in development.
|
||||
// This seed is idempotent by email and keeps role assignment synced via user_roles.
|
||||
func SeedRosterUsers(db *gorm.DB) error {
|
||||
type rosterSeed struct {
|
||||
Email string
|
||||
FirstName string
|
||||
LastName string
|
||||
MobilePhone string
|
||||
RoleCode string
|
||||
}
|
||||
|
||||
seeds := []rosterSeed{
|
||||
{Email: "pilot.main@wucher.local", FirstName: "Pilot", LastName: "Main", MobilePhone: "+620001", RoleCode: auth.RoleCodePilot},
|
||||
{Email: "pilot.additional@wucher.local", FirstName: "Pilot", LastName: "Additional", MobilePhone: "+620002", RoleCode: auth.RoleCodePilot},
|
||||
{Email: "doctor.main@wucher.local", FirstName: "Doctor", LastName: "Main", MobilePhone: "+620003", RoleCode: auth.RoleCodeDoctor},
|
||||
{Email: "rescuer.main@wucher.local", FirstName: "Rescuer", LastName: "Main", MobilePhone: "+620004", RoleCode: auth.RoleCodeAirRescuer},
|
||||
{Email: "helper.main@wucher.local", FirstName: "Helper", LastName: "Main", MobilePhone: "+620005", RoleCode: auth.RoleCodeFlightAssistant},
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
roleKeys := make([]string, 0, len(seeds))
|
||||
seenRole := make(map[string]struct{}, len(seeds))
|
||||
for i := range seeds {
|
||||
k := strings.ToLower(strings.TrimSpace(seeds[i].RoleCode))
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenRole[k]; ok {
|
||||
continue
|
||||
}
|
||||
seenRole[k] = struct{}{}
|
||||
roleKeys = append(roleKeys, k)
|
||||
}
|
||||
|
||||
var roles []auth.Role
|
||||
if err := tx.Where("LOWER(code) IN ? OR LOWER(name) IN ?", roleKeys, roleKeys).Find(&roles).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
roleIDByCode := make(map[string][]byte, len(roles))
|
||||
for i := range roles {
|
||||
code := strings.ToLower(strings.TrimSpace(roles[i].Code))
|
||||
if code == "" {
|
||||
code = strings.ToLower(strings.TrimSpace(roles[i].Name))
|
||||
}
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
roleIDByCode[code] = append([]byte(nil), roles[i].ID...)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
for i := range seeds {
|
||||
roleCode := strings.ToLower(strings.TrimSpace(seeds[i].RoleCode))
|
||||
roleID := roleIDByCode[roleCode]
|
||||
if len(roleID) != 16 {
|
||||
continue
|
||||
}
|
||||
|
||||
row := auth.User{
|
||||
Email: strings.ToLower(strings.TrimSpace(seeds[i].Email)),
|
||||
Username: nil,
|
||||
FirstName: strings.TrimSpace(seeds[i].FirstName),
|
||||
LastName: strings.TrimSpace(seeds[i].LastName),
|
||||
MobilePhone: strings.TrimSpace(seeds[i].MobilePhone),
|
||||
Timezone: "UTC",
|
||||
RoleID: roleID,
|
||||
IsActive: true,
|
||||
EmailVerifiedAt: &now,
|
||||
}
|
||||
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "email"}},
|
||||
DoUpdates: clause.Assignments(map[string]any{
|
||||
"first_name": row.FirstName,
|
||||
"last_name": row.LastName,
|
||||
"mobile_phone": row.MobilePhone,
|
||||
"timezone": row.Timezone,
|
||||
"role_id": row.RoleID,
|
||||
"is_active": row.IsActive,
|
||||
"email_verified_at": row.EmailVerifiedAt,
|
||||
"updated_at": now,
|
||||
}),
|
||||
}).Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var created auth.User
|
||||
if err := tx.Where("email = ?", row.Email).Take(&created).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
link := auth.UserRole{
|
||||
UserID: created.ID,
|
||||
RoleID: roleID,
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}, {Name: "role_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(&link).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
34
internal/app/api/seed_permissions_test.go
Normal file
34
internal/app/api/seed_permissions_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"wucher/internal/domain/auth"
|
||||
)
|
||||
|
||||
// TestDefaultPermissionsHaveRegisteredModule guards that every seeded permission
|
||||
// maps to a module declared in auth.ModuleRegistry. Adding a permission with a
|
||||
// new prefix without registering its module will fail this test.
|
||||
func TestDefaultPermissionsHaveRegisteredModule(t *testing.T) {
|
||||
for _, p := range DefaultPermissions() {
|
||||
module := auth.ModuleForKey(p.Key)
|
||||
if !auth.IsRegisteredModule(module) {
|
||||
t.Errorf("permission %q maps to module %q which is not in auth.ModuleRegistry", p.Key, module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPermissionsHaveUniqueKeys guards against accidental duplicate keys
|
||||
// in the catalog (the upsert is keyed on "key").
|
||||
func TestDefaultPermissionsHaveUniqueKeys(t *testing.T) {
|
||||
seen := make(map[string]bool)
|
||||
for _, p := range DefaultPermissions() {
|
||||
if p.Key == "" {
|
||||
t.Errorf("permission %q has an empty key", p.Name)
|
||||
}
|
||||
if seen[p.Key] {
|
||||
t.Errorf("duplicate permission key %q in DefaultPermissions", p.Key)
|
||||
}
|
||||
seen[p.Key] = true
|
||||
}
|
||||
}
|
||||
54
internal/app/api/server.go
Normal file
54
internal/app/api/server.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
app *fiber.App
|
||||
shutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
type ServerOption func(*Server)
|
||||
|
||||
const defaultShutdownTimeout = 30 * time.Second
|
||||
|
||||
func WithShutdownTimeout(timeout time.Duration) ServerOption {
|
||||
return func(s *Server) {
|
||||
if timeout > 0 {
|
||||
s.shutdownTimeout = timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(app *fiber.App, opts ...ServerOption) *Server {
|
||||
srv := &Server{
|
||||
app: app,
|
||||
shutdownTimeout: defaultShutdownTimeout,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
opt(srv)
|
||||
}
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
func (s *Server) Start(addr string) error {
|
||||
return s.app.Listen(addr)
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if s.shutdownTimeout > 0 {
|
||||
shutdownCtx, cancel := context.WithTimeout(ctx, s.shutdownTimeout)
|
||||
defer cancel()
|
||||
ctx = shutdownCtx
|
||||
}
|
||||
return s.app.ShutdownWithContext(ctx)
|
||||
}
|
||||
160
internal/app/api/server_test.go
Normal file
160
internal/app/api/server_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestServerShutdownDrainsInFlightRequestsAndRejectsNewRequests(t *testing.T) {
|
||||
app := fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
})
|
||||
UseDefaultMiddlewares(app, testHTTPConfig())
|
||||
|
||||
slowStarted := make(chan struct{})
|
||||
app.Get("/slow", func(c *fiber.Ctx) error {
|
||||
select {
|
||||
case <-slowStarted:
|
||||
default:
|
||||
close(slowStarted)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
case <-c.UserContext().Done():
|
||||
return c.Status(http.StatusInternalServerError).SendString("user context canceled")
|
||||
}
|
||||
|
||||
return c.SendString("slow done")
|
||||
})
|
||||
app.Get("/fast", func(c *fiber.Ctx) error {
|
||||
return c.SendString("fast done")
|
||||
})
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
|
||||
serveErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
serveErrCh <- app.Listener(ln)
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = app.Shutdown()
|
||||
})
|
||||
|
||||
baseURL := "http://" + ln.Addr().String()
|
||||
fastClient := newTestHTTPClient()
|
||||
slowClient := newTestHTTPClient()
|
||||
defer fastClient.CloseIdleConnections()
|
||||
defer slowClient.CloseIdleConnections()
|
||||
|
||||
if err := waitForHTTP200(fastClient, baseURL+"/fast", time.Second); err != nil {
|
||||
t.Fatalf("wait for server ready: %v", err)
|
||||
}
|
||||
|
||||
slowRespCh := make(chan httpCallResult, 1)
|
||||
go func() {
|
||||
resp, err := slowClient.Get(baseURL + "/slow")
|
||||
if err != nil {
|
||||
slowRespCh <- httpCallResult{err: err}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
slowRespCh <- httpCallResult{
|
||||
statusCode: resp.StatusCode,
|
||||
body: string(body),
|
||||
err: readErr,
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-slowStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("slow request did not start")
|
||||
}
|
||||
|
||||
srv := NewServer(app, WithShutdownTimeout(2*time.Second))
|
||||
shutdownErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
shutdownErrCh <- srv.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
if err := waitForRequestRejection(fastClient, baseURL+"/fast", time.Second); err == nil {
|
||||
t.Fatal("expected new requests to be rejected during shutdown")
|
||||
}
|
||||
|
||||
slowResult := <-slowRespCh
|
||||
if slowResult.err != nil {
|
||||
t.Fatalf("slow request failed: %v", slowResult.err)
|
||||
}
|
||||
if slowResult.statusCode != http.StatusOK {
|
||||
t.Fatalf("expected slow request 200, got %d with body %q", slowResult.statusCode, slowResult.body)
|
||||
}
|
||||
if slowResult.body != "slow done" {
|
||||
t.Fatalf("expected slow request body %q, got %q", "slow done", slowResult.body)
|
||||
}
|
||||
|
||||
if err := <-shutdownErrCh; err != nil {
|
||||
t.Fatalf("shutdown failed: %v", err)
|
||||
}
|
||||
if err := <-serveErrCh; err != nil {
|
||||
t.Fatalf("listener exited with error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type httpCallResult struct {
|
||||
statusCode int
|
||||
body string
|
||||
err error
|
||||
}
|
||||
|
||||
func newTestHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: time.Second,
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func waitForHTTP200(client *http.Client, url string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := client.Get(url)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
return errors.New("server did not become ready")
|
||||
}
|
||||
|
||||
func waitForRequestRejection(client *http.Client, url string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lastErr = errors.New("request still accepted")
|
||||
resp.Body.Close()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
Reference in New Issue
Block a user