feat: init api, db, app

This commit is contained in:
Khairul Hidayat 2024-11-07 19:07:41 +00:00
parent ab9b3368d1
commit 11b063c2fa
43 changed files with 1488 additions and 138 deletions

View File

@ -12,24 +12,28 @@ const HomePage = () => {
const [sessions, setSessions] = useState<Session[]>([ const [sessions, setSessions] = useState<Session[]>([
{ {
id: "1", id: "1",
type: "incus", type: "ssh",
params: { client: "xtermjs", serverId: "1", shell: "bash" }, params: { hostId: "01jc3v9w609f8e2wzw60amv195" },
},
{
id: "2",
type: "pve",
params: { client: "vnc", hostId: "01jc3wp2b3zvgr777f4e3caw4w" },
},
{
id: "3",
type: "pve",
params: { client: "xtermjs", hostId: "01jc3z3yyn2fgb77tyfxc1tkfy" },
},
{
id: "4",
type: "incus",
params: {
client: "xtermjs",
hostId: "01jc3xz9db0v54dg10hk70a13b",
shell: "fish",
},
}, },
// {
// id: "1",
// type: "ssh",
// params: { serverId: "1" },
// },
// {
// id: "2",
// type: "pve",
// params: { client: "vnc", serverId: "2" },
// },
// {
// id: "3",
// type: "pve",
// params: { client: "xtermjs", serverId: "3" },
// },
]); ]);
const [curSession, setSession] = useState(0); const [curSession, setSession] = useState(0);

View File

@ -6,7 +6,7 @@ import VNCViewer from "./vncviewer";
type SSHSessionProps = { type SSHSessionProps = {
type: "ssh"; type: "ssh";
params: { params: {
serverId: string; hostId: string;
}; };
}; };
@ -14,7 +14,7 @@ type PVESessionProps = {
type: "pve"; type: "pve";
params: { params: {
client: "vnc" | "xtermjs"; client: "vnc" | "xtermjs";
serverId: string; hostId: string;
}; };
}; };
@ -22,7 +22,7 @@ type IncusSessionProps = {
type: "incus"; type: "incus";
params: { params: {
client: "vnc" | "xtermjs"; client: "vnc" | "xtermjs";
serverId: string; hostId: string;
shell?: string; shell?: string;
}; };
}; };
@ -33,29 +33,19 @@ export type InteractiveSessionProps =
| IncusSessionProps; | IncusSessionProps;
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => { const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
let url = ""; const query = new URLSearchParams(params);
const query = new URLSearchParams({ const url = `${BASE_WS_URL}/ws/term?${query}`;
...params,
});
switch (type) { switch (type) {
case "ssh": case "ssh":
return <Terminal wsUrl={`${BASE_WS_URL}/ws/ssh?${query}`} />; return <Terminal url={url} />;
case "pve": case "pve":
url = `${BASE_WS_URL}/ws/pve?${query}`;
return params.client === "vnc" ? (
<VNCViewer url={url} />
) : (
<Terminal wsUrl={url} />
);
case "incus": case "incus":
url = `${BASE_WS_URL}/ws/incus?${query}`;
return params.client === "vnc" ? ( return params.client === "vnc" ? (
<VNCViewer url={url} /> <VNCViewer url={url} />
) : ( ) : (
<Terminal wsUrl={url} /> <Terminal url={url} />
); );
default: default:

View File

@ -28,7 +28,7 @@ const Keys = {
type XTermJsProps = { type XTermJsProps = {
client?: "xtermjs"; client?: "xtermjs";
wsUrl: string; url: string;
}; };
type TerminalProps = ComponentPropsWithoutRef<typeof View> & XTermJsProps; type TerminalProps = ComponentPropsWithoutRef<typeof View> & XTermJsProps;
@ -50,7 +50,7 @@ const Terminal = ({ client = "xtermjs", style, ...props }: TerminalProps) => {
<XTermJs <XTermJs
ref={xtermRef} ref={xtermRef}
dom={{ scrollEnabled: false }} dom={{ scrollEnabled: false }}
wsUrl={props.wsUrl} wsUrl={props.url}
/> />
)} )}

View File

@ -19,6 +19,12 @@ const VNCViewer = ({ ...props }: VNCViewerProps) => {
const rfb = new RFB(screenRef.current!, props.url); const rfb = new RFB(screenRef.current!, props.url);
rfb.scaleViewport = true; rfb.scaleViewport = true;
const canvas: HTMLCanvasElement | null =
rfb._target?.querySelector("canvas");
if (canvas) {
canvas.style.cursor = "default";
}
// @ts-ignore // @ts-ignore
const ws: WebSocket = rfb._sock._websocket; const ws: WebSocket = rfb._sock._websocket;
let password: string | null = null; let password: string | null = null;

1
server/.gitignore vendored
View File

@ -1,2 +1,3 @@
tmp/ tmp/
.env .env
/data.db*

15
server/Makefile Normal file
View File

@ -0,0 +1,15 @@
# Makefile
.PHONY: test build run
# Run tests sequentially with verbose output
test:
go test -count 1 -p 1 -v rul.sh/vaulterm/tests
# Build the Go application
build:
go build -o myapp .
# Run the built application
run: build
./myapp

37
server/app/app.go Normal file
View File

@ -0,0 +1,37 @@
package app
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/joho/godotenv"
"rul.sh/vaulterm/app/auth"
"rul.sh/vaulterm/app/hosts"
"rul.sh/vaulterm/app/keychains"
"rul.sh/vaulterm/app/ws"
"rul.sh/vaulterm/db"
)
func NewApp() *fiber.App {
// Load deps
godotenv.Load()
db.Init()
// Create fiber app
app := fiber.New(fiber.Config{ErrorHandler: ErrorHandler})
// Middlewares
app.Use(cors.New())
// Init app routes
auth.Router(app)
hosts.Router(app)
keychains.Router(app)
ws.Router(app)
// Health check
app.Get("/health-check", func(c *fiber.Ctx) error {
return c.SendString("OK")
})
return app
}

View File

@ -0,0 +1,50 @@
package auth
import (
"gorm.io/gorm"
"rul.sh/vaulterm/db"
"rul.sh/vaulterm/lib"
"rul.sh/vaulterm/models"
)
type Auth struct{ db *gorm.DB }
func NewAuthRepository() *Auth {
return &Auth{db: db.Get()}
}
func (r *Auth) FindUser(username string) (*models.User, error) {
var user models.User
ret := r.db.Where("username = ? OR email = ?", username, username).First(&user)
return &user, ret.Error
}
func (r *Auth) CreateUserSession(user *models.User) (string, error) {
sessionId, err := lib.GenerateSessionID(20)
if err != nil {
return "", err
}
if ret := r.db.Create(&models.UserSession{ID: sessionId, UserID: user.ID}); ret.Error != nil {
return "", ret.Error
}
return sessionId, nil
}
func (r *Auth) GetSession(sessionId string) (*models.UserSession, error) {
var session models.UserSession
res := r.db.Joins("User").Where(&models.UserSession{ID: sessionId}).First(&session)
return &session, res.Error
}
func (r *Auth) RemoveUserSession(sessionId string, force bool) error {
db := r.db
if force {
db = db.Unscoped()
}
res := db.Delete(&models.UserSession{ID: sessionId})
return res.Error
}

93
server/app/auth/router.go Normal file
View File

@ -0,0 +1,93 @@
package auth
import (
"strings"
"github.com/gofiber/fiber/v2"
"rul.sh/vaulterm/lib"
"rul.sh/vaulterm/utils"
)
func Router(app *fiber.App) {
router := app.Group("/auth")
router.Post("/login", login)
router.Get("/user", getUser)
router.Post("/logout", logout)
}
func login(c *fiber.Ctx) error {
repo := NewAuthRepository()
var body LoginSchema
if err := c.BodyParser(&body); err != nil {
return &fiber.Error{
Code: fiber.StatusBadRequest,
Message: err.Error(),
}
}
user, err := repo.FindUser(body.Username)
if err != nil {
return &fiber.Error{
Code: fiber.StatusUnauthorized,
Message: "Username or password is invalid",
}
}
if valid := lib.VerifyPassword(body.Password, user.Password); !valid {
return &fiber.Error{
Code: fiber.StatusUnauthorized,
Message: "Username or password is invalid",
}
}
sessionId, err := repo.CreateUserSession(user)
if err != nil {
return utils.ResponseError(c, err, 500)
}
return c.JSON(fiber.Map{
"user": user,
"sessionId": sessionId,
})
}
func getUser(c *fiber.Ctx) error {
auth := c.Get("Authorization")
var sessionId string
if auth != "" {
sessionId = strings.Split(auth, " ")[1]
}
repo := NewAuthRepository()
session, err := repo.GetSession(sessionId)
if err != nil {
return utils.ResponseError(c, err, 500)
}
return c.JSON(session)
}
func logout(c *fiber.Ctx) error {
auth := c.Get("Authorization")
force := c.Query("force")
var sessionId string
if auth != "" {
sessionId = strings.Split(auth, " ")[1]
}
repo := NewAuthRepository()
err := repo.RemoveUserSession(sessionId, force == "true")
if err != nil {
return utils.ResponseError(c, err, 500)
}
return c.JSON(fiber.Map{
"status": "ok",
"message": "Successfully logged out",
})
}

View File

@ -0,0 +1,6 @@
package auth
type LoginSchema struct {
Username string `json:"username"`
Password string `json:"password"`
}

View File

@ -0,0 +1,25 @@
package app
import (
"errors"
"github.com/gofiber/fiber/v2"
)
func ErrorHandler(ctx *fiber.Ctx, err error) error {
// Status code defaults to 500
code := fiber.StatusInternalServerError
// Retrieve the custom status code if it's a *fiber.Error
var e *fiber.Error
if errors.As(err, &e) {
code = e.Code
}
// Return from handler
return ctx.Status(code).JSON(fiber.Map{
"status": "error",
"code": code,
"message": err.Error(),
})
}

View File

@ -0,0 +1,54 @@
package hosts
import (
"gorm.io/gorm"
"rul.sh/vaulterm/db"
"rul.sh/vaulterm/models"
)
type Hosts struct{ db *gorm.DB }
func NewHostsRepository() *Hosts {
return &Hosts{db: db.Get()}
}
func (r *Hosts) GetAll() ([]*models.Host, error) {
var rows []*models.Host
ret := r.db.Order("created_at DESC").Find(&rows)
return rows, ret.Error
}
type GetHostResult struct {
Host *models.Host
Key map[string]interface{}
AltKey map[string]interface{}
}
func (r *Hosts) Get(id string) (*GetHostResult, error) {
var host models.Host
ret := r.db.Joins("Key").Joins("AltKey").Where("hosts.id = ?", id).First(&host)
if ret.Error != nil {
return nil, ret.Error
}
res := &GetHostResult{Host: &host}
if host.Key.Data != "" {
if err := host.Key.DecryptData(&res.Key); err != nil {
return nil, err
}
}
if host.AltKey.Data != "" {
if err := host.AltKey.DecryptData(&res.AltKey); err != nil {
return nil, err
}
}
return res, ret.Error
}
func (r *Hosts) Create(item *models.Host) error {
return r.db.Create(item).Error
}

View File

@ -0,0 +1,53 @@
package hosts
import (
"net/http"
"github.com/gofiber/fiber/v2"
"rul.sh/vaulterm/models"
"rul.sh/vaulterm/utils"
)
func Router(app *fiber.App) {
router := app.Group("/hosts")
router.Get("/", getAll)
router.Post("/", create)
}
func getAll(c *fiber.Ctx) error {
repo := NewHostsRepository()
rows, err := repo.GetAll()
if err != nil {
return utils.ResponseError(c, err, 500)
}
return c.JSON(fiber.Map{
"rows": rows,
})
}
func create(c *fiber.Ctx) error {
var body CreateHostSchema
if err := c.BodyParser(&body); err != nil {
return utils.ResponseError(c, err, 500)
}
repo := NewHostsRepository()
item := &models.Host{
Type: body.Type,
Label: body.Label,
Host: body.Host,
Port: body.Port,
Metadata: body.Metadata,
ParentID: body.ParentID,
KeyID: body.KeyID,
AltKeyID: body.AltKeyID,
}
if err := repo.Create(item); err != nil {
return utils.ResponseError(c, err, 500)
}
return c.Status(http.StatusCreated).JSON(item)
}

View File

@ -0,0 +1,15 @@
package hosts
import "gorm.io/datatypes"
type CreateHostSchema struct {
Type string `json:"type"`
Label string `json:"label"`
Host string `json:"host"`
Port int `json:"port"`
Metadata datatypes.JSONMap `json:"metadata"`
ParentID *string `json:"parentId"`
KeyID *string `json:"keyId"`
AltKeyID *string `json:"altKeyId"`
}

View File

@ -0,0 +1,24 @@
package keychains
import (
"gorm.io/gorm"
"rul.sh/vaulterm/db"
"rul.sh/vaulterm/models"
)
type Keychains struct{ db *gorm.DB }
func NewKeychainsRepository() *Keychains {
return &Keychains{db: db.Get()}
}
func (r *Keychains) GetAll() ([]*models.Keychain, error) {
var rows []*models.Keychain
ret := r.db.Order("created_at DESC").Find(&rows)
return rows, ret.Error
}
func (r *Keychains) Create(item *models.Keychain) error {
return r.db.Create(item).Error
}

View File

@ -0,0 +1,52 @@
package keychains
import (
"net/http"
"github.com/gofiber/fiber/v2"
"rul.sh/vaulterm/models"
"rul.sh/vaulterm/utils"
)
func Router(app *fiber.App) {
router := app.Group("/keychains")
router.Get("/", getAll)
router.Post("/", create)
}
func getAll(c *fiber.Ctx) error {
repo := NewKeychainsRepository()
rows, err := repo.GetAll()
if err != nil {
return utils.ResponseError(c, err, 500)
}
return c.JSON(fiber.Map{
"rows": rows,
})
}
func create(c *fiber.Ctx) error {
var body CreateKeychainSchema
if err := c.BodyParser(&body); err != nil {
return utils.ResponseError(c, err, 500)
}
repo := NewKeychainsRepository()
item := &models.Keychain{
Type: body.Type,
Label: body.Label,
}
if err := item.EncryptData(body.Data); err != nil {
return utils.ResponseError(c, err, 500)
}
if err := repo.Create(item); err != nil {
return utils.ResponseError(c, err, 500)
}
return c.Status(http.StatusCreated).JSON(item)
}

View File

@ -0,0 +1,7 @@
package keychains
type CreateKeychainSchema struct {
Type string `json:"type"`
Label string `json:"label"`
Data interface{} `json:"data"`
}

19
server/app/ws/router.go Normal file
View File

@ -0,0 +1,19 @@
package ws
import (
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
)
func Router(app *fiber.App) {
router := app.Group("/ws")
router.Use(func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
return c.Next()
}
return fiber.ErrUpgradeRequired
})
router.Get("/term", websocket.New(HandleTerm))
}

115
server/app/ws/term.go Normal file
View File

@ -0,0 +1,115 @@
package ws
import (
"github.com/gofiber/contrib/websocket"
"rul.sh/vaulterm/app/hosts"
"rul.sh/vaulterm/lib"
"rul.sh/vaulterm/utils"
)
func HandleTerm(c *websocket.Conn) {
hostId := c.Query("hostId")
hostRepo := hosts.NewHostsRepository()
data, _ := hostRepo.Get(hostId)
if data == nil {
c.WriteMessage(websocket.TextMessage, []byte("Host not found"))
return
}
switch data.Host.Type {
case "ssh":
sshHandler(c, data)
case "pve":
pveHandler(c, data)
case "incus":
incusHandler(c, data)
default:
c.WriteMessage(websocket.TextMessage, []byte("Invalid host type"))
}
}
func sshHandler(c *websocket.Conn, data *hosts.GetHostResult) {
username, _ := data.Key["username"].(string)
password, _ := data.Key["password"].(string)
cfg := &SSHConfig{
HostName: data.Host.Host,
Port: data.Host.Port,
User: username,
Password: password,
}
if err := NewSSHWebsocketSession(c, cfg); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}
func pveHandler(c *websocket.Conn, data *hosts.GetHostResult) {
client := c.Query("client")
username, _ := data.Key["username"].(string)
password, _ := data.Key["password"].(string)
pve := &lib.PVEServer{
HostName: data.Host.Host,
Port: data.Host.Port,
Username: username,
Password: password,
}
var i *lib.PVEInstance
if err := utils.ParseMapInterface(data.Host.Metadata, &i); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
if i == nil || i.Type == "" || i.Node == "" || i.VMID == "" {
c.WriteMessage(websocket.TextMessage, []byte("Invalid pve instance metadata"))
return
}
var err error
if client == "vnc" {
err = NewVNCSession(c, pve, i)
} else {
err = NewTerminalSession(c, pve, i)
}
if err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}
func incusHandler(c *websocket.Conn, data *hosts.GetHostResult) {
shell := c.Query("shell")
cert, _ := data.Key["cert"].(string)
key, _ := data.Key["key"].(string)
if cert == "" || key == "" {
c.WriteMessage(websocket.TextMessage, []byte("Missing certificate or key"))
return
}
incus := &lib.IncusServer{
HostName: data.Host.Host,
Port: data.Host.Port,
ClientCert: cert,
ClientKey: key,
}
session := &IncusWebsocketSession{}
if err := utils.ParseMapInterface(data.Host.Metadata, session); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
if shell != "" {
session.Shell = shell
}
if err := session.NewTerminal(c, incus); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}

View File

@ -1,4 +1,4 @@
package lib package ws
import ( import (
"crypto/tls" "crypto/tls"
@ -9,10 +9,20 @@ import (
fastWs "github.com/fasthttp/websocket" fastWs "github.com/fasthttp/websocket"
"github.com/gofiber/contrib/websocket" "github.com/gofiber/contrib/websocket"
"rul.sh/vaulterm/lib"
) )
func NewIncusWebsocketSession(c *websocket.Conn, incus *IncusServer) error { type IncusWebsocketSession struct {
exec, err := incus.InstanceExec("test", []string{"/bin/sh"}, true) Instance string `json:"instance"`
Shell string `json:"shell"`
}
func (i *IncusWebsocketSession) NewTerminal(c *websocket.Conn, incus *lib.IncusServer) error {
if i.Shell == "" {
i.Shell = "/bin/sh"
}
exec, err := incus.InstanceExec(i.Instance, []string{i.Shell}, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,4 +1,4 @@
package lib package ws
import ( import (
"crypto/tls" "crypto/tls"
@ -11,27 +11,21 @@ import (
fastWs "github.com/fasthttp/websocket" fastWs "github.com/fasthttp/websocket"
"github.com/gofiber/contrib/websocket" "github.com/gofiber/contrib/websocket"
"rul.sh/vaulterm/lib"
) )
type PVEConfig struct {
HostName string
User string
Password string
Port int
PrivateKey string
PrivateKeyPassphrase string
}
// https://github.com/proxmox/pve-xtermjs/blob/master/README // https://github.com/proxmox/pve-xtermjs/blob/master/README
func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, instance *PVEInstance) error { func NewTerminalSession(c *websocket.Conn, pve *lib.PVEServer, instance *lib.PVEInstance) error {
access, err := pve.GetAccessTicket() access, err := pve.GetAccessTicket()
if err != nil { if err != nil {
log.Println("Error getting access ticket:", err)
return err return err
} }
ticket, err := pve.GetVNCTicket(access, instance, false) ticket, err := pve.GetVNCTicket(access, instance, false)
if err != nil { if err != nil {
log.Println("Error getting vnc ticket:", err)
return err return err
} }
@ -103,14 +97,16 @@ func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, instance *PVEInstanc
return nil return nil
} }
func (pve *PVEServer) NewVNCSession(c *websocket.Conn, instance *PVEInstance) error { func NewVNCSession(c *websocket.Conn, pve *lib.PVEServer, instance *lib.PVEInstance) error {
access, err := pve.GetAccessTicket() access, err := pve.GetAccessTicket()
if err != nil { if err != nil {
log.Println("Error getting access ticket:", err)
return err return err
} }
ticket, err := pve.GetVNCTicket(access, instance, true) ticket, err := pve.GetVNCTicket(access, instance, true)
if err != nil { if err != nil {
log.Println("Error getting vnc ticket:", err)
return err return err
} }

View File

@ -1,4 +1,4 @@
package lib package ws
import ( import (
"fmt" "fmt"

60
server/db/database.go Normal file
View File

@ -0,0 +1,60 @@
package db
import (
"log"
"os"
"strings"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var dbInstance *gorm.DB
func Get() *gorm.DB {
if dbInstance == nil {
log.Fatal("database not initialized")
}
return dbInstance
}
func Init() {
// log.Println("Initializing database...")
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
dsn = "file:data.db?cache=shared&mode=rwc&_journal_mode=WAL"
}
// Open db connection
var con gorm.Dialector
if strings.HasPrefix(dsn, "postgres:") {
con = postgres.Open(dsn)
} else {
con = sqlite.Open(dsn)
}
db, err := gorm.Open(con, &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
log.Fatal(err)
}
dbInstance = db
// Migrate the schema
db.AutoMigrate(Models...)
runSeeders(db)
}
func Close() error {
con, err := dbInstance.DB()
if err != nil {
return err
}
if err := con.Close(); err != nil {
return err
}
return nil
}

10
server/db/models.go Normal file
View File

@ -0,0 +1,10 @@
package db
import "rul.sh/vaulterm/models"
var Models = []interface{}{
&models.User{},
&models.UserSession{},
&models.Keychain{},
&models.Host{},
}

63
server/db/seeders.go Normal file
View File

@ -0,0 +1,63 @@
package db
import (
"gorm.io/gorm"
"rul.sh/vaulterm/lib"
"rul.sh/vaulterm/models"
)
type SeedFn func(*gorm.DB) error
var seeders = []SeedFn{
seedUsers,
}
func seedUsers(tx *gorm.DB) error {
var userCount int64
if res := tx.Model(&models.User{}).Count(&userCount); res.Error != nil {
return res.Error
}
// skip seeder if users already exist
if userCount > 0 {
return nil
}
testPasswd, err := lib.HashPassword("123456")
if err != nil {
return err
}
userList := []models.User{
{
Name: "Admin",
Username: "admin",
Password: testPasswd,
Email: "admin@mail.com",
Role: models.UserRoleAdmin,
},
{
Name: "John Doe",
Username: "user",
Password: testPasswd,
Email: "user@mail.com",
},
}
if res := tx.Create(&userList); res.Error != nil {
return res.Error
}
return nil
}
func runSeeders(db *gorm.DB) {
db.Transaction(func(tx *gorm.DB) error {
for _, seed := range seeders {
if err := seed(db); err != nil {
return err
}
}
return nil
})
}

View File

@ -1,12 +1,19 @@
module rul.sh/vaulterm module rul.sh/vaulterm
go 1.21.1 go 1.22
toolchain go1.23.0
require ( require (
github.com/gofiber/contrib/websocket v1.3.2 github.com/gofiber/contrib/websocket v1.3.2
github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/fiber/v2 v2.52.5
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/oklog/ulid/v2 v2.1.0
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.28.0 golang.org/x/crypto v0.28.0
gorm.io/driver/postgres v1.5.9
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.12
) )
require ( require (
@ -25,4 +32,24 @@ require (
golang.org/x/net v0.26.0 // indirect golang.org/x/net v0.26.0 // indirect
) )
require golang.org/x/sys v0.26.0 // indirect require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/datatypes v1.2.4 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)

View File

@ -1,19 +1,44 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/websocket v1.5.10 h1:bc7NIGyrg1L6sd5pRzCIbXpro54SZLEluZCu0rOpcN4= github.com/fasthttp/websocket v1.5.10 h1:bc7NIGyrg1L6sd5pRzCIbXpro54SZLEluZCu0rOpcN4=
github.com/fasthttp/websocket v1.5.10/go.mod h1:BwHeuXGWzCW1/BIKUKD3+qfCl+cTdsHu/f243NcAI/Q= github.com/fasthttp/websocket v1.5.10/go.mod h1:BwHeuXGWzCW1/BIKUKD3+qfCl+cTdsHu/f243NcAI/Q=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gofiber/contrib/websocket v1.3.2 h1:AUq5PYeKwK50s0nQrnluuINYeep1c4nRCJ0NWsV3cvg= github.com/gofiber/contrib/websocket v1.3.2 h1:AUq5PYeKwK50s0nQrnluuINYeep1c4nRCJ0NWsV3cvg=
github.com/gofiber/contrib/websocket v1.3.2/go.mod h1:07u6QGMsvX+sx7iGNCl5xhzuUVArWwLQ3tBIH24i+S8= github.com/gofiber/contrib/websocket v1.3.2/go.mod h1:07u6QGMsvX+sx7iGNCl5xhzuUVArWwLQ3tBIH24i+S8=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -21,12 +46,22 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@ -39,11 +74,30 @@ golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.4 h1:uZmGAcK/QZ0uyfCuVg0VQY1ZmV9h1fuG0tMwKByO1z4=
gorm.io/datatypes v1.2.4/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@ -1,10 +1,19 @@
package lib package lib
import ( import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io"
"os"
"golang.org/x/crypto/bcrypt"
) )
func LoadClientCertificate(clientCert string, clientKey string) (*tls.Certificate, error) { func LoadClientCertificate(clientCert string, clientKey string) (*tls.Certificate, error) {
@ -35,3 +44,93 @@ func LoadClientCertificate(clientCert string, clientKey string) (*tls.Certificat
PrivateKey: key, PrivateKey: key,
}, nil }, nil
} }
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
return string(bytes), err
}
func VerifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func GenerateSessionID(size int) (string, error) {
sessionID := make([]byte, size)
// Read random bytes into sessionID
_, err := rand.Read(sessionID)
if err != nil {
return "", err
}
// Encode as hex string
return hex.EncodeToString(sessionID), nil
}
func Encrypt(data string) (string, error) {
key := os.Getenv("ENCRYPTION_KEY")
if key == "" {
return "", fmt.Errorf("ENCRYPTION_KEY is not set")
}
keyDec, err := hex.DecodeString(key)
if err != nil {
return "", err
}
block, err := aes.NewCipher(keyDec)
if err != nil {
return "", err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, aesGCM.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := aesGCM.Seal(nonce, nonce, []byte(data), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func Decrypt(encrypted string) (string, error) {
key := os.Getenv("ENCRYPTION_KEY")
if key == "" {
return "", fmt.Errorf("ENCRYPTION_KEY is not set")
}
keyDec, err := hex.DecodeString(key)
if err != nil {
return "", err
}
data, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
return "", err
}
block, err := aes.NewCipher(keyDec)
if err != nil {
return "", err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := aesGCM.NonceSize()
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
res, err := aesGCM.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(res), nil
}

View File

@ -82,6 +82,9 @@ func (i *IncusServer) InstanceExec(instance string, command []string, interactiv
"command": command, "command": command,
"interactive": interactive, "interactive": interactive,
"wait-for-websocket": true, "wait-for-websocket": true,
"environment": map[string]string{
"TERM": "xterm-256color",
},
}, },
}) })
if err != nil { if err != nil {

View File

@ -70,6 +70,7 @@ type PVEAccessTicket struct {
func (pve *PVEServer) GetAccessTicket() (*PVEAccessTicket, error) { func (pve *PVEServer) GetAccessTicket() (*PVEAccessTicket, error) {
url := fmt.Sprintf("https://%s:%d/api2/json/access/ticket", pve.HostName, pve.Port) url := fmt.Sprintf("https://%s:%d/api2/json/access/ticket", pve.HostName, pve.Port)
// note for myself: don't forget the realm
body, err := fetch("POST", url, &PVERequestInit{Body: map[string]string{ body, err := fetch("POST", url, &PVERequestInit{Body: map[string]string{
"username": pve.Username, "username": pve.Username,
"password": pve.Password, "password": pve.Password,
@ -89,9 +90,9 @@ func (pve *PVEServer) GetAccessTicket() (*PVEAccessTicket, error) {
} }
type PVEInstance struct { type PVEInstance struct {
Type string // "qemu" | "lxc" Type string `json:"type"` // "qemu" | "lxc"
Node string Node string `json:"node"`
VMID string VMID string `json:"vmid"`
} }
type PVEVNCTicketData struct { type PVEVNCTicketData struct {

View File

@ -3,91 +3,16 @@ package main
import ( import (
"os" "os"
"github.com/gofiber/contrib/websocket" "rul.sh/vaulterm/app"
"github.com/gofiber/fiber/v2"
"github.com/joho/godotenv"
"rul.sh/vaulterm/lib"
) )
func main() { func main() {
godotenv.Load() app := app.NewApp()
app := fiber.New()
var pve = &lib.PVEServer{ port := os.Getenv("PORT")
HostName: "10.0.0.1", if port == "" {
Port: 8006, port = "3000"
Username: os.Getenv("PVE_USERNAME"),
Password: os.Getenv("PVE_PASSWORD"),
} }
app.Get("/", func(c *fiber.Ctx) error { app.Listen(":" + port)
return c.SendString("Hello, World!")
})
app.Use("/ws", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
return c.Next()
}
return fiber.ErrUpgradeRequired
})
app.Get("/ws/ssh", websocket.New(func(c *websocket.Conn) {
cfg := &lib.SSHConfig{
HostName: "10.0.0.102",
User: "root",
Password: "ausya2",
}
if err := lib.NewSSHWebsocketSession(c, cfg); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}))
app.Get("/ws/pve", websocket.New(func(c *websocket.Conn) {
client := c.Query("client")
serverId := c.Query("serverId")
var node *lib.PVEInstance
switch serverId {
case "2":
node = &lib.PVEInstance{
Type: "qemu",
Node: "pve",
VMID: "105",
}
case "3":
node = &lib.PVEInstance{
Type: "lxc",
Node: "pve",
VMID: "102",
}
}
var err error
if client == "vnc" {
err = pve.NewVNCSession(c, node)
} else {
err = pve.NewTerminalSession(c, node)
}
if err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}))
app.Get("/ws/incus", websocket.New(func(c *websocket.Conn) {
incus := &lib.IncusServer{
HostName: "100.64.0.3",
Port: 8443,
ClientCert: "",
ClientKey: "",
}
if err := lib.NewIncusWebsocketSession(c, incus); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}))
app.Listen(":3000")
} }

View File

@ -0,0 +1,31 @@
package models
import (
"strings"
"time"
"github.com/oklog/ulid/v2"
"gorm.io/gorm"
)
type BaseModel struct {
ID string `gorm:"primarykey;type:varchar(26)" json:"id"`
}
func (m *BaseModel) BeforeCreate(tx *gorm.DB) error {
m.ID = m.GenerateID()
return nil
}
func (m *BaseModel) GenerateID() string {
return strings.ToLower(ulid.Make().String())
}
type Timestamps struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type SoftDeletes struct {
DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"`
}

32
server/models/host.go Normal file
View File

@ -0,0 +1,32 @@
package models
import "gorm.io/datatypes"
const (
HostTypeSSH = "ssh"
HostTypePVE = "pve"
HostTypePVENode = "pve_node"
HostTypePVEHost = "pve_host"
HostTypeIncus = "incus"
HostTypeIncusHost = "incus_host"
)
type Host struct {
BaseModel
Type string `json:"type" gorm:"not null;index:hosts_type_idx;type:varchar(16)"`
Label string `json:"label"`
Host string `json:"host" gorm:"type:varchar(64)"`
Port int `json:"port" gorm:"type:smallint"`
Metadata datatypes.JSONMap `json:"metadata"`
ParentID *string `json:"parentId" gorm:"index:hosts_parent_id_idx;type:varchar(26)"`
Parent *Host `json:"parent" gorm:"foreignKey:ParentID"`
KeyID *string `json:"keyId" gorm:"index:hosts_key_id_idx"`
Key Keychain `gorm:"foreignKey:KeyID"`
AltKeyID *string `json:"altKeyId" gorm:"index:hosts_altkey_id_idx"`
AltKey Keychain `gorm:"foreignKey:AltKeyID"`
Timestamps
SoftDeletes
}

53
server/models/keychain.go Normal file
View File

@ -0,0 +1,53 @@
package models
import (
"encoding/json"
"rul.sh/vaulterm/lib"
)
const (
KeychainTypeUserPass = "user"
KeychainTypeRSA = "rsa"
KeychainTypeCertificate = "cert"
)
type Keychain struct {
BaseModel
Label string `json:"label"`
Type string `json:"type" gorm:"not null;index:keychains_type_idx;type:varchar(12)"`
Data string `json:"-" gorm:"type:text"`
Timestamps
SoftDeletes
}
func (k *Keychain) EncryptData(data interface{}) error {
// Encrypt data
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
enc, err := lib.Encrypt(string(jsonData))
if err == nil {
k.Data = enc
}
return err
}
func (k *Keychain) DecryptData(data interface{}) error {
// Decrypt stored data
dec, err := lib.Decrypt(k.Data)
if err != nil {
return err
}
err = json.Unmarshal([]byte(dec), &data)
if err != nil {
return err
}
return nil
}

28
server/models/user.go Normal file
View File

@ -0,0 +1,28 @@
package models
const (
UserRoleUser = "user"
UserRoleAdmin = "admin"
)
type User struct {
BaseModel
Name string `json:"name"`
Username string `json:"username" gorm:"unique"`
Password string `json:"-"`
Email string `json:"email" gorm:"unique"`
Role string `json:"role" gorm:"default:user;not null;index:users_role_idx;type:varchar(8)"`
Timestamps
SoftDeletes
}
type UserSession struct {
ID string `json:"id" gorm:"primarykey;type:varchar(40)"`
UserID string `json:"userId" gorm:"type:varchar(26)"`
User User `json:"user"`
Timestamps
SoftDeletes
}

17
server/tests/app_test.go Normal file
View File

@ -0,0 +1,17 @@
package tests
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHealthCheck(t *testing.T) {
test := NewTest(t)
res, status, err := test.Fetch("GET", "/health-check", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, "OK", res["data"])
}

37
server/tests/auth_test.go Normal file
View File

@ -0,0 +1,37 @@
package tests
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAuthLogin(t *testing.T) {
test := NewTest(t)
sessionId := test.WithAuth()
assert.NotEmpty(t, sessionId)
}
func TestAuthGetUser(t *testing.T) {
test := NewTestWithAuth(t)
res, status, err := test.Fetch("GET", "/auth/user", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
assert.NotNil(t, res["user"])
user := res["user"].(map[string]interface{})
assert.NotEmpty(t, user["id"])
}
func TestAuthLogout(t *testing.T) {
test := NewTestWithAuth(t)
_, status, err := test.Fetch("POST", "/auth/logout", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
test.SessionID = ""
}

View File

@ -0,0 +1,74 @@
package tests
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHostsGetAll(t *testing.T) {
test := NewTestWithAuth(t)
res, status, err := test.Fetch("GET", "/hosts", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
assert.NotNil(t, res["rows"])
}
func TestHostsCreate(t *testing.T) {
test := NewTestWithAuth(t)
data := map[string]interface{}{
"type": "pve",
"label": "test ssh",
"host": "10.0.0.102",
"port": 22,
"keyId": "01jc3wkctzqrcz8qhwynr4p9pe",
}
// data := map[string]interface{}{
// "type": "pve",
// "label": "test pve qemu",
// "host": "10.0.0.1",
// "port": 8006,
// "keyId": "01jc3wkctzqrcz8qhwynr4p9pe",
// "metadata": map[string]interface{}{
// "node": "pve",
// "type": "qemu",
// "vmid": "105",
// },
// }
// data := map[string]interface{}{
// "type": "pve",
// "label": "test pve lxc",
// "host": "10.0.0.1",
// "port": 8006,
// "keyId": "01jc3xcn5qgybbpfppy9pe14ae",
// "metadata": map[string]interface{}{
// "node": "pve",
// "type": "lxc",
// "vmid": "102",
// },
// }
// data := map[string]interface{}{
// "type": "incus",
// "label": "test incus",
// "host": "100.64.0.3",
// "port": 8443,
// "keyId": "01jc3xjcm6ddt4zc0x7g69nv9q",
// "metadata": map[string]interface{}{
// "instance": "test",
// "shell": "/bin/sh",
// },
// }
res, status, err := test.Fetch("POST", "/hosts", &FetchOptions{Body: data})
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, status)
assert.NotNil(t, res["id"])
}

View File

@ -0,0 +1,55 @@
package tests
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestKeychainsGetAll(t *testing.T) {
test := NewTestWithAuth(t)
res, status, err := test.Fetch("GET", "/keychains", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, status)
assert.NotNil(t, res["rows"])
}
func TestKeychainsCreate(t *testing.T) {
test := NewTestWithAuth(t)
data := map[string]interface{}{
"type": "user",
"label": "SSH Key",
"data": map[string]interface{}{
"username": "",
"password": "",
},
}
// data := map[string]interface{}{
// "type": "user",
// "label": "PVE Key",
// "data": map[string]interface{}{
// "username": "root@pam",
// "password": "",
// },
// }
// data := map[string]interface{}{
// "type": "cert",
// "label": "Certificate Key",
// "data": map[string]interface{}{
// "cert": "",
// "key": "",
// },
// }
res, status, err := test.Fetch("POST", "/keychains", &FetchOptions{Body: data})
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, status)
assert.NotNil(t, res["id"])
}

View File

@ -0,0 +1,24 @@
package tests
import (
"log"
"os"
"testing"
"rul.sh/vaulterm/db"
)
func TestMain(m *testing.M) {
log.Println("Starting tests...")
test := NewTest(nil)
// Run all tests
code := m.Run()
log.Println("Cleaning up...")
// Clean up
test.Close()
db.Close()
os.Exit(code)
}

156
server/tests/utils.go Normal file
View File

@ -0,0 +1,156 @@
package tests
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"path"
"runtime"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"rul.sh/vaulterm/app"
)
type HTTPTest struct {
t *testing.T
app *fiber.App
SessionID string
}
var instance *HTTPTest
func NewTest(t *testing.T) *HTTPTest {
if instance != nil {
return instance
}
instance = &HTTPTest{
t: t,
app: app.NewApp(),
}
return instance
}
func NewTestWithAuth(t *testing.T) *HTTPTest {
test := NewTest(t)
test.WithAuth()
return test
}
func init() {
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
}
type FetchOptions struct {
Headers map[string]string
Body interface{}
SessionID string
}
type AuthOptions struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (h *HTTPTest) Login(options *AuthOptions) string {
body := options
if options == nil {
body = &AuthOptions{
Username: "admin",
Password: "123456",
}
}
res, status, err := h.Fetch("POST", "/auth/login", &FetchOptions{
Body: body,
})
if h.t != nil {
assert.NoError(h.t, err)
assert.Equal(h.t, http.StatusOK, status)
assert.NotNil(h.t, res["user"])
assert.NotEmpty(h.t, res["sessionId"])
}
return res["sessionId"].(string)
}
func (h *HTTPTest) WithAuth() string {
if h.SessionID != "" {
return h.SessionID
}
sessionId := h.Login(nil)
h.SessionID = sessionId
return sessionId
}
func (h *HTTPTest) Close() {
if h.SessionID != "" {
h.Fetch("POST", "/auth/logout?force=true", nil)
}
h.app.Shutdown()
}
func (h *HTTPTest) Fetch(method string, path string, options *FetchOptions) (map[string]interface{}, int, error) {
var payload io.Reader
headers := map[string]string{}
if options != nil && options.Headers != nil {
headers = options.Headers
}
if options != nil && options.Body != nil {
json, _ := json.Marshal(options.Body)
payload = bytes.NewBuffer(json)
headers["Content-Type"] = "application/json"
}
sessionId := h.SessionID
if options != nil && options.SessionID != "" {
sessionId = options.SessionID
}
if sessionId != "" {
headers["Authorization"] = "Bearer " + sessionId
}
req := httptest.NewRequest(method, path, payload)
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := h.app.Test(req, -1)
if err != nil {
return nil, resp.StatusCode, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, err
}
contentType := resp.Header.Get("Content-Type")
if contentType == "application/json" {
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return nil, resp.StatusCode, err
}
return data, resp.StatusCode, nil
}
data := map[string]interface{}{
"data": string(body),
}
return data, resp.StatusCode, err
}

14
server/utils/http.go Normal file
View File

@ -0,0 +1,14 @@
package utils
import "github.com/gofiber/fiber/v2"
func ResponseError(c *fiber.Ctx, err error, status int) error {
if status == 0 {
status = fiber.StatusInternalServerError
}
return &fiber.Error{
Code: status,
Message: err.Error(),
}
}

15
server/utils/parser.go Normal file
View File

@ -0,0 +1,15 @@
package utils
import "encoding/json"
func ParseMapInterface(data interface{}, out interface{}) error {
// Marshal the map to JSON
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
// Unmarshal the JSON data into the struct
err = json.Unmarshal(jsonData, &out)
return err
}