package api import ( "fmt" "net/http" "os" "github.com/gin-gonic/gin" "github.com/mrhid6/keymanager/server/internal/auth" "github.com/mrhid6/keymanager/server/internal/models" "github.com/mrhid6/keymanager/server/internal/services" ) func RegisterRoutes(r *gin.Engine) { r.GET("/install", handleInstallScript) r.GET("/update", handleUpdateScript) // Auth endpoints (no session required) r.GET("/auth/login", auth.HandleLogin) r.GET("/auth/callback", auth.HandleCallback) r.GET("/auth/logout", auth.HandleLogout) r.GET("/auth/me", auth.HandleMe) // API endpoints protected by session middleware apiGroup := r.Group("/api") apiGroup.Use(auth.Middleware()) { apiGroup.GET("/servers", listServers) apiGroup.POST("/servers", createServer) apiGroup.GET("/servers/new", newServer) apiGroup.POST("/servers/new", newServer) apiGroup.GET("/servers/:id", getServer) apiGroup.DELETE("/servers/:id", deleteServer) apiGroup.POST("/servers/:id/generate-key", generateKey) apiGroup.GET("/keys", listKeys) apiGroup.POST("/keys", createKey) apiGroup.GET("/keys/:id", getKey) apiGroup.GET("/keys/:id/private-key", getPrivateKey) apiGroup.DELETE("/keys/:id", deleteKey) apiGroup.POST("/keys/:id/assign", assignKey) apiGroup.DELETE("/keys/:id/assign/:serverId", revokeAssignment) } } func listServers(c *gin.Context) { servers, err := services.ListServers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, servers) } func createServer(c *gin.Context) { s, token, err := services.CreateServer() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "server": s, "token": token, "server_id": s.ServerID, }) } func newServer(c *gin.Context) { s, token, err := services.CreateServer() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } giteaHost := os.Getenv("GITEA_HOST") if giteaHost == "" { giteaHost = "gitea.example.com" } host := os.Getenv("PUBLIC_HOST") if host == "" { host = "https://keymanager.example.com" } installCmd := fmt.Sprintf( `curl -fsSL "%s/install?server_id=%s&token=%s" | bash`, host, s.ServerID, token, ) c.JSON(http.StatusOK, gin.H{ "server_id": s.ServerID, "pre_reg_token": token, "install_command": installCmd, }) } func getServer(c *gin.Context) { id := c.Param("id") s, err := services.GetServer(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) return } assignments, _ := services.GetAssignmentsWithKeysForServer(id) // Build response matching ServerWithKeys shape expected by frontend type serverResponse struct { *models.Server Keys interface{} `json:"keys"` } c.JSON(http.StatusOK, serverResponse{ Server: s, Keys: assignments, }) } func deleteServer(c *gin.Context) { id := c.Param("id") if err := services.DeleteServer(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"deleted": true}) } func generateKey(c *gin.Context) { id := c.Param("id") var body struct { Label string `json:"label"` KeyType string `json:"key_type"` KeySize int `json:"key_size"` Passphrase string `json:"passphrase"` Comment string `json:"comment"` } _ = c.ShouldBindJSON(&body) if body.Label == "" { body.Label = "generated" } s, err := services.GetServer(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) return } cmdID, err := services.DispatchGenerateKey(s.ServerID, services.KeyGenParams{ Label: body.Label, KeyType: body.KeyType, KeySize: body.KeySize, Passphrase: body.Passphrase, Comment: body.Comment, }) if err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()}) return } c.JSON(http.StatusAccepted, gin.H{ "message": "key generation command sent to agent", "command_id": cmdID, "server_id": s.ServerID, }) } func listKeys(c *gin.Context) { keys, err := services.ListKeys() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, keys) } func createKey(c *gin.Context) { var body struct { Label string `json:"label" binding:"required"` PublicKey string `json:"public_key" binding:"required"` PrivateKey string `json:"private_key"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } key, err := services.CreateKey(body.Label, body.PublicKey, "uploaded", "", body.PrivateKey) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, key) } func getPrivateKey(c *gin.Context) { id := c.Param("id") plaintext, err := services.GetPrivateKey(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"private_key": plaintext}) } func getKey(c *gin.Context) { id := c.Param("id") key, err := services.GetKey(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "key not found"}) return } assignments, _ := services.GetAssignmentsWithServers(id) type keyResponse struct { *models.Key Assignments any `json:"assignments"` } c.JSON(http.StatusOK, keyResponse{ Key: key, Assignments: assignments, }) } func deleteKey(c *gin.Context) { id := c.Param("id") if err := services.DeleteKey(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"deleted": true}) } func assignKey(c *gin.Context) { keyID := c.Param("id") var body struct { ServerID string `json:"server_id" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } a, err := services.AssignKey(keyID, body.ServerID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, a) } func revokeAssignment(c *gin.Context) { keyID := c.Param("id") serverID := c.Param("serverId") if err := services.RevokeAssignment(keyID, serverID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"revoked": true}) } func handleUpdateScript(c *gin.Context) { giteaHost := os.Getenv("GITEA_HOST") if giteaHost == "" { giteaHost = "gitea.example.com" } script := fmt.Sprintf(`#!/usr/bin/env bash set -euo pipefail GITEA_HOST="%s" ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH="amd64" ;; aarch64) ARCH="arm64" ;; *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac # Get latest agent release tag LATEST=$(curl -fsSL "https://${GITEA_HOST}/api/v1/repos/mrhid6/keymanager/releases?limit=10" \ | grep -o '"tag_name":"agent/v[^"]*"' | head -1 | sed 's/"tag_name":"//;s/"//') if [ -z "$LATEST" ]; then echo "Could not determine latest agent version" >&2 exit 1 fi VERSION="${LATEST#agent/}" LATEST_ENCODED="${LATEST/\//%%2F}" BINARY_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST_ENCODED}/keymanager-agent-linux-${ARCH}" CHECKSUM_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST_ENCODED}/checksums.txt" echo "Updating keymanager-agent to ${VERSION} (${ARCH})..." curl -fsSL -o /tmp/keymanager-agent "${BINARY_URL}" curl -fsSL -o /tmp/checksums.txt "${CHECKSUM_URL}" cd /tmp EXPECTED=$(grep "keymanager-agent-linux-${ARCH}" checksums.txt | awk '{print $1}') ACTUAL=$(sha256sum keymanager-agent | awk '{print $1}') if [ "$EXPECTED" != "$ACTUAL" ]; then echo "Checksum mismatch!" >&2 exit 1 fi systemctl stop keymanager-agent || true install -m 0755 /tmp/keymanager-agent /usr/local/bin/keymanager-agent systemctl start keymanager-agent echo "keymanager-agent updated to ${VERSION} and restarted." `, giteaHost) c.Header("Content-Type", "text/x-shellscript") c.String(http.StatusOK, script) } func handleInstallScript(c *gin.Context) { serverID := c.Query("server_id") token := c.Query("token") giteaHost := os.Getenv("GITEA_HOST") if giteaHost == "" { giteaHost = "gitea.example.com" } publicHost := os.Getenv("PUBLIC_HOST") if publicHost == "" { publicHost = "keymanager.example.com" } grpcHost := os.Getenv("GRPC_HOST") if grpcHost == "" { grpcHost = publicHost } script := fmt.Sprintf(`#!/usr/bin/env bash set -euo pipefail SERVER_ID="%s" TOKEN="%s" GITEA_HOST="%s" KM_HOST="%s" KM_HOST="${KM_HOST#https://}" KM_HOST="${KM_HOST#http://}" GRPC_HOST="%s" GRPC_HOST="${GRPC_HOST#https://}" GRPC_HOST="${GRPC_HOST#http://}" ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH="amd64" ;; aarch64) ARCH="arm64" ;; *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac # Get latest agent release tag LATEST=$(curl -fsSL "https://${GITEA_HOST}/api/v1/repos/mrhid6/keymanager/releases?limit=10" \ | grep -o '"tag_name":"agent/v[^"]*"' | head -1 | sed 's/"tag_name":"//;s/"//') if [ -z "$LATEST" ]; then echo "Could not determine latest agent version" >&2 exit 1 fi VERSION="${LATEST#agent/}" LATEST_ENCODED="${LATEST/\//%%2F}" BINARY_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST_ENCODED}/keymanager-agent-linux-${ARCH}" CHECKSUM_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST_ENCODED}/checksums.txt" echo "Installing keymanager-agent ${VERSION} (${ARCH})..." curl -fsSL -o /tmp/keymanager-agent "${BINARY_URL}" curl -fsSL -o /tmp/checksums.txt "${CHECKSUM_URL}" cd /tmp EXPECTED=$(grep "keymanager-agent-linux-${ARCH}" checksums.txt | awk '{print $1}') ACTUAL=$(sha256sum keymanager-agent | awk '{print $1}') if [ "$EXPECTED" != "$ACTUAL" ]; then echo "Checksum mismatch!" >&2 exit 1 fi install -m 0755 /tmp/keymanager-agent /usr/local/bin/keymanager-agent mkdir -p /etc/keymanager chmod 0700 /etc/keymanager cat > /etc/keymanager/config.yaml < /etc/systemd/system/keymanager-agent.service <