Skip to content

Commit

Permalink
[hexlet-rus#537] Implement admin section
Browse files Browse the repository at this point in the history
  • Loading branch information
SidorovVladimir committed Jan 19, 2025
1 parent 85e73d4 commit ba1e494
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 28 deletions.
61 changes: 60 additions & 1 deletion backend/src/admins/admins.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
/* eslint-disable no-useless-constructor */

import {
Body,
Controller,
DefaultValuePipe,
Get,
Param,
ParseIntPipe,
Post,
Query,
Redirect,
Render,
} from '@nestjs/common';
import { Snippet } from '../entities/snippet.entity';
import { User } from '../entities/user.entity';
import { AdminsService } from './admins.service';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('admin')
export class AdminsController {
Expand All @@ -22,6 +28,59 @@ export class AdminsController {
) {
const take = 10;
const users: User[] = await this.adminsService.findAllUsers(page, take);
return { users, currentPage: page };
return { users, currentPage: page, activeTemplate: 'users' };
}

/* eslint-disable class-methods-use-this */
@Get('users/create')
@Render('create-user.pug')
/* eslint-disable @typescript-eslint/no-empty-function */
create() {}

@Post('users/create')
@Redirect('/api/admin/users')
createUser(@Body() dto: any): Promise<any> {
return this.adminsService.create(dto);
}

@Get('users/delete/:id')
@Redirect('/api/admin/users')
deleteUser(@Param('id', ParseIntPipe) userId: number): Promise<void> {
return this.adminsService.deleteUser(userId);
}

@Get('users/edit/:id')
@Render('edit-user.pug')
async editUser(@Param('id', ParseIntPipe) userId: number) {
const user: User = await this.adminsService.findOneUser(userId);
return { user };
}

@Post('users/edit/:id')
@Redirect('/api/admin/users')
updateUser(
@Param('id', ParseIntPipe) userId: number,
@Body() dto: UpdateUserDto,
): Promise<any> {
return this.adminsService.updateUser(userId, dto);
}

@Get('snippets')
@Render('snippets.pug')
async findAllSnippets(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page = 1,
): Promise<any> {
const take = 10;
const snippets: Snippet[] = await this.adminsService.findAllSnippets(
page,
take,
);
return { snippets, currentPage: page, activeTemplate: 'snippets' };
}

@Get('snippets/delete/:id')
@Redirect('/api/admin/snippets')
deleteSnippet(@Param('id', ParseIntPipe) snippetId: number): Promise<void> {
return this.adminsService.deleteSnippet(snippetId);
}
}
9 changes: 6 additions & 3 deletions backend/src/admins/admins.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
import { AdminsController } from './admins.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminsService } from './admins.service';
import { AdminsController } from './admins.controller';
import { User } from '../entities/user.entity';
import { Snippet } from '../entities/snippet.entity';
import { UserSettings } from '../entities/user-settings.entity';

@Module({
imports: [UsersModule],
imports: [TypeOrmModule.forFeature([User, Snippet, UserSettings])],
providers: [AdminsService],
controllers: [AdminsController],
})
Expand Down
69 changes: 65 additions & 4 deletions backend/src/admins/admins.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,75 @@
/* eslint-disable no-useless-constructor */

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { UsersService } from '../users/users.service';
import { Snippet } from '../entities/snippet.entity';
import { UserSettings } from '../entities/user-settings.entity';

@Injectable()
export class AdminsService {
constructor(private readonly usersService: UsersService) {}
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(Snippet)
private snippetsRepository: Repository<Snippet>,
@InjectRepository(UserSettings)
private userSettingsRepository: Repository<UserSettings>,
) {}

findAllUsers(page: number, take: number): Promise<User[]> {
return this.usersService.findAllUsers(page, take);
async findAllUsers(page: number, take: number): Promise<User[]> {
return this.usersRepository.find({
skip: (page - 1) * take,
take,
});
}

async deleteUser(id: number): Promise<void> {
await this.snippetsRepository.delete({ userId: id });
await this.userSettingsRepository.delete({ userId: id });
await this.usersRepository.delete({ id });
}

async findOneUser(id: number): Promise<User> {
return this.usersRepository.findOneBy({ id });
}

async updateUser(id: number, dto: any): Promise<User> {
const user = await this.findOneUser(id);

user.username = dto.username;
user.email = dto.email;
user.isAdmin = dto.isAdmin === 'on';
await this.usersRepository.save(user);
return user;
}

async create(createUserDto: any): Promise<User> {
const user = new User();
user.username = createUserDto.username;
user.email = createUserDto.email.toLowerCase();
user.password = createUserDto.password;
user.isAdmin = createUserDto.isAdmin === 'on';
const newUser = await this.usersRepository.save(user);
const userSettings = await this.userSettingsRepository.create({
userId: newUser.id,
theme: 'system',
language: 'ru',
avatar_base64: null,
});
await this.userSettingsRepository.save(userSettings);
return newUser;
}

async findAllSnippets(page: number, take: number): Promise<Snippet[]> {
return this.snippetsRepository.find({
skip: (page - 1) * take,
take,
});
}

async deleteSnippet(id: number): Promise<void> {
await this.snippetsRepository.delete(id);
}
}
52 changes: 52 additions & 0 deletions backend/src/admins/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
IsEmail,
// IsNotEmpty,
IsOptional,
IsString,
Length,
Matches,
// Validate,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
// import { CheckUsername } from '../../users/validation/check-username';
// import { CheckEmail } from '../../users/validation/check-email';

export class UpdateUserDto {
@ApiProperty({
minLength: 3,
maxLength: 20,
description: 'Must be unique!',
example: 'UpdatedJohnDoe',
pattern: '/[A-Za-z]/',
required: false,
})
@IsOptional()
@Length(3, 20)
@IsString()
@Matches(/^[\w\S]*$/)
// @Validate(CheckUsername, {
// message: 'usernameIsUsed',
// })
username?: string;

@ApiProperty({
description: 'Must be unique!',
example: '[email protected]',
required: false,
})
@IsOptional()
@IsString()
@IsEmail()
// @Validate(CheckEmail, {
// message: 'emailIsUsed',
// })
email?: string;

@ApiProperty({
description: 'Administrator privileges',
example: 'on | off',
})
@IsOptional()
@IsString()
isAdmin?: string;
}
34 changes: 34 additions & 0 deletions backend/src/admins/views/create-user.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
doctype html
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title Create User
link(href=' https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css' rel='stylesheet')
body
nav.navbar.navbar-expand-lg
.container-fluid
.collapse.navbar-collapse#navbarSupportedContent
ul.navbar-nav.me-auto.mb-2.mb-lg-0
li.nav-item
a.nav-link(href='/api/admin/users') Назад
.container-fluid
h1.text-center.font-weight-bold.pb-3
| Создание пользователя
.d-flex.align-items-center.justify-content-center
form(action='/api/admin/users/create', method="post")
div.mb-3
label.form-label(for="username") Имя
input.form-control(id="username" name="username" type="text")
div.mb-3
label.form-label(for="email") Емейл
input.form-control(id="email" name="email" type="email")
div.mb-3
label.form-label(for="password") Пароль
input.form-control(id="password" name="password" type="text")
div.mb-3
label.form-label(for="status") Роль
select.form-select(id="status" name="isAdmin")
option(value="off" selected) Пользователь
option(value="on") Администратор
button.btn.btn-primary(type="submit") Создать
31 changes: 31 additions & 0 deletions backend/src/admins/views/edit-user.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
doctype html
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title Edit User
link(href=' https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css' rel='stylesheet')
body
nav.navbar.navbar-expand-lg
.container-fluid
.collapse.navbar-collapse#navbarSupportedContent
ul.navbar-nav.me-auto.mb-2.mb-lg-0
li.nav-item
a.nav-link(href='/api/admin/users') Назад
.container-fluid
h1.text-center.font-weight-bold.pb-3
| Редактирование профиля
.d-flex.align-items-center.justify-content-center
form(action=`/api/admin/users/edit/${user.id}`, method="post")
div.mb-3
label.form-label(for="username") Имя
input.form-control(id="username" name="username" type="text" value=user.username)
div.mb-3
label.form-label(for="email") Емейл
input.form-control(id="email" name="email" type="email" value=user.email)
div.mb-3
label.form-label(for="status") Роль
select.form-select(id="status" name="isAdmin")
option(value="off" selected=user.isAdmin) Пользователь
option(value="on" selected=user.isAdmin) Администратор
button.btn.btn-primary(type="submit") Сохранить
52 changes: 52 additions & 0 deletions backend/src/admins/views/snippets.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
doctype html
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
title Snippets List
link(href=' https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css' rel='stylesheet')
body
//- script(src='https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js')
nav.navbar.navbar-expand-lg
.container-fluid
.collapse.navbar-collapse#navbarSupportedContent
ul.navbar-nav.me-auto.mb-2.mb-lg-0
li.nav-item
a.nav-link(href='http://localhost:3000/') На главную
li.nav-item
if activeTemplate === 'snippets'
a.nav-link(href='/api/admin/users') Пользователи
if activeTemplate === 'users'
a.nav-link(href='/api/admin/snippets') Сниппеты
//- a.btn.btn-primary(href='users/create' role='button') +
.container
.d-flex.align-items-center.justify-content-center
h1.text-center.font-weight-bold.pb-3
| Сниппеты
table.table
thead
tr
th ID
th Имя
th Язык
//- th Админ
th Действие
tbody
each snippet in snippets
tr.align-middle
td= snippet.id
td= snippet.name
td= snippet.language
//- td= user.isAdmin ? 'Да' : 'Нет'
td
//- div
//- a.link-dark.link-offset-2.link-underline-opacity-25.link-underline-opacity-100-hover(href=`users/edit/${user.id}`) Редактировать
div
a.link-dark.link-offset-2.link-underline-opacity-25.link-underline-opacity-100-hover(href=`snippets/delete/${snippet.id}`) Удалить
.container
.d-flex.align-items-center.justify-content-center.gap-3
if currentPage > 1
a(href=`snippets?page=${currentPage - 1}`) Предыдущая
span Страница № #{currentPage}
if snippets.length >= 10
a(href=`snippets?page=${currentPage + 1}`) Следующая
Loading

0 comments on commit ba1e494

Please sign in to comment.