Admin Dashboard, Webdocs, LICENSE, webhook, and ID's update.
This commit is contained in:
53
backend/internal/api/admin.go
Normal file
53
backend/internal/api/admin.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) RegisterAdminRoutes(r *gin.Engine) {
|
||||
admin := r.Group("/api/admin", AuthMiddleware(), AdminMiddleware())
|
||||
{
|
||||
admin.GET("/users", h.adminListUsers)
|
||||
admin.DELETE("/users/:id", h.adminDeleteUser)
|
||||
admin.GET("/stats", h.adminGetStats)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) adminListUsers(c *gin.Context) {
|
||||
var users []models.User
|
||||
if err := db.DB.Preload("Projects").Find(&users).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
func (h *Handler) adminDeleteUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := db.DB.Where("id = ?", id).Delete(&models.User{}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
||||
}
|
||||
|
||||
func (h *Handler) adminGetStats(c *gin.Context) {
|
||||
var userCount int64
|
||||
var projectCount int64
|
||||
var deploymentCount int64
|
||||
|
||||
db.DB.Model(&models.User{}).Count(&userCount)
|
||||
db.DB.Model(&models.Project{}).Count(&projectCount)
|
||||
db.DB.Model(&models.Deployment{}).Count(&deploymentCount)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": userCount,
|
||||
"projects": projectCount,
|
||||
"deployments": deploymentCount,
|
||||
})
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (h *Handler) handleDeploy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
imageName, err := h.builder.Build(req.Repo, req.Name, req.GitToken, "", "", "", "", nil, os.Stdout)
|
||||
imageName, _, err := h.builder.Build(req.Repo, req.Name, req.GitToken, "", "", "", "", nil, os.Stdout)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Build failed: %v", err)})
|
||||
return
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
)
|
||||
|
||||
type AuthRequest struct {
|
||||
@@ -42,11 +43,17 @@ func (h *Handler) register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var count int64
|
||||
db.DB.Model(&models.User{}).Count(&count)
|
||||
|
||||
userID, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
user := models.User{
|
||||
ID: userID,
|
||||
Email: req.Email,
|
||||
Password: hashed,
|
||||
Name: req.Name,
|
||||
Avatar: "https://github.com/shadcn.png",
|
||||
IsAdmin: count == 0,
|
||||
}
|
||||
|
||||
if result := db.DB.Create(&user); result.Error != nil {
|
||||
@@ -101,7 +108,7 @@ func (h *Handler) updateProfile(c *gin.Context) {
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if result := db.DB.First(&user, userID); result.Error != nil {
|
||||
if result := db.DB.Where("id = ?", userID).First(&user); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
@@ -130,7 +137,7 @@ func (h *Handler) updatePassword(c *gin.Context) {
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if result := db.DB.First(&user, userID); result.Error != nil {
|
||||
if result := db.DB.Where("id = ?", userID).First(&user); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"clickploy/internal/auth"
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -31,3 +33,29 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsAdmin {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Admin privileges required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"clickploy/internal/auth"
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -40,14 +40,8 @@ func (h *Handler) updateProjectEnv(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pid, err := strconv.ParseUint(projectID, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Where("id = ? AND owner_id = ?", pid, userID).First(&project); result.Error != nil {
|
||||
if result := db.DB.Where("id = ? AND owner_id = ?", projectID, userID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
@@ -80,19 +74,15 @@ func (h *Handler) redeployProject(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
projectID := c.Param("id")
|
||||
|
||||
pid, err := strconv.ParseUint(projectID, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Preload("EnvVars").Where("id = ? AND owner_id = ?", pid, userID).First(&project); result.Error != nil {
|
||||
if result := db.DB.Preload("EnvVars").Where("id = ? AND owner_id = ?", projectID, userID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
depID, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
deployment := models.Deployment{
|
||||
ID: depID,
|
||||
ProjectID: project.ID,
|
||||
Status: "building",
|
||||
Commit: "MANUAL",
|
||||
@@ -110,7 +100,7 @@ func (h *Handler) redeployProject(c *gin.Context) {
|
||||
envMap[env.Key] = env.Value
|
||||
}
|
||||
|
||||
imageName, err := h.builder.Build(project.RepoURL, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, envMap, multi)
|
||||
imageName, commitHash, err := h.builder.Build(project.RepoURL, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, envMap, multi)
|
||||
deployment.Logs = logBuffer.String()
|
||||
|
||||
if err != nil {
|
||||
@@ -120,6 +110,11 @@ func (h *Handler) redeployProject(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update commit hash if we got one
|
||||
if commitHash != "" {
|
||||
deployment.Commit = commitHash
|
||||
}
|
||||
|
||||
var envStrings []string
|
||||
for _, env := range project.EnvVars {
|
||||
envStrings = append(envStrings, fmt.Sprintf("%s=%s", env.Key, env.Value))
|
||||
@@ -190,12 +185,17 @@ func (h *Handler) createProject(c *gin.Context) {
|
||||
envVarsModel = append(envVarsModel, models.EnvVar{Key: k, Value: v})
|
||||
}
|
||||
|
||||
webhookSecret, _ := auth.HashPassword(fmt.Sprintf("%s-%d-%d", req.Name, userID, time.Now().UnixNano()))
|
||||
secretBytes := make([]byte, 16)
|
||||
rand.Read(secretBytes)
|
||||
webhookSecret := hex.EncodeToString(secretBytes)
|
||||
|
||||
id, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
|
||||
project := models.Project{
|
||||
ID: id,
|
||||
Name: req.Name,
|
||||
RepoURL: req.Repo,
|
||||
OwnerID: userID.(uint),
|
||||
OwnerID: userID.(string),
|
||||
Port: port,
|
||||
WebhookSecret: webhookSecret,
|
||||
GitToken: req.GitToken,
|
||||
@@ -211,7 +211,9 @@ func (h *Handler) createProject(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
depID, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
deployment := models.Deployment{
|
||||
ID: depID,
|
||||
ProjectID: project.ID,
|
||||
Status: "building",
|
||||
Commit: "HEAD",
|
||||
@@ -224,7 +226,7 @@ func (h *Handler) createProject(c *gin.Context) {
|
||||
streamer := &StreamWriter{DeploymentID: deployment.ID}
|
||||
multi := io.MultiWriter(&logBuffer, streamer)
|
||||
|
||||
imageName, err := h.builder.Build(req.Repo, req.Name, req.GitToken, req.BuildCommand, req.StartCommand, req.InstallCommand, req.Runtime, req.EnvVars, multi)
|
||||
imageName, commitHash, err := h.builder.Build(req.Repo, req.Name, req.GitToken, req.BuildCommand, req.StartCommand, req.InstallCommand, req.Runtime, req.EnvVars, multi)
|
||||
deployment.Logs = logBuffer.String()
|
||||
|
||||
if err != nil {
|
||||
@@ -234,6 +236,11 @@ func (h *Handler) createProject(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update commit hash
|
||||
if commitHash != "" {
|
||||
deployment.Commit = commitHash
|
||||
}
|
||||
|
||||
containerID, err := h.deployer.RunContainer(c.Request.Context(), imageName, req.Name, port, envStrings)
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
@@ -266,16 +273,10 @@ func (h *Handler) getProject(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
projectID := c.Param("id")
|
||||
|
||||
pid, err := strconv.ParseUint(projectID, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Order("created_at desc").Preload("Deployments", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("deployments.created_at desc")
|
||||
}).Preload("EnvVars").Where("id = ? AND owner_id = ?", pid, userID).First(&project); result.Error != nil {
|
||||
}).Preload("EnvVars").Where("id = ? AND owner_id = ?", projectID, userID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (h *Handler) handleGetStorageStats(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) handleListDatabases(c *gin.Context) {
|
||||
userId := c.GetUint("userID")
|
||||
userId := c.GetString("userID")
|
||||
var dbs []models.Database
|
||||
if err := db.DB.Where("owner_id = ?", userId).Find(&dbs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list databases"})
|
||||
@@ -63,7 +63,7 @@ func (h *Handler) handleListDatabases(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreateDatabase(c *gin.Context) {
|
||||
userId := c.GetUint("userID")
|
||||
userId := c.GetString("userID")
|
||||
var req CreateDatabaseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -81,7 +81,7 @@ func (h *Handler) handleCreateDatabase(c *gin.Context) {
|
||||
dataDir := "./data/user_dbs"
|
||||
os.MkdirAll(dataDir, 0755)
|
||||
|
||||
dbPath := filepath.Join(dataDir, fmt.Sprintf("%d_%s.db", userId, req.Name))
|
||||
dbPath := filepath.Join(dataDir, fmt.Sprintf("%s_%s.db", userId, req.Name))
|
||||
file, err := os.Create(dbPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create database file"})
|
||||
|
||||
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -16,14 +15,14 @@ var upgrader = websocket.Upgrader{
|
||||
|
||||
type LogHub struct {
|
||||
mu sync.Mutex
|
||||
streams map[uint][]chan []byte
|
||||
streams map[string][]chan []byte
|
||||
}
|
||||
|
||||
var Hub = &LogHub{
|
||||
streams: make(map[uint][]chan []byte),
|
||||
streams: make(map[string][]chan []byte),
|
||||
}
|
||||
|
||||
func (h *LogHub) Broadcast(deploymentID uint, p []byte) {
|
||||
func (h *LogHub) Broadcast(deploymentID string, p []byte) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if sinks, ok := h.streams[deploymentID]; ok {
|
||||
@@ -36,7 +35,7 @@ func (h *LogHub) Broadcast(deploymentID uint, p []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LogHub) Subscribe(deploymentID uint) chan []byte {
|
||||
func (h *LogHub) Subscribe(deploymentID string) chan []byte {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
ch := make(chan []byte, 256)
|
||||
@@ -44,7 +43,7 @@ func (h *LogHub) Subscribe(deploymentID uint) chan []byte {
|
||||
return ch
|
||||
}
|
||||
|
||||
func (h *LogHub) Unsubscribe(deploymentID uint, ch chan []byte) {
|
||||
func (h *LogHub) Unsubscribe(deploymentID string, ch chan []byte) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if sinks, ok := h.streams[deploymentID]; ok {
|
||||
@@ -62,7 +61,7 @@ func (h *LogHub) Unsubscribe(deploymentID uint, ch chan []byte) {
|
||||
}
|
||||
|
||||
type StreamWriter struct {
|
||||
DeploymentID uint
|
||||
DeploymentID string
|
||||
}
|
||||
|
||||
func (w *StreamWriter) Write(p []byte) (n int, err error) {
|
||||
@@ -73,9 +72,8 @@ func (w *StreamWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (h *Handler) streamDeploymentLogs(c *gin.Context) {
|
||||
deploymentIDStr := c.Param("id")
|
||||
deploymentID, err := strconv.ParseUint(deploymentIDStr, 10, 64)
|
||||
if err != nil {
|
||||
deploymentID := c.Param("id")
|
||||
if deploymentID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
@@ -86,8 +84,8 @@ func (h *Handler) streamDeploymentLogs(c *gin.Context) {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
logChan := Hub.Subscribe(uint(deploymentID))
|
||||
defer Hub.Unsubscribe(uint(deploymentID), logChan)
|
||||
logChan := Hub.Subscribe(deploymentID)
|
||||
defer Hub.Unsubscribe(deploymentID, logChan)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
|
||||
@@ -5,38 +5,41 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
)
|
||||
|
||||
func (h *Handler) RegisterWebhookRoutes(r *gin.Engine) {
|
||||
r.POST("/webhooks/trigger", h.handleWebhook)
|
||||
r.POST("/projects/:projectID/webhook/:webhookID", h.handleWebhook)
|
||||
}
|
||||
|
||||
func (h *Handler) handleWebhook(c *gin.Context) {
|
||||
projectIDHex := c.Query("project_id")
|
||||
if projectIDHex == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "project_id required"})
|
||||
return
|
||||
}
|
||||
projectID := c.Param("projectID")
|
||||
webhookSecret := c.Param("webhookID")
|
||||
|
||||
pid, err := strconv.ParseUint(projectIDHex, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"})
|
||||
if projectID == "" || webhookSecret == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid webhook url"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Preload("EnvVars").First(&project, pid); result.Error != nil {
|
||||
if result := db.DB.Preload("EnvVars").Where("id = ?", projectID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if project.WebhookSecret != webhookSecret {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Webhook Secret"})
|
||||
return
|
||||
}
|
||||
|
||||
depID, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
deployment := models.Deployment{
|
||||
ID: depID,
|
||||
ProjectID: project.ID,
|
||||
Status: "building",
|
||||
Commit: "WEBHOOK",
|
||||
@@ -57,7 +60,7 @@ func (h *Handler) handleWebhook(c *gin.Context) {
|
||||
envMap[env.Key] = env.Value
|
||||
}
|
||||
|
||||
imageName, err := h.builder.Build(project.RepoURL, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, envMap, multi)
|
||||
imageName, commitHash, err := h.builder.Build(project.RepoURL, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, envMap, multi)
|
||||
deployment.Logs = logBuffer.String()
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
@@ -66,6 +69,10 @@ func (h *Handler) handleWebhook(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if commitHash != "" {
|
||||
deployment.Commit = commitHash
|
||||
}
|
||||
|
||||
var envStrings []string
|
||||
for _, env := range project.EnvVars {
|
||||
envStrings = append(envStrings, fmt.Sprintf("%s=%s", env.Key, env.Value))
|
||||
|
||||
Reference in New Issue
Block a user