-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #258 from e-picsa/feat/dashboard-user-roles
Feat(dashboard) role-based user permissions
- Loading branch information
Showing
43 changed files
with
1,701 additions
and
875 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
apps/picsa-apps/dashboard/src/app/modules/auth/directives/authRoleRequired.directive.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Directive, effect, inject, input, TemplateRef, ViewContainerRef } from '@angular/core'; | ||
import { IAuthRole } from '@picsa/shared/services/core/supabase/services/supabase-auth.service'; | ||
|
||
import { DashboardAuthService } from '../services/auth.service'; | ||
|
||
/** | ||
* Structural directive used to show/hide UI content based on required deployment auth roles | ||
* https://angular.io/guide/structural-directives#creating-a-structural-directive | ||
* | ||
* @example | ||
* ```html | ||
* <div *roleRequired="'resources.viewer'"> | ||
* ``` | ||
*/ | ||
// eslint-disable-next-line @angular-eslint/directive-selector | ||
@Directive({ selector: '[roleRequired]', standalone: true }) | ||
export class AuthRoleRequiredDirective { | ||
private templateRef = inject(TemplateRef); | ||
private viewContainer = inject(ViewContainerRef); | ||
|
||
/** Track if template currently has view populated */ | ||
private hasView = false; | ||
|
||
/** Input signal to track role required for view */ | ||
public roleRequired = input<IAuthRole>(); | ||
|
||
constructor(service: DashboardAuthService) { | ||
// recalcuate user view permissions whenever requiredRole or deploymentRoles change | ||
effect(() => { | ||
const requiredRole = this.roleRequired(); | ||
const deploymentRoles = service.authRoles(); | ||
const canView = this.doesUserHaveRole(requiredRole, deploymentRoles); | ||
this.setViewContent(canView); | ||
}); | ||
} | ||
|
||
private doesUserHaveRole(requiredRole?: IAuthRole, deploymentRoles?: IAuthRole[]) { | ||
if (!requiredRole) return true; | ||
if (!deploymentRoles) return false; | ||
return deploymentRoles.includes(requiredRole); | ||
} | ||
|
||
/** Dynamically populate or remove associated view content depending on view permissions */ | ||
private setViewContent(canView: boolean) { | ||
if (canView && !this.hasView) { | ||
if (this.viewContainer && this.templateRef) { | ||
this.viewContainer.createEmbeddedView(this.templateRef); | ||
this.hasView = true; | ||
} | ||
} else if (!canView && this.hasView) { | ||
this.viewContainer.clear(); | ||
this.hasView = false; | ||
} | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
apps/picsa-apps/dashboard/src/app/modules/auth/directives/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './authRoleRequired.directive'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './directives'; |
55 changes: 55 additions & 0 deletions
55
apps/picsa-apps/dashboard/src/app/modules/auth/services/auth.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { computed, Injectable } from '@angular/core'; | ||
import { PicsaAsyncService } from '@picsa/shared/services/asyncService.service'; | ||
import { | ||
IAuthRole, | ||
IAuthUser, | ||
SupabaseAuthService, | ||
} from '@picsa/shared/services/core/supabase/services/supabase-auth.service'; | ||
|
||
import { DeploymentDashboardService } from '../../deployment/deployment.service'; | ||
import { IDeploymentRow } from '../../deployment/types'; | ||
|
||
/** | ||
* Authentication and user permission handling | ||
* Adapts Supabase auth to include deployment-specific user role-based-access controls | ||
*/ | ||
@Injectable({ providedIn: 'root' }) | ||
export class DashboardAuthService extends PicsaAsyncService { | ||
public authUser = this.supabaseAuthService.authUser; | ||
|
||
public readonly authRoles = computed<IAuthRole[]>(() => { | ||
const deployment = this.deploymentService.activeDeployment(); | ||
const user = this.supabaseAuthService.authUser(); | ||
return this.getAuthRoles(deployment, user); | ||
}); | ||
|
||
constructor(private deploymentService: DeploymentDashboardService, private supabaseAuthService: SupabaseAuthService) { | ||
super(); | ||
} | ||
|
||
public override async init() { | ||
await this.supabaseAuthService.ready(); | ||
await this.deploymentService.ready(); | ||
} | ||
|
||
private getAuthRoles(deployment: IDeploymentRow | null, user: IAuthUser | undefined) { | ||
if (!deployment) return []; | ||
if (!user) return []; | ||
const authRoles = user.picsa_roles[deployment.id] || []; | ||
// assign default roles to all deployments | ||
const defaultRoles: IAuthRole[] = ['resources.viewer', 'translations.viewer']; | ||
const implicitRoles: IAuthRole[] = []; | ||
for (const role of authRoles) { | ||
const [feature, level] = role.split('.'); | ||
// assign implicit auth roles (anything lower than current level) | ||
if (level === 'admin') { | ||
implicitRoles.push(`${feature}.author` as IAuthRole); | ||
} | ||
if (level === 'admin' || level === 'author') { | ||
implicitRoles.push(`${feature}.viewer` as IAuthRole); | ||
} | ||
} | ||
const uniqueRoles = new Set([...defaultRoles, ...authRoles, ...implicitRoles]); | ||
return [...uniqueRoles]; | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
...oard/src/app/modules/deployment/components/deployment-item/deployment-item.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<div style="display: flex; align-items: center"> | ||
@if(deployment.icon_path){ | ||
<img [src]="deployment.icon_path | storagePath" class="deployment-image" /> | ||
} | ||
<span>{{ deployment.label }}</span> | ||
</div> |
9 changes: 9 additions & 0 deletions
9
...oard/src/app/modules/deployment/components/deployment-item/deployment-item.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
:host { | ||
display: block; | ||
} | ||
.deployment-image { | ||
height: 30px; | ||
width: 30px; | ||
object-fit: contain; | ||
margin-right: 8px; | ||
} |
22 changes: 22 additions & 0 deletions
22
...d/src/app/modules/deployment/components/deployment-item/deployment-item.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
|
||
import { DeploymentItemComponent } from './deployment-item.component'; | ||
|
||
describe('DeploymentItemComponent', () => { | ||
let component: DeploymentItemComponent; | ||
let fixture: ComponentFixture<DeploymentItemComponent>; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
imports: [DeploymentItemComponent], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(DeploymentItemComponent); | ||
component = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('should create', () => { | ||
expect(component).toBeTruthy(); | ||
}); | ||
}); |
17 changes: 17 additions & 0 deletions
17
...hboard/src/app/modules/deployment/components/deployment-item/deployment-item.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { CommonModule } from '@angular/common'; | ||
import { Component, Input } from '@angular/core'; | ||
import { StoragePathPipe } from '@picsa/shared/services/core/supabase'; | ||
|
||
import { IDeploymentRow } from '../../types'; | ||
|
||
/** UI deployment display, consisting of icon and label */ | ||
@Component({ | ||
selector: 'dashboard-deployment-item', | ||
standalone: true, | ||
imports: [CommonModule, StoragePathPipe], | ||
templateUrl: './deployment-item.component.html', | ||
styleUrl: './deployment-item.component.scss', | ||
}) | ||
export class DeploymentItemComponent { | ||
@Input() deployment: IDeploymentRow; | ||
} |
20 changes: 4 additions & 16 deletions
20
.../src/app/modules/deployment/components/deployment-select/deployment-select.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,17 @@ | ||
<button mat-button [matMenuTriggerFor]="menu" [style.visibility]="deployments.length > 0 ? 'visible' : 'hidden'"> | ||
<button mat-button [matMenuTriggerFor]="menu" [style.visibility]="userDeployments().length > 0 ? 'visible' : 'hidden'"> | ||
@if(service.activeDeployment(); as deployment){ | ||
<ng-container *ngTemplateOutlet="deploymentSummary; context: { $implicit: deployment }"></ng-container> | ||
<dashboard-deployment-item [deployment]="deployment" /> | ||
} @else { | ||
<div>Select Deployment</div> | ||
} | ||
<mat-icon iconPositionEnd>unfold_more</mat-icon> | ||
</button> | ||
|
||
<mat-menu #menu="matMenu"> | ||
@for(deployment of deployments; track deployment.id){ | ||
@for(deployment of userDeployments(); track deployment.id){ | ||
<button mat-menu-item class="menu-button" (click)="service.setActiveDeployment(deployment.id)"> | ||
<ng-container *ngTemplateOutlet="deploymentSummary; context: { $implicit: deployment }"></ng-container> | ||
<dashboard-deployment-item [deployment]="deployment" /> | ||
</button> | ||
|
||
} | ||
</mat-menu> | ||
|
||
<ng-template #deploymentSummary let-deployment> | ||
<div style="display: flex; align-items: center"> | ||
@if(deployment.icon_path){ | ||
<img | ||
[src]="deployment.icon_path | storagePath" | ||
style="height: 30px; width: 30px; object-fit: contain; margin-right: 8px" | ||
/> | ||
} | ||
<span>{{ deployment.label }}</span> | ||
</div> | ||
</ng-template> |
Oops, something went wrong.