Skip to content

Commit 3907c3a

Browse files
authored
Finish song search implementation (#81)
2 parents 9cffb55 + 34d834c commit 3907c3a

File tree

142 files changed

+1976
-1756
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

142 files changed

+1976
-1756
lines changed

.github/workflows/typecheck.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Type Check
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
- main
8+
pull_request:
9+
branches:
10+
- develop
11+
- main
12+
13+
jobs:
14+
typecheck:
15+
runs-on: ubuntu-latest
16+
env:
17+
THUMBNAIL_URL: ${{ vars.THUMBNAIL_URL }}
18+
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Install bun
24+
uses: oven-sh/setup-bun@v2
25+
with:
26+
bun-version: latest
27+
28+
- name: Install dependencies
29+
run: bun install
30+
31+
- name: Type check (project references)
32+
run: bun run typecheck

.vscode/settings.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
/***********************************
3+
Linting and formatting settings
4+
**********************************/
25
"editor.defaultFormatter": "esbenp.prettier-vscode",
36
"editor.formatOnSave": true,
47
"eslint.validate": [
@@ -13,8 +16,27 @@
1316
"editor.codeActionsOnSave": {
1417
"source.fixAll.eslint": "always"
1518
},
19+
20+
/***********************************
21+
Tailwind CSS IntelliSense settings
22+
***********************************/
23+
// Enable suggestions inside strings for Tailwind CSS class names
24+
// https://github.com/tailwindlabs/tailwindcss-intellisense#editorquicksuggestions
25+
"editor.quickSuggestions": {
26+
"strings": "on"
27+
},
28+
"tailwindCSS.experimental.configFile": {
29+
"apps/frontend/src/app/globals.css": "apps/frontend/src/**"
30+
},
31+
"tailwindCSS.classFunctions": ["tw", "clsx", "cn"],
1632
"files.associations": {
1733
".css": "tailwindcss",
1834
"*.scss": "tailwindcss"
19-
}
35+
},
36+
37+
/***********************************
38+
Use the workspace version of TypeScript
39+
***********************************/
40+
"typescript.tsdk": "./node_modules/typescript/lib",
41+
"typescript.enablePromptUseWorkspaceTsdk": true
2042
}

apps/backend/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
"@aws-sdk/client-s3": "3.946.0",
2626
"@aws-sdk/s3-request-presigner": "3.946.0",
2727
"@encode42/nbs.js": "^5.0.2",
28+
"@nbw/config": "workspace:*",
29+
"@nbw/database": "workspace:*",
30+
"@nbw/song": "workspace:*",
31+
"@nbw/sounds": "workspace:*",
32+
"@nbw/thumbnail": "workspace:*",
2833
"@nestjs-modules/mailer": "^2.0.2",
2934
"@nestjs/common": "^11.1.9",
3035
"@nestjs/config": "^4.0.2",
@@ -56,12 +61,7 @@
5661
"rxjs": "^7.8.2",
5762
"uuid": "^13.0.0",
5863
"zod": "^4.1.13",
59-
"zod-validation-error": "^5.0.0",
60-
"@nbw/database": "workspace:*",
61-
"@nbw/song": "workspace:*",
62-
"@nbw/thumbnail": "workspace:*",
63-
"@nbw/sounds": "workspace:*",
64-
"@nbw/config": "workspace:*"
64+
"zod-validation-error": "^5.0.0"
6565
},
6666
"devDependencies": {
6767
"@faker-js/faker": "^10.1.0",

apps/backend/scripts/build.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,20 @@ const build = async () => {
6565
target: 'bun',
6666
minify: false,
6767
sourcemap: 'linked',
68-
external: optionalRequirePackages.filter((pkg) => {
69-
try {
70-
require(pkg);
71-
return false;
72-
} catch (_) {
73-
return true;
74-
}
75-
}),
68+
external: [
69+
...optionalRequirePackages.filter((pkg) => {
70+
try {
71+
require(pkg);
72+
return false;
73+
} catch (_) {
74+
return true;
75+
}
76+
}),
77+
'@nbw/config',
78+
'@nbw/database',
79+
'@nbw/song',
80+
'@nbw/sounds',
81+
],
7682
splitting: true,
7783
});
7884

apps/backend/src/auth/auth.controller.spec.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ describe('AuthController', () => {
2222
let authService: AuthService;
2323

2424
beforeEach(async () => {
25+
// Clear all mocks before each test to ensure test isolation
26+
jest.clearAllMocks();
27+
2528
const module: TestingModule = await Test.createTestingModule({
2629
controllers: [AuthController],
2730
providers: [
@@ -68,8 +71,13 @@ describe('AuthController', () => {
6871

6972
describe('githubLogin', () => {
7073
it('should call AuthService.githubLogin', async () => {
71-
await controller.githubLogin();
72-
expect(authService.githubLogin).toHaveBeenCalled();
74+
// githubLogin is just a Passport guard entry point - it doesn't call authService
75+
// The actual login is handled by the callback endpoint (githubRedirect)
76+
controller.githubLogin();
77+
// Verify the method exists and can be called without errors
78+
expect(controller.githubLogin).toBeDefined();
79+
// Verify authService was NOT called (since this is just a guard entry point)
80+
expect(authService.githubLogin).not.toHaveBeenCalled();
7381
});
7482
});
7583

@@ -97,8 +105,13 @@ describe('AuthController', () => {
97105

98106
describe('googleLogin', () => {
99107
it('should call AuthService.googleLogin', async () => {
100-
await controller.googleLogin();
101-
expect(authService.googleLogin).toHaveBeenCalled();
108+
// googleLogin is just a Passport guard entry point - it doesn't call authService
109+
// The actual login is handled by the callback endpoint (googleRedirect)
110+
controller.googleLogin();
111+
// Verify the method exists and can be called without errors
112+
expect(controller.googleLogin).toBeDefined();
113+
// Verify authService was NOT called (since this is just a guard entry point)
114+
expect(authService.googleLogin).not.toHaveBeenCalled();
102115
});
103116
});
104117

@@ -126,8 +139,13 @@ describe('AuthController', () => {
126139

127140
describe('discordLogin', () => {
128141
it('should call AuthService.discordLogin', async () => {
129-
await controller.discordLogin();
130-
expect(authService.discordLogin).toHaveBeenCalled();
142+
// discordLogin is just a Passport guard entry point - it doesn't call authService
143+
// The actual login is handled by the callback endpoint (discordRedirect)
144+
controller.discordLogin();
145+
// Verify the method exists and can be called without errors
146+
expect(controller.discordLogin).toBeDefined();
147+
// Verify authService was NOT called (since this is just a guard entry point)
148+
expect(authService.discordLogin).not.toHaveBeenCalled();
131149
});
132150
});
133151

apps/backend/src/auth/auth.module.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { DynamicModule, Logger, Module } from '@nestjs/common';
1+
import { DynamicModule, Module } from '@nestjs/common';
22
import { ConfigModule, ConfigService } from '@nestjs/config';
33
import { JwtModule } from '@nestjs/jwt';
4+
import ms from 'ms';
45

56
import { MailingModule } from '@server/mailing/mailing.module';
67
import { UserModule } from '@server/user/user.module';
@@ -26,21 +27,13 @@ export class AuthModule {
2627
inject: [ConfigService],
2728
imports: [ConfigModule],
2829
useFactory: async (config: ConfigService) => {
29-
const JWT_SECRET = config.get('JWT_SECRET');
30-
const JWT_EXPIRES_IN = config.get('JWT_EXPIRES_IN');
31-
32-
if (!JWT_SECRET) {
33-
Logger.error('JWT_SECRET is not set');
34-
throw new Error('JWT_SECRET is not set');
35-
}
36-
37-
if (!JWT_EXPIRES_IN) {
38-
Logger.warn('JWT_EXPIRES_IN is not set, using default of 60s');
39-
}
30+
const JWT_SECRET = config.getOrThrow<ms.StringValue>('JWT_SECRET');
31+
const JWT_EXPIRES_IN =
32+
config.getOrThrow<ms.StringValue>('JWT_EXPIRES_IN');
4033

4134
return {
4235
secret: JWT_SECRET,
43-
signOptions: { expiresIn: JWT_EXPIRES_IN || '60s' },
36+
signOptions: { expiresIn: JWT_EXPIRES_IN },
4437
};
4538
},
4639
}),
@@ -58,7 +51,7 @@ export class AuthModule {
5851
inject: [ConfigService],
5952
provide: 'COOKIE_EXPIRES_IN',
6053
useFactory: (configService: ConfigService) =>
61-
configService.getOrThrow<string>('COOKIE_EXPIRES_IN'),
54+
configService.getOrThrow<ms.StringValue>('COOKIE_EXPIRES_IN'),
6255
},
6356
{
6457
inject: [ConfigService],
@@ -82,7 +75,7 @@ export class AuthModule {
8275
inject: [ConfigService],
8376
provide: 'JWT_EXPIRES_IN',
8477
useFactory: (configService: ConfigService) =>
85-
configService.getOrThrow<string>('JWT_EXPIRES_IN'),
78+
configService.getOrThrow<ms.StringValue>('JWT_EXPIRES_IN'),
8679
},
8780
{
8881
inject: [ConfigService],
@@ -94,7 +87,7 @@ export class AuthModule {
9487
inject: [ConfigService],
9588
provide: 'JWT_REFRESH_EXPIRES_IN',
9689
useFactory: (configService: ConfigService) =>
97-
configService.getOrThrow<string>('JWT_REFRESH_EXPIRES_IN'),
90+
configService.getOrThrow<ms.StringValue>('JWT_REFRESH_EXPIRES_IN'),
9891
},
9992
{
10093
inject: [ConfigService],

apps/backend/src/auth/auth.service.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ describe('AuthService', () => {
192192
const refreshToken = 'refresh-token';
193193

194194
spyOn(jwtService, 'signAsync').mockImplementation(
195-
(payload, options: any) => {
195+
(payload: any, options: any) => {
196196
if (options.secret === 'test-jwt-secret') {
197197
return Promise.resolve(accessToken);
198198
} else if (options.secret === 'test-jwt-refresh-secret') {
@@ -253,6 +253,7 @@ describe('AuthService', () => {
253253
expect(res.cookie).toHaveBeenCalledWith('token', 'access-token', {
254254
domain: '.test.com',
255255
maxAge: 3600000,
256+
path: '/',
256257
});
257258

258259
expect(res.cookie).toHaveBeenCalledWith(
@@ -261,6 +262,7 @@ describe('AuthService', () => {
261262
{
262263
domain: '.test.com',
263264
maxAge: 3600000,
265+
path: '/',
264266
},
265267
);
266268

apps/backend/src/auth/auth.service.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Inject, Injectable, Logger } from '@nestjs/common';
22
import { JwtService } from '@nestjs/jwt';
33
import axios from 'axios';
4-
import type { Request, Response } from 'express';
4+
import type { CookieOptions, Request, Response } from 'express';
5+
import ms from 'ms';
56

67
import { CreateUser } from '@nbw/database';
78
import type { UserDocument } from '@nbw/database';
@@ -22,18 +23,18 @@ export class AuthService {
2223
@Inject(JwtService)
2324
private readonly jwtService: JwtService,
2425
@Inject('COOKIE_EXPIRES_IN')
25-
private readonly COOKIE_EXPIRES_IN: string,
26+
private readonly COOKIE_EXPIRES_IN: ms.StringValue,
2627
@Inject('FRONTEND_URL')
2728
private readonly FRONTEND_URL: string,
2829

2930
@Inject('JWT_SECRET')
3031
private readonly JWT_SECRET: string,
3132
@Inject('JWT_EXPIRES_IN')
32-
private readonly JWT_EXPIRES_IN: string,
33+
private readonly JWT_EXPIRES_IN: ms.StringValue,
3334
@Inject('JWT_REFRESH_SECRET')
3435
private readonly JWT_REFRESH_SECRET: string,
3536
@Inject('JWT_REFRESH_EXPIRES_IN')
36-
private readonly JWT_REFRESH_EXPIRES_IN: string,
37+
private readonly JWT_REFRESH_EXPIRES_IN: ms.StringValue,
3738
@Inject('APP_DOMAIN')
3839
private readonly APP_DOMAIN?: string,
3940
) {}
@@ -171,11 +172,11 @@ export class AuthService {
171172

172173
public async createJwtPayload(payload: TokenPayload): Promise<Tokens> {
173174
const [accessToken, refreshToken] = await Promise.all([
174-
this.jwtService.signAsync(payload, {
175+
this.jwtService.signAsync<TokenPayload>(payload, {
175176
secret: this.JWT_SECRET,
176177
expiresIn: this.JWT_EXPIRES_IN,
177178
}),
178-
this.jwtService.signAsync(payload, {
179+
this.jwtService.signAsync<TokenPayload>(payload, {
179180
secret: this.JWT_REFRESH_SECRET,
180181
expiresIn: this.JWT_REFRESH_EXPIRES_IN,
181182
}),
@@ -189,7 +190,7 @@ export class AuthService {
189190

190191
private async GenTokenRedirect(
191192
user_registered: UserDocument,
192-
res: Response<any, Record<string, any>>,
193+
res: Response<unknown, Record<string, unknown>>,
193194
): Promise<void> {
194195
const token = await this.createJwtPayload({
195196
id: user_registered._id.toString(),
@@ -198,18 +199,16 @@ export class AuthService {
198199
});
199200

200201
const frontEndURL = this.FRONTEND_URL;
201-
const domain = this.APP_DOMAIN;
202-
const maxAge = parseInt(this.COOKIE_EXPIRES_IN) * 1000;
202+
const maxAge = ms(this.COOKIE_EXPIRES_IN) * 1000;
203203

204-
res.cookie('token', token.access_token, {
205-
domain: domain,
204+
const cookieOptions: CookieOptions = {
206205
maxAge: maxAge,
207-
});
206+
domain: this.APP_DOMAIN,
207+
path: '/',
208+
};
208209

209-
res.cookie('refresh_token', token.refresh_token, {
210-
domain: domain,
211-
maxAge: maxAge,
212-
});
210+
res.cookie('token', token.access_token, cookieOptions);
211+
res.cookie('refresh_token', token.refresh_token, cookieOptions);
213212

214213
res.redirect(frontEndURL + '/');
215214
}

apps/backend/src/auth/strategies/JWT.strategy.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('JwtStrategy', () => {
3333
it('should throw an error if JWT_SECRET is not set', () => {
3434
jest.spyOn(configService, 'getOrThrow').mockReturnValue(null);
3535

36-
expect(() => new JwtStrategy(configService)).toThrowError(
36+
expect(() => new JwtStrategy(configService)).toThrow(
3737
'JwtStrategy requires a secret or key',
3838
);
3939
});
@@ -84,7 +84,7 @@ describe('JwtStrategy', () => {
8484

8585
const payload = { userId: 'test-user-id' };
8686

87-
expect(() => jwtStrategy.validate(req, payload)).toThrowError(
87+
expect(() => jwtStrategy.validate(req, payload)).toThrow(
8888
'No refresh token',
8989
);
9090
});

apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('DiscordStrategy', () => {
4343
it('should throw an error if Discord config is missing', () => {
4444
jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null);
4545

46-
expect(() => new DiscordStrategy(configService)).toThrowError(
46+
expect(() => new DiscordStrategy(configService)).toThrow(
4747
'OAuth2Strategy requires a clientID option',
4848
);
4949
});

0 commit comments

Comments
 (0)