migrate permission logic to more backend

This commit is contained in:
rubikscraft 2022-03-24 23:08:37 +01:00
parent af82a749bc
commit 2c150c3027
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
36 changed files with 166 additions and 129 deletions

View file

@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import {
AsyncFailable,
Fail,
@ -10,6 +9,7 @@ import {
} from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { In, Repository } from 'typeorm';
import { Permissions } from '../../models/dto/permissions.dto';
import { ImmutableRolesList, UndeletableRolesList } from '../../models/dto/roles.dto';
import { ERoleBackend } from '../../models/entities/role.entity';

View file

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
import { Permissions } from '../../models/dto/permissions.dto';
import { EUserBackend } from '../../models/entities/user.entity';
import { RolesService } from '../roledb/roledb.service';
import { UsersService } from './userdb.service';

View file

@ -1,7 +1,7 @@
import { SetMetadata, UseGuards } from '@nestjs/common';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { CombineDecorators } from 'picsur-shared/dist/util/decorator';
import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard';
import { Permissions } from '../models/dto/permissions.dto';
export const RequiredPermissions = (...permissions: Permissions) => {
return SetMetadata('permissions', permissions);

View file

@ -8,15 +8,13 @@ import {
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import {
Permissions
} from 'picsur-shared/dist/dto/permissions';
import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
import { isPermissionsArray } from 'picsur-shared/dist/util/permissions';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { UsersService } from '../../../collections/userdb/userdb.service';
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
import { Permissions } from '../../../models/dto/permissions.dto';
import { EUserBackend } from '../../../models/entities/user.entity';
import { isPermissionsArray } from '../../../models/util/permissions';
@Injectable()
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {

View file

@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { ImageDBService } from '../../collections/imagedb/imagedb.service';
import { RolesService } from '../../collections/roledb/roledb.service';
import { Permission } from '../../models/dto/permissions.dto';
@Injectable()
export class DemoManagerService {

View file

@ -0,0 +1,10 @@
// Config
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
export { Permission } from 'picsur-shared/dist/dto/permissions.dto';
// Derivatives
export const PermissionsList: Permission[] = Object.values(Permission);
export type Permissions = Permission[];

View file

@ -1,5 +1,5 @@
import { Permission, Permissions, PermissionsList } from 'picsur-shared/dist/dto/permissions';
import tuple from 'picsur-shared/dist/types/tuple';
import { Permission, Permissions, PermissionsList } from './permissions.dto';
// Config
@ -8,7 +8,10 @@ const SoulBoundRolesTuple = tuple('guest', 'user');
// These roles can never be modified
const ImmutableRolesTuple = tuple('admin');
// These roles can never be removed from the server
const UndeletableRolesTuple = tuple(...SoulBoundRolesTuple, ...ImmutableRolesTuple);
const UndeletableRolesTuple = tuple(
...SoulBoundRolesTuple,
...ImmutableRolesTuple,
);
// These roles will be applied by default to new users
export const DefaultRolesList: string[] = ['user'];

View file

@ -1,6 +1,6 @@
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import { Permissions } from '../dto/permissions.dto';
@Entity()
export class ERoleBackend extends ERole {

View file

@ -0,0 +1,10 @@
import { isArray, isEnum, isString } from 'class-validator';
import { Permissions, PermissionsList } from '../dto/permissions.dto';
export function isPermissionsArray(value: any): value is Permissions {
if (!isArray(value)) return false;
if (!value.every((item: unknown) => isString(item))) return false;
if (!value.every((item: string) => isEnum(item, PermissionsList)))
return false;
return true;
}

View file

@ -1,14 +1,17 @@
import { Controller, Get } from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { InfoResponse } from 'picsur-shared/dist/dto/api/info.dto';
import { AllPermissionsResponse } from 'picsur-shared/dist/dto/api/roles.dto';
import { HostConfigService } from '../../../config/host.config.service';
import { NoPermissions } from '../../../decorators/permissions.decorator';
import { PermissionsList } from '../../../models/dto/permissions.dto';
@Controller('api/info')
@NoPermissions()
export class InfoController {
constructor(private hostConfig: HostConfigService) {}
@Get()
@NoPermissions()
async getInfo(): Promise<InfoResponse> {
return {
demo: this.hostConfig.isDemo(),
@ -16,4 +19,14 @@ export class InfoController {
version: this.hostConfig.getVersion(),
};
}
// List all available permissions
@Get('/permissions')
async getPermissions(): Promise<AllPermissionsResponse> {
const result: AllPermissionsResponse = {
Permissions: PermissionsList,
};
return plainToClass(AllPermissionsResponse, result);
}
}

View file

@ -15,11 +15,11 @@ import {
UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse
} from 'picsur-shared/dist/dto/api/pref.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import { Permission } from '../../../models/dto/permissions.dto';
@Controller('api/pref')
@RequiredPermissions(Permission.SysPrefManage)

View file

@ -19,16 +19,19 @@ import {
RoleUpdateResponse,
SpecialRolesResponse
} from 'picsur-shared/dist/dto/api/roles.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types';
import { RolesService } from '../../../collections/roledb/roledb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import {
Permission
} from '../../../models/dto/permissions.dto';
import {
DefaultRolesList,
ImmutableRolesList,
SoulBoundRolesList,
UndeletableRolesList
} from '../../../models/dto/roles.dto';
import { isPermissionsArray } from '../../../models/util/permissions';
@Controller('api/roles')
@RequiredPermissions(Permission.RoleManage)
@ -66,9 +69,14 @@ export class RolesController {
async updateRole(
@Body() body: RoleUpdateRequest,
): Promise<RoleUpdateResponse> {
const permissions = body.permissions;
if (!isPermissionsArray(permissions)) {
throw new InternalServerErrorException('Invalid permissions array');
}
const updatedRole = await this.rolesService.setPermissions(
body.name,
body.permissions,
permissions,
);
if (HasFailed(updatedRole)) {
this.logger.warn(updatedRole.getReason());
@ -82,7 +90,12 @@ export class RolesController {
async createRole(
@Body() role: RoleCreateRequest,
): Promise<RoleCreateResponse> {
const newRole = await this.rolesService.create(role.name, role.permissions);
const permissions = role.permissions;
if (!isPermissionsArray(permissions)) {
throw new InternalServerErrorException('Invalid permissions array');
}
const newRole = await this.rolesService.create(role.name, permissions);
if (HasFailed(newRole)) {
this.logger.warn(newRole.getReason());
throw new InternalServerErrorException('Could not create role');
@ -104,7 +117,7 @@ export class RolesController {
return deletedRole;
}
@Get('special')
@Get('/special')
async getSpecialRoles(): Promise<SpecialRolesResponse> {
const result: SpecialRolesResponse = {
SoulBoundRoles: SoulBoundRolesList,
@ -115,4 +128,5 @@ export class RolesController {
return plainToClass(SpecialRolesResponse, result);
}
}

View file

@ -14,7 +14,6 @@ import {
UserRegisterRequest,
UserRegisterResponse
} from 'picsur-shared/dist/dto/api/user.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service';
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
@ -24,6 +23,7 @@ import {
UseLocalAuth
} from '../../../decorators/permissions.decorator';
import { AuthManagerService } from '../../../managers/auth/auth.service';
import { Permission } from '../../../models/dto/permissions.dto';
import AuthFasityRequest from '../../../models/requests/authrequest.dto';
@Controller('api/user')

View file

@ -20,10 +20,10 @@ import {
UserUpdateRequest,
UserUpdateResponse
} from 'picsur-shared/dist/dto/api/usermanage.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import { Permission } from '../../../models/dto/permissions.dto';
import { ImmutableUsersList, LockedLoginUsersList, UndeletableUsersList } from '../../../models/dto/specialusers.dto';
@Controller('api/user')

View file

@ -13,11 +13,11 @@ import {
import { isHash } from 'class-validator';
import { FastifyReply, FastifyRequest } from 'fastify';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator';
import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { Permission } from '../../models/dto/permissions.dto';
import { ImageUploadDto } from '../../models/requests/imageroute.dto';
@Controller('i')

View file

@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { HasFailed } from 'picsur-shared/dist/types';
import { SnackBarType } from 'src/app/models/snack-bar-type';
@ -21,7 +21,7 @@ export class HeaderComponent implements OnInit {
@Output('onHamburgerClick') onHamburgerClick = new EventEmitter<void>();
private currentUser: EUser | null = null;
private permissions: Permissions = [];
private permissions: string[] = [];
public get user() {
return this.currentUser;

View file

@ -6,8 +6,6 @@ import {
Router,
RouterStateSnapshot
} from '@angular/router';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { isPermissionsArray } from 'picsur-shared/dist/util/permissions';
import { PRouteData } from '../models/picsur-routes';
import { PermissionService } from '../services/api/permission.service';
@ -34,12 +32,13 @@ export class PermissionGuard implements CanActivate, CanActivateChild {
}
private async can(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const requiredPermissions: Permissions = this.nestedPermissions(route);
if (!isPermissionsArray(requiredPermissions)) {
throw new Error(
`PermissionGuard: route data 'permissions' must be an array of Permission values`
);
}
const requiredPermissions: string[] = this.nestedPermissions(route);
// TODO: revive
// if (!isPermissionsArray(requiredPermissions)) {
// throw new Error(
// `PermissionGuard: route data 'permissions' must be an array of Permission values`
// );
// }
const ourPermissions = await this.permissionService.loadedSnapshot();
@ -53,10 +52,10 @@ export class PermissionGuard implements CanActivate, CanActivateChild {
return isOk;
}
private nestedPermissions(route: ActivatedRouteSnapshot): Permissions {
private nestedPermissions(route: ActivatedRouteSnapshot): string[] {
const data: PRouteData = route.data;
let permissions: Permissions = [];
let permissions: string[] = [];
if (data?.permissions) {
permissions = permissions.concat(data.permissions);
}

View file

@ -1,6 +1,4 @@
import { Permissions } from 'picsur-shared/dist/dto/permissions';
export interface RoleModel {
name: string;
permissions: Permissions;
permissions: string[];
}

View file

@ -1,6 +1,5 @@
import { FormControl } from '@angular/forms';
import Fuse from 'fuse.js';
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
import { BehaviorSubject, Subscription } from 'rxjs';
import { RoleNameValidators } from './role-validators';
import { RoleModel } from './role.model';
@ -8,10 +7,10 @@ import { CreateUsernameError } from './user-validators';
export class UpdateRoleControl {
// Set once
private permissions: Permissions = [];
private permissions: string[] = [];
// Variables
private selectablePermissionsSubject = new BehaviorSubject<Permissions>([]);
private selectablePermissionsSubject = new BehaviorSubject<string[]>([]);
private permissionsInputSubscription: null | Subscription;
public rolename = new FormControl('', RoleNameValidators);
@ -19,7 +18,7 @@ export class UpdateRoleControl {
public permissionControl = new FormControl('', []);
public selectablePermissions =
this.selectablePermissionsSubject.asObservable();
public selectedPermissions: Permissions = [];
public selectedPermissions: string[] = [];
public get rolenameValue() {
return this.rolename.value;
@ -43,23 +42,23 @@ export class UpdateRoleControl {
}
}
public addPermission(role: Permission) {
if (!this.selectablePermissionsSubject.value.includes(role)) return;
public addPermission(permission: string) {
if (!this.selectablePermissionsSubject.value.includes(permission)) return;
this.selectedPermissions.push(role);
this.selectedPermissions.push(permission);
this.clearInput();
}
public removePermission(role: Permission) {
public removePermission(permission: string) {
this.selectedPermissions = this.selectedPermissions.filter(
(r) => r !== role
(r) => r !== permission
);
this.updateSelectablePermissions();
}
// Data interaction
public putAllPermissions(permissions: Permissions) {
public putAllPermissions(permissions: string[]) {
this.permissions = permissions;
this.updateSelectablePermissions();
}
@ -68,7 +67,7 @@ export class UpdateRoleControl {
this.rolename.setValue(rolename);
}
public putPermissions(permissions: Permissions) {
public putPermissions(permissions: string[]) {
this.selectedPermissions = permissions;
this.updateSelectablePermissions();
}

View file

@ -1,6 +1,5 @@
import { FormControl } from '@angular/forms';
import Fuse from 'fuse.js';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { BehaviorSubject, Subscription } from 'rxjs';
import { FullUserModel } from './fulluser.model';
@ -74,8 +73,8 @@ export class UpdateUserControl {
return true;
}
public getEffectivePermissions(): Permissions {
const permissions: Permissions = [];
public getEffectivePermissions(): string[] {
const permissions: string[] = [];
for (const role of this.selectedRoles) {
const fullRole = this.fullRoles.find((r) => r.name === role);
if (!fullRole) {

View file

@ -1,6 +1,5 @@
import { ComponentType, Portal } from '@angular/cdk/portal';
import { Route } from '@angular/router';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
export type PRouteData = {
page?: {
@ -8,7 +7,7 @@ export type PRouteData = {
icon?: string;
category?: string;
};
permissions?: Permissions;
permissions?: string[];
noContainer?: boolean;
sidebar?: ComponentType<unknown>;
_sidebar_portal?: Portal<unknown>;

View file

@ -3,14 +3,11 @@ import { Component, OnInit } from '@angular/core';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute, Router } from '@angular/router';
import {
Permission,
PermissionsList
} from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types';
import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n';
import { UpdateRoleControl } from 'src/app/models/forms/updaterole.control';
import { SnackBarType } from 'src/app/models/snack-bar-type';
import { PermissionService } from 'src/app/services/api/permission.service';
import { RolesService } from 'src/app/services/api/roles.service';
import { UtilService } from 'src/app/util/util.service';
@ -42,7 +39,8 @@ export class SettingsRolesEditComponent implements OnInit {
private route: ActivatedRoute,
private router: Router,
private utilService: UtilService,
private rolesService: RolesService
private rolesService: RolesService,
private permissionsService: PermissionService
) {}
ngOnInit() {
@ -70,27 +68,36 @@ export class SettingsRolesEditComponent implements OnInit {
}
private async initPermissions() {
this.model.putAllPermissions(PermissionsList);
const allPermissions = await this.permissionsService.fetchAllPermission();
if (HasFailed(allPermissions)) {
this.utilService.showSnackBar(
'Failed to fetch permissions',
SnackBarType.Error
);
return;
}
this.model.putAllPermissions(allPermissions);
}
removePermission(permission: Permission) {
removePermission(permission: string) {
this.model.removePermission(permission);
}
addPermission(event: MatChipInputEvent) {
const value = (event.value ?? '').trim();
this.model.addPermission(value as Permission);
this.model.addPermission(value);
}
selectedPermission(event: MatAutocompleteSelectedEvent): void {
this.model.addPermission(event.option.viewValue as Permission);
this.model.addPermission(event.option.viewValue);
}
cancel() {
this.router.navigate(['/settings/roles']);
}
uiFriendlyPermission(permission: Permission) {
uiFriendlyPermission(permission: string) {
return UIFriendlyPermissions[permission];
}

View file

@ -2,9 +2,6 @@ import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import {
Permission
} from 'picsur-shared/dist/dto/permissions';
import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { HasFailed } from 'picsur-shared/dist/types';
import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n';
@ -88,7 +85,7 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit {
await this.fetchRoles();
}
uiFriendlyPermission(permission: Permission) {
uiFriendlyPermission(permission: string) {
return UIFriendlyPermissions[permission];
}

View file

@ -1,6 +1,6 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { PermissionGuard } from 'src/app/guards/permission.guard';
import { PRoutes } from 'src/app/models/picsur-routes';
import { SidebarResolverService } from 'src/app/services/sidebar-resolver/sidebar-resolver.service';

View file

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { NgxDropzoneChangeEvent } from 'ngx-dropzone';
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { PermissionService } from 'src/app/services/api/permission.service';
import { UtilService } from 'src/app/util/util.service';
import { ProcessingViewMetadata } from '../../models/processing-view-metadata';
@ -12,7 +12,7 @@ import { ProcessingViewMetadata } from '../../models/processing-view-metadata';
styleUrls: ['./upload.component.scss'],
})
export class UploadComponent implements OnInit {
private permissions: Permissions = [];
private permissions: string[] = [];
// Lets be optimistic here, this makes for a better ux
public get hasUploadPermission() {

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { SnackBarType } from 'src/app/models/snack-bar-type';
import { PermissionService } from 'src/app/services/api/permission.service';
@ -17,7 +17,7 @@ import { UserPassModel } from '../../../models/forms/userpass.model';
export class LoginComponent implements OnInit {
private readonly logger = console;
private permissions: Permissions = [];
private permissions: string[] = [];
public get showRegister() {
return this.permissions.includes(Permission.UserRegister);

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { UserPassModel } from 'src/app/models/forms/userpass.model';
import { SnackBarType } from 'src/app/models/snack-bar-type';
@ -17,7 +17,7 @@ import { RegisterControl } from '../../../models/forms/register.control';
export class RegisterComponent implements OnInit {
private readonly logger = console;
private permissions: Permissions = [];
private permissions: string[] = [];
public get showLogin() {
return this.permissions.includes(Permission.UserLogin);

View file

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { PermissionGuard } from 'src/app/guards/permission.guard';
import { PRoutes } from 'src/app/models/picsur-routes';
import { LoginComponent } from './login/login.component';

View file

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { PermissionGuard } from 'src/app/guards/permission.guard';
import { PRoutes } from 'src/app/models/picsur-routes';
import { ViewComponent } from './view.component';

View file

@ -1,10 +1,7 @@
import { Injectable } from '@angular/core';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { AllPermissionsResponse } from 'picsur-shared/dist/dto/api/roles.dto';
import { UserMePermissionsResponse } from 'picsur-shared/dist/dto/api/user.dto';
import {
Permissions,
PermissionsList
} from 'picsur-shared/dist/dto/permissions';
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
import { BehaviorSubject, filter, map, Observable, take } from 'rxjs';
import { ApiService } from './api.service';
@ -18,29 +15,29 @@ export class PermissionService {
this.onUser();
}
public get live(): Observable<Permissions> {
// TODO: add full permission list as default
public get live(): Observable<string[]> {
return this.permissionsSubject.pipe(
map((permissions) => permissions ?? this.defaultPermissions)
map((permissions) => permissions ?? [])
);
}
public get snapshot(): Permissions {
return this.permissionsSubject.getValue() ?? this.defaultPermissions;
public get snapshot(): string[] {
return this.permissionsSubject.getValue() ?? [];
}
// This will not be optimistic, it will instead wait for correct data
public loadedSnapshot(): Promise<Permissions> {
public loadedSnapshot(): Promise<string[]> {
return new Promise((resolve) => {
const filtered = this.permissionsSubject.pipe(
filter((permissions) => permissions !== null),
take(1)
);
(filtered as Observable<Permissions>).subscribe(resolve);
(filtered as Observable<string[]>).subscribe(resolve);
});
}
private defaultPermissions = PermissionsList as Permissions;
private permissionsSubject = new BehaviorSubject<Permissions | null>(null);
private permissionsSubject = new BehaviorSubject<string[] | null>(null);
@AutoUnsubscribe()
private onUser() {
@ -54,7 +51,7 @@ export class PermissionService {
});
}
private async fetchPermissions(): AsyncFailable<Permissions> {
private async fetchPermissions(): AsyncFailable<string[]> {
const got = await this.api.get(
UserMePermissionsResponse,
'/api/user/me/permissions'
@ -63,4 +60,15 @@ export class PermissionService {
return got.permissions;
}
public async fetchAllPermission(): AsyncFailable<string[]> {
const result = await this.api.get(
AllPermissionsResponse,
'/api/info/permissions'
);
if (HasFailed(result)) return result;
return result.Permissions;
}
}

View file

@ -7,7 +7,7 @@ import {
UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse
} from 'picsur-shared/dist/dto/api/pref.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import {
SysPreferences,
SysPrefValueType
@ -31,7 +31,9 @@ export class SysprefService {
return this.sysprefObservable;
}
private sysprefObservable = new BehaviorSubject<SysPreferenceBaseResponse[]>([]);
private sysprefObservable = new BehaviorSubject<SysPreferenceBaseResponse[]>(
[]
);
constructor(
private api: ApiService,

View file

@ -54,3 +54,10 @@ export class SpecialRolesResponse {
@IsStringList()
DefaultRoles: string[];
}
// AllPermissions
export class AllPermissionsResponse {
@IsDefined()
@IsStringList()
Permissions: string[];
}

View file

@ -1,11 +1,11 @@
import { Type } from 'class-transformer';
import {
IsArray, IsDefined,
IsEnum, IsJWT, IsString,
IsDefined, IsJWT,
IsString,
ValidateNested
} from 'class-validator';
import { EUser, NamePassUser } from '../../entities/user.entity';
import { Permissions, PermissionsList } from '../permissions';
import { IsStringList } from '../../validators/string-list.validator';
// Api
@ -40,7 +40,6 @@ export class UserMeResponse {
// UserMePermissions
export class UserMePermissionsResponse {
@IsDefined()
@IsArray()
@IsEnum(PermissionsList, { each: true })
permissions: Permissions;
@IsStringList()
permissions: string[];
}

View file

@ -1,5 +1,4 @@
// Config
// Only add no rename
export enum Permission {
ImageView = 'image-view',
ImageUpload = 'image-upload',
@ -14,16 +13,3 @@ export enum Permission {
RoleManage = 'role-manage', // Allow modification of roles
SysPrefManage = 'syspref-manage',
}
// Derivatives
export const PermissionsList: Permission[] = Object.values(Permission);
export type Permissions = Permission[];
// Compound permission lists
export const AdminDashboardPermissions: Permissions = [
Permission.UserManage,
Permission.RoleManage,
Permission.SysPrefManage,
];

View file

@ -1,7 +1,7 @@
import { IsArray, IsEnum } from 'class-validator';
import { Permissions, PermissionsList } from '../dto/permissions';
import { IsDefined } from 'class-validator';
import { EntityID } from '../validators/entity-id.validator';
import { IsRoleName } from '../validators/role.validators';
import { IsStringList } from '../validators/string-list.validator';
export class RoleNameObject {
@IsRoleName()
@ -9,9 +9,9 @@ export class RoleNameObject {
}
export class RoleNamePermsObject extends RoleNameObject {
@IsArray()
@IsEnum(PermissionsList, { each: true })
permissions: Permissions;
@IsDefined()
@IsStringList()
permissions: string[];
}
export class ERole extends RoleNamePermsObject {

View file

@ -1,19 +1,8 @@
import { isArray, isEnum, isString } from 'class-validator';
import { Permission, Permissions, PermissionsList } from '../dto/permissions';
export function isPermissionsArray(value: any): value is Permissions {
if (!isArray(value)) return false;
if (!value.every((item: unknown) => isString(item))) return false;
if (!value.every((item: string) => isEnum(item, PermissionsList)))
return false;
return true;
}
export function HasAPermissionOf(
compoundPermission: Permissions,
yourPermissions: Permissions,
compoundPermission: string[],
yourPermissions: string[],
): boolean {
return compoundPermission.some((permission: Permission) =>
return compoundPermission.some((permission: string) =>
yourPermissions.includes(permission),
);
}