Adding new system settings table. (#409)
* Adding new system settings table. Adding associated database migration. Added tests Fixing tests for user-settings. * updated migration, make sure default system settings are created. Added database commands to create and update system settings. Added generic response wrapper. * make sure we can get related versions (fasten sources, onprem and desktop) when starting the app.
This commit is contained in:
parent
dc9704831e
commit
c85e829c46
|
@ -68,3 +68,5 @@ fasten-*.db-wal
|
|||
fasten.db
|
||||
fasten.db-shm
|
||||
fasten.db-wal
|
||||
|
||||
backend/resources/related_versions.json
|
||||
|
|
1
Makefile
1
Makefile
|
@ -39,6 +39,7 @@ clean-backend:
|
|||
.PHONY: dep-backend
|
||||
dep-backend:
|
||||
go mod tidy && go mod vendor
|
||||
cd scripts && go generate ./...
|
||||
|
||||
|
||||
.PHONY: test-backend
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/version"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/web"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/resources"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
"io"
|
||||
|
@ -114,10 +115,13 @@ func main() {
|
|||
settingsData, err := json.Marshal(appconfig.AllSettings())
|
||||
appLogger.Debug(string(settingsData), err)
|
||||
|
||||
relatedVersions, _ := resources.GetRelatedVersions()
|
||||
|
||||
webServer := web.AppEngine{
|
||||
Config: appconfig,
|
||||
Logger: appLogger,
|
||||
EventBus: event_bus.NewEventBusServer(appLogger),
|
||||
Config: appconfig,
|
||||
Logger: appLogger,
|
||||
EventBus: event_bus.NewEventBusServer(appLogger),
|
||||
RelatedVersions: relatedVersions,
|
||||
}
|
||||
return webServer.Start()
|
||||
},
|
||||
|
|
|
@ -7,6 +7,9 @@ type BackgroundJobSchedule string
|
|||
|
||||
type DatabaseRepositoryType string
|
||||
|
||||
type InstallationVerificationStatus string
|
||||
type InstallationQuotaStatus string
|
||||
|
||||
const (
|
||||
ResourceListPageSize int = 20
|
||||
|
||||
|
@ -41,4 +44,10 @@ const (
|
|||
|
||||
DatabaseRepositoryTypeSqlite DatabaseRepositoryType = "sqlite"
|
||||
DatabaseRepositoryTypePostgres DatabaseRepositoryType = "postgres"
|
||||
|
||||
InstallationVerificationStatusMissing InstallationVerificationStatus = "MISSING" //email is missing for this installation
|
||||
InstallationVerificationStatusPending InstallationVerificationStatus = "PENDING" //email has not been verified
|
||||
InstallationVerificationStatusVerified InstallationVerificationStatus = "VERIFIED" //email has been verified
|
||||
InstallationQuotaStatusActive InstallationQuotaStatus = "ACTIVE"
|
||||
InstallationQuotaStatusConsumed InstallationQuotaStatus = "CONSUMED"
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
_20231201122541 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20231201122541"
|
||||
_0240114092806 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114092806"
|
||||
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
|
||||
_20240208112210 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240208112210"
|
||||
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
|
||||
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
|
||||
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
||||
|
@ -141,6 +142,45 @@ func (gr *GormRepository) Migrate() error {
|
|||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "20240208112210", // add system settings
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
err := tx.AutoMigrate(
|
||||
&_20240208112210.SystemSettingEntry{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//add the default system settings
|
||||
defaultSystemSettings := []_20240208112210.SystemSettingEntry{
|
||||
{
|
||||
SettingKeyName: "installation_id",
|
||||
SettingKeyDescription: "installation id is used to identify this installation when making external calls to Fasten Health, Inc. infrastructure. It does not contain any personally identifiable information",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "",
|
||||
},
|
||||
{
|
||||
SettingKeyName: "installation_secret",
|
||||
SettingKeyDescription: "installation secret is used to sign requests/updates to Fasten Health, Inc. infrastructure",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, setting := range defaultSystemSettings {
|
||||
tx.Logger.Info(context.Background(), fmt.Sprintf("Creating System Setting: %s", setting.SettingKeyName))
|
||||
|
||||
settingCreateResp := tx.Create(&setting)
|
||||
if settingCreateResp.Error != nil {
|
||||
tx.Logger.Error(context.Background(), fmt.Sprintf("An error occurred creating System Setting: %s", setting.SettingKeyName))
|
||||
return settingCreateResp.Error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// run when database is empty
|
||||
|
|
|
@ -3,11 +3,77 @@ package database
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// LoadSystemSettings will retrieve settings from the database, and return a SystemSettings struct
|
||||
func (gr *GormRepository) LoadSystemSettings(ctx context.Context) (*models.SystemSettings, error) {
|
||||
|
||||
settingsEntries := []models.SystemSettingEntry{}
|
||||
if err := gr.GormClient.
|
||||
WithContext(ctx).
|
||||
Find(&settingsEntries).Error; err != nil {
|
||||
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
settings := models.SystemSettings{}
|
||||
for _, settingsEntry := range settingsEntries {
|
||||
err := settings.FromSystemSettingsEntry(&settingsEntry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// testing
|
||||
// SaveSystemSettings will update save the settings to the database.
|
||||
func (gr *GormRepository) SaveSystemSettings(ctx context.Context, newSettings *models.SystemSettings) error {
|
||||
|
||||
//retrieve current settings from the database
|
||||
currentSettingsEntries := []models.SystemSettingEntry{}
|
||||
|
||||
if err := gr.GormClient.
|
||||
WithContext(ctx).
|
||||
Find(¤tSettingsEntries).Error; err != nil {
|
||||
return fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
//update settingsEntries
|
||||
|
||||
newSettingsEntries, err := newSettings.ToSystemSettingsEntry(currentSettingsEntries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("merge new settings with DB: %v", err)
|
||||
}
|
||||
|
||||
for ndx, settingsEntry := range newSettingsEntries {
|
||||
|
||||
var upsertErr error
|
||||
if settingsEntry.ID == uuid.Nil {
|
||||
//create new entry
|
||||
upsertErr = gr.GormClient.
|
||||
WithContext(ctx).
|
||||
Model(&models.SystemSettingEntry{}).
|
||||
Create(&settingsEntry).Error
|
||||
} else {
|
||||
// store in database.
|
||||
upsertErr = gr.GormClient.
|
||||
WithContext(ctx).
|
||||
Model(&models.SystemSettingEntry{}).
|
||||
Where([]uuid.UUID{settingsEntry.ID}).
|
||||
Select("setting_value_numeric", "setting_value_string", "setting_value_bool", "setting_value_array").
|
||||
Updates(newSettingsEntries[ndx]).Error
|
||||
}
|
||||
|
||||
if upsertErr != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
|
||||
func (gr *GormRepository) LoadUserSettings(ctx context.Context) (*models.UserSettings, error) {
|
||||
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
|
||||
|
|
|
@ -53,6 +53,8 @@ type DatabaseRepository interface {
|
|||
ListBackgroundJobs(ctx context.Context, queryOptions models.BackgroundJobQueryOptions) ([]models.BackgroundJob, error)
|
||||
|
||||
//settings
|
||||
LoadSystemSettings(ctx context.Context) (*models.SystemSettings, error)
|
||||
SaveSystemSettings(ctx context.Context, newSettings *models.SystemSettings) error
|
||||
LoadUserSettings(ctx context.Context) (*models.UserSettings, error)
|
||||
SaveUserSettings(context.Context, *models.UserSettings) error
|
||||
PopulateDefaultUserSettings(ctx context.Context, userId uuid.UUID) error
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package _20240208112210
|
||||
|
||||
import "github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||
|
||||
// SystemSettingEntry matches a setting row in the database
|
||||
type SystemSettingEntry struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
models.ModelBase
|
||||
|
||||
SettingKeyName string `json:"setting_key_name" gorm:"not null;index:,unique,composite:system_setting_key_name"`
|
||||
SettingKeyDescription string `json:"setting_key_description"`
|
||||
SettingDataType string `json:"setting_data_type"`
|
||||
|
||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
||||
SettingValueString string `json:"setting_value_string"`
|
||||
SettingValueBool bool `json:"setting_value_bool"`
|
||||
SettingValueArray []string `json:"setting_value_array" gorm:"column:setting_value_array;type:text;serializer:json"`
|
||||
}
|
||||
|
||||
func (s SystemSettingEntry) TableName() string {
|
||||
return "system_settings"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
type InstallationRegistrationRequest struct {
|
||||
// AdministratorEmail specifies email address for the administrator of the installation
|
||||
AdministratorEmail string `json:"administrator_email,omitempty"` //opt-in
|
||||
|
||||
SoftwareArchitecture string `json:"software_architecture,omitempty"`
|
||||
SoftwareOS string `json:"software_os,omitempty"`
|
||||
|
||||
FastenDesktopVersion string `json:"fasten_desktop_version,omitempty"`
|
||||
FastenOnpremVersion string `json:"fasten_onprem_version,omitempty"`
|
||||
FastenSourcesVersion string `json:"fasten_sources_version,omitempty"`
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||
"time"
|
||||
)
|
||||
|
||||
type InstallationRegistrationResponse struct {
|
||||
// InstallationID specifies client identifier string. REQUIRED
|
||||
InstallationID string `json:"installation_id"`
|
||||
|
||||
// InstallationSecret specifies client secret string. OPTIONAL
|
||||
InstallationSecret string `json:"installation_secret"`
|
||||
|
||||
// InstallationIDIssuedAt specifies time at which the client identifier was issued. OPTIONAL
|
||||
InstallationIDIssuedAt time.Time `json:"installation_id_issued_at"`
|
||||
|
||||
VerificationStatus pkg.InstallationVerificationStatus `json:"verification_status"`
|
||||
QuotaStatus pkg.InstallationQuotaStatus `json:"quota_status"`
|
||||
|
||||
*InstallationRegistrationRequest `json:",inline"`
|
||||
}
|
|
@ -5,3 +5,9 @@ type ResponseWrapper struct {
|
|||
Error string `json:"error"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type ResponseWrapperTyped[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package models
|
||||
|
||||
// SystemSettingEntry matches a setting row in the database
|
||||
type SystemSettingEntry struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
ModelBase
|
||||
|
||||
SettingKeyName string `json:"setting_key_name" gorm:"not null;index:,unique,composite:system_setting_key_name"`
|
||||
SettingKeyDescription string `json:"setting_key_description"`
|
||||
SettingDataType string `json:"setting_data_type"`
|
||||
|
||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
||||
SettingValueString string `json:"setting_value_string"`
|
||||
SettingValueBool bool `json:"setting_value_bool"`
|
||||
SettingValueArray []string `json:"setting_value_array" gorm:"column:setting_value_array;type:text;serializer:json"`
|
||||
}
|
||||
|
||||
func (s SystemSettingEntry) TableName() string {
|
||||
return "system_settings"
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type SystemSettings struct {
|
||||
InstallationID string `json:"installation_id"`
|
||||
InstallationSecret string `json:"installation_secret"`
|
||||
}
|
||||
|
||||
// see https://gist.github.com/lelandbatey/a5c957b537bed39d1d6fb202c3b8de06
|
||||
func (s *SystemSettings) FromSystemSettingsEntry(entry *SystemSettingEntry) error {
|
||||
|
||||
structType := reflect.ValueOf(s).Elem()
|
||||
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
typeField := structType.Type().Field(i)
|
||||
|
||||
if jsonTagValue := typeField.Tag.Get("json"); jsonTagValue == entry.SettingKeyName {
|
||||
//fmt.Println("found field", field.Name)
|
||||
if entry.SettingDataType == "numeric" {
|
||||
structType.Field(i).SetInt(int64(entry.SettingValueNumeric))
|
||||
} else if entry.SettingDataType == "string" {
|
||||
structType.Field(i).SetString(entry.SettingValueString)
|
||||
} else if entry.SettingDataType == "bool" {
|
||||
structType.Field(i).SetBool(entry.SettingValueBool)
|
||||
} else if entry.SettingDataType == "array" {
|
||||
structType.Field(i).Set(reflect.ValueOf(entry.SettingValueArray))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//if entry.SettingKeyName == "dashboard_locations" {
|
||||
// s.DashboardLocations = entry.SettingValueArray
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SystemSettings) ToSystemSettingsEntry(entries []SystemSettingEntry) ([]SystemSettingEntry, error) {
|
||||
|
||||
structType := reflect.ValueOf(s).Elem()
|
||||
|
||||
fieldNameNdxLookup := map[string]int{}
|
||||
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
typeField := structType.Type().Field(i)
|
||||
jsonTagValue := typeField.Tag.Get("json")
|
||||
fieldNameNdxLookup[jsonTagValue] = i
|
||||
}
|
||||
|
||||
for ndx, entry := range entries {
|
||||
fieldId := fieldNameNdxLookup[entry.SettingKeyName]
|
||||
|
||||
if entry.SettingDataType == "numeric" {
|
||||
entries[ndx].SettingValueNumeric = int(structType.Field(fieldId).Int())
|
||||
} else if entry.SettingDataType == "string" {
|
||||
entries[ndx].SettingValueString = structType.Field(fieldId).String()
|
||||
} else if entry.SettingDataType == "bool" {
|
||||
entries[ndx].SettingValueBool = structType.Field(fieldId).Bool()
|
||||
} else if entry.SettingDataType == "array" {
|
||||
sliceVal := structType.Field(fieldId).Slice(0, structType.Field(fieldId).Len())
|
||||
|
||||
entries[ndx].SettingValueArray = sliceVal.Interface().([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFromSystemSettingsEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
systemSettings := new(SystemSettings)
|
||||
systemSettingsEntry := SystemSettingEntry{
|
||||
SettingKeyName: "installation_id",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "12345",
|
||||
}
|
||||
|
||||
//test
|
||||
err := systemSettings.FromSystemSettingsEntry(&systemSettingsEntry)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "12345", systemSettings.InstallationID)
|
||||
require.Equal(t, "", systemSettings.InstallationSecret)
|
||||
}
|
||||
|
||||
func TestToSystemSettingsEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
systemSettings := new(SystemSettings)
|
||||
previousSystemSettingsEntries := []SystemSettingEntry{{
|
||||
ModelBase: ModelBase{
|
||||
ID: uuid.MustParse("73057947-af24-4739-a4af-ca3496f85b76"),
|
||||
},
|
||||
SettingKeyName: "installation_id",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "4567",
|
||||
}}
|
||||
|
||||
//test
|
||||
systemSettings.InstallationID = "9876"
|
||||
updatedSystemSettingsEntries, err := systemSettings.ToSystemSettingsEntry(previousSystemSettingsEntries)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []SystemSettingEntry{{
|
||||
ModelBase: ModelBase{
|
||||
ID: uuid.MustParse("73057947-af24-4739-a4af-ca3496f85b76"),
|
||||
},
|
||||
SettingKeyName: "installation_id",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "9876",
|
||||
}}, updatedSystemSettingsEntries)
|
||||
}
|
|
@ -21,7 +21,7 @@ func (s *UserSettings) FromUserSettingsEntry(entry *UserSettingEntry) error {
|
|||
if entry.SettingDataType == "numeric" {
|
||||
structType.Field(i).SetInt(int64(entry.SettingValueNumeric))
|
||||
} else if entry.SettingDataType == "string" {
|
||||
structType.Elem().Field(i).SetString(entry.SettingValueString)
|
||||
structType.Field(i).SetString(entry.SettingValueString)
|
||||
} else if entry.SettingDataType == "bool" {
|
||||
structType.Field(i).SetBool(entry.SettingValueBool)
|
||||
} else if entry.SettingDataType == "array" {
|
||||
|
@ -55,7 +55,7 @@ func (s *UserSettings) ToUserSettingsEntry(entries []UserSettingEntry) ([]UserSe
|
|||
if entry.SettingDataType == "numeric" {
|
||||
entries[ndx].SettingValueNumeric = int(structType.Field(fieldId).Int())
|
||||
} else if entry.SettingDataType == "string" {
|
||||
entries[ndx].SettingValueString = structType.Elem().Field(fieldId).String()
|
||||
entries[ndx].SettingValueString = structType.Field(fieldId).String()
|
||||
} else if entry.SettingDataType == "bool" {
|
||||
entries[ndx].SettingValueBool = structType.Field(fieldId).Bool()
|
||||
} else if entry.SettingDataType == "array" {
|
||||
|
|
|
@ -2,20 +2,11 @@ package middleware
|
|||
|
||||
import (
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldLogger, eventBus event_bus.Interface) gin.HandlerFunc {
|
||||
|
||||
deviceRepo, err := database.NewRepository(appConfig, globalLogger, eventBus)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
func RepositoryMiddleware(deviceRepo database.DatabaseRepository) gin.HandlerFunc {
|
||||
//TODO: determine where we can call defer deviceRepo.Close()
|
||||
return func(c *gin.Context) {
|
||||
c.Set(pkg.ContextKeyTypeDatabase, deviceRepo)
|
||||
|
|
|
@ -1,31 +1,46 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/web/handler"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/web/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AppEngine struct {
|
||||
Config config.Interface
|
||||
Logger *logrus.Entry
|
||||
EventBus event_bus.Interface
|
||||
Config config.Interface
|
||||
Logger *logrus.Entry
|
||||
EventBus event_bus.Interface
|
||||
deviceRepo database.DatabaseRepository
|
||||
|
||||
RelatedVersions map[string]string //related versions metadata provided & embedded by the build process
|
||||
}
|
||||
|
||||
func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
|
||||
r := gin.New()
|
||||
|
||||
//setup database
|
||||
deviceRepo, err := database.NewRepository(ae.Config, ae.Logger, ae.EventBus)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ae.deviceRepo = deviceRepo
|
||||
|
||||
r.Use(middleware.LoggerMiddleware(ae.Logger))
|
||||
r.Use(middleware.RepositoryMiddleware(ae.Config, ae.Logger, ae.EventBus))
|
||||
r.Use(middleware.RepositoryMiddleware(ae.deviceRepo))
|
||||
r.Use(middleware.ConfigMiddleware(ae.Config))
|
||||
r.Use(middleware.EventBusMiddleware(ae.EventBus))
|
||||
r.Use(gin.Recovery())
|
||||
|
@ -172,6 +187,81 @@ func (ae *AppEngine) SetupEmbeddedFrontendRouting(embeddedAssetsFS embed.FS, bas
|
|||
return router
|
||||
}
|
||||
|
||||
func (ae *AppEngine) SetupInstallationRegistration() error {
|
||||
//check if installation is already registered
|
||||
systemSettings, err := ae.deviceRepo.LoadSystemSettings(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while loading system settings: %s", err)
|
||||
}
|
||||
|
||||
if systemSettings.InstallationID != "" && systemSettings.InstallationSecret != "" {
|
||||
//already setup, exit
|
||||
//TODO: future, update fasten-onprem, fasten-sources version
|
||||
return nil
|
||||
}
|
||||
|
||||
//setup the installation registration payload
|
||||
registrationData := &models.InstallationRegistrationRequest{
|
||||
SoftwareArchitecture: runtime.GOARCH,
|
||||
SoftwareOS: runtime.GOOS,
|
||||
}
|
||||
|
||||
if ae.RelatedVersions != nil {
|
||||
if fastenSourcesVersion, fastenSourcesVersionOk := ae.RelatedVersions["sources"]; fastenSourcesVersionOk {
|
||||
registrationData.FastenSourcesVersion = fastenSourcesVersion
|
||||
}
|
||||
if fastenOnpremVersion, fastenOnpremVersionOk := ae.RelatedVersions["onprem"]; fastenOnpremVersionOk {
|
||||
registrationData.FastenOnpremVersion = fastenOnpremVersion
|
||||
}
|
||||
if fastenDesktopVersion, fastenDesktopVersionOk := ae.RelatedVersions["desktop"]; fastenDesktopVersionOk {
|
||||
registrationData.FastenDesktopVersion = fastenDesktopVersion
|
||||
}
|
||||
}
|
||||
|
||||
//setup the http request
|
||||
registrationDataJson, err := json.Marshal(registrationData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while serializing installation registration data: %s", err)
|
||||
}
|
||||
|
||||
//send the registration request
|
||||
resp, err := http.Post(
|
||||
"https://api.platform.fastenhealth.com/v1/installation/register",
|
||||
"application/json",
|
||||
bytes.NewBuffer(registrationDataJson),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while sending installation registration request: %s", err)
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("an error occurred while sending installation registration request: %s", resp.Status)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
//unmarshal the registration response
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while reading installation registration response: %s", err)
|
||||
}
|
||||
var registrationResponse models.ResponseWrapperTyped[models.InstallationRegistrationResponse]
|
||||
err = json.Unmarshal(bodyBytes, ®istrationResponse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while unmarshalling installation registration response: %s", err)
|
||||
}
|
||||
|
||||
//now that we have the registration response, store the registration data in the system settings
|
||||
systemSettings.InstallationID = registrationResponse.Data.InstallationID
|
||||
systemSettings.InstallationSecret = registrationResponse.Data.InstallationSecret
|
||||
|
||||
ae.Logger.Infof("Saving installation id to settings table: %s", systemSettings.InstallationID)
|
||||
|
||||
//save the system settings
|
||||
err = ae.deviceRepo.SaveSystemSettings(context.Background(), systemSettings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while saving system settings: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ae *AppEngine) Start() error {
|
||||
//set the gin mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
@ -180,6 +270,10 @@ func (ae *AppEngine) Start() error {
|
|||
}
|
||||
|
||||
baseRouterGroup, ginRouter := ae.Setup()
|
||||
err := ae.SetupInstallationRegistration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter)
|
||||
|
||||
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")))
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
//go:embed related_versions.json
|
||||
var relatedVersionsJson string
|
||||
|
||||
func GetRelatedVersions() (map[string]string, error) {
|
||||
var relatedVersions map[string]string
|
||||
err := json.Unmarshal([]byte(relatedVersionsJson), &relatedVersions)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return relatedVersions, nil
|
||||
}
|
|
@ -29,4 +29,6 @@ log:
|
|||
level: INFO
|
||||
jwt:
|
||||
issuer:
|
||||
# you should ABSOLUTELY change this value before deploying Fasten.
|
||||
# TODO: in future versions, this will be generated on first run with a random value, and stored as a System Setting.
|
||||
key: "thisismysupersecuressessionsecretlength"
|
||||
|
|
1
go.mod
1
go.mod
|
@ -32,6 +32,7 @@ require (
|
|||
github.com/urfave/cli/v2 v2.11.2
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
|
||||
golang.org/x/mod v0.15.0
|
||||
golang.org/x/net v0.17.0
|
||||
gorm.io/datatypes v1.0.7
|
||||
gorm.io/driver/sqlite v1.5.4
|
||||
|
|
2
go.sum
2
go.sum
|
@ -497,6 +497,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
//go:generate go run related_versions.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/version"
|
||||
"golang.org/x/mod/modfile"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const relativeToRepoRoot = "../../"
|
||||
|
||||
func main() {
|
||||
log.Printf("generating related_versions.json file...")
|
||||
relatedVersions, err := getRelatedVersions()
|
||||
if err != nil {
|
||||
log.Fatalf("could not get version info: %s", err)
|
||||
}
|
||||
|
||||
//remove fasten- prefix from keys
|
||||
newRelatedVersion := map[string]string{}
|
||||
for k, v := range relatedVersions {
|
||||
newRelatedVersion[strings.TrimPrefix(k, "fasten-")] = v
|
||||
}
|
||||
|
||||
relatedVersionsJson, err := json.Marshal(newRelatedVersion)
|
||||
if err != nil {
|
||||
log.Fatalf("could not write related version info: %s", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(relativeToRepoRoot, "backend/resources", "related_versions.json"), relatedVersionsJson, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("could not write version info json: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getRelatedVersions() (map[string]string, error) {
|
||||
goModBytes, err := ioutil.ReadFile(filepath.Join(relativeToRepoRoot, "go.mod"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read go.mod file: %w", err)
|
||||
}
|
||||
|
||||
modFile, err := modfile.Parse("go.mod", goModBytes, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse go.mod file: %w", err)
|
||||
}
|
||||
|
||||
fastenOnpremVersion := version.VERSION
|
||||
fastenSourcesVersion := findDependencyVersion("github.com/fastenhealth/fasten-sources", modFile)
|
||||
|
||||
return map[string]string{
|
||||
"fasten-onprem": fastenOnpremVersion,
|
||||
"fasten-sources": fastenSourcesVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func findDependencyVersion(modulePath string, modFile *modfile.File) string {
|
||||
//check replacements first by iterating through the replace statements
|
||||
for _, replace := range modFile.Replace {
|
||||
if replace.Old.Path == modulePath {
|
||||
if len(replace.New.Version) > 0 {
|
||||
return replace.New.Version
|
||||
} else {
|
||||
relativeNewPath := filepath.Join(relativeToRepoRoot, replace.New.Path)
|
||||
|
||||
log.Printf("Attempting to get git version for %s", relativeNewPath)
|
||||
//replace.New.Path is a relative path to the dependency directory
|
||||
//use git describe --tags command
|
||||
gitCommand := exec.Command("git", "describe", "--tags")
|
||||
gitCommand.Dir = relativeNewPath
|
||||
gitCommandOutput := new(strings.Builder)
|
||||
gitCommand.Stdout = gitCommandOutput
|
||||
gitCommand.Run()
|
||||
return strings.TrimSpace(gitCommandOutput.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//find modulePath dependency in modFile Require
|
||||
for _, require := range modFile.Require {
|
||||
if require.Mod.Path == modulePath {
|
||||
//strip "v" prefix from version
|
||||
if strings.HasPrefix(require.Mod.Version, "v") {
|
||||
return require.Mod.Version[1:]
|
||||
} else {
|
||||
return require.Mod.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
Loading…
Reference in New Issue