Skip to content

Commit

Permalink
feat: new auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Psami-wondah committed Feb 1, 2025
1 parent 520dc30 commit 525cc13
Show file tree
Hide file tree
Showing 18 changed files with 170 additions and 83 deletions.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ services:
command: yarn start:prod
ports:
- 8000:8000
- 8001:8001
env_file:
- .env
depends_on:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"crypto": "^1.0.1",
"handlebars": "^4.7.8",
"jose": "^5.9.6",
"lodash": "^4.17.21",
Expand Down
73 changes: 29 additions & 44 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
UseGuards,
} from "@nestjs/common";
import { Request, Response } from "express";
import { SignUpDto } from "./dto/sign-up.dto";
import { AuthService } from "./auth.service";
import { SignInDto } from "./dto/sign-in.dto";
import { VerifyEmailDto } from "./dto/verify-email.dto";
import { ResendOtpEmailDto } from "./dto/resend-otp-email.dto";
import { AuthGuard } from "./auth.guard";
Expand All @@ -26,38 +24,14 @@ import { RefreshTokenDto } from "./dto/refresh-token.dto";
export class AuthController {
constructor(private authService: AuthService) {}

@Post("/sign-up")
async signUp(
@Body() signUpData: SignUpDto,
@Res({ passthrough: true }) response: Response,
): Promise<{ message: string; accessToken: string; user: UserDto }> {
const userAccess = await this.authService.signUpEmail(signUpData);
response
.status(HttpStatus.CREATED)
.cookie("refreshToken", userAccess.refreshToken, {
httpOnly: true,
});
return {
message: "Signup Successful",
...userAccess,
};
}

@Post("/sign-in")
@Post("/sign-in-passwordless")
async signIn(
@Body() signInData: SignInDto,
@Body() signInData: ResendOtpEmailDto,
@Res({ passthrough: true }) response: Response,
): Promise<{ message: string; accessToken: string; user: UserDto }> {
const userAccess = await this.authService.signInEmail(signInData);
response
.status(HttpStatus.OK)
.cookie("refreshToken", userAccess.refreshToken, {
httpOnly: true,
});
return {
message: "Signin Successful",
...userAccess,
};
): Promise<{ message: string; data: { isNew: boolean } }> {
const userAccess = await this.authService.signInPasswordless(signInData);
response.status(HttpStatus.OK);
return userAccess;
}

@Post("/refresh-tokens")
Expand All @@ -81,6 +55,9 @@ export class AuthController {
.status(HttpStatus.OK)
.cookie("refreshToken", userAccess.refreshToken, {
httpOnly: true,
expires: new Date(userAccess.refreshTokenExpiry),
sameSite: "none",
secure: true,
});
return {
message: "Token refresh successful",
Expand All @@ -93,9 +70,18 @@ export class AuthController {
@Body() data: VerifyEmailDto,
@Res({ passthrough: true }) response: Response,
): Promise<{ message: string }> {
const resp = await this.authService.verifyEmail(data);
response.status(HttpStatus.OK);
return resp;
const userAccess = await this.authService.verifyEmail(data);
const { refreshToken, refreshTokenExpiry, ...rest } = userAccess;
response.status(HttpStatus.OK).cookie("refreshToken", refreshToken, {
httpOnly: true,
expires: new Date(refreshTokenExpiry),
sameSite: "none",
secure: true,
});
return {
message: "Signin Successful",
...rest,
};
}

@UseGuards(AuthGuard)
Expand All @@ -116,17 +102,16 @@ export class AuthController {
): Promise<{ message: string }> {
const userData = await this.authService.verifyGoogleAuthToken(data.token);
const userAccess = await this.authService.signGoogle(userData);
response
.status(HttpStatus.OK)
.cookie("refreshToken", userAccess.refreshToken, {
httpOnly: true,
expires: new Date(userAccess.refreshTokenExpiry),
sameSite: "none",
secure: false,
});
const { refreshToken, refreshTokenExpiry, ...rest } = userAccess;
response.status(HttpStatus.OK).cookie("refreshToken", refreshToken, {
httpOnly: true,
expires: new Date(refreshTokenExpiry),
sameSite: "none",
secure: true,
});
return {
message: "Google Auth Successful",
...userAccess,
...rest,
};
}

Expand Down
58 changes: 47 additions & 11 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,7 @@ export class AuthService {
};
}

async refreshTokens(
refreshToken: string,
): Promise<{ accessToken: string; refreshToken: string; user: UserDto }> {
async refreshTokens(refreshToken: string) {
const storedToken = await this.refreshTokenModel.findOne({
token: refreshToken,
});
Expand All @@ -139,7 +137,7 @@ export class AuthService {
async signGoogle(userData: GoogleUser) {
let user = await this.userService.findOne({
authId: userData.id,
authType: AuthType.GOOGLE,
email: userData.email,
});

if (!user) {
Expand All @@ -153,6 +151,10 @@ export class AuthService {
});
}

if (user.authType !== AuthType.GOOGLE) {
throw new UnauthorizedException("Sign in with email");
}

return await this.generateTokensForUser(user);
}

Expand Down Expand Up @@ -194,6 +196,37 @@ export class AuthService {
return await this.generateTokensForUser(user);
}

async signInPasswordless(data: ResendOtpEmailDto) {
let isNew = false;
let user = await this.userService.findOneByEmail(data.email);
if (!user) {
user = await this.userService.create({
email: data.email,
authType: AuthType.EMAIL,
password: data.email,
});
}
if (!user.name) {
isNew = true;
}
if (user.authType !== AuthType.EMAIL) {
throw new UnauthorizedException("Sign in with google");
}
const otp = await this.otpService.generateOtpForUser(user);
this.mailService.send(
[{ name: user.name, email: user.email }],
"Verify your Email",
"verify-email.template.html",
{ code: otp },
);
return {
message: "Email Sent",
data: {
isNew,
},
};
}

// TODO: Protect with Auth
async resendEmailVerificationOtp(data: ResendOtpEmailDto): Promise<{
message: string;
Expand All @@ -217,19 +250,22 @@ export class AuthService {
};
}

async verifyEmail(verifyEmailData: VerifyEmailDto): Promise<{
message: string;
}> {
async verifyEmail(verifyEmailData: VerifyEmailDto) {
const validOtp = await this.otpService.verifyOTP(
verifyEmailData.email,
verifyEmailData.code,
);
if (!validOtp) {
throw new UnauthorizedException();
}
await this.userService.verifyEmail(verifyEmailData.email);
return {
message: "Email Verified",
};
await this.userService.verifyEmail(
verifyEmailData.email,
verifyEmailData.name,
);
const user = await this.userService.findOneByEmail(verifyEmailData.email);
if (!user) {
throw new NotFoundException();
}
return await this.generateTokensForUser(user);
}
}
13 changes: 1 addition & 12 deletions src/auth/dto/sign-up.dto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
IsEmail,
IsNotEmpty,
IsString,
MinLength,
IsPhoneNumber,
} from "class-validator";
import { IsEmail, IsNotEmpty, IsString, MinLength } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";

export class SignUpDto {
Expand All @@ -18,11 +12,6 @@ export class SignUpDto {
@IsEmail()
email: string;

@ApiProperty()
@IsNotEmpty()
@IsPhoneNumber()
phoneNumber: string;

@ApiProperty()
@IsNotEmpty()
@MinLength(8)
Expand Down
7 changes: 6 additions & 1 deletion src/auth/dto/verify-email.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsEmail, IsNotEmpty, IsNumber } from "class-validator";
import { IsEmail, IsNotEmpty, IsNumber, IsString } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";

export class VerifyEmailDto {
Expand All @@ -7,6 +7,11 @@ export class VerifyEmailDto {
@IsEmail()
email: string;

@ApiProperty()
@IsNotEmpty()
@IsString()
name?: string;

@ApiProperty()
@IsNotEmpty()
@IsNumber()
Expand Down
2 changes: 1 addition & 1 deletion src/mail/clients/brevo.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const sendEmailBrevo = async (
) => {
const url = "https://api.sendinblue.com/v3/smtp/email";
const body = {
sender: { email: "fastpoint@psami.com", name: "Fast Point" },
sender: { email: "copyyt@psami.com", name: "Copyyt" },
to: addresses,
subject,
htmlContent: htmlString,
Expand Down
2 changes: 1 addition & 1 deletion src/mail/clients/zeptomail.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const sendEmailZepto = async (
) => {
const url = "https://api.zeptomail.com/v1.1/email";
const body = JSON.stringify({
from: { address: "fastpoint@psami.com", name: "Fast Point" },
from: { address: "copyyt@psami.com", name: "Copyyt" },
to: addresses.map((address) => ({
email_address: { address: address.email, name: address.name },
})),
Expand Down
4 changes: 2 additions & 2 deletions src/mail/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import path from "path";
import fs from "fs";
import * as path from "path";
import * as fs from "fs";
import handlebars from "handlebars";
import { MailAddress } from "./interfaces/mail-address.interface";
import { sendEmailZepto } from "./clients/zeptomail.client";
Expand Down
5 changes: 4 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.enableCors({
origin: ["chrome-extension://ophadgignfjigkbdcmicnklokjeknnbd"],
origin: [
"chrome-extension://ophadgignfjigkbdcmicnklokjeknnbd",
"http://localhost:5173",
],
credentials: true,
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
});
Expand Down
4 changes: 1 addition & 3 deletions src/otp/otp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { OTP } from "./otp.schema";
import { UserDocument } from "src/user/user.schema";
import crypto from "crypto";
import * as crypto from "crypto";

const otpExpiryMinutes = 10;

Expand All @@ -27,8 +27,6 @@ export class OtpService {
async verifyOTP(email: string, code: number): Promise<boolean> {
const record = await this.otpModel.findOne({ email, code });

console.log(record);

if (!record) {
return false;
}
Expand Down
22 changes: 22 additions & 0 deletions src/socket/socket.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,30 @@ export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect {
private userService: UserService,
) {}

isSocketActive(socketId: string): boolean {
if (!this.server) {
throw new Error("Socket.IO server instance is not initialized.");
}
const socket = this.server.sockets.sockets.get(socketId);
return socket ? socket.connected : false;
}

private async initializeUserConnection(
user: UserDocument,
socket: Socket,
): Promise<void> {
socket.data.user = user;

// delete disconnected connections
const connections = await this.userService.getConnections(user._id);
if (connections) {
const activeConnections = connections.filter((connection) =>
this.isSocketActive(connection),
);
if (activeConnections.length !== connections.length) {
await this.userService.setConnections(user._id, activeConnections);
}
}
await this.userService.addConnection(user._id, socket.id);
this.logger.log(
`Client connected: ${socket.id} - User ID: ${user._id.toString()}`,
Expand Down Expand Up @@ -81,6 +100,9 @@ export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect {
const connections = await this.userService.getConnections(
currentUser._id,
);
if (message) {
this.userService.setLastMessage(currentUser._id, String(message));
}
uniq([...(connections ?? []), socket.id]).forEach((connection) => {
this.server.to(connection).emit("message", message);
});
Expand Down
2 changes: 1 addition & 1 deletion src/user/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AuthType } from "../enum/auth-type.enum";
export class CreateUserDto {
@IsNotEmpty()
@IsString()
name: string;
name?: string;

@IsNotEmpty()
@IsEmail()
Expand Down
19 changes: 16 additions & 3 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { Controller } from '@nestjs/common';
import { Controller, Get, Req, UseGuards } from "@nestjs/common";
import { AuthGuard } from "src/auth/auth.guard";
import { ApiBearerAuth } from "@nestjs/swagger";
import { AuthenticatedRequest } from "src/auth/interfaces/request.interface";

@Controller('user')
export class UserController {}
@Controller("api/v1/user")
export class UserController {
@ApiBearerAuth()
@UseGuards(AuthGuard)
@Get("/last-message")
getProfile(@Req() req: AuthenticatedRequest) {
return {
messsage: "Last message",
data: req.user?.lastMessage ?? "",
};
}
}
Loading

0 comments on commit 525cc13

Please sign in to comment.