| 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<ApiResponseType<OAuthTokenResponse>> {
|
| 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<ApiResponseType<RegisterUserResponseSchema>> {
|
| 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);
|
|
|
|
|
| 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<ApiResponseType<ResendOtpResponseSchema>> {
|
| 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<ApiResponseType<OAuthTokenResponse>> {
|
| 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<ApiResponseType<ForgetPasswordResponseSchema>> {
|
| 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<ApiResponseType<{}>> {
|
| 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<ApiResponseType<OAuthTokenResponse>> {
|
| 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}`;
|
| }
|
| }
|
|
|