Add frontend #1
@@ -0,0 +1,52 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"quay/app/repository"
|
||||
"quay/internal/security"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
Repo repository.UserRepository
|
||||
}
|
||||
|
||||
func NewAuthHandler(repo repository.UserRepository) *AuthHandler {
|
||||
return &AuthHandler{Repo: repo}
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c fiber.Ctx) error {
|
||||
var req LoginRequest
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request"})
|
||||
}
|
||||
|
||||
user, err := h.Repo.GetUserByName(req.Name)
|
||||
if err != nil || user == nil {
|
||||
log.Println("login: user lookup failed", err)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
|
||||
}
|
||||
|
||||
if !security.CheckPasswordHash(req.Password, user.HashedPassword) {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
|
||||
}
|
||||
|
||||
token, err := security.GenerateToken(user.ID, user.Role, 24*time.Hour)
|
||||
if err != nil {
|
||||
log.Println("login: token generation failed", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate token"})
|
||||
}
|
||||
|
||||
return c.JSON(LoginResponse{Token: token})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"quay/internal/security"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func RequireAuth() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
auth := c.Get("Authorization")
|
||||
if auth == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing authorization header"})
|
||||
}
|
||||
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid authorization header"})
|
||||
}
|
||||
|
||||
claims, err := security.ValidateToken(parts[1])
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"})
|
||||
}
|
||||
|
||||
c.Locals("user_id", claims.UserID)
|
||||
c.Locals("role", claims.Role)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@@ -26,50 +26,69 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d
|
||||
deploymentsHandler := handlers.NewDeploymentHandler(deploymentRepository)
|
||||
|
||||
api := app.Group("/api/v1", middleware.APIHostGuard(envCfg.DashboardHost))
|
||||
api.Get("/health", handlers.HealthCheck)
|
||||
|
||||
api.Post("/deploy", deploySiteHandler.PostDeploy)
|
||||
public := api.Group("")
|
||||
public.Get("/health", handlers.HealthCheck)
|
||||
|
||||
authHandler := handlers.NewAuthHandler(userRepository)
|
||||
public.Post("/login", authHandler.Login)
|
||||
|
||||
// Protected routes - require auth for everything by default
|
||||
protected := api.Group("", middleware.RequireAuth())
|
||||
|
||||
protected.Post("/deploy", deploySiteHandler.PostDeploy)
|
||||
|
||||
// Sites
|
||||
api.Get("/sites", siteHandler.GetSites)
|
||||
api.Get("/sites/:id", siteHandler.GetSite)
|
||||
api.Post("/sites", siteHandler.PostSite)
|
||||
api.Put("/sites/:id", siteHandler.PutSite)
|
||||
api.Delete("/sites/:id", siteHandler.DeleteSite)
|
||||
api.Patch("/sites/:id/enabled", siteHandler.ToggleEnabled)
|
||||
protected.Get("/sites", siteHandler.GetSites)
|
||||
protected.Get("/sites/:id", siteHandler.GetSite)
|
||||
protected.Post("/sites", siteHandler.PostSite)
|
||||
protected.Put("/sites/:id", siteHandler.PutSite)
|
||||
protected.Delete("/sites/:id", siteHandler.DeleteSite)
|
||||
protected.Patch("/sites/:id/enabled", siteHandler.ToggleEnabled)
|
||||
|
||||
// Forward rules
|
||||
api.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules)
|
||||
api.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule)
|
||||
api.Get("/sites/:id/forward-rules/:ruleId", siteHandler.GetForwardRule)
|
||||
api.Put("/sites/:id/forward-rules/:ruleId", siteHandler.PutForwardRule)
|
||||
api.Delete("/sites/:id/forward-rules/:ruleId", siteHandler.DeleteForwardRule)
|
||||
protected.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules)
|
||||
protected.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule)
|
||||
protected.Get("/sites/:id/forward-rules/:ruleId", siteHandler.GetForwardRule)
|
||||
protected.Put("/sites/:id/forward-rules/:ruleId", siteHandler.PutForwardRule)
|
||||
protected.Delete("/sites/:id/forward-rules/:ruleId", siteHandler.DeleteForwardRule)
|
||||
|
||||
// Custom headers (header rules)
|
||||
api.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders)
|
||||
api.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders)
|
||||
api.Get("/sites/:id/custom-headers/:customHeaderId", siteHandler.GetCustomHeaders)
|
||||
api.Put("/sites/:id/custom-headers/:customHeaderId", siteHandler.PutCustomHeaders)
|
||||
api.Delete("/sites/:id/custom-headers/:customHeaderId", siteHandler.DeleteCustomHeaders)
|
||||
protected.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders)
|
||||
protected.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders)
|
||||
protected.Get("/sites/:id/custom-headers/:customHeaderId", siteHandler.GetCustomHeaders)
|
||||
protected.Put("/sites/:id/custom-headers/:customHeaderId", siteHandler.PutCustomHeaders)
|
||||
protected.Delete("/sites/:id/custom-headers/:customHeaderId", siteHandler.DeleteCustomHeaders)
|
||||
|
||||
// Headers
|
||||
api.Get("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.GetCustomHeaderHeaders)
|
||||
api.Post("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.PostHeader)
|
||||
api.Get("/sites/:id/headers/:headerId", siteHandler.GetHeader)
|
||||
api.Put("/sites/:id/headers/:headerId", siteHandler.PutHeader)
|
||||
api.Delete("/sites/:id/headers/:headerId", siteHandler.DeleteHeader)
|
||||
protected.Get("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.GetCustomHeaderHeaders)
|
||||
protected.Post("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.PostHeader)
|
||||
protected.Get("/sites/:id/headers/:headerId", siteHandler.GetHeader)
|
||||
protected.Put("/sites/:id/headers/:headerId", siteHandler.PutHeader)
|
||||
protected.Delete("/sites/:id/headers/:headerId", siteHandler.DeleteHeader)
|
||||
|
||||
// Deployments
|
||||
api.Get("/deployments/:id", deploymentsHandler.GetDeployment)
|
||||
api.Get("/sites/:id/deployments", deploymentsHandler.GetDeploymentsBySite)
|
||||
protected.Get("/deployments/:id", deploymentsHandler.GetDeployment)
|
||||
protected.Get("/sites/:id/deployments", deploymentsHandler.GetDeploymentsBySite)
|
||||
|
||||
// Users
|
||||
api.Get("/users", userHandler.GetAllUsers)
|
||||
api.Get("/users/:id", userHandler.GetUserById)
|
||||
api.Get("/users/by-name/:name", userHandler.GetUserByName)
|
||||
api.Post("/users", userHandler.CreateUser)
|
||||
api.Put("/users/:id", userHandler.UpdateUser)
|
||||
api.Delete("/users/:id", userHandler.DeleteUser)
|
||||
protected.Get("/users", userHandler.GetAllUsers)
|
||||
protected.Get("/users/:id", userHandler.GetUserById)
|
||||
protected.Get("/users/by-name/:name", userHandler.GetUserByName)
|
||||
|
||||
// Allow creating the very first admin user without auth (bootstrap).
|
||||
// If an admin already exists, require auth to create users.
|
||||
if exists, err := userRepository.AdminUserExists(); err != nil {
|
||||
// if we can't determine, be conservative and require auth
|
||||
protected.Post("/users", userHandler.CreateUser)
|
||||
} else if !exists {
|
||||
public.Post("/users", userHandler.CreateUser)
|
||||
} else {
|
||||
protected.Post("/users", userHandler.CreateUser)
|
||||
}
|
||||
|
||||
protected.Put("/users/:id", userHandler.UpdateUser)
|
||||
protected.Delete("/users/:id", userHandler.DeleteUser)
|
||||
|
||||
api.Use(func(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
|
||||
|
||||
+3
-2
@@ -5,9 +5,12 @@ go 1.25.0
|
||||
require (
|
||||
github.com/Flussen/swagger-fiber-v3 v1.0.1
|
||||
github.com/gofiber/fiber/v3 v3.1.0
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.38
|
||||
github.com/swaggo/swag v1.16.4
|
||||
golang.org/x/crypto v0.48.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -28,11 +31,9 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/swaggo/swag v1.16.4 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
|
||||
@@ -10,7 +10,10 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
@@ -27,6 +30,8 @@ github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
|
||||
github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
|
||||
github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI=
|
||||
github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
@@ -38,6 +43,7 @@ github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxh
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@@ -49,13 +55,19 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
|
||||
github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
||||
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
@@ -64,11 +76,19 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -84,6 +104,7 @@ golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var ErrMissingSecret = errors.New("JWT secret not set")
|
||||
|
||||
type Claims struct {
|
||||
UserID string `json:"sub"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func getSecret() (string, error) {
|
||||
s := os.Getenv("JWT_SECRET")
|
||||
if s == "" {
|
||||
return "", ErrMissingSecret
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func GenerateToken(userID, role string, ttl time.Duration) (string, error) {
|
||||
secret, err := getSecret()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
func ValidateToken(tokenString string) (*Claims, error) {
|
||||
secret, err := getSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { makeApiUrl } from '.';
|
||||
|
||||
export const login = async (name: string, password: string): Promise<string> => {
|
||||
const response = await fetch(makeApiUrl('/login'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to login');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.token;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeApiUrl } from '.';
|
||||
import { fetchWithAuth } from '.';
|
||||
import type {
|
||||
CreateCustomHeadersRequest,
|
||||
CreateHeaderRequest,
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
} from './types/site';
|
||||
|
||||
export const getCustomHeaders = async (siteId: string): Promise<CustomHeaders[]> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/custom-headers`, {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -20,7 +20,7 @@ export const createCustomHeaders = async (
|
||||
siteId: string,
|
||||
data: CreateCustomHeadersRequest
|
||||
): Promise<CustomHeaders> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/custom-headers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -38,7 +38,7 @@ export const updateCustomHeaders = async (
|
||||
groupId: string,
|
||||
data: CreateCustomHeadersRequest
|
||||
): Promise<CustomHeaders> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers/${groupId}`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/custom-headers/${groupId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -52,7 +52,7 @@ export const updateCustomHeaders = async (
|
||||
};
|
||||
|
||||
export const deleteCustomHeaders = async (siteId: string, groupId: string): Promise<void> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers/${groupId}`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/custom-headers/${groupId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -65,7 +65,7 @@ export const createHeader = async (
|
||||
groupId: string,
|
||||
data: CreateHeaderRequest
|
||||
): Promise<Header> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers/${groupId}/headers`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/custom-headers/${groupId}/headers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -83,7 +83,7 @@ export const updateHeader = async (
|
||||
headerId: string,
|
||||
data: CreateHeaderRequest
|
||||
): Promise<Header> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/headers/${headerId}`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/headers/${headerId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -97,7 +97,7 @@ export const updateHeader = async (
|
||||
};
|
||||
|
||||
export const deleteHeader = async (siteId: string, headerId: string): Promise<void> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/headers/${headerId}`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/headers/${headerId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { makeApiUrl } from '.';
|
||||
import { fetchWithAuth } from '.';
|
||||
import type { Deployment } from './types/deployments';
|
||||
|
||||
export const getDeploymentsForSite = async (
|
||||
siteId: string,
|
||||
limit: number = 100
|
||||
): Promise<Deployment[]> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/deployments?limit=${limit}`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/deployments?limit=${limit}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { makeApiUrl } from '.';
|
||||
import { fetchWithAuth } from '.';
|
||||
import type { CreateForwardRuleRequest, ForwardRule } from './types/site';
|
||||
|
||||
export const getForwardRules = async (siteId: string): Promise<ForwardRule[]> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/forward-rules`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/forward-rules`, {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -15,7 +15,7 @@ export const createForwardRule = async (
|
||||
siteId: string,
|
||||
data: CreateForwardRuleRequest
|
||||
): Promise<ForwardRule> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/forward-rules`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/forward-rules`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -33,7 +33,7 @@ export const updateForwardRule = async (
|
||||
ruleId: string,
|
||||
data: CreateForwardRuleRequest
|
||||
): Promise<ForwardRule> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/forward-rules/${ruleId}`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/forward-rules/${ruleId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -47,7 +47,7 @@ export const updateForwardRule = async (
|
||||
};
|
||||
|
||||
export const deleteForwardRule = async (siteId: string, ruleId: string): Promise<void> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/forward-rules/${ruleId}`), {
|
||||
const response = await fetchWithAuth(`/sites/${siteId}/forward-rules/${ruleId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -5,3 +5,20 @@ export const makeApiUrl = (endpoint: string) => {
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
return `${base}${path}`;
|
||||
};
|
||||
|
||||
export const authHeaders = () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchWithAuth = (endpoint: string, options: RequestInit = {}) => {
|
||||
const headers = {
|
||||
...(options.headers || {}),
|
||||
...authHeaders(),
|
||||
} as Record<string, string>;
|
||||
return fetch(makeApiUrl(endpoint), { ...options, headers });
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeApiUrl } from '.';
|
||||
import { fetchWithAuth } from '.';
|
||||
import type {
|
||||
CreateSiteRequest,
|
||||
CreateSiteResponse,
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from './types/site';
|
||||
|
||||
export const getSites = async (): Promise<GetAllSitesResponse> => {
|
||||
const response = await fetch(makeApiUrl('/sites'), {
|
||||
const response = await fetchWithAuth('/sites', {
|
||||
method: 'GET',
|
||||
});
|
||||
if (response.status === 404) {
|
||||
@@ -21,7 +21,7 @@ export const getSites = async (): Promise<GetAllSitesResponse> => {
|
||||
};
|
||||
|
||||
export const getSite = async (id: string): Promise<Site> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${id}`), {
|
||||
const response = await fetchWithAuth(`/sites/${id}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -31,7 +31,7 @@ export const getSite = async (id: string): Promise<Site> => {
|
||||
};
|
||||
|
||||
export const createSite = async (data: CreateSiteRequest): Promise<CreateSiteResponse> => {
|
||||
const response = await fetch(makeApiUrl('/sites'), {
|
||||
const response = await fetchWithAuth('/sites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -45,7 +45,7 @@ export const createSite = async (data: CreateSiteRequest): Promise<CreateSiteRes
|
||||
};
|
||||
|
||||
export const updateSite = async (id: string, data: Partial<CreateSiteRequest>): Promise<Site> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${id}`), {
|
||||
const response = await fetchWithAuth(`/sites/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -59,7 +59,7 @@ export const updateSite = async (id: string, data: Partial<CreateSiteRequest>):
|
||||
};
|
||||
|
||||
export const toggleSiteEnabled = async (id: string): Promise<ToggleSiteEnabledResponse> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${id}/enabled`), {
|
||||
const response = await fetchWithAuth(`/sites/${id}/enabled`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -72,7 +72,7 @@ export const toggleSiteEnabled = async (id: string): Promise<ToggleSiteEnabledRe
|
||||
};
|
||||
|
||||
export const deleteSite = async (id: string): Promise<void> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${id}`), {
|
||||
const response = await fetchWithAuth(`/sites/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { makeApiUrl } from '.';
|
||||
import { fetchWithAuth } from '.';
|
||||
import type { CreateUserRequest, UpdateUserRequest, User } from './types/user';
|
||||
|
||||
export const getUsers = async (): Promise<User[]> => {
|
||||
const response = await fetch(makeApiUrl('/users'), {
|
||||
const response = await fetchWithAuth('/users', {
|
||||
method: 'GET',
|
||||
});
|
||||
if (response.status === 404) {
|
||||
@@ -15,7 +15,7 @@ export const getUsers = async (): Promise<User[]> => {
|
||||
};
|
||||
|
||||
export const getUserById = async (id: string): Promise<User> => {
|
||||
const response = await fetch(makeApiUrl(`/users/${id}`), {
|
||||
const response = await fetchWithAuth(`/users/${id}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -25,7 +25,7 @@ export const getUserById = async (id: string): Promise<User> => {
|
||||
};
|
||||
|
||||
export const createUser = async (data: CreateUserRequest): Promise<User> => {
|
||||
const response = await fetch(makeApiUrl('/users'), {
|
||||
const response = await fetchWithAuth('/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -39,7 +39,7 @@ export const createUser = async (data: CreateUserRequest): Promise<User> => {
|
||||
};
|
||||
|
||||
export const updateUser = async (id: string, data: Partial<UpdateUserRequest>): Promise<User> => {
|
||||
const response = await fetch(makeApiUrl(`/users/${id}`), {
|
||||
const response = await fetchWithAuth(`/users/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -53,7 +53,7 @@ export const updateUser = async (id: string, data: Partial<UpdateUserRequest>):
|
||||
};
|
||||
|
||||
export const deleteUser = async (id: string): Promise<void> => {
|
||||
const response = await fetch(makeApiUrl(`/users/${id}`), {
|
||||
const response = await fetchWithAuth(`/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
+31
-5
@@ -1,4 +1,4 @@
|
||||
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
import { BrowserRouter, Route, Routes, Navigate } from 'react-router';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import MainPage from './pages/Main/Main';
|
||||
@@ -8,6 +8,7 @@ import './index.css';
|
||||
import NewSite from './pages/NewSite/NewSite';
|
||||
import SiteOverview from './pages/SiteOverview/SiteOverview';
|
||||
import Users from './pages/Users/Users';
|
||||
import Login from './pages/Login/Login';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -16,10 +17,35 @@ createRoot(document.getElementById('root')!).render(
|
||||
<ThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainPage />} />
|
||||
<Route path="/sites/new" element={<NewSite />} />
|
||||
<Route path="/sites/:id" element={<SiteOverview />} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
!localStorage.getItem('token') ? <Navigate to="/login" /> : <MainPage />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/sites/new"
|
||||
element={
|
||||
!localStorage.getItem('token') ? <Navigate to="/login" /> : <NewSite />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/sites/:id"
|
||||
element={
|
||||
!localStorage.getItem('token') ? (
|
||||
<Navigate to="/login" />
|
||||
) : (
|
||||
<SiteOverview />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
!localStorage.getItem('token') ? <Navigate to="/login" /> : <Users />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { login } from '../../api/auth.api';
|
||||
import { Input } from '../../components/ui/input';
|
||||
import { Button } from '../../components/ui/button';
|
||||
|
||||
const Login = () => {
|
||||
const [name, setName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
const token = await login(name, password);
|
||||
localStorage.setItem('token', token);
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
setError('Invalid credentials');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto mt-20">
|
||||
<h1 className="text-2xl font-semibold mb-4">Sign in</h1>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Name</label>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
<div>
|
||||
<Button type="submit">Sign in</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
Reference in New Issue
Block a user