Skip to content

Commit bbfd4b1

Browse files
committed
Merge branch 'develop' of https://github.com/OpenNBS/NoteBlockWorld into feature/login-by-email
2 parents 33c5d33 + 3acde54 commit bbfd4b1

File tree

16 files changed

+892
-76
lines changed

16 files changed

+892
-76
lines changed

pnpm-lock.yaml

Lines changed: 9 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
"@nestjs/platform-express": "^10.4.15",
3434
"@nestjs/swagger": "^7.4.2",
3535
"@nestjs/throttler": "^6.3.0",
36-
"@types/passport-discord": "^0.1.14",
3736
"@types/uuid": "^9.0.8",
3837
"axios": "^1.7.9",
3938
"bcryptjs": "^2.4.3",
@@ -45,12 +44,12 @@
4544
"multer": "1.4.5-lts.1",
4645
"nanoid": "^3.3.8",
4746
"passport": "^0.7.0",
48-
"passport-discord": "^0.1.4",
4947
"passport-github": "^1.1.0",
5048
"passport-google-oauth20": "^2.0.0",
5149
"passport-jwt": "^4.0.1",
5250
"passport-local": "^1.0.0",
5351
"passport-magic-login": "^1.2.2",
52+
"passport-oauth2": "^1.8.0",
5453
"reflect-metadata": "^0.1.14",
5554
"rxjs": "^7.8.1",
5655
"uuid": "^9.0.1",
@@ -67,10 +66,12 @@
6766
"@types/jest": "^29.5.14",
6867
"@types/multer": "^1.4.12",
6968
"@types/node": "^20.17.10",
69+
"@types/passport": "^1.0.17",
7070
"@types/passport-github": "^1.1.12",
7171
"@types/passport-google-oauth20": "^2.0.16",
7272
"@types/passport-jwt": "^4.0.1",
7373
"@types/passport-local": "^1.0.38",
74+
"@types/passport-oauth2": "^1.4.17",
7475
"@types/supertest": "^2.0.16",
7576
"jest": "^29.7.0",
7677
"source-map-support": "^0.5.21",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {
2+
IsArray,
3+
IsBoolean,
4+
IsEnum,
5+
IsNumber,
6+
IsOptional,
7+
IsString,
8+
} from 'class-validator';
9+
import {
10+
StrategyOptions as OAuth2StrategyOptions,
11+
StrategyOptionsWithRequest as OAuth2StrategyOptionsWithRequest,
12+
} from 'passport-oauth2';
13+
14+
import { ScopeType } from './types';
15+
16+
type MergedOAuth2StrategyOptions =
17+
| OAuth2StrategyOptions
18+
| OAuth2StrategyOptionsWithRequest;
19+
20+
type DiscordStrategyOptions = Pick<
21+
MergedOAuth2StrategyOptions,
22+
'clientID' | 'clientSecret' | 'scope'
23+
>;
24+
25+
export class DiscordStrategyConfig implements DiscordStrategyOptions {
26+
// The client ID assigned by Discord.
27+
@IsString()
28+
clientID: string;
29+
30+
// The client secret assigned by Discord.
31+
@IsString()
32+
clientSecret: string;
33+
34+
// The URL to which Discord will redirect the user after granting authorization.
35+
@IsString()
36+
callbackUrl: string;
37+
38+
// An array of permission scopes to request.
39+
@IsArray()
40+
@IsString({ each: true })
41+
scope: ScopeType;
42+
43+
// The delay in milliseconds between requests for the same scope.
44+
@IsOptional()
45+
@IsNumber()
46+
scopeDelay?: number;
47+
48+
// Whether to fetch data for the specified scope.
49+
@IsOptional()
50+
@IsBoolean()
51+
fetchScope?: boolean;
52+
53+
@IsEnum(['none', 'consent'])
54+
prompt: 'consent' | 'none';
55+
56+
// The separator for the scope values.
57+
@IsOptional()
58+
@IsString()
59+
scopeSeparator?: string;
60+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { VerifyFunction } from 'passport-oauth2';
2+
3+
import { DiscordStrategyConfig } from './DiscordStrategyConfig';
4+
import DiscordStrategy from './Strategy';
5+
import { DiscordPermissionScope, Profile } from './types';
6+
7+
describe('DiscordStrategy', () => {
8+
let strategy: DiscordStrategy;
9+
const verify: VerifyFunction = jest.fn();
10+
11+
beforeEach(() => {
12+
const config: DiscordStrategyConfig = {
13+
clientID: 'test-client-id',
14+
clientSecret: 'test-client-secret',
15+
callbackUrl: 'http://localhost:3000/callback',
16+
scope: [
17+
DiscordPermissionScope.Email,
18+
DiscordPermissionScope.Identify,
19+
DiscordPermissionScope.Connections,
20+
// DiscordPermissionScope.Bot, // Not allowed scope
21+
],
22+
prompt: 'consent',
23+
};
24+
25+
strategy = new DiscordStrategy(config, verify);
26+
});
27+
28+
it('should be defined', () => {
29+
expect(strategy).toBeDefined();
30+
});
31+
32+
it('should have the correct name', () => {
33+
expect(strategy.name).toBe('discord');
34+
});
35+
36+
it('should validate config', async () => {
37+
const config: DiscordStrategyConfig = {
38+
clientID: 'test-client-id',
39+
clientSecret: 'test-client-secret',
40+
callbackUrl: 'http://localhost:3000/callback',
41+
scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify],
42+
prompt: 'consent',
43+
};
44+
45+
await expect(strategy['validateConfig'](config)).resolves.toBeUndefined();
46+
});
47+
48+
it('should make API request', async () => {
49+
const mockGet = jest.fn((url, accessToken, callback) => {
50+
callback(null, JSON.stringify({ id: '123' }));
51+
});
52+
53+
strategy['_oauth2'].get = mockGet;
54+
55+
const result = await strategy['makeApiRequest']<{ id: string }>(
56+
'https://discord.com/api/users/@me',
57+
'test-access-token',
58+
);
59+
60+
expect(result).toEqual({ id: '123' });
61+
});
62+
63+
it('should fetch user data', async () => {
64+
const mockMakeApiRequest = jest.fn().mockResolvedValue({ id: '123' });
65+
strategy['makeApiRequest'] = mockMakeApiRequest;
66+
67+
const result = await strategy['fetchUserData']('test-access-token');
68+
69+
expect(result).toEqual({ id: '123' });
70+
});
71+
72+
it('should build profile', () => {
73+
const profileData = {
74+
id: '123',
75+
username: 'testuser',
76+
displayName: 'Test User',
77+
avatar: 'avatar.png',
78+
banner: 'banner.png',
79+
80+
verified: true,
81+
mfa_enabled: true,
82+
public_flags: 1,
83+
flags: 1,
84+
locale: 'en-US',
85+
global_name: 'testuser#1234',
86+
premium_type: 1,
87+
connections: [],
88+
guilds: [],
89+
} as unknown as Profile;
90+
91+
const profile = strategy['buildProfile'](profileData, 'test-access-token');
92+
93+
expect(profile).toMatchObject({
94+
provider: 'discord',
95+
id: '123',
96+
username: 'testuser',
97+
displayName: 'Test User',
98+
avatar: 'avatar.png',
99+
banner: 'banner.png',
100+
101+
verified: true,
102+
mfa_enabled: true,
103+
public_flags: 1,
104+
flags: 1,
105+
locale: 'en-US',
106+
global_name: 'testuser#1234',
107+
premium_type: 1,
108+
connections: [],
109+
guilds: [],
110+
access_token: 'test-access-token',
111+
fetchedAt: expect.any(Date),
112+
createdAt: expect.any(Date),
113+
_raw: JSON.stringify(profileData),
114+
_json: profileData,
115+
});
116+
});
117+
118+
it('should fetch scope data', async () => {
119+
const mockMakeApiRequest = jest.fn().mockResolvedValue([{ id: '123' }]);
120+
strategy['makeApiRequest'] = mockMakeApiRequest;
121+
122+
const result = await strategy['fetchScopeData'](
123+
DiscordPermissionScope.Connections,
124+
'test-access-token',
125+
);
126+
127+
expect(result).toEqual([{ id: '123' }]);
128+
});
129+
130+
it('should no fetch out of scope data', async () => {
131+
const mockMakeApiRequest = jest.fn().mockResolvedValue([{ id: '123' }]);
132+
strategy['makeApiRequest'] = mockMakeApiRequest;
133+
134+
const result = await strategy['fetchScopeData'](
135+
DiscordPermissionScope.Bot,
136+
'test-access-token',
137+
);
138+
139+
expect(result).toEqual(null);
140+
});
141+
142+
it('should enrich profile with scopes', async () => {
143+
const profile = {
144+
id: '123',
145+
connections: [],
146+
guilds: [],
147+
} as unknown as Profile;
148+
149+
const mockFetchScopeData = jest
150+
.fn()
151+
.mockResolvedValueOnce([{ id: 'connection1' }])
152+
.mockResolvedValueOnce([{ id: 'guild1' }]);
153+
154+
strategy['fetchScopeData'] = mockFetchScopeData;
155+
156+
await strategy['enrichProfileWithScopes'](profile, 'test-access-token');
157+
158+
expect(profile.connections).toEqual([{ id: 'connection1' }]);
159+
expect(profile.guilds).toEqual([{ id: 'guild1' }]);
160+
expect(profile.fetchedAt).toBeInstanceOf(Date);
161+
});
162+
163+
it('should calculate creation date', () => {
164+
const id = '123456789012345678';
165+
const date = strategy['calculateCreationDate'](id);
166+
167+
expect(date).toBeInstanceOf(Date);
168+
});
169+
170+
it('should return authorization params', () => {
171+
const options = { prompt: 'consent' };
172+
const params = strategy.authorizationParams(options);
173+
174+
expect(params).toMatchObject({
175+
prompt: 'consent',
176+
});
177+
});
178+
});

0 commit comments

Comments
 (0)