mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-13 15:25:39 +01:00
relocate special roles to api request
This commit is contained in:
@@ -12,10 +12,11 @@
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"start": "nest start --exec \"node --experimental-specifier-resolution=node\"",
|
||||
"start:dev": "nest start --watch --exec \"node --experimental-specifier-resolution=node\"",
|
||||
"start:dev": "yarn clean && nest start --watch --exec \"node --experimental-specifier-resolution=node\"",
|
||||
"start:debug": "nest start --debug --watch --exec \"node --experimental-specifier-resolution=node\"",
|
||||
"start:prod": "node --experimental-specifier-resolution=node dist/main",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"clean": "rimraf dist",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import {
|
||||
ImmuteableRolesList,
|
||||
SystemRoleDefaults,
|
||||
SystemRoles,
|
||||
SystemRolesList
|
||||
} from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { PicsurConfigModule } from '../../config/config.module';
|
||||
import { HostConfigService } from '../../config/host.config.service';
|
||||
import { ImmutableRolesList, SystemRoleDefaults, UndeletableRolesList } from '../../models/dto/roles.dto';
|
||||
import { ERoleBackend } from '../../models/entities/role.entity';
|
||||
import { RolesService } from './roledb.service';
|
||||
|
||||
@@ -43,7 +38,7 @@ export class RolesModule implements OnModuleInit {
|
||||
}
|
||||
|
||||
private async ensureSystemRolesExist() {
|
||||
for (const systemRole of SystemRolesList as SystemRoles) {
|
||||
for (const systemRole of UndeletableRolesList) {
|
||||
this.logger.debug(`Ensuring system role "${systemRole}" exists`);
|
||||
|
||||
const exists = await this.rolesService.exists(systemRole);
|
||||
@@ -63,7 +58,7 @@ export class RolesModule implements OnModuleInit {
|
||||
}
|
||||
|
||||
private async updateImmutableRoles() {
|
||||
for (const immutableRole of ImmuteableRolesList as SystemRoles) {
|
||||
for (const immutableRole of ImmutableRolesList) {
|
||||
this.logger.debug(
|
||||
`Updating permissions for immutable role "${immutableRole}"`,
|
||||
);
|
||||
|
||||
@@ -2,11 +2,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 {
|
||||
ImmuteableRolesList,
|
||||
Roles,
|
||||
SystemRolesList
|
||||
} from 'picsur-shared/dist/dto/roles.dto';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
@@ -15,6 +10,7 @@ import {
|
||||
} from 'picsur-shared/dist/types';
|
||||
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { ImmutableRolesList, UndeletableRolesList } from '../../models/dto/roles.dto';
|
||||
import { ERoleBackend } from '../../models/entities/role.entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -51,7 +47,7 @@ export class RolesService {
|
||||
const roleToModify = await this.resolve(role);
|
||||
if (HasFailed(roleToModify)) return roleToModify;
|
||||
|
||||
if (SystemRolesList.includes(roleToModify.name)) {
|
||||
if (UndeletableRolesList.includes(roleToModify.name)) {
|
||||
return Fail('Cannot delete system role');
|
||||
}
|
||||
|
||||
@@ -62,7 +58,7 @@ export class RolesService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getPermissions(roles: Roles): AsyncFailable<Permissions> {
|
||||
public async getPermissions(roles: string[]): AsyncFailable<Permissions> {
|
||||
const permissions: Permissions = [];
|
||||
const foundRoles = await Promise.all(
|
||||
roles.map((role: string) => this.findOne(role)),
|
||||
@@ -113,7 +109,7 @@ export class RolesService {
|
||||
const roleToModify = await this.resolve(role);
|
||||
if (HasFailed(roleToModify)) return roleToModify;
|
||||
|
||||
if (!allowImmutable && ImmuteableRolesList.includes(roleToModify.name)) {
|
||||
if (!allowImmutable && ImmutableRolesList.includes(roleToModify.name)) {
|
||||
return Fail('Cannot modify immutable role');
|
||||
}
|
||||
|
||||
@@ -157,7 +153,7 @@ export class RolesService {
|
||||
if (!iamsure) return Fail('Nuke aborted');
|
||||
try {
|
||||
await this.rolesRepository.delete({
|
||||
name: In(SystemRolesList),
|
||||
name: In(UndeletableRolesList),
|
||||
});
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
|
||||
@@ -2,11 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import {
|
||||
DefaultRolesList,
|
||||
PermanentRolesList,
|
||||
Roles
|
||||
} from 'picsur-shared/dist/dto/roles.dto';
|
||||
import {
|
||||
LockedLoginUsersList,
|
||||
LockedPermsUsersList,
|
||||
@@ -20,6 +15,10 @@ import {
|
||||
} from 'picsur-shared/dist/types';
|
||||
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
DefaultRolesList,
|
||||
SoulBoundRolesList
|
||||
} from '../../models/dto/roles.dto';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
import { GetCols } from '../collectionutils';
|
||||
import { RolesService } from '../roledb/roledb.service';
|
||||
@@ -41,7 +40,7 @@ export class UsersService {
|
||||
public async create(
|
||||
username: string,
|
||||
password: string,
|
||||
roles?: Roles,
|
||||
roles?: string[],
|
||||
byPassRoleCheck?: boolean,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
if (await this.exists(username)) return Fail('User already exists');
|
||||
@@ -90,7 +89,7 @@ export class UsersService {
|
||||
|
||||
public async setRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
roles: string[],
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
@@ -101,7 +100,7 @@ export class UsersService {
|
||||
}
|
||||
|
||||
const rolesToKeep = userToModify.roles.filter((role) =>
|
||||
PermanentRolesList.includes(role),
|
||||
SoulBoundRolesList.includes(role),
|
||||
);
|
||||
const rolesToAdd = this.filterAddedRoles(roles);
|
||||
|
||||
@@ -216,9 +215,9 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
private filterAddedRoles(roles: Roles): Roles {
|
||||
private filterAddedRoles(roles: string[]): string[] {
|
||||
const filteredRoles = roles.filter(
|
||||
(role) => !PermanentRolesList.includes(role),
|
||||
(role) => !SoulBoundRolesList.includes(role),
|
||||
);
|
||||
|
||||
return filteredRoles;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
import { RolesService } from '../roledb/roledb.service';
|
||||
@@ -23,7 +22,7 @@ export class UserRolesService {
|
||||
|
||||
public async addRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
roles: string[],
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.usersService.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
@@ -35,7 +34,7 @@ export class UserRolesService {
|
||||
|
||||
public async removeRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
roles: string[],
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.usersService.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
PipeTransform,
|
||||
Scope
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
PipeTransform,
|
||||
Scope
|
||||
} from '@nestjs/common';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { MultipartFields, MultipartFile } from 'fastify-multipart';
|
||||
@@ -11,9 +11,9 @@ import { Newable } from 'picsur-shared/dist/types';
|
||||
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
||||
import { MultipartConfigService } from '../config/multipart.config.service';
|
||||
import {
|
||||
MultiPartFieldDto,
|
||||
MultiPartFileDto
|
||||
} from '../models/dto/multipart.dto';
|
||||
MultiPartFieldDto,
|
||||
MultiPartFileDto
|
||||
} from '../models/requests/multipart.dto';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class MultiPartPipe implements PipeTransform {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ArgumentsHost, Catch, ExceptionFilter, HttpException
|
||||
} from '@nestjs/common';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { ApiErrorResponse } from 'picsur-shared/dist/dto/api';
|
||||
import { ApiErrorResponse } from 'picsur-shared/dist/dto/api/api.dto';
|
||||
|
||||
@Catch(HttpException)
|
||||
export class MainExceptionFilter implements ExceptionFilter {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
CallHandler, ExecutionContext, Injectable,
|
||||
NestInterceptor
|
||||
} from '@nestjs/common';
|
||||
import { ApiResponse } from 'picsur-shared/dist/dto/api';
|
||||
import { ApiResponse } from 'picsur-shared/dist/dto/api/api.dto';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
|
||||
39
backend/src/models/dto/roles.dto.ts
Normal file
39
backend/src/models/dto/roles.dto.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Permission, Permissions, PermissionsList } from 'picsur-shared/dist/dto/permissions';
|
||||
import tuple from 'picsur-shared/dist/types/tuple';
|
||||
|
||||
// Config
|
||||
|
||||
// These roles can never be removed or added to a user.
|
||||
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);
|
||||
// These roles will be applied by default to new users
|
||||
export const DefaultRolesList: string[] = ['user'];
|
||||
|
||||
// Derivatives
|
||||
export const SoulBoundRolesList: string[] = SoulBoundRolesTuple;
|
||||
export const ImmutableRolesList: string[] = ImmutableRolesTuple;
|
||||
export const UndeletableRolesList: string[] = UndeletableRolesTuple;
|
||||
|
||||
// Defaults
|
||||
type SystemRole = typeof UndeletableRolesTuple[number];
|
||||
const SystemRoleDefaultsTyped: {
|
||||
[key in SystemRole]: Permissions;
|
||||
} = {
|
||||
guest: [Permission.ImageView, Permission.UserLogin],
|
||||
user: [
|
||||
Permission.ImageView,
|
||||
Permission.UserMe,
|
||||
Permission.UserLogin,
|
||||
Permission.Settings,
|
||||
Permission.ImageUpload,
|
||||
],
|
||||
// Grant all permissions to admin
|
||||
admin: PermissionsList,
|
||||
};
|
||||
|
||||
export const SystemRoleDefaults = SystemRoleDefaultsTyped as {
|
||||
[key in string]: Permissions;
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@@ -14,7 +13,7 @@ export class EUserBackend extends EUser {
|
||||
override username: string;
|
||||
|
||||
@Column('text', { nullable: false, array: true })
|
||||
override roles: Roles;
|
||||
override roles: string[];
|
||||
|
||||
@Column({ nullable: false, select: false })
|
||||
override password?: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get, Request } from '@nestjs/common';
|
||||
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
||||
import AuthFasityRequest from '../../../models/requests/authrequest.dto';
|
||||
|
||||
|
||||
@Controller('api/experiment')
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Logger,
|
||||
Post
|
||||
} from '@nestjs/common';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import {
|
||||
RoleCreateRequest,
|
||||
RoleCreateResponse,
|
||||
@@ -15,12 +16,19 @@ import {
|
||||
RoleInfoResponse,
|
||||
RoleListResponse,
|
||||
RoleUpdateRequest,
|
||||
RoleUpdateResponse
|
||||
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 {
|
||||
DefaultRolesList,
|
||||
ImmutableRolesList,
|
||||
SoulBoundRolesList,
|
||||
UndeletableRolesList
|
||||
} from '../../../models/dto/roles.dto';
|
||||
|
||||
@Controller('api/roles')
|
||||
@RequiredPermissions(Permission.RoleManage)
|
||||
@@ -95,4 +103,16 @@ export class RolesController {
|
||||
|
||||
return deletedRole;
|
||||
}
|
||||
|
||||
@Get('special')
|
||||
async getSpecialRoles(): Promise<SpecialRolesResponse> {
|
||||
const result: SpecialRolesResponse = {
|
||||
SoulBoundRoles: SoulBoundRolesList,
|
||||
ImmutableRoles: ImmutableRolesList,
|
||||
UndeletableRoles: UndeletableRolesList,
|
||||
DefaultRoles: DefaultRolesList,
|
||||
};
|
||||
|
||||
return plainToClass(SpecialRolesResponse, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
Post,
|
||||
Request
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
Post,
|
||||
Request
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
UserLoginResponse,
|
||||
UserMePermissionsResponse,
|
||||
UserMeResponse,
|
||||
UserRegisterRequest,
|
||||
UserRegisterResponse
|
||||
UserLoginResponse,
|
||||
UserMePermissionsResponse,
|
||||
UserMeResponse,
|
||||
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';
|
||||
import {
|
||||
NoPermissions,
|
||||
RequiredPermissions,
|
||||
UseLocalAuth
|
||||
NoPermissions,
|
||||
RequiredPermissions,
|
||||
UseLocalAuth
|
||||
} from '../../../decorators/permissions.decorator';
|
||||
import { AuthManagerService } from '../../../managers/auth/auth.service';
|
||||
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
||||
import AuthFasityRequest from '../../../models/requests/authrequest.dto';
|
||||
|
||||
@Controller('api/user')
|
||||
export class UserController {
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 { ImageUploadDto } from '../../models/dto/imageroute.dto';
|
||||
import { ImageUploadDto } from '../../models/requests/imageroute.dto';
|
||||
|
||||
@Controller('i')
|
||||
@RequiredPermissions(Permission.ImageView)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
|
||||
export interface FullUserModel {
|
||||
username: string;
|
||||
password: string;
|
||||
roles: Roles;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FormControl } from '@angular/forms';
|
||||
import Fuse from 'fuse.js';
|
||||
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { PermanentRolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { RoleNameValidators } from './role-validators';
|
||||
import { RoleModel } from './role.model';
|
||||
@@ -58,11 +57,6 @@ export class UpdateRoleControl {
|
||||
this.updateSelectablePermissions();
|
||||
}
|
||||
|
||||
public isRemovable(role: Permission) {
|
||||
if (PermanentRolesList.includes(role)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Data interaction
|
||||
|
||||
public putAllPermissions(permissions: Permissions) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FormControl } from '@angular/forms';
|
||||
import Fuse from 'fuse.js';
|
||||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { PermanentRolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { ERole } from 'picsur-shared/dist/entities/role.entity';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { FullUserModel } from './fulluser.model';
|
||||
@@ -13,6 +12,9 @@ import {
|
||||
} from './user-validators';
|
||||
|
||||
export class UpdateUserControl {
|
||||
// Special roles
|
||||
private SoulBoundRolesList: string[] = [];
|
||||
|
||||
// Set once
|
||||
private fullRoles: ERole[] = [];
|
||||
private roles: string[] = [];
|
||||
@@ -68,7 +70,7 @@ export class UpdateUserControl {
|
||||
}
|
||||
|
||||
public isRemovable(role: string) {
|
||||
if (PermanentRolesList.includes(role)) return false;
|
||||
if (this.SoulBoundRolesList.includes(role)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -106,6 +108,10 @@ export class UpdateUserControl {
|
||||
this.updateSelectableRoles();
|
||||
}
|
||||
|
||||
public putSoulBoundRoles(roles: string[]) {
|
||||
this.SoulBoundRolesList = roles;
|
||||
}
|
||||
|
||||
public getData(): FullUserModel {
|
||||
return {
|
||||
username: this.username.value,
|
||||
@@ -119,7 +125,8 @@ export class UpdateUserControl {
|
||||
private updateSelectableRoles() {
|
||||
const availableRoles = this.roles.filter(
|
||||
// Not available if either already selected, or the role is not addable/removable
|
||||
(r) => !(this.selectedRoles.includes(r) || PermanentRolesList.includes(r))
|
||||
(r) =>
|
||||
!(this.selectedRoles.includes(r) || this.SoulBoundRolesList.includes(r))
|
||||
);
|
||||
|
||||
const searchValue = this.rolesControl.value;
|
||||
|
||||
@@ -33,12 +33,10 @@
|
||||
<mat-chip-list #chipList aria-label="Permissions Selection">
|
||||
<mat-chip
|
||||
*ngFor="let permission of model.selectedPermissions"
|
||||
[removable]="model.isRemovable(permission)"
|
||||
[disabled]="!model.isRemovable(permission)"
|
||||
(removed)="removePermission(permission)"
|
||||
>
|
||||
{{ uiFriendlyPermission(permission) }}
|
||||
<button *ngIf="model.isRemovable(permission)" matChipRemove>
|
||||
<button matChipRemove>
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip>
|
||||
@@ -50,14 +48,14 @@
|
||||
[matAutocomplete]="auto"
|
||||
[matChipInputFor]="chipList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="addRole($event)"
|
||||
(matChipInputTokenEnd)="addPermission($event)"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
/>
|
||||
</mat-chip-list>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
(optionSelected)="selectedRole($event)"
|
||||
(optionSelected)="selectedPermission($event)"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let permission of model.selectablePermissions | async"
|
||||
|
||||
@@ -77,12 +77,12 @@ export class SettingsRolesEditComponent implements OnInit {
|
||||
this.model.removePermission(permission);
|
||||
}
|
||||
|
||||
addRole(event: MatChipInputEvent) {
|
||||
addPermission(event: MatChipInputEvent) {
|
||||
const value = (event.value ?? '').trim();
|
||||
this.model.addPermission(value as Permission);
|
||||
}
|
||||
|
||||
selectedRole(event: MatAutocompleteSelectedEvent): void {
|
||||
selectedPermission(event: MatAutocompleteSelectedEvent): void {
|
||||
this.model.addPermission(event.option.viewValue as Permission);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Permission,
|
||||
UIFriendlyPermissions
|
||||
} from 'picsur-shared/dist/dto/permissions';
|
||||
import { ImmuteableRolesList, SystemRolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { ERole } from 'picsur-shared/dist/entities/role.entity';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SnackBarType } from 'src/app/models/snack-bar-type';
|
||||
@@ -29,6 +28,9 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit {
|
||||
|
||||
public dataSource = new MatTableDataSource<ERole>([]);
|
||||
|
||||
private UndeletableRolesList: string[] = [];
|
||||
private ImmutableRolesList: string[] = [];
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
|
||||
constructor(
|
||||
@@ -91,11 +93,11 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
isSystem(role: ERole) {
|
||||
return SystemRolesList.includes(role.name);
|
||||
return this.UndeletableRolesList.includes(role.name);
|
||||
}
|
||||
|
||||
isImmutable(role: ERole) {
|
||||
return ImmuteableRolesList.includes(role.name);
|
||||
return this.ImmutableRolesList.includes(role.name);
|
||||
}
|
||||
|
||||
private async fetchRoles() {
|
||||
@@ -106,5 +108,17 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
this.dataSource.data = roles;
|
||||
|
||||
const specialRoles = await this.rolesService.getSpecialRoles();
|
||||
if (HasFailed(specialRoles)) {
|
||||
this.utilService.showSnackBar(
|
||||
'Failed to load special roles',
|
||||
SnackBarType.Error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.UndeletableRolesList = specialRoles.UndeletableRoles;
|
||||
this.ImmutableRolesList = specialRoles.ImmutableRoles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { UIFriendlyPermissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { DefaultRolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { LockedPermsUsersList } from 'picsur-shared/dist/dto/specialusers.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UpdateUserControl } from 'src/app/models/forms/updateuser.control';
|
||||
@@ -51,9 +50,15 @@ export class SettingsUsersEditComponent implements OnInit {
|
||||
|
||||
private async initUser() {
|
||||
const username = this.route.snapshot.paramMap.get('username');
|
||||
|
||||
const { DefaultRoles, SoulBoundRoles } =
|
||||
await this.rolesService.getSpecialRolesOptimistic();
|
||||
this.model.putSoulBoundRoles(SoulBoundRoles);
|
||||
|
||||
if (!username) {
|
||||
this.mode = EditMode.add;
|
||||
this.model.putRoles(DefaultRolesList);
|
||||
|
||||
this.model.putRoles(DefaultRoles);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ClassConstructor, plainToClass } from 'class-transformer';
|
||||
import { ApiResponse, ApiSuccessResponse } from 'picsur-shared/dist/dto/api';
|
||||
import { ApiResponse, ApiSuccessResponse } from 'picsur-shared/dist/dto/api/api.dto';
|
||||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
44
frontend/src/app/services/api/cache.service.ts
Normal file
44
frontend/src/app/services/api/cache.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
interface dataWrapper<T> {
|
||||
data: T;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CacheService {
|
||||
private readonly cacheExpiresMS = 1000 * 60 * 60;
|
||||
|
||||
private storage: Storage;
|
||||
|
||||
constructor() {
|
||||
if (window.sessionStorage) {
|
||||
this.storage = window.sessionStorage;
|
||||
} else {
|
||||
throw new Error('Session storage is not supported');
|
||||
}
|
||||
}
|
||||
|
||||
public get<T>(key: string): T | null {
|
||||
try {
|
||||
const data: dataWrapper<T> = JSON.parse(this.storage.getItem(key) ?? '');
|
||||
if (data && data.data && data.expires > Date.now()) {
|
||||
return data.data;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public set<T>(key: string, value: T): void {
|
||||
const data: dataWrapper<T> = {
|
||||
data: value,
|
||||
expires: Date.now() + this.cacheExpiresMS,
|
||||
};
|
||||
|
||||
this.storage.setItem(key, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,23 @@ import {
|
||||
RoleInfoResponse,
|
||||
RoleListResponse,
|
||||
RoleUpdateRequest,
|
||||
RoleUpdateResponse
|
||||
RoleUpdateResponse,
|
||||
SpecialRolesResponse
|
||||
} from 'picsur-shared/dist/dto/api/roles.dto';
|
||||
import { ERole } from 'picsur-shared/dist/entities/role.entity';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { RoleModel } from 'src/app/models/forms/role.model';
|
||||
import { ApiService } from './api.service';
|
||||
import { CacheService } from './cache.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RolesService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private cacheService: CacheService
|
||||
) {}
|
||||
|
||||
public async getRoles(): AsyncFailable<ERole[]> {
|
||||
const result = await this.apiService.get(
|
||||
@@ -85,4 +90,38 @@ export class RolesService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getSpecialRoles(): AsyncFailable<SpecialRolesResponse> {
|
||||
const cached = this.cacheService.get<SpecialRolesResponse>('specialRoles');
|
||||
if (cached !== null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result = await this.apiService.get(
|
||||
SpecialRolesResponse,
|
||||
'/api/roles/special'
|
||||
);
|
||||
|
||||
if (HasFailed(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
this.cacheService.set('specialRoles', result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getSpecialRolesOptimistic(): Promise<SpecialRolesResponse> {
|
||||
const result = await this.getSpecialRoles();
|
||||
if (HasFailed(result)) {
|
||||
return {
|
||||
DefaultRoles: [],
|
||||
ImmutableRoles: [],
|
||||
SoulBoundRoles: [],
|
||||
UndeletableRoles: [],
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"typescript": "4.5.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "tsc-watch",
|
||||
"build": "tsc"
|
||||
"clean": "rm -rf ./dist",
|
||||
"start": "yarn clean && tsc-watch",
|
||||
"build": "yarn clean && tsc"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsDefined, ValidateNested
|
||||
} from 'class-validator';
|
||||
import { IsArray, IsDefined, ValidateNested } from 'class-validator';
|
||||
import {
|
||||
ERole,
|
||||
RoleNameObject,
|
||||
RoleNamePermsObject
|
||||
} from '../../entities/role.entity';
|
||||
import { IsPosInt } from '../../validators/positive-int.validator';
|
||||
import { IsStringList } from '../../validators/string-list.validator';
|
||||
|
||||
// RoleInfo
|
||||
export class RoleInfoRequest extends RoleNameObject {}
|
||||
@@ -37,3 +35,22 @@ export class RoleCreateResponse extends ERole {}
|
||||
// RoleDelete
|
||||
export class RoleDeleteRequest extends RoleNameObject {}
|
||||
export class RoleDeleteResponse extends ERole {}
|
||||
|
||||
// SpecialRoles
|
||||
export class SpecialRolesResponse {
|
||||
@IsDefined()
|
||||
@IsStringList()
|
||||
SoulBoundRoles: string[];
|
||||
|
||||
@IsDefined()
|
||||
@IsStringList()
|
||||
ImmutableRoles: string[];
|
||||
|
||||
@IsDefined()
|
||||
@IsStringList()
|
||||
UndeletableRoles: string[];
|
||||
|
||||
@IsDefined()
|
||||
@IsStringList()
|
||||
DefaultRoles: string[];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsDefined, IsOptional,
|
||||
IsString, ValidateNested
|
||||
IsDefined,
|
||||
IsOptional, ValidateNested
|
||||
} from 'class-validator';
|
||||
import { EUser, NamePassUser, UsernameUser } from '../../entities/user.entity';
|
||||
import { IsPosInt } from '../../validators/positive-int.validator';
|
||||
import { IsStringList } from '../../validators/string-list.validator';
|
||||
import { IsPlainTextPwd } from '../../validators/user.validators';
|
||||
import { Roles } from '../roles.dto';
|
||||
|
||||
// UserList
|
||||
export class UserListRequest {
|
||||
@@ -35,9 +35,8 @@ export class UserListResponse {
|
||||
// UserCreate
|
||||
export class UserCreateRequest extends NamePassUser {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
roles?: Roles;
|
||||
@IsStringList()
|
||||
roles?: string[];
|
||||
}
|
||||
export class UserCreateResponse extends EUser {}
|
||||
|
||||
@@ -52,9 +51,8 @@ export class UserInfoResponse extends EUser {}
|
||||
// UserUpdateRoles
|
||||
export class UserUpdateRequest extends UsernameUser {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
roles?: Roles;
|
||||
@IsStringList()
|
||||
roles?: string[];
|
||||
|
||||
@IsPlainTextPwd()
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import tuple from '../types/tuple';
|
||||
import { Permission, Permissions, PermissionsList } from './permissions';
|
||||
|
||||
// Config
|
||||
|
||||
// These roles can never be removed or added to a user.
|
||||
const PermanentRolesTuple = tuple('guest', 'user');
|
||||
|
||||
// These roles can never be modified
|
||||
const ImmuteableRolesTuple = tuple('admin');
|
||||
// These roles can never be removed from the server
|
||||
const SystemRolesTuple = tuple(...PermanentRolesTuple, ...ImmuteableRolesTuple);
|
||||
|
||||
// These roles will be applied by default to new users
|
||||
export const DefaultRolesList: string[] = ['user'];
|
||||
|
||||
// Derivatives
|
||||
|
||||
export const PermanentRolesList: string[] = PermanentRolesTuple;
|
||||
export const ImmuteableRolesList: string[] = ImmuteableRolesTuple;
|
||||
export const SystemRolesList: string[] = SystemRolesTuple;
|
||||
|
||||
|
||||
export type SystemRole = typeof SystemRolesTuple[number];
|
||||
export type SystemRoles = SystemRole[];
|
||||
|
||||
// Defaults
|
||||
|
||||
export const SystemRoleDefaults: {
|
||||
[key in SystemRole]: Permissions;
|
||||
} = {
|
||||
guest: [Permission.ImageView, Permission.UserLogin],
|
||||
user: [
|
||||
Permission.ImageView,
|
||||
Permission.UserMe,
|
||||
Permission.UserLogin,
|
||||
Permission.Settings,
|
||||
Permission.ImageUpload,
|
||||
],
|
||||
// Grant all permissions to admin
|
||||
admin: PermissionsList,
|
||||
};
|
||||
|
||||
// Normal roles types
|
||||
|
||||
export type Role = SystemRole | string;
|
||||
export type Roles = Role[];
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { IsArray, IsOptional, IsString } from 'class-validator';
|
||||
import { Roles } from '../dto/roles.dto';
|
||||
import { IsDefined, IsOptional, IsString } from 'class-validator';
|
||||
import { EntityID } from '../validators/entity-id.validator';
|
||||
import { IsStringList } from '../validators/string-list.validator';
|
||||
import { IsPlainTextPwd, IsUsername } from '../validators/user.validators';
|
||||
|
||||
export class UsernameUser {
|
||||
@@ -17,9 +17,9 @@ export class NamePassUser extends UsernameUser {
|
||||
|
||||
// Add a user object with just the username and roles for jwt
|
||||
export class NameRolesUser extends UsernameUser {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
roles: Roles;
|
||||
@IsDefined()
|
||||
@IsStringList()
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
// Actual entity that goes in the db
|
||||
|
||||
12
shared/src/validators/string-list.validator.ts
Normal file
12
shared/src/validators/string-list.validator.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsString
|
||||
} from 'class-validator';
|
||||
import { ComposeValidators } from './compose.validator';
|
||||
|
||||
export const IsStringList = ComposeValidators(
|
||||
IsArray(),
|
||||
IsString({ each: true }),
|
||||
IsNotEmpty({ each: true }),
|
||||
);
|
||||
Reference in New Issue
Block a user