Skip to content

Commit

Permalink
Merge pull request #565 from SidorovVladimir/backend_user_settings
Browse files Browse the repository at this point in the history
#501 Backend user settings
  • Loading branch information
dzencot authored Nov 12, 2024
2 parents 1761223 + 18b9e3e commit 6864d96
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 10 deletions.
5 changes: 4 additions & 1 deletion backend/src/entities/snippet.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ export class Snippet {
language: string;

@ManyToOne('User', 'snippets')
@JoinColumn()
@JoinColumn({ name: 'userId' })
user: User;

@Column({ name: 'userId' })
userId: number;

@CreateDateColumn()
created_at: string;

Expand Down
35 changes: 35 additions & 0 deletions backend/src/entities/user-settings.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import type { User } from './user.entity';

@Entity('user_settings')
export class UserSettings {
@PrimaryGeneratedColumn()
settings_id: number;

@Column('text', { default: 'ru' })
language: string;

@Column('text', { default: 'system' })
theme: string;

@CreateDateColumn()
created_at: string;

@UpdateDateColumn()
updated_at: string;

@OneToOne('User', 'user_settings')
@JoinColumn({ name: 'userId' })
user: User;

@Column({ name: 'userId' })
userId: number;
}
7 changes: 7 additions & 0 deletions backend/src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import {
CreateDateColumn,
Entity,
Index,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { encrypt } from '../users/secure/encrypt';
import type { Snippet } from './snippet.entity';
import type { UserSettings } from './user-settings.entity';

@Entity('users')
export class User {
Expand All @@ -31,6 +34,10 @@ export class User {
@OneToMany('Snippet', 'user')
snippets: Snippet[];

@OneToOne('UserSettings', 'user')
@JoinColumn({ name: 'id' })
userSettings: UserSettings;

@Column({ nullable: true })
recover_hash: string;

Expand Down
61 changes: 61 additions & 0 deletions backend/src/migrations/1730013613468-backend_user_settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { MigrationInterface, QueryRunner, Table } from 'typeorm';

export class BackendUserSettings1730013613468 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user_settings',
columns: [
{
name: 'settings_id',
type: 'integer',
isGenerated: true,
generatedIdentity: 'ALWAYS',
generationStrategy: 'increment',
isNullable: false,
isPrimary: true,
},
{
name: 'language',
type: 'varchar(50)',
isNullable: false,
},

{
name: 'theme',
type: 'varchar(50)',
isNullable: false,
},
{
name: 'created_at',
type: 'timestamp',
isNullable: false,
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updated_at',
type: 'timestamp',
isNullable: false,
default: 'CURRENT_TIMESTAMP',
},
{
name: 'userId',
type: 'int',
},
],
foreignKeys: [
{
name: 'userId',
columnNames: ['userId'],
referencedTableName: 'users',
referencedColumnNames: ['id'],
},
],
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "user_settings"`);
}
}
3 changes: 2 additions & 1 deletion backend/src/snippets/snippets.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtService } from '@nestjs/jwt';
import { UserSettings } from '../entities/user-settings.entity';
import { AuthService } from '../auth/auth.service';
import { User } from '../entities/user.entity';
import { Snippet } from '../entities/snippet.entity';
Expand All @@ -10,7 +11,7 @@ import { SnippetSubscriber } from './snippets.subscriber';
import { UsersService } from '../users/users.service';

@Module({
imports: [TypeOrmModule.forFeature([Snippet, User])],
imports: [TypeOrmModule.forFeature([Snippet, User, UserSettings])],
controllers: [SnippetsController],
providers: [
SnippetsService,
Expand Down
12 changes: 12 additions & 0 deletions backend/src/users/dto/update-user-settings.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class UpdateUserSettingsDto {
@ApiProperty()
@IsString()
theme?: string;

@ApiProperty()
@IsString()
language?: string;
}
12 changes: 12 additions & 0 deletions backend/src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { HttpValidationFilter } from './exceptions/validation-exception.filter';
import { AuthService } from '../auth/auth.service';
import { RecoverUserDto } from './dto/recover-user.dto';
import { UpdateUserSettingsDto } from './dto/update-user-settings.dto';

@ApiTags('users')
@Controller('users')
Expand Down Expand Up @@ -133,6 +134,17 @@ export class UsersController {
return this.usersService.update(id, updateUserDto);
}

@Put('settings/:id')
@UseGuards(JwtAuthGuard)
@UseFilters(new HttpValidationFilter())
@ApiCookieAuth('access_token')
updateSettings(
@Param('id', new ParseIntPipe()) id: number,
@Body() updateUserSettingsDto: UpdateUserSettingsDto,
) {
return this.usersService.updateSettings(id, updateUserSettingsDto);
}

@Delete(':id')
@UseGuards(JwtAuthGuard)
@ApiCookieAuth('access_token')
Expand Down
3 changes: 2 additions & 1 deletion backend/src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSettings } from '../entities/user-settings.entity';
import { Snippet } from '../entities/snippet.entity';
import { AuthService } from '../auth/auth.service';
import { User } from '../entities/user.entity';
Expand All @@ -12,7 +13,7 @@ import { CheckUsername } from './validation/check-username';
import { CheckPassword } from './validation/check-password';

@Module({
imports: [TypeOrmModule.forFeature([User, Snippet])],
imports: [TypeOrmModule.forFeature([User, Snippet, UserSettings])],
controllers: [UsersController],
providers: [
UsersService,
Expand Down
73 changes: 67 additions & 6 deletions backend/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from '../entities/user.entity';
import { Snippet } from '../entities/snippet.entity';
import { UserSettings } from '../entities/user-settings.entity';
import { IUser } from './interfaces/users.interface';
import { RecoverUserDto } from './dto/recover-user.dto';
import { cipher, decipher } from './secure/cipher';
import { UpdateUserSettingsDto } from './dto/update-user-settings.dto';

@Injectable()
export class UsersService {
Expand All @@ -25,6 +27,8 @@ export class UsersService {
private snippetsRepository: Repository<Snippet>,
private readonly mailerService: MailerService,
@InjectSentry() private readonly sentryService: SentryService,
@InjectRepository(UserSettings)
private userSettingsRepository: Repository<UserSettings>,
) {}

async findOne(id: number): Promise<User> {
Expand All @@ -43,20 +47,56 @@ export class UsersService {
return this.usersRepository.findOneBy({ email });
}

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

async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
async update(id: number, updateUserDto: UpdateUserDto): Promise<any> {
const { ...data } = updateUserDto;
const currentUser = await this.usersRepository.findOneBy({ id });
const settings = await this.userSettingsRepository.findOneBy({
userId: id,
});
const updatedUser = this.usersRepository.merge(currentUser, data);
await this.usersRepository.save(updatedUser);
return updatedUser;

return {
...updatedUser,
language: settings.language,
theme: settings.theme,
};
}

async updateSettings(
id: number,
updateUserSettingsDto: UpdateUserSettingsDto,
): Promise<any> {
const { ...data } = updateUserSettingsDto;
const currentSettings = await this.userSettingsRepository.findOneBy({
userId: id,
});
const currentUser = await this.usersRepository.findOneBy({ id });
const updateSettings = await this.userSettingsRepository.merge(
currentSettings,
data,
);
await this.userSettingsRepository.save(updateSettings);
return {
...currentUser,
language: updateSettings.language,
theme: updateSettings.theme,
};
}

async recover({ email, frontendUrl }: RecoverUserDto): Promise<void> {
Expand Down Expand Up @@ -133,7 +173,9 @@ export class UsersService {
}

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

findAll(): Promise<User[]> {
Expand All @@ -142,6 +184,9 @@ export class UsersService {

async getData({ id }: IUser): Promise<any> {
const currentUser = await this.usersRepository.findOneBy({ id });
const settingsUser = await this.userSettingsRepository.findOne({
where: { userId: id },
});
const snippets = await this.snippetsRepository.find({
relations: {
user: true,
Expand All @@ -152,6 +197,22 @@ export class UsersService {
},
},
});
return { currentUser, snippets };
if (!settingsUser) {
const createSettingsUser = this.userSettingsRepository.create({
userId: id,
theme: 'system',
language: 'ru',
});
await this.userSettingsRepository.save(createSettingsUser);
}
const settings = await this.userSettingsRepository.findOne({
where: { userId: id },
});
const data = {
...currentUser,
language: settings.language,
theme: settings.theme,
};
return { currentUser: data, snippets };
}
}
2 changes: 1 addition & 1 deletion frontend/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@
},
"saveCode": {
"success": "Сниппет успешно сохранен!",
"error" : "Не удалось сохранить сниппет"
"error": "Не удалось сохранить сниппет"
}
}
}
4 changes: 4 additions & 0 deletions frontend/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export default {
// put - update user info: { name, email, password }
updateUserPath: (id) => [apiPath, 'users', `${id}`].join('/'),

// put - update user settings: { theme, language }
updateUserSettingsPath: (id) =>
[apiPath, 'users', 'settings', `${id}`].join('/'),

resetPassPath: () => [apiPath, 'users', 'recover'].join('/'),

signInPath: () => [apiPath, 'signin'].join('/'),
Expand Down

0 comments on commit 6864d96

Please sign in to comment.