manage user permissions
This commit is contained in:
parent
4a82064521
commit
fa0b0d74b7
|
@ -10,6 +10,7 @@ type DatabaseRepositoryType string
|
||||||
type InstallationVerificationStatus string
|
type InstallationVerificationStatus string
|
||||||
type InstallationQuotaStatus string
|
type InstallationQuotaStatus string
|
||||||
type UserRole string
|
type UserRole string
|
||||||
|
type Permission string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceListPageSize int = 20
|
ResourceListPageSize int = 20
|
||||||
|
@ -54,4 +55,7 @@ const (
|
||||||
|
|
||||||
UserRoleUser UserRole = "user"
|
UserRoleUser UserRole = "user"
|
||||||
UserRoleAdmin UserRole = "admin"
|
UserRoleAdmin UserRole = "admin"
|
||||||
|
|
||||||
|
PermissionManageSources Permission = "manage_sources"
|
||||||
|
PermissionRead Permission = "read"
|
||||||
)
|
)
|
||||||
|
|
|
@ -192,6 +192,104 @@ func (gr *GormRepository) GetUsers(ctx context.Context) ([]models.User, error) {
|
||||||
return sanitizedUsers, result.Error
|
return sanitizedUsers, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gr *GormRepository) GetUser(ctx context.Context, userID uuid.UUID) (*models.FrontendUser, error) {
|
||||||
|
var dbUser models.User
|
||||||
|
var user models.FrontendUser
|
||||||
|
result := gr.GormClient.WithContext(ctx).First(&dbUser, userID)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
user.ID = dbUser.ID
|
||||||
|
user.FullName = dbUser.FullName
|
||||||
|
user.Email = dbUser.Email
|
||||||
|
user.Username = dbUser.Username
|
||||||
|
user.Role = dbUser.Role
|
||||||
|
|
||||||
|
// Populate ACLs for the user
|
||||||
|
var acls []models.UserPermission
|
||||||
|
if err := gr.GormClient.WithContext(ctx).
|
||||||
|
Where("user_id = ?", user.ID).
|
||||||
|
Find(&acls).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.Permissions = make(map[string]map[string]bool)
|
||||||
|
|
||||||
|
for _, acl := range acls {
|
||||||
|
if _, exists := user.Permissions[acl.TargetUserID.String()]; !exists {
|
||||||
|
user.Permissions[acl.TargetUserID.String()] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
user.Permissions[acl.TargetUserID.String()][string(acl.Permission)] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gr *GormRepository) UpdateUserAndPermissions(ctx context.Context, user models.FrontendUser) error {
|
||||||
|
// Lookup user from the db
|
||||||
|
var dbUser models.User
|
||||||
|
result := gr.GormClient.WithContext(ctx).First(&dbUser, user.ID)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
// Update fields on User
|
||||||
|
result = gr.GormClient.WithContext(ctx).Model(dbUser).Updates(map[string]interface{}{"full_name": user.FullName, "username": user.Username, "email": user.Email, "role": user.Role})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
// Update User Permissions
|
||||||
|
var existingPermissions []models.UserPermission
|
||||||
|
if err := gr.GormClient.WithContext(ctx).
|
||||||
|
Where("user_id = ?", user.ID).
|
||||||
|
Find(&existingPermissions).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for targetUserId, permissions := range user.Permissions {
|
||||||
|
for permission, value := range permissions {
|
||||||
|
if !value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check if the permission already exists
|
||||||
|
exists := false
|
||||||
|
for _, existingPermission := range existingPermissions {
|
||||||
|
if existingPermission.TargetUserID.String() == targetUserId && string(existingPermission.Permission) == permission {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
// Add new permission
|
||||||
|
p := models.UserPermission{
|
||||||
|
UserID: user.ID,
|
||||||
|
TargetUserID: uuid.Must(uuid.Parse(targetUserId)),
|
||||||
|
Permission: pkg.Permission(permission),
|
||||||
|
}
|
||||||
|
err := gr.GormClient.WithContext(ctx).Create(&p).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove permissions that are no longer in user.Permissions
|
||||||
|
for _, existingPermission := range existingPermissions {
|
||||||
|
targetUserId := existingPermission.TargetUserID.String()
|
||||||
|
permission := string(existingPermission.Permission)
|
||||||
|
|
||||||
|
// Check if the permission still exists in the new user.Permissions
|
||||||
|
if _, exists := user.Permissions[targetUserId]; !exists || !user.Permissions[targetUserId][permission] {
|
||||||
|
// Permission no longer exists, so delete it
|
||||||
|
err := gr.GormClient.WithContext(ctx).Delete(&existingPermission).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
//</editor-fold>
|
//</editor-fold>
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
|
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
|
||||||
_20240208112210 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240208112210"
|
_20240208112210 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240208112210"
|
||||||
_20240813222836 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240813222836"
|
_20240813222836 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240813222836"
|
||||||
|
_20240827214347 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240827214347"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||||
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
|
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
|
||||||
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
|
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
|
||||||
|
@ -225,6 +226,20 @@ func (gr *GormRepository) Migrate() error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "20240827214347", // add UserPermission model
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
|
||||||
|
err := tx.AutoMigrate(
|
||||||
|
&_20240827214347.UserPermission{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// run when database is empty
|
// run when database is empty
|
||||||
|
|
|
@ -20,6 +20,8 @@ type DatabaseRepository interface {
|
||||||
GetCurrentUser(ctx context.Context) (*models.User, error)
|
GetCurrentUser(ctx context.Context) (*models.User, error)
|
||||||
DeleteCurrentUser(ctx context.Context) error
|
DeleteCurrentUser(ctx context.Context) error
|
||||||
GetUsers(ctx context.Context) ([]models.User, error)
|
GetUsers(ctx context.Context) ([]models.User, error)
|
||||||
|
GetUser(ctx context.Context, userId uuid.UUID) (*models.FrontendUser, error)
|
||||||
|
UpdateUserAndPermissions(ctx context.Context, user models.FrontendUser) error
|
||||||
|
|
||||||
GetSummary(ctx context.Context) (*models.Summary, error)
|
GetSummary(ctx context.Context) (*models.Summary, error)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package _20240827214347
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Permission string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PermissionManageSources Permission = "manage_sources"
|
||||||
|
PermissionRead Permission = "read"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserPermission struct {
|
||||||
|
models.ModelBase
|
||||||
|
UserID uuid.UUID `json:"user_id" gorm:"type:uuid"`
|
||||||
|
TargetUserID uuid.UUID `json:"target_user_id" gorm:"type:uuid"`
|
||||||
|
Permission Permission `json:"permission"`
|
||||||
|
}
|
|
@ -357,6 +357,21 @@ func (mr *MockDatabaseRepositoryMockRecorder) GetSummary(ctx interface{}) *gomoc
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDatabaseRepository)(nil).GetSummary), ctx)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDatabaseRepository)(nil).GetSummary), ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUser mocks base method.
|
||||||
|
func (m *MockDatabaseRepository) GetUser(ctx context.Context, userId uuid.UUID) (*models.FrontendUser, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetUser", ctx, userId)
|
||||||
|
ret0, _ := ret[0].(*models.FrontendUser)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser indicates an expected call of GetUser.
|
||||||
|
func (mr *MockDatabaseRepositoryMockRecorder) GetUser(ctx, userId interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockDatabaseRepository)(nil).GetUser), ctx, userId)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserByUsername mocks base method.
|
// GetUserByUsername mocks base method.
|
||||||
func (m *MockDatabaseRepository) GetUserByUsername(arg0 context.Context, arg1 string) (*models.User, error) {
|
func (m *MockDatabaseRepository) GetUserByUsername(arg0 context.Context, arg1 string) (*models.User, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -575,6 +590,20 @@ func (mr *MockDatabaseRepositoryMockRecorder) UpdateSource(ctx, sourceCreds inte
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSource", reflect.TypeOf((*MockDatabaseRepository)(nil).UpdateSource), ctx, sourceCreds)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSource", reflect.TypeOf((*MockDatabaseRepository)(nil).UpdateSource), ctx, sourceCreds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUserAndPermissions mocks base method.
|
||||||
|
func (m *MockDatabaseRepository) UpdateUserAndPermissions(ctx context.Context, user models.FrontendUser) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdateUserAndPermissions", ctx, user)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserAndPermissions indicates an expected call of UpdateUserAndPermissions.
|
||||||
|
func (mr *MockDatabaseRepositoryMockRecorder) UpdateUserAndPermissions(ctx, user interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserAndPermissions", reflect.TypeOf((*MockDatabaseRepository)(nil).UpdateUserAndPermissions), ctx, user)
|
||||||
|
}
|
||||||
|
|
||||||
// UpsertRawResource mocks base method.
|
// UpsertRawResource mocks base method.
|
||||||
func (m *MockDatabaseRepository) UpsertRawResource(ctx context.Context, sourceCredentials models0.SourceCredential, rawResource models0.RawResourceFhir) (bool, error) {
|
func (m *MockDatabaseRepository) UpsertRawResource(ctx context.Context, sourceCredentials models0.SourceCredential, rawResource models0.RawResourceFhir) (bool, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
@ -14,13 +14,18 @@ type User struct {
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
Username string `json:"username" gorm:"unique"`
|
Username string `json:"username" gorm:"unique"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
|
||||||
//additional optional metadata that Fasten stores with users
|
|
||||||
Picture string `json:"picture"`
|
Picture string `json:"picture"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Role pkg.UserRole `json:"role"`
|
Role pkg.UserRole `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FrontendUser is User with the addition of Permissions arranged
|
||||||
|
// as we want for sending to and from the frontend
|
||||||
|
type FrontendUser struct {
|
||||||
|
User
|
||||||
|
Permissions map[string]map[string]bool `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) HashPassword(password string) error {
|
func (user *User) HashPassword(password string) error {
|
||||||
if len(strings.TrimSpace(password)) == 0 {
|
if len(strings.TrimSpace(password)) == 0 {
|
||||||
return fmt.Errorf("password cannot be empty")
|
return fmt.Errorf("password cannot be empty")
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserPermission struct {
|
||||||
|
ModelBase
|
||||||
|
UserID uuid.UUID `json:"user_id" gorm:"type:uuid"`
|
||||||
|
TargetUserID uuid.UUID `json:"target_user_id" gorm:"type:uuid"`
|
||||||
|
Permission pkg.Permission `json:"permission"`
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,3 +55,43 @@ func CreateUser(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": newUser})
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": newUser})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUser(c *gin.Context) {
|
||||||
|
if !IsAdmin(c) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "Unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
||||||
|
|
||||||
|
user, err := databaseRepo.GetUser(c, uuid.MustParse(c.Param("userId")))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{"success": true, "data": user})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateUser(c *gin.Context) {
|
||||||
|
if !IsAdmin(c) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "Unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
||||||
|
|
||||||
|
var user models.FrontendUser
|
||||||
|
if err := c.ShouldBindJSON(&user); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := databaseRepo.UpdateUserAndPermissions(c, user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{"success": true, "data": user})
|
||||||
|
}
|
||||||
|
|
|
@ -128,6 +128,8 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
|
||||||
|
|
||||||
secure.GET("/users", handler.GetUsers)
|
secure.GET("/users", handler.GetUsers)
|
||||||
secure.POST("/users", handler.CreateUser)
|
secure.POST("/users", handler.CreateUser)
|
||||||
|
secure.GET("/users/:userId", handler.GetUser)
|
||||||
|
secure.POST("/users/:userId", handler.UpdateUser)
|
||||||
|
|
||||||
//server-side-events handler (only supported on mac/linux)
|
//server-side-events handler (only supported on mac/linux)
|
||||||
// TODO: causes deadlock on Windows
|
// TODO: causes deadlock on Windows
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { ResourceCreatorComponent } from './pages/resource-creator/resource-crea
|
||||||
import { ResourceDetailComponent } from './pages/resource-detail/resource-detail.component';
|
import { ResourceDetailComponent } from './pages/resource-detail/resource-detail.component';
|
||||||
import { SourceDetailComponent } from './pages/source-detail/source-detail.component';
|
import { SourceDetailComponent } from './pages/source-detail/source-detail.component';
|
||||||
import { UserCreateComponent } from './pages/user-create/user-create.component';
|
import { UserCreateComponent } from './pages/user-create/user-create.component';
|
||||||
|
import { UserEditComponent } from "./pages/user-edit/user-edit.component";
|
||||||
import { UserListComponent } from './pages/user-list/user-list.component';
|
import { UserListComponent } from './pages/user-list/user-list.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
|
@ -55,6 +56,7 @@ const routes: Routes = [
|
||||||
|
|
||||||
{ path: 'users', component: UserListComponent, canActivate: [ IsAuthenticatedAuthGuard, IsAdminAuthGuard ] },
|
{ path: 'users', component: UserListComponent, canActivate: [ IsAuthenticatedAuthGuard, IsAdminAuthGuard ] },
|
||||||
{ path: 'users/new', component: UserCreateComponent, canActivate: [ IsAuthenticatedAuthGuard, IsAdminAuthGuard ] },
|
{ path: 'users/new', component: UserCreateComponent, canActivate: [ IsAuthenticatedAuthGuard, IsAdminAuthGuard ] },
|
||||||
|
{ path: 'users/:user_id', component: UserEditComponent, canActivate: [ IsAuthenticatedAuthGuard, IsAdminAuthGuard ] },
|
||||||
|
|
||||||
// { path: 'general-pages', loadChildren: () => import('./general-pages/general-pages.module').then(m => m.GeneralPagesModule) },
|
// { path: 'general-pages', loadChildren: () => import('./general-pages/general-pages.module').then(m => m.GeneralPagesModule) },
|
||||||
// { path: 'ui-elements', loadChildren: () => import('./ui-elements/ui-elements.module').then(m => m.UiElementsModule) },
|
// { path: 'ui-elements', loadChildren: () => import('./ui-elements/ui-elements.module').then(m => m.UiElementsModule) },
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
|
export const POSSIBLE_PERMISSIONS = [
|
||||||
|
{ name: 'Manage Sources', value: 'manage_sources' },
|
||||||
|
{ name: 'Read', value: 'read' },
|
||||||
|
]
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
user_id?: number
|
id?: string
|
||||||
full_name?: string
|
full_name?: string
|
||||||
username?: string
|
username?: string
|
||||||
email?: string
|
email?: string
|
||||||
password?: string
|
password?: string
|
||||||
role?: string
|
role?: string
|
||||||
|
permissions?: {
|
||||||
|
[targetUserId: string]: {
|
||||||
|
[key in typeof POSSIBLE_PERMISSIONS[number]['value']]: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
<div class="az-content">
|
||||||
|
<div class="container">
|
||||||
|
<div class="az-content-body">
|
||||||
|
<h2 class="az-content-title">Edit User</h2>
|
||||||
|
|
||||||
|
<div *ngIf="loading" class="text-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="errorMessage" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
{{ errorMessage }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close" (click)="errorMessage = null">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form *ngIf="!loading && !errorMessage" [formGroup]="userForm" (ngSubmit)="onSubmit()" class="pb-5 mb-5">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="full_name">Full Name</label>
|
||||||
|
<input type="text" id="full_name" formControlName="full_name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" formControlName="username" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="role">Role</label>
|
||||||
|
<select id="role" formControlName="role" class="form-control" required>
|
||||||
|
<option value="user" selected>User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
<div *ngIf="userForm.get('role')?.value === 'admin'" class="text-danger mt-2">
|
||||||
|
<strong>Warning:</strong> This will allow full system access including the ability to manage other users.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" formControlName="email" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Access to other Users</h5>
|
||||||
|
|
||||||
|
<div formGroupName="permissions">
|
||||||
|
<div *ngFor="let otherUser of userList" class="">
|
||||||
|
<div class="text">{{ otherUser.full_name }}</div>
|
||||||
|
<div formGroupName="{{otherUser.id}}" class="ml-3 p-1">
|
||||||
|
<div *ngFor="let permission of permissionsList" class="form-check">
|
||||||
|
<input type="checkbox" [formControlName]="permission.value" class="form-check-input" id="{{otherUser.id}}_{{permission.value}}">
|
||||||
|
<label class="form-check-label" for="{{otherUser.id}}_{{permission.value}}">{{ permission.name }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-az-primary mt-2" [disabled]="!userForm.valid || loading">
|
||||||
|
Update User
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { forkJoin } from 'rxjs';
|
||||||
|
import { ToastNotification, ToastType } from '../../models/fasten/toast';
|
||||||
|
import { POSSIBLE_PERMISSIONS, User } from '../../models/fasten/user';
|
||||||
|
import { AuthService } from '../../services/auth.service';
|
||||||
|
import { FastenApiService } from '../../services/fasten-api.service';
|
||||||
|
import { ToastService } from '../../services/toast.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-edit',
|
||||||
|
templateUrl: './user-edit.component.html',
|
||||||
|
styleUrls: ['./user-edit.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ReactiveFormsModule]
|
||||||
|
})
|
||||||
|
export class UserEditComponent implements OnInit {
|
||||||
|
userForm: FormGroup;
|
||||||
|
loading = false;
|
||||||
|
userId: string;
|
||||||
|
userList: User[];
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
permissionsList = POSSIBLE_PERMISSIONS;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private authService: AuthService,
|
||||||
|
private fastenApi: FastenApiService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loading = true;
|
||||||
|
this.userId = this.route.snapshot.paramMap.get('user_id');
|
||||||
|
forkJoin([
|
||||||
|
this.fastenApi.getAllUsers(),
|
||||||
|
this.fastenApi.getUser(this.userId)
|
||||||
|
]).subscribe(([allUsers, user]) => {
|
||||||
|
this.userList = allUsers.filter(user => user.id !== this.userId).sort((a, b) => a.full_name.localeCompare(b.full_name));
|
||||||
|
this.userForm = this.fb.group({
|
||||||
|
full_name: [user.full_name, [Validators.required, Validators.minLength(2)]],
|
||||||
|
username: [user.username, [Validators.required, Validators.minLength(4)]],
|
||||||
|
email: [user.email, [Validators.email]],
|
||||||
|
role: [user.role, Validators.required],
|
||||||
|
permissions: this.fb.group({})
|
||||||
|
});
|
||||||
|
this.userList.forEach(otherUser => {
|
||||||
|
const pfg = (this.userForm.get('permissions') as FormGroup);
|
||||||
|
pfg.addControl(otherUser.id, this.fb.group({}));
|
||||||
|
this.permissionsList.forEach(permission => {
|
||||||
|
const isChecked = user.permissions?.[otherUser.id]?.[permission.value] ?? false;
|
||||||
|
(pfg.get(otherUser.id) as FormGroup).addControl(permission.value, new FormControl(isChecked));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.errorMessage = error.message;
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
if (this.userForm.valid) {
|
||||||
|
this.loading = true;
|
||||||
|
this.errorMessage = null;
|
||||||
|
|
||||||
|
const user: User = this.userForm.value;
|
||||||
|
user.id = this.userId;
|
||||||
|
this.authService.updateUser(user).subscribe(
|
||||||
|
(response) => {
|
||||||
|
this.loading = false;
|
||||||
|
const toastNotification = new ToastNotification();
|
||||||
|
toastNotification.type = ToastType.Success;
|
||||||
|
toastNotification.message = 'User updated successfully';
|
||||||
|
this.toastService.show(toastNotification);
|
||||||
|
this.router.navigate(['/users']);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.errorMessage = 'Error updating user: ' + error.message;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,13 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="az-content-body">
|
<div class="az-content-body">
|
||||||
<h2 class="az-content-title">User List</h2>
|
<h2 class="az-content-title">User List</h2>
|
||||||
|
|
||||||
<div *ngIf="loading" class="text-center">
|
<div *ngIf="loading" class="text-center">
|
||||||
<div class="spinner-border" role="status">
|
<div class="spinner-border" role="status">
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table *ngIf="!loading" class="table">
|
<table *ngIf="!loading" class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -14,6 +16,7 @@
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -22,6 +25,9 @@
|
||||||
<td>{{ user.username }}</td>
|
<td>{{ user.username }}</td>
|
||||||
<td>{{ user.email }}</td>
|
<td>{{ user.email }}</td>
|
||||||
<td><span class="badge badge-primary">{{ user.role }}</span></td>
|
<td><span class="badge badge-primary">{{ user.role }}</span></td>
|
||||||
|
<td>
|
||||||
|
<a class="mx-2" routerLink="/users/{{ user.id }}">Edit</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -24,13 +24,15 @@ export class UserListComponent implements OnInit {
|
||||||
|
|
||||||
loadUsers(): void {
|
loadUsers(): void {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.fastenApi.getAllUsers().subscribe((users: User[]) => {
|
this.fastenApi.getAllUsers().subscribe(
|
||||||
|
(users: User[]) => {
|
||||||
this.users = users;
|
this.users = users;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error => {
|
(error: Error) => {
|
||||||
console.error('Error loading users:', error);
|
console.error('Error loading users:', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,21 @@ export class AuthService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateUser(user: User): Observable<any> {
|
||||||
|
let fastenApiEndpointBase = GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base);
|
||||||
|
return this._httpClient.post<ResponseWrapper>(`${fastenApiEndpointBase}/secure/users/${user.id}`, user)
|
||||||
|
.pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
if (error.status === 400) {
|
||||||
|
// Extract error information from the response body
|
||||||
|
const errorBody = error.error;
|
||||||
|
return throwError(new Error(errorBody.error || error.message));
|
||||||
|
}
|
||||||
|
return throwError(error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: now that we've moved to remote-first database, we can refactor and simplify this function significantly.
|
//TODO: now that we've moved to remote-first database, we can refactor and simplify this function significantly.
|
||||||
public async IsAuthenticated(): Promise<boolean> {
|
public async IsAuthenticated(): Promise<boolean> {
|
||||||
let authToken = this.GetAuthToken()
|
let authToken = this.GetAuthToken()
|
||||||
|
|
|
@ -1,33 +1,27 @@
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { List, ValueSet } from 'fhir/r4';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { Router } from '@angular/router';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import {map} from 'rxjs/operators';
|
|
||||||
import {ResponseWrapper} from '../models/response-wrapper';
|
|
||||||
import {Source} from '../models/fasten/source';
|
|
||||||
import {User} from '../models/fasten/user';
|
|
||||||
import {ResourceFhir} from '../models/fasten/resource_fhir';
|
|
||||||
import {SourceSummary} from '../models/fasten/source-summary';
|
|
||||||
import {Summary} from '../models/fasten/summary';
|
|
||||||
import {AuthService} from './auth.service';
|
|
||||||
import {GetEndpointAbsolutePath} from '../../lib/utils/endpoint_absolute_path';
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import {ValueSet} from 'fhir/r4';
|
|
||||||
import { AttachmentModel } from '../../lib/models/datatypes/attachment-model';
|
import { AttachmentModel } from '../../lib/models/datatypes/attachment-model';
|
||||||
import { BinaryModel } from '../../lib/models/resources/binary-model';
|
import { BinaryModel } from '../../lib/models/resources/binary-model';
|
||||||
|
import { GetEndpointAbsolutePath } from '../../lib/utils/endpoint_absolute_path';
|
||||||
import { HTTP_CLIENT_TOKEN } from "../dependency-injection";
|
import { HTTP_CLIENT_TOKEN } from "../dependency-injection";
|
||||||
import * as fhirpath from 'fhirpath';
|
import { BackgroundJob, BackgroundJobSyncData } from '../models/fasten/background-job';
|
||||||
import _ from 'lodash';
|
import { FormRequestHealthSystem } from '../models/fasten/form-request-health-system';
|
||||||
|
import { ResourceGraphResponse } from '../models/fasten/resource-graph-response';
|
||||||
|
import { ResourceFhir } from '../models/fasten/resource_fhir';
|
||||||
|
import { Source } from '../models/fasten/source';
|
||||||
|
import { SourceSummary } from '../models/fasten/source-summary';
|
||||||
|
import { Summary } from '../models/fasten/summary';
|
||||||
|
import { SupportRequest } from '../models/fasten/support-request';
|
||||||
|
import { User } from '../models/fasten/user';
|
||||||
|
import { ResponseWrapper } from '../models/response-wrapper';
|
||||||
import { DashboardConfig } from '../models/widget/dashboard-config';
|
import { DashboardConfig } from '../models/widget/dashboard-config';
|
||||||
import { DashboardWidgetQuery } from '../models/widget/dashboard-widget-query';
|
import { DashboardWidgetQuery } from '../models/widget/dashboard-widget-query';
|
||||||
import {ResourceGraphResponse} from '../models/fasten/resource-graph-response';
|
import { AuthService } from './auth.service';
|
||||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
||||||
import {BackgroundJob, BackgroundJobSyncData} from '../models/fasten/background-job';
|
|
||||||
import {SupportRequest} from '../models/fasten/support-request';
|
|
||||||
import {
|
|
||||||
List
|
|
||||||
} from 'fhir/r4';
|
|
||||||
import {FormRequestHealthSystem} from '../models/fasten/form-request-health-system';
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
|
@ -368,4 +362,17 @@ export class FastenApiService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUser(userId: string): Observable<User> {
|
||||||
|
return this._httpClient.get<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/users/${userId}`)
|
||||||
|
.pipe(
|
||||||
|
map((response: ResponseWrapper) => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error)
|
||||||
|
}
|
||||||
|
return response.data as User
|
||||||
|
}),
|
||||||
|
// catchError(() => of(null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue