diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d915972..0eadcc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,19 +16,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: "1.24.4" - - - name: Go Mod Tidy - run: go mod tidy - - - name: Go Lint - uses: golangci/golangci-lint-action@v7 - with: - version: v2.1 - - name: Set up Node.js uses: actions/setup-node@v4 with: @@ -44,6 +31,24 @@ jobs: cd ui npm run lint + - name: UI Build + run: | + cd ui + npm run build + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.24.4" + + - name: Go Mod Tidy + run: go mod tidy + + - name: Go Lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.1 + build: name: build runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index ded8e35..426cfab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,20 +10,14 @@ FROM golang:1.24-alpine AS builder WORKDIR /app COPY . . COPY .env.example .env -RUN go mod download -RUN CGO_ENABLED=0 GOOS=linux go build -o barecms ./cmd/main.go +COPY --from=frontend /app/dist ./ui/dist +RUN go build -o /app/barecms ./cmd/main.go # Final stage FROM alpine:latest -RUN apk --no-cache add ca-certificates WORKDIR /app - -# Copy built frontend -COPY --from=frontend /app/dist ./ui/dist - -# Copy built backend COPY --from=builder /app/barecms . EXPOSE 8080 -CMD ["./barecms"] \ No newline at end of file +ENTRYPOINT [ "./barecms" ] diff --git a/cmd/main.go b/cmd/main.go index d433529..a4947ac 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,11 +7,11 @@ import ( "fmt" "log/slog" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" ) type App struct { - router *gin.Engine + router *echo.Echo cfg configs.AppConfig } @@ -32,7 +32,7 @@ func (app *App) setup() { func (app *App) Run() { fmt.Printf("BareCMS running on http://localhost:%d\n", app.cfg.Port) - if err := app.router.Run(fmt.Sprintf(":%d", app.cfg.Port)); err != nil { + if err := app.router.Start(fmt.Sprintf(":%d", app.cfg.Port)); err != nil { panic(fmt.Sprintf("Failed to run server: %v", err)) } } diff --git a/go.mod b/go.mod index 3b5584f..e7b65d4 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,10 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/text v0.2.0 // indirect + github.com/labstack/echo/v4 v4.13.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -55,12 +58,15 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.18.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.34.0 // indirect + golang.org/x/time v0.11.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.6 // indirect diff --git a/go.sum b/go.sum index e8b3112..f43b133 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,14 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -148,6 +154,10 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -185,6 +195,8 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 80aced9..a3a501f 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -6,42 +6,41 @@ import ( "log/slog" "net/http" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" ) -func (h *Handler) Login(c *gin.Context) { +func (h *Handler) Login(c echo.Context) error { var request models.LoginRequest - if err := c.ShouldBindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return + if err := c.Bind(&request); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } + slog.Debug("Attempting to login user", "email", request.Email) + user, err := h.Service.Login(request.Email, request.Password) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) - return + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials") } // Generate JWT token token, err := utils.GenerateJWT(user.ID, user.Email, h.Config.JWTSecret) if err != nil { slog.Error("Failed to generate JWT token", "error", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) - return + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token") } - c.JSON(http.StatusOK, gin.H{ + slog.Info("User logged in successfully", "user", user) + + return c.JSON(http.StatusOK, map[string]any{ "token": token, "user": user, }) } -func (h *Handler) Register(c *gin.Context) { +func (h *Handler) Register(c echo.Context) error { var request models.RegisterRequest - if err := c.ShouldBindJSON(&request); err != nil { - slog.Error("Failed to bind registration request", "error", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return + if err := c.Bind(&request); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } slog.Info("Attempting to register user", "email", request.Email, "username", request.Username) @@ -49,15 +48,13 @@ func (h *Handler) Register(c *gin.Context) { // Validate JWT secret before proceeding if h.Config.JWTSecret == "" { slog.Error("JWT secret is empty") - c.JSON(http.StatusInternalServerError, gin.H{"error": "Server configuration error"}) - return + return echo.NewHTTPError(http.StatusInternalServerError, "Server configuration error") } // Register the user if err := h.Service.Register(request); err != nil { slog.Error("Failed to register user", "error", err, "email", request.Email) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } slog.Info("User registered successfully", "email", request.Email) @@ -66,8 +63,7 @@ func (h *Handler) Register(c *gin.Context) { user, err := h.Service.Login(request.Email, request.Password) if err != nil { slog.Error("Failed to login after registration", "error", err, "email", request.Email) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Registration successful but login failed"}) - return + return echo.NewHTTPError(http.StatusInternalServerError, "Registration successful but login failed") } slog.Info("User logged in after registration", "user_id", user.ID, "email", user.Email) @@ -76,30 +72,14 @@ func (h *Handler) Register(c *gin.Context) { token, err := utils.GenerateJWT(user.ID, user.Email, h.Config.JWTSecret) if err != nil { slog.Error("Failed to generate JWT token after registration", "error", err, "user_id", user.ID) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) - return + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token") } slog.Info("JWT token generated successfully", "user_id", user.ID) - c.JSON(http.StatusCreated, gin.H{ + return c.JSON(http.StatusCreated, map[string]any{ "token": token, "user": user, "message": "User created successfully", }) } - -func (h *Handler) Logout(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) - return - } - - if err := h.Service.Logout(userID.(string)); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "User logged out successfully"}) -} diff --git a/internal/handlers/collections.go b/internal/handlers/collections.go index b9f09ab..cfadc82 100644 --- a/internal/handlers/collections.go +++ b/internal/handlers/collections.go @@ -4,58 +4,53 @@ import ( "barecms/internal/models" "net/http" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" ) -func (h *Handler) CreateCollection(c *gin.Context) { +func (h *Handler) CreateCollection(c echo.Context) error { var req models.CreateCollectionRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } err := h.Service.CreateCollection(req) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusCreated, gin.H{"message": "Collection created!"}) + return c.JSON(http.StatusCreated, map[string]string{"message": "Collection created!"}) } -func (h *Handler) GetCollection(c *gin.Context) { +func (h *Handler) GetCollection(c echo.Context) error { id := c.Param("id") collection, err := h.Service.GetCollectionByID(id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } - c.JSON(http.StatusOK, collection) + return c.JSON(http.StatusOK, collection) } -func (h *Handler) GetCollectionsBySiteID(c *gin.Context) { +func (h *Handler) GetCollectionsBySiteID(c echo.Context) error { siteID := c.Param("id") collections, err := h.Service.GetCollectionsBySiteID(siteID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } - c.JSON(http.StatusOK, gin.H{"collections": collections}) + return c.JSON(http.StatusOK, map[string]interface{}{"collections": collections}) } -func (h *Handler) DeleteCollection(c *gin.Context) { +func (h *Handler) DeleteCollection(c echo.Context) error { id := c.Param("id") err := h.Service.DeleteCollection(id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } - c.JSON(http.StatusOK, gin.H{"message": "Collection deleted!"}) + return c.JSON(http.StatusOK, map[string]string{"message": "Collection deleted!"}) } diff --git a/internal/handlers/entries.go b/internal/handlers/entries.go index 80c772e..8c6e9d7 100644 --- a/internal/handlers/entries.go +++ b/internal/handlers/entries.go @@ -4,56 +4,51 @@ import ( "barecms/internal/models" "net/http" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" ) -func (h *Handler) CreateEntry(c *gin.Context) { +func (h *Handler) CreateEntry(c echo.Context) error { var request models.CreateEntryRequest - if err := c.BindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return + if err := c.Bind(&request); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } if err := h.Service.CreateEntry(&request); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusCreated, gin.H{"message": "Entry created!"}) + return c.JSON(http.StatusCreated, map[string]string{"message": "Entry created!"}) } -func (h *Handler) GetEntry(c *gin.Context) { +func (h *Handler) GetEntry(c echo.Context) error { id := c.Param("id") entry, err := h.Service.GetEntryByID(id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusOK, entry) + return c.JSON(http.StatusOK, entry) } -func (h *Handler) GetCollectionEntries(c *gin.Context) { +func (h *Handler) GetCollectionEntries(c echo.Context) error { id := c.Param("id") entries, err := h.Service.GetEntriesByCollectionID(id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusOK, entries) + return c.JSON(http.StatusOK, entries) } -func (h *Handler) DeleteEntry(c *gin.Context) { +func (h *Handler) DeleteEntry(c echo.Context) error { id := c.Param("id") err := h.Service.DeleteEntry(id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusOK, gin.H{"message": "Entry deleted!"}) + return c.JSON(http.StatusOK, map[string]string{"message": "Entry deleted!"}) } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 642f8ce..1f83040 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -5,7 +5,7 @@ import ( "barecms/internal/services" "net/http" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" ) type Handler struct { @@ -17,6 +17,6 @@ func NewHandler(service *services.Service, config configs.AppConfig) *Handler { return &Handler{Service: service, Config: config} } -func (h *Handler) Health(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "up"}) +func (h *Handler) Health(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"status": "up"}) } diff --git a/internal/handlers/sites.go b/internal/handlers/sites.go index dcf6df7..db0ab02 100644 --- a/internal/handlers/sites.go +++ b/internal/handlers/sites.go @@ -4,78 +4,71 @@ import ( "barecms/internal/models" "net/http" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" ) -func (h *Handler) GetSites(c *gin.Context) { +func (h *Handler) GetSites(c echo.Context) error { sites, err := h.Service.GetSites() if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(200, gin.H{"sites": sites}) + return c.JSON(http.StatusOK, map[string][]models.Site{"sites": sites}) } -func (h *Handler) GetSite(c *gin.Context) { +func (h *Handler) GetSite(c echo.Context) error { id := c.Param("id") site, err := h.Service.GetSite(id) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(200, gin.H{"site": site}) + return c.JSON(http.StatusOK, map[string]models.Site{"site": site}) } -func (h *Handler) GetSiteWithCollections(c *gin.Context) { +func (h *Handler) GetSiteWithCollections(c echo.Context) error { id := c.Param("id") siteWithCollections, err := h.Service.GetSiteWithCollections(id) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(200, siteWithCollections) + return c.JSON(http.StatusOK, siteWithCollections) } -func (h *Handler) CreateSite(c *gin.Context) { +func (h *Handler) CreateSite(c echo.Context) error { var req models.CreateSiteRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } err := h.Service.CreateSite(req) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(200, gin.H{"message": "Site created!"}) + return c.JSON(http.StatusOK, map[string]string{"message": "Site created!"}) } -func (h *Handler) DeleteSite(c *gin.Context) { +func (h *Handler) DeleteSite(c echo.Context) error { id := c.Param("id") err := h.Service.DeleteSite(id) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(200, gin.H{"message": "Site deleted!"}) + return c.JSON(http.StatusOK, map[string]string{"message": "Site deleted!"}) } -func (h *Handler) GetSiteData(c *gin.Context) { +func (h *Handler) GetSiteData(c echo.Context) error { slug := c.Param("siteSlug") siteData, err := h.Service.GetSiteData(slug) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusOK, siteData) + return c.JSON(http.StatusOK, siteData) } diff --git a/internal/handlers/user.go b/internal/handlers/user.go index 9e7a11d..f59761a 100644 --- a/internal/handlers/user.go +++ b/internal/handlers/user.go @@ -1,46 +1,44 @@ package handlers import ( + "log/slog" "net/http" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" ) -func (h *Handler) GetUser(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) - return +func (h *Handler) GetUser(c echo.Context) error { + userID := c.Get("user_id") + if userID == nil { + return echo.NewHTTPError(http.StatusUnauthorized, "User not authenticated") } user, err := h.Service.GetUser(userID.(string)) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) - return + return echo.NewHTTPError(http.StatusNotFound, "User not found") } - c.JSON(http.StatusOK, user) + slog.Info("User retrieved successfully", "user", user) + + return c.JSON(http.StatusOK, map[string]interface{}{"user": user}) } -func (h *Handler) DeleteUser(c *gin.Context) { +func (h *Handler) DeleteUser(c echo.Context) error { userID := c.Param("userId") - currentUserID, exists := c.Get("user_id") + currentUserID := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) - return + if currentUserID == nil { + return echo.NewHTTPError(http.StatusUnauthorized, "User not authenticated") } // Users can only delete their own account if userID != currentUserID.(string) { - c.JSON(http.StatusForbidden, gin.H{"error": "Cannot delete other users"}) - return + return echo.NewHTTPError(http.StatusForbidden, "Cannot delete other users") } if err := h.Service.DeleteUser(userID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) + return c.JSON(http.StatusOK, map[string]string{"message": "User deleted successfully"}) } diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go index aeeefb7..349c0ea 100644 --- a/internal/middlewares/auth.go +++ b/internal/middlewares/auth.go @@ -6,38 +6,34 @@ import ( "net/http" "strings" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" ) -func AuthMiddleware(config configs.AppConfig) gin.HandlerFunc { - return func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) - c.Abort() - return - } +func AuthMiddleware(config configs.AppConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + authHeader := c.Request().Header.Get("Authorization") + if authHeader == "" { + return c.JSON(http.StatusUnauthorized, echo.Map{"error": "Authorization header required"}) + } - // Extract token from "Bearer " - tokenParts := strings.Split(authHeader, " ") - if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"}) - c.Abort() - return - } + // Extract token from "Bearer " + tokenParts := strings.Split(authHeader, " ") + if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { + return c.JSON(http.StatusUnauthorized, echo.Map{"error": "Invalid authorization format"}) + } - token := tokenParts[1] + token := tokenParts[1] - claims, err := utils.ValidateJWT(token, config.JWTSecret) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) - c.Abort() - return - } + claims, err := utils.ValidateJWT(token, config.JWTSecret) + if err != nil { + return c.JSON(http.StatusUnauthorized, echo.Map{"error": "Invalid token"}) + } - // Set user info in context - c.Set("user_id", claims.UserID) - c.Set("user_email", claims.Email) - c.Next() + // Set user info in context + c.Set("user_id", claims.UserID) + c.Set("user_email", claims.Email) + return next(c) + } } } diff --git a/internal/router/router.go b/internal/router/router.go index 9bfe2ac..1d685cb 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -5,76 +5,66 @@ import ( "barecms/internal/handlers" "barecms/internal/middlewares" "barecms/internal/services" + "barecms/ui" - "github.com/gin-contrib/cors" - "github.com/gin-gonic/contrib/static" - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" ) -func Setup(service *services.Service, config configs.AppConfig) *gin.Engine { - router := gin.New() - router.Use(gin.Logger()) - router.Use(gin.Recovery()) +func Setup(service *services.Service, config configs.AppConfig) *echo.Echo { + r := echo.New() if config.Env == "dev" { - router.Use(cors.New(cors.Config{ + r.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"http://localhost:5173", "http://localhost:5172"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, })) } - // Serve static files - router.Use(static.Serve("/", static.LocalFile("./ui/dist", true))) - // Fallback to index.html for client-side routing - router.NoRoute(func(c *gin.Context) { - c.File("./ui/dist/index.html") - }) + // Serve the built frontend files + r.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + Filesystem: ui.BuildHTTPFS(), + HTML5: true, + })) h := handlers.NewHandler(service, config) - api := router.Group("/api") - { - api.GET("/health", h.Health) + api := r.Group("/api") + api.GET("/health", h.Health) - // Public site data endpoint - api.GET("/:siteSlug/data", h.GetSiteData) + // Public site data endpoint + api.GET("/:siteSlug/data", h.GetSiteData) - // Auth routes (public) - auth := api.Group("/auth") - { - auth.POST("/register", h.Register) - auth.POST("/login", h.Login) - auth.POST("/logout", h.Logout) - } + // Auth routes (public) + api.POST("/auth/register", h.Register) + api.POST("/auth/login", h.Login) - // Protected routes - protected := api.Group("/") - protected.Use(middlewares.AuthMiddleware(config)) - { - // User Management - protected.GET("/user", h.GetUser) - protected.DELETE("/user/:userId", h.DeleteUser) + // Protected routes + protected := api.Group("") + protected.Use(middlewares.AuthMiddleware(config)) - // Sites routes - protected.GET("/sites", h.GetSites) - protected.GET("/sites/:id", h.GetSite) - protected.GET("/sites/:id/collections", h.GetSiteWithCollections) - protected.POST("/sites", h.CreateSite) - protected.DELETE("/sites/:id", h.DeleteSite) + // User Management + protected.GET("/user", h.GetUser) + protected.DELETE("/user/:userId", h.DeleteUser) - // Collections routes - protected.POST("/collections", h.CreateCollection) - protected.GET("/collections/:id", h.GetCollection) - protected.GET("/collections/:id/entries", h.GetCollectionEntries) - protected.GET("/collections/site/:id", h.GetCollectionsBySiteID) - protected.DELETE("/collections/:id", h.DeleteCollection) + // Sites routes + protected.GET("/sites", h.GetSites) + protected.GET("/sites/:id", h.GetSite) + protected.GET("/sites/:id/collections", h.GetSiteWithCollections) + protected.POST("/sites", h.CreateSite) + protected.DELETE("/sites/:id", h.DeleteSite) - // Entries routes - protected.POST("/entries", h.CreateEntry) - protected.GET("/entries/:id", h.GetEntry) - protected.DELETE("/entries/:id", h.DeleteEntry) - } - } + // Collections routes + protected.POST("/collections", h.CreateCollection) + protected.GET("/collections/:id", h.GetCollection) + protected.GET("/collections/:id/entries", h.GetCollectionEntries) + protected.GET("/collections/site/:id", h.GetCollectionsBySiteID) + protected.DELETE("/collections/:id", h.DeleteCollection) + + // Entries routes + protected.POST("/entries", h.CreateEntry) + protected.GET("/entries/:id", h.GetEntry) + protected.DELETE("/entries/:id", h.DeleteEntry) - return router + return r } diff --git a/internal/services/auth.go b/internal/services/auth.go index 5cfab20..61e6600 100644 --- a/internal/services/auth.go +++ b/internal/services/auth.go @@ -44,11 +44,3 @@ func (s *Service) Login(email, password string) (models.User, error) { return user, nil } - -func (s *Service) Logout(userID string) error { - if err := s.Storage.RevokeToken(userID); err != nil { - return errors.Wrap(err, "failed to logout user") - } - - return nil -} diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 6a22642..c2f1670 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,9 @@ import "./App.css"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { ThemeProvider } from "@/providers/ThemeProvider"; +import { AuthProvider } from "@/providers/AuthProvider"; +import { useAuth } from "@/contexts/AuthContext"; +import AppSkeleton from "./components/AppSkeleton"; import Footer from "./components/Footer"; import Header from "./components/Header"; import HomePage from "./pages/Home"; @@ -11,30 +14,44 @@ import Register from "./pages/auth/Register"; import PrivateRoute from "./middlewares/PrivateRoute"; import Profile from "./pages/Profile"; +const AppContent = () => { + const { initializing } = useAuth(); + + if (initializing) { + return ; + } + + return ( + +
+
+
+ + } /> + } /> + }> + } /> + } /> + } /> + } + /> + + +
+
+
+
+ ); +}; + function App() { return ( - -
-
-
- - } /> - } /> - }> - } /> - } /> - } /> - } - /> - - -
-
-
-
+ + +
); } diff --git a/ui/src/components/AppSkeleton.tsx b/ui/src/components/AppSkeleton.tsx new file mode 100644 index 0000000..e1ccaa7 --- /dev/null +++ b/ui/src/components/AppSkeleton.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import Skeleton from "./Skeleton"; + +const AppSkeleton: React.FC = () => { + return ( +
+ {/* Header Skeleton */} +
+
+
+ + +
+
+ + +
+
+
+ + {/* Main Content Skeleton */} +
+
+
+
+ + +
+
+ +
+ + +
+
+
+
+
+ + {/* Footer Skeleton */} +
+
+ +
+
+
+ ); +}; + +export default AppSkeleton; \ No newline at end of file diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx index aa310bc..1c272a9 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header.tsx @@ -1,11 +1,9 @@ -import { useAuth } from "@/hooks/useAuth"; -import { useUser } from "@/hooks/useUser"; +import { useAuth } from "@/contexts/AuthContext"; import ThemeToggle from "./ThemeToggle"; import { Link } from "react-router-dom"; const Header = () => { - const { logout } = useAuth(); - const { user } = useUser(); + const { user, logout } = useAuth(); return (
diff --git a/ui/src/components/ProfileSkeleton.tsx b/ui/src/components/ProfileSkeleton.tsx new file mode 100644 index 0000000..67f3a05 --- /dev/null +++ b/ui/src/components/ProfileSkeleton.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import Skeleton from "./Skeleton"; + +const ProfileSkeleton: React.FC = () => { + return ( +
+
+ {/* Header Skeleton */} +
+ + +
+ + {/* Profile Information Skeleton */} +
+ +
+
+ + +
+
+ + +
+
+
+ + {/* Danger Zone Skeleton */} +
+ + + +
+
+
+ ); +}; + +export default ProfileSkeleton; \ No newline at end of file diff --git a/ui/src/components/Skeleton.tsx b/ui/src/components/Skeleton.tsx new file mode 100644 index 0000000..8d59452 --- /dev/null +++ b/ui/src/components/Skeleton.tsx @@ -0,0 +1,56 @@ +import React from "react"; + +interface SkeletonProps { + className?: string; + variant?: "text" | "rectangular" | "circular"; + width?: string | number; + height?: string | number; + lines?: number; +} + +const Skeleton: React.FC = ({ + className = "", + variant = "text", + width, + height, + lines = 1, +}) => { + const baseClasses = "animate-pulse bg-bare-200 rounded"; + + const variantClasses = { + text: "h-4", + rectangular: "h-32", + circular: "rounded-full", + }; + + const skeletonStyle = { + width: width || "100%", + height: height || undefined, + }; + + if (variant === "text" && lines > 1) { + return ( +
+ {Array.from({ length: lines }).map((_, index) => ( +
+ ))} +
+ ); + } + + return ( +
+ ); +}; + +export default Skeleton; \ No newline at end of file diff --git a/ui/src/contexts/AuthContext.ts b/ui/src/contexts/AuthContext.ts new file mode 100644 index 0000000..58dc43f --- /dev/null +++ b/ui/src/contexts/AuthContext.ts @@ -0,0 +1,36 @@ +import { createContext, useContext } from "react"; +import { User } from "@/types/auth"; + +interface AuthResponse { + token?: string; + error?: string; + user?: User; +} + +export interface AuthContextType { + user: User | null; + loading: boolean; + initializing: boolean; + error: string | null; + login: (email: string, password: string) => Promise; + register: ( + email: string, + username: string, + password: string, + ) => Promise; + logout: () => void; + getAuthHeaders: () => { Authorization?: string }; + refetchUser: () => Promise; +} + +export const AuthContext = createContext( + undefined, +); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/ui/src/hooks/useAuth.ts b/ui/src/hooks/useAuth.ts deleted file mode 100644 index 54e4e95..0000000 --- a/ui/src/hooks/useAuth.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { useState } from "react"; -import axios from "axios"; - -interface AuthResponse { - token?: string; - error?: string; - user?: any; -} - -export const useAuth = () => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const login = async ( - email: string, - password: string, - ): Promise => { - setLoading(true); - setError(null); - try { - const response = await axios.post("/api/auth/login", { email, password }); - const { token, user } = response.data; - localStorage.setItem("token", token); - setLoading(false); - return { token, user }; - } catch (error: any) { - setLoading(false); - setError( - error.response?.data?.error || "An error occurred. Please try again.", - ); - return { - error: - error.response?.data?.error || "An error occurred. Please try again.", - }; - } - }; - - const register = async ( - email: string, - username: string, - password: string, - ): Promise => { - setLoading(true); - setError(null); - try { - const response = await axios.post("/api/auth/register", { - email, - username, - password, - }); - setLoading(false); - - const { token, user } = response.data; - if (token) { - localStorage.setItem("token", token); - return { token, user }; - } - - return { user }; - } catch (err: any) { - setLoading(false); - setError( - err.response?.data?.error || "An error occurred. Please try again.", - ); - return { - error: - err.response?.data?.error || "An error occurred. Please try again.", - }; - } - }; - - const logout = () => { - localStorage.removeItem("token"); - window.location.href = "/login"; - }; - - const getAuthHeaders = () => { - const token = localStorage.getItem("token"); - return token ? { Authorization: `Bearer ${token}` } : {}; - }; - - return { - loading, - error, - login, - register, - logout, - getAuthHeaders, - }; -}; diff --git a/ui/src/hooks/useUser.ts b/ui/src/hooks/useUser.ts deleted file mode 100644 index 51bdbdc..0000000 --- a/ui/src/hooks/useUser.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useState, useEffect } from "react"; -import apiClient from "@/lib/api"; -import { User } from "@/types"; - -export const useUser = () => { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchUser = async () => { - setLoading(true); - setError(null); - try { - const token = localStorage.getItem("token"); - if (!token) { - setLoading(false); - setUser(null); - return; - } - const response = await apiClient.get("/user"); - setUser(response.data); - } catch (err: any) { - setError( - err.response?.data?.error || "An error occurred. Please try again.", - ); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchUser(); - }, []); - - return { - user, - loading, - error, - fetchUser, - }; -}; diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index b5119ed..056f08c 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { AUTH_TOKEN_KEY } from "@/types/auth"; const apiClient = axios.create({ baseURL: "/api", @@ -10,7 +11,7 @@ const apiClient = axios.create({ // Request interceptor to add auth token apiClient.interceptors.request.use( (config) => { - const token = localStorage.getItem("token"); + const token = localStorage.getItem(AUTH_TOKEN_KEY); if (token) { config.headers.Authorization = `Bearer ${token}`; } @@ -24,7 +25,7 @@ apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { - localStorage.removeItem("token"); + localStorage.removeItem(AUTH_TOKEN_KEY); window.location.href = "/login"; } return Promise.reject(error); diff --git a/ui/src/middlewares/PrivateRoute.tsx b/ui/src/middlewares/PrivateRoute.tsx index de98176..1aca795 100644 --- a/ui/src/middlewares/PrivateRoute.tsx +++ b/ui/src/middlewares/PrivateRoute.tsx @@ -1,8 +1,9 @@ -import React from 'react'; -import { Navigate, Outlet } from 'react-router-dom'; +import { AUTH_TOKEN_KEY } from "@/types/auth"; +import React from "react"; +import { Navigate, Outlet } from "react-router-dom"; const PrivateRoute: React.FC = () => { - const token = localStorage.getItem('token'); + const token = localStorage.getItem(AUTH_TOKEN_KEY); return token ? : ; }; diff --git a/ui/src/pages/Home.tsx b/ui/src/pages/Home.tsx index f08b961..7562bc4 100644 --- a/ui/src/pages/Home.tsx +++ b/ui/src/pages/Home.tsx @@ -1,11 +1,11 @@ import React, { useRef } from "react"; import CreateSiteModal from "@/components/modals/CreateSiteModal"; -import { useUser } from "@/hooks/useUser"; +import { useAuth } from "@/contexts/AuthContext"; import { useSites } from "@/hooks/useSites"; import Loader from "@/components/Loader"; const HomePage: React.FC = () => { - const { user, loading: userLoading } = useUser(); + const { user, loading: userLoading } = useAuth(); const { sites, loading: sitesLoading, error } = useSites(); const modalRef = useRef(null); diff --git a/ui/src/pages/Profile.tsx b/ui/src/pages/Profile.tsx index bd53e16..9a62b63 100644 --- a/ui/src/pages/Profile.tsx +++ b/ui/src/pages/Profile.tsx @@ -1,24 +1,25 @@ import React from "react"; import Loader from "@/components/Loader"; -import { useUser } from "@/hooks/useUser"; +import ProfileSkeleton from "@/components/ProfileSkeleton"; +import { useAuth } from "@/contexts/AuthContext"; import useDeleteUser from "@/hooks/useDeleteUser"; const Profile: React.FC = () => { - const { user, loading, error } = useUser(); + const { user, loading, initializing, error } = useAuth(); const { isDeleting, error: deleteError, handleDelete, } = useDeleteUser(user?.id as string); - if (loading || isDeleting) { - return ( -
-
- -
-
- ); + // Show skeleton during initial app load + if (initializing) { + return ; + } + + // Show inline loading for specific actions (like delete) + if (loading && !user) { + return ; } if (error) { @@ -85,10 +86,17 @@ const Profile: React.FC = () => { )}
diff --git a/ui/src/pages/auth/Login.tsx b/ui/src/pages/auth/Login.tsx index 6a2c39c..4c09b68 100644 --- a/ui/src/pages/auth/Login.tsx +++ b/ui/src/pages/auth/Login.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { useAuth } from "@/hooks/useAuth"; +import { useAuth } from "@/contexts/AuthContext"; import { Eye, EyeOff, Loader } from "lucide-react"; import { Link, useNavigate } from "react-router-dom"; @@ -13,9 +13,8 @@ const Login: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const response = await login(email, password); - if (response.token) { - localStorage.setItem("token", response.token); - navigate("/"); + if (response.token && response.user) { + navigate("/", { replace: true }); } }; diff --git a/ui/src/pages/auth/Register.tsx b/ui/src/pages/auth/Register.tsx index 9606b6a..ccc31ae 100644 --- a/ui/src/pages/auth/Register.tsx +++ b/ui/src/pages/auth/Register.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { useAuth } from "@/hooks/useAuth"; +import { useAuth } from "@/contexts/AuthContext"; import Loader from "@/components/Loader"; import { Eye, EyeOff } from "lucide-react"; import { Link } from "react-router-dom"; diff --git a/ui/src/providers/AuthProvider.tsx b/ui/src/providers/AuthProvider.tsx new file mode 100644 index 0000000..22f9868 --- /dev/null +++ b/ui/src/providers/AuthProvider.tsx @@ -0,0 +1,128 @@ +import React, { useEffect, useState, ReactNode } from "react"; +import axios from "axios"; +import apiClient from "@/lib/api"; +import { AUTH_TOKEN_KEY, User } from "@/types/auth"; +import { AuthContext, AuthContextType } from "@/contexts/AuthContext"; + +interface AuthResponse { + token?: string; + error?: string; + user?: User; +} + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(false); + const [initializing, setInitializing] = useState(true); + const [error, setError] = useState(null); + + const fetchUser = async () => { + const token = localStorage.getItem(AUTH_TOKEN_KEY); + if (!token) { + setUser(null); + setInitializing(false); + return; + } + + try { + const response = await apiClient.get("/user"); + setUser(response.data.user); + } catch (err: any) { + console.error("Failed to fetch user:", err); + if (err.response?.status === 401) { + localStorage.removeItem(AUTH_TOKEN_KEY); + setUser(null); + } + } finally { + setInitializing(false); + } + }; + + const login = async ( + email: string, + password: string, + ): Promise => { + setLoading(true); + setError(null); + try { + const response = await axios.post("/api/auth/login", { email, password }); + const { token, user } = response.data; + localStorage.setItem(AUTH_TOKEN_KEY, token); + setUser(user); + setLoading(false); + return { token, user }; + } catch (error: any) { + setLoading(false); + const errorMessage = + error.response?.data?.error || "An error occurred. Please try again."; + setError(errorMessage); + return { error: errorMessage }; + } + }; + + const register = async ( + email: string, + username: string, + password: string, + ): Promise => { + setLoading(true); + setError(null); + try { + const response = await axios.post("/api/auth/register", { + email, + username, + password, + }); + const { token, user } = response.data; + if (token) { + localStorage.setItem(AUTH_TOKEN_KEY, token); + setUser(user); + } + setLoading(false); + return { token, user }; + } catch (err: any) { + setLoading(false); + const errorMessage = + err.response?.data?.error || "An error occurred. Please try again."; + setError(errorMessage); + return { error: errorMessage }; + } + }; + + const logout = () => { + localStorage.removeItem(AUTH_TOKEN_KEY); + setUser(null); + window.location.href = "/login"; + }; + + const getAuthHeaders = () => { + const token = localStorage.getItem(AUTH_TOKEN_KEY); + return token ? { Authorization: `Bearer ${token}` } : {}; + }; + + const refetchUser = async () => { + await fetchUser(); + }; + + useEffect(() => { + fetchUser(); + }, []); + + const value: AuthContextType = { + user, + loading, + initializing, + error, + login, + register, + logout, + getAuthHeaders, + refetchUser, + }; + + return {children}; +}; diff --git a/ui/src/types/auth.ts b/ui/src/types/auth.ts new file mode 100644 index 0000000..c7b8473 --- /dev/null +++ b/ui/src/types/auth.ts @@ -0,0 +1,7 @@ +export interface User { + id: string; + email: string; + username: string; +} + +export const AUTH_TOKEN_KEY = "auth_token"; diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..d63d673 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,22 @@ +package ui + +import ( + "embed" + "io/fs" + "log" + "net/http" +) + +// Embed the build directory from the frontend. +// +//go:embed dist/* +var BuildFs embed.FS + +// Get the subtree of the embedded files with `dist` directory as a root. +func BuildHTTPFS() http.FileSystem { + build, err := fs.Sub(BuildFs, "dist") + if err != nil { + log.Fatal(err) + } + return http.FS(build) +}