import { getSupabaseClient } from "@/infra/supabase/supabase.client"; import { ForgetPasswordResponseSchema, LoginRequestSchema, OAuthTokenResponse, RefreshSessionRequestSchema, RegisterUserRequestSchema, RegisterUserResponseSchema, ResendOtpRequestSchema, ResendOtpResponseSchema, ResetPasswordRequestSchema, VerifyOtpRequestSchema, } from "./auth.dto"; import { IAuthRepository } from "./auth.repository"; import { logger } from "@/shared/logger/logger"; import RedisService from "@/infra/redis/redis.service"; import { ApiResponse, ApiResponseType } from "@/shared/http/api-response"; import { nodemailerService } from "@/infra/nodemail/nodemail.service"; import { HttpStatusCode } from "@/shared/http/status-code"; export class AuthService { private readonly redisService: RedisService; constructor(private readonly authRepo: IAuthRepository) { this.redisService = RedisService.getInstance(); } public async login(dto: LoginRequestSchema): Promise> { const {data, error} = await getSupabaseClient().auth.signInWithPassword({email: dto.email, password: dto.password}); if (!data.session) { logger.error("Unable to create session"); return ApiResponse.error( "Unable to create session", "SESSION_CREATION_FAILED", error, HttpStatusCode.UNAUTHORIZED ); } return ApiResponse.success(data.weakPassword?.message || '', data.session); } public async register(dto: RegisterUserRequestSchema): Promise> { try { const {data, error} = await getSupabaseClient().auth.admin.listUsers(); if (error) { logger.error(error, "Failed to check user existence"); return ApiResponse.error(error.message, error.name, null, 500); } if (data.users.some((u) => u.email === dto.email)) { logger.info(`User with email ${dto.email} already exists`); return ApiResponse.error( "User with email already exists", "USER_ALREADY_EXISTS", null, HttpStatusCode.CONFLICT ); } const otpKey = this.getOtpRedisKey(dto.email); const existingOtpSession = await this.redisService.get(otpKey); if (existingOtpSession) { const ttl = await this.redisService.ttl(otpKey); if (ttl > 30) { return ApiResponse.error( "OTP already sent. Please wait before requesting again.", "OTP_ALREADY_SENT", null, HttpStatusCode.TOO_MANY_REQUESTS ); } } const otp = this.generateOtp(); await this.redisService.set(otpKey, { ...dto, otp }, 60); // In production send OTP via email/SMS here await nodemailerService.sendEmail({ to: dto.email, subject: 'Your OTP Code', text: `Your OTP code is ${otp}. It is valid for 1 minutes.`, }) return ApiResponse.success(`OTP sent to email ${dto.email}`, { email: dto.email }); } catch (err) { logger.error(err, "Error in AuthService.register"); const error = err as Error; return ApiResponse.error(error.message, error.name, null, 500); } } public async resendOtp(dto: ResendOtpRequestSchema): Promise> { try { const { data, error } = await getSupabaseClient() .from("auth.users") .select("id") .eq("email", dto.email) .maybeSingle(); if (error) { logger.error(error, "Failed to check user existence"); return ApiResponse.error(error.message, error.name, null, 500); } if (data?.id) { return ApiResponse.error( "User with email already exists", "USER_ALREADY_EXISTS", null, HttpStatusCode.CONFLICT ); } const otpKey = this.getOtpRedisKey(dto.email); const existingSession = await this.redisService.get(otpKey); if (existingSession) { const ttl = await this.redisService.ttl(otpKey); if (ttl > 30) { return ApiResponse.error( "OTP already sent. Please wait before requesting again.", "OTP_ALREADY_SENT", null, HttpStatusCode.TOO_MANY_REQUESTS ); } } const otp = this.generateOtp(); await this.redisService.set(otpKey, { ...existingSession, otp }, 60); return ApiResponse.success(`OTP resent to email ${dto.email}`, {otpCode: otp}); } catch (err) { logger.error(err, "Error in AuthService.resendOtp"); const error = err as Error; return ApiResponse.error(error.message, error.name, null, 500); } } public async verifyEmailOtp(dto: VerifyOtpRequestSchema): Promise> { try { const otpKey = this.getOtpRedisKey(dto.email); const session = await this.redisService.get(otpKey); if (!session || session.otp !== dto.otpCode) { logger.warn(`OTP session expired or invalid for ${dto.email}`); return ApiResponse.error( "OTP session expired or invalid", "OTP_SESSION_EXPIRED", null, HttpStatusCode.UNAUTHORIZED ); } const { error } = await getSupabaseClient().auth.admin.createUser({ email: session.email, password: session.password, user_metadata: { fullName: session.fullName, phone: session.phone, }, email_confirm: true, }); if (error) { logger.error(error, "Unable to create user"); return ApiResponse.error( error.message, error.name, null, error.status || HttpStatusCode.INTERNAL_SERVER_ERROR ); } const { data } = await getSupabaseClient().auth.signInWithPassword({email: session.email, password: session.password}); if (!data.session) { logger.error("Unable to create session"); return ApiResponse.error( "Unable to create session", "SESSION_CREATION_FAILED", null, HttpStatusCode.UNAUTHORIZED ); } await this.redisService.del(otpKey); return ApiResponse.success("OTP verified successfully", data.session); } catch (err) { logger.error(err, "Error in AuthService.verifySmsOtp"); const error = err as Error; return ApiResponse.error(error.message, error.name, null, 500); } } public async forgetPassword(email: string): Promise> { try { const { data, error } = await getSupabaseClient().auth.admin.listUsers(); if (error) { logger.error(error, "Failed to list users"); return ApiResponse.error(error.message, error.name, null, 500); } const user = data.users.find((u) => u.email === email); if (!user) { return ApiResponse.error( "No user found with that email", "USER_NOT_FOUND", null, HttpStatusCode.NOT_FOUND ); } const otpKey = this.getForgetPasswordRedisKey(email); const existingOtp = await this.redisService.get(otpKey); if (existingOtp) { const ttl = await this.redisService.ttl(otpKey); if (ttl > 30) { return ApiResponse.error( "OTP already sent. Please wait before requesting again.", "OTP_ALREADY_SENT", null, HttpStatusCode.TOO_MANY_REQUESTS ); } } const otp = this.generateOtp(); await this.redisService.set(otpKey, { email, otp }, 60); await nodemailerService.sendEmail({ to: email, subject: "Reset your password", text: `Your password reset OTP is ${otp}. It is valid for 1 minute.`, }); return ApiResponse.success(`OTP sent to ${email}`, { email }); } catch (err) { logger.error(err, "Error in forgetPasswordRequest"); const error = err as Error; return ApiResponse.error(error.message, error.name, null, 500); } } public async resetPassword(resetPasswordDto: ResetPasswordRequestSchema): Promise> { try { const otpKey = this.getForgetPasswordRedisKey(resetPasswordDto.email); const session = await this.redisService.get(otpKey); if (!session || session.otp !== resetPasswordDto.otpCode) { logger.warn(`Invalid or expired OTP for ${resetPasswordDto.email}`); return ApiResponse.error( "OTP invalid or expired", "OTP_INVALID", null, HttpStatusCode.UNAUTHORIZED ); } const { data, error } = await getSupabaseClient().auth.admin.listUsers(); if (error) { logger.error(error, "Failed to list users"); return ApiResponse.error(error.message, error.name, null, 500); } const user = data.users.find((u) => u.email === resetPasswordDto.email); if (!user) { return ApiResponse.error( "User not found", "USER_NOT_FOUND", null, HttpStatusCode.NOT_FOUND ); } const { error: updateError } = await getSupabaseClient().auth.admin.updateUserById(user.id, { password: resetPasswordDto.newPassword, }); if (updateError) { logger.error(updateError, "Failed to update user password"); return ApiResponse.error(updateError.message, updateError.name, null, 500); } await this.redisService.del(otpKey); return ApiResponse.success("Password updated successfully", {}); } catch (err) { logger.error(err, "Error in verifyForgetPasswordOtp"); const error = err as Error; return ApiResponse.error(error.message, error.name, null, 500); } } public async refreshSession(sessionDto: RefreshSessionRequestSchema): Promise> { try { const { data, error } = await getSupabaseClient().auth.setSession({ refresh_token: sessionDto.refreshToken, access_token: sessionDto.accessToken, }); if (error || !data.session) { console.error("Refresh token error:", error); return ApiResponse.error( "Unable to refresh session", "REFRESH_FAILED", error, HttpStatusCode.UNAUTHORIZED ); } return ApiResponse.success("Session refreshed successfully", data.session); } catch (err) { console.error("Unexpected error in refreshSession:", err); const error = err as Error; return ApiResponse.error(error.message, error.name, null, 500); } } private generateOtp(): string { return Math.floor(100000 + Math.random() * 900000).toString(); } private getOtpRedisKey(email: string): string { return `register_otp:${email}`; } private getForgetPasswordRedisKey(email: string): string { return `forget_password_otp:${email}`; } }