Skip to content

Commit

Permalink
add basic user management
Browse files Browse the repository at this point in the history
  • Loading branch information
dwradcliffe committed Aug 15, 2024
1 parent 7d07157 commit 178ba18
Show file tree
Hide file tree
Showing 34 changed files with 558 additions and 320 deletions.
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/models"
)

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 models.Role `json:"role"`
}
9 changes: 8 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,12 @@ 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)
return users, 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.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: 9 additions & 6 deletions backend/pkg/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package models

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

"golang.org/x/crypto/bcrypt"
)

type UserWizard struct {
*User `json:",inline"`
JoinMailingList bool `json:"join_mailing_list"`
}
type Role string

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

type User struct {
ModelBase
Expand All @@ -20,7 +23,7 @@ type User struct {
//additional optional metadata that Fasten stores with users
Picture string `json:"picture"`
Email string `json:"email"`
//Roles datatypes.JSON `json:"roles"`
Role Role `json:"role"`
}

func (user *User) HashPassword(password string) error {
Expand Down
52 changes: 48 additions & 4 deletions backend/pkg/web/handler/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,73 @@ package handler

import (
"fmt"
"net/http"

"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/auth"
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-onprem/backend/pkg/utils"
"github.com/gin-gonic/gin"
"net/http"
"github.com/sirupsen/logrus"
)

type UserWizard struct {
*models.User `json:",inline"`
JoinMailingList bool `json:"join_mailing_list"`
}

func RequireAdmin(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)

currentUser, err := databaseRepo.GetCurrentUser(c)
if err != nil {
logger.Errorf("Error getting current user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
if currentUser.Role != models.RoleAdmin {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "Unauthorized"})
return
}
}

func AuthSignup(c *gin.Context) {
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
appConfig := c.MustGet(pkg.ContextKeyTypeConfig).(config.Interface)

var userWizard models.UserWizard
var userWizard UserWizard
if err := c.ShouldBindJSON(&userWizard); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
err := databaseRepo.CreateUser(c, userWizard.User)

// Check if this is the first user in the database
userCount, err := databaseRepo.GetUserCount(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "Failed to check user count"})
return
}

if userCount == 0 {
userWizard.User.Role = "admin"
} else {
userWizard.User.Role = "user"
}
err = databaseRepo.CreateUser(c, userWizard.User)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}

//TODO: we can derive the encryption key and the hash'ed user from the responseData sub. For now the Sub will be the user id prepended with hello.
userFastenToken, err := auth.JwtGenerateFastenTokenFromUser(*userWizard.User, appConfig.GetString("jwt.issuer.key"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}

//check if the user wants to join the mailing list
if userWizard.JoinMailingList {
Expand Down Expand Up @@ -62,7 +102,11 @@ func AuthSignin(c *gin.Context) {
}

//TODO: we can derive the encryption key and the hash'ed user from the responseData sub. For now the Sub will be the user id prepended with hello.
userFastenToken, err := auth.JwtGenerateFastenTokenFromUser(user, appConfig.GetString("jwt.issuer.key"))
userFastenToken, err := auth.JwtGenerateFastenTokenFromUser(*foundUser, appConfig.GetString("jwt.issuer.key"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"success": true, "data": userFastenToken})
}
93 changes: 93 additions & 0 deletions backend/pkg/web/handler/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package handler_test

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/fastenhealth/fasten-onprem/backend/pkg"
mock_config "github.com/fastenhealth/fasten-onprem/backend/pkg/config/mock"
mock_database "github.com/fastenhealth/fasten-onprem/backend/pkg/database/mock"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-onprem/backend/pkg/web/handler"
"github.com/gin-gonic/gin"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)

func TestAuthSignup(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

t.Run("First user should be assigned admin role", func(t *testing.T) {
mockDB := mock_database.NewMockDatabaseRepository(mockCtrl)
mockConfig := mock_config.NewMockInterface(mockCtrl)

mockDB.EXPECT().GetUserCount(gomock.Any()).Return(0, nil)
mockDB.EXPECT().CreateUser(gomock.Any(), gomock.Any()).Do(func(_ interface{}, user *models.User) {
assert.Equal(t, "admin", user.Role)
}).Return(nil)
mockConfig.EXPECT().GetString("jwt.issuer.key").Return("test_key")

w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set(pkg.ContextKeyTypeDatabase, mockDB)
c.Set(pkg.ContextKeyTypeConfig, mockConfig)

userWizard := models.UserWizard{
User: &models.User{
Username: "testuser",
Password: "testpass",
},
}
jsonData, _ := json.Marshal(userWizard)
c.Request, _ = http.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(jsonData))
c.Request.Header.Set("Content-Type", "application/json")

handler.AuthSignup(c)

assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.True(t, response["success"].(bool))
})

t.Run("Subsequent user should be assigned user role", func(t *testing.T) {
mockDB := mock_database.NewMockDatabaseRepository(mockCtrl)
mockConfig := mock_config.NewMockInterface(mockCtrl)

mockDB.EXPECT().GetUserCount(gomock.Any()).Return(1, nil)
mockDB.EXPECT().CreateUser(gomock.Any(), gomock.Any()).Do(func(_ interface{}, user *models.User) {
assert.Equal(t, "user", user.Role)
}).Return(nil)
mockConfig.EXPECT().GetString("jwt.issuer.key").Return("test_key")

w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set(pkg.ContextKeyTypeDatabase, mockDB)
c.Set(pkg.ContextKeyTypeConfig, mockConfig)

userWizard := models.UserWizard{
User: &models.User{
Username: "testuser2",
Password: "testpass2",
},
}
jsonData, _ := json.Marshal(userWizard)
c.Request, _ = http.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(jsonData))
c.Request.Header.Set("Content-Type", "application/json")

handler.AuthSignup(c)

assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.True(t, response["success"].(bool))
})
}
Loading

0 comments on commit 178ba18

Please sign in to comment.