mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 16:49:39 +07:00
feat: init api, db, app
This commit is contained in:
parent
ab9b3368d1
commit
11b063c2fa
@ -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);
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -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
1
server/.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
tmp/
|
tmp/
|
||||||
.env
|
.env
|
||||||
|
/data.db*
|
||||||
|
15
server/Makefile
Normal file
15
server/Makefile
Normal 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
37
server/app/app.go
Normal 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
|
||||||
|
}
|
50
server/app/auth/repository.go
Normal file
50
server/app/auth/repository.go
Normal 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
93
server/app/auth/router.go
Normal 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",
|
||||||
|
})
|
||||||
|
}
|
6
server/app/auth/schema.go
Normal file
6
server/app/auth/schema.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
type LoginSchema struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
25
server/app/error_handler.go
Normal file
25
server/app/error_handler.go
Normal 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(),
|
||||||
|
})
|
||||||
|
}
|
54
server/app/hosts/repository.go
Normal file
54
server/app/hosts/repository.go
Normal 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
|
||||||
|
}
|
53
server/app/hosts/router.go
Normal file
53
server/app/hosts/router.go
Normal 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)
|
||||||
|
}
|
15
server/app/hosts/schema.go
Normal file
15
server/app/hosts/schema.go
Normal 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"`
|
||||||
|
}
|
24
server/app/keychains/repository.go
Normal file
24
server/app/keychains/repository.go
Normal 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
|
||||||
|
}
|
52
server/app/keychains/router.go
Normal file
52
server/app/keychains/router.go
Normal 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)
|
||||||
|
}
|
7
server/app/keychains/schema.go
Normal file
7
server/app/keychains/schema.go
Normal 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
19
server/app/ws/router.go
Normal 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
115
server/app/ws/term.go
Normal 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()))
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package lib
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
60
server/db/database.go
Normal file
60
server/db/database.go
Normal 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
10
server/db/models.go
Normal 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
63
server/db/seeders.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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=
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
|
||||||
}
|
}
|
||||||
|
31
server/models/base_model.go
Normal file
31
server/models/base_model.go
Normal 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
32
server/models/host.go
Normal 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
53
server/models/keychain.go
Normal 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
28
server/models/user.go
Normal 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
17
server/tests/app_test.go
Normal 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
37
server/tests/auth_test.go
Normal 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 = ""
|
||||||
|
}
|
74
server/tests/hosts_test.go
Normal file
74
server/tests/hosts_test.go
Normal 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"])
|
||||||
|
}
|
55
server/tests/keychains_test.go
Normal file
55
server/tests/keychains_test.go
Normal 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"])
|
||||||
|
}
|
24
server/tests/setup_test.go
Normal file
24
server/tests/setup_test.go
Normal 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
156
server/tests/utils.go
Normal 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
14
server/utils/http.go
Normal 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
15
server/utils/parser.go
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user