From d955f52835b80ba9878bdb70cd07244a962a7d07 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Fri, 17 Jan 2025 22:47:27 -0300 Subject: [PATCH 01/12] refactor: clean up imports and add missing line breaks in service and controller files --- server/src/song/song-webhook/song-webhook.service.spec.ts | 1 + server/src/song/song.module.ts | 4 ++-- server/src/song/song.service.spec.ts | 2 +- server/src/song/song.util.ts | 1 - server/src/user/user.controller.ts | 2 +- server/src/user/user.service.spec.ts | 1 + server/src/user/user.service.ts | 3 ++- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/server/src/song/song-webhook/song-webhook.service.spec.ts b/server/src/song/song-webhook/song-webhook.service.spec.ts index 59381f9c..d404b866 100644 --- a/server/src/song/song-webhook/song-webhook.service.spec.ts +++ b/server/src/song/song-webhook/song-webhook.service.spec.ts @@ -64,6 +64,7 @@ describe('SongWebhookService', () => { const result = await service.postSongWebhook(song); expect(result).toBe('message-id'); + expect(fetch).toHaveBeenCalledWith('http://localhost/webhook?wait=true', { method: 'POST', headers: { diff --git a/server/src/song/song.module.ts b/server/src/song/song.module.ts index 963da5ff..b15aff89 100644 --- a/server/src/song/song.module.ts +++ b/server/src/song/song.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { MongooseModule } from '@nestjs/mongoose'; import { AuthModule } from '@server/auth/auth.module'; @@ -8,10 +9,9 @@ import { UserModule } from '@server/user/user.module'; import { Song, SongSchema } from './entity/song.entity'; import { MySongsController } from './my-songs/my-songs.controller'; import { SongUploadService } from './song-upload/song-upload.service'; +import { SongWebhookService } from './song-webhook/song-webhook.service'; import { SongController } from './song.controller'; import { SongService } from './song.service'; -import { SongWebhookService } from './song-webhook/song-webhook.service'; -import { ConfigService } from '@nestjs/config'; @Module({ imports: [ diff --git a/server/src/song/song.service.spec.ts b/server/src/song/song.service.spec.ts index 0533f333..48536450 100644 --- a/server/src/song/song.service.spec.ts +++ b/server/src/song/song.service.spec.ts @@ -18,8 +18,8 @@ import { SongWithUser, } from './entity/song.entity'; import { SongUploadService } from './song-upload/song-upload.service'; -import { SongService } from './song.service'; import { SongWebhookService } from './song-webhook/song-webhook.service'; +import { SongService } from './song.service'; const mockFileService = { deleteSong: jest.fn(), diff --git a/server/src/song/song.util.ts b/server/src/song/song.util.ts index cd320434..84aada71 100644 --- a/server/src/song/song.util.ts +++ b/server/src/song/song.util.ts @@ -1,5 +1,4 @@ import { UploadConst } from '@shared/validation/song/constants'; - import { customAlphabet } from 'nanoid'; import { SongWithUser } from './entity/song.entity'; diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index 6e13d102..2f81af93 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -2,12 +2,12 @@ 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'; +import { UpdateUsernameDto } from '@shared/validation/user/dto/UpdateUsername.dto'; 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 { diff --git a/server/src/user/user.service.spec.ts b/server/src/user/user.service.spec.ts index 36ac541d..0855811d 100644 --- a/server/src/user/user.service.spec.ts +++ b/server/src/user/user.service.spec.ts @@ -332,6 +332,7 @@ describe('UserService', () => { username: 'testuser', save: jest.fn().mockReturnThis(), } as unknown as UserDocument; + const body = { username: 'newuser' }; jest.spyOn(service, 'usernameExists').mockResolvedValue(false); diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index ec3a10f8..c0f39540 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -3,9 +3,10 @@ import { InjectModel } from '@nestjs/mongoose'; import { PageQueryDTO } from '@shared/validation/common/dto/PageQuery.dto'; import { CreateUser } from '@shared/validation/user/dto/CreateUser.dto'; import { GetUser } from '@shared/validation/user/dto/GetUser.dto'; +import { UpdateUsernameDto } from '@shared/validation/user/dto/UpdateUsername.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() From f820f67021667a88b07d10889e8de1e29032416f Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Fri, 17 Jan 2025 22:47:32 -0300 Subject: [PATCH 02/12] refactor: reorder imports for better organization in app.module.ts --- server/src/app.module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 40d15cc1..ab916a76 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -2,12 +2,12 @@ import { Logger, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { MongooseModule, MongooseModuleFactoryOptions } from '@nestjs/mongoose'; -import { validate } from './config/EnvironmentVariables'; import { AuthModule } from './auth/auth.module'; +import { validate } from './config/EnvironmentVariables'; import { FileModule } from './file/file.module'; import { ParseTokenPipe } from './parseToken'; -import { SongBrowserModule } from './song-browser/song-browser.module'; import { SongModule } from './song/song.module'; +import { SongBrowserModule } from './song-browser/song-browser.module'; import { UserModule } from './user/user.module'; @Module({ From 8d700550dcaf1b6f8c88d2eaef79aa6f9e945717 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Fri, 17 Jan 2025 22:47:38 -0300 Subject: [PATCH 03/12] chore: add passport-oauth2 and related type definitions to dependencies --- pnpm-lock.yaml | 9 +++++++++ server/package.json | 3 +++ 2 files changed, 12 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac2f1551..a43b818d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: passport-local: specifier: ^1.0.0 version: 1.0.0 + passport-oauth2: + specifier: ^1.8.0 + version: 1.8.0 reflect-metadata: specifier: ^0.1.14 version: 0.1.14 @@ -169,6 +172,9 @@ importers: '@types/node': specifier: ^20.17.10 version: 20.17.10 + '@types/passport': + specifier: ^1.0.17 + version: 1.0.17 '@types/passport-github': specifier: ^1.1.12 version: 1.1.12 @@ -181,6 +187,9 @@ importers: '@types/passport-local': specifier: ^1.0.38 version: 1.0.38 + '@types/passport-oauth2': + specifier: ^1.4.17 + version: 1.4.17 '@types/supertest': specifier: ^2.0.16 version: 2.0.16 diff --git a/server/package.json b/server/package.json index e86ea453..2f078563 100644 --- a/server/package.json +++ b/server/package.json @@ -48,6 +48,7 @@ "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", + "passport-oauth2": "^1.8.0", "reflect-metadata": "^0.1.14", "rxjs": "^7.8.1", "uuid": "^9.0.1", @@ -63,10 +64,12 @@ "@types/jest": "^29.5.14", "@types/multer": "^1.4.12", "@types/node": "^20.17.10", + "@types/passport": "^1.0.17", "@types/passport-github": "^1.1.12", "@types/passport-google-oauth20": "^2.0.16", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.38", + "@types/passport-oauth2": "^1.4.17", "@types/supertest": "^2.0.16", "jest": "^29.7.0", "source-map-support": "^0.5.21", From 1be83fb6ae6b2b25cc827cd35b0a87b0689ef5a9 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 11:22:00 -0300 Subject: [PATCH 04/12] refactor: rename Discord strategy files and update import paths --- .../discord.strategy.spec.ts | 2 +- .../index.ts} | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) rename server/src/auth/strategies/{ => discord.strategy}/discord.strategy.spec.ts (97%) rename server/src/auth/strategies/{discord.strategy.ts => discord.strategy/index.ts} (76%) diff --git a/server/src/auth/strategies/discord.strategy.spec.ts b/server/src/auth/strategies/discord.strategy/discord.strategy.spec.ts similarity index 97% rename from server/src/auth/strategies/discord.strategy.spec.ts rename to server/src/auth/strategies/discord.strategy/discord.strategy.spec.ts index a4251eb8..0dbc8608 100644 --- a/server/src/auth/strategies/discord.strategy.spec.ts +++ b/server/src/auth/strategies/discord.strategy/discord.strategy.spec.ts @@ -1,7 +1,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { DiscordStrategy } from './discord.strategy'; +import { DiscordStrategy } from './index'; describe('DiscordStrategy', () => { let discordStrategy: DiscordStrategy; diff --git a/server/src/auth/strategies/discord.strategy.ts b/server/src/auth/strategies/discord.strategy/index.ts similarity index 76% rename from server/src/auth/strategies/discord.strategy.ts rename to server/src/auth/strategies/discord.strategy/index.ts index 6164b4cf..1a91f16a 100644 --- a/server/src/auth/strategies/discord.strategy.ts +++ b/server/src/auth/strategies/discord.strategy/index.ts @@ -1,7 +1,9 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; -import strategy from 'passport-discord'; + +import strategy from './Strategy'; +import { DiscordPermissionScope } from './types'; @Injectable() export class DiscordStrategy extends PassportStrategy(strategy, 'discord') { @@ -19,13 +21,15 @@ export class DiscordStrategy extends PassportStrategy(strategy, 'discord') { const SERVER_URL = configService.getOrThrow('SERVER_URL'); - super({ + const config = { clientID: DISCORD_CLIENT_ID, clientSecret: DISCORD_CLIENT_SECRET, - redirect_uri: `${SERVER_URL}/api/v1/auth/discord/callback`, - scope: ['identify', 'email'], - state: false, - }); + callbackUrl: `${SERVER_URL}/api/v1/auth/discord/callback`, + scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], + fetchScope: true, + }; + + super(config); } async validate(accessToken: string, refreshToken: string, profile: any) { From 5ed1b79c9acd745af556b5fb60712281c4f11404 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 17:42:51 -0300 Subject: [PATCH 05/12] refactor: remove unnecessary console logs from song services and utility --- server/src/song/song-upload/song-upload.service.spec.ts | 2 -- server/src/song/song-webhook/song-webhook.service.ts | 2 +- server/src/song/song.util.ts | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/song/song-upload/song-upload.service.spec.ts b/server/src/song/song-upload/song-upload.service.spec.ts index 3c8a580d..ce34d401 100644 --- a/server/src/song/song-upload/song-upload.service.spec.ts +++ b/server/src/song/song-upload/song-upload.service.spec.ts @@ -393,8 +393,6 @@ describe('SongUploadService', () => { const buffer = songTest.toArrayBuffer(); - console.log(fromArrayBuffer(buffer).length); - const song = songUploadService.getSongObject(buffer); //TODO: For some reason the song is always empty expect(song).toBeInstanceOf(Song); diff --git a/server/src/song/song-webhook/song-webhook.service.ts b/server/src/song/song-webhook/song-webhook.service.ts index d700d524..8d86d03d 100644 --- a/server/src/song/song-webhook/song-webhook.service.ts +++ b/server/src/song/song-webhook/song-webhook.service.ts @@ -50,7 +50,7 @@ export class SongWebhookService implements OnModuleInit { const data = await response.json(); - this.logger.log(`Posted webhook message for song ${song.publicId}`); + //this.logger.log(`Posted webhook message for song ${song.publicId}`); return data.id; // Discord message ID } catch (e) { this.logger.error('Error sending Discord webhook', e); diff --git a/server/src/song/song.util.ts b/server/src/song/song.util.ts index 84aada71..71013710 100644 --- a/server/src/song/song.util.ts +++ b/server/src/song/song.util.ts @@ -44,8 +44,6 @@ export function getUploadDiscordEmbed({ license, stats, }: SongWithUser) { - console.log(Number('0x' + thumbnailData.backgroundColor.replace('#', ''))); - let fieldsArray = []; if (originalAuthor) { From 5903cd0d2f39f7f1b7b987a4f6f0ef8e9bf4856f Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 17:42:55 -0300 Subject: [PATCH 06/12] feat: add Discord strategy configuration and permission scopes --- .../discord.strategy/DiscordStrategyConfig.ts | 56 ++++ .../strategies/discord.strategy/Strategy.ts | 242 ++++++++++++++++++ .../auth/strategies/discord.strategy/types.ts | 229 +++++++++++++++++ 3 files changed, 527 insertions(+) create mode 100644 server/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts create mode 100644 server/src/auth/strategies/discord.strategy/Strategy.ts create mode 100644 server/src/auth/strategies/discord.strategy/types.ts diff --git a/server/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts b/server/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts new file mode 100644 index 00000000..8bae3186 --- /dev/null +++ b/server/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts @@ -0,0 +1,56 @@ +import { + IsArray, + IsBoolean, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; +import { + StrategyOptions as OAuth2StrategyOptions, + StrategyOptionsWithRequest as OAuth2StrategyOptionsWithRequest, +} from 'passport-oauth2'; + +import { ScopeType } from './types'; + +type MergedOAuth2StrategyOptions = + | OAuth2StrategyOptions + | OAuth2StrategyOptionsWithRequest; + +type DiscordStrategyOptions = Pick< + MergedOAuth2StrategyOptions, + 'clientID' | 'clientSecret' | 'scope' +>; + +export class DiscordStrategyConfig implements DiscordStrategyOptions { + // The client ID assigned by Discord. + @IsString() + clientID: string; + + // The client secret assigned by Discord. + @IsString() + clientSecret: string; + + // The URL to which Discord will redirect the user after granting authorization. + @IsString() + callbackUrl: string; + + // An array of permission scopes to request. + @IsArray() + @IsString({ each: true }) + scope: ScopeType; + + // The delay in milliseconds between requests for the same scope. + @IsOptional() + @IsNumber() + scopeDelay?: number; + + // Whether to fetch data for the specified scope. + @IsOptional() + @IsBoolean() + fetchScope?: boolean; + + // The separator for the scope values. + @IsOptional() + @IsString() + scopeSeparator?: string; +} diff --git a/server/src/auth/strategies/discord.strategy/Strategy.ts b/server/src/auth/strategies/discord.strategy/Strategy.ts new file mode 100644 index 00000000..4c153b0c --- /dev/null +++ b/server/src/auth/strategies/discord.strategy/Strategy.ts @@ -0,0 +1,242 @@ +import { Logger } from '@nestjs/common'; +import { plainToClass } from 'class-transformer'; +import { validateOrReject } from 'class-validator'; +import { + InternalOAuthError, + Strategy as OAuth2Strategy, + StrategyOptions as OAuth2StrategyOptions, + VerifyCallback, +} from 'passport-oauth2'; + +import { DiscordStrategyConfig } from './DiscordStrategyConfig'; +import { + Profile, + ProfileConnection, + ProfileGuild, + ScopeType, + SingleScopeType, +} from './types'; + +type VerifyFunction = ( + accessToken: string, + refreshToken: string, + profile: Profile, + verified: VerifyCallback, +) => void; + +interface AuthorizationParams { + permissions?: string; + prompt?: string; + disable_guild_select?: string; + guild_id?: string; +} + +export default class Strategy extends OAuth2Strategy { + // Static properties + public static DISCORD_EPOCH = 1420070400000; + public static DISCORD_SHIFT = 1 << 22; + + public static DISCORD_API_BASE = 'https://discord.com/api'; + + private readonly logger = new Logger('DiscordStrategy'); + private scope: ScopeType; + private scopeDelay: number; + private fetchScopeEnabled: boolean; + public override name = 'discord'; + + public constructor(options: DiscordStrategyConfig, verify: VerifyFunction) { + super( + { + scopeSeparator: ' ', + ...options, + authorizationURL: 'https://discord.com/api/oauth2/authorize', + tokenURL: 'https://discord.com/api/oauth2/token', + } as OAuth2StrategyOptions, + verify, + ); + + this.validateConfig(options); + this.scope = options.scope; + this.scopeDelay = options.scopeDelay ?? 0; + this.fetchScopeEnabled = options.fetchScope ?? true; + this._oauth2.useAuthorizationHeaderforGET(true); + } + + private async validateConfig(config: DiscordStrategyConfig): Promise { + try { + const validatedConfig = plainToClass(DiscordStrategyConfig, config); + await validateOrReject(validatedConfig); + } catch (errors) { + this.logger.error(errors); + throw new Error(`Configuration validation failed: ${errors}`); + } + } + + private async makeApiRequest( + url: string, + accessToken: string, + ): Promise { + return new Promise((resolve, reject) => { + this._oauth2.get(url, accessToken, (err, body) => { + if (err) { + reject(new InternalOAuthError(`Failed to fetch from ${url}`, err)); + return; + } + + try { + resolve(JSON.parse(body as string) as T); + } catch (parseError) { + reject(new Error(`Failed to parse response from ${url}`)); + } + }); + }); + } + + private async fetchUserData(accessToken: string): Promise { + return this.makeApiRequest( + `${Strategy.DISCORD_API_BASE}/users/@me`, + accessToken, + ); + } + + public override async userProfile(accessToken: string, done: VerifyCallback) { + try { + const userData = await this.fetchUserData(accessToken); + const profile = this.buildProfile(userData, accessToken); + + if (this.fetchScopeEnabled) { + await this.enrichProfileWithScopes(profile, accessToken); + } + + done(null, profile); + } catch (error) { + this.logger.error('Failed to fetch user profile', error); + done(error); + } + } + + private async enrichProfileWithScopes( + profile: Profile, + accessToken: string, + ): Promise { + await Promise.all([ + this.fetchScopeData('connections', accessToken).then( + (data) => (profile.connections = data as ProfileConnection[]), + ), + this.fetchScopeData('guilds', accessToken).then( + (data) => (profile.guilds = data as ProfileGuild[]), + ), + ]); + + profile.fetchedAt = new Date(); + } + + private async fetchScopeData( + scope: SingleScopeType, + accessToken: string, + ): Promise { + if (!this.scope.includes(scope)) { + return null; + } + + if (this.scopeDelay > 0) { + await new Promise((resolve) => setTimeout(resolve, this.scopeDelay)); + } + + return this.makeApiRequest( + `${Strategy.DISCORD_API_BASE}/users/@me/${scope}`, + accessToken, + ); + } + + private calculateCreationDate(id: string) { + return new Date(+id / Strategy.DISCORD_SHIFT + Strategy.DISCORD_EPOCH); + } + + private buildProfile(data: Profile, accessToken: string): Profile { + const { id } = data; + return { + provider: 'discord', + id: id, + username: data.username, + displayName: data.displayName, + avatar: data.avatar, + banner: data.banner, + email: data.email, + verified: data.verified, + mfa_enabled: data.mfa_enabled, + public_flags: data.public_flags, + flags: data.flags, + locale: data.locale, + global_name: data.global_name, + premium_type: data.premium_type, + connections: data.connections, + guilds: data.guilds, + access_token: accessToken, + fetchedAt: new Date(), + createdAt: this.calculateCreationDate(id), + _raw: JSON.stringify(data), + _json: data as unknown as Record, + }; + } + + public async fetchScope( + scope: SingleScopeType, + accessToken: string, + ): Promise | null> { + if (!this.scope.includes(scope)) return null; + + await new Promise((resolve) => setTimeout(resolve, this.scopeDelay ?? 0)); + + return new Promise((resolve, reject) => { + this._oauth2.get( + `https://discord.com/api/users/@me/${scope}`, + accessToken, + (err, body) => { + if (err) { + return reject( + new InternalOAuthError( + `Failed to fetch the scope: ${scope}`, + err, + ), + ); + } + + try { + if (typeof body !== 'string') { + return reject( + new Error(`Failed to parse the returned scope data: ${scope}`), + ); + } + + const json = JSON.parse(body) as Record; + resolve(json); + } catch (err) { + this.logger.error(err); + + reject( + new Error(`Failed to parse the returned scope data: ${scope}`), + ); + } + }, + ); + }); + } + + public override authorizationParams( + options: AuthorizationParams, + ): AuthorizationParams & Record { + const params: AuthorizationParams & Record = + super.authorizationParams(options) as Record; + + const { permissions, prompt, disable_guild_select, guild_id } = options; + + if (permissions) params.permissions = permissions; + if (prompt) params.prompt = prompt; + if (guild_id) params.guild_id = guild_id; + if (disable_guild_select) + params.disable_guild_select = disable_guild_select; + + return params; + } +} diff --git a/server/src/auth/strategies/discord.strategy/types.ts b/server/src/auth/strategies/discord.strategy/types.ts new file mode 100644 index 00000000..cb276741 --- /dev/null +++ b/server/src/auth/strategies/discord.strategy/types.ts @@ -0,0 +1,229 @@ +import passport from 'passport'; + +/** + * https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes + */ +export enum DiscordPermissionScope { + ActivitiesRead = 'activities.read', + ActivitiesWrite = 'activities.write', + ApplicationBuildsRead = 'applications.builds.read', + ApplicationBuildsUpload = 'applications.builds.upload', + ApplicationsCommands = 'applications.commands', + ApplicationsCommandsUpdate = 'applications.commands.update', + ApplicationsCommandsPermissionsUpdate = 'applications.commands.permissions.update', + ApplicationsEntitlements = 'applications.entitlements', + ApplicationsStoreUpdate = 'applications.store.update', + Bot = 'bot', + Connections = 'connections', + DmRead = 'dm_channels.read', + Email = 'email', + GdmJoin = 'gdm.join', + Guilds = 'guilds', + GuildsJoin = 'guilds.join', + GuildMembersRead = 'guilds.members.read', + Identify = 'identify', + MessagesRead = 'messages.read', + RelationshipsRead = 'relationships.read', + RoleConnectionsWrite = 'role_connections.write', + Rpc = 'rpc', + RpcActivitiesUpdate = 'rpc.activities.update', + RpcNotificationsRead = 'rpc.notifications.read', + RpcVoiceRead = 'rpc.voice.read', + RpcVoiceWrite = 'rpc.voice.write', + Voice = 'voice', + WebhookIncoming = 'webhook.incoming', +} + +export type SingleScopeType = `${DiscordPermissionScope}`; + +export type ScopeType = SingleScopeType[]; + +/** + * https://discord.com/developers/docs/resources/user#user-object + */ +export interface DiscordUser { + id: string; + username: string; + global_name?: string | undefined; + avatar: string; + bot?: string | undefined; + system?: boolean | undefined; + mfa_enabled?: boolean | undefined; + banner?: string | undefined; + accent_color?: number | undefined; + locale?: string | undefined; + verified?: boolean | undefined; + email?: string | undefined; + flags?: number | undefined; + premium_type?: number | undefined; + public_flags?: number | undefined; + avatar_decoration_data?: AvatarDecorationData | undefined; +} + +export interface AvatarDecorationData { + asset: string; + sku_id: string; +} + +export interface DiscordAccount { + id: string; + name: string; +} + +export interface DiscordApplication { + id: string; + name: string; + icon?: string | undefined; + description: string; + bot?: DiscordUser; +} + +export interface DiscordIntegration { + id: string; + name: string; + type: string; + enabled: boolean; + syncing?: boolean | undefined; + role_id?: string | undefined; + enable_emoticons?: boolean | undefined; + expire_behavior?: number | undefined; + expire_grace_period?: number | undefined; + user?: DiscordUser | undefined; + account: DiscordAccount; + synced_at?: Date | undefined; + subscriber_count?: number | undefined; + revoked?: boolean | undefined; + application?: DiscordApplication | undefined; + scopes?: ScopeType | undefined; +} + +export interface ProfileConnection { + id: string; + name: string; + type: string; + revoked?: boolean | undefined; + integrations?: DiscordIntegration[] | undefined; + verified: boolean; + friend_sync: boolean; + show_activity: boolean; + two_way_link: boolean; + visibility: number; +} + +export interface DiscordRoleTag { + bot_id?: string | undefined; + integration_id?: string | undefined; + premium_subscriber?: null | undefined; + subscription_listing_id?: string | undefined; + available_for_purchase?: null | undefined; + guild_connections?: null | undefined; +} + +export interface DiscordRole { + id: string; + name: string; + color: number; + hoist: boolean; + icon?: string | undefined; + unicode_emoji?: string | undefined; + position: number; + permissions: string; + managed: boolean; + tags?: DiscordRoleTag | undefined; + flags: number; +} + +export interface DiscordEmoji { + id?: string | undefined; + name: string | undefined; + roles?: string[]; + user?: DiscordUser; + require_colons?: boolean | undefined; + managed?: boolean | undefined; + animated?: boolean | undefined; + available?: boolean | undefined; +} + +export interface DiscordWelcomeScreenChannel { + channel_id: string; + description: string; + emoji_id?: string | undefined; + emoji_name?: string | undefined; +} + +export interface DiscordWelcomeScreen { + description?: string | undefined; + welcome_channels: DiscordWelcomeScreenChannel[]; +} + +export interface DiscordSticker { + id: string; + pack_id?: string | undefined; + name: string; + description: string; + tags: string; + type: number; + format_type: number; + available?: boolean | undefined; + guild_id?: string | undefined; + user?: DiscordUser | undefined; + sort_value?: number | undefined; +} + +export interface ProfileGuild { + id: string; + name: string; + icon?: string | undefined; + icon_hash?: string | undefined; + splash?: string | undefined; + discovery_splash?: string | undefined; + owner?: boolean | string; + owner_id: string; + permissions?: string | undefined; + afk_channel_id?: string | undefined; + afk_timeout?: number | undefined; + widget_enabled: boolean | undefined; + widget_channel_id?: string | undefined; + verification_level?: number | undefined; + default_message_notifications?: number | undefined; + explicit_content_filter?: number | undefined; + roles: DiscordRole[]; + emojis: DiscordEmoji[]; + features: string[]; + mfa_level?: number | undefined; + application_id?: string | undefined; + system_channel_id?: string | undefined; + system_channel_flags?: number | undefined; + rules_channel_id?: string | undefined; + max_presences?: number | undefined; + max_members?: number | undefined; + vanity_url_code?: string | undefined; + description?: string | undefined; + banner?: string | undefined; + premium_tier?: number | undefined; + premium_subscription_count?: number | undefined; + preferred_locale?: string | undefined; + public_updates_channel_id?: string | undefined; + max_video_channel_users?: number | undefined; + max_stage_video_channel_users?: number | undefined; + approximate_member_count?: number | undefined; + approximate_presence_count?: number | undefined; + welcome_screen?: DiscordWelcomeScreen | undefined; + nsfw_level?: number | undefined; + stickers?: DiscordSticker[] | undefined; + premium_progress_bar_enabled?: boolean | undefined; + safety_alerts_channel_id?: string | undefined; +} + +export interface Profile + extends Omit, + DiscordUser { + provider: string; + connections?: ProfileConnection[] | undefined; + guilds?: ProfileGuild[] | undefined; + access_token: string; + fetchedAt: Date; + createdAt: Date; + _raw: unknown; + _json: Record; +} From 91c5574866a7494cff812edd7e24c395fb5e5f17 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 17:53:50 -0300 Subject: [PATCH 07/12] feat: add prompt option to Discord strategy configuration --- .../auth/strategies/discord.strategy/DiscordStrategyConfig.ts | 4 ++++ server/src/auth/strategies/discord.strategy/index.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/server/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts b/server/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts index 8bae3186..6146e2c9 100644 --- a/server/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts +++ b/server/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts @@ -1,6 +1,7 @@ import { IsArray, IsBoolean, + IsEnum, IsNumber, IsOptional, IsString, @@ -49,6 +50,9 @@ export class DiscordStrategyConfig implements DiscordStrategyOptions { @IsBoolean() fetchScope?: boolean; + @IsEnum(['none', 'consent']) + prompt: 'consent' | 'none'; + // The separator for the scope values. @IsOptional() @IsString() diff --git a/server/src/auth/strategies/discord.strategy/index.ts b/server/src/auth/strategies/discord.strategy/index.ts index 1a91f16a..61dc578a 100644 --- a/server/src/auth/strategies/discord.strategy/index.ts +++ b/server/src/auth/strategies/discord.strategy/index.ts @@ -27,6 +27,7 @@ export class DiscordStrategy extends PassportStrategy(strategy, 'discord') { callbackUrl: `${SERVER_URL}/api/v1/auth/discord/callback`, scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], fetchScope: true, + prompt: 'none', }; super(config); From 5c2cade432d91d4262818e04a3592d0401cd4f0e Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 21:31:32 -0300 Subject: [PATCH 08/12] feat: enhance Discord strategy to support prompt option and improve scope fetching --- .../strategies/discord.strategy/Strategy.ts | 113 ++++++++++-------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/server/src/auth/strategies/discord.strategy/Strategy.ts b/server/src/auth/strategies/discord.strategy/Strategy.ts index 4c153b0c..d0c16949 100644 --- a/server/src/auth/strategies/discord.strategy/Strategy.ts +++ b/server/src/auth/strategies/discord.strategy/Strategy.ts @@ -6,6 +6,7 @@ import { Strategy as OAuth2Strategy, StrategyOptions as OAuth2StrategyOptions, VerifyCallback, + VerifyFunction, } from 'passport-oauth2'; import { DiscordStrategyConfig } from './DiscordStrategyConfig'; @@ -17,18 +18,8 @@ import { SingleScopeType, } from './types'; -type VerifyFunction = ( - accessToken: string, - refreshToken: string, - profile: Profile, - verified: VerifyCallback, -) => void; - interface AuthorizationParams { - permissions?: string; prompt?: string; - disable_guild_select?: string; - guild_id?: string; } export default class Strategy extends OAuth2Strategy { @@ -43,7 +34,7 @@ export default class Strategy extends OAuth2Strategy { private scopeDelay: number; private fetchScopeEnabled: boolean; public override name = 'discord'; - + prompt?: string; public constructor(options: DiscordStrategyConfig, verify: VerifyFunction) { super( { @@ -60,6 +51,7 @@ export default class Strategy extends OAuth2Strategy { this.scopeDelay = options.scopeDelay ?? 0; this.fetchScopeEnabled = options.fetchScope ?? true; this._oauth2.useAuthorizationHeaderforGET(true); + this.prompt = options.prompt; } private async validateConfig(config: DiscordStrategyConfig): Promise { @@ -180,47 +172,68 @@ export default class Strategy extends OAuth2Strategy { }; } - public async fetchScope( + public fetchScope( scope: SingleScopeType, accessToken: string, - ): Promise | null> { - if (!this.scope.includes(scope)) return null; + callback: (err: Error | null, data: Record | null) => void, + ): void { + // Early return if scope is not included + if (!this.scope.includes(scope)) { + callback(null, null); + return; + } - await new Promise((resolve) => setTimeout(resolve, this.scopeDelay ?? 0)); + // Handle scope delay + const delayPromise = new Promise((resolve) => + setTimeout(resolve, this.scopeDelay ?? 0), + ); - return new Promise((resolve, reject) => { - this._oauth2.get( - `https://discord.com/api/users/@me/${scope}`, - accessToken, - (err, body) => { - if (err) { - return reject( - new InternalOAuthError( - `Failed to fetch the scope: ${scope}`, - err, - ), - ); - } - - try { - if (typeof body !== 'string') { - return reject( - new Error(`Failed to parse the returned scope data: ${scope}`), + delayPromise + .then(() => { + this._oauth2.get( + `${Strategy.DISCORD_API_BASE}/users/@me/${scope}`, + accessToken, + (err, body) => { + if (err) { + this.logger.error(`Failed to fetch scope ${scope}:`, err); + + callback( + new InternalOAuthError(`Failed to fetch scope: ${scope}`, err), + null, ); + + return; } - const json = JSON.parse(body) as Record; - resolve(json); - } catch (err) { - this.logger.error(err); - - reject( - new Error(`Failed to parse the returned scope data: ${scope}`), - ); - } - }, - ); - }); + try { + if (typeof body !== 'string') { + const error = new Error( + `Invalid response type for scope: ${scope}`, + ); + + this.logger.error(error.message); + callback(error, null); + return; + } + + const json = JSON.parse(body) as Record; + callback(null, json); + } catch (parseError) { + const error = + parseError instanceof Error + ? parseError + : new Error(`Failed to parse scope data: ${scope}`); + + this.logger.error('Parse error:', error); + callback(error, null); + } + }, + ); + }) + .catch((error) => { + this.logger.error('Unexpected error:', error); + callback(error, null); + }); } public override authorizationParams( @@ -229,14 +242,12 @@ export default class Strategy extends OAuth2Strategy { const params: AuthorizationParams & Record = super.authorizationParams(options) as Record; - const { permissions, prompt, disable_guild_select, guild_id } = options; - - if (permissions) params.permissions = permissions; + const { prompt } = this; if (prompt) params.prompt = prompt; - if (guild_id) params.guild_id = guild_id; - if (disable_guild_select) - params.disable_guild_select = disable_guild_select; + console.log('Authorization Params'); + console.log('params', params); + console.log('options', options); return params; } } From 10d78e3d835cb6f13e0600bc2944c8689c7e1290 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 21:36:20 -0300 Subject: [PATCH 09/12] test: add unit tests for DiscordStrategy functionality and configuration validation --- .../discord.strategy/Strategy.spec.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 server/src/auth/strategies/discord.strategy/Strategy.spec.ts diff --git a/server/src/auth/strategies/discord.strategy/Strategy.spec.ts b/server/src/auth/strategies/discord.strategy/Strategy.spec.ts new file mode 100644 index 00000000..5f9831d4 --- /dev/null +++ b/server/src/auth/strategies/discord.strategy/Strategy.spec.ts @@ -0,0 +1,160 @@ +import DiscordStrategy from './Strategy'; +import { DiscordStrategyConfig } from './DiscordStrategyConfig'; +import { VerifyFunction } from 'passport-oauth2'; +import { DiscordPermissionScope, Profile } from './types'; + +describe('DiscordStrategy', () => { + let strategy: DiscordStrategy; + const verify: VerifyFunction = jest.fn(); + + beforeEach(() => { + const config: DiscordStrategyConfig = { + clientID: 'test-client-id', + clientSecret: 'test-client-secret', + callbackUrl: 'http://localhost:3000/callback', + scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], + prompt: 'consent', + }; + + strategy = new DiscordStrategy(config, verify); + }); + + it('should be defined', () => { + expect(strategy).toBeDefined(); + }); + + it('should have the correct name', () => { + expect(strategy.name).toBe('discord'); + }); + + it('should validate config', async () => { + const config: DiscordStrategyConfig = { + clientID: 'test-client-id', + clientSecret: 'test-client-secret', + callbackUrl: 'http://localhost:3000/callback', + scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], + prompt: 'consent', + }; + + await expect(strategy['validateConfig'](config)).resolves.toBeUndefined(); + }); + + it('should make API request', async () => { + const mockGet = jest.fn((url, accessToken, callback) => { + callback(null, JSON.stringify({ id: '123' })); + }); + + strategy['_oauth2'].get = mockGet; + + const result = await strategy['makeApiRequest']<{ id: string }>( + 'https://discord.com/api/users/@me', + 'test-access-token', + ); + + expect(result).toEqual({ id: '123' }); + }); + + it('should fetch user data', async () => { + const mockMakeApiRequest = jest.fn().mockResolvedValue({ id: '123' }); + strategy['makeApiRequest'] = mockMakeApiRequest; + + const result = await strategy['fetchUserData']('test-access-token'); + + expect(result).toEqual({ id: '123' }); + }); + + it('should build profile', () => { + const profileData = { + id: '123', + username: 'testuser', + displayName: 'Test User', + avatar: 'avatar.png', + banner: 'banner.png', + email: 'test@example.com', + verified: true, + mfa_enabled: true, + public_flags: 1, + flags: 1, + locale: 'en-US', + global_name: 'testuser#1234', + premium_type: 1, + connections: [], + guilds: [], + } as unknown as Profile; + + const profile = strategy['buildProfile'](profileData, 'test-access-token'); + + expect(profile).toMatchObject({ + provider: 'discord', + id: '123', + username: 'testuser', + displayName: 'Test User', + avatar: 'avatar.png', + banner: 'banner.png', + email: 'test@example.com', + verified: true, + mfa_enabled: true, + public_flags: 1, + flags: 1, + locale: 'en-US', + global_name: 'testuser#1234', + premium_type: 1, + connections: [], + guilds: [], + access_token: 'test-access-token', + fetchedAt: expect.any(Date), + createdAt: expect.any(Date), + _raw: JSON.stringify(profileData), + _json: profileData, + }); + }); + + it('should fetch scope data', async () => { + const mockMakeApiRequest = jest.fn().mockResolvedValue([{ id: '123' }]); + strategy['makeApiRequest'] = mockMakeApiRequest; + + const result = await strategy['fetchScopeData']( + 'connections', + 'test-access-token', + ); + + expect(result).toEqual([{ id: '123' }]); + }); + + it('should enrich profile with scopes', async () => { + const profile = { + id: '123', + connections: [], + guilds: [], + } as unknown as Profile; + + const mockFetchScopeData = jest + .fn() + .mockResolvedValueOnce([{ id: 'connection1' }]) + .mockResolvedValueOnce([{ id: 'guild1' }]); + + strategy['fetchScopeData'] = mockFetchScopeData; + + await strategy['enrichProfileWithScopes'](profile, 'test-access-token'); + + expect(profile.connections).toEqual([{ id: 'connection1' }]); + expect(profile.guilds).toEqual([{ id: 'guild1' }]); + expect(profile.fetchedAt).toBeInstanceOf(Date); + }); + + it('should calculate creation date', () => { + const id = '123456789012345678'; + const date = strategy['calculateCreationDate'](id); + + expect(date).toBeInstanceOf(Date); + }); + + it('should return authorization params', () => { + const options = { prompt: 'consent' }; + const params = strategy.authorizationParams(options); + + expect(params).toMatchObject({ + prompt: 'consent', + }); + }); +}); From ebf214bef5d254615e01fac4457033c5d3cf956d Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 21:38:41 -0300 Subject: [PATCH 10/12] test: add unit tests for fetching scope data and handling out of scope requests in DiscordStrategy --- .../discord.strategy/Strategy.spec.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/server/src/auth/strategies/discord.strategy/Strategy.spec.ts b/server/src/auth/strategies/discord.strategy/Strategy.spec.ts index 5f9831d4..6b0de56a 100644 --- a/server/src/auth/strategies/discord.strategy/Strategy.spec.ts +++ b/server/src/auth/strategies/discord.strategy/Strategy.spec.ts @@ -12,7 +12,12 @@ describe('DiscordStrategy', () => { clientID: 'test-client-id', clientSecret: 'test-client-secret', callbackUrl: 'http://localhost:3000/callback', - scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], + scope: [ + DiscordPermissionScope.Email, + DiscordPermissionScope.Identify, + DiscordPermissionScope.Connections, + // DiscordPermissionScope.Bot, // Not allowed scope + ], prompt: 'consent', }; @@ -114,13 +119,25 @@ describe('DiscordStrategy', () => { strategy['makeApiRequest'] = mockMakeApiRequest; const result = await strategy['fetchScopeData']( - 'connections', + DiscordPermissionScope.Connections, 'test-access-token', ); expect(result).toEqual([{ id: '123' }]); }); + it('should no fetch out of scope data', async () => { + const mockMakeApiRequest = jest.fn().mockResolvedValue([{ id: '123' }]); + strategy['makeApiRequest'] = mockMakeApiRequest; + + const result = await strategy['fetchScopeData']( + DiscordPermissionScope.Bot, + 'test-access-token', + ); + + expect(result).toEqual(null); + }); + it('should enrich profile with scopes', async () => { const profile = { id: '123', From 723402dedb9119506382d5b2c0c7f99fe78b12ea Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 21:43:34 -0300 Subject: [PATCH 11/12] refactor: reorganize imports in DiscordStrategy and song-upload service tests --- .../src/auth/strategies/discord.strategy/Strategy.spec.ts | 5 +++-- server/src/song/song-upload/song-upload.service.spec.ts | 8 +------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/server/src/auth/strategies/discord.strategy/Strategy.spec.ts b/server/src/auth/strategies/discord.strategy/Strategy.spec.ts index 6b0de56a..e075f778 100644 --- a/server/src/auth/strategies/discord.strategy/Strategy.spec.ts +++ b/server/src/auth/strategies/discord.strategy/Strategy.spec.ts @@ -1,6 +1,7 @@ -import DiscordStrategy from './Strategy'; -import { DiscordStrategyConfig } from './DiscordStrategyConfig'; import { VerifyFunction } from 'passport-oauth2'; + +import { DiscordStrategyConfig } from './DiscordStrategyConfig'; +import DiscordStrategy from './Strategy'; import { DiscordPermissionScope, Profile } from './types'; describe('DiscordStrategy', () => { diff --git a/server/src/song/song-upload/song-upload.service.spec.ts b/server/src/song/song-upload/song-upload.service.spec.ts index ce34d401..47c425ac 100644 --- a/server/src/song/song-upload/song-upload.service.spec.ts +++ b/server/src/song/song-upload/song-upload.service.spec.ts @@ -1,10 +1,4 @@ -import { - Instrument, - Layer, - Note, - Song, - fromArrayBuffer, -} from '@encode42/nbs.js'; +import { Instrument, Layer, Note, Song } from '@encode42/nbs.js'; import { HttpException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ThumbnailData } from '@shared/validation/song/dto/ThumbnailData.dto'; From e10b4625dbcf403a9a0dab496d536e3ed5b967f6 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 18 Jan 2025 21:49:57 -0300 Subject: [PATCH 12/12] chore: remove unused passport-discord dependency from package.json and pnpm-lock.yaml --- pnpm-lock.yaml | 22 ---------------------- server/package.json | 2 -- 2 files changed, 24 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a43b818d..d0b124b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,9 +78,6 @@ importers: '@nestjs/swagger': specifier: ^7.4.2 version: 7.4.2(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.1.14)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14) - '@types/passport-discord': - specifier: ^0.1.14 - version: 0.1.14 '@types/uuid': specifier: ^9.0.8 version: 9.0.8 @@ -114,9 +111,6 @@ importers: passport: specifier: ^0.7.0 version: 0.7.0 - passport-discord: - specifier: ^0.1.4 - version: 0.1.4 passport-github: specifier: ^1.1.0 version: 1.1.0 @@ -2152,9 +2146,6 @@ packages: '@types/oauth@0.9.6': resolution: {integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==} - '@types/passport-discord@0.1.14': - resolution: {integrity: sha512-JE7Wbtr4bqqV9poWAbwB+aeVkd3/TM6wgGTtn4Ym6KPLJlJju73BEIH3uS+EeR+D7tY3lP1MtUpJPbxC86PXzA==} - '@types/passport-github@1.1.12': resolution: {integrity: sha512-VJpMEIH+cOoXB694QgcxuvWy2wPd1Oq3gqrg2Y9DMVBYs9TmH9L14qnqPDZsNMZKBDH+SvqRsGZj9SgHYeDgcA==} @@ -5016,9 +5007,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - passport-discord@0.1.4: - resolution: {integrity: sha512-VJWPYqSOmh7SaCLw/C+k1ZqCzJnn2frrmQRx1YrcPJ3MQ+Oa31XclbbmqFICSvl8xv3Fqd6YWQ4H4p1MpIN9rA==} - passport-github@1.1.0: resolution: {integrity: sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==} engines: {node: '>= 0.4.0'} @@ -8739,12 +8727,6 @@ snapshots: dependencies: '@types/node': 20.17.10 - '@types/passport-discord@0.1.14': - dependencies: - '@types/express': 4.17.21 - '@types/passport': 1.0.17 - '@types/passport-oauth2': 1.4.17 - '@types/passport-github@1.1.12': dependencies: '@types/express': 4.17.21 @@ -12456,10 +12438,6 @@ snapshots: parseurl@1.3.3: {} - passport-discord@0.1.4: - dependencies: - passport-oauth2: 1.8.0 - passport-github@1.1.0: dependencies: passport-oauth2: 1.8.0 diff --git a/server/package.json b/server/package.json index 2f078563..0f24d493 100644 --- a/server/package.json +++ b/server/package.json @@ -31,7 +31,6 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.15", "@nestjs/swagger": "^7.4.2", - "@types/passport-discord": "^0.1.14", "@types/uuid": "^9.0.8", "axios": "^1.7.9", "bcryptjs": "^2.4.3", @@ -43,7 +42,6 @@ "multer": "1.4.5-lts.1", "nanoid": "^3.3.8", "passport": "^0.7.0", - "passport-discord": "^0.1.4", "passport-github": "^1.1.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1",