diff --git a/backend/pkg/database/gorm_common.go b/backend/pkg/database/gorm_common.go index e6d7a8d1..a183db3d 100644 --- a/backend/pkg/database/gorm_common.go +++ b/backend/pkg/database/gorm_common.go @@ -107,6 +107,66 @@ func (gr *GormRepository) GetCurrentUser(ctx context.Context) (*models.User, err return ¤tUser, nil } +// SECURITY: this should only be called after the user has confirmed they want to delete their account. +func (gr *GormRepository) DeleteCurrentUser(ctx context.Context) error { + currentUser, err := gr.GetCurrentUser(ctx) + if err != nil { + return err + } + + //delete all records associated with this user. + // - background jobs + // - FHIR Resources + // - source credentials + // - related resources + // - user settings + // - user + + //delete background jobs + err = gr.GormClient. + Where(models.BackgroundJob{UserID: currentUser.ID}). + Delete(&models.BackgroundJob{}).Error + if err != nil { + return fmt.Errorf("could not delete background jobs for user: %w", err) + } + + //delete FHIR Resources & sources + sources, err := gr.GetSources(ctx) + if err != nil { + return fmt.Errorf("could not get sources: %w", err) + } + for _, source := range sources { + _, err = gr.DeleteSource(ctx, source.ID.String()) + if err != nil { + return fmt.Errorf("could not delete source (%s) & resources for user: %w", source.ID.String(), err) + } + } + + //delete related resources + err = gr.GormClient. + Where(models.RelatedResource{ResourceBaseUserID: currentUser.ID}). + Delete(&models.RelatedResource{}).Error + if err != nil { + return fmt.Errorf("could not delete related resources for user: %w", err) + } + + //delete user settings + err = gr.GormClient. + Where(models.UserSettingEntry{UserID: currentUser.ID}). + Delete(&models.UserSettingEntry{}).Error + if err != nil { + return fmt.Errorf("could not delete user settings for user: %w", err) + } + //delete user + err = gr.GormClient. + Where(models.User{ModelBase: models.ModelBase{ID: currentUser.ID}}). + Delete(&models.User{}).Error + if err != nil { + return fmt.Errorf("could not delete user: %w", err) + } + return nil +} + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/backend/pkg/database/interface.go b/backend/pkg/database/interface.go index a5ec4f2d..2a6109e5 100644 --- a/backend/pkg/database/interface.go +++ b/backend/pkg/database/interface.go @@ -17,6 +17,7 @@ type DatabaseRepository interface { GetUserByUsername(context.Context, string) (*models.User, error) GetCurrentUser(ctx context.Context) (*models.User, error) + DeleteCurrentUser(ctx context.Context) error GetSummary(ctx context.Context) (*models.Summary, error) diff --git a/backend/pkg/web/handler/account.go b/backend/pkg/web/handler/account.go new file mode 100644 index 00000000..c90aad64 --- /dev/null +++ b/backend/pkg/web/handler/account.go @@ -0,0 +1,25 @@ +package handler + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg" + "github.com/fastenhealth/fasten-onprem/backend/pkg/database" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "net/http" +) + +// SECURITY: this is a secure endpoint, and should only be called after a double confirmation +func DeleteAccount(c *gin.Context) { + logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry) + databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository) + + err := databaseRepo.DeleteCurrentUser(c) + + if err != nil { + logger.Errorln("An error occurred while deleting current user", err) + c.JSON(http.StatusInternalServerError, 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 d5b2c58b..fcc55f31 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -62,6 +62,8 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) { secure := api.Group("/secure").Use(middleware.RequireAuth()) { + secure.DELETE("/account/me", handler.DeleteAccount) + secure.GET("/summary", handler.GetSummary) secure.POST("/source", handler.CreateReconnectSource) diff --git a/frontend/src/app/pages/patient-profile/patient-profile.component.html b/frontend/src/app/pages/patient-profile/patient-profile.component.html index d029af66..8cdb6e15 100644 --- a/frontend/src/app/pages/patient-profile/patient-profile.component.html +++ b/frontend/src/app/pages/patient-profile/patient-profile.component.html @@ -5,7 +5,7 @@ -
+
@@ -173,6 +173,18 @@

+ + +
+ +
+
+
+ +
+
+
+
@@ -186,3 +198,30 @@
+ + + + + + + + + + + diff --git a/frontend/src/app/pages/patient-profile/patient-profile.component.ts b/frontend/src/app/pages/patient-profile/patient-profile.component.ts index ae77a0b3..4890558e 100644 --- a/frontend/src/app/pages/patient-profile/patient-profile.component.ts +++ b/frontend/src/app/pages/patient-profile/patient-profile.component.ts @@ -6,6 +6,7 @@ import {fhirModelFactory} from '../../../lib/models/factory'; import {ResourceType} from '../../../lib/models/constants'; import {ImmunizationModel} from '../../../lib/models/resources/immunization-model'; import {AllergyIntoleranceModel} from '../../../lib/models/resources/allergy-intolerance-model'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-patient-profile', @@ -13,24 +14,27 @@ import {AllergyIntoleranceModel} from '../../../lib/models/resources/allergy-int styleUrls: ['./patient-profile.component.scss'] }) export class PatientProfileComponent implements OnInit { - loading: boolean = false + loading: {[name: string]: boolean} = {page: false, delete: false} + + modalCloseResult = ''; patient: ResourceFhir = null immunizations: ImmunizationModel[] = [] allergyIntolerances: AllergyIntoleranceModel[] = [] constructor( private fastenApi: FastenApiService, + private modalService: NgbModal, ) { } ngOnInit(): void { - this.loading = true + this.loading['page'] = true forkJoin([ this.fastenApi.getResources("Patient"), this.fastenApi.getResources("Immunization"), this.fastenApi.getResources("AllergyIntolerance") ]).subscribe(results => { - this.loading = false + this.loading['page'] = false console.log(results) this.patient = results[0][0] this.immunizations = results[1].map((immunization) => { @@ -40,8 +44,27 @@ export class PatientProfileComponent implements OnInit { return fhirModelFactory(allergy.source_resource_type as ResourceType, allergy) as AllergyIntoleranceModel }) }, error => { - this.loading = false + this.loading['page'] = false }) } + deleteAccount() { + this.loading['delete'] = true + this.fastenApi.deleteAccount().subscribe(result => { + this.loading['delete'] = false + console.log(result) + }, error => { + this.loading['delete'] = false + console.log(error) + }) + } + + openModal(contentModalRef) { + this.modalService.open(contentModalRef, {ariaLabelledBy: 'modal-basic-title'}).result.then((result) => { + this.modalCloseResult = `Closed with: ${result}`; + }, (reason) => { + this.modalCloseResult = `Dismissed ${reason}`; + }); + } + } diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index b331faa3..11d9281b 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -63,6 +63,21 @@ export class FastenApiService { SECURE ENDPOINTS */ + deleteAccount(): Observable { + return this._httpClient.delete(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/account/me`) + .pipe( + map((response: ResponseWrapper) => { + console.log("DELETE ACCOUNT RESPONSE", response) + if(response.success) { + this.authService.Logout().then(() => { + this.router.navigateByUrl('/auth/signup') + }) + } + return response.success + }) + ); + } + //TODO: Any significant API changes here should also be reflected in EventBusService getDashboards(): Observable {