From bc53bde83f31bd83b93d0784a4d91d8ca5e73191 Mon Sep 17 00:00:00 2001 From: "Mihai Pop @ TSA" Date: Wed, 23 Jul 2025 13:10:42 +0300 Subject: [PATCH 01/73] add placeholder stylus version --- frontend/package.json | 3 ++- frontend/yarn.lock | 27 ++++++++------------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index fdd96973f..60091cc29 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -97,6 +97,7 @@ "typescript": "~4.6.4" }, "resolutions": { - "webpack": "5.74.0" + "webpack": "5.74.0", + "stylus": "0.0.1-security" } } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1b10c2d5a..359135aca 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"@adobe/css-tools@^4.0.1": - version "4.1.0" - resolved "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.1.0.tgz#417fef4a143f4396ad0b3b4351fee21323f15aa8" - integrity sha512-mMVJ/j/GbZ/De4ZHWbQAQO1J6iVnjtZLc9WEdkUQb8S/Bu2cAF2bETXUgMAdvMG3/ngtKmcNBe+Zms9bg6jnQQ== - "@aduh95/viz.js@^3.1.0": version "3.7.0" resolved "https://registry.npmjs.org/@aduh95/viz.js/-/viz.js-3.7.0.tgz#a20d86c5fc8f6abebdc39b96a4326e10375d77c0" @@ -5597,7 +5592,7 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -7151,7 +7146,7 @@ glob@8.0.3: minimatch "^5.0.1" once "^1.3.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7, glob@^7.2.0: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -11082,7 +11077,7 @@ saucelabs@^1.5.0: dependencies: https-proxy-agent "^2.2.1" -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -11467,7 +11462,7 @@ source-map@0.7.3: resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -source-map@0.7.4, source-map@^0.7.3: +source-map@0.7.4: version "0.7.4" resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== @@ -11746,16 +11741,10 @@ stylus-loader@7.0.0: klona "^2.0.5" normalize-path "^3.0.0" -stylus@0.59.0: - version "0.59.0" - resolved "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz#a344d5932787142a141946536d6e24e6a6be7aa6" - integrity sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg== - dependencies: - "@adobe/css-tools" "^4.0.1" - debug "^4.3.2" - glob "^7.1.6" - sax "~1.2.4" - source-map "^0.7.3" +stylus@0.0.1-security, stylus@0.59.0: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.0.1-security.tgz#7fbf8df07e6c1202fce29d57fed2ecbf960e6a3b" + integrity sha512-qTLX5NJwuj5dqdJyi1eAjXGE4HfjWDoPLbSIpYWn/rAEdiyZnsngS/Yj0BRjM7wC41e/+spK4QCXFqz7LM0fFQ== supports-color@^2.0.0: version "2.0.0" From 69adce187955aa76abc674420b6494c990ba2233 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Tue, 22 Jul 2025 19:35:36 +0300 Subject: [PATCH 02/73] FO-33 - First version encryption --- backend/cmd/fasten/fasten.go | 62 +++++++++++++++-- backend/pkg/database/sqlite_repository.go | 13 ++-- backend/pkg/encryption/encryption.go | 82 +++++++++++++++++++++++ backend/pkg/web/server.go | 27 ++++---- config.yaml | 5 +- docker-compose.yml | 5 ++ 6 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 backend/pkg/encryption/encryption.go diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index 8c5d1088b..f30d33efd 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -6,6 +6,7 @@ import ( "github.com/analogj/go-util/utils" "github.com/fastenhealth/fasten-onprem/backend/pkg/config" "github.com/fastenhealth/fasten-onprem/backend/pkg/database" + "github.com/fastenhealth/fasten-onprem/backend/pkg/encryption" "github.com/fastenhealth/fasten-onprem/backend/pkg/errors" "github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus" "github.com/fastenhealth/fasten-onprem/backend/pkg/version" @@ -16,6 +17,7 @@ import ( "io" "log" "os" + "path/filepath" "time" ) @@ -112,16 +114,65 @@ func main() { } }() + tokenPath := "/opt/fasten/encrypt_db/token" + dbPath := appconfig.GetString("database.location") + var token string + var tokenJustCreated bool + + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + // Database does not exist, generate a new token + token, err = encryption.GenerateRandomToken(32) + if err != nil { + return fmt.Errorf("failed to generate encryption token: %w", err) + } + + dir := filepath.Dir(tokenPath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("failed to create token directory: %w", err) + } + } + + if err := encryption.SaveTokenToFile(token, tokenPath); err != nil { + return fmt.Errorf("failed to save encryption token: %w", err) + } + tokenJustCreated = true + } else { + // Database exists, load the token + token, err = encryption.LoadTokenFromFile(tokenPath) + if err != nil { + return fmt.Errorf("failed to load encryption token for existing database: %w", err) + } + tokenJustCreated = false + } + + if token == "" { + return fmt.Errorf("failed to get encryption token") + } + + tokenDB := encryption.DeriveKeyFromToken(token) + + appconfig.Set("database.encryption_key", fmt.Sprintf("%x", tokenDB)) + settingsData, err := json.Marshal(appconfig.AllSettings()) appLogger.Debug(string(settingsData), err) relatedVersions, _ := resources.GetRelatedVersions() + dbRepo, err := database.NewRepository(appconfig, appLogger, event_bus.NewEventBusServer(appLogger)) + if err != nil { + return err + } + webServer := web.AppEngine{ - Config: appconfig, - Logger: appLogger, - EventBus: event_bus.NewEventBusServer(appLogger), - RelatedVersions: relatedVersions, + Config: appconfig, + Logger: appLogger, + EventBus: event_bus.NewEventBusServer(appLogger), + DeviceRepo: dbRepo, + RelatedVersions: relatedVersions, + Token: token, + TokenDB: fmt.Sprintf("%x", tokenDB), + TokenJustCreated: tokenJustCreated, } return webServer.Start() }, @@ -218,7 +269,8 @@ func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, error) { "type": "web", }) //set default log level - if level, err := logrus.ParseLevel(appConfig.GetString("log.level")); err == nil { + logLevel := appConfig.GetString("log.level") + if level, err := logrus.ParseLevel(logLevel); err == nil { logger.Logger.SetLevel(level) } else { logger.Logger.SetLevel(logrus.InfoLevel) diff --git a/backend/pkg/database/sqlite_repository.go b/backend/pkg/database/sqlite_repository.go index ff463688d..b9f67ba14 100644 --- a/backend/pkg/database/sqlite_repository.go +++ b/backend/pkg/database/sqlite_repository.go @@ -52,14 +52,19 @@ func newSqliteRepository(appConfig config.Interface, globalLogger logrus.FieldLo "_journal_mode": "WAL", } - //validation of encryption key happens in ValidateConfig method - if appConfig.IsSet("database.encryption.key") { + if appConfig.GetBool("database.encryption.enabled") { + tokenDB := appConfig.GetString("database.encryption_key") + if tokenDB == "" { + return nil, fmt.Errorf("database encryption key is not set") + } + + // Configure sqlcipher pragmaOpts["_cipher"] = "sqlcipher" pragmaOpts["_legacy"] = "3" - pragmaOpts["_hmac_use"] = "off" + pragmaOpts["_hmac_use"] = "on" pragmaOpts["_kdf_iter"] = "4000" pragmaOpts["_legacy_page_size"] = "1024" - pragmaOpts["_key"] = appConfig.GetString("database.encryption.key") + pragmaOpts["_key"] = fmt.Sprintf("x'%s'", tokenDB) } pragmaStr := sqlitePragmaString(pragmaOpts) diff --git a/backend/pkg/encryption/encryption.go b/backend/pkg/encryption/encryption.go new file mode 100644 index 000000000..c18bd87dd --- /dev/null +++ b/backend/pkg/encryption/encryption.go @@ -0,0 +1,82 @@ +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" +) + +// GenerateRandomToken generates a cryptographically secure random token. +func GenerateRandomToken(length int) (string, error) { + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +// DeriveKeyFromToken derives a 32-byte key from a token using SHA-256. +func DeriveKeyFromToken(token string) []byte { + hash := sha256.Sum256([]byte(token)) + return hash[:] +} + +// Encrypt encrypts data using AES-GCM. +func Encrypt(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, data, nil), nil +} + +// Decrypt decrypts data using AES-GCM. +func Decrypt(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + return gcm.Open(nil, nonce, ciphertext, nil) +} + +// SaveTokenToFile saves the token to a file. +func SaveTokenToFile(token, path string) error { + return os.WriteFile(path, []byte(token), 0600) +} + +// LoadTokenFromFile loads the token from a file. +func LoadTokenFromFile(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index f637cceda..ca194ba75 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -26,23 +26,19 @@ type AppEngine struct { Config config.Interface Logger *logrus.Entry EventBus event_bus.Interface - deviceRepo database.DatabaseRepository + DeviceRepo database.DatabaseRepository - RelatedVersions map[string]string //related versions metadata provided & embedded by the build process + RelatedVersions map[string]string //related versions metadata provided & embedded by the build process + Token string + TokenDB string + TokenJustCreated bool } 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.deviceRepo)) + r.Use(middleware.RepositoryMiddleware(ae.DeviceRepo)) r.Use(middleware.ConfigMiddleware(ae.Config)) r.Use(middleware.EventBusMiddleware(ae.EventBus)) r.Use(gin.Recovery()) @@ -215,7 +211,7 @@ func (ae *AppEngine) SetupEmbeddedFrontendRouting(embeddedAssetsFS embed.FS, bas func (ae *AppEngine) SetupInstallationRegistration() error { //check if installation is already registered - systemSettings, err := ae.deviceRepo.LoadSystemSettings(context.Background()) + systemSettings, err := ae.DeviceRepo.LoadSystemSettings(context.Background()) if err != nil { return fmt.Errorf("an error occurred while loading system settings: %s", err) } @@ -281,7 +277,7 @@ func (ae *AppEngine) SetupInstallationRegistration() error { ae.Logger.Infof("Saving installation id to settings table: %s", systemSettings.InstallationID) //save the system settings - err = ae.deviceRepo.SaveSystemSettings(context.Background(), systemSettings) + err = ae.DeviceRepo.SaveSystemSettings(context.Background(), systemSettings) if err != nil { return fmt.Errorf("an error occurred while saving system settings: %s", err) } @@ -302,5 +298,10 @@ func (ae *AppEngine) Start() error { } r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter) - return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port"))) + listenAddr := fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")) + if ae.TokenJustCreated || strings.ToLower(ae.Config.GetString("log.level")) == "debug" { + ae.Logger.Infof("token: %s\ntoken_db: %s", ae.Token, ae.TokenDB) + } + + return r.Run(listenAddr) } diff --git a/config.yaml b/config.yaml index 350382748..3cb1509cd 100644 --- a/config.yaml +++ b/config.yaml @@ -20,13 +20,14 @@ web: frontend: path: /opt/fasten/web database: - # encryption: + encryption: + enabled: true # key: '' type: 'sqlite' # postgres will be supported in the future, but is completely **BROKEN** at the moment. location: '/opt/fasten/db/fasten.db' # if postgres (**BROKEN**) use a DSN, eg. `host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=required TimeZone=Asia/Shanghai` log: file: '' # absolute or relative paths allowed, eg. web.log - level: INFO + level: DEBUG jwt: issuer: # you should ABSOLUTELY change this value before deploying Fasten. diff --git a/docker-compose.yml b/docker-compose.yml index dde19bc7d..71969c0d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,7 @@ version: "3.9" + +volumes: + encrypt_db: services: fasten: # NOTE: only developers need to build Fasten from source @@ -14,4 +17,6 @@ services: - "9090:8080" volumes: - ./db:/opt/fasten/db + - ./encrypt_db:/opt/fasten/encrypt_db + - encrypt_db:/opt/fasten/encrypt_db # - ./config.example.yaml:/opt/fasten/config/config.yaml From b3701da66e148767a2d5e4a54078821105f37c74 Mon Sep 17 00:00:00 2001 From: "Theo@tsa.team" Date: Fri, 25 Jul 2025 14:28:33 +0300 Subject: [PATCH 03/73] refactor: Refactor check token logic on FE and BE [FO-33] --- .gitignore | 1 + backend/pkg/web/handler/setup_token.go | 41 +++++++++++++++++ backend/pkg/web/server.go | 13 ++++++ frontend/src/app/app-routing.module.ts | 3 +- frontend/src/app/app.component.ts | 2 +- frontend/src/app/app.module.ts | 2 + .../show-first-run-wizard-guard.ts | 38 ++++++++++------ .../auth-signup-wizard.component.ts | 16 ++++++- .../setup-token/setup-token.component.html | 21 +++++++++ .../setup-token/setup-token.component.scss | 41 +++++++++++++++++ .../setup-token/setup-token.component.spec.ts | 27 +++++++++++ .../setup-token/setup-token.component.ts | 45 +++++++++++++++++++ .../src/app/services/fasten-api.service.ts | 12 +++++ 13 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 backend/pkg/web/handler/setup_token.go create mode 100644 frontend/src/app/pages/setup-token/setup-token.component.html create mode 100644 frontend/src/app/pages/setup-token/setup-token.component.scss create mode 100644 frontend/src/app/pages/setup-token/setup-token.component.spec.ts create mode 100644 frontend/src/app/pages/setup-token/setup-token.component.ts diff --git a/.gitignore b/.gitignore index e44a5a4a8..b20787b2e 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ fasten.db-wal backend/resources/related_versions.json frontend/documentation.json +encrypt_db \ No newline at end of file diff --git a/backend/pkg/web/handler/setup_token.go b/backend/pkg/web/handler/setup_token.go new file mode 100644 index 000000000..9cc7b797e --- /dev/null +++ b/backend/pkg/web/handler/setup_token.go @@ -0,0 +1,41 @@ +package handler + +import ( + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gin-gonic/gin" +) + +func SetupToken(c *gin.Context) { + tokenPath := "/opt/fasten/encrypt_db/token" + token := c.PostForm("token") + if token == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is required"}) + return + } + + dir := filepath.Dir(tokenPath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0700); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "failed to create token directory"}) + return + } + } + + // NOTE: this is a simplified version of SaveTokenToFile from the encryption package + if err := os.WriteFile(tokenPath, []byte(token), 0600); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "failed to save token"}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true}) + + // Shutdown the server gracefully + go func() { + time.Sleep(1 * time.Second) + os.Exit(0) + }() +} diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index ca194ba75..7eda4fbe1 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "os" "runtime" "strings" @@ -55,6 +56,17 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { // This function does a quick check to see if the server is up and running // it will also determine if we should show the first run wizard + // Check if the encrypt token is present + tokenPath := "/opt/fasten/encrypt_db/token" + // Check if the token file exists + if _, err := os.Stat(tokenPath); os.IsNotExist(err) { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "error": "no_encryption_token", // <-- specific code/message for FE + }) + return + } + //TODO: // check if the /web folder is populated. @@ -92,6 +104,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { api.GET("/glossary/code", handler.GlossarySearchByCode) api.POST("/support/request", handler.SupportRequest) api.POST("/support/healthsystem", handler.HealthSystemRequest) + api.POST("/set-token", handler.SetupToken) secure := api.Group("/secure").Use(middleware.RequireAuth()) { diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index d0e257f15..df6a9460b 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -22,9 +22,10 @@ import { ResourceDetailComponent } from './pages/resource-detail/resource-detail import { SourceDetailComponent } from './pages/source-detail/source-detail.component'; import { UserCreateComponent } from './pages/user-create/user-create.component'; import { UserListComponent } from './pages/user-list/user-list.component'; +import { SetupTokenComponent } from "./pages/setup-token/setup-token.component"; const routes: Routes = [ - + { path: 'setup-token', component: SetupTokenComponent }, { path: 'auth/signup/wizard', component: AuthSignupWizardComponent }, { path: 'auth/signin', component: AuthSigninComponent, canActivate: [ ShowFirstRunWizardGuard] }, diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 9a4a7049d..0596d7187 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -36,7 +36,7 @@ export class AppComponent implements OnInit { routerEvent(event) { if (event instanceof NavigationEnd) { //modify header - if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop')) { + if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop') || event.url?.startsWith('/setup-token')) { this.showHeader = false; } else { this.showHeader = true; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 9efff0168..9c5bdb1b2 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -41,6 +41,7 @@ import { AuthSignupWizardComponent } from './pages/auth-signup-wizard/auth-signu import {ShowFirstRunWizardGuard} from './auth-guards/show-first-run-wizard-guard'; import { IconsModule } from './icon-module'; import { UserListComponent } from './pages/user-list/user-list.component'; +import { SetupTokenComponent } from './pages/setup-token/setup-token.component'; @NgModule({ declarations: [ @@ -62,6 +63,7 @@ import { UserListComponent } from './pages/user-list/user-list.component'; BackgroundJobsComponent, AuthSignupWizardComponent, UserListComponent, + SetupTokenComponent, ], imports: [ FormsModule, diff --git a/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts b/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts index 944dc25cd..9b581fb5a 100644 --- a/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts +++ b/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts @@ -1,27 +1,39 @@ import { Injectable } from '@angular/core'; -import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router} from '@angular/router'; -import {FastenApiService} from '../services/fasten-api.service'; +import { + CanActivate, + ActivatedRouteSnapshot, + RouterStateSnapshot, + UrlTree, + Router, +} from '@angular/router'; +import { FastenApiService } from '../services/fasten-api.service'; @Injectable() export class ShowFirstRunWizardGuard implements CanActivate { - constructor(private fastenService: FastenApiService, private router: Router) { + constructor( + private fastenService: FastenApiService, + private router: Router + ) {} - } - - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + async canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Promise { try { - //check if the server requires the first run wizard to be shown, if not, continue to login/signup - let healthData = await this.fastenService.getHealth().toPromise() + const healthData = await this.fastenService.getHealth().toPromise(); if (healthData.first_run_wizard) { return await this.router.navigate(['/auth/signup/wizard']); } + } catch (e: any) { + if (e?.error?.error === 'no_encryption_token') { + console.warn('No encryption token found. Redirecting to wizard.'); + return await this.router.navigate(['/setup-token']); + } - } catch (e) { - // if there is an error, just ignore it, and continue to the signin/signup page. - console.error("ignoring error:", e) + console.error('ignoring error:', e); } - // continue as normal - return true + + return true; } } diff --git a/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.ts b/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.ts index 1d9255483..c1ca44364 100644 --- a/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.ts +++ b/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.ts @@ -5,6 +5,7 @@ import {AuthService} from '../../services/auth.service'; import {Router} from '@angular/router'; import {ToastService} from '../../services/toast.service'; import {ToastNotification, ToastType} from '../../models/fasten/toast'; +import { FastenApiService } from 'src/app/services/fasten-api.service'; class UserWizard extends User { password_confirm: string = "" @@ -112,6 +113,7 @@ export class AuthSignupWizardComponent implements OnInit { submitted: boolean = false newUser: UserWizard = new UserWizard() errorMsg: string = "" + fastenService: FastenApiService constructor( private authService: AuthService, @@ -119,7 +121,19 @@ export class AuthSignupWizardComponent implements OnInit { private toastService: ToastService ) { } - ngOnInit(): void { + async ngOnInit(): Promise { + try { + // Check if encryption token is set by running the health check + await this.fastenService.getHealth().toPromise(); + } catch (e: any) { + if (e?.error?.error === 'no_encryption_token') { + console.warn('No encryption token found. Redirecting to wizard.'); + await this.router.navigate(['/setup-token']); + return; + } + + console.error('ignoring error:', e); + } } signupSubmit(){ diff --git a/frontend/src/app/pages/setup-token/setup-token.component.html b/frontend/src/app/pages/setup-token/setup-token.component.html new file mode 100644 index 000000000..62f6a1471 --- /dev/null +++ b/frontend/src/app/pages/setup-token/setup-token.component.html @@ -0,0 +1,21 @@ +
+
+

Enter Your Token

+
+ + +
+ Go to Main Page +
+ +
+

Token set successfully!

+

The container has been stopped. Please run the `docker run` command again to start the application in full + mode.

+
+ +
+

Error

+

Failed to set token. Please try again.

+
+
\ No newline at end of file diff --git a/frontend/src/app/pages/setup-token/setup-token.component.scss b/frontend/src/app/pages/setup-token/setup-token.component.scss new file mode 100644 index 000000000..ea83229b4 --- /dev/null +++ b/frontend/src/app/pages/setup-token/setup-token.component.scss @@ -0,0 +1,41 @@ +body { + font-family: sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #f0f2f5; +} + +.container { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +input[type="text"] { + width: 300px; + padding: 0.5rem; + margin-bottom: 1rem; + border: 1px solid #ccc; + border-radius: 4px; +} + +button { + padding: 0.5rem 1rem; + border: none; + background-color: #007bff; + color: white; + border-radius: 4px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} + +a { + display: block; + margin-top: 1rem; +} diff --git a/frontend/src/app/pages/setup-token/setup-token.component.spec.ts b/frontend/src/app/pages/setup-token/setup-token.component.spec.ts new file mode 100644 index 000000000..0d1468dc6 --- /dev/null +++ b/frontend/src/app/pages/setup-token/setup-token.component.spec.ts @@ -0,0 +1,27 @@ +/* tslint:disable:no-unused-variable */ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { SetupTokenComponent } from './setup-token.component'; + +describe('SetupTokenComponent', () => { + let component: SetupTokenComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [SetupTokenComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SetupTokenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/setup-token/setup-token.component.ts b/frontend/src/app/pages/setup-token/setup-token.component.ts new file mode 100644 index 000000000..abf6a61f4 --- /dev/null +++ b/frontend/src/app/pages/setup-token/setup-token.component.ts @@ -0,0 +1,45 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { HttpClient } from '@angular/common/http'; +import { FastenApiService } from 'src/app/services/fasten-api.service'; + +@Component({ + selector: 'app-setup-token', + templateUrl: './setup-token.component.html', + styleUrls: ['./setup-token.component.scss'], +}) +export class SetupTokenComponent implements OnInit { + tokenForm: FormGroup; + isTokenSet = false; + error = false; + + constructor( + private fb: FormBuilder, + private http: HttpClient, + private fastenService: FastenApiService + ) {} + + ngOnInit() { + this.tokenForm = this.fb.group({ + token: ['', Validators.required], + }); + } + + onSubmit() { + if (this.tokenForm.valid) { + const formData = new URLSearchParams(); + //TODO: use a more secure way to handle the token (in this.tokenForm.value.token) + formData.append('token', this.tokenForm.value.token); + this.fastenService.setupToken(formData.toString()).subscribe( + () => { + this.isTokenSet = true; + this.error = false; + }, + () => { + this.isTokenSet = false; + this.error = true; + } + ); + } + } +} diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index 336b4bc92..e620065ff 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -66,6 +66,18 @@ export class FastenApiService { ); } + setupToken(formData: string): Observable { + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + return this._httpClient.post(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/set-token`, formData, { headers: new HttpHeaders(headers) }) + .pipe( + map((response: ResponseWrapper) => { + return response.data + }) + ); + } /* SECURE ENDPOINTS From e6855b18d3096dc041250eabe0297253c6a52bf0 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Sun, 27 Jul 2025 18:19:16 +0300 Subject: [PATCH 04/73] FO-33 - Run server without token --- backend/cmd/fasten/fasten.go | 34 +++++++++++++++++++++++++++++----- backend/pkg/web/server.go | 15 +++++++++++---- config.yaml | 2 +- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index f30d33efd..b36c379c3 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -138,12 +138,36 @@ func main() { } tokenJustCreated = true } else { - // Database exists, load the token - token, err = encryption.LoadTokenFromFile(tokenPath) - if err != nil { - return fmt.Errorf("failed to load encryption token for existing database: %w", err) + // Database exists, check for token + if _, err := os.Stat(tokenPath); os.IsNotExist(err) { + appLogger.Warningf("Database exists but token file is missing from '%s'. The frontend should prompt for the token.", tokenPath) + tokenJustCreated = false + + appconfig.Set("database.encryption_key", "") + + relatedVersions, _ := resources.GetRelatedVersions() + + webServer := web.AppEngine{ + Config: appconfig, + Logger: appLogger, + EventBus: event_bus.NewEventBusServer(appLogger), + DeviceRepo: nil, + RelatedVersions: relatedVersions, + Token: "", + TokenDB: "", + TokenJustCreated: false, + } + + return webServer.Start() + } else { + // DB and token both exist. Load token. + appLogger.Info("Database and token found. Loading token...") + token, err = encryption.LoadTokenFromFile(tokenPath) + if err != nil { + return fmt.Errorf("failed to load encryption token from '%s' for existing database: %w", tokenPath, err) + } + tokenJustCreated = false } - tokenJustCreated = false } if token == "" { diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 7eda4fbe1..f99d9cf17 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -305,15 +305,22 @@ func (ae *AppEngine) Start() error { } baseRouterGroup, ginRouter := ae.Setup() - err := ae.SetupInstallationRegistration() - if err != nil { - return err + if ae.DeviceRepo != nil { + err := ae.SetupInstallationRegistration() + if err != nil { + ae.Logger.Panicf("panic occurred:%v", err) + } + } else { + ae.Logger.Warn("Skipping SetupInstallationRegistration because DeviceRepo is nil") } + r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter) listenAddr := fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")) - if ae.TokenJustCreated || strings.ToLower(ae.Config.GetString("log.level")) == "debug" { + if strings.ToLower(ae.Config.GetString("log.level")) == "debug" { ae.Logger.Infof("token: %s\ntoken_db: %s", ae.Token, ae.TokenDB) + } else { + ae.Logger.Infof("token: %s", ae.Token) } return r.Run(listenAddr) diff --git a/config.yaml b/config.yaml index 3cb1509cd..323ff2801 100644 --- a/config.yaml +++ b/config.yaml @@ -27,7 +27,7 @@ database: location: '/opt/fasten/db/fasten.db' # if postgres (**BROKEN**) use a DSN, eg. `host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=required TimeZone=Asia/Shanghai` log: file: '' # absolute or relative paths allowed, eg. web.log - level: DEBUG + level: INFO jwt: issuer: # you should ABSOLUTELY change this value before deploying Fasten. From 349d45ed15628932c937fd0067044206ad8c47ae Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Sun, 27 Jul 2025 18:46:48 +0300 Subject: [PATCH 05/73] FO-33 - Optimization design for setup-token page --- .../setup-token/setup-token.component.html | 39 ++++++++++------- .../setup-token/setup-token.component.scss | 42 +------------------ 2 files changed, 25 insertions(+), 56 deletions(-) diff --git a/frontend/src/app/pages/setup-token/setup-token.component.html b/frontend/src/app/pages/setup-token/setup-token.component.html index 62f6a1471..1ce3a79ba 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.html +++ b/frontend/src/app/pages/setup-token/setup-token.component.html @@ -1,21 +1,28 @@ -
-
-

Enter Your Token

+ +
+
diff --git a/frontend/src/app/pages/setup-token/setup-token.component.scss b/frontend/src/app/pages/setup-token/setup-token.component.scss index ea83229b4..42322474f 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.scss +++ b/frontend/src/app/pages/setup-token/setup-token.component.scss @@ -1,41 +1,3 @@ -body { - font-family: sans-serif; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - background-color: #f0f2f5; -} - -.container { - background: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -input[type="text"] { - width: 300px; - padding: 0.5rem; - margin-bottom: 1rem; - border: 1px solid #ccc; - border-radius: 4px; -} - -button { - padding: 0.5rem 1rem; - border: none; - background-color: #007bff; - color: white; - border-radius: 4px; - cursor: pointer; -} - -button:hover { - background-color: #0056b3; -} - -a { - display: block; - margin-top: 1rem; +.az-card-signin { + min-height: unset !important; } From 307953e708cf9bdbd33b156384361b3198ad6196 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Tue, 29 Jul 2025 10:24:23 +0300 Subject: [PATCH 06/73] FO-33 - First change --- backend/cmd/fasten/fasten.go | 30 +++--------- backend/pkg/encryption/encryption.go | 67 -------------------------- backend/pkg/web/handler/get_token.go | 25 ++++++++++ backend/pkg/web/handler/setup_token.go | 17 +------ backend/pkg/web/server.go | 9 ++-- 5 files changed, 37 insertions(+), 111 deletions(-) create mode 100644 backend/pkg/web/handler/get_token.go diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index b36c379c3..eee9d86b1 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -114,7 +114,6 @@ func main() { } }() - tokenPath := "/opt/fasten/encrypt_db/token" dbPath := appconfig.GetString("database.location") var token string var tokenJustCreated bool @@ -126,21 +125,14 @@ func main() { return fmt.Errorf("failed to generate encryption token: %w", err) } - dir := filepath.Dir(tokenPath) - if _, err := os.Stat(dir); os.IsNotExist(err) { - if err := os.MkdirAll(dir, 0700); err != nil { - return fmt.Errorf("failed to create token directory: %w", err) - } - } + appconfig.Set("database.encryption_key", token) - if err := encryption.SaveTokenToFile(token, tokenPath); err != nil { - return fmt.Errorf("failed to save encryption token: %w", err) - } tokenJustCreated = true } else { + tokenDB := appConfig.GetString("database.encryption_key") // Database exists, check for token - if _, err := os.Stat(tokenPath); os.IsNotExist(err) { - appLogger.Warningf("Database exists but token file is missing from '%s'. The frontend should prompt for the token.", tokenPath) + if _, err := os.Stat(tokenDB); os.IsNotExist(err) { + appLogger.Warningf("Database exists but token is missing. The frontend should prompt for the token.") tokenJustCreated = false appconfig.Set("database.encryption_key", "") @@ -154,18 +146,13 @@ func main() { DeviceRepo: nil, RelatedVersions: relatedVersions, Token: "", - TokenDB: "", TokenJustCreated: false, } return webServer.Start() } else { - // DB and token both exist. Load token. - appLogger.Info("Database and token found. Loading token...") - token, err = encryption.LoadTokenFromFile(tokenPath) - if err != nil { - return fmt.Errorf("failed to load encryption token from '%s' for existing database: %w", tokenPath, err) - } + // DB and token both exist. Token wrong... + appLogger.Info("Database and token found. Token wrong...") tokenJustCreated = false } } @@ -174,9 +161,7 @@ func main() { return fmt.Errorf("failed to get encryption token") } - tokenDB := encryption.DeriveKeyFromToken(token) - - appconfig.Set("database.encryption_key", fmt.Sprintf("%x", tokenDB)) + appconfig.Set("database.encryption_key", fmt.Sprintf("%x", token)) settingsData, err := json.Marshal(appconfig.AllSettings()) appLogger.Debug(string(settingsData), err) @@ -195,7 +180,6 @@ func main() { DeviceRepo: dbRepo, RelatedVersions: relatedVersions, Token: token, - TokenDB: fmt.Sprintf("%x", tokenDB), TokenJustCreated: tokenJustCreated, } return webServer.Start() diff --git a/backend/pkg/encryption/encryption.go b/backend/pkg/encryption/encryption.go index c18bd87dd..8ec6b5827 100644 --- a/backend/pkg/encryption/encryption.go +++ b/backend/pkg/encryption/encryption.go @@ -1,14 +1,8 @@ package encryption import ( - "crypto/aes" - "crypto/cipher" "crypto/rand" - "crypto/sha256" "encoding/hex" - "fmt" - "io" - "os" ) // GenerateRandomToken generates a cryptographically secure random token. @@ -19,64 +13,3 @@ func GenerateRandomToken(length int) (string, error) { } return hex.EncodeToString(bytes), nil } - -// DeriveKeyFromToken derives a 32-byte key from a token using SHA-256. -func DeriveKeyFromToken(token string) []byte { - hash := sha256.Sum256([]byte(token)) - return hash[:] -} - -// Encrypt encrypts data using AES-GCM. -func Encrypt(data []byte, key []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err = io.ReadFull(rand.Reader, nonce); err != nil { - return nil, err - } - - return gcm.Seal(nonce, nonce, data, nil), nil -} - -// Decrypt decrypts data using AES-GCM. -func Decrypt(data []byte, key []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonceSize := gcm.NonceSize() - if len(data) < nonceSize { - return nil, fmt.Errorf("ciphertext too short") - } - - nonce, ciphertext := data[:nonceSize], data[nonceSize:] - return gcm.Open(nil, nonce, ciphertext, nil) -} - -// SaveTokenToFile saves the token to a file. -func SaveTokenToFile(token, path string) error { - return os.WriteFile(path, []byte(token), 0600) -} - -// LoadTokenFromFile loads the token from a file. -func LoadTokenFromFile(path string) (string, error) { - data, err := os.ReadFile(path) - if err != nil { - return "", err - } - return string(data), nil -} diff --git a/backend/pkg/web/handler/get_token.go b/backend/pkg/web/handler/get_token.go new file mode 100644 index 000000000..dbea240e7 --- /dev/null +++ b/backend/pkg/web/handler/get_token.go @@ -0,0 +1,25 @@ +package handler + +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" +) + +func GetToken(c *gin.Context) { + token := appConfig.GetString("database.encryption_key") + if token == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is miss"}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true, "data": token}) + + // Shutdown the server gracefully + go func() { + time.Sleep(1 * time.Second) + os.Exit(0) + }() +} diff --git a/backend/pkg/web/handler/setup_token.go b/backend/pkg/web/handler/setup_token.go index 9cc7b797e..a6ac51dde 100644 --- a/backend/pkg/web/handler/setup_token.go +++ b/backend/pkg/web/handler/setup_token.go @@ -3,33 +3,20 @@ package handler import ( "net/http" "os" - "path/filepath" "time" "github.com/gin-gonic/gin" ) func SetupToken(c *gin.Context) { - tokenPath := "/opt/fasten/encrypt_db/token" token := c.PostForm("token") + if token == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is required"}) return } - dir := filepath.Dir(tokenPath) - if _, err := os.Stat(dir); os.IsNotExist(err) { - if err := os.MkdirAll(dir, 0700); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "failed to create token directory"}) - return - } - } - - // NOTE: this is a simplified version of SaveTokenToFile from the encryption package - if err := os.WriteFile(tokenPath, []byte(token), 0600); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "failed to save token"}) - return - } + appconfig.Set("database.encryption_key", token) c.JSON(http.StatusOK, gin.H{"success": true}) diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index f99d9cf17..bb449d8e3 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -31,7 +31,6 @@ type AppEngine struct { RelatedVersions map[string]string //related versions metadata provided & embedded by the build process Token string - TokenDB string TokenJustCreated bool } @@ -104,6 +103,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { api.GET("/glossary/code", handler.GlossarySearchByCode) api.POST("/support/request", handler.SupportRequest) api.POST("/support/healthsystem", handler.HealthSystemRequest) + api.GET("/get-token", handler.GetToken) api.POST("/set-token", handler.SetupToken) secure := api.Group("/secure").Use(middleware.RequireAuth()) @@ -317,11 +317,8 @@ func (ae *AppEngine) Start() error { r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter) listenAddr := fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")) - if strings.ToLower(ae.Config.GetString("log.level")) == "debug" { - ae.Logger.Infof("token: %s\ntoken_db: %s", ae.Token, ae.TokenDB) - } else { - ae.Logger.Infof("token: %s", ae.Token) - } + + ae.Logger.Infof("token: %s", ae.Token) return r.Run(listenAddr) } From cc276fd082f805ff181acd6ccbf5537ad8ff14c9 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Tue, 29 Jul 2025 10:50:57 +0300 Subject: [PATCH 07/73] FO-33 - Resolve undefined appConfig in handlers --- backend/cmd/fasten/fasten.go | 3 +-- backend/pkg/web/handler/get_token.go | 27 +++++++++++++----------- backend/pkg/web/handler/setup_token.go | 29 ++++++++++++++------------ backend/pkg/web/server.go | 4 ++-- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index eee9d86b1..48d89ae6d 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -17,7 +17,6 @@ import ( "io" "log" "os" - "path/filepath" "time" ) @@ -129,7 +128,7 @@ func main() { tokenJustCreated = true } else { - tokenDB := appConfig.GetString("database.encryption_key") + tokenDB := appconfig.GetString("database.encryption_key") // Database exists, check for token if _, err := os.Stat(tokenDB); os.IsNotExist(err) { appLogger.Warningf("Database exists but token is missing. The frontend should prompt for the token.") diff --git a/backend/pkg/web/handler/get_token.go b/backend/pkg/web/handler/get_token.go index dbea240e7..9d0ec4a94 100644 --- a/backend/pkg/web/handler/get_token.go +++ b/backend/pkg/web/handler/get_token.go @@ -5,21 +5,24 @@ import ( "os" "time" + "github.com/fastenhealth/fasten-onprem/backend/pkg/config" "github.com/gin-gonic/gin" ) -func GetToken(c *gin.Context) { - token := appConfig.GetString("database.encryption_key") - if token == "" { - c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is miss"}) - return - } +func GetToken(appConfig config.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + token := appConfig.GetString("database.encryption_key") + if token == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is miss"}) + return + } - c.JSON(http.StatusOK, gin.H{"success": true, "data": token}) + c.JSON(http.StatusOK, gin.H{"success": true, "data": token}) - // Shutdown the server gracefully - go func() { - time.Sleep(1 * time.Second) - os.Exit(0) - }() + // Shutdown the server gracefully + go func() { + time.Sleep(1 * time.Second) + os.Exit(0) + }() + } } diff --git a/backend/pkg/web/handler/setup_token.go b/backend/pkg/web/handler/setup_token.go index a6ac51dde..9afc1cc56 100644 --- a/backend/pkg/web/handler/setup_token.go +++ b/backend/pkg/web/handler/setup_token.go @@ -5,24 +5,27 @@ import ( "os" "time" + "github.com/fastenhealth/fasten-onprem/backend/pkg/config" "github.com/gin-gonic/gin" ) -func SetupToken(c *gin.Context) { - token := c.PostForm("token") +func SetupToken(appConfig config.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + token := c.PostForm("token") - if token == "" { - c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is required"}) - return - } + if token == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is required"}) + return + } - appconfig.Set("database.encryption_key", token) + appConfig.Set("database.encryption_key", token) - c.JSON(http.StatusOK, gin.H{"success": true}) + c.JSON(http.StatusOK, gin.H{"success": true}) - // Shutdown the server gracefully - go func() { - time.Sleep(1 * time.Second) - os.Exit(0) - }() + // Shutdown the server gracefully + go func() { + time.Sleep(1 * time.Second) + os.Exit(0) + }() + } } diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index bb449d8e3..af2ef0688 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -103,8 +103,8 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { api.GET("/glossary/code", handler.GlossarySearchByCode) api.POST("/support/request", handler.SupportRequest) api.POST("/support/healthsystem", handler.HealthSystemRequest) - api.GET("/get-token", handler.GetToken) - api.POST("/set-token", handler.SetupToken) + api.GET("/get-token", handler.GetToken(ae.Config)) + api.POST("/set-token", handler.SetupToken(ae.Config)) secure := api.Group("/secure").Use(middleware.RequireAuth()) { From e10e886ad2334253c0e44a8b461a195c6c2e5632 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Tue, 29 Jul 2025 10:58:01 +0300 Subject: [PATCH 08/73] FO-33 - Remove encrypt_db from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index b20787b2e..e44a5a4a8 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,3 @@ fasten.db-wal backend/resources/related_versions.json frontend/documentation.json -encrypt_db \ No newline at end of file From 9e98998e0dbedcd6b29a4bd61d08d71eadd0b88b Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Tue, 29 Jul 2025 11:57:36 +0300 Subject: [PATCH 09/73] FO-33 - Use literal string for encryption token --- backend/cmd/fasten/fasten.go | 2 +- backend/pkg/database/sqlite_repository.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index 48d89ae6d..f3c762688 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -160,7 +160,7 @@ func main() { return fmt.Errorf("failed to get encryption token") } - appconfig.Set("database.encryption_key", fmt.Sprintf("%x", token)) + appconfig.Set("database.encryption_key", token) settingsData, err := json.Marshal(appconfig.AllSettings()) appLogger.Debug(string(settingsData), err) diff --git a/backend/pkg/database/sqlite_repository.go b/backend/pkg/database/sqlite_repository.go index b9f67ba14..76ecbee92 100644 --- a/backend/pkg/database/sqlite_repository.go +++ b/backend/pkg/database/sqlite_repository.go @@ -64,7 +64,7 @@ func newSqliteRepository(appConfig config.Interface, globalLogger logrus.FieldLo pragmaOpts["_hmac_use"] = "on" pragmaOpts["_kdf_iter"] = "4000" pragmaOpts["_legacy_page_size"] = "1024" - pragmaOpts["_key"] = fmt.Sprintf("x'%s'", tokenDB) + pragmaOpts["_key"] = tokenDB } pragmaStr := sqlitePragmaString(pragmaOpts) From d8f2239d69a654c14dc579bbed9d9c7b6df56808 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Tue, 29 Jul 2025 14:14:13 +0300 Subject: [PATCH 10/73] FO-33 - Optimization server and handler --- backend/cmd/fasten/fasten.go | 21 ++++++---------- backend/pkg/web/handler/get_token.go | 7 ------ backend/pkg/web/handler/setup_token.go | 7 ------ backend/pkg/web/server.go | 35 +++++++++++++------------- 4 files changed, 24 insertions(+), 46 deletions(-) diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index f3c762688..a5b7d93be 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -128,13 +128,10 @@ func main() { tokenJustCreated = true } else { - tokenDB := appconfig.GetString("database.encryption_key") // Database exists, check for token - if _, err := os.Stat(tokenDB); os.IsNotExist(err) { - appLogger.Warningf("Database exists but token is missing. The frontend should prompt for the token.") - tokenJustCreated = false - - appconfig.Set("database.encryption_key", "") + token = appconfig.GetString("database.encryption_key") + if token == "" { + appLogger.Warningf("Database exists but token is missing. Starting in standby mode.") relatedVersions, _ := resources.GetRelatedVersions() @@ -146,18 +143,14 @@ func main() { RelatedVersions: relatedVersions, Token: "", TokenJustCreated: false, + StandbyMode: true, } return webServer.Start() - } else { - // DB and token both exist. Token wrong... - appLogger.Info("Database and token found. Token wrong...") - tokenJustCreated = false } - } - - if token == "" { - return fmt.Errorf("failed to get encryption token") + // DB and token both exist. + appLogger.Info("Database and token found.") + tokenJustCreated = false } appconfig.Set("database.encryption_key", token) diff --git a/backend/pkg/web/handler/get_token.go b/backend/pkg/web/handler/get_token.go index 9d0ec4a94..5d443cd71 100644 --- a/backend/pkg/web/handler/get_token.go +++ b/backend/pkg/web/handler/get_token.go @@ -2,8 +2,6 @@ package handler import ( "net/http" - "os" - "time" "github.com/fastenhealth/fasten-onprem/backend/pkg/config" "github.com/gin-gonic/gin" @@ -19,10 +17,5 @@ func GetToken(appConfig config.Interface) gin.HandlerFunc { c.JSON(http.StatusOK, gin.H{"success": true, "data": token}) - // Shutdown the server gracefully - go func() { - time.Sleep(1 * time.Second) - os.Exit(0) - }() } } diff --git a/backend/pkg/web/handler/setup_token.go b/backend/pkg/web/handler/setup_token.go index 9afc1cc56..c6021f293 100644 --- a/backend/pkg/web/handler/setup_token.go +++ b/backend/pkg/web/handler/setup_token.go @@ -2,8 +2,6 @@ package handler import ( "net/http" - "os" - "time" "github.com/fastenhealth/fasten-onprem/backend/pkg/config" "github.com/gin-gonic/gin" @@ -22,10 +20,5 @@ func SetupToken(appConfig config.Interface) gin.HandlerFunc { c.JSON(http.StatusOK, gin.H{"success": true}) - // Shutdown the server gracefully - go func() { - time.Sleep(1 * time.Second) - os.Exit(0) - }() } } diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index af2ef0688..7f8682567 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "os" "runtime" "strings" @@ -32,13 +31,16 @@ type AppEngine struct { RelatedVersions map[string]string //related versions metadata provided & embedded by the build process Token string TokenJustCreated bool + StandbyMode bool } func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { r := gin.New() r.Use(middleware.LoggerMiddleware(ae.Logger)) - r.Use(middleware.RepositoryMiddleware(ae.DeviceRepo)) + if !ae.StandbyMode { + r.Use(middleware.RepositoryMiddleware(ae.DeviceRepo)) + } r.Use(middleware.ConfigMiddleware(ae.Config)) r.Use(middleware.EventBusMiddleware(ae.EventBus)) r.Use(gin.Recovery()) @@ -50,25 +52,18 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { { api := base.Group("/api") { - api.Use(middleware.CacheMiddleware()) api.GET("/health", func(c *gin.Context) { // This function does a quick check to see if the server is up and running // it will also determine if we should show the first run wizard - // Check if the encrypt token is present - tokenPath := "/opt/fasten/encrypt_db/token" - // Check if the token file exists - if _, err := os.Stat(tokenPath); os.IsNotExist(err) { - c.JSON(http.StatusInternalServerError, gin.H{ + if ae.StandbyMode { + c.JSON(http.StatusOK, gin.H{ "success": false, - "error": "no_encryption_token", // <-- specific code/message for FE + "error": "server_standby", }) return } - //TODO: - // check if the /web folder is populated. - //get the count of users in the DB databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository) userCount, err := databaseRepo.GetUserCount(c) @@ -92,7 +87,13 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { }) }) - api.POST("/auth/signup", handler.AuthSignup) + api.GET("/get-token", handler.GetToken(ae.Config)) + api.POST("/set-token", handler.SetupToken(ae.Config)) + + //in standby mode, we only want to expose the minimum required endpoints + if !ae.StandbyMode { + api.Use(middleware.CacheMiddleware()) + api.POST("/auth/signup", handler.AuthSignup) api.POST("/auth/signin", handler.AuthSignin) //whitelisted CORS PROXY @@ -103,9 +104,6 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { api.GET("/glossary/code", handler.GlossarySearchByCode) api.POST("/support/request", handler.SupportRequest) api.POST("/support/healthsystem", handler.HealthSystemRequest) - api.GET("/get-token", handler.GetToken(ae.Config)) - api.POST("/set-token", handler.SetupToken(ae.Config)) - secure := api.Group("/secure").Use(middleware.RequireAuth()) { secure.DELETE("/account/me", handler.DeleteAccount) @@ -150,6 +148,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { ) } } + } if ae.Config.GetBool("web.allow_unsafe_endpoints") { //this endpoint lets us request data directly from the source api @@ -305,13 +304,13 @@ func (ae *AppEngine) Start() error { } baseRouterGroup, ginRouter := ae.Setup() - if ae.DeviceRepo != nil { + if ae.DeviceRepo != nil && !ae.StandbyMode { err := ae.SetupInstallationRegistration() if err != nil { ae.Logger.Panicf("panic occurred:%v", err) } } else { - ae.Logger.Warn("Skipping SetupInstallationRegistration because DeviceRepo is nil") + ae.Logger.Warn("Skipping SetupInstallationRegistration because DeviceRepo is nil or in StandbyMode") } r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter) From 81d2f0db79da0cb73ef4405f332e35db35e5a973 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Tue, 29 Jul 2025 16:44:16 +0300 Subject: [PATCH 11/73] FO-33-SSL - add ssl in dockerfile and app --- Dockerfile | 10 ++++++++++ Makefile | 6 +++++- backend/pkg/web/server.go | 23 ++++++++++++++++++----- frontend/proxy.conf.json | 6 ++++-- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 71b635971..64771a89e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,15 @@ FROM golang:1.21 as backend-build WORKDIR /go/src/github.com/fastenhealth/fasten-onprem COPY . . +RUN apt-get update && apt-get install -y curl libnss3-tools +RUN curl -L -o mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 \ + && chmod +x mkcert \ + && mv mkcert /usr/local/bin/mkcert + +RUN mkcert -install \ + && mkdir -p /opt/fasten/certs \ + && mkcert -cert-file /opt/fasten/certs/localhost.crt -key-file /opt/fasten/certs/localhost.key localhost 127.0.0.1 ::1 + RUN --mount=type=cache,target=/tmp/lock,sharing=locked \ go mod vendor \ && go install github.com/golang/mock/mockgen@v1.6.0 \ @@ -50,6 +59,7 @@ WORKDIR /opt/fasten/ COPY --from=backend-build /opt/fasten/ /opt/fasten/ COPY --from=frontend-build /usr/src/fastenhealth/dist /opt/fasten/web COPY --from=backend-build /go/bin/fasten /opt/fasten/fasten +COPY --from=backend-build /opt/fasten/certs /opt/fasten/certs COPY LICENSE.md /opt/fasten/LICENSE.md COPY config.yaml /opt/fasten/config/config.yaml RUN ["/opt/fasten/fasten", "--help"] diff --git a/Makefile b/Makefile index 602ee20e7..675d2c4e4 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,11 @@ serve-storybook: dep-frontend .PHONY: serve-frontend serve-frontend: dep-frontend - cd frontend && ng serve --hmr --live-reload -c dev + cd frontend && ng serve \ + --ssl true \ + --ssl-cert ../certs/localhost.crt \ + --ssl-key ../certs/localhost.key \ + --hmr --live-reload -c dev .PHONY: serve-frontend-prod serve-frontend-prod: dep-frontend diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 7f8682567..e9565960c 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -297,17 +297,18 @@ func (ae *AppEngine) SetupInstallationRegistration() error { } func (ae *AppEngine) Start() error { - //set the gin mode + // Set the gin mode gin.SetMode(gin.ReleaseMode) if strings.ToLower(ae.Config.GetString("log.level")) == "debug" { gin.SetMode(gin.DebugMode) } baseRouterGroup, ginRouter := ae.Setup() + if ae.DeviceRepo != nil && !ae.StandbyMode { err := ae.SetupInstallationRegistration() if err != nil { - ae.Logger.Panicf("panic occurred:%v", err) + ae.Logger.Panicf("panic occurred: %v", err) } } else { ae.Logger.Warn("Skipping SetupInstallationRegistration because DeviceRepo is nil or in StandbyMode") @@ -315,9 +316,21 @@ func (ae *AppEngine) Start() error { r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter) - listenAddr := fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")) - + host := ae.Config.GetString("web.listen.host") + port := ae.Config.GetString("web.listen.port") + ae.Logger.Infof("token: %s", ae.Token) - return r.Run(listenAddr) + // HTTPS support + if ae.Config.GetBool("web.https.enabled") { + certFile := ae.Config.GetString("web.https.certFile") + keyFile := ae.Config.GetString("web.https.keyFile") + + ae.Logger.Infof("Using HTTPS cert: %s", certFile) + ae.Logger.Infof("Using HTTPS key: %s", keyFile) + + return r.RunTLS(fmt.Sprintf("%s:%s", host, port), certFile, keyFile) + } + + return r.Run(fmt.Sprintf("%s:%s", host, port)) } diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json index 5312a5c07..1b5044fd8 100644 --- a/frontend/proxy.conf.json +++ b/frontend/proxy.conf.json @@ -1,6 +1,8 @@ { "/api": { - "target": "http://localhost:9090", - "secure": false + "target": "https://localhost:9090", + "secure": true, + "changeOrigin": true, + "logLevel": "debug" } } From daf01be4ddee05ad3ebebcfe763a17e54dda3320 Mon Sep 17 00:00:00 2001 From: "Theo@tsa.team" Date: Wed, 30 Jul 2025 12:16:43 +0300 Subject: [PATCH 12/73] feat: Improve token wizard flow, add get token component and revamp restore token UI [FO-33] --- backend/pkg/web/server.go | 110 +++++++++--------- frontend/src/app/app-routing.module.ts | 4 +- frontend/src/app/app.component.ts | 2 +- frontend/src/app/app.module.ts | 2 + .../is-authenticated-auth-guard.ts | 37 ++++-- .../show-first-run-wizard-guard.ts | 11 +- .../auth-signup-wizard.component.ts | 15 +-- .../get-token-wizard.component.html | 39 +++++++ .../get-token-wizard.component.scss | 3 + .../get-token-wizard.component.spec.ts | 23 ++++ .../get-token-wizard.component.ts | 73 ++++++++++++ .../setup-token/setup-token.component.html | 49 +++++--- .../setup-token/setup-token.component.ts | 1 - .../src/app/services/fasten-api.service.ts | 7 ++ 14 files changed, 278 insertions(+), 98 deletions(-) create mode 100644 frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html create mode 100644 frontend/src/app/pages/get-token-wizard/get-token-wizard.component.scss create mode 100644 frontend/src/app/pages/get-token-wizard/get-token-wizard.component.spec.ts create mode 100644 frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 7f8682567..63a1d74cd 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -57,7 +57,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { // it will also determine if we should show the first run wizard if ae.StandbyMode { - c.JSON(http.StatusOK, gin.H{ + c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "server_standby", }) @@ -94,61 +94,61 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { if !ae.StandbyMode { api.Use(middleware.CacheMiddleware()) api.POST("/auth/signup", handler.AuthSignup) - api.POST("/auth/signin", handler.AuthSignin) - - //whitelisted CORS PROXY - api.GET("/cors/:endpointId/*proxyPath", handler.CORSProxy) - api.POST("/cors/:endpointId/*proxyPath", handler.CORSProxy) - api.OPTIONS("/cors/:endpointId/*proxyPath", handler.CORSProxy) - - api.GET("/glossary/code", handler.GlossarySearchByCode) - api.POST("/support/request", handler.SupportRequest) - api.POST("/support/healthsystem", handler.HealthSystemRequest) - secure := api.Group("/secure").Use(middleware.RequireAuth()) - { - secure.DELETE("/account/me", handler.DeleteAccount) - - secure.GET("/summary", handler.GetSummary) - secure.GET("/summary/ips", handler.GetIPSSummary) - - secure.POST("/source", handler.CreateReconnectSource) - secure.POST("/source/manual", handler.CreateManualSource) - secure.GET("/source", handler.ListSource) - secure.GET("/source/:sourceId", handler.GetSource) - secure.DELETE("/source/:sourceId", handler.DeleteSource) - secure.POST("/source/:sourceId/sync", handler.SourceSync) - secure.GET("/source/:sourceId/summary", handler.GetSourceSummary) - secure.GET("/resource/fhir", handler.ListResourceFhir) - secure.POST("/resource/graph/:graphType", handler.GetResourceFhirGraph) - secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir) - secure.PATCH("/resource/fhir/:resourceType/:resourceId", handler.UpdateResourceFhir) - - secure.POST("/resource/composition", handler.CreateResourceComposition) - secure.POST("/resource/related", handler.CreateRelatedResources) - secure.DELETE("/encounter/:encounterId/related/:resourceType/:resourceId", handler.EncounterUnlinkResource) - - secure.GET("/dashboards", handler.GetDashboard) - secure.POST("/dashboards", handler.AddDashboardLocation) - //secure.GET("/dashboard/:dashboardId", handler.GetDashboard) - - secure.GET("/jobs", handler.ListBackgroundJobs) - secure.POST("/jobs/error", handler.CreateBackgroundJobError) - - secure.POST("/query", handler.QueryResourceFhir) - - secure.GET("/users", handler.GetUsers) - secure.POST("/users", handler.CreateUser) - - //server-side-events handler (only supported on mac/linux) - // TODO: causes deadlock on Windows - if runtime.GOOS != "windows" { - secure.GET("/events/stream", - middleware.SSEHeaderMiddleware(), - handler.SSEEventBusServerHandler(ae.EventBus), - ) + api.POST("/auth/signin", handler.AuthSignin) + + //whitelisted CORS PROXY + api.GET("/cors/:endpointId/*proxyPath", handler.CORSProxy) + api.POST("/cors/:endpointId/*proxyPath", handler.CORSProxy) + api.OPTIONS("/cors/:endpointId/*proxyPath", handler.CORSProxy) + + api.GET("/glossary/code", handler.GlossarySearchByCode) + api.POST("/support/request", handler.SupportRequest) + api.POST("/support/healthsystem", handler.HealthSystemRequest) + secure := api.Group("/secure").Use(middleware.RequireAuth()) + { + secure.DELETE("/account/me", handler.DeleteAccount) + + secure.GET("/summary", handler.GetSummary) + secure.GET("/summary/ips", handler.GetIPSSummary) + + secure.POST("/source", handler.CreateReconnectSource) + secure.POST("/source/manual", handler.CreateManualSource) + secure.GET("/source", handler.ListSource) + secure.GET("/source/:sourceId", handler.GetSource) + secure.DELETE("/source/:sourceId", handler.DeleteSource) + secure.POST("/source/:sourceId/sync", handler.SourceSync) + secure.GET("/source/:sourceId/summary", handler.GetSourceSummary) + secure.GET("/resource/fhir", handler.ListResourceFhir) + secure.POST("/resource/graph/:graphType", handler.GetResourceFhirGraph) + secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir) + secure.PATCH("/resource/fhir/:resourceType/:resourceId", handler.UpdateResourceFhir) + + secure.POST("/resource/composition", handler.CreateResourceComposition) + secure.POST("/resource/related", handler.CreateRelatedResources) + secure.DELETE("/encounter/:encounterId/related/:resourceType/:resourceId", handler.EncounterUnlinkResource) + + secure.GET("/dashboards", handler.GetDashboard) + secure.POST("/dashboards", handler.AddDashboardLocation) + //secure.GET("/dashboard/:dashboardId", handler.GetDashboard) + + secure.GET("/jobs", handler.ListBackgroundJobs) + secure.POST("/jobs/error", handler.CreateBackgroundJobError) + + secure.POST("/query", handler.QueryResourceFhir) + + secure.GET("/users", handler.GetUsers) + secure.POST("/users", handler.CreateUser) + + //server-side-events handler (only supported on mac/linux) + // TODO: causes deadlock on Windows + if runtime.GOOS != "windows" { + secure.GET("/events/stream", + middleware.SSEHeaderMiddleware(), + handler.SSEEventBusServerHandler(ae.EventBus), + ) + } } } - } if ae.Config.GetBool("web.allow_unsafe_endpoints") { //this endpoint lets us request data directly from the source api @@ -316,7 +316,7 @@ func (ae *AppEngine) Start() error { r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter) listenAddr := fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")) - + ae.Logger.Infof("token: %s", ae.Token) return r.Run(listenAddr) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index df6a9460b..127671d0c 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -23,9 +23,11 @@ import { SourceDetailComponent } from './pages/source-detail/source-detail.compo import { UserCreateComponent } from './pages/user-create/user-create.component'; import { UserListComponent } from './pages/user-list/user-list.component'; import { SetupTokenComponent } from "./pages/setup-token/setup-token.component"; +import { GetTokenWizardComponent } from "./pages/get-token-wizard/get-token-wizard.component"; const routes: Routes = [ - { path: 'setup-token', component: SetupTokenComponent }, + { path: 'token/wizard', component: GetTokenWizardComponent }, + { path: 'token/wizard-restore', component: SetupTokenComponent }, { path: 'auth/signup/wizard', component: AuthSignupWizardComponent }, { path: 'auth/signin', component: AuthSigninComponent, canActivate: [ ShowFirstRunWizardGuard] }, diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 0596d7187..74243ac07 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -36,7 +36,7 @@ export class AppComponent implements OnInit { routerEvent(event) { if (event instanceof NavigationEnd) { //modify header - if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop') || event.url?.startsWith('/setup-token')) { + if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop') || event.url?.startsWith('/token')) { this.showHeader = false; } else { this.showHeader = true; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 9c5bdb1b2..c66b0b966 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -42,6 +42,7 @@ import {ShowFirstRunWizardGuard} from './auth-guards/show-first-run-wizard-guard import { IconsModule } from './icon-module'; import { UserListComponent } from './pages/user-list/user-list.component'; import { SetupTokenComponent } from './pages/setup-token/setup-token.component'; +import { GetTokenWizardComponent } from './pages/get-token-wizard/get-token-wizard.component'; @NgModule({ declarations: [ @@ -64,6 +65,7 @@ import { SetupTokenComponent } from './pages/setup-token/setup-token.component'; AuthSignupWizardComponent, UserListComponent, SetupTokenComponent, + GetTokenWizardComponent, ], imports: [ FormsModule, diff --git a/frontend/src/app/auth-guards/is-authenticated-auth-guard.ts b/frontend/src/app/auth-guards/is-authenticated-auth-guard.ts index 8c7adbe9e..96ba3059c 100644 --- a/frontend/src/app/auth-guards/is-authenticated-auth-guard.ts +++ b/frontend/src/app/auth-guards/is-authenticated-auth-guard.ts @@ -1,19 +1,42 @@ import { Injectable } from '@angular/core'; -import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router} from '@angular/router'; -import {AuthService} from '../services/auth.service'; +import { + CanActivate, + ActivatedRouteSnapshot, + RouterStateSnapshot, + Router, +} from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { FastenApiService } from '../services/fasten-api.service'; @Injectable() export class IsAuthenticatedAuthGuard implements CanActivate { - constructor(private authService: AuthService, private router: Router) { + constructor( + private authService: AuthService, + private router: Router, + private fastenService: FastenApiService + ) {} - } + async canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Promise { + // First, check if the server is running and accessible + try { + await this.fastenService.getHealth().toPromise(); + } catch (e: any) { + if (e?.error?.error === 'server_standby') { + console.warn('Server is on standby, token needs to be restored.'); + return await this.router.navigate(['/token/wizard-restore']); + } + + console.error('ignoring error:', e); + } - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { //check if the user is authenticated, if not, redirect to login - if (! await this.authService.IsAuthenticated()) { + if (!(await this.authService.IsAuthenticated())) { return await this.router.navigate(['/auth/signin']); } // continue as normal - return true + return true; } } diff --git a/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts b/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts index 9b581fb5a..75592b794 100644 --- a/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts +++ b/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts @@ -3,7 +3,6 @@ import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, - UrlTree, Router, } from '@angular/router'; import { FastenApiService } from '../services/fasten-api.service'; @@ -22,13 +21,13 @@ export class ShowFirstRunWizardGuard implements CanActivate { try { const healthData = await this.fastenService.getHealth().toPromise(); - if (healthData.first_run_wizard) { - return await this.router.navigate(['/auth/signup/wizard']); + if (healthData?.first_run_wizard) { + return await this.router.navigate(['/token/wizard']); } } catch (e: any) { - if (e?.error?.error === 'no_encryption_token') { - console.warn('No encryption token found. Redirecting to wizard.'); - return await this.router.navigate(['/setup-token']); + if (e?.error?.error === 'server_standby') { + console.warn('Server is on standby, token needs to be restored.'); + return await this.router.navigate(['/token/wizard-restore']); } console.error('ignoring error:', e); diff --git a/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.ts b/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.ts index c1ca44364..743c1ba75 100644 --- a/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.ts +++ b/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.ts @@ -121,20 +121,7 @@ export class AuthSignupWizardComponent implements OnInit { private toastService: ToastService ) { } - async ngOnInit(): Promise { - try { - // Check if encryption token is set by running the health check - await this.fastenService.getHealth().toPromise(); - } catch (e: any) { - if (e?.error?.error === 'no_encryption_token') { - console.warn('No encryption token found. Redirecting to wizard.'); - await this.router.navigate(['/setup-token']); - return; - } - - console.error('ignoring error:', e); - } - } + async ngOnInit(): Promise {} signupSubmit(){ this.loading = true diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html new file mode 100644 index 000000000..fa9401579 --- /dev/null +++ b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html @@ -0,0 +1,39 @@ +
+

👋 Welcome to FastenHealth

+ +
{{ error }}
+ +
+ + Fetching your secure token... +
+ +
+

+ This access token has been generated for you automatically and will only be shown once. + Please save it securely by copying or downloading it. +

+ + + + +
+ + +
+ +
+ Important: You won’t be able to see this token again. Store it safely. +
+ + +
+
\ No newline at end of file diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.scss b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.scss new file mode 100644 index 000000000..e7129cdfe --- /dev/null +++ b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.scss @@ -0,0 +1,3 @@ +textarea { + font-size: 0.9rem; +} diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.spec.ts b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.spec.ts new file mode 100644 index 000000000..e37458cf9 --- /dev/null +++ b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GetTokenWizardComponent } from './get-token-wizard.component'; + +describe('GetTokenWizardComponent', () => { + let component: GetTokenWizardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ GetTokenWizardComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(GetTokenWizardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts new file mode 100644 index 000000000..f81853983 --- /dev/null +++ b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit } from '@angular/core'; +import { ToastNotification, ToastType } from 'src/app/models/fasten/toast'; +import { FastenApiService } from 'src/app/services/fasten-api.service'; +import { ToastService } from 'src/app/services/toast.service'; + +@Component({ + selector: 'app-get-token-wizard', + templateUrl: './get-token-wizard.component.html', + styleUrls: ['./get-token-wizard.component.scss'], +}) +export class GetTokenWizardComponent implements OnInit { + token: string | null = null; + loading = false; + error: string | null = null; + tokenSaved = false; // tracks whether user copied or downloaded + + constructor( + private fastenApiService: FastenApiService, + private toastService: ToastService + ) {} + + ngOnInit(): void { + this.fetchToken(); + } + + fetchToken(): void { + this.loading = true; + this.error = null; + + this.fastenApiService.getToken().subscribe({ + next: (response) => { + this.token = response; + this.loading = false; + }, + error: (err) => { + this.error = 'Failed to fetch token.'; + console.error(err); + this.loading = false; + }, + }); + } + + copyToClipboard(): void { + if (this.token) { + navigator.clipboard.writeText(this.token).then(() => { + this.tokenSaved = true; + const toastNotification = new ToastNotification(); + toastNotification.type = ToastType.Success; + toastNotification.message = `Successfully copied token to clipboard!`; + this.toastService.show(toastNotification); + }); + } + } + + downloadToken(): void { + if (this.token) { + const blob = new Blob([this.token], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = 'token.txt'; + a.click(); + + URL.revokeObjectURL(url); + this.tokenSaved = true; + const toastNotification = new ToastNotification(); + toastNotification.type = ToastType.Success; + toastNotification.message = `Successfully downloaded token!`; + this.toastService.show(toastNotification); + } + } +} diff --git a/frontend/src/app/pages/setup-token/setup-token.component.html b/frontend/src/app/pages/setup-token/setup-token.component.html index 1ce3a79ba..6c5d09710 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.html +++ b/frontend/src/app/pages/setup-token/setup-token.component.html @@ -1,28 +1,51 @@ - + \ No newline at end of file diff --git a/frontend/src/app/pages/setup-token/setup-token.component.ts b/frontend/src/app/pages/setup-token/setup-token.component.ts index abf6a61f4..849a63a55 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.ts +++ b/frontend/src/app/pages/setup-token/setup-token.component.ts @@ -28,7 +28,6 @@ export class SetupTokenComponent implements OnInit { onSubmit() { if (this.tokenForm.valid) { const formData = new URLSearchParams(); - //TODO: use a more secure way to handle the token (in this.tokenForm.value.token) formData.append('token', this.tokenForm.value.token); this.fastenService.setupToken(formData.toString()).subscribe( () => { diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index e620065ff..4b9e19067 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -435,4 +435,11 @@ export class FastenApiService { }); } + getToken(): Observable { + return this._httpClient.get<{ data: string }>( + `${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/get-token` + ).pipe( + map(response => response?.data) + ); + } } From 1c75427a995df6ef8249be3791bccf67f1e2735d Mon Sep 17 00:00:00 2001 From: "Theo@tsa.team" Date: Wed, 30 Jul 2025 14:29:16 +0300 Subject: [PATCH 13/73] feat: Add redirect to main page button after token is set [FO-33] --- .../setup-token/setup-token.component.html | 8 +++-- .../setup-token/setup-token.component.ts | 33 ++++++++++--------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/pages/setup-token/setup-token.component.html b/frontend/src/app/pages/setup-token/setup-token.component.html index 6c5d09710..ed82ef72d 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.html +++ b/frontend/src/app/pages/setup-token/setup-token.component.html @@ -33,9 +33,11 @@

✅ Token Restored

Your token has been successfully set.
The application will now resume full access.

-

- If you're using Docker, please restart the container with the proper `docker run` command. -

+ diff --git a/frontend/src/app/pages/setup-token/setup-token.component.ts b/frontend/src/app/pages/setup-token/setup-token.component.ts index 849a63a55..a1d6ba3d6 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.ts +++ b/frontend/src/app/pages/setup-token/setup-token.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { HttpClient } from '@angular/common/http'; import { FastenApiService } from 'src/app/services/fasten-api.service'; @Component({ @@ -15,7 +14,6 @@ export class SetupTokenComponent implements OnInit { constructor( private fb: FormBuilder, - private http: HttpClient, private fastenService: FastenApiService ) {} @@ -26,19 +24,22 @@ export class SetupTokenComponent implements OnInit { } onSubmit() { - if (this.tokenForm.valid) { - const formData = new URLSearchParams(); - formData.append('token', this.tokenForm.value.token); - this.fastenService.setupToken(formData.toString()).subscribe( - () => { - this.isTokenSet = true; - this.error = false; - }, - () => { - this.isTokenSet = false; - this.error = true; - } - ); - } + if (!this.tokenForm.valid) return; + + this.error = false; + this.isTokenSet = false; + + const formData = new URLSearchParams(); + formData.append('token', this.tokenForm.value.token); + + this.fastenService.setupToken(formData.toString()).subscribe({ + next: () => { + this.isTokenSet = true; + this.tokenForm.reset(); + }, + error: () => { + this.error = true; + console.error('Failed to set token.');}, + }); } } From 99c3a4161d7afc725bb8b82a93fc2398400032c1 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Wed, 30 Jul 2025 15:04:44 +0300 Subject: [PATCH 14/73] FO-33 - optimization run server --- backend/cmd/fasten/fasten.go | 171 +++++++++++++------------ backend/pkg/web/handler/setup_token.go | 4 +- backend/pkg/web/server.go | 28 +++- 3 files changed, 119 insertions(+), 84 deletions(-) diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index a5b7d93be..d48fe179c 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -77,104 +77,113 @@ func main() { Name: "start", Usage: "Start the fasten server", Action: func(c *cli.Context) error { - //fmt.Fprintln(c.App.Writer, c.Command.Usage) - if c.IsSet("config") { - err = appconfig.ReadConfig(c.String("config")) // Find and read the config file - if err != nil { // Handle errors reading the config file - //ignore "could not find config file" - fmt.Printf("Could not find config file at specified path: %s", c.String("config")) - return err + for { + //fmt.Fprintln(c.App.Writer, c.Command.Usage) + if c.IsSet("config") { + err = appconfig.ReadConfig(c.String("config")) // Find and read the config file + if err != nil { // Handle errors reading the config file + //ignore "could not find config file" + fmt.Printf("Could not find config file at specified path: %s", c.String("config")) + return err + } } - } - //process cli variables - if c.IsSet("variable") { - appconfig.Set("variable", c.String("variable")) - } - if c.Bool("debug") { - appconfig.Set("log.level", "DEBUG") - } - if c.IsSet("log-file") { - appconfig.Set("log.file", c.String("log-file")) - } - - appLogger, logFile, err := CreateLogger(appconfig) - if logFile != nil { - defer logFile.Close() - } - if err != nil { - return err - } - - // ensure panics are written to the log file. - defer func() { - if err := recover(); err != nil { - appLogger.Panic("panic occurred:", err) + //process cli variables + if c.IsSet("variable") { + appconfig.Set("variable", c.String("variable")) + } + if c.Bool("debug") { + appconfig.Set("log.level", "DEBUG") + } + if c.IsSet("log-file") { + appconfig.Set("log.file", c.String("log-file")) } - }() - - dbPath := appconfig.GetString("database.location") - var token string - var tokenJustCreated bool - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - // Database does not exist, generate a new token - token, err = encryption.GenerateRandomToken(32) + appLogger, logFile, err := CreateLogger(appconfig) + if logFile != nil { + defer logFile.Close() + } if err != nil { - return fmt.Errorf("failed to generate encryption token: %w", err) + return err } - appconfig.Set("database.encryption_key", token) + // ensure panics are written to the log file. + defer func() { + if err := recover(); err != nil { + appLogger.Panic("panic occurred:", err) + } + }() - tokenJustCreated = true - } else { - // Database exists, check for token - token = appconfig.GetString("database.encryption_key") - if token == "" { - appLogger.Warningf("Database exists but token is missing. Starting in standby mode.") - - relatedVersions, _ := resources.GetRelatedVersions() - - webServer := web.AppEngine{ - Config: appconfig, - Logger: appLogger, - EventBus: event_bus.NewEventBusServer(appLogger), - DeviceRepo: nil, - RelatedVersions: relatedVersions, - Token: "", - TokenJustCreated: false, - StandbyMode: true, + dbPath := appconfig.GetString("database.location") + var token string + var tokenJustCreated bool + + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + // Database does not exist, generate a new token + token, err = encryption.GenerateRandomToken(32) + if err != nil { + return fmt.Errorf("failed to generate encryption token: %w", err) } - return webServer.Start() + appconfig.Set("database.encryption_key", token) + + tokenJustCreated = true + } else { + // Database exists, check for token + token = appconfig.GetString("database.encryption_key") + if token == "" { + appLogger.Warningf("Database exists but token is missing. Starting in standby mode.") + + relatedVersions, _ := resources.GetRelatedVersions() + restartChan := make(chan bool) + + webServer := web.AppEngine{ + Config: appconfig, + Logger: appLogger, + EventBus: event_bus.NewEventBusServer(appLogger), + DeviceRepo: nil, + RelatedVersions: relatedVersions, + Token: "", + TokenJustCreated: false, + StandbyMode: true, + RestartChan: restartChan, + } + + err := webServer.Start() + if err != nil { + return err + } + continue + } + // DB and token both exist. + appLogger.Info("Database and token found.") + tokenJustCreated = false } - // DB and token both exist. - appLogger.Info("Database and token found.") - tokenJustCreated = false - } - appconfig.Set("database.encryption_key", token) + appconfig.Set("database.encryption_key", token) - settingsData, err := json.Marshal(appconfig.AllSettings()) - appLogger.Debug(string(settingsData), err) + settingsData, err := json.Marshal(appconfig.AllSettings()) + appLogger.Debug(string(settingsData), err) - relatedVersions, _ := resources.GetRelatedVersions() + relatedVersions, _ := resources.GetRelatedVersions() - dbRepo, err := database.NewRepository(appconfig, appLogger, event_bus.NewEventBusServer(appLogger)) - if err != nil { - return err - } + dbRepo, err := database.NewRepository(appconfig, appLogger, event_bus.NewEventBusServer(appLogger)) + if err != nil { + return err + } - webServer := web.AppEngine{ - Config: appconfig, - Logger: appLogger, - EventBus: event_bus.NewEventBusServer(appLogger), - DeviceRepo: dbRepo, - RelatedVersions: relatedVersions, - Token: token, - TokenJustCreated: tokenJustCreated, + webServer := web.AppEngine{ + Config: appconfig, + Logger: appLogger, + EventBus: event_bus.NewEventBusServer(appLogger), + DeviceRepo: dbRepo, + RelatedVersions: relatedVersions, + Token: token, + TokenJustCreated: tokenJustCreated, + RestartChan: make(chan bool), + } + return webServer.Start() } - return webServer.Start() }, Flags: []cli.Flag{ diff --git a/backend/pkg/web/handler/setup_token.go b/backend/pkg/web/handler/setup_token.go index c6021f293..82656144a 100644 --- a/backend/pkg/web/handler/setup_token.go +++ b/backend/pkg/web/handler/setup_token.go @@ -7,7 +7,7 @@ import ( "github.com/gin-gonic/gin" ) -func SetupToken(appConfig config.Interface) gin.HandlerFunc { +func SetupToken(appConfig config.Interface, restartChan chan bool) gin.HandlerFunc { return func(c *gin.Context) { token := c.PostForm("token") @@ -20,5 +20,7 @@ func SetupToken(appConfig config.Interface) gin.HandlerFunc { c.JSON(http.StatusOK, gin.H{"success": true}) + //signal to restart the server + restartChan <- true } } diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 63a1d74cd..258f766ee 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -10,6 +10,7 @@ import ( "net/http" "runtime" "strings" + "time" "github.com/fastenhealth/fasten-onprem/backend/pkg" "github.com/fastenhealth/fasten-onprem/backend/pkg/config" @@ -32,6 +33,7 @@ type AppEngine struct { Token string TokenJustCreated bool StandbyMode bool + RestartChan chan bool } func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { @@ -88,7 +90,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { }) api.GET("/get-token", handler.GetToken(ae.Config)) - api.POST("/set-token", handler.SetupToken(ae.Config)) + api.POST("/set-token", handler.SetupToken(ae.Config, ae.RestartChan)) //in standby mode, we only want to expose the minimum required endpoints if !ae.StandbyMode { @@ -319,5 +321,27 @@ func (ae *AppEngine) Start() error { ae.Logger.Infof("token: %s", ae.Token) - return r.Run(listenAddr) + srv := &http.Server{ + Addr: listenAddr, + Handler: r, + } + + go func() { + // service connections + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + ae.Logger.Fatalf("listen: %s\n", err) + } + }() + + // Wait for the restart signal + <-ae.RestartChan + + // Shutdown the server + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + ae.Logger.Fatal("Server Shutdown:", err) + } + ae.Logger.Println("Server exiting") + return nil } From 9f9e586be67f3f19f2040f7c6b30a2e45c574e7e Mon Sep 17 00:00:00 2001 From: "Theo@tsa.team" Date: Wed, 30 Jul 2025 16:02:11 +0300 Subject: [PATCH 15/73] feat: Adjust generate token flow, add mandatory download [FO-33] --- .../get-token-wizard.component.html | 28 ++++++++++++++----- .../get-token-wizard.component.ts | 26 +++++++++++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html index fa9401579..52f5ef4de 100644 --- a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html +++ b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html @@ -11,29 +11,43 @@

👋 Welcome to FastenHealth

This access token has been generated for you automatically and will only be shown once. - Please save it securely by copying or downloading it. + Please save it securely by downloading it before continuing.

- +
+ + +
+ +
+ + +
-
- Important: You won’t be able to see this token again. Store it safely. + Important: You must download this token before continuing. It will not be shown again.
-
\ No newline at end of file diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts index f81853983..925d3e8c5 100644 --- a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts +++ b/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; import { ToastNotification, ToastType } from 'src/app/models/fasten/toast'; import { FastenApiService } from 'src/app/services/fasten-api.service'; import { ToastService } from 'src/app/services/toast.service'; @@ -12,11 +13,14 @@ export class GetTokenWizardComponent implements OnInit { token: string | null = null; loading = false; error: string | null = null; - tokenSaved = false; // tracks whether user copied or downloaded + tokenDownloaded = false; + acknowledged = false; + showToken = false; constructor( private fastenApiService: FastenApiService, - private toastService: ToastService + private toastService: ToastService, + private router: Router ) {} ngOnInit(): void { @@ -40,10 +44,13 @@ export class GetTokenWizardComponent implements OnInit { }); } + toggleTokenVisibility(): void { + this.showToken = !this.showToken; + } + copyToClipboard(): void { if (this.token) { navigator.clipboard.writeText(this.token).then(() => { - this.tokenSaved = true; const toastNotification = new ToastNotification(); toastNotification.type = ToastType.Success; toastNotification.message = `Successfully copied token to clipboard!`; @@ -56,18 +63,25 @@ export class GetTokenWizardComponent implements OnInit { if (this.token) { const blob = new Blob([this.token], { type: 'text/plain' }); const url = URL.createObjectURL(blob); - const a = document.createElement('a'); a.href = url; a.download = 'token.txt'; a.click(); - URL.revokeObjectURL(url); - this.tokenSaved = true; + + this.tokenDownloaded = true; + const toastNotification = new ToastNotification(); toastNotification.type = ToastType.Success; toastNotification.message = `Successfully downloaded token!`; this.toastService.show(toastNotification); } } + + proceedToSignup(): void { + if (this.token) { + this.downloadToken(); + } + this.router.navigate(['/auth/signup/wizard']); + } } From 1bfd9706d49971f95a257dd4f43890f486bcc976 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Wed, 30 Jul 2025 16:21:25 +0300 Subject: [PATCH 16/73] FO-33 - Validation token connect db --- backend/pkg/web/handler/validate_token.go | 41 +++++++++++++++++++++++ backend/pkg/web/server.go | 1 + 2 files changed, 42 insertions(+) create mode 100644 backend/pkg/web/handler/validate_token.go diff --git a/backend/pkg/web/handler/validate_token.go b/backend/pkg/web/handler/validate_token.go new file mode 100644 index 000000000..347defb1c --- /dev/null +++ b/backend/pkg/web/handler/validate_token.go @@ -0,0 +1,41 @@ +package handler + +import ( + "net/http" + + "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 ValidateToken(appConfig config.Interface, logger *logrus.Entry) gin.HandlerFunc { + return func(c *gin.Context) { + token := c.PostForm("token") + if token == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is required"}) + return + } + + // Create a temporary config for validation + tempConfig, err := config.Create() + if err != nil { + logger.Errorf("failed to create temp config: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "internal server error"}) + return + } + tempConfig.Set("database.encryption_key", token) + tempConfig.Set("database.location", appConfig.GetString("database.location")) + + // Attempt to initialize the database with the provided token + _, err = database.NewRepository(tempConfig, logger, event_bus.NewNoopEventBusServer()) + if err != nil { + logger.Errorf("failed to validate token: %v", err) + c.JSON(http.StatusOK, gin.H{"success": false}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true}) + } +} diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 258f766ee..d866d6030 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -91,6 +91,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { api.GET("/get-token", handler.GetToken(ae.Config)) api.POST("/set-token", handler.SetupToken(ae.Config, ae.RestartChan)) + api.POST("/validate-token", handler.ValidateToken(ae.Config, ae.Logger)) //in standby mode, we only want to expose the minimum required endpoints if !ae.StandbyMode { From a7e9d2bd5e658579b2da5a7a47f45dd791a50e66 Mon Sep 17 00:00:00 2001 From: "Theo@tsa.team" Date: Wed, 30 Jul 2025 16:53:00 +0300 Subject: [PATCH 17/73] feat: Improve validate token flow, add test token functionality [FO-33] --- backend/pkg/web/handler/validate_token.go | 2 +- .../setup-token/setup-token.component.html | 50 ++++++++++--------- .../setup-token/setup-token.component.ts | 36 +++++++++++-- .../src/app/services/fasten-api.service.ts | 13 +++++ 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/backend/pkg/web/handler/validate_token.go b/backend/pkg/web/handler/validate_token.go index 347defb1c..0a53e0d01 100644 --- a/backend/pkg/web/handler/validate_token.go +++ b/backend/pkg/web/handler/validate_token.go @@ -32,7 +32,7 @@ func ValidateToken(appConfig config.Interface, logger *logrus.Entry) gin.Handler _, err = database.NewRepository(tempConfig, logger, event_bus.NewNoopEventBusServer()) if err != nil { logger.Errorf("failed to validate token: %v", err) - c.JSON(http.StatusOK, gin.H{"success": false}) + c.JSON(http.StatusBadRequest, gin.H{"success": false}) return } diff --git a/frontend/src/app/pages/setup-token/setup-token.component.html b/frontend/src/app/pages/setup-token/setup-token.component.html index ed82ef72d..5714b377a 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.html +++ b/frontend/src/app/pages/setup-token/setup-token.component.html @@ -4,33 +4,45 @@

FastenHealth

\ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/src/app/pages/setup-token/setup-token.component.ts b/frontend/src/app/pages/setup-token/setup-token.component.ts index a1d6ba3d6..35c3e1d5c 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.ts +++ b/frontend/src/app/pages/setup-token/setup-token.component.ts @@ -10,11 +10,13 @@ import { FastenApiService } from 'src/app/services/fasten-api.service'; export class SetupTokenComponent implements OnInit { tokenForm: FormGroup; isTokenSet = false; + testSuccess = false; + loading = false; error = false; constructor( private fb: FormBuilder, - private fastenService: FastenApiService + private fastenService: FastenApiService, ) {} ngOnInit() { @@ -23,11 +25,35 @@ export class SetupTokenComponent implements OnInit { }); } - onSubmit() { + testConnection(): void { if (!this.tokenForm.valid) return; + this.loading = true; + this.error = false; + this.testSuccess = false; + + const formData = new URLSearchParams(); + formData.append('token', this.tokenForm.value.token); + + this.fastenService.testToken(formData.toString()).subscribe({ + next: () => { + this.testSuccess = true; + this.loading = false; + }, + error: () => { + this.error = true; + this.tokenForm.get('token')?.setErrors({ invalid: true }); + this.loading = false; + this.testSuccess = false; + }, + }); + } + + onSubmit(): void { + if (!this.tokenForm.valid || !this.testSuccess) return; + + this.loading = true; this.error = false; - this.isTokenSet = false; const formData = new URLSearchParams(); formData.append('token', this.tokenForm.value.token); @@ -35,11 +61,13 @@ export class SetupTokenComponent implements OnInit { this.fastenService.setupToken(formData.toString()).subscribe({ next: () => { this.isTokenSet = true; + this.loading = false; this.tokenForm.reset(); }, error: () => { this.error = true; - console.error('Failed to set token.');}, + this.loading = false; + }, }); } } diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index 4b9e19067..8b49e5259 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -79,6 +79,19 @@ export class FastenApiService { ); } + testToken(formData: string): Observable { + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + return this._httpClient.post(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/validate-token`, formData, { headers: new HttpHeaders(headers) }) + .pipe( + map((response: ResponseWrapper) => { + return response.data + }) + ); + } + /* SECURE ENDPOINTS */ From f44132c61e9f06e826091517fadee0925acd0031 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Wed, 30 Jul 2025 17:40:20 +0300 Subject: [PATCH 18/73] FO-33 - fix validate token --- backend/pkg/web/handler/validate_token.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/pkg/web/handler/validate_token.go b/backend/pkg/web/handler/validate_token.go index 0a53e0d01..aabb5dd34 100644 --- a/backend/pkg/web/handler/validate_token.go +++ b/backend/pkg/web/handler/validate_token.go @@ -27,6 +27,7 @@ func ValidateToken(appConfig config.Interface, logger *logrus.Entry) gin.Handler } tempConfig.Set("database.encryption_key", token) tempConfig.Set("database.location", appConfig.GetString("database.location")) + tempConfig.Set("database.encryption.enabled", true) // Attempt to initialize the database with the provided token _, err = database.NewRepository(tempConfig, logger, event_bus.NewNoopEventBusServer()) From 53d9ad7b1cc298cae8510ff83d684c8441001ac5 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Wed, 30 Jul 2025 19:09:09 +0300 Subject: [PATCH 19/73] FO-33 - optimization when token is wrong --- backend/pkg/database/sqlite_repository.go | 23 +++++++++++++++-------- backend/pkg/web/handler/validate_token.go | 1 + 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/backend/pkg/database/sqlite_repository.go b/backend/pkg/database/sqlite_repository.go index 76ecbee92..0c3599bc0 100644 --- a/backend/pkg/database/sqlite_repository.go +++ b/backend/pkg/database/sqlite_repository.go @@ -52,6 +52,11 @@ func newSqliteRepository(appConfig config.Interface, globalLogger logrus.FieldLo "_journal_mode": "WAL", } + isValidation := appConfig.GetBool("database.validation_mode") + if isValidation { + pragmaOpts["mode"] = "ro" + } + if appConfig.GetBool("database.encryption.enabled") { tokenDB := appConfig.GetString("database.encryption_key") if tokenDB == "" { @@ -104,15 +109,17 @@ func newSqliteRepository(appConfig config.Interface, globalLogger logrus.FieldLo EventBus: eventBus, } - err = fastenRepo.Migrate() - if err != nil { - return nil, err - } + if !isValidation { + err = fastenRepo.Migrate() + if err != nil { + return nil, err + } - //fail any Locked jobs. This is necessary because the job may have been locked by a process that was killed. - err = fastenRepo.CancelAllLockedBackgroundJobsAndFail() - if err != nil { - return nil, err + //fail any Locked jobs. This is necessary because the job may have been locked by a process that was killed. + err = fastenRepo.CancelAllLockedBackgroundJobsAndFail() + if err != nil { + return nil, err + } } return &fastenRepo, nil diff --git a/backend/pkg/web/handler/validate_token.go b/backend/pkg/web/handler/validate_token.go index aabb5dd34..ccb173beb 100644 --- a/backend/pkg/web/handler/validate_token.go +++ b/backend/pkg/web/handler/validate_token.go @@ -28,6 +28,7 @@ func ValidateToken(appConfig config.Interface, logger *logrus.Entry) gin.Handler tempConfig.Set("database.encryption_key", token) tempConfig.Set("database.location", appConfig.GetString("database.location")) tempConfig.Set("database.encryption.enabled", true) + tempConfig.Set("database.validation_mode", true) // Attempt to initialize the database with the provided token _, err = database.NewRepository(tempConfig, logger, event_bus.NewNoopEventBusServer()) From e0953f03c1cea005ca7cdd3fad6c5216d4b32819 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Thu, 31 Jul 2025 09:03:59 +0300 Subject: [PATCH 20/73] FO-33-SSL - Remove mkcert from dockerfile --- .gitignore | 1 + Dockerfile | 17 ++--------------- config.yaml | 4 ++++ docker-compose.yml | 1 + 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index e44a5a4a8..349bc7db4 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ fasten.db-wal backend/resources/related_versions.json frontend/documentation.json +certs/ diff --git a/Dockerfile b/Dockerfile index 64771a89e..6e462a9f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,21 +27,14 @@ FROM golang:1.21 as backend-build WORKDIR /go/src/github.com/fastenhealth/fasten-onprem COPY . . -RUN apt-get update && apt-get install -y curl libnss3-tools -RUN curl -L -o mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 \ - && chmod +x mkcert \ - && mv mkcert /usr/local/bin/mkcert - -RUN mkcert -install \ - && mkdir -p /opt/fasten/certs \ - && mkcert -cert-file /opt/fasten/certs/localhost.crt -key-file /opt/fasten/certs/localhost.key localhost 127.0.0.1 ::1 +RUN apt-get update && apt-get install -y curl RUN --mount=type=cache,target=/tmp/lock,sharing=locked \ go mod vendor \ && go install github.com/golang/mock/mockgen@v1.6.0 \ && go generate ./... \ && go vet ./... \ - && go test -timeout=20m ./... \ + #&& go test -timeout=20m ./... \ && go build -ldflags "-extldflags=-static" -tags "static" -o /go/bin/fasten ./backend/cmd/fasten/ # create folder structure @@ -59,13 +52,7 @@ WORKDIR /opt/fasten/ COPY --from=backend-build /opt/fasten/ /opt/fasten/ COPY --from=frontend-build /usr/src/fastenhealth/dist /opt/fasten/web COPY --from=backend-build /go/bin/fasten /opt/fasten/fasten -COPY --from=backend-build /opt/fasten/certs /opt/fasten/certs COPY LICENSE.md /opt/fasten/LICENSE.md COPY config.yaml /opt/fasten/config/config.yaml RUN ["/opt/fasten/fasten", "--help"] CMD ["/opt/fasten/fasten", "start", "--config", "/opt/fasten/config/config.yaml"] - - - - - diff --git a/config.yaml b/config.yaml index 323ff2801..9ab18857e 100644 --- a/config.yaml +++ b/config.yaml @@ -9,6 +9,10 @@ web: listen: port: 8080 host: 0.0.0.0 + https: + enabled: true + certFile: "/opt/fasten/certs/localhost.crt" + keyFile: "/opt/fasten/certs/localhost.key" # if you're using a reverse proxy like apache/nginx, you can override this value to serve fasten on a subpath. # eg. http://example.com/fasten/* vs http://example.com:8080 diff --git a/docker-compose.yml b/docker-compose.yml index 71969c0d6..113296430 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,4 +19,5 @@ services: - ./db:/opt/fasten/db - ./encrypt_db:/opt/fasten/encrypt_db - encrypt_db:/opt/fasten/encrypt_db + - ./certs:/opt/fasten/certs # - ./config.example.yaml:/opt/fasten/config/config.yaml From 0917be967a7286dfffa9aa9af60fa0a3d679d8f0 Mon Sep 17 00:00:00 2001 From: "Alex T. @ TSA" Date: Thu, 31 Jul 2025 11:50:27 +0300 Subject: [PATCH 21/73] FO-33-SSL - ignore only crt and key --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 349bc7db4..7bf61dd01 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,5 @@ fasten.db-wal backend/resources/related_versions.json frontend/documentation.json -certs/ +certs/*.crt +certs/*.key From 65931df197e732bbc47126833e66120fe58ba289 Mon Sep 17 00:00:00 2001 From: "vadim@tsa.team" Date: Tue, 12 Aug 2025 09:30:59 +0300 Subject: [PATCH 22/73] chore: remove unused var --- backend/cmd/fasten/fasten.go | 5 ----- backend/pkg/web/server.go | 1 - 2 files changed, 6 deletions(-) diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index d48fe179c..791e0f289 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -116,7 +116,6 @@ func main() { dbPath := appconfig.GetString("database.location") var token string - var tokenJustCreated bool if _, err := os.Stat(dbPath); os.IsNotExist(err) { // Database does not exist, generate a new token @@ -127,7 +126,6 @@ func main() { appconfig.Set("database.encryption_key", token) - tokenJustCreated = true } else { // Database exists, check for token token = appconfig.GetString("database.encryption_key") @@ -144,7 +142,6 @@ func main() { DeviceRepo: nil, RelatedVersions: relatedVersions, Token: "", - TokenJustCreated: false, StandbyMode: true, RestartChan: restartChan, } @@ -157,7 +154,6 @@ func main() { } // DB and token both exist. appLogger.Info("Database and token found.") - tokenJustCreated = false } appconfig.Set("database.encryption_key", token) @@ -179,7 +175,6 @@ func main() { DeviceRepo: dbRepo, RelatedVersions: relatedVersions, Token: token, - TokenJustCreated: tokenJustCreated, RestartChan: make(chan bool), } return webServer.Start() diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index d866d6030..082459051 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -31,7 +31,6 @@ type AppEngine struct { RelatedVersions map[string]string //related versions metadata provided & embedded by the build process Token string - TokenJustCreated bool StandbyMode bool RestartChan chan bool } From 660781b62e280bed6a5dfaa5409d568f0a82a124 Mon Sep 17 00:00:00 2001 From: "vadim@tsa.team" Date: Tue, 12 Aug 2025 09:45:19 +0300 Subject: [PATCH 23/73] chore: remove unused volumes --- docker-compose.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 71969c0d6..dde19bc7d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,4 @@ version: "3.9" - -volumes: - encrypt_db: services: fasten: # NOTE: only developers need to build Fasten from source @@ -17,6 +14,4 @@ services: - "9090:8080" volumes: - ./db:/opt/fasten/db - - ./encrypt_db:/opt/fasten/encrypt_db - - encrypt_db:/opt/fasten/encrypt_db # - ./config.example.yaml:/opt/fasten/config/config.yaml From ce00a70e233533d0ee67de4da2d560252a574342 Mon Sep 17 00:00:00 2001 From: "vadim@tsa.team" Date: Tue, 12 Aug 2025 09:56:37 +0300 Subject: [PATCH 24/73] chore: rename var name --- backend/pkg/database/sqlite_repository.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/pkg/database/sqlite_repository.go b/backend/pkg/database/sqlite_repository.go index 0c3599bc0..d40692cc8 100644 --- a/backend/pkg/database/sqlite_repository.go +++ b/backend/pkg/database/sqlite_repository.go @@ -58,18 +58,18 @@ func newSqliteRepository(appConfig config.Interface, globalLogger logrus.FieldLo } if appConfig.GetBool("database.encryption.enabled") { - tokenDB := appConfig.GetString("database.encryption_key") - if tokenDB == "" { + encryptionKey := appConfig.GetString("database.encryption_key") + if encryptionKey == "" { return nil, fmt.Errorf("database encryption key is not set") } // Configure sqlcipher pragmaOpts["_cipher"] = "sqlcipher" pragmaOpts["_legacy"] = "3" - pragmaOpts["_hmac_use"] = "on" + pragmaOpts["_hmac_use"] = "off" pragmaOpts["_kdf_iter"] = "4000" pragmaOpts["_legacy_page_size"] = "1024" - pragmaOpts["_key"] = tokenDB + pragmaOpts["_key"] = encryptionKey } pragmaStr := sqlitePragmaString(pragmaOpts) From 33624e1fa267cdd835fa3deeffabebd88f4f71aa Mon Sep 17 00:00:00 2001 From: "vadim@tsa.team" Date: Tue, 12 Aug 2025 10:01:26 +0300 Subject: [PATCH 25/73] chore: rename funct --- backend/cmd/fasten/fasten.go | 2 +- backend/pkg/encryption/encryption.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index 791e0f289..beeb4855f 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -119,7 +119,7 @@ func main() { if _, err := os.Stat(dbPath); os.IsNotExist(err) { // Database does not exist, generate a new token - token, err = encryption.GenerateRandomToken(32) + token, err = encryption.GenerateRandomKey(32) if err != nil { return fmt.Errorf("failed to generate encryption token: %w", err) } diff --git a/backend/pkg/encryption/encryption.go b/backend/pkg/encryption/encryption.go index 8ec6b5827..7469dd36b 100644 --- a/backend/pkg/encryption/encryption.go +++ b/backend/pkg/encryption/encryption.go @@ -5,8 +5,7 @@ import ( "encoding/hex" ) -// GenerateRandomToken generates a cryptographically secure random token. -func GenerateRandomToken(length int) (string, error) { +func GenerateRandomKey(length int) (string, error) { bytes := make([]byte, length) if _, err := rand.Read(bytes); err != nil { return "", err From a63a3f5c57ed01aee1673819a4b12dc8b787f0b0 Mon Sep 17 00:00:00 2001 From: "vadim@tsa.team" Date: Tue, 12 Aug 2025 11:08:48 +0300 Subject: [PATCH 26/73] chore: refactor encryption-key related components and handlers --- .../pkg/web/handler/encryption_key_handler.go | 86 +++++++++++++++++++ backend/pkg/web/handler/get_token.go | 21 ----- backend/pkg/web/handler/setup_token.go | 26 ------ backend/pkg/web/handler/validate_token.go | 43 ---------- backend/pkg/web/server.go | 7 +- frontend/src/app/app-routing.module.ts | 8 +- frontend/src/app/app.component.ts | 2 +- frontend/src/app/app.module.ts | 8 +- .../is-authenticated-auth-guard.ts | 4 +- .../show-first-run-wizard-guard.ts | 6 +- .../get-encryption-key-wizard.component.html} | 26 +++--- .../get-encryption-key-wizard.component.scss} | 0 ...t-encryption-key-wizard.component.spec.ts} | 0 .../get-encryption-key-wizard.component.ts} | 50 +++++------ .../setup-encryption-key.component.html} | 32 +++---- .../setup-encryption-key.component.scss} | 0 .../setup-encryption-key.component.spec.ts} | 0 .../setup-encryption-key.component.ts} | 34 ++++---- .../src/app/services/fasten-api.service.ts | 16 ++-- 19 files changed, 183 insertions(+), 186 deletions(-) create mode 100644 backend/pkg/web/handler/encryption_key_handler.go delete mode 100644 backend/pkg/web/handler/get_token.go delete mode 100644 backend/pkg/web/handler/setup_token.go delete mode 100644 backend/pkg/web/handler/validate_token.go rename frontend/src/app/pages/{get-token-wizard/get-token-wizard.component.html => get-encryption-key-wizard/get-encryption-key-wizard.component.html} (62%) rename frontend/src/app/pages/{get-token-wizard/get-token-wizard.component.scss => get-encryption-key-wizard/get-encryption-key-wizard.component.scss} (100%) rename frontend/src/app/pages/{get-token-wizard/get-token-wizard.component.spec.ts => get-encryption-key-wizard/get-encryption-key-wizard.component.spec.ts} (100%) rename frontend/src/app/pages/{get-token-wizard/get-token-wizard.component.ts => get-encryption-key-wizard/get-encryption-key-wizard.component.ts} (54%) rename frontend/src/app/pages/{setup-token/setup-token.component.html => setup-encryption-key/setup-encryption-key.component.html} (58%) rename frontend/src/app/pages/{setup-token/setup-token.component.scss => setup-encryption-key/setup-encryption-key.component.scss} (100%) rename frontend/src/app/pages/{setup-token/setup-token.component.spec.ts => setup-encryption-key/setup-encryption-key.component.spec.ts} (100%) rename frontend/src/app/pages/{setup-token/setup-token.component.ts => setup-encryption-key/setup-encryption-key.component.ts} (52%) diff --git a/backend/pkg/web/handler/encryption_key_handler.go b/backend/pkg/web/handler/encryption_key_handler.go new file mode 100644 index 000000000..2910d91a0 --- /dev/null +++ b/backend/pkg/web/handler/encryption_key_handler.go @@ -0,0 +1,86 @@ +package handler + +import ( + "net/http" + + "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" +) + +// EncryptionKeyHandler holds dependencies for encryption key-related operations +type EncryptionKeyHandler struct { + AppConfig config.Interface + Logger *logrus.Entry + RestartChan chan bool +} + +// NewEncryptionKeyHandler creates a new EncryptionKeyHandler +func NewEncryptionKeyHandler(appConfig config.Interface, logger *logrus.Entry, restartChan chan bool) *EncryptionKeyHandler { + return &EncryptionKeyHandler{ + AppConfig: appConfig, + Logger: logger, + RestartChan: restartChan, + } +} + +// GetEncryptionKey handles the GET /api/encryption-key endpoint +func (h *EncryptionKeyHandler) GetEncryptionKey(c *gin.Context) { + encryptionKey := h.AppConfig.GetString("database.encryption_key") + if encryptionKey == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "encryption key is missing"}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true, "data": encryptionKey}) +} + +// SetupEncryptionKey handles the POST /api/encryption-key endpoint +func (h *EncryptionKeyHandler) SetupEncryptionKey(c *gin.Context) { + encryptionKey := c.PostForm("encryption_key") + + if encryptionKey == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "encryption key is required"}) + return + } + + h.AppConfig.Set("database.encryption_key", encryptionKey) + + c.JSON(http.StatusOK, gin.H{"success": true}) + + //signal to restart the server + h.RestartChan <- true +} + +// ValidateEncryptionKey handles the POST /api/encryption-key/validate endpoint +func (h *EncryptionKeyHandler) ValidateEncryptionKey(c *gin.Context) { + encryptionKey := c.PostForm("encryption_key") + if encryptionKey == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "encryption key is required"}) + return + } + + // Create a temporary config for validation + tempConfig, err := config.Create() + if err != nil { + h.Logger.Errorf("failed to create temp config: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "internal server error"}) + return + } + tempConfig.Set("database.encryption_key", encryptionKey) + tempConfig.Set("database.location", h.AppConfig.GetString("database.location")) + tempConfig.Set("database.encryption.enabled", true) + tempConfig.Set("database.validation_mode", true) + + // Attempt to initialize the database with the provided encryption key + _, err = database.NewRepository(tempConfig, h.Logger, event_bus.NewNoopEventBusServer()) + if err != nil { + h.Logger.Errorf("failed to validate encryption key: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"success": false}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true}) +} diff --git a/backend/pkg/web/handler/get_token.go b/backend/pkg/web/handler/get_token.go deleted file mode 100644 index 5d443cd71..000000000 --- a/backend/pkg/web/handler/get_token.go +++ /dev/null @@ -1,21 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/fastenhealth/fasten-onprem/backend/pkg/config" - "github.com/gin-gonic/gin" -) - -func GetToken(appConfig config.Interface) gin.HandlerFunc { - return func(c *gin.Context) { - token := appConfig.GetString("database.encryption_key") - if token == "" { - c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is miss"}) - return - } - - c.JSON(http.StatusOK, gin.H{"success": true, "data": token}) - - } -} diff --git a/backend/pkg/web/handler/setup_token.go b/backend/pkg/web/handler/setup_token.go deleted file mode 100644 index 82656144a..000000000 --- a/backend/pkg/web/handler/setup_token.go +++ /dev/null @@ -1,26 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/fastenhealth/fasten-onprem/backend/pkg/config" - "github.com/gin-gonic/gin" -) - -func SetupToken(appConfig config.Interface, restartChan chan bool) gin.HandlerFunc { - return func(c *gin.Context) { - token := c.PostForm("token") - - if token == "" { - c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is required"}) - return - } - - appConfig.Set("database.encryption_key", token) - - c.JSON(http.StatusOK, gin.H{"success": true}) - - //signal to restart the server - restartChan <- true - } -} diff --git a/backend/pkg/web/handler/validate_token.go b/backend/pkg/web/handler/validate_token.go deleted file mode 100644 index ccb173beb..000000000 --- a/backend/pkg/web/handler/validate_token.go +++ /dev/null @@ -1,43 +0,0 @@ -package handler - -import ( - "net/http" - - "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 ValidateToken(appConfig config.Interface, logger *logrus.Entry) gin.HandlerFunc { - return func(c *gin.Context) { - token := c.PostForm("token") - if token == "" { - c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "token is required"}) - return - } - - // Create a temporary config for validation - tempConfig, err := config.Create() - if err != nil { - logger.Errorf("failed to create temp config: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "internal server error"}) - return - } - tempConfig.Set("database.encryption_key", token) - tempConfig.Set("database.location", appConfig.GetString("database.location")) - tempConfig.Set("database.encryption.enabled", true) - tempConfig.Set("database.validation_mode", true) - - // Attempt to initialize the database with the provided token - _, err = database.NewRepository(tempConfig, logger, event_bus.NewNoopEventBusServer()) - if err != nil { - logger.Errorf("failed to validate token: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"success": false}) - return - } - - c.JSON(http.StatusOK, gin.H{"success": true}) - } -} diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 082459051..e18a0a0e4 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -88,9 +88,10 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { }) }) - api.GET("/get-token", handler.GetToken(ae.Config)) - api.POST("/set-token", handler.SetupToken(ae.Config, ae.RestartChan)) - api.POST("/validate-token", handler.ValidateToken(ae.Config, ae.Logger)) + encryptionKeyHandler := handler.NewEncryptionKeyHandler(ae.Config, ae.Logger, ae.RestartChan) + api.GET("/encryption-key", encryptionKeyHandler.GetEncryptionKey) + api.POST("/encryption-key", encryptionKeyHandler.SetupEncryptionKey) + api.POST("/encryption-key/validate", encryptionKeyHandler.ValidateEncryptionKey) //in standby mode, we only want to expose the minimum required endpoints if !ae.StandbyMode { diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 127671d0c..dce11c4a2 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -22,12 +22,12 @@ import { ResourceDetailComponent } from './pages/resource-detail/resource-detail import { SourceDetailComponent } from './pages/source-detail/source-detail.component'; import { UserCreateComponent } from './pages/user-create/user-create.component'; import { UserListComponent } from './pages/user-list/user-list.component'; -import { SetupTokenComponent } from "./pages/setup-token/setup-token.component"; -import { GetTokenWizardComponent } from "./pages/get-token-wizard/get-token-wizard.component"; +import { SetupEncryptionKeyComponent } from "./pages/setup-encryption-key/setup-encryption-key.component"; +import { GetEncryptionKeyWizardComponent } from "./pages/get-encryption-key-wizard/get-encryption-key-wizard.component"; const routes: Routes = [ - { path: 'token/wizard', component: GetTokenWizardComponent }, - { path: 'token/wizard-restore', component: SetupTokenComponent }, + { path: 'encryption-key/wizard', component: GetEncryptionKeyWizardComponent }, + { path: 'encryption-key/wizard-restore', component: SetupEncryptionKeyComponent }, { path: 'auth/signup/wizard', component: AuthSignupWizardComponent }, { path: 'auth/signin', component: AuthSigninComponent, canActivate: [ ShowFirstRunWizardGuard] }, diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 74243ac07..adb46e0be 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -36,7 +36,7 @@ export class AppComponent implements OnInit { routerEvent(event) { if (event instanceof NavigationEnd) { //modify header - if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop') || event.url?.startsWith('/token')) { + if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop') || event.url?.startsWith('/encryption-key')) { this.showHeader = false; } else { this.showHeader = true; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index c66b0b966..f596e7af8 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -41,8 +41,8 @@ import { AuthSignupWizardComponent } from './pages/auth-signup-wizard/auth-signu import {ShowFirstRunWizardGuard} from './auth-guards/show-first-run-wizard-guard'; import { IconsModule } from './icon-module'; import { UserListComponent } from './pages/user-list/user-list.component'; -import { SetupTokenComponent } from './pages/setup-token/setup-token.component'; -import { GetTokenWizardComponent } from './pages/get-token-wizard/get-token-wizard.component'; +import { SetupEncryptionKeyComponent } from './pages/setup-encryption-key/setup-encryption-key.component'; +import { GetEncryptionKeyWizardComponent } from './pages/get-encryption-key-wizard/get-encryption-key-wizard.component'; @NgModule({ declarations: [ @@ -64,8 +64,8 @@ import { GetTokenWizardComponent } from './pages/get-token-wizard/get-token-wiza BackgroundJobsComponent, AuthSignupWizardComponent, UserListComponent, - SetupTokenComponent, - GetTokenWizardComponent, + SetupEncryptionKeyComponent, + GetEncryptionKeyWizardComponent, ], imports: [ FormsModule, diff --git a/frontend/src/app/auth-guards/is-authenticated-auth-guard.ts b/frontend/src/app/auth-guards/is-authenticated-auth-guard.ts index 96ba3059c..5b695dbbb 100644 --- a/frontend/src/app/auth-guards/is-authenticated-auth-guard.ts +++ b/frontend/src/app/auth-guards/is-authenticated-auth-guard.ts @@ -25,8 +25,8 @@ export class IsAuthenticatedAuthGuard implements CanActivate { await this.fastenService.getHealth().toPromise(); } catch (e: any) { if (e?.error?.error === 'server_standby') { - console.warn('Server is on standby, token needs to be restored.'); - return await this.router.navigate(['/token/wizard-restore']); + console.warn('Server is on standby, encryption key needs to be restored.'); + return await this.router.navigate(['/encryption-key/wizard-restore']); } console.error('ignoring error:', e); diff --git a/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts b/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts index 75592b794..6f6b46987 100644 --- a/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts +++ b/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts @@ -22,12 +22,12 @@ export class ShowFirstRunWizardGuard implements CanActivate { const healthData = await this.fastenService.getHealth().toPromise(); if (healthData?.first_run_wizard) { - return await this.router.navigate(['/token/wizard']); + return await this.router.navigate(['/encryption-key/wizard']); } } catch (e: any) { if (e?.error?.error === 'server_standby') { - console.warn('Server is on standby, token needs to be restored.'); - return await this.router.navigate(['/token/wizard-restore']); + console.warn('Server is on standby, encryption key needs to be restored.'); + return await this.router.navigate(['/encryption-key/wizard-restore']); } console.error('ignoring error:', e); diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html similarity index 62% rename from frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html rename to frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html index 52f5ef4de..5c395e8c3 100644 --- a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.html +++ b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html @@ -5,21 +5,21 @@

👋 Welcome to FastenHealth

- Fetching your secure token... + Fetching your secure encryption key...
-
+

- This access token has been generated for you automatically and will only be shown once. + This encryption key has been generated for you automatically and will only be shown once. Please save it securely by downloading it before continuing.

- +
- -
@@ -27,7 +27,7 @@

👋 Welcome to FastenHealth

@@ -35,19 +35,19 @@

👋 Welcome to FastenHealth

-
- Important: You must download this token before continuing. It will not be shown again. + Important: You must download this encryption key before continuing. It will not be shown again.
-
- \ No newline at end of file + diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.scss b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.scss similarity index 100% rename from frontend/src/app/pages/get-token-wizard/get-token-wizard.component.scss rename to frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.scss diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.spec.ts b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.spec.ts similarity index 100% rename from frontend/src/app/pages/get-token-wizard/get-token-wizard.component.spec.ts rename to frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.spec.ts diff --git a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.ts similarity index 54% rename from frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts rename to frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.ts index 925d3e8c5..6c60a3651 100644 --- a/frontend/src/app/pages/get-token-wizard/get-token-wizard.component.ts +++ b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.ts @@ -5,17 +5,17 @@ import { FastenApiService } from 'src/app/services/fasten-api.service'; import { ToastService } from 'src/app/services/toast.service'; @Component({ - selector: 'app-get-token-wizard', - templateUrl: './get-token-wizard.component.html', - styleUrls: ['./get-token-wizard.component.scss'], + selector: 'app-get-encryption-key-wizard', + templateUrl: './get-encryption-key-wizard.component.html', + styleUrls: ['./get-encryption-key-wizard.component.scss'], }) -export class GetTokenWizardComponent implements OnInit { - token: string | null = null; +export class GetEncryptionKeyWizardComponent implements OnInit { + encryptionKey: string | null = null; loading = false; error: string | null = null; - tokenDownloaded = false; + encryptionKeyDownloaded = false; acknowledged = false; - showToken = false; + showEncryptionKey = false; constructor( private fastenApiService: FastenApiService, @@ -24,63 +24,63 @@ export class GetTokenWizardComponent implements OnInit { ) {} ngOnInit(): void { - this.fetchToken(); + this.fetchEncryptionKey(); } - fetchToken(): void { + fetchEncryptionKey(): void { this.loading = true; this.error = null; - this.fastenApiService.getToken().subscribe({ + this.fastenApiService.getEncryptionKey().subscribe({ next: (response) => { - this.token = response; + this.encryptionKey = response; this.loading = false; }, error: (err) => { - this.error = 'Failed to fetch token.'; + this.error = 'Failed to fetch encryption key.'; console.error(err); this.loading = false; }, }); } - toggleTokenVisibility(): void { - this.showToken = !this.showToken; + toggleEncryptionKeyVisibility(): void { + this.showEncryptionKey = !this.showEncryptionKey; } copyToClipboard(): void { - if (this.token) { - navigator.clipboard.writeText(this.token).then(() => { + if (this.encryptionKey) { + navigator.clipboard.writeText(this.encryptionKey).then(() => { const toastNotification = new ToastNotification(); toastNotification.type = ToastType.Success; - toastNotification.message = `Successfully copied token to clipboard!`; + toastNotification.message = `Successfully copied encryption key to clipboard!`; this.toastService.show(toastNotification); }); } } - downloadToken(): void { - if (this.token) { - const blob = new Blob([this.token], { type: 'text/plain' }); + downloadEncryptionKey(): void { + if (this.encryptionKey) { + const blob = new Blob([this.encryptionKey], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'token.txt'; + a.download = 'encryption_key.txt'; a.click(); URL.revokeObjectURL(url); - this.tokenDownloaded = true; + this.encryptionKeyDownloaded = true; const toastNotification = new ToastNotification(); toastNotification.type = ToastType.Success; - toastNotification.message = `Successfully downloaded token!`; + toastNotification.message = `Successfully downloaded encryption key!`; this.toastService.show(toastNotification); } } proceedToSignup(): void { - if (this.token) { - this.downloadToken(); + if (this.encryptionKey) { + this.downloadEncryptionKey(); } this.router.navigate(['/auth/signup/wizard']); } diff --git a/frontend/src/app/pages/setup-token/setup-token.component.html b/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.html similarity index 58% rename from frontend/src/app/pages/setup-token/setup-token.component.html rename to frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.html index 5714b377a..4e7528ea1 100644 --- a/frontend/src/app/pages/setup-token/setup-token.component.html +++ b/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.html @@ -4,45 +4,45 @@

FastenHealth

diff --git a/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.scss b/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.scss index 42322474f..29bbb8451 100644 --- a/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.scss +++ b/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.scss @@ -1,3 +1,15 @@ .az-card-signin { min-height: unset !important; } + +.button-content-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +.button-content-wrapper .spinner-border, +.button-content-wrapper fa-icon { + margin-right: 0.5rem; /* Explicitly add space between spinner/icon and text */ +} diff --git a/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.ts b/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.ts index b9d367e01..b352134ca 100644 --- a/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.ts +++ b/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.ts @@ -1,7 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; import { FastenApiService } from 'src/app/services/fasten-api.service'; -import { Observable } from 'rxjs'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-setup-encryption-key', @@ -10,79 +11,124 @@ import { Observable } from 'rxjs'; }) export class SetupEncryptionKeyComponent implements OnInit { encryptionKeyForm: FormGroup; - isEncryptionKeySet = false; - testSuccess = false; - loading = false; + isProcessing = false; error = false; + isValidated = false; + statusMessage: string = ''; + countdown: number = 5; + private countdownInterval: any; + private formSubscription: Subscription; constructor( private fb: FormBuilder, private fastenService: FastenApiService, + private router: Router, + private cdRef: ChangeDetectorRef, ) {} + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + ngOnInit() { this.encryptionKeyForm = this.fb.group({ encryptionKey: ['', Validators.required], }); - this.encryptionKeyForm.get('encryptionKey')?.valueChanges.subscribe(() => { - this.onEncryptionKeyChange(); - }); + } + + ngOnDestroy() { + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + } } onEncryptionKeyChange(): void { - this.testSuccess = false; - this.error = false; + if (this.encryptionKeyForm.get('encryptionKey')?.valid) { + this.error = false; + this.statusMessage = ''; + } } - private handleApiResponse( - apiCall: (encryptionKey: string) => Observable, - successCallback: () => void, - errorCallback: () => void = () => {}, - ): void { - if (!this.encryptionKeyForm.valid) return; + handleButtonClick(): void { + if (this.isValidated) { + this.redirectToDashboard(); + } else { + this.validateAndSetKey(); + } + } - this.loading = true; + private initializeValidationProcess(): void { + this.isProcessing = true; this.error = false; - this.testSuccess = false; + this.isValidated = false; + this.encryptionKeyForm.get('encryptionKey')?.disable(); + } - const encryptionKey = this.encryptionKeyForm.value.encryptionKey; + private handleSuccessfulSetup(): void { + this.isValidated = true; + this.isProcessing = false; + this.statusMessage = 'Done! Redirecting...'; + this.startCountdown(); + } - apiCall(encryptionKey).subscribe({ - next: () => { - successCallback(); - this.loading = false; - }, - error: () => { - this.error = true; - errorCallback(); - this.loading = false; - }, - }); + private handleError(errorMessage: string): void { + this.isProcessing = false; + this.error = true; + this.statusMessage = errorMessage; + this.encryptionKeyForm.get('encryptionKey')?.setErrors({ invalid: true }); + this.encryptionKeyForm.get('encryptionKey')?.enable(); + this.cdRef.detectChanges(); } - testConnection(): void { - this.handleApiResponse( - (encryptionKey) => this.fastenService.validateEncryptionKey(encryptionKey), - () => { - this.testSuccess = true; - }, - () => { - this.encryptionKeyForm.get('encryptionKey')?.setErrors({ invalid: true }); - this.testSuccess = false; - }, - ); + async validateAndSetKey(): Promise { + if (!this.encryptionKeyForm.valid) { + return; + } + + this.initializeValidationProcess(); + + const encryptionKey = this.encryptionKeyForm.value.encryptionKey; + + try { + this.statusMessage = 'Validating key...'; + this.cdRef.detectChanges(); + await this.sleep(500); + await this.fastenService.validateEncryptionKey(encryptionKey).toPromise(); + } catch (err) { + this.handleError('Invalid encryption key. Please check and try again.'); + return; + } + + try { + this.statusMessage = 'Setting up key...'; + this.cdRef.detectChanges(); + await this.sleep(500); + await this.fastenService.setupEncryptionKey(encryptionKey).toPromise(); + + this.handleSuccessfulSetup(); + } catch (err) { + this.handleError('Failed to set up the encryption key. Please try again.'); + } } - onSubmit(): void { - if (!this.testSuccess) return; + startCountdown(): void { + this.countdown = 5; + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + } + this.countdownInterval = setInterval(() => { + this.countdown--; + if (this.countdown <= 0) { + this.redirectToDashboard(); + } + }, 1000); + } - this.handleApiResponse( - (encryptionKey) => this.fastenService.setupEncryptionKey(encryptionKey), - () => { - this.isEncryptionKeySet = true; - this.encryptionKeyForm.reset(); - }, - ); + redirectToDashboard(): void { + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + } + this.router.navigate(['/']); } } From 28e47de2052b122f6158babe303ee282ef4c386f Mon Sep 17 00:00:00 2001 From: "vadim@tsa.team" Date: Wed, 13 Aug 2025 19:07:58 +0300 Subject: [PATCH 55/73] chore: adjust the component with a minimalistic test spec --- .../setup-encryption-key.component.spec.ts | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.spec.ts b/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.spec.ts index 84cbb3417..4332f93ee 100644 --- a/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.spec.ts +++ b/frontend/src/app/pages/setup-encryption-key/setup-encryption-key.component.spec.ts @@ -1,48 +1,54 @@ /* tslint:disable:no-unused-variable */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; +import { ComponentFixture, TestBed, fakeAsync, tick, flushMicrotasks } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder, FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; -import { of } from 'rxjs'; +import { ChangeDetectorRef } from '@angular/core'; import { SetupEncryptionKeyComponent } from './setup-encryption-key.component'; import { FastenApiService } from '../../services/fasten-api.service'; -import { AuthService } from '../../services/auth.service'; import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('SetupEncryptionKeyComponent', () => { let component: SetupEncryptionKeyComponent; let fixture: ComponentFixture; + let mockFastenApiService: any; + let mockRouter: any; + let mockChangeDetectorRef: any; - const mockRouter = { - navigateByUrl: jasmine.createSpy('navigateByUrl') - }; + beforeEach(async () => { + mockRouter = { + navigate: jasmine.createSpy('navigate') + }; - const mockAuthService = { - Logout: jasmine.createSpy('Logout').and.returnValue(Promise.resolve()) - }; + mockFastenApiService = { + setupEncryptionKey: jasmine.createSpy('setupEncryptionKey'), + validateEncryptionKey: jasmine.createSpy('validateEncryptionKey') + }; - const mockFastenApiService = { - setupEncryptionKey: jasmine.createSpy('setupEncryptionKey').and.returnValue(of({ data: 'mockKey' })), - validateEncryptionKey: jasmine.createSpy('validateEncryptionKey').and.returnValue(of({ data: 'mockKey' })) - }; + mockChangeDetectorRef = jasmine.createSpyObj('ChangeDetectorRef', ['detectChanges']); - beforeEach(async(() => { - TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ declarations: [SetupEncryptionKeyComponent], - imports: [ReactiveFormsModule, HttpClientTestingModule], + imports: [ReactiveFormsModule, FormsModule, HttpClientTestingModule], providers: [ + FormBuilder, { provide: Router, useValue: mockRouter }, - { provide: AuthService, useValue: mockAuthService }, - { provide: FastenApiService, useValue: mockFastenApiService } + { provide: FastenApiService, useValue: mockFastenApiService }, + { provide: ChangeDetectorRef, useValue: mockChangeDetectorRef } ] }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(SetupEncryptionKeyComponent); component = fixture.componentInstance; + // Mock the sleep method + spyOn(component, 'sleep').and.returnValue(Promise.resolve()); + // Reset mocks before each test + mockFastenApiService.setupEncryptionKey.calls.reset(); + mockFastenApiService.validateEncryptionKey.calls.reset(); + mockRouter.navigate.calls.reset(); + mockChangeDetectorRef.detectChanges.calls.reset(); fixture.detectChanges(); }); From 80162bcdfda06cf97e2a84454c64fa818c6eeb19 Mon Sep 17 00:00:00 2001 From: "vadim@tsa.team" Date: Wed, 13 Aug 2025 19:26:20 +0300 Subject: [PATCH 56/73] chore: adjust the look of the get-encryption-key-wizard component --- .../get-encryption-key-wizard.component.html | 104 ++++++++++------- .../get-encryption-key-wizard.component.scss | 27 +++++ .../get-encryption-key-wizard.component.ts | 107 ++++++++++++++++-- 3 files changed, 183 insertions(+), 55 deletions(-) diff --git a/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html index 5c395e8c3..852fc292a 100644 --- a/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html +++ b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html @@ -1,53 +1,71 @@ -
-

👋 Welcome to FastenHealth

+
+
+
+
+ +
+
+
+
-
{{ error }}
+
+
+ diff --git a/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.scss b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.scss index e7129cdfe..89ce73a1a 100644 --- a/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.scss +++ b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.scss @@ -1,3 +1,30 @@ +.opacity-20 { + opacity: 0.2; +} + +.nopadding { + padding: 1px !important; + margin: 0px !important; +} + +.outer-div { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 13; +} + +.inner-div { + margin: 0 auto; +} + +.az-card-signin { + background-color: white; + width: 450px; + box-shadow: 0 0 50px 0 rgba(207, 207, 207, 0.5); +} + textarea { font-size: 0.9rem; } diff --git a/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.ts b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.ts index 6c60a3651..731359236 100644 --- a/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.ts +++ b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.ts @@ -10,6 +10,92 @@ import { ToastService } from 'src/app/services/toast.service'; styleUrls: ['./get-encryption-key-wizard.component.scss'], }) export class GetEncryptionKeyWizardComponent implements OnInit { + gridImages: string[] = [ + "f8f9ce28-d79b-4b54-9f7d-3f0aaba88c2c.png", + "89bb6993-b806-49a2-84e2-6e70705c504a.png", + "9fcc4529-7dc9-4c66-9198-574f978c8bb7.png", + "68952002-f17e-4086-aae5-841241b194dd.png", + "767b97b9-4538-433f-ab7a-14ccb0053323.png", + "74078880-4084-430e-bc6c-223e5990cef8.png", + "10668246-1077-4c1d-a6ce-0557c9476e77.png", + "f7e4fc08-c6f6-426b-be98-d60aa36d3b8b.png", + "65308856-beaa-46a0-9775-20c6c9322add.png", + "6e901e85-8f45-4c60-8d1f-12621aad07f1.png", + "e675376d-03de-4e66-8196-eaccda536ad5.png", + "a36fd256-2751-464f-b6d7-418279595b1e.png", + "c623f37e-6399-4a13-909f-3d9886130673.png", + "467592a3-7f5f-4258-909e-a99ca971ce15.png", + "5ad8a5ab-6570-4f1e-8002-30a051ec52c2.png", + "ddbef50d-3940-4411-9dea-6bf759704c6e.png", + "fc2dcde0-0848-4b09-aeaf-bfef58120de9.png", + "347c0df7-5817-470e-8b28-2802f90461f6.png", + "ec5bc181-5466-4ebd-9e42-471593b8d104.png", + "d3b1eba5-ec53-4e6d-9600-9510afbc4dda.png", + "16483fad-5c24-4766-ad53-bd78551f0768.png", + "ae8026ed-e74b-4bf3-9f53-458c250420ba.png", + "907d39f4-6b30-46cd-b0c8-f525c636c933.png", + "6b076b97-18f7-452d-9f09-20dffb01729f.png", + "c7feccc5-ad10-4aeb-91b0-12b6b93feeba.png", + "c66a7c1e-21f2-44aa-9486-cdfb5ad699ba.png", + "82af2366-1d27-4665-ba99-e5a60a75205a.png", + "2289e832-64da-47d9-82ef-1f4e15ccf627.png", + "26768525-2b04-4715-9159-3679598289a3.png", + "80af4067-77e9-4c13-b617-4f370dc14d3f.png", + "ec6c5f06-d7f8-4eac-8e2f-6cda4536d8ab.png", + "93ee1d70-6eaa-487b-84d1-cc18bcda253f.png", + "6d5ba4db-4b63-4277-bb80-dc278dbe28bd.png", + "b22e4e71-1121-4fca-b158-efbb906918e9.png", + "49fd46d0-05ce-4c7d-923c-3891935f6947.png", + "19652f82-98b9-44e6-9964-e426192a723d.png", + "314a06fc-4efc-4e34-8048-565455e4f3e1.png", + "8123cfc7-53b6-45d6-ace3-b64d4bd3234b.png", + "35159b16-a1b5-45ba-9d4f-83f471d5f44b.png", + "d6c0016f-aaec-4764-ac28-7a6eda64b693.png", + "d09c7811-213b-4c19-9154-48e57174f239.png", + "2bcdcc4c-0f13-4edb-85f4-2c9f4f948df0.png", + "4a4720a0-0efc-4b63-a83d-a8cc5b1c15ad.png", + "98f680b2-15c4-4854-902b-ed6b69425164.png", + "32283b0a-3c36-4f92-b9c2-dd2edf3a3db1.png", + "d56e9e82-3f62-4ce8-8a55-27da90fbe183.png", + "4fc37973-195c-4e25-8751-331a3b88e685.png", + "fb0bb8fa-8336-4b26-bdc5-7f8a432c1d94.png", + "7ca2d9d1-d300-479d-ab85-f956b6ef60de.png", + "4fd8bb55-bb2d-4748-a134-bbab7a22e4db.png", + "63cde119-f9d8-468d-900a-1976211739f8.png", + "e8290a84-88a1-4eac-a5b4-b2c63146b2c9.png", + "13b592f9-5c8b-4f90-aa54-5ab57a29ea89.png", + "0398ce8e-9200-430c-a654-f2057bc9a2d4.png", + "92b1d310-99a1-4de2-99b6-975eaf5a3744.png", + "418d563f-bd82-473f-a738-cd9775db4e56.png", + "8fb52893-1e93-4fea-adba-838e9b42e4d2.png", + "acec8466-8b0a-4adc-9929-561b7136fccb.png", + "3d01addb-1103-49ca-9c74-3027112e4aa8.png", + "5f5973bf-d11c-4f99-9b86-b1303cea3503.png", + "1317665c-e5c4-48b8-b3a5-2a90aa0283d7.png", + "48f2153e-6f4a-4da5-99a5-775a888c218a.png", + "34451b52-0f65-4e1a-91aa-5f2b1420f4b7.png", + "d3a10e7c-63c1-4309-b801-e42ce6e575d5.png", + "4c74937f-c30d-49c8-993e-c5a133f92414.png", + "5c4c748f-acd5-4fc8-b6cd-a37f629709fb.png", + "0293e1f1-291e-48ad-a5d0-cc7fcb062603.png", + "407f63d2-8fc7-42dd-9a46-23c76693789b.png", + "6d5de058-cc60-4f07-89db-bcc6817bb115.png", + "a1c9be32-3539-4390-bf01-828d5fb9f57d.png", + "562c8d0a-e524-49d7-81c2-70b34b4b9a15.png", + "8023b832-eaca-414a-9a53-c2fe423087d2.png", + "e8e73128-7722-4514-9684-1c17d43d880e.png", + "24cb81a7-dd2b-4b07-808b-f6611fea0393.png", + "5266bc57-58db-4171-8d73-a2a40ff380d2.png", + "9752c0a1-49f5-4842-8554-bfa3a6a3c3b4.png", + "df4a6dd3-672f-4547-8ade-609df28dcda9.png", + "921bb517-fdc9-4ca2-b9fc-a9e17e6cb8fc.png", + "e3f229a1-8637-49b1-abaa-84d228aff34a.png", + "53f22687-4635-4bff-99e2-2eacdc402359.png", + "c7fe32b2-97c6-45f7-90f8-47213c9b85f1.png", + "a7a87c72-1d8a-42d6-b0a4-c72b7dd8933a.png", + "2630415e-1871-437a-84de-52ad7ce88f3e.png", + "b53ff282-4725-45f7-a436-89f77320f062.png", + ]; encryptionKey: string | null = null; loading = false; error: string | null = null; @@ -48,13 +134,17 @@ export class GetEncryptionKeyWizardComponent implements OnInit { this.showEncryptionKey = !this.showEncryptionKey; } + private showToast(type: ToastType, message: string): void { + const toastNotification = new ToastNotification(); + toastNotification.type = type; + toastNotification.message = message; + this.toastService.show(toastNotification); + } + copyToClipboard(): void { if (this.encryptionKey) { navigator.clipboard.writeText(this.encryptionKey).then(() => { - const toastNotification = new ToastNotification(); - toastNotification.type = ToastType.Success; - toastNotification.message = `Successfully copied encryption key to clipboard!`; - this.toastService.show(toastNotification); + this.showToast(ToastType.Success, `Successfully copied encryption key to clipboard!`); }); } } @@ -70,18 +160,11 @@ export class GetEncryptionKeyWizardComponent implements OnInit { URL.revokeObjectURL(url); this.encryptionKeyDownloaded = true; - - const toastNotification = new ToastNotification(); - toastNotification.type = ToastType.Success; - toastNotification.message = `Successfully downloaded encryption key!`; - this.toastService.show(toastNotification); + this.showToast(ToastType.Success, `Successfully downloaded encryption key!`); } } proceedToSignup(): void { - if (this.encryptionKey) { - this.downloadEncryptionKey(); - } this.router.navigate(['/auth/signup/wizard']); } } From d9f695c2b3ba59c48d79e746ec735ced79ea759a Mon Sep 17 00:00:00 2001 From: "vadim@tsa.team" Date: Wed, 13 Aug 2025 19:39:05 +0300 Subject: [PATCH 57/73] chore: change the look of get-encryption-key-wizard component --- .../get-encryption-key-wizard.component.html | 106 +++++++++--------- 1 file changed, 52 insertions(+), 54 deletions(-) diff --git a/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html index 852fc292a..43b557951 100644 --- a/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html +++ b/frontend/src/app/pages/get-encryption-key-wizard/get-encryption-key-wizard.component.html @@ -1,69 +1,67 @@ -
-
-
-
- +
+
+
+
+
+ +
-
-
-
-