Skip to content

Feature/username edit #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5bc83e6
feat: add popup with licensing info when downloading a song
Bentroen Dec 20, 2024
6e1680f
fix: test ad slot in download popup
Bentroen Dec 20, 2024
4ef57db
fix: random button shade clashing with background + repeated width class
Bentroen Dec 20, 2024
c19164a
fix: require sign in for downloading songs
Bentroen Dec 20, 2024
eeaf02e
fix: song page buttons overflowing page width on mobile
Bentroen Dec 20, 2024
150c6d3
fix: hide 'Open in NBS' button in mobile layout
Bentroen Dec 20, 2024
d437fe0
fix: song only downloading after modal closed
Bentroen Dec 20, 2024
97c3944
fix: song detail rows not showing full text (clamped to single line)
Bentroen Dec 20, 2024
114e304
fix: song only downloading after modal closed (again)
Bentroen Dec 20, 2024
7bc7a75
fix: recent song browser not loading more than 100 songs
Bentroen Dec 20, 2024
76793c0
fix: homepage initial load skipping first page of recent songs
Bentroen Dec 20, 2024
e371b4b
fix: increase recent song page size to 12
Bentroen Dec 20, 2024
50298d6
fix: button can't be a child of button (hydration error)
Bentroen Dec 20, 2024
ccaa1dc
fix: notes that exceed C0 or A8 via pitch changes wrap to the other end
Bentroen Dec 20, 2024
0ab2263
fix: missing authorization when downloading song for editing
Bentroen Dec 20, 2024
9931c2b
fix: long word without space overflowing song details cell
Bentroen Dec 20, 2024
ad199fa
fix: outdated discord links
Bentroen Dec 24, 2024
089f533
fix: auth cookie expiring too early
Bentroen Dec 25, 2024
4cdaf98
feat: add UI for changing username in user menu popup
Bentroen Dec 31, 2024
4e9badb
feat: add username update functionality and normalization logic
tomast1337 Dec 31, 2024
eb96bcd
Merge branch 'develop' of https://github.com/OpenNBS/NoteBlockWorld i…
tomast1337 Dec 31, 2024
6f02dcf
fix: make children prop optional in GenericModal component
tomast1337 Dec 31, 2024
a1a26f0
feat: add EditUsernameModal component for username editing functionality
tomast1337 Dec 31, 2024
cad0637
feat: integrate EditUsernameModal into UserMenu for username editing
tomast1337 Dec 31, 2024
44af478
fix: prevent username update to the same value in UserService
tomast1337 Dec 31, 2024
3f6a059
feat: implement ClientAxios for API requests and update EditUsernameM…
tomast1337 Dec 31, 2024
9b0c83b
Merge branch 'feat/username-change' of https://github.com/OpenNBS/Not…
tomast1337 Dec 31, 2024
e335b26
refactor: remove EditUsernameModal component
tomast1337 Jan 1, 2025
2ec8276
feat: enhance UserMenu to support username editing with validation an…
tomast1337 Jan 1, 2025
1ae51df
fix: update cookie expiration time in tests
tomast1337 Jan 1, 2025
b3e55b3
fix: correct button action to stop username editing in UserMenu
tomast1337 Jan 1, 2025
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Hello! We're glad to have you interested in contributing to Note Block World. This document will guide you through the process of setting up the project and submitting your contributions.

This page is a work-in-progress and will be updated as the project evolves. If you have any questions or need help, feel free to reach out to us on our [Discord server](https://discord.gg/open-note-block-studio-608692895179997252).
This page is a work-in-progress and will be updated as the project evolves. If you have any questions or need help, feel free to reach out to us on our [Discord server](https://discord.gg/note-block-world-608692895179997252).

## Stack

Expand Down
6 changes: 3 additions & 3 deletions server/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('AuthService', () => {
},
{
provide: 'COOKIE_EXPIRES_IN',
useValue: '1d',
useValue: '3600',
},
{
provide: 'JWT_SECRET',
Expand Down Expand Up @@ -373,15 +373,15 @@ describe('AuthService', () => {

expect(res.cookie).toHaveBeenCalledWith('token', 'access-token', {
domain: '.test.com',
maxAge: 1,
maxAge: 3600000,
});

expect(res.cookie).toHaveBeenCalledWith(
'refresh_token',
'refresh-token',
{
domain: '.test.com',
maxAge: 1,
maxAge: 3600000,
},
);

Expand Down
2 changes: 1 addition & 1 deletion server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export class AuthService {

const frontEndURL = this.FRONTEND_URL;
const domain = this.APP_DOMAIN;
const maxAge = parseInt(this.COOKIE_EXPIRES_IN);
const maxAge = parseInt(this.COOKIE_EXPIRES_IN) * 1000;

res.cookie('token', token.access_token, {
domain: domain,
Expand Down
2 changes: 2 additions & 0 deletions server/src/song/song.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export class SongController {
@GetRequestToken() user: UserDocument | null,
@Res() res: Response,
): Promise<void> {
user = validateUser(user);

// TODO: no longer used
res.set({
'Content-Disposition': 'attachment; filename="song.nbs"',
Expand Down
15 changes: 14 additions & 1 deletion server/src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, Inject, Query } from '@nestjs/common';
import { Body, Controller, Get, Inject, Patch, Query } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { PageQueryDTO } from '@shared/validation/common/dto/PageQuery.dto';
import { GetUser } from '@shared/validation/user/dto/GetUser.dto';
Expand All @@ -7,6 +7,7 @@ import { GetRequestToken, validateUser } from '@server/GetRequestUser';

import { UserDocument } from './entity/user.entity';
import { UserService } from './user.service';
import { UpdateUsernameDto } from '@shared/validation/user/dto/UpdateUsername.dto';

@Controller('user')
export class UserController {
Expand Down Expand Up @@ -37,4 +38,16 @@ export class UserController {
user = validateUser(user);
return await this.userService.getSelfUserData(user);
}

@Patch('username')
@ApiTags('user')
@ApiBearerAuth()
@ApiOperation({ summary: 'Update the username' })
async updateUsername(
@GetRequestToken() user: UserDocument | null,
@Body() body: UpdateUsernameDto,
) {
user = validateUser(user);
return await this.userService.updateUsername(user, body);
}
}
71 changes: 71 additions & 0 deletions server/src/user/user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,75 @@ describe('UserService', () => {
expect(service.usernameExists).toHaveBeenCalledWith(baseUsername);
});
});

describe('normalizeUsername', () => {
it('should normalize a username', () => {
const inputUsername = 'tést user';
const normalizedUsername = 'test_user';

const result = (service as any).normalizeUsername(inputUsername);

expect(result).toBe(normalizedUsername);
});

it('should remove special characters from a username', () => {
const inputUsername = '静_かな';
const normalizedUsername = '_';

const result = (service as any).normalizeUsername(inputUsername);

expect(result).toBe(normalizedUsername);
});

it('should replace spaces with underscores in a username', () => {
const inputUsername = 'Имя пользователя';
const normalizedUsername = '_';

const result = (service as any).normalizeUsername(inputUsername);

expect(result).toBe(normalizedUsername);
});

it('should replace spaces with underscores in a username', () => {
const inputUsername = 'Eglė Čepulytė';
const normalizedUsername = 'Egle_Cepulyte';

const result = (service as any).normalizeUsername(inputUsername);

expect(result).toBe(normalizedUsername);
});
});

describe('updateUsername', () => {
it('should update a user username', async () => {
const user = {
username: 'testuser',
save: jest.fn().mockReturnThis(),
} as unknown as UserDocument;
const body = { username: 'newuser' };

jest.spyOn(service, 'usernameExists').mockResolvedValue(false);

const result = await service.updateUsername(user, body);

expect(result).toEqual(user);
expect(user.username).toBe(body.username);
expect(service.usernameExists).toHaveBeenCalledWith(body.username);
});

it('should throw an error if username already exists', async () => {
const user = {
username: 'testuser',
save: jest.fn().mockReturnThis(),
} as unknown as UserDocument;

const body = { username: 'newuser' };

jest.spyOn(service, 'usernameExists').mockResolvedValue(true);

await expect(service.updateUsername(user, body)).rejects.toThrow(
new HttpException('Username already exists', HttpStatus.BAD_REQUEST),
);
});
});
});
31 changes: 27 additions & 4 deletions server/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CreateUser } from '@shared/validation/user/dto/CreateUser.dto';
import { GetUser } from '@shared/validation/user/dto/GetUser.dto';
import { validate } from 'class-validator';
import { Model } from 'mongoose';

import { UpdateUsernameDto } from '@shared/validation/user/dto/UpdateUsername.dto';
import { User, UserDocument } from './entity/user.entity';

@Injectable()
Expand Down Expand Up @@ -106,14 +106,17 @@ export class UserService {
return !!user;
}

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

public async generateUsername(inputUsername: string) {
// Normalize username (remove accents, replace spaces with underscores)
const baseUsername = this.normalizeUsername(inputUsername);

let newUsername = baseUsername;
let counter = 1;

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

return newUsername;
}

public async updateUsername(user: UserDocument, body: UpdateUsernameDto) {
let { username } = body;
username = this.normalizeUsername(username);

if (await this.usernameExists(username)) {
throw new HttpException(
'Username already exists',
HttpStatus.BAD_REQUEST,
);
}

if (user.username === username) {
throw new HttpException('Username is the same', HttpStatus.BAD_REQUEST);
}

user.username = username;

return await user.save();
}
}
17 changes: 15 additions & 2 deletions shared/features/song/obfuscate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,21 @@ export class SongObfuscator {

const resolveKeyAndPitch = (note: Note) => {
const factoredPitch = note.key * 100 + note.pitch;
const key = Math.floor((factoredPitch + 50) / 100);
const pitch = ((factoredPitch + 50) % 100) - 50;

let key, pitch;

if (factoredPitch < 0) {
// Below A0
key = 0;
pitch = factoredPitch;
} else if (factoredPitch >= 87 * 100) {
// Above C8
key = 87;
pitch = factoredPitch - 87 * 100;
} else {
key = Math.floor((factoredPitch + 50) / 100);
pitch = ((factoredPitch + 50) % 100) - 50;
}

return { key, pitch };
};
Expand Down
1 change: 0 additions & 1 deletion shared/validation/song/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ export const MY_SONGS = deepFreeze({
});

export const BROWSER_SONGS = deepFreeze({
max_recent_songs: 100,
featuredPageSize: 10,
paddedFeaturedPageSize: 5,
});
13 changes: 13 additions & 0 deletions shared/validation/user/dto/UpdateUsername.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator';

export class UpdateUsernameDto {
@IsString()
@MaxLength(64)
@MinLength(3)
@ApiProperty({
description: 'Username of the user',
example: 'tomast1137',
})
username: string;
}
2 changes: 1 addition & 1 deletion web/src/app/(content)/(info)/contact/contact.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ Have you got any question, suggestion or feedback? We'd love to hear from you!

To report an issue or suggest a new feature, please [open a new issue](https://github.com/issues/new/choose) in our GitHub repository. Make sure to search the existing issues to see if your suggestion has already been made. Also, check our [project roadmap](https://github.com/orgs/OpenNBS/projects/4) to see if the feature you want is already planned!

For general support, or if you'd just like to have a chat, the fastest way to get in touch with us is by joining our public community on [Discord](https://discord.gg/open-note-block-studio-608692895179997252). We're always happy to help!
For general support, or if you'd just like to have a chat, the fastest way to get in touch with us is by joining our public community on [Discord](https://discord.gg/note-block-world-608692895179997252). We're always happy to help!

For business inquiries, or if you need to reach out to us privately, you can send us an email at [opennbs@gmail.com](opennbs@gmail.com).
4 changes: 2 additions & 2 deletions web/src/app/(content)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ export const metadata: Metadata = {
};

async function Home() {
const recentSongs = await fetchRecentSongs();
//const recentSongs = await fetchRecentSongs();
const featuredSongs = await fetchFeaturedSongs();

return (
<HomePageProvider
initialRecentSongs={recentSongs}
initialRecentSongs={[]}
initialFeaturedSongs={featuredSongs}
>
<HomePageComponent />
Expand Down
24 changes: 24 additions & 0 deletions web/src/lib/axios/ClientAxios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import axios from 'axios';
import { getTokenLocal } from './token.utils';
export const baseApiURL = process.env.NEXT_PUBLIC_API_URL;

const ClientAxios = axios.create({
baseURL: baseApiURL,
withCredentials: true,
});

// Add a request interceptor to add the token to the request
ClientAxios.interceptors.request.use(
(config) => {
const token = getTokenLocal();

config.headers.authorization = `Bearer ${token}`;

return config;
},
(error) => {
return Promise.reject(error);
},
);

export default ClientAxios;
2 changes: 1 addition & 1 deletion web/src/modules/browse/WelcomeBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const WelcomeBanner = () => {
</Link>
{' • '}
<Link
href='https://github.com/OpenNBS/NoteBlockWorld/issues/new/choose'
href='https://discord.gg/note-block-world-608692895179997252'
className='text-blue-400 hover:text-blue-300'
>
Discord Server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

import { BROWSER_SONGS } from '@shared/validation/song/constants';
import { SongPreviewDtoType } from '@shared/validation/song/dto/types';
import {
createContext,
Expand Down Expand Up @@ -39,7 +38,7 @@ export function RecentSongsProvider({
useState<SongPreviewDtoType[]>(initialRecentSongs);

const [recentError, setRecentError] = useState<string>('');
const [page, setPage] = useState<number>(3);
const [page, setPage] = useState<number>(0);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [categories, setCategories] = useState<Record<string, number>>({});
Expand All @@ -56,7 +55,7 @@ export function RecentSongsProvider({
{
params: {
page,
limit: 8, // TODO: fix constants
limit: 12, // TODO: fix constants
order: false,
},
},
Expand All @@ -67,7 +66,7 @@ export function RecentSongsProvider({
...response.data,
]);

if (response.data.length < 8) {
if (response.data.length < 12) {
setHasMore(false);
}
} catch (error) {
Expand Down Expand Up @@ -114,18 +113,13 @@ export function RecentSongsProvider({
setEndpoint(newEndpoint);
}, [selectedCategory]);

// Fetch recent songs when the page or endpoint changes
useEffect(() => {
if (page === 0) return;
fetchRecentSongs();
}, [page, endpoint]);
}, [page, endpoint, fetchRecentSongs]);

async function increasePageRecent() {
if (
BROWSER_SONGS.max_recent_songs <= recentSongs.length ||
loading ||
recentError ||
!hasMore
) {
if (loading || recentError || !hasMore) {
return;
}

Expand Down
Loading
Loading