eletrotupi / tcc / api/src/controllers/auth.ts master
5.5 KB Raw
import { Request, Response, NextFunction } from "express";
import { AuthenticatedRequest } from "@app/middleware/auth";

import {
  login,
  generateToken,
  requestPasswordReset,
  resetPassword,
  activateUser,
  storeActivationCode,
} from "@app/services/auth.service";

import { verifyToken } from "@app/lib/jwt";

import {
  findUserByEmail,
  findUserById,
  findUserByActivationCode,
} from "@app/services/user.service";

import {
  LoginSchema,
  PasswordResetSchema,
  PasswordResetRequestSchema,
  ActivateUserSchema,
} from "@app/schemas";

import { addMinutes } from "date-fns";
import { getQueue, MailJobName } from "@app/lib/queue";
import crypto from "crypto";

export const AuthController = {
  login: async (req: Request, res: Response, next: NextFunction) => {
    try {
      const parsed = LoginSchema.safeParse(req.body);

      if (!parsed.success) {
        return res.status(400).json({
          errors: parsed.error!.issues,
        });
      }

      const { user, token } = await login(parsed.data);

      if (
        !user.active &&
        new Date() > new Date(user.activationCodeExpiresAt!)
      ) {
        const code = [...Array(6)].map(() => crypto.randomInt(9)).join("");

        const expiresAt = addMinutes(new Date(), 5);
        await storeActivationCode(user.id, code, expiresAt);

        const mailQueue = getQueue("mail");
        const job = await mailQueue.add(MailJobName.ActivateAccountEmail, {
          userId: user.id,
          code: code,
        });
      }

      return res.json({
        token,
        user,
      });
    } catch (err: any) {
      if (err.message === "Invalid credentials") {
        return res.status(401).json({ error: "Invalid credentials" });
      }
      next(err);
    }
  },

  forgotPassword: async (req: Request, res: Response, next: NextFunction) => {
    try {
      const parsed = PasswordResetRequestSchema.safeParse(req.body);

      if (!parsed.success) {
        return res.status(400).json({
          errors: parsed.error!.issues,
        });
      }

      const result = await requestPasswordReset(parsed.data);
      return res.status(200).json(result);
    } catch (err) {
      next(err);
    }
  },

  resetPassword: async (req: Request, res: Response, next: NextFunction) => {
    try {
      const parsed = PasswordResetSchema.safeParse(req.body);

      if (!parsed.success) {
        return res.status(400).json({
          errors: parsed.error!.issues,
        });
      }

      const result = await resetPassword(parsed.data);
      return res.status(200).json(result);
    } catch (err: any) {
      if (err.message.includes("token")) {
        return res.status(400).json({ error: err.message });
      }
      next(err);
    }
  },

  verify: async (req: Request, res: Response, next: NextFunction) => {
    try {
      const authHeader = req.headers.authorization;

      if (!authHeader || !authHeader.startsWith("Bearer ")) {
        return res.status(401).json({ error: "No token provided" });
      }

      const token = authHeader.substring(7);
      const payload = verifyToken(token);
      const user = await findUserByEmail(payload.email);

      if (!user) {
        // XXX: Investigate which actual cases this would happen
        // But the possible reason is that a token is living inside
        // the user's phone, but somehow he was banned/deleted
        return res.status(404).json({
          errors: "User does not exist anymore",
        });
      }

      res.json({
        valid: true,
        userId: payload.userId,
        email: payload.email,
        user: {
          firstName: user.firstName,
          email: user.email,
          lastName: user.lastName,
          updatedAt: user.updatedAt,
          active: user.active,
          avatarKey: user.avatarKey,
          avatarURL: user.avatarURL,
          pushToken: user.pushToken,
        },
      });
    } catch (err: any) {
      if (
        err.name === "JsonWebTokenError" ||
        err.name === "TokenExpiredError"
      ) {
        return res.status(401).json({ error: "Invalid or expired token" });
      }
      next(err);
    }
  },

  activate: async (req: Request, res: Response, next: NextFunction) => {
    try {
      const parsed = ActivateUserSchema.safeParse(req.body);

      if (!parsed.success) {
        return res.status(400).json({
          errors: parsed.error!.issues,
        });
      }

      const user = await findUserByActivationCode(String(parsed.data.code));

      if (!user) {
        return res.status(422).json({
          error: "Código incorreto. Verifique o código e tente novamente",
        });
      }

      const result = await activateUser(user);

      return res.status(200).json({
        user: result,
      });
    } catch (err: any) {
      next(err);
    }
  },

  resendActivationCode: async (
    req: AuthenticatedRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const user = await findUserById(req.userId!);

      if (!user) {
        return res.status(400).json({
          errors: "Usuário não encontrado",
        });
      }

      const code = [...Array(6)].map(() => crypto.randomInt(9)).join("");

      const expiresAt = addMinutes(new Date(), 5);
      await storeActivationCode(user.id, code, expiresAt);

      const mailQueue = getQueue("mail");
      const job = await mailQueue.add(MailJobName.ActivateAccountEmail, {
        userId: user.id,
        code: code,
      });

      res.json({
        user,
      });
    } catch (err: any) {
      next(err);
    }
  },
};