Skip to content

Commit

Permalink
feat: Connect social accounts (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
adelinaenache authored Jun 7, 2024
1 parent 3a48f65 commit 49a74bf
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 38 deletions.
130 changes: 117 additions & 13 deletions src/auth/controller/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
import { userProperties } from '../../schemas/user.properties';
import { LowercasePipe } from '../../common/pipes/lowercase.pipe';
import { GithubOAuthStrategyFactory } from '../../oauth/factory/github/github-strategy.factory';
import { CurrentUser } from '../../decorators/current-user.decorator';
import { SocialAccountType, User } from '@prisma/client';

@Controller('auth')
@ApiTags('Auth Controller')
Expand Down Expand Up @@ -96,17 +98,50 @@ export class AuthController {
res.status(302).redirect('/api/auth/facebook/callback');
}

@Public()
@Get('facebook/connect')
@ApiOperation({
summary: 'Facebook Social Account connect',
description: 'Connect account with Facebook profile',
})
async facebookSocialConnect(@Res() res, @Query() query, @Req() req) {
if (!this.facebookOAuthStrategyFactory.isOAuthEnabled()) {
throw new HttpException(
'Facebook Auth is not enabled in this environment.',
HttpStatus.BAD_REQUEST,
);
}

req.session.app_url = query.app_url;

const token = query.token;
const user = await this.authService.getUserFromToken(token);
req.session.intent = { facebook: 'connect' };
req.session.userId = user.id;

res.status(302).redirect('/api/auth/facebook/callback');
}

@Public()
@Get('facebook/callback')
@UseGuards(AuthGuard('facebook'))
async facebookOAuthCallback(@Req() req, @Res() res) {
const user = await this.authService.handleFacebookOAuthLogin(req);

const host = req.session.app_url;

res.send(
`<script>window.location.replace("${host}?token=${user.token}")</script>`,
);
if (req.session.intent?.facebook == 'connect') {
await this.authService.connectSocialPlatform(
SocialAccountType.FACEBOOK,
req.session.userId,
req,
);

res.send(`<script>window.location.replace("${host}")</script>`);
} else {
const user = await this.authService.handleFacebookOAuthLogin(req);
res.send(
`<script>window.location.replace("${host}?token=${user.token}")</script>`,
);
}
}

@Public()
Expand All @@ -127,16 +162,46 @@ export class AuthController {
res.status(302).redirect('/api/auth/linkedin/callback');
}

@Public()
@Get('linkedin/connect')
@ApiOperation({
summary: 'LinkedIn Social Account connect',
description: 'Sign in or sign up with LinkedIn',
})
async linkedinSocialConnect(@Res() res, @Query() query, @Req() req) {
if (!this.linkedinOAuthStrategyFactory.isOAuthEnabled()) {
throw new HttpException(
'LinkedIn Auth is not enabled in this environment.',
HttpStatus.BAD_REQUEST,
);
}
const token = query.token;
const user = await this.authService.getUserFromToken(token);
req.session.app_url = query.app_url;
req.session.intent = { linkedin: 'connect' };
req.session.userId = user.id;
res.status(302).redirect('/api/auth/linkedin/callback');
}

@Public()
@Get('linkedin/callback')
@UseGuards(AuthGuard('linkedin'))
async linkedinOAuthCallback(@Req() req, @Res() res) {
const user = await this.authService.handleLinkedInOAuthLogin(req);
const host = req.session.app_url;

res.send(
`<script>window.location.replace("${host}?token=${user.token}")</script>`,
);
if (req.session.intent?.linkedin == 'connect') {
await this.authService.connectSocialPlatform(
SocialAccountType.LINKEDIN,
req.session.userId,
req,
);
res.send(`<script>window.location.replace("${host}")</script>`);
} else {
const user = await this.authService.handleLinkedInOAuthLogin(req);
res.send(
`<script>window.location.replace("${host}?token=${user.token}")</script>`,
);
}
}

@Public()
Expand Down Expand Up @@ -189,16 +254,50 @@ export class AuthController {
res.status(302).redirect('/api/auth/github/callback');
}

@Public()
@Get('github/connect')
@ApiOperation({
summary: 'Github Social Account connect',
description: 'Connect account with Github profile',
})
async githubSocialConnect(@Res() res, @Query() query, @Req() req) {
if (!this.githubOAuthStrategyFactory.isOAuthEnabled()) {
throw new HttpException(
'Github Auth is not enabled in this environment.',
HttpStatus.BAD_REQUEST,
);
}

req.session.app_url = query.app_url;

const token = query.token;
const user = await this.authService.getUserFromToken(token);
req.session.intent = { github: 'connect' };
req.session.userId = user.id;

res.status(302).redirect('/api/auth/github/callback');
}

@Public()
@Get('github/callback')
@UseGuards(AuthGuard('github'))
async githubOAuthCallback(@Req() req, @Res() res) {
const user = await this.authService.handleGithubOAuthLogin(req);
const host = req.session.app_url;

res.send(
`<script>window.location.replace("${host}?token=${user.token}")</script>`,
);
if (req.session.intent?.github == 'connect') {
await this.authService.connectSocialPlatform(
SocialAccountType.GITHUB,
req.session.userId,
req,
);

res.send(`<script>window.location.replace("${host}")</script>`);
} else {
const user = await this.authService.handleGithubOAuthLogin(req);
res.send(
`<script>window.location.replace("${host}?token=${user.token}")</script>`,
);
}
}

@Public()
Expand Down Expand Up @@ -287,4 +386,9 @@ export class AuthController {
async verifyEmail(@Body() dto: EmailVerificationDto) {
return await this.authService.verifyEmail(dto.email, dto.code);
}

@Get('/social-accounts')
async getSocialAccounts(@CurrentUser() user: User) {
return this.authService.getSocialAccounts(user.id);
}
}
156 changes: 136 additions & 20 deletions src/auth/service/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { JwtService } from '@nestjs/jwt';
import { AuthType, User } from '@prisma/client';
import { AuthType, SocialAccountType, User } from '@prisma/client';
import { SignupDto } from '../dto/signup.dto';
import { MailService } from '../../mail/mail.service';
import { SigninDto } from '../dto/signin.dto';
Expand Down Expand Up @@ -77,13 +78,34 @@ export class AuthService {
const displayName = name.givenName + ' ' + name.familyName;
const profilePictureUrl = photos[0].value;

const user = await this.createUserIfNotExists(
email,
AuthType.FACEBOOK,
displayName,
profilePictureUrl,
false,
);
const socialAccount = await this.prisma.socialAccount.findFirst({
where: {
socialId: req.user.socialId,
platform: SocialAccountType.FACEBOOK,
},
include: {
user: true,
},
});

let user;

if (socialAccount) {
user = socialAccount.user;
} else {
user = await this.createUserIfNotExists(
email,
AuthType.FACEBOOK,
displayName,
profilePictureUrl,
false,
);
await this.connectSocialPlatform(
SocialAccountType.FACEBOOK,
user.id,
req,
);
}

const token = await this.generateToken(user);

Expand All @@ -99,12 +121,34 @@ export class AuthService {
const displayName = name.givenName + ' ' + name.familyName;
const profilePictureUrl = photos[0].value;

const user = await this.createUserIfNotExists(
email,
AuthType.LINKEDIN,
displayName,
profilePictureUrl,
);
const socialAccount = await this.prisma.socialAccount.findFirst({
where: {
socialId: req.user.socialId,
platform: SocialAccountType.LINKEDIN,
},
include: {
user: true,
},
});

let user;

if (socialAccount) {
user = socialAccount.user;
} else {
user = await this.createUserIfNotExists(
email,
AuthType.LINKEDIN,
displayName,
profilePictureUrl,
false,
);
await this.connectSocialPlatform(
SocialAccountType.LINKEDIN,
user.id,
req,
);
}

const token = await this.generateToken(user);

Expand Down Expand Up @@ -134,12 +178,30 @@ export class AuthService {

async handleGithubOAuthLogin(req: any) {
const { email, name, login, avatar_url } = req.user._json;
const user = await this.createUserIfNotExists(
email || login,
AuthType.GITHUB,
name,
avatar_url,
);
const socialAccount = await this.prisma.socialAccount.findFirst({
where: {
socialId: req.user.socialId,
platform: SocialAccountType.GITHUB,
},
include: {
user: true,
},
});

let user;

if (socialAccount) {
user = socialAccount.user;
} else {
user = await this.createUserIfNotExists(
email || login,
AuthType.GITHUB,
name,
avatar_url,
false,
);
await this.connectSocialPlatform(SocialAccountType.GITHUB, user.id, req);
}

const token = await this.generateToken(user);

Expand Down Expand Up @@ -322,4 +384,58 @@ export class AuthService {
});
await this.mailService.sendEmailVerificationCode(email, code);
}

async connectSocialPlatform(
platform: SocialAccountType,
userId: string,
req: any,
) {
console.log(platform, userId, req.user);

const socialAcc = await this.prisma.socialAccount.findMany({
where: { socialId: req.user.id, platform },
});

if (socialAcc.length !== 0) {
throw new BadRequestException(
'Social Account Already conected with another account',
);
}

return this.prisma.socialAccount.create({
data: {
platform,
displayName:
req.user.displayName ||
(req.user._json.first_name
? `${req.user._json.first_name} ${req.user._json.last_name}`
: req.user._json.login),
email: req.user.emails?.[0]?.value || req.user._json.email,
socialId: req.user.id,
profileUrl: req.user.profileUrl,
pictureUrl: req.user.photos[0].value,
userId,
},
});
}

async getSocialAccounts(userId: string) {
return this.prisma.socialAccount.findMany({ where: { userId } });
}

async getUserFromToken(token: string) {
try {
const payload = await this.jwt.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});

return await this.prisma.user.findUnique({
where: {
id: payload['id'],
},
});
} catch {
throw new ForbiddenException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "SocialAccount" ADD COLUMN "displayName" TEXT,
ADD COLUMN "email" TEXT,
ADD COLUMN "pictureUrl" TEXT,
ADD COLUMN "socialId" TEXT,
ALTER COLUMN "profileUrl" DROP NOT NULL;
Loading

0 comments on commit 49a74bf

Please sign in to comment.