feat: add team member role change & removal, add uptime server stats, etc

This commit is contained in:
2024-11-14 16:28:44 +00:00
parent b574f83e74
commit 33dda374c7
25 changed files with 566 additions and 110 deletions
+6 -12
View File
@@ -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)
+39
View File
@@ -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
}
+140
View File
@@ -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)
}
+11
View File
@@ -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"`
}
-12
View File
@@ -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
}
+3 -34
View File
@@ -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)
}
-5
View File
@@ -9,8 +9,3 @@ type GetOptions struct {
ID string
WithMembers bool
}
type InviteTeamSchema struct {
Username string `json:"username"`
Role string `json:"role"`
}
+7
View File
@@ -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
}
+28 -4
View File
@@ -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
View File
@@ -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
}
+30 -3
View File
@@ -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) {