Skip to content

Commit 9a546c4

Browse files
committed
Merge branch 'develop' of https://github.com/OpenNBS/NoteBlockWorld into feature/login-by-email
2 parents 57fad57 + e5c6301 commit 9a546c4

File tree

9 files changed

+297
-26
lines changed

9 files changed

+297
-26
lines changed

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ describe('AuthService', () => {
5353
provide: 'FRONTEND_URL',
5454
useValue: 'http://frontend.test.com',
5555
},
56+
{
57+
provide: 'COOKIE_EXPIRES_IN',
58+
useValue: '3600',
59+
},
5660
{
5761
provide: 'JWT_SECRET',
5862
useValue: 'test-jwt-secret',
@@ -373,15 +377,15 @@ describe('AuthService', () => {
373377

374378
expect(res.cookie).toHaveBeenCalledWith('token', 'access-token', {
375379
domain: '.test.com',
376-
maxAge: 3600,
380+
maxAge: 3600000,
377381
});
378382

379383
expect(res.cookie).toHaveBeenCalledWith(
380384
'refresh_token',
381385
'refresh-token',
382386
{
383387
domain: '.test.com',
384-
maxAge: 3600,
388+
maxAge: 3600000,
385389
},
386390
);
387391

server/src/auth/auth.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export class AuthService {
230230

231231
const frontEndURL = this.FRONTEND_URL;
232232
const domain = this.APP_DOMAIN;
233-
const maxAge = parseInt(this.COOKIE_EXPIRES_IN);
233+
const maxAge = parseInt(this.COOKIE_EXPIRES_IN) * 1000;
234234

235235
res.cookie('token', token.access_token, {
236236
domain: domain,

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
@@ -6,7 +6,7 @@ import { GetUser } from '@shared/validation/user/dto/GetUser.dto';
66
import { NewEmailUserDto } from '@shared/validation/user/dto/NewEmailUser.dto';
77
import { validate } from 'class-validator';
88
import { Model } from 'mongoose';
9-
9+
import { UpdateUsernameDto } from '@shared/validation/user/dto/UpdateUsername.dto';
1010
import { User, UserDocument } from './entity/user.entity';
1111

1212
@Injectable()
@@ -155,14 +155,17 @@ export class UserService {
155155
return !!user;
156156
}
157157

158-
public async generateUsername(inputUsername: string) {
159-
// Normalize username (remove accents, replace spaces with underscores)
160-
const baseUsername = inputUsername
158+
private normalizeUsername = (inputUsername: string) =>
159+
inputUsername
161160
.replace(' ', '_')
162161
.normalize('NFKD')
163162
.replace(/[\u0300-\u036f]/g, '')
164163
.replace(/[^a-zA-Z0-9_]/g, '');
165164

165+
public async generateUsername(inputUsername: string) {
166+
// Normalize username (remove accents, replace spaces with underscores)
167+
const baseUsername = this.normalizeUsername(inputUsername);
168+
166169
let newUsername = baseUsername;
167170
let counter = 1;
168171

@@ -174,4 +177,24 @@ export class UserService {
174177

175178
return newUsername;
176179
}
180+
181+
public async updateUsername(user: UserDocument, body: UpdateUsernameDto) {
182+
let { username } = body;
183+
username = this.normalizeUsername(username);
184+
185+
if (await this.usernameExists(username)) {
186+
throw new HttpException(
187+
'Username already exists',
188+
HttpStatus.BAD_REQUEST,
189+
);
190+
}
191+
192+
if (user.username === username) {
193+
throw new HttpException('Username is the same', HttpStatus.BAD_REQUEST);
194+
}
195+
196+
user.username = username;
197+
198+
return await user.save();
199+
}
177200
}
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)