Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/auth/social.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { I18nService } from 'nestjs-i18n';
import { CacheService } from 'omniboxd/common/cache.service';
import { NamespacesService } from 'omniboxd/namespaces/namespaces.service';
import { isNameBlocked } from 'omniboxd/utils/blocked-names';
import { filterEmoji } from 'omniboxd/utils/emoji';

export interface UserSocialState {
type: string;
Expand Down Expand Up @@ -105,10 +106,12 @@ export class SocialService {
nickname: string,
manager: EntityManager,
): Promise<string> {
let username = nickname;
// Filter out emoji characters from nickname
const filteredNickname = filterEmoji(nickname);
let username = filteredNickname;

if (username.length > this.maxUsernameLength) {
username = nickname.slice(0, this.maxUsernameLength);
username = filteredNickname.slice(0, this.maxUsernameLength);
}
if (username.length >= this.minUsernameLength) {
const ok = await this.isUsernameValid(username, manager);
Expand All @@ -117,7 +120,7 @@ export class SocialService {
}
}

username = nickname.slice(0, this.maxUsernameLength - 5);
username = filteredNickname.slice(0, this.maxUsernameLength - 5);
for (let i = 0; i < 5; i++) {
const curUsername = username + this.generateSuffix();
const ok = await this.isUsernameValid(curUsername, manager);
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en/user.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"errors": {
"accountAlreadyExists": "Account already exists",
"usernameAlreadyExists": "Username already exists",
"invalidEmailFormat": "Invalid email format",
"pleaseVerifyEmail": "Please verify your email first",
"incorrectVerificationCode": "Incorrect verification code",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/zh/user.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"errors": {
"accountAlreadyExists": "账户已存在",
"usernameAlreadyExists": "用户名已存在",
"invalidEmailFormat": "邮箱格式无效",
"pleaseVerifyEmail": "请先验证您的邮箱",
"incorrectVerificationCode": "邮箱验证码不正确",
Expand Down
5 changes: 4 additions & 1 deletion src/namespaces/namespaces.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,10 @@ describe('NamespacesController (e2e)', () => {
.send(specialNameWorkspace)
.expect(HttpStatus.CREATED);

expect(response.body.name).toBe(specialNameWorkspace.name);
// Emojis should be filtered out
expect(response.body.name).toBe(
'Test Workspace with 特殊字符 and émojis',
);

// Clean up
await client
Expand Down
21 changes: 15 additions & 6 deletions src/namespaces/namespaces.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ResourceMetaDto } from 'omniboxd/resources/dto/resource-meta.dto';
import { AppException } from 'omniboxd/common/exceptions/app.exception';
import { I18nService } from 'nestjs-i18n';
import { isNameBlocked } from 'omniboxd/utils/blocked-names';
import { filterEmoji } from 'omniboxd/utils/emoji';

@Injectable()
export class NamespacesService {
Expand Down Expand Up @@ -174,9 +175,12 @@ export class NamespacesService {
name: string,
manager: EntityManager,
): Promise<Namespace> {
// Filter emoji from namespace name
const filteredName = filterEmoji(name);

if (
isNameBlocked(name) ||
(await manager.countBy(Namespace, { name })) > 0
isNameBlocked(filteredName) ||
(await manager.countBy(Namespace, { name: filteredName })) > 0
) {
const message = this.i18n.t('namespace.errors.namespaceConflict');
throw new AppException(
Expand All @@ -185,7 +189,9 @@ export class NamespacesService {
HttpStatus.CONFLICT,
);
}
const namespace = await manager.save(manager.create(Namespace, { name }));
const namespace = await manager.save(
manager.create(Namespace, { name: filteredName }),
);
const publicRoot = await this.resourcesService.createResource(
{
namespaceId: namespace.id,
Expand Down Expand Up @@ -221,9 +227,12 @@ export class NamespacesService {
: this.namespaceRepository;
const namespace = await this.getNamespace(id, manager);
if (updateDto.name && updateDto.name !== namespace.name) {
// Filter emoji from namespace name
const filteredName = filterEmoji(updateDto.name);

if (
isNameBlocked(updateDto.name) ||
(await repo.countBy({ name: updateDto.name })) > 0
isNameBlocked(filteredName) ||
(await repo.countBy({ name: filteredName })) > 0
) {
const message = this.i18n.t('namespace.errors.namespaceConflict');
throw new AppException(
Expand All @@ -232,7 +241,7 @@ export class NamespacesService {
HttpStatus.CONFLICT,
);
}
namespace.name = updateDto.name;
namespace.name = filteredName;
}
return await repo.update(id, namespace);
}
Expand Down
23 changes: 13 additions & 10 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export class UserController {
}

@Post('email/validate')
async validateEmail(@Req() req, @Body('email') email: string) {
return await this.userService.validateEmail(req.user.id, email);
async validateEmail(@UserId() userId: string, @Body('email') email: string) {
return await this.userService.validateEmail(userId, email);
}

@Patch(':id')
Expand All @@ -54,29 +54,32 @@ export class UserController {
}

@Post('option')
async createOption(@Req() req, @Body() createOptionDto: CreateUserOptionDto) {
async createOption(
@UserId() userId: string,
@Body() createOptionDto: CreateUserOptionDto,
) {
const option = await this.userService.getOption(
req.user.id,
userId,
createOptionDto.name,
);
if (option && option.name) {
return await this.userService.updateOption(
req.user.id,
userId,
option.name,
createOptionDto.value,
);
}
return await this.userService.createOption(req.user.id, createOptionDto);
return await this.userService.createOption(userId, createOptionDto);
}

@Get('option/list')
async listOption(@Req() req) {
return await this.userService.listOption(req.user.id);
async listOption(@UserId() userId: string) {
return await this.userService.listOption(userId);
}

@Get('option/:name')
async getOption(@Req() req, @Param('name') name: string) {
return await this.userService.getOption(req.user.id, name);
async getOption(@UserId() userId: string, @Param('name') name: string) {
return await this.userService.getOption(userId, name);
}

@Get('binding/list')
Expand Down
3 changes: 2 additions & 1 deletion src/user/user.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ describe('UserController (e2e)', () => {
});

it('should fail with existing email', async () => {
// Use secondClient's email to test email already in use by another user
await client
.post('/api/v1/user/email/validate')
.send({ email: client.user.email })
.send({ email: secondClient.user.email })
.expect(HttpStatus.BAD_REQUEST);
});

Expand Down
33 changes: 31 additions & 2 deletions src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { isNameBlocked } from 'omniboxd/utils/blocked-names';
import { AppException } from 'omniboxd/common/exceptions/app.exception';
import { I18nService } from 'nestjs-i18n';
import { CacheService } from 'omniboxd/common/cache.service';
import { filterEmoji } from 'omniboxd/utils/emoji';

interface EmailVerificationState {
code: string;
Expand Down Expand Up @@ -84,6 +85,11 @@ export class UserService {
}

async create(account: CreateUserDto, manager?: EntityManager) {
// Filter emoji from username if provided
if (account.username) {
account.username = filterEmoji(account.username);
}

if (account.username && isNameBlocked(account.username)) {
const message = this.i18n.t('user.errors.accountAlreadyExists');
throw new AppException(
Expand Down Expand Up @@ -202,10 +208,14 @@ export class UserService {
) {
const repo = manager ? manager.getRepository(User) : this.userRepository;
const hash = await bcrypt.hash(Math.random().toString(36), 10);

// Filter emoji from username
const username = filterEmoji(userData.username);

const newUser = repo.create({
password: hash,
email: userData.email,
username: userData.username,
username: username,
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -304,7 +314,7 @@ export class UserService {
}

const userExists = await this.findByEmail(email);
if (userExists) {
if (userExists && userExists.id !== userId) {
const message = this.i18n.t('user.errors.emailAlreadyInUse');
throw new AppException(
message,
Expand Down Expand Up @@ -333,6 +343,11 @@ export class UserService {
}

async update(id: string, account: UpdateUserDto) {
// Filter emoji from username if provided
if (account.username) {
account.username = filterEmoji(account.username);
}

if (account.username && !account.username.trim().length) {
const message = this.i18n.t('user.errors.userCannotbeEmptyStr');
throw new AppException(
Expand All @@ -354,6 +369,20 @@ export class UserService {
existUser.password = await bcrypt.hash(account.password, 10);
}
if (account.username && existUser.username !== account.username) {
// Check if the new username is already taken by another user
const duplicateUser = await this.userRepository.findOne({
where: { username: account.username },
});

if (duplicateUser && duplicateUser.id !== id) {
const message = this.i18n.t('user.errors.usernameAlreadyExists');
throw new AppException(
message,
'USERNAME_ALREADY_EXISTS',
HttpStatus.CONFLICT,
);
}

existUser.username = account.username;
}
if (account.email && existUser.email !== account.email) {
Expand Down
31 changes: 31 additions & 0 deletions src/utils/emoji.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export function isEmoji(char: string) {
const code = char.codePointAt(0);

if (!code) {
return false;
}

return (
(code >= 0x1f600 && code <= 0x1f64f) || // Emoticons
(code >= 0x1f300 && code <= 0x1f5ff) || // Misc Symbols and Pictographs
(code >= 0x1f680 && code <= 0x1f6ff) || // Transport and Map Symbols
(code >= 0x1f700 && code <= 0x1f77f) || // Alchemical Symbols
(code >= 0x1f780 && code <= 0x1f7ff) || // Geometric Shapes Extended
(code >= 0x1f800 && code <= 0x1f8ff) || // Supplemental Arrows-C
(code >= 0x1f900 && code <= 0x1f9ff) || // Supplemental Symbols and Pictographs
(code >= 0x1fa00 && code <= 0x1fa6f) || // Chess Symbols
(code >= 0x1fa70 && code <= 0x1faff) || // Symbols and Pictographs Extended-A
(code >= 0x2600 && code <= 0x26ff) || // Miscellaneous Symbols
(code >= 0x2700 && code <= 0x27bf) || // Dingbats
(code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors
(code >= 0x1f000 && code <= 0x1f02f) || // Mahjong Tiles
(code >= 0x1f0a0 && code <= 0x1f0ff) // Playing Cards
);
}

export function filterEmoji(data: string) {
return Array.from(data)
.filter((char) => !isEmoji(char))
.join('')
.trim();
}