mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-06-19 07:36:59 +00:00
feat(api/host): expose host-setup endpoints and finish CLI test action
Adds /api/host/setup/{preview,keypair,publickey,verify,known-host} and
wires the CLI 'host-setup test' subcommand through the shared
setup.NewClientFromSettings + setup.Verify helpers introduced in this
commit. Mounted under the authenticated router group.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package host
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func InitRouter(r *gin.RouterGroup) {
|
||||
g := r.Group("host/setup")
|
||||
{
|
||||
g.GET("preview", Preview)
|
||||
g.POST("preview", Preview)
|
||||
g.POST("keypair", GenerateKeypair)
|
||||
g.GET("publickey", GetPublicKey)
|
||||
g.DELETE("keypair", DeleteKeypair)
|
||||
g.POST("verify", Verify)
|
||||
g.POST("known-host", TrustHostKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/host/setup"
|
||||
hostssh "github.com/0xJacky/Nginx-UI/internal/host/ssh"
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/uozi-tech/cosy"
|
||||
)
|
||||
|
||||
// Preview renders all snippets from the posted SetupParams (or current
|
||||
// settings if body is empty). Does not persist anything.
|
||||
func Preview(c *gin.Context) {
|
||||
var p setup.SetupParams
|
||||
if err := c.ShouldBindJSON(&p); err != nil {
|
||||
p = setup.ParamsFromSettings()
|
||||
}
|
||||
r, err := setup.RenderAll(p)
|
||||
if err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, r)
|
||||
}
|
||||
|
||||
type keypairResponse struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
PrivateKey string `json:"private_key,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateKeypair creates a fresh ed25519 keypair, writes the private key to
|
||||
// HostPrivateKeyPath, returns the public key. The private key is also returned
|
||||
// once for the caller to display/download — never returned by GetPublicKey().
|
||||
func GenerateKeypair(c *gin.Context) {
|
||||
path := settings.NginxSettings.HostPrivateKeyPath
|
||||
if path == "" {
|
||||
path = "/etc/nginx-ui/host_key"
|
||||
}
|
||||
pub, err := setup.GenerateKeypair(path)
|
||||
if err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
priv, _ := os.ReadFile(path)
|
||||
c.JSON(http.StatusOK, keypairResponse{PublicKey: pub, PrivateKey: string(priv)})
|
||||
}
|
||||
|
||||
func GetPublicKey(c *gin.Context) {
|
||||
path := settings.NginxSettings.HostPrivateKeyPath
|
||||
pub, err := setup.LoadPublicKey(path)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"public_key": ""})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"public_key": pub})
|
||||
}
|
||||
|
||||
func DeleteKeypair(c *gin.Context) {
|
||||
path := settings.NginxSettings.HostPrivateKeyPath
|
||||
if path == "" {
|
||||
c.JSON(http.StatusNoContent, nil)
|
||||
return
|
||||
}
|
||||
_ = os.Remove(path)
|
||||
c.JSON(http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
type verifyRequest struct {
|
||||
SkipNginxT bool `json:"skip_nginx_t"`
|
||||
}
|
||||
|
||||
func Verify(c *gin.Context) {
|
||||
var req verifyRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
client, err := setup.NewClientFromSettings()
|
||||
if err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := setup.Verify(ctx, setup.VerifyOptions{
|
||||
Client: client,
|
||||
Params: setup.ParamsFromSettings(),
|
||||
SkipNginxT: req.SkipNginxT,
|
||||
})
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
type knownHostRequest struct {
|
||||
HostAddress string `json:"host_address" binding:"required"`
|
||||
Fingerprint string `json:"fingerprint" binding:"required"`
|
||||
PublicKey string `json:"public_key" binding:"required"`
|
||||
}
|
||||
|
||||
// TrustHostKey appends a known_hosts entry after the user confirms a fingerprint.
|
||||
func TrustHostKey(c *gin.Context) {
|
||||
var req knownHostRequest
|
||||
if !cosy.BindAndValid(c, &req) {
|
||||
return
|
||||
}
|
||||
path := settings.NginxSettings.HostKnownHostsPath
|
||||
if path == "" {
|
||||
path = "/etc/nginx-ui/known_hosts"
|
||||
}
|
||||
if err := hostssh.TrustHostKey(path, req.HostAddress, req.PublicKey); err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "trusted"})
|
||||
}
|
||||
@@ -136,8 +136,16 @@ func hostSetupPrint(ctx context.Context, c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// hostSetupTest is a stub here — Task 14 wires it through the shared
|
||||
// setup.NewClientFromSettings + setup.Verify helpers introduced in that task.
|
||||
func hostSetupTest(ctx context.Context, c *cli.Command) error {
|
||||
return fmt.Errorf("host-setup test: not yet wired to live settings; use the Web UI verify endpoint until Task 14 lands")
|
||||
client, err := setup.NewClientFromSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
result := setup.Verify(ctx, setup.VerifyOptions{
|
||||
Client: client,
|
||||
Params: setup.ParamsFromSettings(),
|
||||
})
|
||||
return json.NewEncoder(os.Stdout).Encode(result)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
hostssh "github.com/0xJacky/Nginx-UI/internal/host/ssh"
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
)
|
||||
|
||||
// NewClientFromSettings constructs a hostssh.Client using the currently loaded
|
||||
// settings.NginxSettings. The returned client is single-use for verify flows;
|
||||
// the long-lived client used by sshRunner is independent.
|
||||
func NewClientFromSettings() (*hostssh.Client, error) {
|
||||
n := settings.NginxSettings
|
||||
kh, err := hostssh.NewKnownHosts(n.HostKnownHostsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sudo := n.HostSudoPrefix
|
||||
if sudo == "" {
|
||||
sudo = "sudo -n"
|
||||
}
|
||||
systemctl := n.HostSystemctlPath
|
||||
if systemctl == "" {
|
||||
systemctl = "/bin/systemctl"
|
||||
}
|
||||
return hostssh.NewClient(hostssh.ClientOptions{
|
||||
Address: n.HostAddress,
|
||||
User: n.HostUser,
|
||||
AuthMethod: n.HostAuthMethod,
|
||||
PrivateKeyPath: n.HostPrivateKeyPath,
|
||||
KnownHosts: kh,
|
||||
Strict: n.HostStrictHostKey,
|
||||
Config: hostssh.Config{
|
||||
SudoPrefix: sudo,
|
||||
SystemctlPath: systemctl,
|
||||
NginxSbinPath: n.SbinPath,
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
|
||||
// ParamsFromSettings builds a SetupParams reflecting current settings.
|
||||
func ParamsFromSettings() SetupParams {
|
||||
n := settings.NginxSettings
|
||||
p := SetupParams{
|
||||
HostAddress: n.HostAddress,
|
||||
HostUser: n.HostUser,
|
||||
SystemdUnit: n.HostSystemdUnitName,
|
||||
SystemctlPath: n.HostSystemctlPath,
|
||||
NginxSbinPath: n.SbinPath,
|
||||
HostConfigDir: n.HostConfigDir,
|
||||
HostLogDir: n.HostLogDir,
|
||||
ContainerKeyPath: n.HostPrivateKeyPath,
|
||||
ContainerKnownHostsPath: n.HostKnownHostsPath,
|
||||
}
|
||||
return p.FillDefaults()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"github.com/uozi-tech/cosy"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// TrustHostKey parses an OpenSSH-formatted public key string and appends it to
|
||||
// the known_hosts file at path. Returns ErrKnownHostsWrite on file errors.
|
||||
func TrustHostKey(path, hostPort, publicKeyOpenSSH string) error {
|
||||
kh, err := NewKnownHosts(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, _, _, _, err := gossh.ParseAuthorizedKey([]byte(publicKeyOpenSSH))
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrHostKeyMismatch, "parse public key", err.Error())
|
||||
}
|
||||
return kh.Trust(hostPort, parsed)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/0xJacky/Nginx-UI/api/event"
|
||||
"github.com/0xJacky/Nginx-UI/api/external_notify"
|
||||
"github.com/0xJacky/Nginx-UI/api/geolite"
|
||||
"github.com/0xJacky/Nginx-UI/api/host"
|
||||
"github.com/0xJacky/Nginx-UI/api/license"
|
||||
"github.com/0xJacky/Nginx-UI/api/llm"
|
||||
"github.com/0xJacky/Nginx-UI/api/nginx"
|
||||
@@ -103,6 +104,7 @@ func InitRouter() {
|
||||
settings.InitRouter(g)
|
||||
llm.InitRouter(g)
|
||||
cluster.InitRouter(g)
|
||||
host.InitRouter(g)
|
||||
notification.InitRouter(g)
|
||||
external_notify.InitRouter(g)
|
||||
backup.InitAutoBackupRouter(g)
|
||||
|
||||
Reference in New Issue
Block a user