Skip to content

Commit 144a9eb

Browse files
committed
Merge branch 'develop' of github.com:OpenNBS/NoteBlockWorld into feature/zod-db-packages
2 parents 91c89da + f9a031e commit 144a9eb

96 files changed

Lines changed: 675 additions & 760 deletions

File tree

Some content is hidden

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

.github/workflows/tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ jobs:
2323
- name: Install bun
2424
uses: oven-sh/setup-bun@v2
2525
with:
26-
bun-version: latest
26+
# Pin to 1.3.5: Bun 1.3.10 has a regression (descriptor.value undefined with NestJS/Swagger).
27+
# Fix merged in PR #27527 (Feb 28) but not yet in a release. Revert when 1.3.11+ is latest.
28+
bun-version: '1.3.5'
2729

2830
- name: Install dependencies
2931
run: bun install

.vscode/settings.json

Lines changed: 20 additions & 0 deletions
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,10 +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"
1935
},
36+
37+
/***********************************
38+
Use the workspace version of TypeScript
39+
***********************************/
2040
"typescript.tsdk": "./node_modules/typescript/lib",
2141
"typescript.enablePromptUseWorkspaceTsdk": true
2242
}

apps/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"esm": "^3.2.25",
4949
"express": "^5.2.1",
5050
"mongoose": "^9.0.1",
51-
"multer": "2.0.2",
51+
"multer": "2.1.1",
5252
"nanoid": "^5.1.6",
5353
"passport": "^0.7.0",
5454
"passport-github": "^1.1.0",

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
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class DiscordStrategy extends PassportStrategy(strategy, 'discord') {
2727
callbackUrl: `${SERVER_URL}/v1/auth/discord/callback`,
2828
scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify],
2929
fetchScope: true,
30-
prompt: 'none',
30+
prompt: 'none' as const,
3131
};
3232

3333
super(config);

0 commit comments

Comments
 (0)