Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
12 changes: 3 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
ENTRYPOINT [ "./barecms" ]
6 changes: 3 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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))
}
}
Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
58 changes: 19 additions & 39 deletions internal/handlers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,55 @@ 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)

// 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)
Expand All @@ -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)
Expand All @@ -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"})
}
35 changes: 15 additions & 20 deletions internal/handlers/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -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!"})
}
Loading