mirror of
https://github.com/khairul169/vaulterm.git
synced 2026-09-15 17:03:30 +07:00
feat: add team member role change & removal, add uptime server stats, etc
This commit is contained in:
+6
-12
@@ -5,21 +5,15 @@ import (
|
||||
"rul.sh/vaulterm/app/hosts"
|
||||
"rul.sh/vaulterm/app/keychains"
|
||||
"rul.sh/vaulterm/app/teams"
|
||||
"rul.sh/vaulterm/app/teams/members"
|
||||
"rul.sh/vaulterm/app/ws"
|
||||
)
|
||||
|
||||
func InitRouter(app *fiber.App) {
|
||||
// App route list
|
||||
routes := []Router{
|
||||
hosts.Router,
|
||||
keychains.Router,
|
||||
teams.Router,
|
||||
ws.Router,
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
route(app)
|
||||
}
|
||||
hosts.Router(app)
|
||||
keychains.Router(app)
|
||||
teams := teams.Router(app)
|
||||
members.Router(teams)
|
||||
ws.Router(app)
|
||||
}
|
||||
|
||||
type Router func(app fiber.Router)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package members
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"rul.sh/vaulterm/db"
|
||||
"rul.sh/vaulterm/models"
|
||||
"rul.sh/vaulterm/utils"
|
||||
)
|
||||
|
||||
type TeamMembers struct {
|
||||
db *gorm.DB
|
||||
User *utils.UserContext
|
||||
}
|
||||
|
||||
func NewRepository(r *TeamMembers) *TeamMembers {
|
||||
if r == nil {
|
||||
r = &TeamMembers{}
|
||||
}
|
||||
r.db = db.Get()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *TeamMembers) Add(data *models.TeamMembers) error {
|
||||
ret := r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(data)
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
func (r *TeamMembers) SetRole(data *models.TeamMembers) error {
|
||||
ret := r.db.
|
||||
Where("team_id = ? AND user_id = ?", data.TeamID, data.UserID).
|
||||
Updates(&models.TeamMembers{Role: data.Role})
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
func (r *TeamMembers) Remove(data *models.TeamMembers) error {
|
||||
ret := r.db.Delete(&models.TeamMembers{TeamID: data.TeamID, UserID: data.UserID})
|
||||
return ret.Error
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package members
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"rul.sh/vaulterm/app/teams"
|
||||
"rul.sh/vaulterm/app/users"
|
||||
"rul.sh/vaulterm/models"
|
||||
"rul.sh/vaulterm/utils"
|
||||
)
|
||||
|
||||
func Router(app fiber.Router) {
|
||||
router := app.Group("/:id/members")
|
||||
|
||||
// router.Get("/", getAll)
|
||||
router.Post("/", invite)
|
||||
router.Put("/:userId/role", setRole)
|
||||
router.Delete("/:userId", remove)
|
||||
}
|
||||
|
||||
// func getAll(c *fiber.Ctx) error {
|
||||
// user := utils.GetUser(c)
|
||||
// repo := NewRepository(&TeamMembers{User: user})
|
||||
|
||||
// rows, err := repo.GetAll()
|
||||
// if err != nil {
|
||||
// return utils.ResponseError(c, err, 500)
|
||||
// }
|
||||
|
||||
// return c.JSON(fiber.Map{"rows": rows})
|
||||
// }
|
||||
|
||||
func invite(c *fiber.Ctx) error {
|
||||
var body InviteSchema
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
user := utils.GetUser(c)
|
||||
teamRepo := teams.NewRepository(&teams.Teams{User: user})
|
||||
repo := NewRepository(&TeamMembers{User: user})
|
||||
|
||||
id := c.Params("id")
|
||||
exist, _ := teamRepo.Exists(id)
|
||||
if !exist {
|
||||
return utils.ResponseError(c, errors.New("team not found"), 404)
|
||||
}
|
||||
if !user.TeamCanWrite(&id) {
|
||||
return utils.ResponseError(c, errors.New("no access"), 403)
|
||||
}
|
||||
|
||||
userRepo := users.NewRepository(&users.Users{User: user})
|
||||
userData, _ := userRepo.Find(body.Username)
|
||||
if userData.ID == "" {
|
||||
return utils.ResponseError(c, errors.New("user not found"), 404)
|
||||
}
|
||||
|
||||
err := repo.Add(&models.TeamMembers{TeamID: id, UserID: userData.ID, Role: body.Role})
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(true)
|
||||
}
|
||||
|
||||
func setRole(c *fiber.Ctx) error {
|
||||
var body PutRoleSchema
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
user := utils.GetUser(c)
|
||||
teamRepo := teams.NewRepository(&teams.Teams{User: user})
|
||||
repo := NewRepository(&TeamMembers{User: user})
|
||||
|
||||
id := c.Params("id")
|
||||
userId := c.Params("userId")
|
||||
|
||||
exist, _ := teamRepo.Exists(id)
|
||||
if !exist {
|
||||
return utils.ResponseError(c, errors.New("team not found"), 404)
|
||||
}
|
||||
if !user.TeamCanWrite(&id) {
|
||||
return utils.ResponseError(c, errors.New("no access"), 403)
|
||||
}
|
||||
|
||||
userRepo := users.NewRepository(nil)
|
||||
userData, _ := userRepo.Get(userId)
|
||||
if userData == nil || userData.ID == "" {
|
||||
return utils.ResponseError(c, errors.New("user not found"), 404)
|
||||
}
|
||||
if !userData.IsInTeam(&id) {
|
||||
return utils.ResponseError(c, errors.New("user not in team"), 400)
|
||||
}
|
||||
|
||||
err := repo.SetRole(&models.TeamMembers{TeamID: id, UserID: userData.ID, Role: body.Role})
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(true)
|
||||
}
|
||||
|
||||
func remove(c *fiber.Ctx) error {
|
||||
user := utils.GetUser(c)
|
||||
teamRepo := teams.NewRepository(&teams.Teams{User: user})
|
||||
repo := NewRepository(&TeamMembers{User: user})
|
||||
|
||||
id := c.Params("id")
|
||||
userId := c.Params("userId")
|
||||
|
||||
exist, _ := teamRepo.Exists(id)
|
||||
if !exist {
|
||||
return utils.ResponseError(c, errors.New("team not found"), 404)
|
||||
}
|
||||
if !user.TeamCanWrite(&id) {
|
||||
return utils.ResponseError(c, errors.New("no access"), 403)
|
||||
}
|
||||
|
||||
userRepo := users.NewRepository(&users.Users{User: user})
|
||||
userData, _ := userRepo.Get(userId)
|
||||
if userData.ID == "" {
|
||||
return utils.ResponseError(c, errors.New("user not found"), 404)
|
||||
}
|
||||
userRole := userData.GetTeamRole(&id)
|
||||
if userRole == "" {
|
||||
return utils.ResponseError(c, errors.New("user not in team"), 400)
|
||||
}
|
||||
if userRole == models.TeamRoleOwner {
|
||||
return utils.ResponseError(c, errors.New("cannot remove owner"), 400)
|
||||
}
|
||||
|
||||
err := repo.Remove(&models.TeamMembers{TeamID: id, UserID: userData.ID})
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(true)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package members
|
||||
|
||||
type InviteSchema struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type PutRoleSchema struct {
|
||||
UserID string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package teams
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"rul.sh/vaulterm/db"
|
||||
"rul.sh/vaulterm/models"
|
||||
"rul.sh/vaulterm/utils"
|
||||
@@ -94,14 +93,3 @@ func (r *Teams) Delete(id string) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Teams) Invite(teamId string, userId string, role string) error {
|
||||
ret := r.db.
|
||||
Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Create(&models.TeamMembers{
|
||||
TeamID: teamId,
|
||||
UserID: userId,
|
||||
Role: role,
|
||||
})
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"rul.sh/vaulterm/app/users"
|
||||
"rul.sh/vaulterm/models"
|
||||
"rul.sh/vaulterm/utils"
|
||||
)
|
||||
|
||||
func Router(app fiber.Router) {
|
||||
func Router(app fiber.Router) fiber.Router {
|
||||
router := app.Group("/teams")
|
||||
|
||||
router.Get("/", getAll)
|
||||
@@ -18,7 +17,8 @@ func Router(app fiber.Router) {
|
||||
router.Post("/", create)
|
||||
router.Put("/:id", update)
|
||||
router.Delete("/:id", delete)
|
||||
router.Post("/:id/invite", invite)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func getAll(c *fiber.Ctx) error {
|
||||
@@ -116,34 +116,3 @@ func delete(c *fiber.Ctx) error {
|
||||
|
||||
return c.JSON(true)
|
||||
}
|
||||
|
||||
func invite(c *fiber.Ctx) error {
|
||||
var body InviteTeamSchema
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
user := utils.GetUser(c)
|
||||
repo := NewRepository(&Teams{User: user})
|
||||
|
||||
id := c.Params("id")
|
||||
exist, _ := repo.Exists(id)
|
||||
if !exist {
|
||||
return utils.ResponseError(c, errors.New("team not found"), 404)
|
||||
}
|
||||
if !user.TeamCanWrite(&id) {
|
||||
return utils.ResponseError(c, errors.New("no access"), 403)
|
||||
}
|
||||
|
||||
userRepo := users.NewRepository(&users.Users{User: user})
|
||||
userData, _ := userRepo.Find(body.Username)
|
||||
if userData.ID == "" {
|
||||
return utils.ResponseError(c, errors.New("user not found"), 404)
|
||||
}
|
||||
|
||||
if err := repo.Invite(id, userData.ID, body.Role); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(true)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,3 @@ type GetOptions struct {
|
||||
ID string
|
||||
WithMembers bool
|
||||
}
|
||||
|
||||
type InviteTeamSchema struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
@@ -26,3 +26,10 @@ func (r *Users) Find(username string) (*models.User, error) {
|
||||
|
||||
return &user, ret.Error
|
||||
}
|
||||
|
||||
func (r *Users) Get(id string) (*models.User, error) {
|
||||
var user models.User
|
||||
ret := r.db.Preload("Teams").Where("id = ?", id).First(&user)
|
||||
|
||||
return &user, ret.Error
|
||||
}
|
||||
|
||||
@@ -32,11 +32,12 @@ func HandleSSHStats(c *websocket.Conn, client *lib.SSHClient) error {
|
||||
return
|
||||
default:
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(4)
|
||||
wg.Add(5)
|
||||
go getCPUUsage(client, wg, msgCh)
|
||||
go getMemoryUsage(client, wg, msgCh)
|
||||
go getDiskUsage(client, wg, msgCh)
|
||||
go getNetworkUsage(client, wg, msgCh)
|
||||
go getUptime(client, wg, msgCh)
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
@@ -63,7 +64,8 @@ func HandleSSHStats(c *websocket.Conn, client *lib.SSHClient) error {
|
||||
func getCPUUsage(client *lib.SSHClient, wg *sync.WaitGroup, result chan<- string) {
|
||||
defer wg.Done()
|
||||
|
||||
cpuData, err := client.Exec("cat /proc/stat | grep '^cpu '")
|
||||
cmd := "cat /proc/stat | grep '^cpu '"
|
||||
cpuData, err := client.Exec(cmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -73,8 +75,7 @@ func getCPUUsage(client *lib.SSHClient, wg *sync.WaitGroup, result chan<- string
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
cpuData, err = client.Exec("cat /proc/stat | grep '^cpu '")
|
||||
cpuData, err = client.Exec(cmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -207,3 +208,26 @@ func parseNetwork(data string) (int, int) {
|
||||
|
||||
return txBytes, rxBytes
|
||||
}
|
||||
|
||||
func getUptime(client *lib.SSHClient, wg *sync.WaitGroup, result chan<- string) {
|
||||
defer wg.Done()
|
||||
|
||||
// Try to read uptime from /proc/uptime
|
||||
data, err := client.Exec("cat /proc/uptime")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data = strings.TrimSpace(data)
|
||||
uptimeParts := strings.Split(data, " ")
|
||||
if len(uptimeParts) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
uptimeSeconds, err := strconv.ParseFloat(uptimeParts[0], 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result <- fmt.Sprintf("\x05%d", int(uptimeSeconds))
|
||||
}
|
||||
|
||||
+27
-20
@@ -1,6 +1,7 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"strconv"
|
||||
@@ -10,67 +11,75 @@ import (
|
||||
"rul.sh/vaulterm/lib"
|
||||
)
|
||||
|
||||
func NewSSHWebsocketSession(c *websocket.Conn, client *lib.SSHClient) error {
|
||||
func NewSSHWebsocketSession(c *websocket.Conn, client *lib.SSHClient) ([]byte, error) {
|
||||
if err := client.Connect(); err != nil {
|
||||
log.Printf("error connecting to SSH: %v", err)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
shell, err := client.StartPtyShell()
|
||||
if err != nil {
|
||||
log.Printf("error starting SSH shell: %v", err)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session := shell.Session
|
||||
defer session.Close()
|
||||
|
||||
// Goroutine to send SSH stdout to WebSocket
|
||||
sessionCapture, sessionCaptureWriter := io.Pipe()
|
||||
defer sessionCapture.Close()
|
||||
sessionLog := []byte{}
|
||||
|
||||
// Capture SSH session output
|
||||
go func() {
|
||||
reader := bufio.NewReader(sessionCapture)
|
||||
for {
|
||||
b, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
sessionLog = append(sessionLog, b...)
|
||||
}
|
||||
}()
|
||||
|
||||
// Pass SSH stdout to WebSocket
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := shell.Stdout.Read(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Printf("error reading from SSH stdout: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
sessionCaptureWriter.Write(buf[:n])
|
||||
if err := c.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||
log.Printf("error writing to websocket: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Goroutine to handle SSH stderr
|
||||
// Pass SSH stderr to WebSocket
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := shell.Stderr.Read(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Printf("error reading from SSH stderr: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
sessionCaptureWriter.Write(buf[:n])
|
||||
if err := c.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||
log.Printf("error writing to websocket: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle WebSocket to SSH data streaming
|
||||
// Handle user input
|
||||
go func() {
|
||||
defer session.Close()
|
||||
|
||||
for {
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("error reading from websocket: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -86,15 +95,13 @@ func NewSSHWebsocketSession(c *websocket.Conn, client *lib.SSHClient) error {
|
||||
|
||||
shell.Stdin.Write(msg)
|
||||
}
|
||||
|
||||
log.Println("SSH session closed")
|
||||
}()
|
||||
|
||||
// Wait for the SSH session to close
|
||||
if err := session.Wait(); err != nil {
|
||||
log.Printf("SSH session ended with error: %v", err)
|
||||
return err
|
||||
return sessionLog, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return sessionLog, nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package term
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"rul.sh/vaulterm/app/hosts"
|
||||
"rul.sh/vaulterm/db"
|
||||
"rul.sh/vaulterm/lib"
|
||||
"rul.sh/vaulterm/models"
|
||||
"rul.sh/vaulterm/utils"
|
||||
@@ -23,9 +25,19 @@ func HandleTerm(c *websocket.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
log := ""
|
||||
term := &models.TermSession{
|
||||
UserID: user.ID,
|
||||
HostID: hostId,
|
||||
Reason: c.Query("reason"),
|
||||
}
|
||||
if err := db.Get().Create(term).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch data.Host.Type {
|
||||
case "ssh":
|
||||
sshHandler(c, data)
|
||||
sshHandler(c, data, &log)
|
||||
case "pve":
|
||||
pveHandler(c, data)
|
||||
case "incus":
|
||||
@@ -33,9 +45,18 @@ func HandleTerm(c *websocket.Conn) {
|
||||
default:
|
||||
c.WriteMessage(websocket.TextMessage, []byte("Invalid host type"))
|
||||
}
|
||||
|
||||
// save session log
|
||||
endsAt := time.Now()
|
||||
db.Get().
|
||||
Where("id = ?", term.ID).
|
||||
Updates(&models.TermSession{
|
||||
EndsAt: &endsAt,
|
||||
Log: log,
|
||||
})
|
||||
}
|
||||
|
||||
func sshHandler(c *websocket.Conn, data *models.HostDecrypted) {
|
||||
func sshHandler(c *websocket.Conn, data *models.HostDecrypted, log *string) {
|
||||
cfg := lib.NewSSHClient(&lib.SSHClientConfig{
|
||||
HostName: data.Host.Host,
|
||||
Port: data.Port,
|
||||
@@ -43,9 +64,15 @@ func sshHandler(c *websocket.Conn, data *models.HostDecrypted) {
|
||||
AltKey: data.AltKey,
|
||||
})
|
||||
|
||||
if err := NewSSHWebsocketSession(c, cfg); err != nil {
|
||||
out, err := NewSSHWebsocketSession(c, cfg)
|
||||
if err != nil {
|
||||
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||
}
|
||||
|
||||
// copy output
|
||||
if log != nil {
|
||||
*log = string(out)
|
||||
}
|
||||
}
|
||||
|
||||
func pveHandler(c *websocket.Conn, data *models.HostDecrypted) {
|
||||
|
||||
Reference in New Issue
Block a user