diff --git a/.gitignore b/.gitignore index c8df5ebd..fd950fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,5 @@ fasten-*.db-wal fasten.db fasten.db-shm fasten.db-wal + +backend/resources/related_versions.json diff --git a/Makefile b/Makefile index 4cefe17c..1027a3be 100644 --- a/Makefile +++ b/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 diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index 86c4fdf5..8c5d1088 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -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() }, diff --git a/backend/pkg/constants.go b/backend/pkg/constants.go index 6d056e26..8062ffe0 100644 --- a/backend/pkg/constants.go +++ b/backend/pkg/constants.go @@ -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" ) diff --git a/backend/pkg/database/gorm_repository_migrations.go b/backend/pkg/database/gorm_repository_migrations.go index ec3f7c2c..5d304dea 100644 --- a/backend/pkg/database/gorm_repository_migrations.go +++ b/backend/pkg/database/gorm_repository_migrations.go @@ -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 diff --git a/backend/pkg/database/gorm_repository_settings.go b/backend/pkg/database/gorm_repository_settings.go index e9f3eb8a..edd97218 100644 --- a/backend/pkg/database/gorm_repository_settings.go +++ b/backend/pkg/database/gorm_repository_settings.go @@ -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) diff --git a/backend/pkg/database/interface.go b/backend/pkg/database/interface.go index 2a6109e5..2564ff61 100644 --- a/backend/pkg/database/interface.go +++ b/backend/pkg/database/interface.go @@ -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 diff --git a/backend/pkg/database/migrations/20240208112210/system_setting_entry.go b/backend/pkg/database/migrations/20240208112210/system_setting_entry.go new file mode 100644 index 00000000..a59e8202 --- /dev/null +++ b/backend/pkg/database/migrations/20240208112210/system_setting_entry.go @@ -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" +} diff --git a/backend/pkg/models/installation_registration_request.go b/backend/pkg/models/installation_registration_request.go new file mode 100644 index 00000000..a7ced60c --- /dev/null +++ b/backend/pkg/models/installation_registration_request.go @@ -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"` +} diff --git a/backend/pkg/models/installation_registration_response.go b/backend/pkg/models/installation_registration_response.go new file mode 100644 index 00000000..8e462d94 --- /dev/null +++ b/backend/pkg/models/installation_registration_response.go @@ -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"` +} diff --git a/backend/pkg/models/response_wrapper.go b/backend/pkg/models/response_wrapper.go index e5d076ce..14a74df0 100644 --- a/backend/pkg/models/response_wrapper.go +++ b/backend/pkg/models/response_wrapper.go @@ -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"` +} diff --git a/backend/pkg/models/system_setting_entry.go b/backend/pkg/models/system_setting_entry.go new file mode 100644 index 00000000..f2ee9544 --- /dev/null +++ b/backend/pkg/models/system_setting_entry.go @@ -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" +} diff --git a/backend/pkg/models/system_settings.go b/backend/pkg/models/system_settings.go new file mode 100644 index 00000000..569514e4 --- /dev/null +++ b/backend/pkg/models/system_settings.go @@ -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 +} diff --git a/backend/pkg/models/system_settings_test.go b/backend/pkg/models/system_settings_test.go new file mode 100644 index 00000000..a4ef201f --- /dev/null +++ b/backend/pkg/models/system_settings_test.go @@ -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) +} diff --git a/backend/pkg/models/user_settings.go b/backend/pkg/models/user_settings.go index 90025ef9..c5f84716 100644 --- a/backend/pkg/models/user_settings.go +++ b/backend/pkg/models/user_settings.go @@ -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" { diff --git a/backend/pkg/web/middleware/database.go b/backend/pkg/web/middleware/database.go index 757d81f4..1af8a558 100644 --- a/backend/pkg/web/middleware/database.go +++ b/backend/pkg/web/middleware/database.go @@ -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) diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 9c55eb82..de7160d4 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -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"))) diff --git a/backend/resources/related_versions.go b/backend/resources/related_versions.go new file mode 100644 index 00000000..4053b590 --- /dev/null +++ b/backend/resources/related_versions.go @@ -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 +} diff --git a/config.yaml b/config.yaml index 14497a20..35038274 100644 --- a/config.yaml +++ b/config.yaml @@ -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" diff --git a/go.mod b/go.mod index df2f3fb1..b4c1cc78 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index ea443cb0..76243a3d 100644 --- a/go.sum +++ b/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= diff --git a/scripts/related_versions/related_versions.go b/scripts/related_versions/related_versions.go new file mode 100644 index 00000000..71669db2 --- /dev/null +++ b/scripts/related_versions/related_versions.go @@ -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" +}