manage user permissions
This commit is contained in:
parent
4a82064521
commit
fa0b0d74b7
|
@ -10,6 +10,7 @@ type DatabaseRepositoryType string
|
|||
type InstallationVerificationStatus string
|
||||
type InstallationQuotaStatus string
|
||||
type UserRole string
|
||||
type Permission string
|
||||
|
||||
const (
|
||||
ResourceListPageSize int = 20
|
||||
|
@ -54,4 +55,7 @@ const (
|
|||
|
||||
UserRoleUser UserRole = "user"
|
||||
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
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
_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"
|
||||
_20240827214347 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240827214347"
|
||||
"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"
|
||||
|
@ -225,6 +226,20 @@ func (gr *GormRepository) Migrate() error {
|
|||
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
|
||||
|
|
|
@ -20,6 +20,8 @@ type DatabaseRepository interface {
|
|||
GetCurrentUser(ctx context.Context) (*models.User, error)
|
||||
DeleteCurrentUser(ctx context.Context) 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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockDatabaseRepository) GetUserByUsername(arg0 context.Context, arg1 string) (*models.User, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockDatabaseRepository) UpsertRawResource(ctx context.Context, sourceCredentials models0.SourceCredential, rawResource models0.RawResourceFhir) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -11,14 +11,19 @@ import (
|
|||
|
||||
type User struct {
|
||||
ModelBase
|
||||
FullName string `json:"full_name"`
|
||||
Username string `json:"username" gorm:"unique"`
|
||||
Password string `json:"password"`
|
||||
FullName string `json:"full_name"`
|
||||
Username string `json:"username" gorm:"unique"`
|
||||
Password string `json:"password"`
|
||||
Picture string `json:"picture"`
|
||||
Email string `json:"email"`
|
||||
Role pkg.UserRole `json:"role"`
|
||||
}
|
||||
|
||||
//additional optional metadata that Fasten stores with users
|
||||
Picture string `json:"picture"`
|
||||
Email string `json:"email"`
|
||||
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 {
|
||||
|
|
|
@ -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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
@ -54,3 +55,43 @@ func CreateUser(c *gin.Context) {
|
|||
|
||||
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.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)
|
||||
// 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 { SourceDetailComponent } from './pages/source-detail/source-detail.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';
|
||||
|
||||
const routes: Routes = [
|
||||
|
@ -55,6 +56,7 @@ const routes: Routes = [
|
|||
|
||||
{ path: 'users', component: UserListComponent, 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: '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 {
|
||||
user_id?: number
|
||||
id?: string
|
||||
full_name?: string
|
||||
username?: string
|
||||
email?: string
|
||||
password?: 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="az-content-body">
|
||||
<h2 class="az-content-title">User List</h2>
|
||||
|
||||
<div *ngIf="loading" class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table *ngIf="!loading" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -14,6 +16,7 @@
|
|||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -22,6 +25,9 @@
|
|||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td><span class="badge badge-primary">{{ user.role }}</span></td>
|
||||
<td>
|
||||
<a class="mx-2" routerLink="/users/{{ user.id }}">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -24,13 +24,15 @@ export class UserListComponent implements OnInit {
|
|||
|
||||
loadUsers(): void {
|
||||
this.loading = true;
|
||||
this.fastenApi.getAllUsers().subscribe((users: User[]) => {
|
||||
this.users = users;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.fastenApi.getAllUsers().subscribe(
|
||||
(users: User[]) => {
|
||||
this.users = users;
|
||||
this.loading = false;
|
||||
},
|
||||
(error: Error) => {
|
||||
console.error('Error loading users:', error);
|
||||
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.
|
||||
public async IsAuthenticated(): Promise<boolean> {
|
||||
let authToken = this.GetAuthToken()
|
||||
|
|
|
@ -1,33 +1,27 @@
|
|||
import {Inject, Injectable} from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
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 {ValueSet} from 'fhir/r4';
|
||||
import {AttachmentModel} from '../../lib/models/datatypes/attachment-model';
|
||||
import {BinaryModel} from '../../lib/models/resources/binary-model';
|
||||
import {HTTP_CLIENT_TOKEN} from "../dependency-injection";
|
||||
import * as fhirpath from 'fhirpath';
|
||||
import _ from 'lodash';
|
||||
import {DashboardConfig} from '../models/widget/dashboard-config';
|
||||
import {DashboardWidgetQuery} from '../models/widget/dashboard-widget-query';
|
||||
import {ResourceGraphResponse} from '../models/fasten/resource-graph-response';
|
||||
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';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { List, ValueSet } from 'fhir/r4';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { AttachmentModel } from '../../lib/models/datatypes/attachment-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 { BackgroundJob, BackgroundJobSyncData } from '../models/fasten/background-job';
|
||||
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 { DashboardWidgetQuery } from '../models/widget/dashboard-widget-query';
|
||||
import { AuthService } from './auth.service';
|
||||
@Injectable({
|
||||
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