diff --git a/apps/cliq-hub-backend/go.mod b/apps/cliq-hub-backend/go.mod index db341b6..5364333 100644 --- a/apps/cliq-hub-backend/go.mod +++ b/apps/cliq-hub-backend/go.mod @@ -7,19 +7,28 @@ toolchain go1.24.8 require ( github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.10.1 + github.com/glebarez/sqlite v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/sashabaranov/go-openai v1.26.0 + golang.org/x/crypto v0.39.0 + gorm.io/gorm v1.31.1 ) require ( github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect 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 @@ -28,13 +37,17 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect golang.org/x/arch v0.18.0 // indirect - golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) diff --git a/apps/cliq-hub-backend/go.sum b/apps/cliq-hub-backend/go.sum index 62468e3..a4b72e7 100644 --- a/apps/cliq-hub-backend/go.sum +++ b/apps/cliq-hub-backend/go.sum @@ -10,6 +10,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -18,6 +20,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -28,9 +34,19 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -54,6 +70,9 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/sashabaranov/go-openai v1.26.0 h1:upM565hxdqvCxNzuAcEBZ1XsfGehH0/9kgk9rFVpDxQ= @@ -91,4 +110,14 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/apps/cliq-hub-backend/internal/auth/jwt.go b/apps/cliq-hub-backend/internal/auth/jwt.go new file mode 100644 index 0000000..9706738 --- /dev/null +++ b/apps/cliq-hub-backend/internal/auth/jwt.go @@ -0,0 +1,47 @@ +package auth + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var secretKey = []byte("cliq-hub-secret-key-change-this-in-prod") + +type Claims struct { + UserID uint `json:"user_id"` + Email string `json:"email"` + jwt.RegisteredClaims +} + +func GenerateToken(userID uint, email string) (string, error) { + expirationTime := time.Now().Add(24 * time.Hour) + claims := &Claims{ + UserID: userID, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(secretKey) +} + +func ValidateToken(tokenString string) (*Claims, error) { + claims := &Claims{} + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return secretKey, nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, errors.New("invalid token") + } + + return claims, nil +} diff --git a/apps/cliq-hub-backend/internal/db/db.go b/apps/cliq-hub-backend/internal/db/db.go new file mode 100644 index 0000000..d4dee30 --- /dev/null +++ b/apps/cliq-hub-backend/internal/db/db.go @@ -0,0 +1,39 @@ +package db + +import ( + "log" + "path/filepath" + "runtime" + + "cliq-hub-backend/internal/models" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func Init(dbPath string) { + if dbPath == "" { + // Default to a file in the project root if not specified + _, b, _, _ := runtime.Caller(0) + basepath := filepath.Dir(b) + // Go up to apps/cliq-hub-backend + root := filepath.Join(basepath, "../../..") + dbPath = filepath.Join(root, "cliqhub.db") + } + + var err error + DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + log.Fatal("failed to connect database:", err) + } + + // Auto Migrate + err = DB.AutoMigrate(&models.User{}, &models.Template{}) + if err != nil { + log.Fatal("failed to migrate database:", err) + } + + log.Println("Database initialized at", dbPath) +} diff --git a/apps/cliq-hub-backend/internal/http/handlers/auth_handler.go b/apps/cliq-hub-backend/internal/http/handlers/auth_handler.go new file mode 100644 index 0000000..7dcda16 --- /dev/null +++ b/apps/cliq-hub-backend/internal/http/handlers/auth_handler.go @@ -0,0 +1,83 @@ +package handlers + +import ( + "net/http" + + "cliq-hub-backend/internal/auth" + "cliq-hub-backend/internal/db" + "cliq-hub-backend/internal/models" + + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" +) + +type RegisterRequest struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` +} + +type LoginRequest struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` +} + +type AuthHandler struct{} + +func NewAuthHandler() *AuthHandler { + return &AuthHandler{} +} + +func (h *AuthHandler) Register(c *gin.Context) { + var req RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) + return + } + + user := models.User{ + Username: req.Username, + Email: req.Email, + Password: string(hashedPassword), + } + + if result := db.DB.Create(&user); result.Error != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "User already exists"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "User created successfully"}) +} + +func (h *AuthHandler) Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var user models.User + if result := db.DB.Where("email = ?", req.Email).First(&user); result.Error != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"}) + return + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"}) + return + } + + token, err := auth.GenerateToken(user.ID, user.Email) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) + return + } + + c.JSON(http.StatusOK, gin.H{"token": token, "username": user.Username, "id": user.ID}) +} diff --git a/apps/cliq-hub-backend/internal/http/handlers/handler_test.go b/apps/cliq-hub-backend/internal/http/handlers/handler_test.go new file mode 100644 index 0000000..9f6a7b8 --- /dev/null +++ b/apps/cliq-hub-backend/internal/http/handlers/handler_test.go @@ -0,0 +1,94 @@ +package handlers_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + + "cliq-hub-backend/internal/db" + "cliq-hub-backend/internal/http/handlers" + "cliq-hub-backend/internal/models" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func setupRouter() *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.Default() + + // Init DB + db.Init(os.TempDir() + "/test.db") + db.DB.AutoMigrate(&models.User{}, &models.Template{}) + + authHandler := handlers.NewAuthHandler() + r.POST("/register", authHandler.Register) + r.POST("/login", authHandler.Login) + + templateHandler := handlers.NewTemplateHandler() + r.GET("/templates", templateHandler.List) + r.GET("/templates/:id", templateHandler.Get) + + protected := r.Group("/") + protected.Use(handlers.AuthMiddleware()) + protected.POST("/templates", templateHandler.Create) + + return r +} + +func TestAuthAndTemplateFlow(t *testing.T) { + r := setupRouter() + + // 1. Register + registerPayload := map[string]string{ + "username": "testuser", + "email": "test@example.com", + "password": "password123", + } + body, _ := json.Marshal(registerPayload) + req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(body)) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusCreated, w.Code) + + // 2. Login + loginPayload := map[string]string{ + "email": "test@example.com", + "password": "password123", + } + body, _ = json.Marshal(loginPayload) + req, _ = http.NewRequest("POST", "/login", bytes.NewBuffer(body)) + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + + var loginResp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &loginResp) + token := loginResp["token"].(string) + + // 3. Create Template + templatePayload := map[string]string{ + "title": "Test Template", + "content": "test: content", + } + body, _ = json.Marshal(templatePayload) + req, _ = http.NewRequest("POST", "/templates", bytes.NewBuffer(body)) + req.Header.Set("Authorization", "Bearer "+token) + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusCreated, w.Code) + + // 4. List Templates + req, _ = http.NewRequest("GET", "/templates", nil) + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + + var templates []map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &templates) + assert.Len(t, templates, 1) + assert.Equal(t, "Test Template", templates[0]["title"]) +} diff --git a/apps/cliq-hub-backend/internal/http/handlers/template_handler.go b/apps/cliq-hub-backend/internal/http/handlers/template_handler.go new file mode 100644 index 0000000..1382967 --- /dev/null +++ b/apps/cliq-hub-backend/internal/http/handlers/template_handler.go @@ -0,0 +1,96 @@ +package handlers + +import ( + "net/http" + + "cliq-hub-backend/internal/auth" + "cliq-hub-backend/internal/db" + "cliq-hub-backend/internal/models" + + "github.com/gin-gonic/gin" +) + +type TemplateHandler struct{} + +func NewTemplateHandler() *TemplateHandler { + return &TemplateHandler{} +} + +func (h *TemplateHandler) List(c *gin.Context) { + var templates []models.Template + if result := db.DB.Preload("Author").Find(&templates); result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + c.JSON(http.StatusOK, templates) +} + +func (h *TemplateHandler) Get(c *gin.Context) { + id := c.Param("id") + var template models.Template + if result := db.DB.Preload("Author").First(&template, id); result.Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"}) + return + } + c.JSON(http.StatusOK, template) +} + +func (h *TemplateHandler) Create(c *gin.Context) { + // Simple auth middleware check + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var req struct { + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Content string `json:"content" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + template := models.Template{ + Title: req.Title, + Description: req.Description, + Content: req.Content, + AuthorID: userID.(uint), + } + + if result := db.DB.Create(&template); result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + c.JSON(http.StatusCreated, template) +} + +// AuthMiddleware extracts the user ID from the JWT token +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + tokenString := c.GetHeader("Authorization") + if tokenString == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"}) + return + } + + // Handle "Bearer " + if len(tokenString) > 7 && tokenString[:7] == "Bearer " { + tokenString = tokenString[7:] + } + + claims, err := auth.ValidateToken(tokenString) + + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + return + } + + c.Set("userID", claims.UserID) + c.Next() + } +} diff --git a/apps/cliq-hub-backend/internal/http/router/router.go b/apps/cliq-hub-backend/internal/http/router/router.go index 30eb3bc..ab69315 100644 --- a/apps/cliq-hub-backend/internal/http/router/router.go +++ b/apps/cliq-hub-backend/internal/http/router/router.go @@ -1,23 +1,50 @@ package router import ( - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" - + "cliq-hub-backend/internal/db" "cliq-hub-backend/internal/http/handlers" "cliq-hub-backend/internal/llm" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" ) func New(client llm.Client, debugMode bool) *gin.Engine { + // Initialize DB + db.Init("") // Use default path + r := gin.Default() r.Use(cors.New(cors.Config{ - AllowAllOrigins: true, - AllowHeaders: []string{"*"}, + AllowAllOrigins: true, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, })) - h := handlers.NewGenerateHandler(client, debugMode) + + genHandler := handlers.NewGenerateHandler(client, debugMode) + authHandler := handlers.NewAuthHandler() + templateHandler := handlers.NewTemplateHandler() v1 := r.Group("/v1") + + // Auth + auth := v1.Group("/auth") + auth.POST("/register", authHandler.Register) + auth.POST("/login", authHandler.Login) + + // Templates tm := v1.Group("/templates") - tm.POST("/generate", h.Handle) + tm.POST("/generate", genHandler.Handle) // Existing + tm.GET("", templateHandler.List) + tm.GET("/:id", templateHandler.Get) + + // Protected routes + protected := v1.Group("/") + protected.Use(handlers.AuthMiddleware()) + { + protected.POST("/templates", templateHandler.Create) + } + return r } diff --git a/apps/cliq-hub-backend/internal/models/models.go b/apps/cliq-hub-backend/internal/models/models.go new file mode 100644 index 0000000..fc06bb1 --- /dev/null +++ b/apps/cliq-hub-backend/internal/models/models.go @@ -0,0 +1,23 @@ +package models + +import ( + "gorm.io/gorm" +) + +type User struct { + gorm.Model + Username string `gorm:"uniqueIndex;not null" json:"username"` + Password string `gorm:"not null" json:"-"` + Email string `gorm:"uniqueIndex;not null" json:"email"` +} + +type Template struct { + gorm.Model + Title string `gorm:"not null" json:"title"` + Description string `json:"description"` + Content string `gorm:"type:text;not null" json:"content"` // YAML content + AuthorID uint `json:"author_id"` + Author User `gorm:"foreignKey:AuthorID" json:"author,omitempty"` + Downloads int `gorm:"default:0" json:"downloads"` + Status string `gorm:"default:published" json:"status"` // published, draft, archived +} diff --git a/apps/cliq-hub-frontend/package.json b/apps/cliq-hub-frontend/package.json index 0be071d..49107a8 100644 --- a/apps/cliq-hub-frontend/package.json +++ b/apps/cliq-hub-frontend/package.json @@ -8,11 +8,19 @@ "build": "vite build" }, "dependencies": { - "vue": "^3.2.37" + "@primevue/themes": "^4.5.0", + "axios": "^1.13.2", + "pinia": "^3.0.4", + "primevue": "^4.4.1", + "vue": "^3.2.37", + "vue-router": "^4.6.3" }, "devDependencies": { - "vite": "^6.0.0", "@vitejs/plugin-vue": "^6.0.1", - "typescript": "^5.1.4" + "autoprefixer": "^10.4.22", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.1", + "typescript": "^5.1.4", + "vite": "^6.0.0" } } \ No newline at end of file diff --git a/apps/cliq-hub-frontend/postcss.config.js b/apps/cliq-hub-frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/apps/cliq-hub-frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/apps/cliq-hub-frontend/src/App.vue b/apps/cliq-hub-frontend/src/App.vue index c00dcc1..020b2d4 100644 --- a/apps/cliq-hub-frontend/src/App.vue +++ b/apps/cliq-hub-frontend/src/App.vue @@ -1,10 +1,37 @@ - - \ No newline at end of file + + + diff --git a/apps/cliq-hub-frontend/src/main.ts b/apps/cliq-hub-frontend/src/main.ts index 58cdcf4..535e224 100644 --- a/apps/cliq-hub-frontend/src/main.ts +++ b/apps/cliq-hub-frontend/src/main.ts @@ -1,4 +1,20 @@ import { createApp } from 'vue' +import './style.css' import App from './App.vue' +import PrimeVue from 'primevue/config'; +import Aura from '@primevue/themes/aura'; +import router from './router'; +import { createPinia } from 'pinia'; -createApp(App).mount('#app') \ No newline at end of file +const app = createApp(App); +const pinia = createPinia(); + +app.use(pinia); +app.use(router); +app.use(PrimeVue, { + theme: { + preset: Aura + } +}); + +app.mount('#app') diff --git a/apps/cliq-hub-frontend/src/router/index.ts b/apps/cliq-hub-frontend/src/router/index.ts new file mode 100644 index 0000000..0e30ab1 --- /dev/null +++ b/apps/cliq-hub-frontend/src/router/index.ts @@ -0,0 +1,35 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '../views/HomeView.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + component: HomeView + }, + { + path: '/login', + name: 'login', + component: () => import('../views/LoginView.vue') + }, + { + path: '/register', + name: 'register', + component: () => import('../views/RegisterView.vue') + }, + { + path: '/templates/:id', + name: 'template-detail', + component: () => import('../views/TemplateDetailView.vue') + }, + { + path: '/upload', + name: 'upload', + component: () => import('../views/UploadView.vue') + } + ] +}) + +export default router diff --git a/apps/cliq-hub-frontend/src/stores/template.ts b/apps/cliq-hub-frontend/src/stores/template.ts new file mode 100644 index 0000000..94849da --- /dev/null +++ b/apps/cliq-hub-frontend/src/stores/template.ts @@ -0,0 +1,44 @@ +import { defineStore } from 'pinia'; +import axios from 'axios'; +import { useUserStore } from './user'; + +const API_URL = 'http://localhost:8080/v1/templates'; + +export const useTemplateStore = defineStore('template', { + state: () => ({ + templates: [], + currentTemplate: null, + }), + actions: { + async fetchTemplates() { + try { + const response = await axios.get(API_URL); + this.templates = response.data; + } catch (error) { + console.error(error); + } + }, + async fetchTemplate(id) { + try { + const response = await axios.get(`${API_URL}/${id}`); + this.currentTemplate = response.data; + } catch (error) { + console.error(error); + } + }, + async createTemplate(template) { + const userStore = useUserStore(); + try { + await axios.post(API_URL, template, { + headers: { + Authorization: `Bearer ${userStore.token}` + } + }); + return true; + } catch (error) { + console.error(error); + return false; + } + } + } +}); diff --git a/apps/cliq-hub-frontend/src/stores/user.ts b/apps/cliq-hub-frontend/src/stores/user.ts new file mode 100644 index 0000000..5d05afb --- /dev/null +++ b/apps/cliq-hub-frontend/src/stores/user.ts @@ -0,0 +1,44 @@ +import { defineStore } from 'pinia'; +import axios from 'axios'; + +const API_URL = 'http://localhost:8080/v1/auth'; + +export const useUserStore = defineStore('user', { + state: () => ({ + token: localStorage.getItem('token') || '', + user: JSON.parse(localStorage.getItem('user') || 'null'), + }), + getters: { + isAuthenticated: (state) => !!state.token, + }, + actions: { + async login(email, password) { + try { + const response = await axios.post(`${API_URL}/login`, { email, password }); + this.token = response.data.token; + this.user = { username: response.data.username, id: response.data.id }; + localStorage.setItem('token', this.token); + localStorage.setItem('user', JSON.stringify(this.user)); + return true; + } catch (error) { + console.error(error); + return false; + } + }, + async register(username, email, password) { + try { + await axios.post(`${API_URL}/register`, { username, email, password }); + return true; + } catch (error) { + console.error(error); + return false; + } + }, + logout() { + this.token = ''; + this.user = null; + localStorage.removeItem('token'); + localStorage.removeItem('user'); + } + } +}); diff --git a/apps/cliq-hub-frontend/src/style.css b/apps/cliq-hub-frontend/src/style.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/cliq-hub-frontend/src/style.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/cliq-hub-frontend/src/views/HomeView.vue b/apps/cliq-hub-frontend/src/views/HomeView.vue new file mode 100644 index 0000000..a281d46 --- /dev/null +++ b/apps/cliq-hub-frontend/src/views/HomeView.vue @@ -0,0 +1,45 @@ + + + diff --git a/apps/cliq-hub-frontend/src/views/LoginView.vue b/apps/cliq-hub-frontend/src/views/LoginView.vue new file mode 100644 index 0000000..da8987b --- /dev/null +++ b/apps/cliq-hub-frontend/src/views/LoginView.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/cliq-hub-frontend/src/views/RegisterView.vue b/apps/cliq-hub-frontend/src/views/RegisterView.vue new file mode 100644 index 0000000..4f88138 --- /dev/null +++ b/apps/cliq-hub-frontend/src/views/RegisterView.vue @@ -0,0 +1,54 @@ + + + diff --git a/apps/cliq-hub-frontend/src/views/TemplateDetailView.vue b/apps/cliq-hub-frontend/src/views/TemplateDetailView.vue new file mode 100644 index 0000000..38cf836 --- /dev/null +++ b/apps/cliq-hub-frontend/src/views/TemplateDetailView.vue @@ -0,0 +1,70 @@ + + + diff --git a/apps/cliq-hub-frontend/src/views/UploadView.vue b/apps/cliq-hub-frontend/src/views/UploadView.vue new file mode 100644 index 0000000..0bb91ad --- /dev/null +++ b/apps/cliq-hub-frontend/src/views/UploadView.vue @@ -0,0 +1,67 @@ +