adding ability to delete user account completely

This commit is contained in:
Jason Kulatunga 2024-01-11 13:31:36 -08:00
parent f1c120cbb2
commit 86b336e489
No known key found for this signature in database
7 changed files with 170 additions and 5 deletions

View File

@ -107,6 +107,66 @@ func (gr *GormRepository) GetCurrentUser(ctx context.Context) (*models.User, err
return &currentUser, nil return &currentUser, 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
}
//</editor-fold> //</editor-fold>
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -17,6 +17,7 @@ type DatabaseRepository interface {
GetUserByUsername(context.Context, string) (*models.User, error) GetUserByUsername(context.Context, string) (*models.User, error)
GetCurrentUser(ctx context.Context) (*models.User, error) GetCurrentUser(ctx context.Context) (*models.User, error)
DeleteCurrentUser(ctx context.Context) error
GetSummary(ctx context.Context) (*models.Summary, error) GetSummary(ctx context.Context) (*models.Summary, error)

View File

@ -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})
}

View File

@ -62,6 +62,8 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
secure := api.Group("/secure").Use(middleware.RequireAuth()) secure := api.Group("/secure").Use(middleware.RequireAuth())
{ {
secure.DELETE("/account/me", handler.DeleteAccount)
secure.GET("/summary", handler.GetSummary) secure.GET("/summary", handler.GetSummary)
secure.POST("/source", handler.CreateReconnectSource) secure.POST("/source", handler.CreateReconnectSource)

View File

@ -5,7 +5,7 @@
<!-- Header Row --> <!-- Header Row -->
<report-header [reportHeaderTitle]="'Patient Profile'"></report-header> <report-header [reportHeaderTitle]="'Patient Profile'"></report-header>
<div *ngIf="!loading else isLoadingTemplate" class="pl-3 pr-3"> <div *ngIf="!loading['page'] else isLoadingTemplate" class="pl-3 pr-3">
<!-- Patient Name Row --> <!-- Patient Name Row -->
<div class="row mt-5 mb-3"> <div class="row mt-5 mb-3">
<div class="col-6"> <div class="col-6">
@ -173,6 +173,18 @@
</p> </p>
</div> </div>
</div> </div>
<!-- Account Actions -->
<hr/>
<div class="row" style="padding-bottom:20px">
<div class="col-12">
<div class="btn-group" role="group">
<button (click)="openModal(manageModalRef)" type="button" class="btn btn-outline-danger">Delete Account</button>
</div>
</div>
</div>
</div> </div>
<ng-template #isLoadingTemplate> <ng-template #isLoadingTemplate>
@ -186,3 +198,30 @@
</div> </div>
</div> </div>
</div> </div>
<ng-template #manageModalRef let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Delete Account</h4>
<button type="button" class="btn close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you would like to delete your Fasten Health Account?</p>
<p>
All personal and medical data on this device will be deleted. You will not be able to recover this data.
</p>
</div>
<div class="modal-footer">
<button type="button" (click)="deleteAccount()" class="btn btn-danger">
<span *ngIf="loading['delete']" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Delete
</button>
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
</div>
</ng-template>

View File

@ -6,6 +6,7 @@ import {fhirModelFactory} from '../../../lib/models/factory';
import {ResourceType} from '../../../lib/models/constants'; import {ResourceType} from '../../../lib/models/constants';
import {ImmunizationModel} from '../../../lib/models/resources/immunization-model'; import {ImmunizationModel} from '../../../lib/models/resources/immunization-model';
import {AllergyIntoleranceModel} from '../../../lib/models/resources/allergy-intolerance-model'; import {AllergyIntoleranceModel} from '../../../lib/models/resources/allergy-intolerance-model';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
@Component({ @Component({
selector: 'app-patient-profile', selector: 'app-patient-profile',
@ -13,24 +14,27 @@ import {AllergyIntoleranceModel} from '../../../lib/models/resources/allergy-int
styleUrls: ['./patient-profile.component.scss'] styleUrls: ['./patient-profile.component.scss']
}) })
export class PatientProfileComponent implements OnInit { export class PatientProfileComponent implements OnInit {
loading: boolean = false loading: {[name: string]: boolean} = {page: false, delete: false}
modalCloseResult = '';
patient: ResourceFhir = null patient: ResourceFhir = null
immunizations: ImmunizationModel[] = [] immunizations: ImmunizationModel[] = []
allergyIntolerances: AllergyIntoleranceModel[] = [] allergyIntolerances: AllergyIntoleranceModel[] = []
constructor( constructor(
private fastenApi: FastenApiService, private fastenApi: FastenApiService,
private modalService: NgbModal,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.loading = true this.loading['page'] = true
forkJoin([ forkJoin([
this.fastenApi.getResources("Patient"), this.fastenApi.getResources("Patient"),
this.fastenApi.getResources("Immunization"), this.fastenApi.getResources("Immunization"),
this.fastenApi.getResources("AllergyIntolerance") this.fastenApi.getResources("AllergyIntolerance")
]).subscribe(results => { ]).subscribe(results => {
this.loading = false this.loading['page'] = false
console.log(results) console.log(results)
this.patient = results[0][0] this.patient = results[0][0]
this.immunizations = results[1].map((immunization) => { 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 return fhirModelFactory(allergy.source_resource_type as ResourceType, allergy) as AllergyIntoleranceModel
}) })
}, error => { }, 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}`;
});
}
} }

View File

@ -63,6 +63,21 @@ export class FastenApiService {
SECURE ENDPOINTS SECURE ENDPOINTS
*/ */
deleteAccount(): Observable<boolean> {
return this._httpClient.delete<any>(`${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 //TODO: Any significant API changes here should also be reflected in EventBusService
getDashboards(): Observable<DashboardConfig[]> { getDashboards(): Observable<DashboardConfig[]> {