add user role api

This commit is contained in:
rubikscraft 2022-03-12 20:15:48 +01:00
parent 1febcd8147
commit 6d2e950230
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
25 changed files with 236 additions and 149 deletions

View file

@ -36,7 +36,7 @@ export class RolesModule implements OnModuleInit {
private async nukeRoles() {
this.logger.error('Nuking system roles');
const result = this.rolesService.nukeSystemRoles(true);
const result = await this.rolesService.nukeSystemRoles(true);
if (HasFailed(result)) {
this.logger.error(`Failed to nuke roles because: ${result.getReason()}`);
}

View file

@ -132,7 +132,7 @@ export class RolesService {
where: { name },
});
if (!found) return Fail('User not found');
if (!found) return Fail('Role not found');
return found as ERoleBackend;
} catch (e: any) {
return Fail(e?.message);

View file

@ -88,43 +88,46 @@ export class UsersService {
public async addRoles(
user: string | EUserBackend,
roles: Roles,
): AsyncFailable<true> {
): AsyncFailable<EUserBackend> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
// This is stupid
userToModify.roles = [...new Set([...userToModify.roles, ...roles])];
const newRoles = [...new Set([...userToModify.roles, ...roles])];
try {
await this.usersRepository.save(userToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
return this.setRoles(userToModify, newRoles);
}
public async removeRoles(
user: string | EUserBackend,
roles: Roles,
): AsyncFailable<true> {
): AsyncFailable<EUserBackend> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
// Make sure we don't remove unremovable roles
roles = roles.filter((role) => !PermanentRolesList.includes(role));
const newRoles = userToModify.roles.filter((role) => !roles.includes(role));
userToModify.roles = userToModify.roles.filter(
(role) => !roles.includes(role),
return this.setRoles(userToModify, newRoles);
}
public async setRoles(
user: string | EUserBackend,
roles: Roles,
): AsyncFailable<EUserBackend> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
const rolesToKeep = userToModify.roles.filter((role) =>
PermanentRolesList.includes(role),
);
const newRoles = [...new Set([...rolesToKeep, ...roles])];
userToModify.roles = newRoles;
try {
await this.usersRepository.save(userToModify);
return await this.usersRepository.save(userToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
public async findOne<B extends true | undefined = undefined>(

View file

@ -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.dto';
import { ApiErrorResponse } from 'picsur-shared/dist/dto/api';
@Catch(HttpException)
export class MainExceptionFilter implements ExceptionFilter {

View file

@ -2,7 +2,7 @@ import {
CallHandler, ExecutionContext, Injectable,
NestInterceptor
} from '@nestjs/common';
import { ApiResponse } from 'picsur-shared/dist/dto/api.dto';
import { ApiResponse } from 'picsur-shared/dist/dto/api';
import { map, Observable } from 'rxjs';
@Injectable()

View file

@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { instanceToPlain, plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto';
import { EUserBackend } from '../../models/entities/user.entity';
@Injectable()

View file

@ -8,8 +8,9 @@ import { PassportStrategy } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto';
import { EUserBackend } from '../../../models/entities/user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
private readonly logger = new Logger('JwtStrategy');

View file

@ -42,13 +42,13 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
const permissions = this.extractPermissions(context);
if (HasFailed(permissions)) {
this.logger.warn(permissions.getReason());
this.logger.warn("222"+permissions.getReason());
throw new InternalServerErrorException();
}
const userPermissions = await this.usersService.getPermissions(user);
if (HasFailed(userPermissions)) {
this.logger.warn(userPermissions.getReason());
this.logger.warn("111"+userPermissions.getReason());
throw new InternalServerErrorException();
}

View file

@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UserApiModule } from './auth/user.module';
import { ExperimentModule } from './experiment/experiment.module';
import { InfoModule } from './info/info.module';
import { PrefModule } from './pref/pref.module';
@ -7,7 +7,7 @@ import { RolesApiModule } from './roles/roles.module';
@Module({
imports: [
AuthModule,
UserApiModule,
PrefModule,
ExperimentModule,
InfoModule,

View file

@ -7,13 +7,19 @@ import {
Post,
Request
} from '@nestjs/common';
import { AuthUserInfoRequest } from 'picsur-shared/dist/dto/api/auth.dto';
import {
AuthDeleteRequest,
AuthLoginResponse,
AuthMeResponse,
AuthRegisterRequest
} from 'picsur-shared/dist/dto/auth.dto';
UserDeleteRequest,
UserDeleteResponse,
UserInfoRequest,
UserInfoResponse,
UserListResponse,
UserLoginResponse,
UserMeResponse,
UserRegisterRequest,
UserRegisterResponse,
UserUpdateRolesRequest,
UserUpdateRolesResponse
} from 'picsur-shared/dist/dto/api/user.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service';
import {
@ -23,8 +29,8 @@ import {
import { AuthManagerService } from '../../../managers/auth/auth.service';
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
@Controller('api/auth')
export class AuthController {
@Controller('api/user')
export class UserController {
private readonly logger = new Logger('AuthController');
constructor(
@ -34,20 +40,17 @@ export class AuthController {
@Post('login')
@UseLocalAuth('user-login')
async login(@Request() req: AuthFasityRequest) {
const response: AuthLoginResponse = {
async login(@Request() req: AuthFasityRequest): Promise<UserLoginResponse> {
return {
jwt_token: await this.authService.createToken(req.user),
};
return response;
}
@Post('register')
@RequiredPermissions('user-register')
async register(
@Request() req: AuthFasityRequest,
@Body() register: AuthRegisterRequest,
) {
@Body() register: UserRegisterRequest,
): Promise<UserRegisterResponse> {
const user = await this.usersService.create(
register.username,
register.password,
@ -58,7 +61,11 @@ export class AuthController {
}
if (register.isAdmin) {
await this.usersService.addRoles(user, ['admin']);
const result = await this.usersService.addRoles(user, ['admin']);
if (HasFailed(result)) {
this.logger.warn(result.getReason());
throw new InternalServerErrorException('Could not add admin role');
}
}
return user;
@ -67,9 +74,8 @@ export class AuthController {
@Post('delete')
@RequiredPermissions('user-manage')
async delete(
@Request() req: AuthFasityRequest,
@Body() deleteData: AuthDeleteRequest,
) {
@Body() deleteData: UserDeleteRequest,
): Promise<UserDeleteResponse> {
const user = await this.usersService.delete(deleteData.username);
if (HasFailed(user)) {
this.logger.warn(user.getReason());
@ -79,9 +85,27 @@ export class AuthController {
return user;
}
@Post('roles')
@RequiredPermissions('user-manage')
async setPermissions(
@Body() body: UserUpdateRolesRequest,
): Promise<UserUpdateRolesResponse> {
const updatedUser = await this.usersService.setRoles(
body.username,
body.roles,
);
if (HasFailed(updatedUser)) {
this.logger.warn(updatedUser.getReason());
throw new InternalServerErrorException('Could not update user');
}
return updatedUser;
}
@Post('info')
@RequiredPermissions('user-manage')
async getUser(@Body() body: AuthUserInfoRequest) {
async getUser(@Body() body: UserInfoRequest): Promise<UserInfoResponse> {
const user = await this.usersService.findOne(body.username);
if (HasFailed(user)) {
this.logger.warn(user.getReason());
@ -93,30 +117,32 @@ export class AuthController {
@Get('list')
@RequiredPermissions('user-manage')
async listUsers(@Request() req: AuthFasityRequest) {
const users = this.usersService.findAll();
async listUsers(): Promise<UserListResponse> {
const users = await this.usersService.findAll();
if (HasFailed(users)) {
this.logger.warn(users.getReason());
throw new InternalServerErrorException('Could not list users');
}
return users;
return {
users,
total: users.length,
};
}
@Get('me')
@RequiredPermissions('user-view')
async me(@Request() req: AuthFasityRequest) {
async me(@Request() req: AuthFasityRequest): Promise<UserMeResponse> {
const permissions = await this.usersService.getPermissions(req.user);
if (HasFailed(permissions)) {
this.logger.warn(permissions.getReason());
throw new InternalServerErrorException('Could not get permissions');
}
const meResponse: AuthMeResponse = new AuthMeResponse();
meResponse.user = req.user;
meResponse.permissions = permissions;
meResponse.newJwtToken = await this.authService.createToken(req.user);
return meResponse;
return {
user: req.user,
permissions,
newJwtToken: await this.authService.createToken(req.user),
};
}
}

View file

@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { AuthManagerModule } from '../../../managers/auth/auth.module';
import { AuthController } from './auth.controller';
import { UserController } from './user.controller';
@Module({
imports: [AuthManagerModule],
controllers: [AuthController],
controllers: [UserController],
})
export class AuthModule {}
export class UserApiModule {}

View file

@ -9,11 +9,10 @@ export class InfoController {
@Get()
@NoAuth()
getInfo() {
const response: InfoResponse = {
async getInfo(): Promise<InfoResponse> {
return {
demo: this.hostConfig.isDemo(),
production: this.hostConfig.isProduction(),
};
return response;
}
}

View file

@ -8,9 +8,10 @@ import {
Post
} from '@nestjs/common';
import {
SysPreferences,
SysPreferenceResponse,
UpdateSysPreferenceRequest
} from 'picsur-shared/dist/dto/syspreferences.dto';
} from 'picsur-shared/dist/dto/api/pref.dto';
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';
@ -23,7 +24,7 @@ export class PrefController {
constructor(private prefService: SysPreferenceService) {}
@Get('sys/:key')
async getSysPref(@Param('key') key: string) {
async getSysPref(@Param('key') key: string): Promise<SysPreferenceResponse> {
const returned = await this.prefService.getPreference(
key as SysPreferences,
);
@ -39,7 +40,7 @@ export class PrefController {
async setSysPref(
@Param('key') key: string,
@Body() body: UpdateSysPreferenceRequest,
) {
): Promise<SysPreferenceResponse> {
const value = body.value;
const returned = await this.prefService.setPreference(
key as SysPreferences,

View file

@ -8,9 +8,14 @@ import {
} from '@nestjs/common';
import {
RoleCreateRequest,
RoleCreateResponse,
RoleDeleteRequest,
RoleDeleteResponse,
RoleInfoRequest,
RoleUpdateRequest
RoleInfoResponse,
RoleListResponse,
RoleUpdateRequest,
RoleUpdateResponse
} from 'picsur-shared/dist/dto/api/roles.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { RolesService } from '../../../collections/roledb/roledb.service';
@ -24,19 +29,22 @@ export class RolesController {
constructor(private rolesService: RolesService) {}
@Get('/list')
getRoles() {
const roles = this.rolesService.findAll();
async getRoles(): Promise<RoleListResponse> {
const roles = await this.rolesService.findAll();
if (HasFailed(roles)) {
this.logger.warn(roles.getReason());
throw new InternalServerErrorException('Could not list roles');
}
return roles;
return {
roles,
total: roles.length,
};
}
@Post('/info')
getRole(@Body() body: RoleInfoRequest) {
const role = this.rolesService.findOne(body.name);
async getRole(@Body() body: RoleInfoRequest): Promise<RoleInfoResponse> {
const role = await this.rolesService.findOne(body.name);
if (HasFailed(role)) {
this.logger.warn(role.getReason());
throw new InternalServerErrorException('Could not find role');
@ -45,9 +53,11 @@ export class RolesController {
return role;
}
@Post('/permissions/set')
updateRole(@Body() body: RoleUpdateRequest) {
const updatedRole = this.rolesService.setPermissions(
@Post('/permissions')
async updateRole(
@Body() body: RoleUpdateRequest,
): Promise<RoleUpdateResponse> {
const updatedRole = await this.rolesService.setPermissions(
body.name,
body.permissions,
);
@ -59,39 +69,11 @@ export class RolesController {
return updatedRole;
}
@Post('/permissions/add')
addPermissions(@Body() body: RoleUpdateRequest) {
const updatedRole = this.rolesService.addPermissions(
body.name,
body.permissions,
);
if (HasFailed(updatedRole)) {
this.logger.warn(updatedRole.getReason());
throw new InternalServerErrorException('Could not add role permissions');
}
return updatedRole;
}
@Post('/permissions/remove')
removePermissions(@Body() body: RoleUpdateRequest) {
const updatedRole = this.rolesService.removePermissions(
body.name,
body.permissions,
);
if (HasFailed(updatedRole)) {
this.logger.warn(updatedRole.getReason());
throw new InternalServerErrorException(
'Could not remove role permissions',
);
}
return updatedRole;
}
@Post('/create')
createRole(@Body() role: RoleCreateRequest) {
const newRole = this.rolesService.create(role.name, role.permissions);
async createRole(
@Body() role: RoleCreateRequest,
): Promise<RoleCreateResponse> {
const newRole = await this.rolesService.create(role.name, role.permissions);
if (HasFailed(newRole)) {
this.logger.warn(newRole.getReason());
throw new InternalServerErrorException('Could not create role');
@ -101,8 +83,10 @@ export class RolesController {
}
@Post('/delete')
deleteRole(@Body() role: RoleDeleteRequest) {
const deletedRole = this.rolesService.delete(role.name);
async deleteRole(
@Body() role: RoleDeleteRequest,
): Promise<RoleDeleteResponse> {
const deletedRole = await this.rolesService.delete(role.name);
if (HasFailed(deletedRole)) {
this.logger.warn(deletedRole.getReason());
throw new InternalServerErrorException('Could not delete role');

View file

@ -12,6 +12,7 @@ import {
} from '@nestjs/common';
import { isHash } from 'class-validator';
import { FastifyReply, FastifyRequest } from 'fastify';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator';
import { RequiredPermissions } from '../../decorators/permissions.decorator';
@ -29,7 +30,7 @@ export class ImageController {
async getImage(
@Res({ passthrough: true }) res: FastifyReply,
@Param('hash') hash: string,
) {
): Promise<Buffer> {
if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash');
const image = await this.imagesService.retrieveComplete(hash);
@ -43,7 +44,7 @@ export class ImageController {
}
@Get('meta/:hash')
async getImageMeta(@Param('hash') hash: string) {
async getImageMeta(@Param('hash') hash: string): Promise<ImageMetaResponse> {
if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash');
const image = await this.imagesService.retrieveInfo(hash);
@ -60,7 +61,7 @@ export class ImageController {
async uploadImage(
@Req() req: FastifyRequest,
@MultiPart(ImageUploadDto) multipart: ImageUploadDto,
) {
): Promise<ImageMetaResponse> {
const fileBuffer = await multipart.image.toBuffer();
const image = await this.imagesService.upload(fileBuffer);
if (HasFailed(image)) {

View file

@ -4,12 +4,11 @@ import { validate } from 'class-validator';
import {
ApiResponse,
ApiSuccessResponse
} from 'picsur-shared/dist/dto/api.dto';
} from 'picsur-shared/dist/dto/api';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { MultiPartRequest } from '../models/multi-part-request';
import { KeyService } from './key.service';
@Injectable({
providedIn: 'root',
})

View file

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
import { EImage } from 'picsur-shared/dist/entities/image.entity';
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
import { ImageUploadRequest } from '../models/image-upload-request';
@ -12,7 +13,7 @@ export class ImageService {
public async UploadImage(image: File): AsyncFailable<string> {
const result = await this.api.postForm(
EImage,
ImageMetaResponse,
'/i',
new ImageUploadRequest(image)
);
@ -23,7 +24,7 @@ export class ImageService {
}
public async GetImageMeta(image: string): AsyncFailable<EImage> {
return await this.api.get(EImage, `/i/meta/${image}`);
return await this.api.get(ImageMetaResponse, `/i/meta/${image}`);
}
public GetImageURL(image: string): string {

View file

@ -3,16 +3,13 @@ import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import jwt_decode from 'jwt-decode';
import {
AuthLoginRequest,
AuthLoginResponse,
AuthMeResponse,
JwtDataDto
} from 'picsur-shared/dist/dto/auth.dto';
UserLoginRequest,
UserLoginResponse,
UserMeResponse
} from 'picsur-shared/dist/dto/api/user.dto';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto';
import { EUser } from 'picsur-shared/dist/entities/user.entity';
import {
AsyncFailable,
Fail, HasFailed
} from 'picsur-shared/dist/types';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { BehaviorSubject } from 'rxjs';
import { ApiService } from './api.service';
import { KeyService } from './key.service';
@ -38,19 +35,19 @@ export class UserService {
private userSubject = new BehaviorSubject<EUser | null>(null);
constructor(private api: ApiService, private key: KeyService) {
this.init().catch(console.error);
this.init().catch(this.logger.error);
}
public async login(username: string, password: string): AsyncFailable<EUser> {
const request: AuthLoginRequest = {
const request: UserLoginRequest = {
username,
password,
};
const response = await this.api.post(
AuthLoginRequest,
AuthLoginResponse,
'/api/auth/login',
UserLoginRequest,
UserLoginResponse,
'/api/user/login',
request
);
@ -117,7 +114,7 @@ export class UserService {
}
private async fetchUser(): AsyncFailable<EUser> {
const got = await this.api.get(AuthMeResponse, '/api/auth/me');
const got = await this.api.get(UserMeResponse, '/api/user/me');
if (HasFailed(got)) return got;
this.key.set(got.newJwtToken);

View file

@ -13,6 +13,8 @@ import { UtilService } from 'src/app/util/util.service';
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
private readonly logger = console;
private currentUser: EUser | null = null;
public get user() {
@ -36,7 +38,7 @@ export class HeaderComponent implements OnInit {
@AutoUnsubscribe()
subscribeUser() {
return this.userService.liveUser.subscribe((user) => {
console.log('user', user);
this.logger.log('user', user);
this.currentUser = user;
});
}

View file

@ -0,0 +1,3 @@
import { EImage } from '../../entities/image.entity';
export class ImageMetaResponse extends EImage {}

View file

@ -0,0 +1,9 @@
import { IsNotEmpty } from 'class-validator';
import { ESysPreference } from '../../entities/syspreference.entity';
export class UpdateSysPreferenceRequest {
@IsNotEmpty()
value: string;
}
export class SysPreferenceResponse extends ESysPreference {}

View file

@ -1,13 +1,32 @@
import { IsArray, IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { Type } from 'class-transformer';
import { IsArray, IsDefined, IsEnum, IsInt, IsNotEmpty, IsPositive, IsString, ValidateNested } from 'class-validator';
import { ERole } from '../../entities/role.entity';
import { Permissions, PermissionsList } from '../permissions';
// RoleInfo
export class RoleInfoRequest {
@IsNotEmpty()
@IsString()
name: string;
}
export class RoleInfoResponse extends ERole {}
// RoleList
export class RoleListResponse {
@IsArray()
@IsDefined()
@ValidateNested()
@Type(() => ERole)
roles: ERole[];
@IsInt()
@IsPositive()
@IsDefined()
total: number;
}
// RoleUpdate
export class RoleUpdateRequest {
@IsNotEmpty()
@IsString()
@ -17,10 +36,15 @@ export class RoleUpdateRequest {
@IsEnum(PermissionsList, { each: true })
permissions: Permissions;
}
export class RoleUpdateResponse extends ERole {}
// RoleCreate
export class RoleCreateRequest extends ERole {}
export class RoleCreateResponse extends ERole {}
// RoleDelete
export class RoleDeleteRequest {
@IsNotEmpty()
name: string;
}
export class RoleDeleteResponse extends ERole {}

View file

@ -3,17 +3,22 @@ import {
IsArray,
IsBoolean,
IsDefined,
IsEnum, IsNotEmpty,
IsEnum,
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString,
ValidateNested
} from 'class-validator';
import { EUser } from '../../entities/user.entity';
import { Permissions, PermissionsList } from '../permissions';
import { Roles } from '../roles.dto';
// Api
export class AuthLoginRequest {
// UserLogin
export class UserLoginRequest {
@IsNotEmpty()
@IsString()
username: string;
@ -23,13 +28,14 @@ export class AuthLoginRequest {
password: string;
}
export class AuthLoginResponse {
export class UserLoginResponse {
@IsString()
@IsDefined()
jwt_token: string;
}
export class AuthRegisterRequest {
// UserRegister
export class UserRegisterRequest {
@IsString()
@IsNotEmpty()
username: string;
@ -43,15 +49,42 @@ export class AuthRegisterRequest {
isAdmin?: boolean;
}
export class AuthDeleteRequest {
export class UserRegisterResponse extends EUser {}
// UserDelete
export class UserDeleteRequest {
@IsString()
@IsNotEmpty()
username: string;
}
export class AuthDeleteResponse extends EUser {}
export class UserDeleteResponse extends EUser {}
export class AuthMeResponse {
// UserInfo
export class UserInfoRequest {
@IsString()
@IsNotEmpty()
username: string;
}
export class UserInfoResponse extends EUser {}
// UserList
export class UserListResponse {
@IsArray()
@IsDefined()
@ValidateNested()
@Type(() => EUser)
users: EUser[];
@IsInt()
@IsPositive()
@IsDefined()
total: number;
}
// UserMe
export class UserMeResponse {
@IsDefined()
@ValidateNested()
@Type(() => EUser)
@ -67,8 +100,16 @@ export class AuthMeResponse {
newJwtToken: string;
}
export class AuthUserInfoRequest {
// UserUpdateRoles
export class UserUpdateRolesRequest {
@IsString()
@IsNotEmpty()
username: string;
@IsArray()
@IsDefined()
@IsString({ each: true })
roles: Roles;
}
export class UserUpdateRolesResponse extends EUser {}

View file

@ -1,4 +1,3 @@
import { IsNotEmpty } from 'class-validator';
import tuple from '../types/tuple';
const SysPreferencesTuple = tuple(
@ -9,8 +8,3 @@ const SysPreferencesTuple = tuple(
export const SysPreferences: string[] = SysPreferencesTuple;
export type SysPreferences = typeof SysPreferencesTuple[number];
export class UpdateSysPreferenceRequest {
@IsNotEmpty()
value: string;
}

View file

@ -14,6 +14,8 @@ export type Failable<T> = T | Failure;
export type AsyncFailable<T> = Promise<Failable<T>>;
// TODO: prevent promise from being allowed in these 2 functions
export function HasFailed<T>(failable: Failable<T>): failable is Failure {
return failable instanceof Failure;
}