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
|
||||||
fasten.db-shm
|
fasten.db-shm
|
||||||
fasten.db-wal
|
fasten.db-wal
|
||||||
|
|
||||||
|
backend/resources/related_versions.json
|
||||||
|
|
1
Makefile
1
Makefile
|
@ -39,6 +39,7 @@ clean-backend:
|
||||||
.PHONY: dep-backend
|
.PHONY: dep-backend
|
||||||
dep-backend:
|
dep-backend:
|
||||||
go mod tidy && go mod vendor
|
go mod tidy && go mod vendor
|
||||||
|
cd scripts && go generate ./...
|
||||||
|
|
||||||
|
|
||||||
.PHONY: test-backend
|
.PHONY: test-backend
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/version"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/version"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/web"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/web"
|
||||||
|
"github.com/fastenhealth/fasten-onprem/backend/resources"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"io"
|
"io"
|
||||||
|
@ -114,10 +115,13 @@ func main() {
|
||||||
settingsData, err := json.Marshal(appconfig.AllSettings())
|
settingsData, err := json.Marshal(appconfig.AllSettings())
|
||||||
appLogger.Debug(string(settingsData), err)
|
appLogger.Debug(string(settingsData), err)
|
||||||
|
|
||||||
|
relatedVersions, _ := resources.GetRelatedVersions()
|
||||||
|
|
||||||
webServer := web.AppEngine{
|
webServer := web.AppEngine{
|
||||||
Config: appconfig,
|
Config: appconfig,
|
||||||
Logger: appLogger,
|
Logger: appLogger,
|
||||||
EventBus: event_bus.NewEventBusServer(appLogger),
|
EventBus: event_bus.NewEventBusServer(appLogger),
|
||||||
|
RelatedVersions: relatedVersions,
|
||||||
}
|
}
|
||||||
return webServer.Start()
|
return webServer.Start()
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,9 @@ type BackgroundJobSchedule string
|
||||||
|
|
||||||
type DatabaseRepositoryType string
|
type DatabaseRepositoryType string
|
||||||
|
|
||||||
|
type InstallationVerificationStatus string
|
||||||
|
type InstallationQuotaStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceListPageSize int = 20
|
ResourceListPageSize int = 20
|
||||||
|
|
||||||
|
@ -41,4 +44,10 @@ const (
|
||||||
|
|
||||||
DatabaseRepositoryTypeSqlite DatabaseRepositoryType = "sqlite"
|
DatabaseRepositoryTypeSqlite DatabaseRepositoryType = "sqlite"
|
||||||
DatabaseRepositoryTypePostgres DatabaseRepositoryType = "postgres"
|
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"
|
_20231201122541 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20231201122541"
|
||||||
_0240114092806 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114092806"
|
_0240114092806 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114092806"
|
||||||
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
|
_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"
|
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
|
||||||
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
|
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
|
||||||
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
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
|
// run when database is empty
|
||||||
|
|
|
@ -3,11 +3,77 @@ package database
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||||
"github.com/google/uuid"
|
"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
|
// 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) {
|
func (gr *GormRepository) LoadUserSettings(ctx context.Context) (*models.UserSettings, error) {
|
||||||
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
|
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
|
||||||
|
|
|
@ -53,6 +53,8 @@ type DatabaseRepository interface {
|
||||||
ListBackgroundJobs(ctx context.Context, queryOptions models.BackgroundJobQueryOptions) ([]models.BackgroundJob, error)
|
ListBackgroundJobs(ctx context.Context, queryOptions models.BackgroundJobQueryOptions) ([]models.BackgroundJob, error)
|
||||||
|
|
||||||
//settings
|
//settings
|
||||||
|
LoadSystemSettings(ctx context.Context) (*models.SystemSettings, error)
|
||||||
|
SaveSystemSettings(ctx context.Context, newSettings *models.SystemSettings) error
|
||||||
LoadUserSettings(ctx context.Context) (*models.UserSettings, error)
|
LoadUserSettings(ctx context.Context) (*models.UserSettings, error)
|
||||||
SaveUserSettings(context.Context, *models.UserSettings) error
|
SaveUserSettings(context.Context, *models.UserSettings) error
|
||||||
PopulateDefaultUserSettings(ctx context.Context, userId uuid.UUID) 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"`
|
Error string `json:"error"`
|
||||||
Data interface{} `json:"data"`
|
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" {
|
if entry.SettingDataType == "numeric" {
|
||||||
structType.Field(i).SetInt(int64(entry.SettingValueNumeric))
|
structType.Field(i).SetInt(int64(entry.SettingValueNumeric))
|
||||||
} else if entry.SettingDataType == "string" {
|
} else if entry.SettingDataType == "string" {
|
||||||
structType.Elem().Field(i).SetString(entry.SettingValueString)
|
structType.Field(i).SetString(entry.SettingValueString)
|
||||||
} else if entry.SettingDataType == "bool" {
|
} else if entry.SettingDataType == "bool" {
|
||||||
structType.Field(i).SetBool(entry.SettingValueBool)
|
structType.Field(i).SetBool(entry.SettingValueBool)
|
||||||
} else if entry.SettingDataType == "array" {
|
} else if entry.SettingDataType == "array" {
|
||||||
|
@ -55,7 +55,7 @@ func (s *UserSettings) ToUserSettingsEntry(entries []UserSettingEntry) ([]UserSe
|
||||||
if entry.SettingDataType == "numeric" {
|
if entry.SettingDataType == "numeric" {
|
||||||
entries[ndx].SettingValueNumeric = int(structType.Field(fieldId).Int())
|
entries[ndx].SettingValueNumeric = int(structType.Field(fieldId).Int())
|
||||||
} else if entry.SettingDataType == "string" {
|
} 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" {
|
} else if entry.SettingDataType == "bool" {
|
||||||
entries[ndx].SettingValueBool = structType.Field(fieldId).Bool()
|
entries[ndx].SettingValueBool = structType.Field(fieldId).Bool()
|
||||||
} else if entry.SettingDataType == "array" {
|
} else if entry.SettingDataType == "array" {
|
||||||
|
|
|
@ -2,20 +2,11 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
"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/database"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldLogger, eventBus event_bus.Interface) gin.HandlerFunc {
|
func RepositoryMiddleware(deviceRepo database.DatabaseRepository) gin.HandlerFunc {
|
||||||
|
|
||||||
deviceRepo, err := database.NewRepository(appConfig, globalLogger, eventBus)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: determine where we can call defer deviceRepo.Close()
|
//TODO: determine where we can call defer deviceRepo.Close()
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Set(pkg.ContextKeyTypeDatabase, deviceRepo)
|
c.Set(pkg.ContextKeyTypeDatabase, deviceRepo)
|
||||||
|
|
|
@ -1,31 +1,46 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
|
"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/event_bus"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
"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/handler"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/web/middleware"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/web/middleware"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppEngine struct {
|
type AppEngine struct {
|
||||||
Config config.Interface
|
Config config.Interface
|
||||||
Logger *logrus.Entry
|
Logger *logrus.Entry
|
||||||
EventBus event_bus.Interface
|
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) {
|
func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
|
||||||
r := gin.New()
|
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.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.ConfigMiddleware(ae.Config))
|
||||||
r.Use(middleware.EventBusMiddleware(ae.EventBus))
|
r.Use(middleware.EventBusMiddleware(ae.EventBus))
|
||||||
r.Use(gin.Recovery())
|
r.Use(gin.Recovery())
|
||||||
|
@ -172,6 +187,81 @@ func (ae *AppEngine) SetupEmbeddedFrontendRouting(embeddedAssetsFS embed.FS, bas
|
||||||
return router
|
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 {
|
func (ae *AppEngine) Start() error {
|
||||||
//set the gin mode
|
//set the gin mode
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
@ -180,6 +270,10 @@ func (ae *AppEngine) Start() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
baseRouterGroup, ginRouter := ae.Setup()
|
baseRouterGroup, ginRouter := ae.Setup()
|
||||||
|
err := ae.SetupInstallationRegistration()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter)
|
r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter)
|
||||||
|
|
||||||
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")))
|
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
|
level: INFO
|
||||||
jwt:
|
jwt:
|
||||||
issuer:
|
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"
|
key: "thisismysupersecuressessionsecretlength"
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -32,6 +32,7 @@ require (
|
||||||
github.com/urfave/cli/v2 v2.11.2
|
github.com/urfave/cli/v2 v2.11.2
|
||||||
golang.org/x/crypto v0.14.0
|
golang.org/x/crypto v0.14.0
|
||||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
|
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
|
||||||
|
golang.org/x/mod v0.15.0
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.17.0
|
||||||
gorm.io/datatypes v1.0.7
|
gorm.io/datatypes v1.0.7
|
||||||
gorm.io/driver/sqlite v1.5.4
|
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.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.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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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