diff --git a/backend/pkg/database/gorm_common.go b/backend/pkg/database/gorm_common.go index dc3e8585..9f04aca3 100644 --- a/backend/pkg/database/gorm_common.go +++ b/backend/pkg/database/gorm_common.go @@ -73,6 +73,13 @@ func (gr *GormRepository) CreateUser(ctx context.Context, user *models.User) err return nil } + +func (gr *GormRepository) GetUserCount(ctx context.Context) (int, error) { + var count int64 + result := gr.GormClient.WithContext(ctx).Model(&models.User{}).Count(&count) + return int(count), result.Error +} + func (gr *GormRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) { var foundUser models.User result := gr.GormClient.WithContext(ctx).Where(models.User{Username: username}).First(&foundUser) diff --git a/backend/pkg/database/interface.go b/backend/pkg/database/interface.go index 2564ff61..65adea78 100644 --- a/backend/pkg/database/interface.go +++ b/backend/pkg/database/interface.go @@ -14,7 +14,7 @@ type DatabaseRepository interface { Migrate() error CreateUser(context.Context, *models.User) error - + GetUserCount(context.Context) (int, error) GetUserByUsername(context.Context, string) (*models.User, error) GetCurrentUser(ctx context.Context) (*models.User, error) DeleteCurrentUser(ctx context.Context) error diff --git a/backend/pkg/database/mock/mock_database.go b/backend/pkg/database/mock/mock_database.go index 53f7c3ef..aa9592e8 100644 --- a/backend/pkg/database/mock/mock_database.go +++ b/backend/pkg/database/mock/mock_database.go @@ -148,6 +148,20 @@ func (mr *MockDatabaseRepositoryMockRecorder) CreateUser(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockDatabaseRepository)(nil).CreateUser), arg0, arg1) } +// DeleteCurrentUser mocks base method. +func (m *MockDatabaseRepository) DeleteCurrentUser(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCurrentUser", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCurrentUser indicates an expected call of DeleteCurrentUser. +func (mr *MockDatabaseRepositoryMockRecorder) DeleteCurrentUser(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCurrentUser", reflect.TypeOf((*MockDatabaseRepository)(nil).DeleteCurrentUser), ctx) +} + // DeleteSource mocks base method. func (m *MockDatabaseRepository) DeleteSource(ctx context.Context, sourceId string) (int64, error) { m.ctrl.T.Helper() @@ -209,13 +223,12 @@ func (mr *MockDatabaseRepositoryMockRecorder) GetCurrentUser(ctx interface{}) *g } // GetFlattenedResourceGraph mocks base method. -func (m *MockDatabaseRepository) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, *models.ResourceGraphMetadata, error) { +func (m *MockDatabaseRepository) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFlattenedResourceGraph", ctx, graphType, options) ret0, _ := ret[0].(map[string][]*models.ResourceBase) - ret1, _ := ret[1].(*models.ResourceGraphMetadata) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret1, _ := ret[1].(error) + return ret0, ret1 } // GetFlattenedResourceGraph indicates an expected call of GetFlattenedResourceGraph. @@ -359,6 +372,21 @@ func (mr *MockDatabaseRepositoryMockRecorder) GetUserByUsername(arg0, arg1 inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockDatabaseRepository)(nil).GetUserByUsername), arg0, arg1) } +// GetUserCount mocks base method. +func (m *MockDatabaseRepository) GetUserCount(arg0 context.Context) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserCount", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserCount indicates an expected call of GetUserCount. +func (mr *MockDatabaseRepositoryMockRecorder) GetUserCount(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCount", reflect.TypeOf((*MockDatabaseRepository)(nil).GetUserCount), arg0) +} + // ListBackgroundJobs mocks base method. func (m *MockDatabaseRepository) ListBackgroundJobs(ctx context.Context, queryOptions models.BackgroundJobQueryOptions) ([]models.BackgroundJob, error) { m.ctrl.T.Helper() @@ -389,6 +417,21 @@ func (mr *MockDatabaseRepositoryMockRecorder) ListResources(arg0, arg1 interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResources", reflect.TypeOf((*MockDatabaseRepository)(nil).ListResources), arg0, arg1) } +// LoadSystemSettings mocks base method. +func (m *MockDatabaseRepository) LoadSystemSettings(ctx context.Context) (*models.SystemSettings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadSystemSettings", ctx) + ret0, _ := ret[0].(*models.SystemSettings) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadSystemSettings indicates an expected call of LoadSystemSettings. +func (mr *MockDatabaseRepositoryMockRecorder) LoadSystemSettings(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSystemSettings", reflect.TypeOf((*MockDatabaseRepository)(nil).LoadSystemSettings), ctx) +} + // LoadUserSettings mocks base method. func (m *MockDatabaseRepository) LoadUserSettings(ctx context.Context) (*models.UserSettings, error) { m.ctrl.T.Helper() @@ -461,6 +504,20 @@ func (mr *MockDatabaseRepositoryMockRecorder) RemoveResourceAssociation(ctx, sou return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveResourceAssociation", reflect.TypeOf((*MockDatabaseRepository)(nil).RemoveResourceAssociation), ctx, source, resourceType, resourceId, relatedSource, relatedResourceType, relatedResourceId) } +// SaveSystemSettings mocks base method. +func (m *MockDatabaseRepository) SaveSystemSettings(ctx context.Context, newSettings *models.SystemSettings) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveSystemSettings", ctx, newSettings) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveSystemSettings indicates an expected call of SaveSystemSettings. +func (mr *MockDatabaseRepositoryMockRecorder) SaveSystemSettings(ctx, newSettings interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSystemSettings", reflect.TypeOf((*MockDatabaseRepository)(nil).SaveSystemSettings), ctx, newSettings) +} + // SaveUserSettings mocks base method. func (m *MockDatabaseRepository) SaveUserSettings(arg0 context.Context, arg1 *models.UserSettings) error { m.ctrl.T.Helper() @@ -517,3 +574,17 @@ func (mr *MockDatabaseRepositoryMockRecorder) UpsertRawResource(ctx, sourceCrede mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRawResource", reflect.TypeOf((*MockDatabaseRepository)(nil).UpsertRawResource), ctx, sourceCredentials, rawResource) } + +// UpsertRawResourceAssociation mocks base method. +func (m *MockDatabaseRepository) UpsertRawResourceAssociation(ctx context.Context, sourceId, sourceResourceType, sourceResourceId, targetSourceId, targetResourceType, targetResourceId string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertRawResourceAssociation", ctx, sourceId, sourceResourceType, sourceResourceId, targetSourceId, targetResourceType, targetResourceId) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertRawResourceAssociation indicates an expected call of UpsertRawResourceAssociation. +func (mr *MockDatabaseRepositoryMockRecorder) UpsertRawResourceAssociation(ctx, sourceId, sourceResourceType, sourceResourceId, targetSourceId, targetResourceType, targetResourceId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRawResourceAssociation", reflect.TypeOf((*MockDatabaseRepository)(nil).UpsertRawResourceAssociation), ctx, sourceId, sourceResourceType, sourceResourceId, targetSourceId, targetResourceType, targetResourceId) +} diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index de7160d4..1d6eac81 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -6,6 +6,7 @@ import ( "embed" "encoding/json" "fmt" + "github.com/fastenhealth/fasten-onprem/backend/pkg" "github.com/fastenhealth/fasten-onprem/backend/pkg/config" "github.com/fastenhealth/fasten-onprem/backend/pkg/database" "github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus" @@ -54,15 +55,32 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { { 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 + //TODO: // check if the /web folder is populated. - // check if access to database + + //get the count of users in the DB + databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository) + userCount, err := databaseRepo.GetUserCount(c) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false}) + return + } keepAliveMsg := models.NewEventKeepAlive("heartbeat") - err := ae.EventBus.PublishMessage(keepAliveMsg) + err = ae.EventBus.PublishMessage(keepAliveMsg) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false}) + return + } c.JSON(http.StatusOK, gin.H{ - "success": err == nil, + "success": true, + "data": gin.H{ + "first_run_wizard": userCount == 0, + }, }) }) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 352dbb39..d6a92114 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -18,14 +18,15 @@ import {environment} from '../environments/environment'; import {DesktopCallbackComponent} from './pages/desktop-callback/desktop-callback.component'; import {BackgroundJobsComponent} from './pages/background-jobs/background-jobs.component'; import {AuthSignupWizardComponent} from './pages/auth-signup-wizard/auth-signup-wizard.component'; +import {ShowFirstRunWizardGuard} from './auth-guards/show-first-run-wizard-guard'; const routes: Routes = [ { path: 'auth/signup/wizard', component: AuthSignupWizardComponent }, - { path: 'auth/signin', component: AuthSigninComponent }, + { path: 'auth/signin', component: AuthSigninComponent, canActivate: [ ShowFirstRunWizardGuard] }, { path: 'auth/signin/callback/:idp_type', component: AuthSigninComponent }, - { path: 'auth/signup', component: AuthSignupComponent }, + { path: 'auth/signup', component: AuthSignupComponent, canActivate: [ ShowFirstRunWizardGuard] }, { path: 'auth/signup/callback/:idp_type', component: AuthSignupComponent }, { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index a02ebc11..221699ed 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -41,6 +41,7 @@ import { BackgroundJobsComponent } from './pages/background-jobs/background-jobs import {FhirCardModule} from './components/fhir-card/fhir-card.module'; import {FhirDatatableModule} from './components/fhir-datatable/fhir-datatable.module'; import { AuthSignupWizardComponent } from './pages/auth-signup-wizard/auth-signup-wizard.component'; +import {ShowFirstRunWizardGuard} from './auth-guards/show-first-run-wizard-guard'; @NgModule({ declarations: [ @@ -95,6 +96,7 @@ import { AuthSignupWizardComponent } from './pages/auth-signup-wizard/auth-signu deps: [AuthService, Router] }, IsAuthenticatedAuthGuard, + ShowFirstRunWizardGuard, { provide: HIGHLIGHT_OPTIONS, useValue: { 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 new file mode 100644 index 00000000..944dc25c --- /dev/null +++ b/frontend/src/app/auth-guards/show-first-run-wizard-guard.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +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) { + + } + + 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() + + if (healthData.first_run_wizard) { + return await this.router.navigate(['/auth/signup/wizard']); + } + + } catch (e) { + // if there is an error, just ignore it, and continue to the signin/signup page. + console.error("ignoring error:", e) + } + // continue as normal + return true + } +} diff --git a/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.html b/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.html index 3e80ab3b..176bc843 100644 --- a/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.html +++ b/frontend/src/app/pages/auth-signup-wizard/auth-signup-wizard.component.html @@ -71,8 +71,8 @@
diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index ed293be5..acf5bccb 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -56,6 +56,16 @@ export class FastenApiService { ); } + getHealth(): Observable { + return this._httpClient.get(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/health`) + .pipe( + map((response: ResponseWrapper) => { + console.log("Health RESPONSE", response) + return response.data + }) + ); + } + /* SECURE ENDPOINTS diff --git a/go.mod b/go.mod index d563f7d3..f2a6a492 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/dave/jennifer v1.6.1 github.com/dominikbraun/graph v0.15.0 github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 - github.com/fastenhealth/fasten-sources v0.5.19 + github.com/fastenhealth/fasten-sources v0.5.20 github.com/fastenhealth/gofhir-models v0.0.6 github.com/gin-gonic/gin v1.9.0 github.com/go-gormigrate/gormigrate/v2 v2.1.1 diff --git a/go.sum b/go.sum index 39247fff..e702cf16 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fastenhealth/fasten-sources v0.5.19 h1:E5NX1LD5P6Ejk2crAxQaA/1rtnFqoiUYYYY2c/3REHQ= -github.com/fastenhealth/fasten-sources v0.5.19/go.mod h1:wD94lmBq6F2mIwT2CBF8owU4gO7E1oY2HUX6bbovOXM= +github.com/fastenhealth/fasten-sources v0.5.20 h1:rNEmn9wmzaa3W1uWmm+uCDjOjyAwUEf7hLrDCMdLfqk= +github.com/fastenhealth/fasten-sources v0.5.20/go.mod h1:wD94lmBq6F2mIwT2CBF8owU4gO7E1oY2HUX6bbovOXM= github.com/fastenhealth/gofhir-models v0.0.6 h1:yJYYaV1eJtHiGEfA1rXLsyOm/9hIi6s2cGoZzGfW1tM= github.com/fastenhealth/gofhir-models v0.0.6/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=