mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-13 23:35:39 +01:00
add user edit page
This commit is contained in:
@@ -6,6 +6,7 @@ import { PicsurConfigModule } from '../../config/config.module';
|
|||||||
import { EUserBackend } from '../../models/entities/user.entity';
|
import { EUserBackend } from '../../models/entities/user.entity';
|
||||||
import { RolesModule } from '../roledb/roledb.module';
|
import { RolesModule } from '../roledb/roledb.module';
|
||||||
import { UsersService } from './userdb.service';
|
import { UsersService } from './userdb.service';
|
||||||
|
import { UserRolesService } from './userrolesdb.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -13,14 +14,15 @@ import { UsersService } from './userdb.service';
|
|||||||
RolesModule,
|
RolesModule,
|
||||||
TypeOrmModule.forFeature([EUserBackend]),
|
TypeOrmModule.forFeature([EUserBackend]),
|
||||||
],
|
],
|
||||||
providers: [UsersService],
|
providers: [UsersService, UserRolesService],
|
||||||
exports: [UsersService],
|
exports: [UsersService, UserRolesService],
|
||||||
})
|
})
|
||||||
export class UsersModule implements OnModuleInit {
|
export class UsersModule implements OnModuleInit {
|
||||||
private readonly logger = new Logger('UsersModule');
|
private readonly logger = new Logger('UsersModule');
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private usersService: UsersService,
|
private usersService: UsersService,
|
||||||
|
private userRolesService: UserRolesService,
|
||||||
private authConfigService: AuthConfigService,
|
private authConfigService: AuthConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ export class UsersModule implements OnModuleInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.usersService.addRoles(newUser, ['admin']);
|
const result = await this.userRolesService.addRoles(newUser, ['admin']);
|
||||||
if (HasFailed(result)) {
|
if (HasFailed(result)) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to make admin user "${username}" because: ${result.getReason()}`,
|
`Failed to make admin user "${username}" because: ${result.getReason()}`,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { plainToClass } from 'class-transformer';
|
import { plainToClass } from 'class-transformer';
|
||||||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
|
||||||
import { PermanentRolesList, Roles } from 'picsur-shared/dist/dto/roles.dto';
|
import { PermanentRolesList, Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||||
import {
|
import {
|
||||||
AsyncFailable,
|
AsyncFailable,
|
||||||
@@ -16,6 +15,8 @@ import { EUserBackend } from '../../models/entities/user.entity';
|
|||||||
import { GetCols } from '../collectionutils';
|
import { GetCols } from '../collectionutils';
|
||||||
import { RolesService } from '../roledb/roledb.service';
|
import { RolesService } from '../roledb/roledb.service';
|
||||||
|
|
||||||
|
const BCryptStrength = 12;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
private readonly logger = new Logger('UsersService');
|
private readonly logger = new Logger('UsersService');
|
||||||
@@ -35,7 +36,7 @@ export class UsersService {
|
|||||||
): AsyncFailable<EUserBackend> {
|
): AsyncFailable<EUserBackend> {
|
||||||
if (await this.exists(username)) return Fail('User already exists');
|
if (await this.exists(username)) return Fail('User already exists');
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
const hashedPassword = await bcrypt.hash(password, BCryptStrength);
|
||||||
|
|
||||||
let user = new EUserBackend();
|
let user = new EUserBackend();
|
||||||
user.username = username;
|
user.username = username;
|
||||||
@@ -65,6 +66,52 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updating
|
||||||
|
|
||||||
|
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 rolesToAdd = roles.filter(
|
||||||
|
(role) => !PermanentRolesList.includes(role),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newRoles = [...new Set([...rolesToKeep, ...rolesToAdd])];
|
||||||
|
|
||||||
|
userToModify.roles = newRoles;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.usersRepository.save(userToModify);
|
||||||
|
} catch (e: any) {
|
||||||
|
return Fail(e?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updatePassword(
|
||||||
|
user: string | EUserBackend,
|
||||||
|
password: string,
|
||||||
|
): AsyncFailable<EUserBackend> {
|
||||||
|
const userToModify = await this.resolve(user);
|
||||||
|
if (HasFailed(userToModify)) return userToModify;
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, BCryptStrength);
|
||||||
|
|
||||||
|
userToModify.password = hashedPassword;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullUser = await this.usersRepository.save(userToModify);
|
||||||
|
return plainToClass(EUserBackend, fullUser);
|
||||||
|
} catch (e: any) {
|
||||||
|
return Fail(e?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
|
|
||||||
async authenticate(
|
async authenticate(
|
||||||
@@ -80,62 +127,6 @@ export class UsersService {
|
|||||||
return await this.findOne(username);
|
return await this.findOne(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permissions and roles
|
|
||||||
|
|
||||||
public async getPermissions(
|
|
||||||
user: string | EUserBackend,
|
|
||||||
): AsyncFailable<Permissions> {
|
|
||||||
const userToModify = await this.resolve(user);
|
|
||||||
if (HasFailed(userToModify)) return userToModify;
|
|
||||||
|
|
||||||
return await this.rolesService.getPermissions(userToModify.roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addRoles(
|
|
||||||
user: string | EUserBackend,
|
|
||||||
roles: Roles,
|
|
||||||
): AsyncFailable<EUserBackend> {
|
|
||||||
const userToModify = await this.resolve(user);
|
|
||||||
if (HasFailed(userToModify)) return userToModify;
|
|
||||||
|
|
||||||
const newRoles = [...new Set([...userToModify.roles, ...roles])];
|
|
||||||
|
|
||||||
return this.setRoles(userToModify, newRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async removeRoles(
|
|
||||||
user: string | EUserBackend,
|
|
||||||
roles: Roles,
|
|
||||||
): AsyncFailable<EUserBackend> {
|
|
||||||
const userToModify = await this.resolve(user);
|
|
||||||
if (HasFailed(userToModify)) return userToModify;
|
|
||||||
|
|
||||||
const newRoles = 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 {
|
|
||||||
return await this.usersRepository.save(userToModify);
|
|
||||||
} catch (e: any) {
|
|
||||||
return Fail(e?.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listing
|
// Listing
|
||||||
|
|
||||||
public async findOne<B extends true | undefined = undefined>(
|
public async findOne<B extends true | undefined = undefined>(
|
||||||
@@ -182,7 +173,7 @@ export class UsersService {
|
|||||||
|
|
||||||
// Internal resolver
|
// Internal resolver
|
||||||
|
|
||||||
private async resolve(
|
public async resolve(
|
||||||
user: string | EUserBackend,
|
user: string | EUserBackend,
|
||||||
): AsyncFailable<EUserBackend> {
|
): AsyncFailable<EUserBackend> {
|
||||||
if (typeof user === 'string') {
|
if (typeof user === 'string') {
|
||||||
|
|||||||
47
backend/src/collections/userdb/userrolesdb.service.ts
Normal file
47
backend/src/collections/userdb/userrolesdb.service.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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';
|
||||||
|
import { UsersService } from './userdb.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserRolesService {
|
||||||
|
constructor(private usersService: UsersService, private rolesService: RolesService){}
|
||||||
|
|
||||||
|
// Permissions and roles
|
||||||
|
|
||||||
|
public async getPermissions(
|
||||||
|
user: string | EUserBackend,
|
||||||
|
): AsyncFailable<Permissions> {
|
||||||
|
const userToModify = await this.usersService.resolve(user);
|
||||||
|
if (HasFailed(userToModify)) return userToModify;
|
||||||
|
|
||||||
|
return await this.rolesService.getPermissions(userToModify.roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addRoles(
|
||||||
|
user: string | EUserBackend,
|
||||||
|
roles: Roles,
|
||||||
|
): AsyncFailable<EUserBackend> {
|
||||||
|
const userToModify = await this.usersService.resolve(user);
|
||||||
|
if (HasFailed(userToModify)) return userToModify;
|
||||||
|
|
||||||
|
const newRoles = [...new Set([...userToModify.roles, ...roles])];
|
||||||
|
|
||||||
|
return this.usersService.setRoles(userToModify, newRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeRoles(
|
||||||
|
user: string | EUserBackend,
|
||||||
|
roles: Roles,
|
||||||
|
): AsyncFailable<EUserBackend> {
|
||||||
|
const userToModify = await this.usersService.resolve(user);
|
||||||
|
if (HasFailed(userToModify)) return userToModify;
|
||||||
|
|
||||||
|
const newRoles = userToModify.roles.filter((role) => !roles.includes(role));
|
||||||
|
|
||||||
|
return this.usersService.setRoles(userToModify, newRoles);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import * as multipart from 'fastify-multipart';
|
|||||||
import { ValidateOptions } from 'picsur-shared/dist/util/validate';
|
import { ValidateOptions } from 'picsur-shared/dist/util/validate';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { UsersService } from './collections/userdb/userdb.service';
|
import { UsersService } from './collections/userdb/userdb.service';
|
||||||
|
import { UserRolesService } from './collections/userdb/userrolesdb.service';
|
||||||
import { HostConfigService } from './config/host.config.service';
|
import { HostConfigService } from './config/host.config.service';
|
||||||
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
||||||
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
||||||
@@ -15,11 +16,12 @@ import { PicsurLoggerService } from './logger/logger.service';
|
|||||||
import { MainAuthGuard } from './managers/auth/guards/main.guard';
|
import { MainAuthGuard } from './managers/auth/guards/main.guard';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
// Create fasify
|
||||||
const fastifyAdapter = new FastifyAdapter();
|
const fastifyAdapter = new FastifyAdapter();
|
||||||
|
|
||||||
// TODO: generic error messages
|
// TODO: generic error messages
|
||||||
fastifyAdapter.register(multipart as any);
|
fastifyAdapter.register(multipart as any);
|
||||||
|
|
||||||
|
// Create nest app
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
AppModule,
|
AppModule,
|
||||||
fastifyAdapter,
|
fastifyAdapter,
|
||||||
@@ -27,15 +29,23 @@ async function bootstrap() {
|
|||||||
bufferLogs: true,
|
bufferLogs: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Configure nest app
|
||||||
app.useGlobalFilters(new MainExceptionFilter());
|
app.useGlobalFilters(new MainExceptionFilter());
|
||||||
app.useGlobalInterceptors(new SuccessInterceptor());
|
app.useGlobalInterceptors(new SuccessInterceptor());
|
||||||
app.useGlobalPipes(new ValidationPipe(ValidateOptions));
|
app.useGlobalPipes(new ValidationPipe(ValidateOptions));
|
||||||
app.useGlobalGuards(
|
app.useGlobalGuards(
|
||||||
new MainAuthGuard(app.get(Reflector), app.get(UsersService)),
|
new MainAuthGuard(
|
||||||
|
app.get(Reflector),
|
||||||
|
app.get(UsersService),
|
||||||
|
app.get(UserRolesService),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Configure logger
|
||||||
app.useLogger(app.get(PicsurLoggerService));
|
app.useLogger(app.get(PicsurLoggerService));
|
||||||
|
|
||||||
|
// Start app
|
||||||
const hostConfigService = app.get(HostConfigService);
|
const hostConfigService = app.get(HostConfigService);
|
||||||
await app.listen(hostConfigService.getPort(), hostConfigService.getHost());
|
await app.listen(hostConfigService.getPort(), hostConfigService.getHost());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
|
|||||||
import { isPermissionsArray } from 'picsur-shared/dist/util/permissions';
|
import { isPermissionsArray } from 'picsur-shared/dist/util/permissions';
|
||||||
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
||||||
import { UsersService } from '../../../collections/userdb/userdb.service';
|
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||||
|
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
|
||||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -24,6 +25,7 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
|||||||
constructor(
|
constructor(
|
||||||
private reflector: Reflector,
|
private reflector: Reflector,
|
||||||
private usersService: UsersService,
|
private usersService: UsersService,
|
||||||
|
private userRolesService: UserRolesService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -45,7 +47,7 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
|||||||
throw new InternalServerErrorException();
|
throw new InternalServerErrorException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const userPermissions = await this.usersService.getPermissions(user);
|
const userPermissions = await this.userRolesService.getPermissions(user);
|
||||||
if (HasFailed(userPermissions)) {
|
if (HasFailed(userPermissions)) {
|
||||||
this.logger.warn('111' + userPermissions.getReason());
|
this.logger.warn('111' + userPermissions.getReason());
|
||||||
throw new InternalServerErrorException();
|
throw new InternalServerErrorException();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { Permission } from 'picsur-shared/dist/dto/permissions';
|
import { Permission } from 'picsur-shared/dist/dto/permissions';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types';
|
import { HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { UsersService } from '../../../collections/userdb/userdb.service';
|
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||||
|
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
|
||||||
import {
|
import {
|
||||||
NoPermissions,
|
NoPermissions,
|
||||||
RequiredPermissions,
|
RequiredPermissions,
|
||||||
@@ -31,6 +32,7 @@ export class UserController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private usersService: UsersService,
|
private usersService: UsersService,
|
||||||
|
private userRolesSerivce: UserRolesService,
|
||||||
private authService: AuthManagerService,
|
private authService: AuthManagerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ export class UserController {
|
|||||||
async refresh(
|
async refresh(
|
||||||
@Request() req: AuthFasityRequest,
|
@Request() req: AuthFasityRequest,
|
||||||
): Promise<UserMePermissionsResponse> {
|
): Promise<UserMePermissionsResponse> {
|
||||||
const permissions = await this.usersService.getPermissions(req.user);
|
const permissions = await this.userRolesSerivce.getPermissions(req.user);
|
||||||
if (HasFailed(permissions)) {
|
if (HasFailed(permissions)) {
|
||||||
this.logger.warn(permissions.getReason());
|
this.logger.warn(permissions.getReason());
|
||||||
throw new InternalServerErrorException('Could not get permissions');
|
throw new InternalServerErrorException('Could not get permissions');
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
UserInfoResponse,
|
UserInfoResponse,
|
||||||
UserListRequest,
|
UserListRequest,
|
||||||
UserListResponse,
|
UserListResponse,
|
||||||
UserUpdateRolesRequest,
|
UserUpdateRequest,
|
||||||
UserUpdateRolesResponse
|
UserUpdateResponse
|
||||||
} from 'picsur-shared/dist/dto/api/usermanage.dto';
|
} from 'picsur-shared/dist/dto/api/usermanage.dto';
|
||||||
import { Permission } from 'picsur-shared/dist/dto/permissions';
|
import { Permission } from 'picsur-shared/dist/dto/permissions';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types';
|
import { HasFailed } from 'picsur-shared/dist/types';
|
||||||
@@ -63,6 +63,7 @@ export class UserManageController {
|
|||||||
const user = await this.usersService.create(
|
const user = await this.usersService.create(
|
||||||
create.username,
|
create.username,
|
||||||
create.password,
|
create.password,
|
||||||
|
create.roles,
|
||||||
);
|
);
|
||||||
if (HasFailed(user)) {
|
if (HasFailed(user)) {
|
||||||
this.logger.warn(user.getReason());
|
this.logger.warn(user.getReason());
|
||||||
@@ -96,20 +97,32 @@ export class UserManageController {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('roles')
|
@Post('update')
|
||||||
async setPermissions(
|
async setPermissions(
|
||||||
@Body() body: UserUpdateRolesRequest,
|
@Body() body: UserUpdateRequest,
|
||||||
): Promise<UserUpdateRolesResponse> {
|
): Promise<UserUpdateResponse> {
|
||||||
const updatedUser = await this.usersService.setRoles(
|
let user = await this.usersService.findOne(body.username);
|
||||||
body.username,
|
if (HasFailed(user)) {
|
||||||
body.roles,
|
this.logger.warn(user.getReason());
|
||||||
);
|
throw new InternalServerErrorException('Could not find user');
|
||||||
|
}
|
||||||
|
|
||||||
if (HasFailed(updatedUser)) {
|
if (body.roles) {
|
||||||
this.logger.warn(updatedUser.getReason());
|
user = await this.usersService.setRoles(user, body.roles);
|
||||||
|
if (HasFailed(user)) {
|
||||||
|
this.logger.warn(user.getReason());
|
||||||
throw new InternalServerErrorException('Could not update user');
|
throw new InternalServerErrorException('Could not update user');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return updatedUser;
|
if (body.password) {
|
||||||
|
user = await this.usersService.updatePassword(user, body.password);
|
||||||
|
if (HasFailed(user)) {
|
||||||
|
this.logger.warn(user.getReason());
|
||||||
|
throw new InternalServerErrorException('Could not update user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
|
"fuse.js": "^6.5.3",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"ngx-auto-unsubscribe-decorator": "^1.0.0",
|
"ngx-auto-unsubscribe-decorator": "^1.0.0",
|
||||||
"ngx-dropzone": "^3.1.0",
|
"ngx-dropzone": "^3.1.0",
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ export class HeaderComponent implements OnInit {
|
|||||||
this.router.navigate(['/user/login']);
|
this.router.navigate(['/user/login']);
|
||||||
}
|
}
|
||||||
|
|
||||||
doLogout() {
|
async doLogout() {
|
||||||
const user = this.userService.logout();
|
const user = await this.userService.logout();
|
||||||
if (HasFailed(user)) {
|
if (HasFailed(user)) {
|
||||||
this.utilService.showSnackBar(user.getReason(), SnackBarType.Error);
|
this.utilService.showSnackBar(user.getReason(), SnackBarType.Error);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ function errorsToError(errors: ValidationErrors | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UsernameValidators = [
|
export const UsernameValidators = [
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(4),
|
Validators.minLength(4),
|
||||||
Validators.maxLength(32),
|
Validators.maxLength(32),
|
||||||
Validators.pattern('^[a-zA-Z0-9]+$'),
|
Validators.pattern('^[a-zA-Z0-9]+$'),
|
||||||
@@ -37,7 +36,6 @@ export const CreateUsernameError = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PasswordValidators = [
|
export const PasswordValidators = [
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(4),
|
Validators.minLength(4),
|
||||||
Validators.maxLength(1024),
|
Validators.maxLength(1024),
|
||||||
];
|
];
|
||||||
|
|||||||
7
frontend/src/app/models/forms/fulluser.model.ts
Normal file
7
frontend/src/app/models/forms/fulluser.model.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||||
|
|
||||||
|
export interface FullUserModel {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
roles: Roles;
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
PasswordValidators,
|
PasswordValidators,
|
||||||
UsernameValidators
|
UsernameValidators
|
||||||
} from './default-validators';
|
} from './default-validators';
|
||||||
import { UserPassModel } from './userpass';
|
import { UserPassModel } from './userpass.model';
|
||||||
|
|
||||||
export class LoginControl {
|
export class LoginControl {
|
||||||
public username = new FormControl('', UsernameValidators);
|
public username = new FormControl('', UsernameValidators);
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
PasswordValidators,
|
PasswordValidators,
|
||||||
UsernameValidators
|
UsernameValidators
|
||||||
} from './default-validators';
|
} from './default-validators';
|
||||||
import { UserPassModel } from './userpass';
|
import { UserPassModel } from './userpass.model';
|
||||||
|
|
||||||
export class RegisterControl {
|
export class RegisterControl {
|
||||||
public username = new FormControl('', UsernameValidators);
|
public username = new FormControl('', UsernameValidators);
|
||||||
123
frontend/src/app/models/forms/updateuser.control.ts
Normal file
123
frontend/src/app/models/forms/updateuser.control.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
import { PermanentRolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||||
|
import { ERole } from 'picsur-shared/dist/entities/role.entity';
|
||||||
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||||
|
import {
|
||||||
|
CreatePasswordError,
|
||||||
|
CreateUsernameError,
|
||||||
|
PasswordValidators,
|
||||||
|
UsernameValidators
|
||||||
|
} from './default-validators';
|
||||||
|
import { FullUserModel } from './fulluser.model';
|
||||||
|
|
||||||
|
export class UpdateUserControl {
|
||||||
|
// Set once
|
||||||
|
private fullRoles: ERole[] = [];
|
||||||
|
private roles: string[] = [];
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
private selectableRolesSubject = new BehaviorSubject<string[]>([]);
|
||||||
|
private rolesInputSubscription: null | Subscription;
|
||||||
|
|
||||||
|
public username = new FormControl('', UsernameValidators);
|
||||||
|
public password = new FormControl('', PasswordValidators);
|
||||||
|
|
||||||
|
public rolesControl = new FormControl('', []);
|
||||||
|
public selectableRoles = this.selectableRolesSubject.asObservable();
|
||||||
|
public selectedRoles: string[] = [];
|
||||||
|
|
||||||
|
public get usernameValue() {
|
||||||
|
return this.username.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get usernameError() {
|
||||||
|
return CreateUsernameError(this.username.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get passwordError() {
|
||||||
|
return CreatePasswordError(this.password.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.rolesInputSubscription = this.rolesControl.valueChanges.subscribe(
|
||||||
|
(roles) => {
|
||||||
|
this.updateSelectableRoles();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
if (this.rolesInputSubscription) {
|
||||||
|
this.rolesInputSubscription.unsubscribe();
|
||||||
|
this.rolesInputSubscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addRole(role: string) {
|
||||||
|
if (!this.selectableRolesSubject.value.includes(role)) return;
|
||||||
|
|
||||||
|
this.selectedRoles.push(role);
|
||||||
|
this.clearInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeRole(role: string) {
|
||||||
|
this.selectedRoles = this.selectedRoles.filter((r) => r !== role);
|
||||||
|
this.updateSelectableRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isRemovable(role: string) {
|
||||||
|
if (PermanentRolesList.includes(role)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data interaction
|
||||||
|
|
||||||
|
public putAllRoles(roles: ERole[]) {
|
||||||
|
this.fullRoles = roles;
|
||||||
|
this.roles = roles.map((role) => role.name);
|
||||||
|
this.updateSelectableRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public putUsername(username: string) {
|
||||||
|
this.username.setValue(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public putRoles(roles: string[]) {
|
||||||
|
this.selectedRoles = roles;
|
||||||
|
this.updateSelectableRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getData(): FullUserModel {
|
||||||
|
return {
|
||||||
|
username: this.username.value,
|
||||||
|
password: this.password.value,
|
||||||
|
roles: this.selectedRoles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic
|
||||||
|
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchValue = this.rolesControl.value;
|
||||||
|
if (searchValue && availableRoles.length > 0) {
|
||||||
|
const fuse = new Fuse(availableRoles);
|
||||||
|
const result = fuse
|
||||||
|
.search(this.rolesControl.value ?? '')
|
||||||
|
.map((r) => r.item);
|
||||||
|
|
||||||
|
this.selectableRolesSubject.next(result);
|
||||||
|
} else {
|
||||||
|
this.selectableRolesSubject.next(availableRoles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearInput() {
|
||||||
|
this.rolesControl.setValue('');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,100 @@
|
|||||||
<p>settings-users-edit works!</p>
|
<ng-container *ngIf="editing">
|
||||||
|
<h1>Editing {{ model.usernameValue }}</h1>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="adding">
|
||||||
|
<h1>Add new user</h1>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<form (ngSubmit)="updateUser()">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 py-2" *ngIf="updateFail">
|
||||||
|
<mat-error *ngIf="adding"> Failed to add user </mat-error>
|
||||||
|
<mat-error *ngIf="editing"> Failed to update user </mat-error>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" *ngIf="adding">
|
||||||
|
<div class="col-lg-6 col-12">
|
||||||
|
<mat-form-field appearance="outline" color="accent">
|
||||||
|
<mat-label>Username</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[formControl]="model.username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="model.username.errors">{{
|
||||||
|
model.usernameError
|
||||||
|
}}</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6 col-12">
|
||||||
|
<mat-form-field appearance="outline" color="accent">
|
||||||
|
<mat-label>{{ editing ? "New Password" : "Password" }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="password"
|
||||||
|
[formControl]="model.password"
|
||||||
|
name="password"
|
||||||
|
[required]="adding"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="model.password.errors">{{
|
||||||
|
model.passwordError
|
||||||
|
}}</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6 col-12">
|
||||||
|
<mat-form-field appearance="outline" color="accent">
|
||||||
|
<mat-label>Roles</mat-label>
|
||||||
|
<mat-chip-list #chipList aria-label="Roles Selection">
|
||||||
|
<mat-chip
|
||||||
|
*ngFor="let role of model.selectedRoles"
|
||||||
|
[removable]="model.isRemovable(role)"
|
||||||
|
(removed)="removeRole(role)"
|
||||||
|
>
|
||||||
|
{{ role }}
|
||||||
|
<button *ngIf="model.isRemovable(role)" matChipRemove>
|
||||||
|
<mat-icon>cancel</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-chip>
|
||||||
|
<input
|
||||||
|
placeholder="Add role..."
|
||||||
|
#fruitInput
|
||||||
|
[formControl]="model.rolesControl"
|
||||||
|
[value]="model.rolesControl.value"
|
||||||
|
[matAutocomplete]="auto"
|
||||||
|
[matChipInputFor]="chipList"
|
||||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
|
(matChipInputTokenEnd)="addRole($event)"
|
||||||
|
/>
|
||||||
|
</mat-chip-list>
|
||||||
|
<mat-autocomplete
|
||||||
|
#auto="matAutocomplete"
|
||||||
|
(optionSelected)="selectedRole($event)"
|
||||||
|
>
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let role of model.selectableRoles | async"
|
||||||
|
[value]="role"
|
||||||
|
>
|
||||||
|
{{ role }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 py-2">
|
||||||
|
<button mat-raised-button color="accent" type="submit">
|
||||||
|
{{ editing ? "Update" : "Add" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,128 @@
|
|||||||
|
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||||
import { Component, OnInit } from '@angular/core';
|
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 { HasFailed } from 'picsur-shared/dist/types';
|
||||||
|
import { UpdateUserControl } from 'src/app/models/forms/updateuser.control';
|
||||||
|
import { SnackBarType } from 'src/app/models/snack-bar-type';
|
||||||
|
import { RolesService } from 'src/app/services/api/roles.service';
|
||||||
|
import { UserManageService } from 'src/app/services/api/usermanage.service';
|
||||||
|
import { UtilService } from 'src/app/util/util.service';
|
||||||
|
|
||||||
|
enum EditMode {
|
||||||
|
edit = 'edit',
|
||||||
|
add = 'add',
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings-users-edit',
|
selector: 'app-settings-users-edit',
|
||||||
templateUrl: './settings-users-edit.component.html',
|
templateUrl: './settings-users-edit.component.html',
|
||||||
styleUrls: ['./settings-users-edit.component.scss']
|
styleUrls: ['./settings-users-edit.component.scss'],
|
||||||
})
|
})
|
||||||
export class SettingsUsersEditComponent implements OnInit {
|
export class SettingsUsersEditComponent implements OnInit {
|
||||||
|
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||||
|
|
||||||
constructor() { }
|
private mode: EditMode = EditMode.edit;
|
||||||
|
|
||||||
ngOnInit(): void {
|
model = new UpdateUserControl();
|
||||||
|
updateFail: boolean = false;
|
||||||
|
|
||||||
|
get adding() {
|
||||||
|
return this.mode === EditMode.add;
|
||||||
|
}
|
||||||
|
get editing() {
|
||||||
|
return this.mode === EditMode.edit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private userManageService: UserManageService,
|
||||||
|
private utilService: UtilService,
|
||||||
|
private rolesService: RolesService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
Promise.all([this.initUser(), this.initRoles()]).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initUser() {
|
||||||
|
const username = this.route.snapshot.paramMap.get('username');
|
||||||
|
if (!username) {
|
||||||
|
this.mode = EditMode.add;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mode = EditMode.edit;
|
||||||
|
this.model.putUsername(username);
|
||||||
|
|
||||||
|
const user = await this.userManageService.getUser(username);
|
||||||
|
if (HasFailed(user)) {
|
||||||
|
this.utilService.showSnackBar('Failed to get user', SnackBarType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.model.putUsername(user.username);
|
||||||
|
this.model.putRoles(user.roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initRoles() {
|
||||||
|
const roles = await this.rolesService.getRoles();
|
||||||
|
if (HasFailed(roles)) {
|
||||||
|
this.utilService.showSnackBar('Failed to get roles', SnackBarType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.model.putAllRoles(roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRole(role: string) {
|
||||||
|
this.model.removeRole(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
addRole(event: MatChipInputEvent) {
|
||||||
|
const value = (event.value ?? '').trim();
|
||||||
|
this.model.addRole(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRole(event: MatAutocompleteSelectedEvent): void {
|
||||||
|
this.model.addRole(event.option.viewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser() {
|
||||||
|
const data = this.model.getData();
|
||||||
|
|
||||||
|
if (this.adding) {
|
||||||
|
const resultUser = await this.userManageService.createUser(data);
|
||||||
|
if (HasFailed(resultUser)) {
|
||||||
|
this.utilService.showSnackBar(
|
||||||
|
'Failed to create user',
|
||||||
|
SnackBarType.Error
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.utilService.showSnackBar('User created', SnackBarType.Success);
|
||||||
|
} else {
|
||||||
|
const updateData = data.password
|
||||||
|
? data
|
||||||
|
: { username: data.username, roles: data.roles };
|
||||||
|
|
||||||
|
const resultUser = await this.userManageService.updateUser(
|
||||||
|
updateData as any
|
||||||
|
);
|
||||||
|
if (HasFailed(resultUser)) {
|
||||||
|
this.utilService.showSnackBar(
|
||||||
|
'Failed to update user',
|
||||||
|
SnackBarType.Error
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.utilService.showSnackBar('User updated', SnackBarType.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['/settings/users']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<h1>Users</h1>
|
<h1>Users</h1>
|
||||||
|
|
||||||
<mat-table [dataSource]="dataSubject" class="mat-elevation-z2">
|
<mat-table [dataSource]="dataSubject" class="mat-elevation-z2">
|
||||||
<ng-container matColumnDef="id">
|
<!-- <ng-container matColumnDef="id">
|
||||||
<mat-header-cell *matHeaderCellDef>ID</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef>ID</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let user">{{ user.id }}</mat-cell>
|
<mat-cell *matCellDef="let user">{{ user.id }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container> -->
|
||||||
|
|
||||||
<ng-container matColumnDef="username">
|
<ng-container matColumnDef="username">
|
||||||
<mat-header-cell *matHeaderCellDef>Username</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef>Username</mat-header-cell>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let user">
|
<mat-cell *matCellDef="let user">
|
||||||
<button mat-icon-button>
|
<button mat-icon-button (click)="editUser(user)">
|
||||||
<mat-icon aria-label="Edit">edit</mat-icon>
|
<mat-icon aria-label="Edit">edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
@@ -24,8 +24,23 @@
|
|||||||
<mat-row *matRowDef="let row; columns: displayedColumns"> pog </mat-row>
|
<mat-row *matRowDef="let row; columns: displayedColumns"> pog </mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator
|
||||||
|
color="accent"
|
||||||
|
[pageSizeOptions]="pageSizeOptions"
|
||||||
|
[pageSize]="startingPageSize"
|
||||||
|
length="Infinity"
|
||||||
|
aria-label="Select page of users"
|
||||||
|
(page)="updateSubject.next($event)"
|
||||||
|
>
|
||||||
|
</mat-paginator>
|
||||||
|
|
||||||
<div class="fabholder">
|
<div class="fabholder">
|
||||||
<button mat-fab color="accent" class="fabbutton fullanimate mat-elevation-z6">
|
<button
|
||||||
|
mat-fab
|
||||||
|
color="accent"
|
||||||
|
class="fabbutton fullanimate mat-elevation-z6"
|
||||||
|
(click)="addUser()"
|
||||||
|
>
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types';
|
import { HasFailed } from 'picsur-shared/dist/types';
|
||||||
@@ -13,7 +14,7 @@ import { UtilService } from 'src/app/util/util.service';
|
|||||||
styleUrls: ['./settings-users.component.scss'],
|
styleUrls: ['./settings-users.component.scss'],
|
||||||
})
|
})
|
||||||
export class SettingsUsersComponent implements OnInit {
|
export class SettingsUsersComponent implements OnInit {
|
||||||
public readonly displayedColumns: string[] = ['id', 'username', 'actions'];
|
public readonly displayedColumns: string[] = [/*'id',*/ 'username', 'actions'];
|
||||||
public readonly pageSizeOptions: number[] = [5, 10, 25, 100];
|
public readonly pageSizeOptions: number[] = [5, 10, 25, 100];
|
||||||
public readonly startingPageSize = this.pageSizeOptions[2];
|
public readonly startingPageSize = this.pageSizeOptions[2];
|
||||||
|
|
||||||
@@ -24,7 +25,8 @@ export class SettingsUsersComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userManageService: UserManageService,
|
private userManageService: UserManageService,
|
||||||
private utilService: UtilService
|
private utilService: UtilService,
|
||||||
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -32,6 +34,14 @@ export class SettingsUsersComponent implements OnInit {
|
|||||||
this.fetchUsers(this.startingPageSize, 0);
|
this.fetchUsers(this.startingPageSize, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public editUser(user: EUser) {
|
||||||
|
this.router.navigate(['/settings/users/edit', user.username]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addUser() {
|
||||||
|
this.router.navigate(['/settings/users/add']);
|
||||||
|
}
|
||||||
|
|
||||||
@AutoUnsubscribe()
|
@AutoUnsubscribe()
|
||||||
private subscribeToUpdate() {
|
private subscribeToUpdate() {
|
||||||
return this.updateSubject
|
return this.updateSubject
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { SettingsUsersEditComponent } from './settings-users-edit/settings-users-edit.component';
|
import { SettingsUsersEditComponent } from './settings-users-edit/settings-users-edit.component';
|
||||||
@@ -19,6 +23,11 @@ import { SettingsUsersRoutingModule } from './settings-users.routing.module';
|
|||||||
MatTableModule,
|
MatTableModule,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsUsersRouteModule {}
|
export class SettingsUsersRouteModule {}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ const routes: PRoutes = [
|
|||||||
component: SettingsUsersComponent,
|
component: SettingsUsersComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit/:id',
|
path: 'edit/:username',
|
||||||
|
component: SettingsUsersEditComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'add',
|
||||||
component: SettingsUsersEditComponent,
|
component: SettingsUsersEditComponent,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
4
frontend/src/app/routes/user/login/login.component.scss
Normal file
4
frontend/src/app/routes/user/login/login.component.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
mat-form-field {
|
||||||
|
max-width: 40rem;
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
@@ -7,11 +7,12 @@ import { SnackBarType } from 'src/app/models/snack-bar-type';
|
|||||||
import { PermissionService } from 'src/app/services/api/permission.service';
|
import { PermissionService } from 'src/app/services/api/permission.service';
|
||||||
import { UserService } from 'src/app/services/api/user.service';
|
import { UserService } from 'src/app/services/api/user.service';
|
||||||
import { UtilService } from 'src/app/util/util.service';
|
import { UtilService } from 'src/app/util/util.service';
|
||||||
import { LoginControl } from '../../../models/forms/login.model';
|
import { LoginControl } from '../../../models/forms/login.control';
|
||||||
import { UserPassModel } from '../../../models/forms/userpass';
|
import { UserPassModel } from '../../../models/forms/userpass.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
|
styleUrls: ['./login.component.scss'],
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent implements OnInit {
|
||||||
private readonly logger = console;
|
private readonly logger = console;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
mat-form-field {
|
||||||
|
max-width: 40rem;
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
@@ -3,15 +3,16 @@ import { Router } from '@angular/router';
|
|||||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||||
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
|
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types';
|
import { HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { UserPassModel } from 'src/app/models/forms/userpass';
|
import { UserPassModel } from 'src/app/models/forms/userpass.model';
|
||||||
import { SnackBarType } from 'src/app/models/snack-bar-type';
|
import { SnackBarType } from 'src/app/models/snack-bar-type';
|
||||||
import { PermissionService } from 'src/app/services/api/permission.service';
|
import { PermissionService } from 'src/app/services/api/permission.service';
|
||||||
import { UserService } from 'src/app/services/api/user.service';
|
import { UserService } from 'src/app/services/api/user.service';
|
||||||
import { UtilService } from 'src/app/util/util.service';
|
import { UtilService } from 'src/app/util/util.service';
|
||||||
import { RegisterControl } from '../../../models/forms/register.model';
|
import { RegisterControl } from '../../../models/forms/register.control';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './register.component.html',
|
templateUrl: './register.component.html',
|
||||||
|
styleUrls: ['./register.component.scss'],
|
||||||
})
|
})
|
||||||
export class RegisterComponent implements OnInit {
|
export class RegisterComponent implements OnInit {
|
||||||
private readonly logger = console;
|
private readonly logger = console;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
import { CopyFieldModule } from 'src/app/components/copyfield/copyfield.module';
|
import { CopyFieldModule } from 'src/app/components/copyfield/copyfield.module';
|
||||||
import { ViewComponent } from './view.component';
|
import { ViewComponent } from './view.component';
|
||||||
import { ViewRoutingModule } from './view.routing.module';
|
import { ViewRoutingModule } from './view.routing.module';
|
||||||
|
const a = Fuse;
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [ViewComponent],
|
declarations: [ViewComponent],
|
||||||
imports: [CommonModule, CopyFieldModule, ViewRoutingModule, MatButtonModule],
|
imports: [CommonModule, CopyFieldModule, ViewRoutingModule, MatButtonModule],
|
||||||
|
|||||||
25
frontend/src/app/services/api/roles.service.ts
Normal file
25
frontend/src/app/services/api/roles.service.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RoleListResponse } 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 { ApiService } from './api.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class RolesService {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
public async getRoles(): AsyncFailable<ERole[]> {
|
||||||
|
const result = await this.apiService.get(
|
||||||
|
RoleListResponse,
|
||||||
|
'/api/roles/list'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (HasFailed(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.roles;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
|
UserCreateRequest,
|
||||||
|
UserCreateResponse,
|
||||||
|
UserInfoRequest,
|
||||||
|
UserInfoResponse,
|
||||||
UserListRequest,
|
UserListRequest,
|
||||||
UserListResponse
|
UserListResponse,
|
||||||
|
UserUpdateRequest,
|
||||||
|
UserUpdateResponse
|
||||||
} from 'picsur-shared/dist/dto/api/usermanage.dto';
|
} from 'picsur-shared/dist/dto/api/usermanage.dto';
|
||||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||||
|
import { FullUserModel } from 'src/app/models/forms/fulluser.model';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './api.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -13,6 +20,21 @@ import { ApiService } from './api.service';
|
|||||||
export class UserManageService {
|
export class UserManageService {
|
||||||
constructor(private apiService: ApiService) {}
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
public async getUser(username: string): AsyncFailable<EUser> {
|
||||||
|
const body = {
|
||||||
|
username,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.apiService.post(
|
||||||
|
UserInfoRequest,
|
||||||
|
UserInfoResponse,
|
||||||
|
'api/user/info',
|
||||||
|
body
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async getUsers(count: number, page: number): AsyncFailable<EUser[]> {
|
public async getUsers(count: number, page: number): AsyncFailable<EUser[]> {
|
||||||
const body = {
|
const body = {
|
||||||
count,
|
count,
|
||||||
@@ -32,4 +54,26 @@ export class UserManageService {
|
|||||||
|
|
||||||
return result.users;
|
return result.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async createUser(user: FullUserModel): AsyncFailable<EUser> {
|
||||||
|
const result = await this.apiService.post(
|
||||||
|
UserCreateRequest,
|
||||||
|
UserCreateResponse,
|
||||||
|
'/api/user/create',
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateUser(user: FullUserModel): AsyncFailable<EUser> {
|
||||||
|
const result = await this.apiService.post(
|
||||||
|
UserUpdateRequest,
|
||||||
|
UserUpdateResponse,
|
||||||
|
'/api/user/update',
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,3 @@ html {
|
|||||||
width: initial !important;
|
width: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix small form inputs
|
|
||||||
|
|
||||||
form mat-form-field {
|
|
||||||
width: inherit;
|
|
||||||
max-width: 40rem;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 5px;
|
border-width: 5px;
|
||||||
|
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Easily center content
|
// Easily center content
|
||||||
@@ -63,6 +65,11 @@
|
|||||||
|
|
||||||
// Anim
|
// Anim
|
||||||
|
|
||||||
.fullanimate, .fullanimate * {
|
.container, .row > div {
|
||||||
|
transition: ease-in-out all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullanimate,
|
||||||
|
.fullanimate * {
|
||||||
transition: ease-in-out all 0.2s !important;
|
transition: ease-in-out all 0.2s !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import {
|
|||||||
IsArray,
|
IsArray,
|
||||||
IsDefined,
|
IsDefined,
|
||||||
IsInt,
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Min,
|
Min,
|
||||||
ValidateNested
|
ValidateNested
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { EUser, NamePassUser, UsernameUser } from '../../entities/user.entity';
|
import { EUser, NamePassUser, UsernameUser } from '../../entities/user.entity';
|
||||||
|
import { IsPlainTextPwd } from '../../validators/user.validators';
|
||||||
import { Roles } from '../roles.dto';
|
import { Roles } from '../roles.dto';
|
||||||
|
|
||||||
// UserList
|
// UserList
|
||||||
@@ -42,7 +44,12 @@ export class UserListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserCreate
|
// UserCreate
|
||||||
export class UserCreateRequest extends NamePassUser {}
|
export class UserCreateRequest extends NamePassUser {
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
roles?: Roles;
|
||||||
|
}
|
||||||
export class UserCreateResponse extends EUser {}
|
export class UserCreateResponse extends EUser {}
|
||||||
|
|
||||||
// UserDelete
|
// UserDelete
|
||||||
@@ -54,11 +61,15 @@ export class UserInfoRequest extends UsernameUser {}
|
|||||||
export class UserInfoResponse extends EUser {}
|
export class UserInfoResponse extends EUser {}
|
||||||
|
|
||||||
// UserUpdateRoles
|
// UserUpdateRoles
|
||||||
export class UserUpdateRolesRequest extends UsernameUser {
|
export class UserUpdateRequest extends UsernameUser {
|
||||||
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsDefined()
|
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
roles: Roles;
|
roles?: Roles;
|
||||||
|
|
||||||
|
@IsPlainTextPwd()
|
||||||
|
@IsOptional()
|
||||||
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserUpdateRolesResponse extends EUser {}
|
export class UserUpdateResponse extends EUser {}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Permission, Permissions, PermissionsList } from './permissions';
|
|||||||
|
|
||||||
// Config
|
// Config
|
||||||
|
|
||||||
// These roles can never be removed from a user
|
// These roles can never be removed or added to a user.
|
||||||
const PermanentRolesTuple = tuple('guest', 'user');
|
const PermanentRolesTuple = tuple('guest', 'user');
|
||||||
// These reles can never be modified
|
// These roles can never be modified
|
||||||
const ImmuteableRolesTuple = tuple('admin');
|
const ImmuteableRolesTuple = tuple('admin');
|
||||||
// These roles can never be removed from the server
|
// These roles can never be removed from the server
|
||||||
const SystemRolesTuple = tuple(...PermanentRolesTuple, ...ImmuteableRolesTuple);
|
const SystemRolesTuple = tuple(...PermanentRolesTuple, ...ImmuteableRolesTuple);
|
||||||
|
|||||||
17
yarn.lock
17
yarn.lock
@@ -4252,6 +4252,11 @@ functional-red-black-tree@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||||
|
|
||||||
|
fuse.js@^6.5.3:
|
||||||
|
version "6.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.5.3.tgz#7446c0acbc4ab0ab36fa602e97499bdb69452b93"
|
||||||
|
integrity sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg==
|
||||||
|
|
||||||
gauge@^3.0.0:
|
gauge@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
|
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
|
||||||
@@ -5494,7 +5499,17 @@ minimatch@^3.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@1.2.5, minimist@^1.2.0, minimist@^1.2.6, "minimist@npm:minimist-lite":
|
minimist@1.2.5:
|
||||||
|
version "1.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
|
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
|
||||||
|
minimist@^1.2.0, minimist@^1.2.6:
|
||||||
|
version "1.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
|
"minimist@npm:minimist-lite":
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/minimist-lite/-/minimist-lite-2.2.1.tgz#abb71db2c9b454d7cf4496868c03e9802de9934d"
|
resolved "https://registry.yarnpkg.com/minimist-lite/-/minimist-lite-2.2.1.tgz#abb71db2c9b454d7cf4496868c03e9802de9934d"
|
||||||
integrity sha512-RSrWIRWGYoM2TDe102s7aIyeSipXMIXKb1fSHYx1tAbxAV0z4g2xR6ra3oPzkTqFb0EIUz1H3A/qvYYeDd+/qQ==
|
integrity sha512-RSrWIRWGYoM2TDe102s7aIyeSipXMIXKb1fSHYx1tAbxAV0z4g2xR6ra3oPzkTqFb0EIUz1H3A/qvYYeDd+/qQ==
|
||||||
|
|||||||
Reference in New Issue
Block a user