feature: Change state management from global variables to object passing
This restructures dbcore (now the db package) and jwtcore (now the jwt package) to use a single struct. There is now a state package, which contains a struct with the full application state. After this, instead of initializing the API routes directly in the main function, the state object gets passed, and the API routes get initialized with their accompanying code. One fix done to reduce memory usage and increase speed is that the validator object is now persistent across requests, instead of recreating it each time. This should speed things up slightly, and improve memory usage. One additional chore done is that the database models have been moved to be a seperate file from the DB initialization itself.
This commit is contained in:
parent
71d53990de
commit
d56a8eb7bf
23 changed files with 1901 additions and 2161 deletions
|
@ -1,298 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/gzip"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Data structures
|
|
||||||
type BackupBackend struct {
|
|
||||||
ID uint `json:"id" validate:"required"`
|
|
||||||
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
Backend string `json:"backend" validate:"required"`
|
|
||||||
BackendParameters string `json:"connectionDetails" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackupProxy struct {
|
|
||||||
ID uint `json:"id" validate:"required"`
|
|
||||||
BackendID uint `json:"destProviderID" validate:"required"`
|
|
||||||
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
Protocol string `json:"protocol" validate:"required"`
|
|
||||||
SourceIP string `json:"sourceIP" validate:"required"`
|
|
||||||
SourcePort uint16 `json:"sourcePort" validate:"required"`
|
|
||||||
DestinationPort uint16 `json:"destPort" validate:"required"`
|
|
||||||
AutoStart bool `json:"enabled" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackupPermission struct {
|
|
||||||
ID uint `json:"id" validate:"required"`
|
|
||||||
|
|
||||||
PermissionNode string `json:"permission" validate:"required"`
|
|
||||||
HasPermission bool `json:"has" validate:"required"`
|
|
||||||
UserID uint `json:"userID" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackupUser struct {
|
|
||||||
ID uint `json:"id" validate:"required"`
|
|
||||||
|
|
||||||
Email string `json:"email" validate:"required"`
|
|
||||||
Username *string `json:"username"`
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
Password string `json:"password" validate:"required"`
|
|
||||||
IsBot *bool `json:"isRootServiceAccount"`
|
|
||||||
|
|
||||||
Token *string `json:"rootToken" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackupData struct {
|
|
||||||
Backends []*BackupBackend `json:"destinationProviders" validate:"required"`
|
|
||||||
Proxies []*BackupProxy `json:"forwardRules" validate:"required"`
|
|
||||||
Permissions []*BackupPermission `json:"allPermissions" validate:"required"`
|
|
||||||
Users []*BackupUser `json:"users" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// From https://stackoverflow.com/questions/54461423/efficient-way-to-remove-all-non-alphanumeric-characters-from-large-text
|
|
||||||
// Strips all alphanumeric characters from a string
|
|
||||||
func stripAllAlphanumeric(s string) string {
|
|
||||||
var result strings.Builder
|
|
||||||
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
b := s[i]
|
|
||||||
if ('a' <= b && b <= 'z') ||
|
|
||||||
('A' <= b && b <= 'Z') ||
|
|
||||||
('0' <= b && b <= '9') {
|
|
||||||
result.WriteByte(b)
|
|
||||||
} else {
|
|
||||||
result.WriteByte('_')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func backupRestoreEntrypoint(cCtx *cli.Context) error {
|
|
||||||
log.Info("Decompressing backup...")
|
|
||||||
|
|
||||||
backupFile, err := os.Open(cCtx.String("backup-path"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open backup: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := gzip.NewReader(backupFile)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize Gzip (compression) reader: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
backupDataBytes, err := io.ReadAll(reader)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read backup contents: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Decompressed backup. Cleaning up...")
|
|
||||||
|
|
||||||
err = reader.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to close Gzip reader: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = backupFile.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to close backup: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Parsing backup into internal structures...")
|
|
||||||
|
|
||||||
backupData := &BackupData{}
|
|
||||||
|
|
||||||
err = json.Unmarshal(backupDataBytes, backupData)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse backup: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validator.New().Struct(backupData); err != nil {
|
|
||||||
return fmt.Errorf("failed to validate backup: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Initializing database and opening it...")
|
|
||||||
|
|
||||||
err = dbcore.InitializeDatabase(&gorm.Config{})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to initialize database: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Running database migrations...")
|
|
||||||
|
|
||||||
if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil {
|
|
||||||
return fmt.Errorf("Failed to run database migrations: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Restoring database...")
|
|
||||||
bestEffortOwnerUIDFromBackup := -1
|
|
||||||
|
|
||||||
log.Info("Attempting to find user to use as owner of resources...")
|
|
||||||
|
|
||||||
for _, user := range backupData.Users {
|
|
||||||
foundUser := false
|
|
||||||
failedAdministrationCheck := false
|
|
||||||
|
|
||||||
for _, permission := range backupData.Permissions {
|
|
||||||
if permission.UserID != user.ID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
foundUser = true
|
|
||||||
|
|
||||||
if !strings.HasPrefix(permission.PermissionNode, "routes.") && permission.PermissionNode != "permissions.see" && !permission.HasPermission {
|
|
||||||
log.Infof("User with email '%s' and ID of '%d' failed administration check (lacks all permissions required). Attempting to find better user", user.Email, user.ID)
|
|
||||||
failedAdministrationCheck = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundUser {
|
|
||||||
log.Warnf("User with email '%s' and ID of '%d' lacks any permissions!", user.Email, user.ID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if failedAdministrationCheck {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Using user with email '%s', and ID of '%d'", user.Email, user.ID)
|
|
||||||
bestEffortOwnerUIDFromBackup = int(user.ID)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if bestEffortOwnerUIDFromBackup == -1 {
|
|
||||||
log.Warnf("Could not find Administrative level user to use as the owner of resources. Using user with email '%s', and ID of '%d'", backupData.Users[0].Email, backupData.Users[0].ID)
|
|
||||||
bestEffortOwnerUIDFromBackup = int(backupData.Users[0].ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var bestEffortOwnerUID uint
|
|
||||||
|
|
||||||
for _, user := range backupData.Users {
|
|
||||||
log.Infof("Migrating user with email '%s' and ID of '%d'", user.Email, user.ID)
|
|
||||||
tokens := make([]dbcore.Token, 0)
|
|
||||||
permissions := make([]dbcore.Permission, 0)
|
|
||||||
|
|
||||||
if user.Token != nil {
|
|
||||||
tokens = append(tokens, dbcore.Token{
|
|
||||||
Token: *user.Token,
|
|
||||||
DisableExpiry: true,
|
|
||||||
CreationIPAddr: "127.0.0.1", // We don't know the creation IP address...
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, permission := range backupData.Permissions {
|
|
||||||
if permission.UserID != user.ID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
permissions = append(permissions, dbcore.Permission{
|
|
||||||
PermissionNode: permission.PermissionNode,
|
|
||||||
HasPermission: permission.HasPermission,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
username := ""
|
|
||||||
|
|
||||||
if user.Username == nil {
|
|
||||||
username = strings.ToLower(stripAllAlphanumeric(user.Email))
|
|
||||||
log.Warnf("User with ID of '%d' doesn't have a username. Derived username from email is '%s' (email is '%s')", user.ID, username, user.Email)
|
|
||||||
} else {
|
|
||||||
username = *user.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
userDatabase := &dbcore.User{
|
|
||||||
Email: user.Email,
|
|
||||||
Username: username,
|
|
||||||
Name: user.Name,
|
|
||||||
Password: base64.StdEncoding.EncodeToString([]byte(user.Password)),
|
|
||||||
IsBot: user.IsBot,
|
|
||||||
|
|
||||||
Tokens: tokens,
|
|
||||||
Permissions: permissions,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dbcore.DB.Create(userDatabase).Error; err != nil {
|
|
||||||
log.Errorf("Failed to create user: %s", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if uint(bestEffortOwnerUIDFromBackup) == user.ID {
|
|
||||||
bestEffortOwnerUID = userDatabase.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, backend := range backupData.Backends {
|
|
||||||
log.Infof("Migrating backend ID '%d' with name '%s'", backend.ID, backend.Name)
|
|
||||||
|
|
||||||
backendDatabase := &dbcore.Backend{
|
|
||||||
UserID: bestEffortOwnerUID,
|
|
||||||
Name: backend.Name,
|
|
||||||
Description: backend.Description,
|
|
||||||
Backend: backend.Backend,
|
|
||||||
BackendParameters: base64.StdEncoding.EncodeToString([]byte(backend.BackendParameters)),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dbcore.DB.Create(backendDatabase).Error; err != nil {
|
|
||||||
log.Errorf("Failed to create backend: %s", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Migrating proxies for backend ID '%d'", backend.ID)
|
|
||||||
|
|
||||||
for _, proxy := range backupData.Proxies {
|
|
||||||
if proxy.BackendID != backend.ID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Migrating proxy ID '%d' with name '%s'", proxy.ID, proxy.Name)
|
|
||||||
|
|
||||||
proxyDatabase := &dbcore.Proxy{
|
|
||||||
BackendID: backendDatabase.ID,
|
|
||||||
UserID: bestEffortOwnerUID,
|
|
||||||
|
|
||||||
Name: proxy.Name,
|
|
||||||
Description: proxy.Description,
|
|
||||||
Protocol: proxy.Protocol,
|
|
||||||
SourceIP: proxy.SourceIP,
|
|
||||||
SourcePort: proxy.SourcePort,
|
|
||||||
DestinationPort: proxy.DestinationPort,
|
|
||||||
AutoStart: proxy.AutoStart,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dbcore.DB.Create(proxyDatabase).Error; err != nil {
|
|
||||||
log.Errorf("Failed to create proxy: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Successfully upgraded to Hermes from NextNet.")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -7,13 +7,12 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BackendCreationRequest struct {
|
type BackendCreationRequest struct {
|
||||||
|
@ -24,7 +23,8 @@ type BackendCreationRequest struct {
|
||||||
BackendParameters interface{} `json:"connectionDetails" validate:"required"`
|
BackendParameters interface{} `json:"connectionDetails" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateBackend(c *gin.Context) {
|
func SetupCreateBackend(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/backends/create", func(c *gin.Context) {
|
||||||
var req BackendCreationRequest
|
var req BackendCreationRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -35,7 +35,7 @@ func CreateBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -43,7 +43,7 @@ func CreateBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
|
@ -190,7 +190,7 @@ func CreateBackend(c *gin.Context) {
|
||||||
|
|
||||||
log.Info("Passed backend checks successfully")
|
log.Info("Passed backend checks successfully")
|
||||||
|
|
||||||
backendInDatabase := &dbcore.Backend{
|
backendInDatabase := &db.Backend{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
|
@ -198,7 +198,7 @@ func CreateBackend(c *gin.Context) {
|
||||||
BackendParameters: base64.StdEncoding.EncodeToString(backendParameters),
|
BackendParameters: base64.StdEncoding.EncodeToString(backendParameters),
|
||||||
}
|
}
|
||||||
|
|
||||||
if result := dbcore.DB.Create(&backendInDatabase); result.Error != nil {
|
if result := state.DB.DB.Create(&backendInDatabase); result.Error != nil {
|
||||||
log.Warnf("Failed to create backend: %s", result.Error.Error())
|
log.Warnf("Failed to create backend: %s", result.Error.Error())
|
||||||
|
|
||||||
err = backend.Stop()
|
err = backend.Stop()
|
||||||
|
@ -266,4 +266,5 @@ func CreateBackend(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BackendLookupRequest struct {
|
type BackendLookupRequest struct {
|
||||||
|
@ -38,7 +37,8 @@ type LookupResponse struct {
|
||||||
Data []*SanitizedBackend `json:"data"`
|
Data []*SanitizedBackend `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LookupBackend(c *gin.Context) {
|
func SetupLookupBackend(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/backends/lookup", func(c *gin.Context) {
|
||||||
var req BackendLookupRequest
|
var req BackendLookupRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -49,7 +49,7 @@ func LookupBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -57,7 +57,7 @@ func LookupBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
|
@ -85,7 +85,7 @@ func LookupBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
backends := []dbcore.Backend{}
|
backends := []db.Backend{}
|
||||||
queryString := []string{}
|
queryString := []string{}
|
||||||
queryParameters := []interface{}{}
|
queryParameters := []interface{}{}
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ func LookupBackend(c *gin.Context) {
|
||||||
queryParameters = append(queryParameters, req.Backend)
|
queryParameters = append(queryParameters, req.Backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil {
|
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil {
|
||||||
log.Warnf("Failed to get backends: %s", err.Error())
|
log.Warnf("Failed to get backends: %s", err.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
@ -160,4 +160,5 @@ func LookupBackend(c *gin.Context) {
|
||||||
Success: true,
|
Success: true,
|
||||||
Data: sanitizedBackends,
|
Data: sanitizedBackends,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BackendRemovalRequest struct {
|
type BackendRemovalRequest struct {
|
||||||
|
@ -18,7 +17,8 @@ type BackendRemovalRequest struct {
|
||||||
BackendID uint `json:"id" validate:"required"`
|
BackendID uint `json:"id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveBackend(c *gin.Context) {
|
func SetupRemoveBackend(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/backends/remove", func(c *gin.Context) {
|
||||||
var req BackendRemovalRequest
|
var req BackendRemovalRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -29,7 +29,7 @@ func RemoveBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -37,7 +37,7 @@ func RemoveBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
|
@ -65,8 +65,8 @@ func RemoveBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var backend *dbcore.Backend
|
var backend *db.Backend
|
||||||
backendRequest := dbcore.DB.Where("id = ?", req.BackendID).Find(&backend)
|
backendRequest := state.DB.DB.Where("id = ?", req.BackendID).Find(&backend)
|
||||||
|
|
||||||
if backendRequest.Error != nil {
|
if backendRequest.Error != nil {
|
||||||
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
|
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
|
||||||
|
@ -88,7 +88,7 @@ func RemoveBackend(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbcore.DB.Delete(backend).Error; err != nil {
|
if err := state.DB.DB.Delete(backend).Error; err != nil {
|
||||||
log.Warnf("failed to delete backend: %s", err.Error())
|
log.Warnf("failed to delete backend: %s", err.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
@ -120,4 +120,5 @@ func RemoveBackend(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,12 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConnectionsRequest struct {
|
type ConnectionsRequest struct {
|
||||||
|
@ -37,7 +36,8 @@ type ConnectionsResponse struct {
|
||||||
Data []*SanitizedConnection `json:"data"`
|
Data []*SanitizedConnection `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConnections(c *gin.Context) {
|
func SetupGetConnections(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/forward/connections", func(c *gin.Context) {
|
||||||
var req ConnectionsRequest
|
var req ConnectionsRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -48,7 +48,7 @@ func GetConnections(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -56,7 +56,8 @@ func GetConnections(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
@ -83,8 +84,8 @@ func GetConnections(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy dbcore.Proxy
|
var proxy db.Proxy
|
||||||
proxyRequest := dbcore.DB.Where("id = ?", req.Id).First(&proxy)
|
proxyRequest := state.DB.DB.Where("id = ?", req.Id).First(&proxy)
|
||||||
|
|
||||||
if proxyRequest.Error != nil {
|
if proxyRequest.Error != nil {
|
||||||
log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error())
|
log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error())
|
||||||
|
@ -160,4 +161,5 @@ func GetConnections(c *gin.Context) {
|
||||||
"error": "Got illegal response type",
|
"error": "Got illegal response type",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,12 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxyCreationRequest struct {
|
type ProxyCreationRequest struct {
|
||||||
|
@ -26,7 +25,8 @@ type ProxyCreationRequest struct {
|
||||||
AutoStart *bool `json:"autoStart"`
|
AutoStart *bool `json:"autoStart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateProxy(c *gin.Context) {
|
func SetupCreateProxy(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/forward/create", func(c *gin.Context) {
|
||||||
var req ProxyCreationRequest
|
var req ProxyCreationRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -37,7 +37,7 @@ func CreateProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -45,7 +45,8 @@ func CreateProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
@ -80,8 +81,8 @@ func CreateProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var backend dbcore.Backend
|
var backend db.Backend
|
||||||
backendRequest := dbcore.DB.Where("id = ?", req.ProviderID).First(&backend)
|
backendRequest := state.DB.DB.Where("id = ?", req.ProviderID).First(&backend)
|
||||||
|
|
||||||
if backendRequest.Error != nil {
|
if backendRequest.Error != nil {
|
||||||
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
|
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
|
||||||
|
@ -105,7 +106,7 @@ func CreateProxy(c *gin.Context) {
|
||||||
autoStart = *req.AutoStart
|
autoStart = *req.AutoStart
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := &dbcore.Proxy{
|
proxy := &db.Proxy{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
BackendID: req.ProviderID,
|
BackendID: req.ProviderID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
|
@ -117,7 +118,7 @@ func CreateProxy(c *gin.Context) {
|
||||||
AutoStart: autoStart,
|
AutoStart: autoStart,
|
||||||
}
|
}
|
||||||
|
|
||||||
if result := dbcore.DB.Create(proxy); result.Error != nil {
|
if result := state.DB.DB.Create(proxy); result.Error != nil {
|
||||||
log.Warnf("failed to create proxy: %s", result.Error.Error())
|
log.Warnf("failed to create proxy: %s", result.Error.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
@ -172,4 +173,5 @@ func CreateProxy(c *gin.Context) {
|
||||||
"success": true,
|
"success": true,
|
||||||
"id": proxy.ID,
|
"id": proxy.ID,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxyLookupRequest struct {
|
type ProxyLookupRequest struct {
|
||||||
|
@ -43,7 +42,8 @@ type ProxyLookupResponse struct {
|
||||||
Data []*SanitizedProxy `json:"data"`
|
Data []*SanitizedProxy `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LookupProxy(c *gin.Context) {
|
func SetupLookupProxy(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/forward/lookup", func(c *gin.Context) {
|
||||||
var req ProxyLookupRequest
|
var req ProxyLookupRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -54,7 +54,7 @@ func LookupProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -62,7 +62,7 @@ func LookupProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
|
@ -100,7 +100,7 @@ func LookupProxy(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxies := []dbcore.Proxy{}
|
proxies := []db.Proxy{}
|
||||||
|
|
||||||
queryString := []string{}
|
queryString := []string{}
|
||||||
queryParameters := []interface{}{}
|
queryParameters := []interface{}{}
|
||||||
|
@ -150,7 +150,7 @@ func LookupProxy(c *gin.Context) {
|
||||||
queryParameters = append(queryParameters, req.Protocol)
|
queryParameters = append(queryParameters, req.Protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil {
|
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil {
|
||||||
log.Warnf("failed to get proxies: %s", err.Error())
|
log.Warnf("failed to get proxies: %s", err.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
@ -180,4 +180,5 @@ func LookupProxy(c *gin.Context) {
|
||||||
Success: true,
|
Success: true,
|
||||||
Data: sanitizedProxies,
|
Data: sanitizedProxies,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,12 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxyRemovalRequest struct {
|
type ProxyRemovalRequest struct {
|
||||||
|
@ -19,7 +18,8 @@ type ProxyRemovalRequest struct {
|
||||||
ID uint `validate:"required" json:"id"`
|
ID uint `validate:"required" json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveProxy(c *gin.Context) {
|
func SetupRemoveProxy(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/forward/remove", func(c *gin.Context) {
|
||||||
var req ProxyRemovalRequest
|
var req ProxyRemovalRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -30,7 +30,7 @@ func RemoveProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,8 @@ func RemoveProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
@ -65,8 +66,8 @@ func RemoveProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy *dbcore.Proxy
|
var proxy *db.Proxy
|
||||||
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
|
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||||
|
|
||||||
if proxyRequest.Error != nil {
|
if proxyRequest.Error != nil {
|
||||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||||
|
@ -88,7 +89,7 @@ func RemoveProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbcore.DB.Delete(proxy).Error; err != nil {
|
if err := state.DB.DB.Delete(proxy).Error; err != nil {
|
||||||
log.Warnf("failed to delete proxy: %s", err.Error())
|
log.Warnf("failed to delete proxy: %s", err.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
@ -133,8 +134,10 @@ func RemoveProxy(c *gin.Context) {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": "Failed to stop proxy. Proxy was still successfully deleted",
|
"error": "Failed to stop proxy. Proxy was still successfully deleted",
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
return
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||||
|
@ -142,11 +145,6 @@ func RemoveProxy(c *gin.Context) {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": "Got invalid response from backend. Proxy was still successfully deleted",
|
"error": "Got invalid response from backend. Proxy was still successfully deleted",
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,12 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxyStartRequest struct {
|
type ProxyStartRequest struct {
|
||||||
|
@ -19,7 +18,8 @@ type ProxyStartRequest struct {
|
||||||
ID uint `validate:"required" json:"id"`
|
ID uint `validate:"required" json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartProxy(c *gin.Context) {
|
func SetupStartProxy(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/forward/start", func(c *gin.Context) {
|
||||||
var req ProxyStartRequest
|
var req ProxyStartRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -30,7 +30,7 @@ func StartProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,8 @@ func StartProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
@ -65,8 +66,8 @@ func StartProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy *dbcore.Proxy
|
var proxy *db.Proxy
|
||||||
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
|
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||||
|
|
||||||
if proxyRequest.Error != nil {
|
if proxyRequest.Error != nil {
|
||||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||||
|
@ -114,29 +115,22 @@ func StartProxy(c *gin.Context) {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": "failed to get response from backend",
|
"error": "failed to get response from backend",
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
|
||||||
case *commonbackend.ProxyStatusResponse:
|
case *commonbackend.ProxyStatusResponse:
|
||||||
if !responseMessage.IsActive {
|
if !responseMessage.IsActive {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": "failed to start proxy",
|
"error": "failed to start proxy",
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
return
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": "Got invalid response from backend. Proxy was still successfully deleted",
|
"error": "Got invalid response from backend. Proxy was likely still successfully started",
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,12 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxyStopRequest struct {
|
type ProxyStopRequest struct {
|
||||||
|
@ -19,8 +18,9 @@ type ProxyStopRequest struct {
|
||||||
ID uint `validate:"required" json:"id"`
|
ID uint `validate:"required" json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func StopProxy(c *gin.Context) {
|
func SetupStopProxy(state *state.State) {
|
||||||
var req ProxyStopRequest
|
state.Engine.POST("/api/v1/forward/stop", func(c *gin.Context) {
|
||||||
|
var req ProxyStartRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
@ -30,7 +30,7 @@ func StopProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,8 @@ func StopProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
@ -65,8 +66,8 @@ func StopProxy(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy *dbcore.Proxy
|
var proxy *db.Proxy
|
||||||
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
|
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||||
|
|
||||||
if proxyRequest.Error != nil {
|
if proxyRequest.Error != nil {
|
||||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||||
|
@ -107,36 +108,29 @@ func StopProxy(c *gin.Context) {
|
||||||
Protocol: proxy.Protocol,
|
Protocol: proxy.Protocol,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
switch responseMessage := backendResponse.(type) {
|
||||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
|
case error:
|
||||||
|
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": "failed to get response from backend",
|
"error": "failed to get response from backend",
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch responseMessage := backendResponse.(type) {
|
|
||||||
case *commonbackend.ProxyStatusResponse:
|
case *commonbackend.ProxyStatusResponse:
|
||||||
if responseMessage.IsActive {
|
if responseMessage.IsActive {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": "failed to stop proxy",
|
"error": "failed to stop proxy",
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
return
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": "Got invalid response from backend. Proxy was still successfully deleted",
|
"error": "Got invalid response from backend. Proxy was likely still successfully stopped",
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
permissionHelper "git.terah.dev/imterah/hermes/backend/api/permissions"
|
permissionHelper "git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
@ -22,13 +20,11 @@ type UserCreationRequest struct {
|
||||||
Email string `validate:"required"`
|
Email string `validate:"required"`
|
||||||
Password string `validate:"required"`
|
Password string `validate:"required"`
|
||||||
Username string `validate:"required"`
|
Username string `validate:"required"`
|
||||||
|
|
||||||
// TODO: implement support
|
|
||||||
ExistingUserToken string `json:"token"`
|
|
||||||
IsBot bool
|
IsBot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateUser(c *gin.Context) {
|
func SetupCreateUser(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/users/create", func(c *gin.Context) {
|
||||||
if !signupEnabled && !unsafeSignup {
|
if !signupEnabled && !unsafeSignup {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
"error": "Signing up is not enabled at this time.",
|
"error": "Signing up is not enabled at this time.",
|
||||||
|
@ -47,7 +43,7 @@ func CreateUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -55,8 +51,8 @@ func CreateUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var user *dbcore.User
|
var user *db.User
|
||||||
userRequest := dbcore.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user)
|
userRequest := state.DB.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user)
|
||||||
|
|
||||||
if userRequest.Error != nil {
|
if userRequest.Error != nil {
|
||||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||||
|
@ -90,7 +86,7 @@ func CreateUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
permissions := []dbcore.Permission{}
|
permissions := []db.Permission{}
|
||||||
|
|
||||||
for _, permission := range permissionHelper.DefaultPermissionNodes {
|
for _, permission := range permissionHelper.DefaultPermissionNodes {
|
||||||
permissionEnabledState := false
|
permissionEnabledState := false
|
||||||
|
@ -99,7 +95,7 @@ func CreateUser(c *gin.Context) {
|
||||||
permissionEnabledState = true
|
permissionEnabledState = true
|
||||||
}
|
}
|
||||||
|
|
||||||
permissions = append(permissions, dbcore.Permission{
|
permissions = append(permissions, db.Permission{
|
||||||
PermissionNode: permission,
|
PermissionNode: permission,
|
||||||
HasPermission: permissionEnabledState,
|
HasPermission: permissionEnabledState,
|
||||||
})
|
})
|
||||||
|
@ -117,14 +113,14 @@ func CreateUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user = &dbcore.User{
|
user = &db.User{
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
IsBot: &req.IsBot,
|
IsBot: &req.IsBot,
|
||||||
Password: base64.StdEncoding.EncodeToString(passwordHashed),
|
Password: base64.StdEncoding.EncodeToString(passwordHashed),
|
||||||
Permissions: permissions,
|
Permissions: permissions,
|
||||||
Tokens: []dbcore.Token{
|
Tokens: []db.Token{
|
||||||
{
|
{
|
||||||
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
|
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||||
DisableExpiry: forceNoExpiryTokens,
|
DisableExpiry: forceNoExpiryTokens,
|
||||||
|
@ -133,7 +129,7 @@ func CreateUser(c *gin.Context) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if result := dbcore.DB.Create(&user); result.Error != nil {
|
if result := state.DB.DB.Create(&user); result.Error != nil {
|
||||||
log.Warnf("Failed to create user: %s", result.Error.Error())
|
log.Warnf("Failed to create user: %s", result.Error.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
@ -143,7 +139,7 @@ func CreateUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt, err := jwtcore.Generate(user.ID)
|
jwt, err := state.JWT.Generate(user.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||||
|
@ -160,4 +156,5 @@ func CreateUser(c *gin.Context) {
|
||||||
"token": jwt,
|
"token": jwt,
|
||||||
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
|
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,7 +20,8 @@ type UserLoginRequest struct {
|
||||||
Password string `validate:"required"`
|
Password string `validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoginUser(c *gin.Context) {
|
func SetupLoginUser(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/users/login", func(c *gin.Context) {
|
||||||
var req UserLoginRequest
|
var req UserLoginRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -32,7 +32,7 @@ func LoginUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -61,8 +61,8 @@ func LoginUser(c *gin.Context) {
|
||||||
userFindRequest += "username = ?"
|
userFindRequest += "username = ?"
|
||||||
}
|
}
|
||||||
|
|
||||||
var user *dbcore.User
|
var user *db.User
|
||||||
userRequest := dbcore.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user)
|
userRequest := state.DB.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user)
|
||||||
|
|
||||||
if userRequest.Error != nil {
|
if userRequest.Error != nil {
|
||||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||||
|
@ -119,7 +119,7 @@ func LoginUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token := &dbcore.Token{
|
token := &db.Token{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
|
|
||||||
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
|
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||||
|
@ -127,7 +127,7 @@ func LoginUser(c *gin.Context) {
|
||||||
CreationIPAddr: c.ClientIP(),
|
CreationIPAddr: c.ClientIP(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if result := dbcore.DB.Create(&token); result.Error != nil {
|
if result := state.DB.DB.Create(&token); result.Error != nil {
|
||||||
log.Warnf("Failed to create user: %s", result.Error.Error())
|
log.Warnf("Failed to create user: %s", result.Error.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
@ -137,7 +137,7 @@ func LoginUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt, err := jwtcore.Generate(user.ID)
|
jwt, err := state.JWT.Generate(user.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||||
|
@ -154,4 +154,5 @@ func LoginUser(c *gin.Context) {
|
||||||
"token": jwt,
|
"token": jwt,
|
||||||
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
|
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserLookupRequest struct {
|
type UserLookupRequest struct {
|
||||||
|
@ -35,7 +34,8 @@ type LookupResponse struct {
|
||||||
Data []*SanitizedUsers `json:"data"`
|
Data []*SanitizedUsers `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LookupUser(c *gin.Context) {
|
func SetupLookupUser(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/users/lookup", func(c *gin.Context) {
|
||||||
var req UserLookupRequest
|
var req UserLookupRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -46,7 +46,7 @@ func LookupUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -54,7 +54,7 @@ func LookupUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
|
@ -74,7 +74,7 @@ func LookupUser(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
users := []dbcore.User{}
|
users := []db.User{}
|
||||||
queryString := []string{}
|
queryString := []string{}
|
||||||
queryParameters := []interface{}{}
|
queryParameters := []interface{}{}
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ func LookupUser(c *gin.Context) {
|
||||||
queryParameters = append(queryParameters, req.IsBot)
|
queryParameters = append(queryParameters, req.IsBot)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil {
|
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil {
|
||||||
log.Warnf("Failed to get users: %s", err.Error())
|
log.Warnf("Failed to get users: %s", err.Error())
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
@ -133,4 +133,5 @@ func LookupUser(c *gin.Context) {
|
||||||
Success: true,
|
Success: true,
|
||||||
Data: sanitizedUsers,
|
Data: sanitizedUsers,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,18 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserRefreshRequest struct {
|
type UserRefreshRequest struct {
|
||||||
Token string `validate:"required"`
|
Token string `validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func RefreshUserToken(c *gin.Context) {
|
func SetupRefreshUserToken(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/users/refresh", func(c *gin.Context) {
|
||||||
var req UserRefreshRequest
|
var req UserRefreshRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -27,7 +27,7 @@ func RefreshUserToken(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -35,8 +35,8 @@ func RefreshUserToken(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenInDatabase *dbcore.Token
|
var tokenInDatabase *db.Token
|
||||||
tokenRequest := dbcore.DB.Where("token = ?", req.Token).Find(&tokenInDatabase)
|
tokenRequest := state.DB.DB.Where("token = ?", req.Token).Find(&tokenInDatabase)
|
||||||
|
|
||||||
if tokenRequest.Error != nil {
|
if tokenRequest.Error != nil {
|
||||||
log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error())
|
log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error())
|
||||||
|
@ -65,7 +65,7 @@ func RefreshUserToken(c *gin.Context) {
|
||||||
"error": "Token has expired",
|
"error": "Token has expired",
|
||||||
})
|
})
|
||||||
|
|
||||||
tx := dbcore.DB.Delete(tokenInDatabase)
|
tx := state.DB.DB.Delete(tokenInDatabase)
|
||||||
|
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error())
|
log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error())
|
||||||
|
@ -75,8 +75,8 @@ func RefreshUserToken(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the user to check if the user exists before doing anything
|
// Get the user to check if the user exists before doing anything
|
||||||
var user *dbcore.User
|
var user *db.User
|
||||||
userRequest := dbcore.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user)
|
userRequest := state.DB.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user)
|
||||||
|
|
||||||
if tokenRequest.Error != nil {
|
if tokenRequest.Error != nil {
|
||||||
log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error())
|
log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error())
|
||||||
|
@ -98,7 +98,7 @@ func RefreshUserToken(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt, err := jwtcore.Generate(user.ID)
|
jwt, err := state.JWT.Generate(user.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||||
|
@ -114,4 +114,5 @@ func RefreshUserToken(c *gin.Context) {
|
||||||
"success": true,
|
"success": true,
|
||||||
"token": jwt,
|
"token": jwt,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserRemovalRequest struct {
|
type UserRemovalRequest struct {
|
||||||
|
@ -17,7 +16,8 @@ type UserRemovalRequest struct {
|
||||||
UID *uint `json:"uid"`
|
UID *uint `json:"uid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveUser(c *gin.Context) {
|
func SetupRemoveUser(state *state.State) {
|
||||||
|
state.Engine.POST("/api/v1/users/remove", func(c *gin.Context) {
|
||||||
var req UserRemovalRequest
|
var req UserRemovalRequest
|
||||||
|
|
||||||
if err := c.BindJSON(&req); err != nil {
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
@ -28,7 +28,7 @@ func RemoveUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.New().Struct(&req); err != nil {
|
if err := state.Validator.Struct(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||||
})
|
})
|
||||||
|
@ -36,7 +36,7 @@ func RemoveUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||||
|
@ -73,8 +73,8 @@ func RemoveUser(c *gin.Context) {
|
||||||
// Make sure the user exists first if we have a custom UserID
|
// Make sure the user exists first if we have a custom UserID
|
||||||
|
|
||||||
if uid != user.ID {
|
if uid != user.ID {
|
||||||
var customUser *dbcore.User
|
var customUser *db.User
|
||||||
userRequest := dbcore.DB.Where("id = ?", uid).Find(customUser)
|
userRequest := state.DB.DB.Where("id = ?", uid).Find(customUser)
|
||||||
|
|
||||||
if userRequest.Error != nil {
|
if userRequest.Error != nil {
|
||||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||||
|
@ -97,9 +97,10 @@ func RemoveUser(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dbcore.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user)
|
state.DB.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
77
backend/api/db/db.go
Normal file
77
backend/api/db/db.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(backend, params string) (*DB, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
dialector, err := initDialector(backend, params)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize physical database: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
database, err := gorm.Open(dialector)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DB{DB: database}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DoMigrations() error {
|
||||||
|
if err := db.DB.AutoMigrate(&Proxy{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.AutoMigrate(&Backend{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.AutoMigrate(&Permission{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.AutoMigrate(&Token{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.AutoMigrate(&User{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDialector(backend, params string) (gorm.Dialector, error) {
|
||||||
|
switch backend {
|
||||||
|
case "sqlite":
|
||||||
|
if params == "" {
|
||||||
|
return nil, fmt.Errorf("sqlite database file not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqlite.Open(params), nil
|
||||||
|
case "postgresql":
|
||||||
|
if params == "" {
|
||||||
|
return nil, fmt.Errorf("postgres DSN not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return postgres.Open(params), nil
|
||||||
|
case "":
|
||||||
|
return nil, fmt.Errorf("no database backend specified in environment variables")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown database backend specified: %s", os.Getenv(backend))
|
||||||
|
}
|
||||||
|
}
|
66
backend/api/db/models.go
Normal file
66
backend/api/db/models.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Backend struct {
|
||||||
|
gorm.Model
|
||||||
|
|
||||||
|
UserID uint
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Description *string
|
||||||
|
Backend string
|
||||||
|
BackendParameters string
|
||||||
|
|
||||||
|
Proxies []Proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
type Proxy struct {
|
||||||
|
gorm.Model
|
||||||
|
|
||||||
|
BackendID uint
|
||||||
|
UserID uint
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Description *string
|
||||||
|
Protocol string
|
||||||
|
SourceIP string
|
||||||
|
SourcePort uint16
|
||||||
|
DestinationPort uint16
|
||||||
|
AutoStart bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Permission struct {
|
||||||
|
gorm.Model
|
||||||
|
|
||||||
|
PermissionNode string
|
||||||
|
HasPermission bool
|
||||||
|
UserID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
gorm.Model
|
||||||
|
|
||||||
|
UserID uint
|
||||||
|
|
||||||
|
Token string
|
||||||
|
DisableExpiry bool
|
||||||
|
CreationIPAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
gorm.Model
|
||||||
|
|
||||||
|
Email string `gorm:"unique"`
|
||||||
|
Username string `gorm:"unique"`
|
||||||
|
Name string
|
||||||
|
Password string
|
||||||
|
IsBot *bool
|
||||||
|
|
||||||
|
Permissions []Permission
|
||||||
|
OwnedProxies []Proxy
|
||||||
|
OwnedBackends []Backend
|
||||||
|
Tokens []Token
|
||||||
|
}
|
|
@ -1,142 +0,0 @@
|
||||||
package dbcore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Backend struct {
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
UserID uint
|
|
||||||
|
|
||||||
Name string
|
|
||||||
Description *string
|
|
||||||
Backend string
|
|
||||||
BackendParameters string
|
|
||||||
|
|
||||||
Proxies []Proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
type Proxy struct {
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
BackendID uint
|
|
||||||
UserID uint
|
|
||||||
|
|
||||||
Name string
|
|
||||||
Description *string
|
|
||||||
Protocol string
|
|
||||||
SourceIP string
|
|
||||||
SourcePort uint16
|
|
||||||
DestinationPort uint16
|
|
||||||
AutoStart bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Permission struct {
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
PermissionNode string
|
|
||||||
HasPermission bool
|
|
||||||
UserID uint
|
|
||||||
}
|
|
||||||
|
|
||||||
type Token struct {
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
UserID uint
|
|
||||||
|
|
||||||
Token string
|
|
||||||
DisableExpiry bool
|
|
||||||
CreationIPAddr string
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
Email string `gorm:"unique"`
|
|
||||||
Username string `gorm:"unique"`
|
|
||||||
Name string
|
|
||||||
Password string
|
|
||||||
IsBot *bool
|
|
||||||
|
|
||||||
Permissions []Permission
|
|
||||||
OwnedProxies []Proxy
|
|
||||||
OwnedBackends []Backend
|
|
||||||
Tokens []Token
|
|
||||||
}
|
|
||||||
|
|
||||||
var DB *gorm.DB
|
|
||||||
|
|
||||||
func InitializeDatabaseDialector() (gorm.Dialector, error) {
|
|
||||||
databaseBackend := os.Getenv("HERMES_DATABASE_BACKEND")
|
|
||||||
|
|
||||||
switch databaseBackend {
|
|
||||||
case "sqlite":
|
|
||||||
filePath := os.Getenv("HERMES_SQLITE_FILEPATH")
|
|
||||||
|
|
||||||
if filePath == "" {
|
|
||||||
return nil, fmt.Errorf("sqlite database file not specified (missing HERMES_SQLITE_FILEPATH)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sqlite.Open(filePath), nil
|
|
||||||
case "postgresql":
|
|
||||||
postgresDSN := os.Getenv("HERMES_POSTGRES_DSN")
|
|
||||||
|
|
||||||
if postgresDSN == "" {
|
|
||||||
return nil, fmt.Errorf("postgres DSN not specified (missing HERMES_POSTGRES_DSN)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return postgres.Open(postgresDSN), nil
|
|
||||||
case "":
|
|
||||||
return nil, fmt.Errorf("no database backend specified in environment variables (missing HERMES_DATABASE_BACKEND)")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown database backend specified: %s", os.Getenv(databaseBackend))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitializeDatabase(config *gorm.Config) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
dialector, err := InitializeDatabaseDialector()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize physical database: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
DB, err = gorm.Open(dialector, config)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open database: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DoDatabaseMigrations(db *gorm.DB) error {
|
|
||||||
if err := db.AutoMigrate(&Proxy{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.AutoMigrate(&Backend{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.AutoMigrate(&Permission{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.AutoMigrate(&Token{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.AutoMigrate(&User{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
107
backend/api/jwt/jwt.go
Normal file
107
backend/api/jwt/jwt.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DevelopmentModeTimings = time.Duration(60*24) * time.Minute
|
||||||
|
NormalModeTimings = time.Duration(3) * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type JWTCore struct {
|
||||||
|
Key []byte
|
||||||
|
Database *db.DB
|
||||||
|
TimeMultiplier time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(key []byte, database *db.DB, timeMultiplier time.Duration) *JWTCore {
|
||||||
|
jwtCore := &JWTCore{
|
||||||
|
Key: key,
|
||||||
|
Database: database,
|
||||||
|
TimeMultiplier: timeMultiplier,
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwtCore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jwtCore *JWTCore) Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) {
|
||||||
|
return jwt.Parse(tokenString, jwtCore.jwtKeyCallback, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jwtCore *JWTCore) GetUserFromJWT(token string) (*db.User, error) {
|
||||||
|
if jwtCore.Database == nil {
|
||||||
|
return nil, fmt.Errorf("database is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedJWT, err := jwtCore.Parse(token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||||
|
return nil, fmt.Errorf("token is expired")
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audience, err := parsedJWT.Claims.GetAudience()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(audience) < 1 {
|
||||||
|
return nil, fmt.Errorf("audience is too small")
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := strconv.Atoi(audience[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &db.User{}
|
||||||
|
userRequest := jwtCore.Database.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user)
|
||||||
|
|
||||||
|
if userRequest.Error != nil {
|
||||||
|
return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
userExists := userRequest.RowsAffected > 0
|
||||||
|
|
||||||
|
if !userExists {
|
||||||
|
return user, fmt.Errorf("user does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jwtCore *JWTCore) Generate(uid uint) (string, error) {
|
||||||
|
currentJWTTime := jwt.NewNumericDate(time.Now())
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtCore.TimeMultiplier)),
|
||||||
|
IssuedAt: currentJWTTime,
|
||||||
|
NotBefore: currentJWTTime,
|
||||||
|
// Convert the user ID to a string, and then set it as the audience parameters only value (there's only 1 user per key)
|
||||||
|
Audience: []string{strconv.Itoa(int(uid))},
|
||||||
|
})
|
||||||
|
|
||||||
|
signedToken, err := token.SignedString(jwtCore.Key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jwtCore *JWTCore) jwtKeyCallback(*jwt.Token) (any, error) {
|
||||||
|
return jwtCore.Key, nil
|
||||||
|
}
|
|
@ -1,117 +0,0 @@
|
||||||
package jwtcore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
JWTKey []byte
|
|
||||||
developmentMode bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func SetupJWT() error {
|
|
||||||
var err error
|
|
||||||
jwtDataString := os.Getenv("HERMES_JWT_SECRET")
|
|
||||||
|
|
||||||
if jwtDataString == "" {
|
|
||||||
return fmt.Errorf("JWT secret isn't set (missing HERMES_JWT_SECRET)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("HERMES_JWT_BASE64_ENCODED") != "" {
|
|
||||||
JWTKey, err = base64.StdEncoding.DecodeString(jwtDataString)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to decode base64 JWT: %s", err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
JWTKey = []byte(jwtDataString)
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("HERMES_DEVELOPMENT_MODE") != "" {
|
|
||||||
developmentMode = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) {
|
|
||||||
return jwt.Parse(tokenString, JWTKeyCallback, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUserFromJWT(token string) (*dbcore.User, error) {
|
|
||||||
parsedJWT, err := Parse(token)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
|
||||||
return nil, fmt.Errorf("token is expired")
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audience, err := parsedJWT.Claims.GetAudience()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(audience) < 1 {
|
|
||||||
return nil, fmt.Errorf("audience is too small")
|
|
||||||
}
|
|
||||||
|
|
||||||
uid, err := strconv.Atoi(audience[0])
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &dbcore.User{}
|
|
||||||
userRequest := dbcore.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user)
|
|
||||||
|
|
||||||
if userRequest.Error != nil {
|
|
||||||
return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
userExists := userRequest.RowsAffected > 0
|
|
||||||
|
|
||||||
if !userExists {
|
|
||||||
return user, fmt.Errorf("user does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Generate(uid uint) (string, error) {
|
|
||||||
timeMultiplier := 3
|
|
||||||
|
|
||||||
if developmentMode {
|
|
||||||
timeMultiplier = 60 * 24
|
|
||||||
}
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(timeMultiplier) * time.Minute)),
|
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
||||||
Audience: []string{strconv.Itoa(int(uid))},
|
|
||||||
})
|
|
||||||
|
|
||||||
signedToken, err := token.SignedString(JWTKey)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return signedToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func JWTKeyCallback(*jwt.Token) (interface{}, error) {
|
|
||||||
return JWTKey, nil
|
|
||||||
}
|
|
|
@ -9,18 +9,19 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/backends"
|
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/backends"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/proxies"
|
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/proxies"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/users"
|
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/users"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
"git.terah.dev/imterah/hermes/backend/api/jwt"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func apiEntrypoint(cCtx *cli.Context) error {
|
func apiEntrypoint(cCtx *cli.Context) error {
|
||||||
|
@ -34,7 +35,26 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
||||||
log.Info("Hermes is initializing...")
|
log.Info("Hermes is initializing...")
|
||||||
log.Debug("Initializing database and opening it...")
|
log.Debug("Initializing database and opening it...")
|
||||||
|
|
||||||
err := dbcore.InitializeDatabase(&gorm.Config{})
|
databaseBackendName := os.Getenv("HERMES_DATABASE_BACKEND")
|
||||||
|
var databaseBackendParams string
|
||||||
|
|
||||||
|
if databaseBackendName == "sqlite" {
|
||||||
|
databaseBackendParams = os.Getenv("HERMES_SQLITE_FILEPATH")
|
||||||
|
|
||||||
|
if databaseBackendParams == "" {
|
||||||
|
log.Fatal("HERMES_SQLITE_FILEPATH is not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if databaseBackendName == "postgres" {
|
||||||
|
databaseBackendParams = os.Getenv("HERMES_POSTGRES_DSN")
|
||||||
|
|
||||||
|
if databaseBackendParams == "" {
|
||||||
|
log.Fatal("HERMES_POSTGRES_DSN is not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbInstance, err := db.New(databaseBackendName, databaseBackendParams)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to initialize database: %s", err)
|
log.Fatalf("Failed to initialize database: %s", err)
|
||||||
|
@ -42,16 +62,38 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
||||||
|
|
||||||
log.Debug("Running database migrations...")
|
log.Debug("Running database migrations...")
|
||||||
|
|
||||||
if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil {
|
if err := dbInstance.DoMigrations(); err != nil {
|
||||||
return fmt.Errorf("Failed to run database migrations: %s", err)
|
return fmt.Errorf("Failed to run database migrations: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Initializing the JWT subsystem...")
|
log.Debug("Initializing the JWT subsystem...")
|
||||||
|
|
||||||
if err := jwtcore.SetupJWT(); err != nil {
|
jwtDataString := os.Getenv("HERMES_JWT_SECRET")
|
||||||
return fmt.Errorf("Failed to initialize the JWT subsystem: %s", err.Error())
|
var jwtKey []byte
|
||||||
|
var jwtValidityTimeDuration time.Duration
|
||||||
|
|
||||||
|
if jwtDataString == "" {
|
||||||
|
log.Fatalf("HERMES_JWT_SECRET is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("HERMES_JWT_BASE64_ENCODED") != "" {
|
||||||
|
jwtKey, err = base64.StdEncoding.DecodeString(jwtDataString)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decode base64 JWT: %s", err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jwtKey = []byte(jwtDataString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if developmentMode {
|
||||||
|
jwtValidityTimeDuration = jwt.DevelopmentModeTimings
|
||||||
|
} else {
|
||||||
|
jwtValidityTimeDuration = jwt.NormalModeTimings
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtInstance := jwt.New(jwtKey, dbInstance, jwtValidityTimeDuration)
|
||||||
|
|
||||||
log.Debug("Initializing the backend subsystem...")
|
log.Debug("Initializing the backend subsystem...")
|
||||||
|
|
||||||
backendMetadataPath := cCtx.String("backends-path")
|
backendMetadataPath := cCtx.String("backends-path")
|
||||||
|
@ -76,9 +118,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
||||||
|
|
||||||
log.Debug("Enumerating backends...")
|
log.Debug("Enumerating backends...")
|
||||||
|
|
||||||
backendList := []dbcore.Backend{}
|
backendList := []db.Backend{}
|
||||||
|
|
||||||
if err := dbcore.DB.Find(&backendList).Error; err != nil {
|
if err := dbInstance.DB.Find(&backendList).Error; err != nil {
|
||||||
return fmt.Errorf("Failed to enumerate backends: %s", err.Error())
|
return fmt.Errorf("Failed to enumerate backends: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,9 +183,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
||||||
|
|
||||||
log.Warnf("Backend #%d has reinitialized! Starting up auto-starting proxies...", backend.ID)
|
log.Warnf("Backend #%d has reinitialized! Starting up auto-starting proxies...", backend.ID)
|
||||||
|
|
||||||
autoStartProxies := []dbcore.Proxy{}
|
autoStartProxies := []db.Proxy{}
|
||||||
|
|
||||||
if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||||
log.Errorf("Failed to query proxies to autostart: %s", err.Error())
|
log.Errorf("Failed to query proxies to autostart: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -243,9 +285,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
||||||
|
|
||||||
log.Infof("Successfully initialized backend #%d", backend.ID)
|
log.Infof("Successfully initialized backend #%d", backend.ID)
|
||||||
|
|
||||||
autoStartProxies := []dbcore.Proxy{}
|
autoStartProxies := []db.Proxy{}
|
||||||
|
|
||||||
if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||||
log.Errorf("Failed to query proxies to autostart: %s", err.Error())
|
log.Errorf("Failed to query proxies to autostart: %s", err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -309,23 +351,25 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
||||||
engine.SetTrustedProxies(nil)
|
engine.SetTrustedProxies(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state := state.New(dbInstance, jwtInstance, engine)
|
||||||
|
|
||||||
// Initialize routes
|
// Initialize routes
|
||||||
engine.POST("/api/v1/users/create", users.CreateUser)
|
users.SetupCreateUser(state)
|
||||||
engine.POST("/api/v1/users/login", users.LoginUser)
|
users.SetupLoginUser(state)
|
||||||
engine.POST("/api/v1/users/refresh", users.RefreshUserToken)
|
users.SetupRefreshUserToken(state)
|
||||||
engine.POST("/api/v1/users/remove", users.RemoveUser)
|
users.SetupRemoveUser(state)
|
||||||
engine.POST("/api/v1/users/lookup", users.LookupUser)
|
users.SetupLookupUser(state)
|
||||||
|
|
||||||
engine.POST("/api/v1/backends/create", backends.CreateBackend)
|
backends.SetupCreateBackend(state)
|
||||||
engine.POST("/api/v1/backends/remove", backends.RemoveBackend)
|
backends.SetupRemoveBackend(state)
|
||||||
engine.POST("/api/v1/backends/lookup", backends.LookupBackend)
|
backends.SetupLookupBackend(state)
|
||||||
|
|
||||||
engine.POST("/api/v1/forward/create", proxies.CreateProxy)
|
proxies.SetupCreateProxy(state)
|
||||||
engine.POST("/api/v1/forward/lookup", proxies.LookupProxy)
|
proxies.SetupRemoveProxy(state)
|
||||||
engine.POST("/api/v1/forward/remove", proxies.RemoveProxy)
|
proxies.SetupLookupProxy(state)
|
||||||
engine.POST("/api/v1/forward/start", proxies.StartProxy)
|
proxies.SetupStartProxy(state)
|
||||||
engine.POST("/api/v1/forward/stop", proxies.StopProxy)
|
proxies.SetupStopProxy(state)
|
||||||
engine.POST("/api/v1/forward/connections", proxies.GetConnections)
|
proxies.SetupGetConnections(state)
|
||||||
|
|
||||||
log.Infof("Listening on '%s'", listeningAddress)
|
log.Infof("Listening on '%s'", listeningAddress)
|
||||||
err = engine.Run(listeningAddress)
|
err = engine.Run(listeningAddress)
|
||||||
|
@ -362,22 +406,6 @@ func main() {
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "hermes",
|
Name: "hermes",
|
||||||
Usage: "port forwarding across boundaries",
|
Usage: "port forwarding across boundaries",
|
||||||
Commands: []*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "import",
|
|
||||||
Usage: "imports from legacy NextNet/Hermes source",
|
|
||||||
Aliases: []string{"i"},
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "backup-path",
|
|
||||||
Aliases: []string{"bp"},
|
|
||||||
Usage: "path to the backup file",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Action: backupRestoreEntrypoint,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "backends-path",
|
Name: "backends-path",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package permissions
|
package permissions
|
||||||
|
|
||||||
import "git.terah.dev/imterah/hermes/backend/api/dbcore"
|
import "git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
|
|
||||||
var DefaultPermissionNodes []string = []string{
|
var DefaultPermissionNodes []string = []string{
|
||||||
"routes.add",
|
"routes.add",
|
||||||
|
@ -27,7 +27,7 @@ var DefaultPermissionNodes []string = []string{
|
||||||
"users.edit",
|
"users.edit",
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserHasPermission(user *dbcore.User, node string) bool {
|
func UserHasPermission(user *db.User, node string) bool {
|
||||||
for _, permission := range user.Permissions {
|
for _, permission := range user.Permissions {
|
||||||
if permission.PermissionNode == node && permission.HasPermission {
|
if permission.PermissionNode == node && permission.HasPermission {
|
||||||
return true
|
return true
|
||||||
|
|
24
backend/api/state/state.go
Normal file
24
backend/api/state/state.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||||
|
"git.terah.dev/imterah/hermes/backend/api/jwt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
DB *db.DB
|
||||||
|
JWT *jwt.JWTCore
|
||||||
|
Engine *gin.Engine
|
||||||
|
Validator *validator.Validate
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *db.DB, jwt *jwt.JWTCore, engine *gin.Engine) *State {
|
||||||
|
return &State{
|
||||||
|
DB: db,
|
||||||
|
JWT: jwt,
|
||||||
|
Engine: engine,
|
||||||
|
Validator: validator.New(),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue