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 @@
+
+
+
+
+
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
+
+
+
fasten
+
-
-
Token set successfully!
-
The container has been stopped. Please run the `docker run` command again to start the application in full
- mode.
-
+
+
Token set successfully!
+
The token has been set successfully. The container has been stopped. Please run the `docker run` command again to start the application in full mode.
+
-
-
Error
+
+
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
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.
+
+
+
Your Token
+
+
+
+
+ 📋 Copy
+
+
+ 💾 Download as .txt
+
+
+
+
+ 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 @@
-
fasten
+
FastenHealth
+
+
-
Token set successfully!
-
The token has been set successfully. The container has been stopped. Please run the `docker run` command again to start the application in full mode.
+
✅ 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.
+
+
-
Error
-
Failed to set token. Please try again.
+
❌ Failed to Set Token
+
+ There was a problem setting the token. Please check your input and try again.
+
+
-
+
\ 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.
Your Token
-
+
+
+
+ {{ showToken ? 'Hide' : 'Show' }}
+
+
+
+
+
+
+ I understand that I will no longer have access to this token once I leave this page.
+
+
📋 Copy
-
+
💾 Download as .txt
- 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...
-
+
@@ -35,19 +35,19 @@
👋 Welcome to FastenHealth
📋 Copy
-
+
💾 Download as .txt
- 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.
-
+
Continue to Sign Up →
-
\ 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 @@
-
-
-
-
-
+
+
+
+
+
+
+
-
-