diff --git a/src/auth/controller/auth.controller.ts b/src/auth/controller/auth.controller.ts
index 2e99498..87a617b 100644
--- a/src/auth/controller/auth.controller.ts
+++ b/src/auth/controller/auth.controller.ts
@@ -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')
@@ -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(
- ``,
- );
+ if (req.session.intent?.facebook == 'connect') {
+ await this.authService.connectSocialPlatform(
+ SocialAccountType.FACEBOOK,
+ req.session.userId,
+ req,
+ );
+
+ res.send(``);
+ } else {
+ const user = await this.authService.handleFacebookOAuthLogin(req);
+ res.send(
+ ``,
+ );
+ }
}
@Public()
@@ -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(
- ``,
- );
+ if (req.session.intent?.linkedin == 'connect') {
+ await this.authService.connectSocialPlatform(
+ SocialAccountType.LINKEDIN,
+ req.session.userId,
+ req,
+ );
+ res.send(``);
+ } else {
+ const user = await this.authService.handleLinkedInOAuthLogin(req);
+ res.send(
+ ``,
+ );
+ }
}
@Public()
@@ -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(
- ``,
- );
+ if (req.session.intent?.github == 'connect') {
+ await this.authService.connectSocialPlatform(
+ SocialAccountType.GITHUB,
+ req.session.userId,
+ req,
+ );
+
+ res.send(``);
+ } else {
+ const user = await this.authService.handleGithubOAuthLogin(req);
+ res.send(
+ ``,
+ );
+ }
}
@Public()
@@ -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);
+ }
}
diff --git a/src/auth/service/auth.service.ts b/src/auth/service/auth.service.ts
index 0842425..d01981a 100644
--- a/src/auth/service/auth.service.ts
+++ b/src/auth/service/auth.service.ts
@@ -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';
@@ -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);
@@ -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);
@@ -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);
@@ -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();
+ }
+ }
}
diff --git a/src/prisma/migrations/20240606141406_social_accounts_fields/migration.sql b/src/prisma/migrations/20240606141406_social_accounts_fields/migration.sql
new file mode 100644
index 0000000..347f80d
--- /dev/null
+++ b/src/prisma/migrations/20240606141406_social_accounts_fields/migration.sql
@@ -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;
diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma
index b8c149d..b9d135a 100644
--- a/src/prisma/schema.prisma
+++ b/src/prisma/schema.prisma
@@ -57,10 +57,14 @@ model Connection {
}
model SocialAccount {
- id Int @id @default(autoincrement())
- platform SocialAccountType
- profileUrl String
- addedOn DateTime @default(now())
+ id Int @id @default(autoincrement())
+ platform SocialAccountType
+ displayName String?
+ email String?
+ socialId String?
+ profileUrl String?
+ pictureUrl String?
+ addedOn DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
userId String
diff --git a/src/user/service/user.service.ts b/src/user/service/user.service.ts
index a13e434..b267090 100644
--- a/src/user/service/user.service.ts
+++ b/src/user/service/user.service.ts
@@ -13,7 +13,6 @@ import { S3_CLIENT } from '../../provider/s3.provider';
import { REDIS_CLIENT } from '../../provider/redis.provider';
import { Redis } from 'ioredis';
import { v4 } from 'uuid';
-
import { getMimeType } from '../../utils/image';
import { UpdateUserSettingsDto } from '../dto/update-user-settings.dto';