Skip to content

Commit

Permalink
add basic user management (#503)
Browse files Browse the repository at this point in the history
* add basic user management

* allow admin to create another admin

* fix tests

* add multi user info to README
  • Loading branch information
dwradcliffe authored Aug 29, 2024
1 parent fa0decc commit 4a82064
Show file tree
Hide file tree
Showing 35 changed files with 678 additions and 94 deletions.
55 changes: 33 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,10 @@

**Fasten securely connects your healthcare providers together, creating a personal health record that never leaves your hands**

> [!NOTE]
> [!NOTE]
> NOTE: Fasten is a Work-in-Progress and can only communicate with a limited number of Healthcare Instutions (approx 25,000 at last count).
> Please fill out this [Google Form](https://forms.gle/SNsYX9BNMXB6TuTw6) if you'd like to be kept up-to-date on Fasten
> [!IMPORTANT]
> To ensure Fasten's long-term sustainability, we're exploring some funding options. While we're still deciding a long-term monetization strategy, I'm kicking off with a crowdfunding/fundraising experiment for the first 500 users (including a surprise desktop app):
>
> - [Fasten Self-Hosted Lifetime License - **$200**](https://buy.stripe.com/fZe00deiUexS58Y4gg)
>
> Got questions or want to learn more about our fundraising experiment? [Click here to dive into the details & FAQs](https://docs.fastenhealth.com/funding.html)

<p align="center">
<br/>
Expand All @@ -49,19 +42,19 @@

# Introduction

Like many of you, I've worked for many companies over my career. In that time, I've had multiple health, vision and dental
Like many of you, I've worked for many companies over my career. In that time, I've had multiple health, vision and dental
insurance providers, and visited many different clinics, hospitals and labs to get procedures & tests done.

Recently I had a semi-serious medical issue, and I realized that my medical history (and the medical history of my family members)
is a lot more complicated than I realized and distributed across the many healthcare providers I've used over the years.
Recently I had a semi-serious medical issue, and I realized that my medical history (and the medical history of my family members)
is a lot more complicated than I realized and distributed across the many healthcare providers I've used over the years.
I wanted a single (private) location to store our medical records, and I just couldn't find any software that worked as I'd like:

- self-hosted/offline - this is my medical history, I'm not willing to give it to some random multi-national corporation to data-mine and sell
- It should aggregate my data from multiple healthcare providers (insurance companies, hospital networks, clinics, labs) across multiple industries (vision, dental, medical) -- all in one dashboard
- self-hosted/offline - this is my medical history, I'm not willing to give it to some random multi-national corporation to data-mine and sell
- It should aggregate my data from multiple healthcare providers (insurance companies, hospital networks, clinics, labs) across multiple industries (vision, dental, medical) -- all in one dashboard
- automatic - it should pull my EMR (electronic medical record) directly from my insurance provider/clinic/hospital network - I dont want to scan/OCR physical documents (unless I have to)
- open source - the code should be available for contributions & auditing

So, I built it
So, I built it.

**Fasten is an open-source, self-hosted, personal/family electronic medical record aggregator, designed to integrate with 1000's of insurances/hospitals/clinics**

Expand All @@ -74,9 +67,9 @@ It's pretty basic right now, but it's designed with a easily extensible core aro
- Supports the Medical industry's (semi-standard) FHIR protocol
- Uses OAuth2 (Smart-on-FHIR) authentication (no passwords necessary)
- Uses OAuth's `offline_access` scope (where possible) to automatically pull changes/updates
- Multi-user support for household/family use
- (Future) Multi-user support for household/family use
- Condition specific user Dashboards & tracking for diagnostic tests
- (Future) Vaccination & condition specific recommendations using NIH/WHO clinical care guidelines (HEDIS/CQL)
- (Future) Vaccination & condition specific recommendations using NIH/WHO clinical care guidelines (HEDIS/CQL)
- (Future) ChatGPT-style interface to query your own medical history (offline)
- (Future) Integration with smart-devices & wearables

Expand All @@ -95,13 +88,13 @@ First, if you don't have Docker installed on your computer, get Docker by follow
Next, run the following commands from the Windows command line or Mac/Linux terminal in order to download and start the Fasten docker container.

```
docker pull ghcr.io/fastenhealth/fasten-onprem:main
docker pull ghcr.io/fastenhealth/fasten-onprem:main
docker run --rm \
-p 9090:8080 \
-v ./db:/opt/fasten/db \
-v ./cache:/opt/fasten/cache \
ghcr.io/fastenhealth/fasten-onprem:main
ghcr.io/fastenhealth/fasten-onprem:main
```

Next, open a browser to `http://localhost:9090`
Expand All @@ -123,6 +116,26 @@ If you're using the `sandbox` version of Fasten, you'll only be able to connect

https://docs.fastenhealth.com/getting-started/sandbox.html#connecting-a-new-source

## Using with multiple people

> [!NOTE]
> NOTE: Multi-user features are a work in progress. This section describes the eventual goals.
Fasten is designd to work well for an individual or a family. Since it is self-hosted, by nature the person running the service will have full root access to all user records. For most families, this is perfect! If you need stronger security, Fasten might not be for you.

Fasten assumes that all records connected from a single user account (from one or more sources) belong to a single individual, and thus will show aggregations that will only make sense for a single person. Be careful to not connect sources for different people to the same Fasten user account.

Tracking health data for multiple family members works by creating new user accounts for each person. Any user with the `admin` role can manage users and permissions. Any user can be granted access (by an admin) to view another user's records. Through this mechanism, it's easy to setup any family configuration needed. For example: a family of four can have two parents that can each see the records of the two children.

It is also possible to create users with the `viewer` role that only have access to view records of other users. This can be used to share records with a caregiver.

This allows for a more complex example:

- a family consisting of 2 parents, and 2 children and a caregiver (nurse, babysitter, grandparent).
- both parents need to be able to access both children's records, and maybe each-others
- the caregiver should have view-only access to 1 or both children, but not the parents.


# FAQ's

See [FAQs](https://docs.fastenhealth.com/faqs.html) for common questions (& answers) regarding Fasten
Expand Down Expand Up @@ -161,11 +174,9 @@ Jason Kulatunga - Initial Development - @AnalogJ

# Fundraising & Sponsorships

To ensure Fasten's long-term sustainability, we're exploring some funding options. While we're still deciding a long-term monetization strategy, I'm kicking off with a crowdfunding/fundraising experiment for the first 500 users (including a surprise desktop app):

- [Fasten Self-Hosted Lifetime License - **$200**](https://buy.stripe.com/fZe00deiUexS58Y4gg)
To ensure Fasten's long-term sustainability, we're exploring some funding options. We're still deciding a long-term monetization strategy.

Got questions or want to learn more about our fundraising experiment? [Click here to dive into the details & FAQs](https://docs.fastenhealth.com/FUNDRAISING.html)
Got questions or want to learn more about our fundraising experiment? [Click here to dive into the details & FAQs](https://docs.fastenhealth.com/FUNDRAISING.html)

I'd also like to thank the following Corporate Sponsors:

Expand Down
11 changes: 6 additions & 5 deletions backend/pkg/auth/jwt_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package auth
import (
"errors"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/golang-jwt/jwt/v4"
"strings"
"time"

"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/golang-jwt/jwt/v4"
)

// JwtGenerateFastenTokenFromUser Note: these functions are duplicated, in Fasten Cloud
//Any changes here must be replicated in that repo
// Any changes here must be replicated in that repo
func JwtGenerateFastenTokenFromUser(user models.User, issuerSigningKey string) (string, error) {
if len(strings.TrimSpace(issuerSigningKey)) == 0 {
return "", fmt.Errorf("issuer signing key cannot be empty")
Expand All @@ -26,8 +27,8 @@ func JwtGenerateFastenTokenFromUser(user models.User, issuerSigningKey string) (
},
UserMetadata: UserMetadata{
FullName: user.FullName,
Picture: "",
Email: user.ID.String(),
Email: user.Email,
Role: user.Role,
},
}

Expand Down
11 changes: 8 additions & 3 deletions backend/pkg/auth/user_metadata.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package auth

import (
"github.com/fastenhealth/fasten-onprem/backend/pkg"
)

type UserMetadata struct {
FullName string `json:"full_name"`
Picture string `json:"picture"`
Email string `json:"email"`
FullName string `json:"full_name"`
Picture string `json:"picture"`
Email string `json:"email"`
Role pkg.UserRole `json:"role"`
}
4 changes: 4 additions & 0 deletions backend/pkg/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type DatabaseRepositoryType string

type InstallationVerificationStatus string
type InstallationQuotaStatus string
type UserRole string

const (
ResourceListPageSize int = 20
Expand Down Expand Up @@ -50,4 +51,7 @@ const (
InstallationVerificationStatusVerified InstallationVerificationStatus = "VERIFIED" //email has been verified
InstallationQuotaStatusActive InstallationQuotaStatus = "ACTIVE"
InstallationQuotaStatusConsumed InstallationQuotaStatus = "CONSUMED"

UserRoleUser UserRole = "user"
UserRoleAdmin UserRole = "admin"
)
15 changes: 14 additions & 1 deletion backend/pkg/database/gorm_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"strings"
"time"

sourcePkg "github.com/fastenhealth/fasten-sources/pkg"

"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
Expand Down Expand Up @@ -179,6 +180,18 @@ func (gr *GormRepository) DeleteCurrentUser(ctx context.Context) error {
return nil
}

func (gr *GormRepository) GetUsers(ctx context.Context) ([]models.User, error) {
var users []models.User
result := gr.GormClient.WithContext(ctx).Find(&users)
// Remove password field from each user
var sanitizedUsers []models.User
for _, user := range users {
user.Password = "" // Clear the password field
sanitizedUsers = append(sanitizedUsers, user)
}
return sanitizedUsers, result.Error
}

//</editor-fold>

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
33 changes: 32 additions & 1 deletion backend/pkg/database/gorm_repository_migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ package database
import (
"context"
"fmt"
"log"

_20231017112246 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20231017112246"
_20231201122541 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20231201122541"
_0240114092806 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114092806"
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
_20240208112210 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240208112210"
_20240813222836 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240813222836"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/go-gormigrate/gormigrate/v2"
"github.com/google/uuid"
"gorm.io/gorm"
"log"
)

func (gr *GormRepository) Migrate() error {
Expand Down Expand Up @@ -194,6 +196,35 @@ func (gr *GormRepository) Migrate() error {
return nil
},
},
{
ID: "20240813222836", // add role to user
Migrate: func(tx *gorm.DB) error {

err := tx.AutoMigrate(
&_20240813222836.User{},
)
if err != nil {
return err
}

// set first user to admin
// set all other users to user
users := []_20240813222836.User{}
results := tx.Order("created_at ASC").Find(&users)
if results.Error != nil {
return results.Error
}
for ndx, user := range users {
if ndx == 0 {
user.Role = _20240813222836.RoleAdmin
} else {
user.Role = _20240813222836.RoleUser
}
tx.Save(&user)
}
return nil
},
},
})

// run when database is empty
Expand Down
2 changes: 2 additions & 0 deletions backend/pkg/database/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package database

import (
"context"

"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
sourcePkg "github.com/fastenhealth/fasten-sources/clients/models"
Expand All @@ -18,6 +19,7 @@ type DatabaseRepository interface {
GetUserByUsername(context.Context, string) (*models.User, error)
GetCurrentUser(ctx context.Context) (*models.User, error)
DeleteCurrentUser(ctx context.Context) error
GetUsers(ctx context.Context) ([]models.User, error)

GetSummary(ctx context.Context) (*models.Summary, error)

Expand Down
1 change: 0 additions & 1 deletion backend/pkg/database/migrations/20231017112246/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ type User struct {
//additional optional metadata that Fasten stores with users
Picture string `json:"picture"`
Email string `json:"email"`
//Roles datatypes.JSON `json:"roles"`
}
1 change: 0 additions & 1 deletion backend/pkg/database/migrations/20231201122541/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ type User struct {
//additional optional metadata that Fasten stores with users
Picture string `json:"picture"`
Email string `json:"email"`
//Roles datatypes.JSON `json:"roles"`
}
24 changes: 24 additions & 0 deletions backend/pkg/database/migrations/20240813222836/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package _20240813222836

import (
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
)

type Role string

const (
RoleUser Role = "user"
RoleAdmin Role = "admin"
)

type User struct {
models.ModelBase
FullName string `json:"full_name"`
Username string `json:"username" gorm:"unique"`
Password string `json:"password"`

//additional optional metadata that Fasten stores with users
Picture string `json:"picture"`
Email string `json:"email"`
Role Role `json:"role"`
}
15 changes: 15 additions & 0 deletions backend/pkg/database/mock/mock_database.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions backend/pkg/database/sqlite_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package database

import (
"fmt"
"net/url"
"strings"

"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/sirupsen/logrus"
"net/url"
"strings"

//"github.com/glebarez/sqlite"
"gorm.io/driver/sqlite"
Expand Down
16 changes: 7 additions & 9 deletions backend/pkg/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ package models

import (
"fmt"
"golang.org/x/crypto/bcrypt"
"strings"
)

type UserWizard struct {
*User `json:",inline"`
JoinMailingList bool `json:"join_mailing_list"`
}
"golang.org/x/crypto/bcrypt"

"github.com/fastenhealth/fasten-onprem/backend/pkg"
)

type User struct {
ModelBase
Expand All @@ -18,9 +16,9 @@ type User struct {
Password string `json:"password"`

//additional optional metadata that Fasten stores with users
Picture string `json:"picture"`
Email string `json:"email"`
//Roles datatypes.JSON `json:"roles"`
Picture string `json:"picture"`
Email string `json:"email"`
Role pkg.UserRole `json:"role"`
}

func (user *User) HashPassword(password string) error {
Expand Down
Loading

0 comments on commit 4a82064

Please sign in to comment.