Skip to content

Commit 765fce8

Browse files
authored
Merge pull request #26 from OpenNBS/feature/username-edit
Feature/username edit
2 parents cbc9206 + b3e55b3 commit 765fce8

File tree

8 files changed

+286
-26
lines changed

8 files changed

+286
-26
lines changed

server/src/auth/auth.service.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('AuthService', () => {
5151
},
5252
{
5353
provide: 'COOKIE_EXPIRES_IN',
54-
useValue: '1d',
54+
useValue: '3600',
5555
},
5656
{
5757
provide: 'JWT_SECRET',
@@ -373,15 +373,15 @@ describe('AuthService', () => {
373373

374374
expect(res.cookie).toHaveBeenCalledWith('token', 'access-token', {
375375
domain: '.test.com',
376-
maxAge: 1,
376+
maxAge: 3600000,
377377
});
378378

379379
expect(res.cookie).toHaveBeenCalledWith(
380380
'refresh_token',
381381
'refresh-token',
382382
{
383383
domain: '.test.com',
384-
maxAge: 1,
384+
maxAge: 3600000,
385385
},
386386
);
387387

server/src/user/user.controller.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Controller, Get, Inject, Query } from '@nestjs/common';
1+
import { Body, Controller, Get, Inject, Patch, Query } from '@nestjs/common';
22
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
33
import { PageQueryDTO } from '@shared/validation/common/dto/PageQuery.dto';
44
import { GetUser } from '@shared/validation/user/dto/GetUser.dto';
@@ -7,6 +7,7 @@ import { GetRequestToken, validateUser } from '@server/GetRequestUser';
77

88
import { UserDocument } from './entity/user.entity';
99
import { UserService } from './user.service';
10+
import { UpdateUsernameDto } from '@shared/validation/user/dto/UpdateUsername.dto';
1011

1112
@Controller('user')
1213
export class UserController {
@@ -37,4 +38,16 @@ export class UserController {
3738
user = validateUser(user);
3839
return await this.userService.getSelfUserData(user);
3940
}
41+
42+
@Patch('username')
43+
@ApiTags('user')
44+
@ApiBearerAuth()
45+
@ApiOperation({ summary: 'Update the username' })
46+
async updateUsername(
47+
@GetRequestToken() user: UserDocument | null,
48+
@Body() body: UpdateUsernameDto,
49+
) {
50+
user = validateUser(user);
51+
return await this.userService.updateUsername(user, body);
52+
}
4053
}

server/src/user/user.service.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,75 @@ describe('UserService', () => {
287287
expect(service.usernameExists).toHaveBeenCalledWith(baseUsername);
288288
});
289289
});
290+
291+
describe('normalizeUsername', () => {
292+
it('should normalize a username', () => {
293+
const inputUsername = 'tést user';
294+
const normalizedUsername = 'test_user';
295+
296+
const result = (service as any).normalizeUsername(inputUsername);
297+
298+
expect(result).toBe(normalizedUsername);
299+
});
300+
301+
it('should remove special characters from a username', () => {
302+
const inputUsername = '静_かな';
303+
const normalizedUsername = '_';
304+
305+
const result = (service as any).normalizeUsername(inputUsername);
306+
307+
expect(result).toBe(normalizedUsername);
308+
});
309+
310+
it('should replace spaces with underscores in a username', () => {
311+
const inputUsername = 'Имя пользователя';
312+
const normalizedUsername = '_';
313+
314+
const result = (service as any).normalizeUsername(inputUsername);
315+
316+
expect(result).toBe(normalizedUsername);
317+
});
318+
319+
it('should replace spaces with underscores in a username', () => {
320+
const inputUsername = 'Eglė Čepulytė';
321+
const normalizedUsername = 'Egle_Cepulyte';
322+
323+
const result = (service as any).normalizeUsername(inputUsername);
324+
325+
expect(result).toBe(normalizedUsername);
326+
});
327+
});
328+
329+
describe('updateUsername', () => {
330+
it('should update a user username', async () => {
331+
const user = {
332+
username: 'testuser',
333+
save: jest.fn().mockReturnThis(),
334+
} as unknown as UserDocument;
335+
const body = { username: 'newuser' };
336+
337+
jest.spyOn(service, 'usernameExists').mockResolvedValue(false);
338+
339+
const result = await service.updateUsername(user, body);
340+
341+
expect(result).toEqual(user);
342+
expect(user.username).toBe(body.username);
343+
expect(service.usernameExists).toHaveBeenCalledWith(body.username);
344+
});
345+
346+
it('should throw an error if username already exists', async () => {
347+
const user = {
348+
username: 'testuser',
349+
save: jest.fn().mockReturnThis(),
350+
} as unknown as UserDocument;
351+
352+
const body = { username: 'newuser' };
353+
354+
jest.spyOn(service, 'usernameExists').mockResolvedValue(true);
355+
356+
await expect(service.updateUsername(user, body)).rejects.toThrow(
357+
new HttpException('Username already exists', HttpStatus.BAD_REQUEST),
358+
);
359+
});
360+
});
290361
});

server/src/user/user.service.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CreateUser } from '@shared/validation/user/dto/CreateUser.dto';
55
import { GetUser } from '@shared/validation/user/dto/GetUser.dto';
66
import { validate } from 'class-validator';
77
import { Model } from 'mongoose';
8-
8+
import { UpdateUsernameDto } from '@shared/validation/user/dto/UpdateUsername.dto';
99
import { User, UserDocument } from './entity/user.entity';
1010

1111
@Injectable()
@@ -106,14 +106,17 @@ export class UserService {
106106
return !!user;
107107
}
108108

109-
public async generateUsername(inputUsername: string) {
110-
// Normalize username (remove accents, replace spaces with underscores)
111-
const baseUsername = inputUsername
109+
private normalizeUsername = (inputUsername: string) =>
110+
inputUsername
112111
.replace(' ', '_')
113112
.normalize('NFKD')
114113
.replace(/[\u0300-\u036f]/g, '')
115114
.replace(/[^a-zA-Z0-9_]/g, '');
116115

116+
public async generateUsername(inputUsername: string) {
117+
// Normalize username (remove accents, replace spaces with underscores)
118+
const baseUsername = this.normalizeUsername(inputUsername);
119+
117120
let newUsername = baseUsername;
118121
let counter = 1;
119122

@@ -125,4 +128,24 @@ export class UserService {
125128

126129
return newUsername;
127130
}
131+
132+
public async updateUsername(user: UserDocument, body: UpdateUsernameDto) {
133+
let { username } = body;
134+
username = this.normalizeUsername(username);
135+
136+
if (await this.usernameExists(username)) {
137+
throw new HttpException(
138+
'Username already exists',
139+
HttpStatus.BAD_REQUEST,
140+
);
141+
}
142+
143+
if (user.username === username) {
144+
throw new HttpException('Username is the same', HttpStatus.BAD_REQUEST);
145+
}
146+
147+
user.username = username;
148+
149+
return await user.save();
150+
}
128151
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator';
3+
4+
export class UpdateUsernameDto {
5+
@IsString()
6+
@MaxLength(64)
7+
@MinLength(3)
8+
@ApiProperty({
9+
description: 'Username of the user',
10+
example: 'tomast1137',
11+
})
12+
username: string;
13+
}

web/src/lib/axios/ClientAxios.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import axios from 'axios';
2+
import { getTokenLocal } from './token.utils';
3+
export const baseApiURL = process.env.NEXT_PUBLIC_API_URL;
4+
5+
const ClientAxios = axios.create({
6+
baseURL: baseApiURL,
7+
withCredentials: true,
8+
});
9+
10+
// Add a request interceptor to add the token to the request
11+
ClientAxios.interceptors.request.use(
12+
(config) => {
13+
const token = getTokenLocal();
14+
15+
config.headers.authorization = `Bearer ${token}`;
16+
17+
return config;
18+
},
19+
(error) => {
20+
return Promise.reject(error);
21+
},
22+
);
23+
24+
export default ClientAxios;

web/src/modules/shared/components/client/GenericModal.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
55
import { Dialog, Transition } from '@headlessui/react';
66
import { Fragment } from 'react';
77

8-
export default function GenericModal({
8+
interface GenericModalProps {
9+
isOpen: boolean;
10+
setIsOpen?: (isOpen: boolean) => void;
11+
title: string;
12+
children?: React.ReactNode | React.ReactNode[] | string;
13+
}
14+
15+
const GenericModal = ({
916
isOpen,
1017
setIsOpen,
1118
title,
1219
children,
13-
}: {
14-
isOpen: boolean;
15-
setIsOpen?: (isOpen: boolean) => void;
16-
title: string;
17-
children: React.ReactNode;
18-
}) {
20+
}: GenericModalProps) => {
1921
return (
2022
<Transition appear show={isOpen} as={Fragment}>
2123
<Dialog
@@ -80,4 +82,6 @@ export default function GenericModal({
8082
</Dialog>
8183
</Transition>
8284
);
83-
}
85+
};
86+
87+
export default GenericModal;

0 commit comments

Comments
 (0)