From 3fa19104cb9c4a38f5e815caa40443c152343ca7 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sun, 11 Jan 2026 11:51:10 -0300 Subject: [PATCH 01/13] update dev script in package.json to include webpack --- apps/frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index f6186544..09e6a894 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --webpack", "build": "next build --webpack", "start": "next start", "lint": "eslint \"src/**/*.{ts,tsx}\" --fix", From c332eb5f0ed80d3173bf095ca9b8c09ae36cb2ec Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sun, 11 Jan 2026 14:31:04 -0300 Subject: [PATCH 02/13] refactor: update TypeScript configurations and improve type handling in auth and song modules --- apps/backend/src/auth/auth.service.spec.ts | 2 +- apps/backend/src/auth/auth.service.ts | 22 ++++++++++------- .../src/auth/strategies/JWT.strategy.spec.ts | 4 ++-- .../discord.strategy/discord.strategy.spec.ts | 2 +- .../auth/strategies/discord.strategy/index.ts | 2 +- .../auth/strategies/github.strategy.spec.ts | 2 +- .../src/auth/strategies/github.strategy.ts | 4 ++-- .../auth/strategies/google.strategy.spec.ts | 2 +- apps/backend/src/lib/GetRequestUser.spec.ts | 2 +- apps/backend/src/song/song.controller.spec.ts | 24 +++++++++---------- apps/backend/src/song/song.controller.ts | 4 +++- apps/backend/src/song/song.service.spec.ts | 2 -- apps/backend/src/types/thumbnail.d.ts | 14 +++++++++++ apps/backend/tsconfig.json | 2 ++ bun.lock | 1 - 15 files changed, 55 insertions(+), 34 deletions(-) create mode 100644 apps/backend/src/types/thumbnail.d.ts diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts index 311fece9..2fb54343 100644 --- a/apps/backend/src/auth/auth.service.spec.ts +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -192,7 +192,7 @@ describe('AuthService', () => { const refreshToken = 'refresh-token'; spyOn(jwtService, 'signAsync').mockImplementation( - (payload, options: any) => { + (payload: any, options: any) => { if (options.secret === 'test-jwt-secret') { return Promise.resolve(accessToken); } else if (options.secret === 'test-jwt-refresh-secret') { diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index aab47ca4..36072a8b 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -171,14 +171,20 @@ export class AuthService { public async createJwtPayload(payload: TokenPayload): Promise { const [accessToken, refreshToken] = await Promise.all([ - this.jwtService.signAsync(payload, { - secret: this.JWT_SECRET, - expiresIn: this.JWT_EXPIRES_IN, - }), - this.jwtService.signAsync(payload, { - secret: this.JWT_REFRESH_SECRET, - expiresIn: this.JWT_REFRESH_EXPIRES_IN, - }), + this.jwtService.signAsync( + payload as object, + { + secret: this.JWT_SECRET, + expiresIn: this.JWT_EXPIRES_IN, + } as any, + ), + this.jwtService.signAsync( + payload as object, + { + secret: this.JWT_REFRESH_SECRET, + expiresIn: this.JWT_REFRESH_EXPIRES_IN, + } as any, + ), ]); return { diff --git a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts index 052cae9e..c17b4f3f 100644 --- a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts @@ -33,7 +33,7 @@ describe('JwtStrategy', () => { it('should throw an error if JWT_SECRET is not set', () => { jest.spyOn(configService, 'getOrThrow').mockReturnValue(null); - expect(() => new JwtStrategy(configService)).toThrowError( + expect(() => new JwtStrategy(configService)).toThrow( 'JwtStrategy requires a secret or key', ); }); @@ -84,7 +84,7 @@ describe('JwtStrategy', () => { const payload = { userId: 'test-user-id' }; - expect(() => jwtStrategy.validate(req, payload)).toThrowError( + expect(() => jwtStrategy.validate(req, payload)).toThrow( 'No refresh token', ); }); diff --git a/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts b/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts index 0dbc8608..588074d7 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts @@ -43,7 +43,7 @@ describe('DiscordStrategy', () => { it('should throw an error if Discord config is missing', () => { jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); - expect(() => new DiscordStrategy(configService)).toThrowError( + expect(() => new DiscordStrategy(configService)).toThrow( 'OAuth2Strategy requires a clientID option', ); }); diff --git a/apps/backend/src/auth/strategies/discord.strategy/index.ts b/apps/backend/src/auth/strategies/discord.strategy/index.ts index c31ea7e0..de2d6ebd 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/index.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/index.ts @@ -27,7 +27,7 @@ export class DiscordStrategy extends PassportStrategy(strategy, 'discord') { callbackUrl: `${SERVER_URL}/v1/auth/discord/callback`, scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], fetchScope: true, - prompt: 'none', + prompt: 'none' as const, }; super(config); diff --git a/apps/backend/src/auth/strategies/github.strategy.spec.ts b/apps/backend/src/auth/strategies/github.strategy.spec.ts index c8793e00..a13b099b 100644 --- a/apps/backend/src/auth/strategies/github.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/github.strategy.spec.ts @@ -43,7 +43,7 @@ describe('GithubStrategy', () => { it('should throw an error if GitHub config is missing', () => { jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); - expect(() => new GithubStrategy(configService)).toThrowError( + expect(() => new GithubStrategy(configService)).toThrow( 'OAuth2Strategy requires a clientID option', ); }); diff --git a/apps/backend/src/auth/strategies/github.strategy.ts b/apps/backend/src/auth/strategies/github.strategy.ts index b7ea82ab..fbcc74d2 100644 --- a/apps/backend/src/auth/strategies/github.strategy.ts +++ b/apps/backend/src/auth/strategies/github.strategy.ts @@ -22,10 +22,10 @@ export class GithubStrategy extends PassportStrategy(strategy, 'github') { super({ clientID: GITHUB_CLIENT_ID, clientSecret: GITHUB_CLIENT_SECRET, - redirect_uri: `${SERVER_URL}/v1/auth/github/callback`, + callbackURL: `${SERVER_URL}/v1/auth/github/callback`, scope: 'user:read,user:email', state: false, - }); + } as any); } async validate(accessToken: string, refreshToken: string, profile: any) { diff --git a/apps/backend/src/auth/strategies/google.strategy.spec.ts b/apps/backend/src/auth/strategies/google.strategy.spec.ts index c1f1233e..ccee9e71 100644 --- a/apps/backend/src/auth/strategies/google.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/google.strategy.spec.ts @@ -44,7 +44,7 @@ describe('GoogleStrategy', () => { it('should throw an error if Google config is missing', () => { jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); - expect(() => new GoogleStrategy(configService)).toThrowError( + expect(() => new GoogleStrategy(configService)).toThrow( 'OAuth2Strategy requires a clientID option', ); }); diff --git a/apps/backend/src/lib/GetRequestUser.spec.ts b/apps/backend/src/lib/GetRequestUser.spec.ts index ebc2e65f..e85694fd 100644 --- a/apps/backend/src/lib/GetRequestUser.spec.ts +++ b/apps/backend/src/lib/GetRequestUser.spec.ts @@ -28,7 +28,7 @@ describe('validateUser', () => { }); it('should throw an error if the user does not exist', () => { - expect(() => validateUser(null)).toThrowError( + expect(() => validateUser(null)).toThrow( new HttpException( { error: { diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts index 51865a09..1a2fa4d6 100644 --- a/apps/backend/src/song/song.controller.spec.ts +++ b/apps/backend/src/song/song.controller.spec.ts @@ -249,7 +249,7 @@ describe('SongController', () => { const query: SongListQueryDTO = { page: 1, limit: 10 }; const songList: SongPreviewDto[] = Array(10) .fill(null) - .map((_, i) => ({ id: `song-${i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -271,7 +271,7 @@ describe('SongController', () => { const query: SongListQueryDTO = { page: 1, limit: 10 }; const songList: SongPreviewDto[] = Array(5) .fill(null) - .map((_, i) => ({ id: `song-${i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -291,7 +291,7 @@ describe('SongController', () => { const query: SongListQueryDTO = { page: 3, limit: 10 }; const songList: SongPreviewDto[] = Array(10) .fill(null) - .map((_, i) => ({ id: `song-${20 + i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${20 + i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -312,7 +312,7 @@ describe('SongController', () => { const query: SongListQueryDTO = { page: 1, limit: 10, q: 'test search' }; const songList: SongPreviewDto[] = Array(8) .fill(null) - .map((_, i) => ({ id: `song-${i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -337,7 +337,7 @@ describe('SongController', () => { }; const songList: SongPreviewDto[] = Array(3) .fill(null) - .map((_, i) => ({ id: `rock-song-${i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `rock-song-${i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -421,7 +421,7 @@ describe('SongController', () => { const q = 'test query'; const songList: SongPreviewDto[] = Array(5) .fill(null) - .map((_, i) => ({ id: `song-${i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -484,7 +484,7 @@ describe('SongController', () => { const q = 'test search'; const songList: SongPreviewDto[] = Array(10) .fill(null) - .map((_, i) => ({ id: `song-${10 + i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${10 + i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -507,7 +507,7 @@ describe('SongController', () => { const q = 'popular song'; const songList: SongPreviewDto[] = Array(50) .fill(null) - .map((_, i) => ({ id: `song-${i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -529,7 +529,7 @@ describe('SongController', () => { const q = 'search term'; const songList: SongPreviewDto[] = Array(3) .fill(null) - .map((_, i) => ({ id: `song-${40 + i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${40 + i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -587,7 +587,7 @@ describe('SongController', () => { const q = 'test'; const songList: SongPreviewDto[] = Array(25) .fill(null) - .map((_, i) => ({ id: `song-${i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -614,7 +614,7 @@ describe('SongController', () => { const q = 'trending'; const songList: SongPreviewDto[] = Array(10) .fill(null) - .map((_, i) => ({ id: `song-${i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, @@ -635,7 +635,7 @@ describe('SongController', () => { const q = 'search'; const songList: SongPreviewDto[] = Array(20) .fill(null) - .map((_, i) => ({ id: `song-${40 + i}` } as SongPreviewDto)); + .map((_, i) => ({ id: `song-${40 + i}` } as unknown as SongPreviewDto)); mockSongService.querySongs.mockResolvedValueOnce({ content: songList, diff --git a/apps/backend/src/song/song.controller.ts b/apps/backend/src/song/song.controller.ts index d50f2a8f..2d311b8b 100644 --- a/apps/backend/src/song/song.controller.ts +++ b/apps/backend/src/song/song.controller.ts @@ -129,7 +129,9 @@ export class SongController { [SongSortType.NOTE_COUNT, 'stats.noteCount'], ]); - const sortField = sortFieldMap.get(query.sort) ?? 'createdAt'; + const sortField = query.sort + ? sortFieldMap.get(query.sort) ?? 'createdAt' + : 'createdAt'; const isDescending = query.order ? query.order === 'desc' : true; // Build PageQueryDTO with the sort field diff --git a/apps/backend/src/song/song.service.spec.ts b/apps/backend/src/song/song.service.spec.ts index 4a944f7f..4d0e7f21 100644 --- a/apps/backend/src/song/song.service.spec.ts +++ b/apps/backend/src/song/song.service.spec.ts @@ -1212,7 +1212,6 @@ describe('SongService', () => { }; jest.spyOn(songModel, 'aggregate').mockReturnValue(mockAggregate as any); - jest.spyOn(songModel, 'populate').mockResolvedValue(songList); const result = await service.getRandomSongs(count); @@ -1236,7 +1235,6 @@ describe('SongService', () => { }; jest.spyOn(songModel, 'aggregate').mockReturnValue(mockAggregate as any); - jest.spyOn(songModel, 'populate').mockResolvedValue(songList); const result = await service.getRandomSongs(count, category); diff --git a/apps/backend/src/types/thumbnail.d.ts b/apps/backend/src/types/thumbnail.d.ts new file mode 100644 index 00000000..172e7cde --- /dev/null +++ b/apps/backend/src/types/thumbnail.d.ts @@ -0,0 +1,14 @@ +declare module '@nbw/thumbnail/node' { + interface DrawParams { + notes: unknown; + startTick: number; + startLayer: number; + zoomLevel: number; + backgroundColor: string; + imgWidth: number; + imgHeight: number; + } + + export function drawToImage(params: DrawParams): Promise; + export function drawNotesOffscreen(params: DrawParams): Promise; +} diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 400170a1..35b9a8d0 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -9,6 +9,8 @@ "allowSyntheticDefaultImports": true, "sourceMap": true, "emitDecoratorMetadata": true, + "moduleResolution": "node", + "noEmit": true, // Path mapping "baseUrl": ".", diff --git a/bun.lock b/bun.lock index b2cdbafe..d68c86ae 100644 --- a/bun.lock +++ b/bun.lock @@ -206,7 +206,6 @@ "name": "@nbw/song", "dependencies": { "@encode42/nbs.js": "^5.0.2", - "@nbw/database": "workspace:*", "@timohausmann/quadtree-ts": "^2.2.2", "jszip": "^3.10.1", "unidecode": "^1.1.0", From 57c969c2dc7ff2dc76ee351cbcbf1531bd59a1dd Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sun, 11 Jan 2026 14:46:06 -0300 Subject: [PATCH 03/13] chore(deps): update Next.js and related packages to version 16.0.10 --- bun.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index d68c86ae..c5cf1237 100644 --- a/bun.lock +++ b/bun.lock @@ -140,7 +140,7 @@ "i": "^0.3.7", "js-confetti": "^0.13.1", "lucide-react": "^0.556.0", - "next": "16.0.8", + "next": "16.0.10", "next-recaptcha-v3": "^1.5.3", "nextjs-toploader": "^3.9.17", "npm": "^11.7.0", @@ -720,27 +720,27 @@ "@nestjs/throttler": ["@nestjs/throttler@6.5.0", "", { "peerDependencies": { "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "reflect-metadata": "^0.1.13 || ^0.2.0" } }, "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ=="], - "@next/env": ["@next/env@16.0.8", "", {}, "sha512-xP4WrQZuj9MdmLJy3eWFHepo+R3vznsMSS8Dy3wdA7FKpjCiesQ6DxZvdGziQisj0tEtCgBKJzjcAc4yZOgLEQ=="], + "@next/env": ["@next/env@16.0.10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.0.8", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-1miV0qXDcLUaOdHridVPCh4i39ElRIAraseVIbb3BEqyZ5ol9sPyjTP/GNTPV5rBxqxjF6/vv5zQTVbhiNaLqA=="], "@next/mdx": ["@next/mdx@16.0.8", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-GPv0ouRVp6T/qaOZj3LJiiIUFputrHvC/FVgyedbCaIsfbXmfu+Ky24KWWjf6uQSvDWQnQTk3UMSHqHb5UMYLw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yjVMvTQN21ZHOclQnhSFbjBTEizle+1uo4NV6L4rtS9WO3nfjaeJYw+H91G+nEf3Ef43TaEZvY5mPWfB/De7tA=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-+zu2N3QQ0ZOb6RyqQKfcu/pn0UPGmg+mUDqpAAEviAcEVEYgDckemOpiMRsBP3IsEKpcoKuNzekDcPczEeEIzA=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-LConttk+BeD0e6RG0jGEP9GfvdaBVMYsLJ5aDDweKiJVVCu6sGvo+Ohz9nQhvj7EQDVVRJMCGhl19DmJwGr6bQ=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.8", "", { "os": "linux", "cpu": "x64" }, "sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.8", "", { "os": "win32", "cpu": "x64" }, "sha512-jWpWjWcMQu2iZz4pEK2IktcfR+OA9+cCG8zenyLpcW8rN4rzjfOzH4yj/b1FiEAZHKS+5Vq8+bZyHi+2yqHbFA=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="], "@next/third-parties": ["@next/third-parties@16.0.8", "", { "dependencies": { "third-party-capital": "1.0.20" }, "peerDependencies": { "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0" } }, "sha512-F8TNI1GFm7ivZX6HBMDVsDklAXOHlACbtw7w3WFbDk2oFXdVgKZBfdK+ILhjTBK4ibIXzLMWO1Py3bgSETKHYQ=="], @@ -2474,7 +2474,7 @@ "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "next": ["next@16.0.8", "", { "dependencies": { "@next/env": "16.0.8", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.8", "@next/swc-darwin-x64": "16.0.8", "@next/swc-linux-arm64-gnu": "16.0.8", "@next/swc-linux-arm64-musl": "16.0.8", "@next/swc-linux-x64-gnu": "16.0.8", "@next/swc-linux-x64-musl": "16.0.8", "@next/swc-win32-arm64-msvc": "16.0.8", "@next/swc-win32-x64-msvc": "16.0.8", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-LmcZzG04JuzNXi48s5P+TnJBsTGPJunViNKV/iE4uM6kstjTQsQhvsAv+xF6MJxU2Pr26tl15eVbp0jQnsv6/g=="], + "next": ["next@16.0.10", "", { "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="], "next-recaptcha-v3": ["next-recaptcha-v3@1.5.3", "", { "peerDependencies": { "next": "^13 || ^14 || ^15 || ^16", "react": "^18 || ^19" } }, "sha512-Osnt1gj0+Mor8rc42NCzpteQrrSbcxskGLOeWLU/T0xdXtJE90y/gFyp87/yN1goIMI+gXs5f0PMymMa29nuLA=="], From ed398097de180c2c7a4a4da7e104cf22b1906184 Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:18:24 -0300 Subject: [PATCH 04/13] chore: add `@nbw/thumbnail/node` type defs on backend --- apps/backend/src/types/thumbnail.d.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/backend/src/types/thumbnail.d.ts b/apps/backend/src/types/thumbnail.d.ts index 172e7cde..c4e70c18 100644 --- a/apps/backend/src/types/thumbnail.d.ts +++ b/apps/backend/src/types/thumbnail.d.ts @@ -1,3 +1,28 @@ +/* + * Type Definitions for '@nbw/thumbnail/node' Module + * + * PURPOSE: + * This declaration file explicitly declares type definitions for the '@nbw/thumbnail/node' + * module. While the '@nbw/thumbnail' package exports both Node.js and browser-specific + * implementations via the modern 'exports' field in package.json, the backend's + * moduleResolution is set to 'node' (= node10, required for CommonJS module system compatibility), + * which does not understand the 'exports' field. This file bridges that gap. + * + * BACKGROUND: + * The @nbw/thumbnail package uses conditional exports to provide different entry points + * for Node.js and browser environments. Modern module resolution (e.g., 'bundler', 'node16+') + * understands this, but the 'node' resolver does not, causing import resolution issues like + * 'Cannot find module '@nbw/thumbnail' or its corresponding type declarations.' + * + * MAINTENANCE NOTE: + * If the @nbw/thumbnail package's exports change or if new functions/interfaces are added, + * this file must be updated to reflect those changes. Sync the DrawParams interface and + * exported functions with the actual implementation in packages/thumbnail/src/node/. + * + * ALTERNATIVES: + * // TODO: Update backend's moduleResolution to 'bundler', 'node16' or 'nodenext' + */ + declare module '@nbw/thumbnail/node' { interface DrawParams { notes: unknown; From d34865f2441174425798a00b9288807152c63d8f Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:57:36 -0300 Subject: [PATCH 05/13] chore(types): change backend's `moduleResolution` to `bundler` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change stems from the fact that Nest.js now defaults to ``` "module": "nodenext", "moduleResolution": "nodenext" ``` in recent releases. See: https://github.com/nestjs/nest/issues/15919#issuecomment-3533271585 This change lets us support modern `exports` syntax such as `@nbw/thumbnail/node`, without requiring external type declarations or even causing any additional typing errors (which was the case with `moduleResolution: nodenext` due to its stricter syntax around exports and extensions). Type-checking works fine, builds work fine, the backend starts just fine. ✅ --- apps/backend/tsconfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 35b9a8d0..a23a38c9 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -2,14 +2,14 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { // NestJS specific settings - "module": "commonjs", - "target": "ES2021", + "module": "esnext", + "target": "esnext", "declaration": true, "removeComments": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "emitDecoratorMetadata": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "noEmit": true, // Path mapping From 58a90d8e95e4d1e10e4eeabcfa94888d817e6716 Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:58:26 -0300 Subject: [PATCH 06/13] chore(types): remove type definitions for `@nbw/thumbnail/node` No longer needed after changing `moduleResolution` to `bundler` in the backend. --- apps/backend/src/types/thumbnail.d.ts | 39 --------------------------- 1 file changed, 39 deletions(-) delete mode 100644 apps/backend/src/types/thumbnail.d.ts diff --git a/apps/backend/src/types/thumbnail.d.ts b/apps/backend/src/types/thumbnail.d.ts deleted file mode 100644 index c4e70c18..00000000 --- a/apps/backend/src/types/thumbnail.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Type Definitions for '@nbw/thumbnail/node' Module - * - * PURPOSE: - * This declaration file explicitly declares type definitions for the '@nbw/thumbnail/node' - * module. While the '@nbw/thumbnail' package exports both Node.js and browser-specific - * implementations via the modern 'exports' field in package.json, the backend's - * moduleResolution is set to 'node' (= node10, required for CommonJS module system compatibility), - * which does not understand the 'exports' field. This file bridges that gap. - * - * BACKGROUND: - * The @nbw/thumbnail package uses conditional exports to provide different entry points - * for Node.js and browser environments. Modern module resolution (e.g., 'bundler', 'node16+') - * understands this, but the 'node' resolver does not, causing import resolution issues like - * 'Cannot find module '@nbw/thumbnail' or its corresponding type declarations.' - * - * MAINTENANCE NOTE: - * If the @nbw/thumbnail package's exports change or if new functions/interfaces are added, - * this file must be updated to reflect those changes. Sync the DrawParams interface and - * exported functions with the actual implementation in packages/thumbnail/src/node/. - * - * ALTERNATIVES: - * // TODO: Update backend's moduleResolution to 'bundler', 'node16' or 'nodenext' - */ - -declare module '@nbw/thumbnail/node' { - interface DrawParams { - notes: unknown; - startTick: number; - startLayer: number; - zoomLevel: number; - backgroundColor: string; - imgWidth: number; - imgHeight: number; - } - - export function drawToImage(params: DrawParams): Promise; - export function drawNotesOffscreen(params: DrawParams): Promise; -} From 742930141f6d33a99454098a394998c6dd1db85a Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:06:12 -0300 Subject: [PATCH 07/13] refactor(types): remove redundant ternary operator in song query `sort` param --- apps/backend/src/song/song.controller.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/backend/src/song/song.controller.ts b/apps/backend/src/song/song.controller.ts index 4c315f3b..c1005d66 100644 --- a/apps/backend/src/song/song.controller.ts +++ b/apps/backend/src/song/song.controller.ts @@ -132,9 +132,7 @@ export class SongController { [SongSortType.NOTE_COUNT, 'stats.noteCount'], ]); - const sortField = query.sort - ? sortFieldMap.get(query.sort) ?? 'createdAt' - : 'createdAt'; + const sortField = sortFieldMap.get(query.sort ?? SongSortType.RECENT); const isDescending = query.order ? query.order === 'desc' : true; // Build PageQueryDTO with the sort field From c5a22d58bebc844834137a973826994098b724af Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:14:07 -0300 Subject: [PATCH 08/13] refactor(types): remove catch-all types for `jwtService.signAsync` call --- apps/backend/src/auth/auth.service.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index 36072a8b..8bfca85e 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -171,20 +171,14 @@ export class AuthService { public async createJwtPayload(payload: TokenPayload): Promise { const [accessToken, refreshToken] = await Promise.all([ - this.jwtService.signAsync( - payload as object, - { - secret: this.JWT_SECRET, - expiresIn: this.JWT_EXPIRES_IN, - } as any, - ), - this.jwtService.signAsync( - payload as object, - { - secret: this.JWT_REFRESH_SECRET, - expiresIn: this.JWT_REFRESH_EXPIRES_IN, - } as any, - ), + this.jwtService.signAsync(payload, { + secret: this.JWT_SECRET, + expiresIn: this.JWT_EXPIRES_IN, + }), + this.jwtService.signAsync(payload, { + secret: this.JWT_REFRESH_SECRET, + expiresIn: this.JWT_REFRESH_EXPIRES_IN, + }), ]); return { From 8e431228b5814710e66a523ac0e3a82eeed6d87f Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:17:26 -0300 Subject: [PATCH 09/13] fix(config): parse duration env configs on startup and coerce to `ms.StringValue` --- apps/backend/src/auth/auth.module.ts | 11 ++--- apps/backend/src/auth/auth.service.ts | 9 ++-- .../src/config/EnvironmentVariables.ts | 44 ++++++++++++++++--- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index e266b0bf..2a7b7a84 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,6 +1,7 @@ import { DynamicModule, Logger, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; +import ms from 'ms'; import { MailingModule } from '@server/mailing/mailing.module'; import { UserModule } from '@server/user/user.module'; @@ -26,8 +27,8 @@ export class AuthModule { inject: [ConfigService], imports: [ConfigModule], useFactory: async (config: ConfigService) => { - const JWT_SECRET = config.get('JWT_SECRET'); - const JWT_EXPIRES_IN = config.get('JWT_EXPIRES_IN'); + const JWT_SECRET = config.get('JWT_SECRET'); + const JWT_EXPIRES_IN = config.get('JWT_EXPIRES_IN'); if (!JWT_SECRET) { Logger.error('JWT_SECRET is not set'); @@ -58,7 +59,7 @@ export class AuthModule { inject: [ConfigService], provide: 'COOKIE_EXPIRES_IN', useFactory: (configService: ConfigService) => - configService.getOrThrow('COOKIE_EXPIRES_IN'), + configService.getOrThrow('COOKIE_EXPIRES_IN'), }, { inject: [ConfigService], @@ -82,7 +83,7 @@ export class AuthModule { inject: [ConfigService], provide: 'JWT_EXPIRES_IN', useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_EXPIRES_IN'), + configService.getOrThrow('JWT_EXPIRES_IN'), }, { inject: [ConfigService], @@ -94,7 +95,7 @@ export class AuthModule { inject: [ConfigService], provide: 'JWT_REFRESH_EXPIRES_IN', useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_REFRESH_EXPIRES_IN'), + configService.getOrThrow('JWT_REFRESH_EXPIRES_IN'), }, { inject: [ConfigService], diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index 8bfca85e..53237ea1 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import axios from 'axios'; import type { Request, Response } from 'express'; +import ms from 'ms'; import { CreateUser } from '@nbw/database'; import type { UserDocument } from '@nbw/database'; @@ -22,18 +23,18 @@ export class AuthService { @Inject(JwtService) private readonly jwtService: JwtService, @Inject('COOKIE_EXPIRES_IN') - private readonly COOKIE_EXPIRES_IN: string, + private readonly COOKIE_EXPIRES_IN: ms.StringValue, @Inject('FRONTEND_URL') private readonly FRONTEND_URL: string, @Inject('JWT_SECRET') private readonly JWT_SECRET: string, @Inject('JWT_EXPIRES_IN') - private readonly JWT_EXPIRES_IN: string, + private readonly JWT_EXPIRES_IN: ms.StringValue, @Inject('JWT_REFRESH_SECRET') private readonly JWT_REFRESH_SECRET: string, @Inject('JWT_REFRESH_EXPIRES_IN') - private readonly JWT_REFRESH_EXPIRES_IN: string, + private readonly JWT_REFRESH_EXPIRES_IN: ms.StringValue, @Inject('APP_DOMAIN') private readonly APP_DOMAIN?: string, ) {} @@ -199,7 +200,7 @@ export class AuthService { const frontEndURL = this.FRONTEND_URL; const domain = this.APP_DOMAIN; - const maxAge = parseInt(this.COOKIE_EXPIRES_IN) * 1000; + const maxAge = ms(this.COOKIE_EXPIRES_IN) * 1000; res.cookie('token', token.access_token, { domain: domain, diff --git a/apps/backend/src/config/EnvironmentVariables.ts b/apps/backend/src/config/EnvironmentVariables.ts index cbb15109..73e5afd2 100644 --- a/apps/backend/src/config/EnvironmentVariables.ts +++ b/apps/backend/src/config/EnvironmentVariables.ts @@ -1,5 +1,35 @@ import { plainToInstance } from 'class-transformer'; -import { IsEnum, IsOptional, IsString, validateSync } from 'class-validator'; +import { + IsEnum, + IsOptional, + IsString, + registerDecorator, + validateSync, + ValidationArguments, + ValidationOptions, +} from 'class-validator'; +import ms from 'ms'; + +// Validate if the value is a valid duration string from the 'ms' library +function IsDuration(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isDuration', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: unknown) { + if (typeof value !== 'string') return false; + return typeof ms(value as ms.StringValue) === 'number'; + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a valid duration string (e.g., "1h", "30m", "7d")`; + }, + }, + }); + }; +} enum Environment { Development = 'development', @@ -38,14 +68,14 @@ export class EnvironmentVariables { @IsString() JWT_SECRET: string; - @IsString() - JWT_EXPIRES_IN: string; + @IsDuration() + JWT_EXPIRES_IN: ms.StringValue; @IsString() JWT_REFRESH_SECRET: string; - @IsString() - JWT_REFRESH_EXPIRES_IN: string; + @IsDuration() + JWT_REFRESH_EXPIRES_IN: ms.StringValue; // database @IsString() @@ -91,8 +121,8 @@ export class EnvironmentVariables { @IsString() DISCORD_WEBHOOK_URL: string; - @IsString() - COOKIE_EXPIRES_IN: string; + @IsDuration() + COOKIE_EXPIRES_IN: ms.StringValue; } export function validate(config: Record) { From bf1acef919e196198e97b35f6019135099bbf7d4 Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:20:00 -0300 Subject: [PATCH 10/13] fix(config) use `config.getOrThrow` to simplify logic in `AuthModule` + don't assume 60s if no value is provided --- apps/backend/src/auth/auth.module.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index 2a7b7a84..725ad27e 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,4 +1,4 @@ -import { DynamicModule, Logger, Module } from '@nestjs/common'; +import { DynamicModule, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import ms from 'ms'; @@ -27,21 +27,13 @@ export class AuthModule { inject: [ConfigService], imports: [ConfigModule], useFactory: async (config: ConfigService) => { - const JWT_SECRET = config.get('JWT_SECRET'); - const JWT_EXPIRES_IN = config.get('JWT_EXPIRES_IN'); - - if (!JWT_SECRET) { - Logger.error('JWT_SECRET is not set'); - throw new Error('JWT_SECRET is not set'); - } - - if (!JWT_EXPIRES_IN) { - Logger.warn('JWT_EXPIRES_IN is not set, using default of 60s'); - } + const JWT_SECRET = config.getOrThrow('JWT_SECRET'); + const JWT_EXPIRES_IN = + config.getOrThrow('JWT_EXPIRES_IN'); return { secret: JWT_SECRET, - signOptions: { expiresIn: JWT_EXPIRES_IN || '60s' }, + signOptions: { expiresIn: JWT_EXPIRES_IN }, }; }, }), From 98c472bce3085a234ff67a099bbe446774472caf Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:21:20 -0300 Subject: [PATCH 11/13] fix(config): validator's `defaultMessage` not being shown on env validation --- apps/backend/src/config/EnvironmentVariables.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/config/EnvironmentVariables.ts b/apps/backend/src/config/EnvironmentVariables.ts index 73e5afd2..a933ce57 100644 --- a/apps/backend/src/config/EnvironmentVariables.ts +++ b/apps/backend/src/config/EnvironmentVariables.ts @@ -135,7 +135,13 @@ export function validate(config: Record) { }); if (errors.length > 0) { - throw new Error(errors.toString()); + const messages = errors + .map((error) => { + const constraints = Object.values(error.constraints || {}); + return ` - ${error.property}: ${constraints.join(', ')}`; + }) + .join('\n'); + throw new Error(`Environment validation failed:\n${messages}`); } return validatedConfig; From ba1d3b90067c57e1d35fcda4b89015532c804030 Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:39:33 -0300 Subject: [PATCH 12/13] chore(types): add TODO for improving typing on GitHub login strategy --- apps/backend/src/auth/strategies/github.strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/auth/strategies/github.strategy.ts b/apps/backend/src/auth/strategies/github.strategy.ts index fbcc74d2..048a491e 100644 --- a/apps/backend/src/auth/strategies/github.strategy.ts +++ b/apps/backend/src/auth/strategies/github.strategy.ts @@ -25,7 +25,7 @@ export class GithubStrategy extends PassportStrategy(strategy, 'github') { callbackURL: `${SERVER_URL}/v1/auth/github/callback`, scope: 'user:read,user:email', state: false, - } as any); + } as any); // TODO: Fix types } async validate(accessToken: string, refreshToken: string, profile: any) { From b25030b5b09f7a6441793b5b773c83a359c148f9 Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:28:47 -0300 Subject: [PATCH 13/13] test(song): remove duplicate authorization tests --- apps/backend/src/song/song.service.spec.ts | 50 ---------------------- 1 file changed, 50 deletions(-) diff --git a/apps/backend/src/song/song.service.spec.ts b/apps/backend/src/song/song.service.spec.ts index bdb63285..edf8dfb6 100644 --- a/apps/backend/src/song/song.service.spec.ts +++ b/apps/backend/src/song/song.service.spec.ts @@ -308,23 +308,6 @@ describe('SongService', () => { HttpException, ); }); - - it('should throw an error if user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const songEntity = new SongEntity(); - songEntity.uploader = new mongoose.Types.ObjectId(); // Different uploader - - const mockFindOne = { - exec: jest.fn().mockResolvedValue(songEntity), - }; - - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); - - await expect(service.deleteSong(publicId, user)).rejects.toThrow( - HttpException, - ); - }); }); describe('patchSong', () => { @@ -495,39 +478,6 @@ describe('SongService', () => { ); }); - it('should throw an error if user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', - customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', - }, - file: 'somebytes', - allowDownload: false, - }; - - const songEntity = { - uploader: 'different-user-id', - } as any; - - jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity); - - await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException, - ); - }); - it('should throw an error if user no changes are provided', async () => { const publicId = 'test-id'; const user: UserDocument = { _id: 'test-user-id' } as UserDocument;