Skip to content

Commit

Permalink
feat: Added feature to search by social profile (#77)
Browse files Browse the repository at this point in the history
* added feature to search by social profile

* add linkedin profile fetching functionality

* finishing touches

* delete user controller test file

* fixed inconsistencies in e2e tests

* made search user endpoint public
  • Loading branch information
rajdip-b authored May 21, 2024
1 parent f128c7f commit 527498c
Show file tree
Hide file tree
Showing 26 changed files with 644 additions and 235 deletions.
28 changes: 19 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@ GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=

FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
FACEBOOK_CALLBACK_URL=
FACEBOOK_OAUTH_CLIENT_ID=
FACEBOOK_OAUTH_CLIENT_SECRET=
FACEBOOK_OAUTH_CALLBACK_URL=

FACEBOOK_SOCIAL_ACCOUNT_LINK_CLIENT_ID=
FACEBOOK_SOCIAL_ACCOUNT_LINK_CLIENT_SECRET=
FACEBOOK_SOCIAL_ACCOUNT_LINK_CALLBACK_URL=

APPLE_CLIENT_ID=
APPLE_CALLBACK_URL=
APPLE_TEAM_ID=
APPLE_KEY_ID=
APPLE_KEY_CONTENTS=

LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
LINKEDIN_CALLBACK_URL=
LINKEDIN_OAUTH_CLIENT_ID=
LINKEDIN_OAUTH_CLIENT_SECRET=
LINKEDIN_OAUTH_CALLBACK_URL=

LINKEDIN_SOCIAL_ACCOUNT_LINK_CLIENT_ID=
LINKEDIN_SOCIAL_ACCOUNT_LINK_CLIENT_SECRET=
LINKEDIN_SOCIAL_ACCOUNT_LINK_CALLBACK_URL=

FLY_ACCESS_TOKEN=
DATABASE_URL=postgresql://postgres:[email protected]:5432/culero
DB_STAGE_URL=
DB_PROD_URL=
JWT_SECRET=secret

SMTP_HOST=
Expand All @@ -34,4 +41,7 @@ AWS_REGION=
DOMAIN=https://culero.com/
PROFILES_DIRECTORY=

REDIS_URL=redis://localhost:6379
REDIS_URL=redis://localhost:6379

LINKEDIN_PROFILE_FETCHER_API_KEY=
LINKEDIN_PROFILE_FETCHER_HOST=
3 changes: 0 additions & 3 deletions docker-compose-test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# Set the version of docker compose to use
version: '3.9'

# The containers that compose the project
services:
db:
Expand Down
3 changes: 0 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# Set the version of docker compose to use
version: '3.9'

# The containers that compose the project
services:
db:
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@nestjs/platform-fastify": "^10.3.3",
"@nestjs/swagger": "^7.3.0",
"@prisma/client": "^5.10.2",
"@types/uuid": "^9.0.8",
"aws-sdk": "^2.1577.0",
"axios": "^1.6.8",
"cheerio": "1.0.0-rc.12",
Expand All @@ -56,7 +57,8 @@
"passport-linkedin-oauth2": "^2.0.0",
"prisma": "^5.10.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions src/auth/auth.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Test } from '@nestjs/testing';
import { AppModule } from '../app/app.module';
import { ValidationPipe } from '@nestjs/common';
import { AuthType } from '@prisma/client';
import { SHA256 } from 'crypto-js';
import { MailService } from '../mail/mail.service';
import { mockDeep } from 'jest-mock-extended';
import { MailModule } from '../mail/mail.module';
Expand Down Expand Up @@ -98,7 +97,6 @@ describe('Auth Controller Tests', () => {
await prisma.user.create({
data: {
email: '[email protected]',
password: SHA256('password').toString(),
isEmailVerified: true,
authType: AuthType.EMAIL,
},
Expand Down
20 changes: 17 additions & 3 deletions src/auth/guard/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class AuthGuard implements CanActivate {
},
});
} else {
const token = this.extractTokenFromHeader(request);
const token = this.extractToken(request);
if (!token) {
throw new ForbiddenException();
}
Expand Down Expand Up @@ -79,8 +79,22 @@ export class AuthGuard implements CanActivate {
return true;
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
private extractToken(request: Request): string | undefined {
const tokenFromHeader = request.headers.authorization;
const tokenFromParams = request.query.token;

let type: string;
let token: string;

if (tokenFromHeader) {
[type, token] = request.headers.authorization?.split(' ') ?? [];
} else if (tokenFromParams) {
type = 'Bearer';
token = tokenFromParams as string;
} else {
return undefined;
}

return type === 'Bearer' ? token : undefined;
}
}
10 changes: 10 additions & 0 deletions src/oauth/factory/apple/apple-strategy.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export class AppleOAuthStrategyFactory implements OAuthStrategyFactory {
);
}

public isSocialAccountLinkEnabled(): boolean {
return false;
}

public createOAuthStrategy<AppleStrategy>(): AppleStrategy | null {
if (this.isOAuthEnabled()) {
return new AppleStrategy(
Expand All @@ -43,4 +47,10 @@ export class AppleOAuthStrategyFactory implements OAuthStrategyFactory {
return null;
}
}

public createSocialAccountLinkStrategy<
AppleStrategy,
>(): AppleStrategy | null {
return null;
}
}
62 changes: 57 additions & 5 deletions src/oauth/factory/facebook/facebook-strategy.factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,85 @@ describe('FacebookOAuthStrategyFactory', () => {
configService = moduleRef.get<ConfigService>(ConfigService);
});

it('disable when credentials are not present', () => {
it('disable OAuth when credentials are not present', () => {
jest.spyOn(configService, 'get').mockReturnValue('');
factory = new FacebookOAuthStrategyFactory(configService);
expect(factory.isOAuthEnabled()).toBe(false);
});

it('disable social account link when credentials are not present', () => {
jest.spyOn(configService, 'get').mockReturnValue('');
factory = new FacebookOAuthStrategyFactory(configService);
expect(factory.isSocialAccountLinkEnabled()).toBe(false);
});

it('return null when OAuth disabled', () => {
const strategy = factory.createOAuthStrategy();
expect(strategy).toBeNull();
});

it('return null when social account link disabled', () => {
const strategy = factory.createSocialAccountLinkStrategy();
expect(strategy).toBeNull();
});

it('enable OAuth when credentials present', () => {
jest
.spyOn(configService, 'get')
.mockImplementation((key) =>
key === 'FACEBOOK_CLIENT_ID' ||
key === 'FACEBOOK_CLIENT_SECRET' ||
key === 'FACEBOOK_CALLBACK_URL'
key === 'FACEBOOK_OAUTH_CLIENT_ID' ||
key === 'FACEBOOK_OAUTH_CLIENT_SECRET' ||
key === 'FACEBOOK_OAUTH_CALLBACK_URL'
? 'test'
: '',
);
factory = new FacebookOAuthStrategyFactory(configService);
expect(factory.isOAuthEnabled()).toBe(true);
});

it('enable social account link when credentials present', () => {
jest
.spyOn(configService, 'get')
.mockImplementation((key) =>
key === 'FACEBOOK_SOCIAL_ACCOUNT_LINK_CLIENT_ID' ||
key === 'FACEBOOK_SOCIAL_ACCOUNT_LINK_CLIENT_SECRET' ||
key === 'FACEBOOK_SOCIAL_ACCOUNT_LINK_CALLBACK_URL'
? 'test'
: '',
);
factory = new FacebookOAuthStrategyFactory(configService);
expect(factory.isSocialAccountLinkEnabled()).toBe(true);
});

it('create OAuth strategy when enabled', () => {
const strategy = factory.createOAuthStrategy();
jest
.spyOn(configService, 'get')
.mockImplementation((key) =>
key === 'FACEBOOK_OAUTH_CLIENT_ID' ||
key === 'FACEBOOK_OAUTH_CLIENT_SECRET' ||
key === 'FACEBOOK_OAUTH_CALLBACK_URL'
? 'test'
: '',
);
const strategy = new FacebookOAuthStrategyFactory(
configService,
).createOAuthStrategy();
expect(strategy).toBeInstanceOf(FacebookStrategy);
});

it('create social account link strategy when enabled', () => {
jest
.spyOn(configService, 'get')
.mockImplementation((key) =>
key === 'FACEBOOK_SOCIAL_ACCOUNT_LINK_CLIENT_ID' ||
key === 'FACEBOOK_SOCIAL_ACCOUNT_LINK_CLIENT_SECRET' ||
key === 'FACEBOOK_SOCIAL_ACCOUNT_LINK_CALLBACK_URL'
? 'test'
: '',
);
const strategy = new FacebookOAuthStrategyFactory(
configService,
).createSocialAccountLinkStrategy();
expect(strategy).toBeInstanceOf(FacebookStrategy);
});
});
65 changes: 54 additions & 11 deletions src/oauth/factory/facebook/facebook-strategy.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,75 @@ import { FacebookStrategy } from '../../strategy/facebook/facebook.strategy';

@Injectable()
export class FacebookOAuthStrategyFactory implements OAuthStrategyFactory {
private readonly clientID: string;
private readonly clientSecret: string;
private readonly callbackURL: string;
private readonly oAuthClientID: string;
private readonly oAuthClientSecret: string;
private readonly oAuthCallbackURL: string;
private readonly socialAccountLinkClientID: string;
private readonly socialAccountLinkClientSecret: string;
private readonly socialAccountLinkCallbackURL: string;

constructor(private readonly configService: ConfigService) {
this.clientID = this.configService.get<string>('FACEBOOK_CLIENT_ID');
this.clientSecret = this.configService.get<string>(
'FACEBOOK_CLIENT_SECRET',
this.oAuthClientID = this.configService.get<string>(
'FACEBOOK_OAUTH_CLIENT_ID',
);
this.oAuthClientSecret = this.configService.get<string>(
'FACEBOOK_OAUTH_CLIENT_SECRET',
);
this.oAuthCallbackURL = this.configService.get<string>(
'FACEBOOK_OAUTH_CALLBACK_URL',
);
this.socialAccountLinkClientID = this.configService.get<string>(
'FACEBOOK_SOCIAL_ACCOUNT_LINK_CLIENT_ID',
);
this.socialAccountLinkClientSecret = this.configService.get<string>(
'FACEBOOK_SOCIAL_ACCOUNT_LINK_CLIENT_SECRET',
);
this.socialAccountLinkCallbackURL = this.configService.get<string>(
'FACEBOOK_SOCIAL_ACCOUNT_LINK_CALLBACK_URL',
);
this.callbackURL = this.configService.get<string>('FACEBOOK_CALLBACK_URL');
}

public isOAuthEnabled(): boolean {
return Boolean(this.clientID && this.clientSecret && this.callbackURL);
return Boolean(
this.oAuthClientID && this.oAuthClientSecret && this.oAuthCallbackURL,
);
}

public isSocialAccountLinkEnabled(): boolean {
return Boolean(
this.socialAccountLinkClientID &&
this.socialAccountLinkClientSecret &&
this.socialAccountLinkCallbackURL,
);
}

public createOAuthStrategy<FacebookStrategy>(): FacebookStrategy | null {
if (this.isOAuthEnabled()) {
return new FacebookStrategy(
this.clientID,
this.clientSecret,
this.callbackURL,
this.oAuthClientID,
this.oAuthClientSecret,
this.oAuthCallbackURL,
) as FacebookStrategy;
} else {
Logger.warn('Facebook Auth is not enabled in this environment.');
return null;
}
}

public createSocialAccountLinkStrategy<
FacebookStrategy,
>(): FacebookStrategy | null {
if (this.isSocialAccountLinkEnabled()) {
return new FacebookStrategy(
this.socialAccountLinkClientID,
this.socialAccountLinkClientSecret,
this.socialAccountLinkCallbackURL,
) as FacebookStrategy;
} else {
Logger.warn(
'Facebook Social Account Link is not enabled in this environment.',
);
return null;
}
}
}
10 changes: 10 additions & 0 deletions src/oauth/factory/google/google-strategy.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export class GoogleOAuthStrategyFactory implements OAuthStrategyFactory {
return Boolean(this.clientID && this.clientSecret && this.callbackURL);
}

public isSocialAccountLinkEnabled(): boolean {
return false;
}

public createOAuthStrategy<GoogleStrategy>(): GoogleStrategy | null {
if (this.isOAuthEnabled()) {
return new GoogleStrategy(
Expand All @@ -31,4 +35,10 @@ export class GoogleOAuthStrategyFactory implements OAuthStrategyFactory {
return null;
}
}

public createSocialAccountLinkStrategy<
GoogleStrategy,
>(): GoogleStrategy | null {
return null;
}
}
Loading

0 comments on commit 527498c

Please sign in to comment.