Ctrl K
ComunidadGuías…

server/api/TeamRoute.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const { teamRoles } = require("../../shared/roles.json");
const _ = require("lodash");
const jwt = require("jsonwebtoken");
const rateLimit = require("express-rate-limit");

const TeamController = require("../controllers/TeamController");
const UserController = require("../controllers/UserController");
const verifyToken = require("../modules/verifyToken");
const accessControl = require("../modules/accessControl");
const { getTeamAnalytics } = require("../modules/platformAnalytics");
const {
  discoverBusinessProfile,
  normalizeWebsiteUrl,
} = require("../modules/teamOnboarding/businessProfileDiscovery");

const starterSuggestions = require("../modules/ai/homeSuggestions/onboarding");
const { log: logHomeSuggestions } = require("../modules/ai/homeSuggestions/service");

const apiLimiter = (max = 10) => {
  return rateLimit({
    windowMs: 60 * 1000, // 1 minute
    max,
  });
};

const onboardingDiscoveryUserLimiter = rateLimit({
  windowMs: 10 * 60 * 1000,
  max: 10,
  keyGenerator: (req) => `user:${req.user?.id || "unknown"}`,
  message: { error: "Please wait before reading another website." },
});

const onboardingDiscoveryDomainLimiter = rateLimit({
  windowMs: 10 * 60 * 1000,
  max: 30,
  keyGenerator: (req) => {
    try {
      return `domain:${new URL(normalizeWebsiteUrl(req.body?.websiteUrl)).hostname}`;
    } catch (error) {
      return "domain:invalid";
    }
  },
  message: { error: "Please wait before reading this website again." },
});

function filterProjects(projects, teamRole) {
  if (teamRoles.includes(teamRole?.role)) return projects;
  return projects.filter((p) => _.indexOf(teamRole.projects, p.id) > -1);
}

module.exports = (app) => {
  const teamController = new TeamController();
  const userController = new UserController();

  const handleOnboardingDiscovery = (requiresTeamAccess) => {
    return async (req, res) => {
      try {
        if (requiresTeamAccess) {
          const teamRole = await teamController.getTeamRole(req.params.id, req.user.id);
          if (!teamRole || !["teamOwner", "teamAdmin"].includes(teamRole.role)) {
            return res.status(403).send({ error: "Access denied" });
          }
        }
        const profile = await discoverBusinessProfile(req.body?.websiteUrl);
        return res.status(200).send(profile);
      } catch (error) {
        return res.status(400).send({
          error: "We could not read that website. Check the address or add the details yourself.",
        });
      }
    };
  };

  const checkPermissions = (actionType = "readOwn", entity = "team") => {
    return async (req, res, next) => {
      const { id } = req.params;

      // Fetch the TeamRole for the user
      const teamRole = await teamController.getTeamRole(id, req.user.id);

      if (!teamRole) {
        return res.status(403).json({ message: "Access denied" });
      }

      const permission = accessControl.can(teamRole.role)[actionType](entity);
      if (!permission.granted) {
        return res.status(403).json({ message: "Access denied" });
      }

      req.user.teamRole = teamRole;
      const { role } = teamRole;

      // Handle permissions for teamOwner and teamAdmin
      if (["teamOwner", "teamAdmin"].includes(role)) {
        req.user.isEditor = true;
        return next();
      }

      if (["teamEditor", "teamViewer", "projectAdmin", "projectViewer", "projectEditor"].includes(role)) {
        // const connections = await connectionController.findByProjects(projects);
        // if (!connections || connections.length === 0) {
        //   return res.status(404).json({ message: "No connections found" });
        // }

        return next();
      }

      return res.status(403).json({ message: "Access denied" });
    };
  };

  /**
   * Get all teams based on the authentication token
   */
  app.get("/team", verifyToken, (req, res) => {
    return teamController.getUserTeams(req.user.id)
      .then((teams) => {
        return res.status(200).send(teams);
      })
      .catch((error) => {
        return res.status(400).send(error);
      });
  });
  // --------------------------------------

  // route to get a team by its id
  app.get("/team/:id", verifyToken, checkPermissions(), (req, res) => {
    return teamController.findById(req.params.id)
      .then((team) => {
        const modTeam = team;
        if (team.Projects) modTeam.setDataValue("Projects", filterProjects(team.Projects, req.user.teamRole));
        return res.status(200).send(modTeam);
      })
      .catch((error) => {
        if (error.message === "401") return res.status(401).send({ error: "Not authorized" });
        if (error === "404") return res.status(404).send({ error: "The team is not found" });
        return res.status(400).send(error);
      });
  });
  // --------------------------------------

  app.get("/team/:id/analytics", verifyToken, apiLimiter(30), async (req, res) => {
    res.set("Cache-Control", "no-store");
    const teamId = Number(req.params.id);
    if (!/^[1-9]\d*$/.test(req.params.id) || !Number.isSafeInteger(teamId)) {
      return res.status(400).send({ error: "Choose a valid team." });
    }

    try {
      const teamRole = await teamController.getTeamRole(teamId, req.user.id);
      if (!["teamOwner", "teamAdmin"].includes(teamRole?.role)) {
        return res.status(403).send({ error: "You do not have access to this team's analytics." });
      }
      return res.send(await getTeamAnalytics(teamId, req.query.days));
    } catch (error) {
      const statusCode = error.statusCode || 500;
      return res.status(statusCode).send({
        error: statusCode === 400 ? error.message : "Could not load team analytics. Try again.",
      });
    }
  });

  // route to create a team
  app.post("/team", verifyToken, apiLimiter(10), async (req, res) => {
    if (app.settings.teamRestricted === "1") {
      return res.status(400).send({ error: "Team restricted" });
    }

    try {
      const team = await teamController.createTeam(req.body, req.user.id);
      return res.status(200).send(team);
    } catch (error) {
      return res.status(400).send({ error: error.message || "Error creating team" });
    }
  });
  // --------------------------------------

  app.post(
    "/team/onboarding/discover",
    verifyToken,
    onboardingDiscoveryUserLimiter,
    onboardingDiscoveryDomainLimiter,
    handleOnboardingDiscovery(false)
  );
  // --------------------------------------

  app.post(
    "/team/:id/onboarding/discover",
    verifyToken,
    onboardingDiscoveryUserLimiter,
    onboardingDiscoveryDomainLimiter,
    handleOnboardingDiscovery(true)
  );
  // --------------------------------------

  app.patch("/team/:id/onboarding", verifyToken, apiLimiter(20), async (req, res) => {
    try {
      const team = await teamController.saveOnboarding(req.params.id, req.user.id, req.body);
      await starterSuggestions.queueSuggestions(team.id, app.get("onboardingSuggestionsQueue"))
        .catch(() => logHomeSuggestions("starter_queue_failed"));
      return res.status(200).send(team);
    } catch (error) {
      if (error.message === "401") return res.status(403).send({ error: "Access denied" });
      return res.status(400).send({ error: error.message || "Unable to save team setup" });
    }
  });
  // --------------------------------------

  app.put("/team/:id/business-profile", verifyToken, apiLimiter(20), async (req, res) => {
    try {
      const profile = await teamController.updateBusinessProfile(req.params.id, req.user.id, req.body);
      await starterSuggestions.queueSuggestions(profile.team_id, app.get("onboardingSuggestionsQueue"))
        .catch(() => logHomeSuggestions("starter_queue_failed"));
      return res.status(200).send(profile);
    } catch (error) {
      if (error.message === "401") return res.status(403).send({ error: "Access denied" });
      return res.status(400).send({ error: error.message || "Unable to save business profile" });
    }
  });
  // --------------------------------------

  app.get("/team/:id/business-profile/logo", verifyToken, async (req, res) => {
    try {
      const profile = await teamController.getBusinessProfileLogo(req.params.id, req.user.id);
      if (!profile?.logoData || !profile?.logoMimeType) {
        return res.status(404).send({ error: "Business logo not found" });
      }
      res.set("Content-Type", profile.logoMimeType);
      res.set("Cache-Control", "private, max-age=3600");
      res.set("X-Content-Type-Options", "nosniff");
      if (profile.updatedAt) res.set("Last-Modified", profile.updatedAt.toUTCString());
      return res.status(200).send(profile.logoData);
    } catch (error) {
      if (error.message === "401") return res.status(403).send({ error: "Access denied" });
      return res.status(400).send({ error: "Unable to load business logo" });
    }
  });
  // --------------------------------------

  // route to delete a team
  app.delete("/team/:id", verifyToken, checkPermissions("deleteOwn", "team"), (req, res) => {
    return teamController.deleteTeam(req.params.id, req.user.id)
      .then(() => {
        return res.status(200).send({ deleted: true });
      })
      .catch((error) => {
        if (error?.message === "401") return res.status(401).send({ error: "Not authorized" });
        return res.status(400).send({ error: error?.message || "Error deleting team" });
      });
  });
  // --------------------------------------

  // route to update a team
  app.put("/team/:id", verifyToken, (req, res) => {
    if (!req.params || !req.body) return res.status(400).send("Missing fields");
    let gTeamRole;
    return teamController.getTeamRole(req.params.id, req.user.id)
      .then((teamRole) => {
        gTeamRole = teamRole;
        if (req.body.aiSuggestionsEnabled !== undefined && !["teamOwner", "teamAdmin"].includes(teamRole.role)) {
          throw new Error(401);
        }
        const permission = accessControl.can(teamRole.role).updateOwn("team");
        if (!permission.granted) {
          return new Promise((resolve, reject) => reject(new Error(401)));
        }
        return teamController.update(req.params.id, req.body);
      })
      .then((team) => {
        const modTeam = team;
        if (team.Projects) modTeam.setDataValue("Projects", filterProjects(team.Projects, gTeamRole));
        return res.status(200).send(modTeam);
      })
      .catch((error) => {
        if (error.message === "401") return res.status(401).send({ error: "Not authorized" });
        return res.status(400).send(error);
      });
  });
  // --------------------------------------

  // route to transfer ownership of a team
  app.put("/team/:id/transfer", verifyToken, checkPermissions("updateAny", "team"), (req, res) => {
    return teamController.transferOwnership(req.params.id, req.user.id, req.body.newOwnerId)
      .then((updated) => {
        return res.status(200).send(updated);
      })
      .catch((error) => {
        return res.status(400).send(error);
      });
  });
  // --------------------------------------

  // a route to send a team invite
  app.post("/team/:id/invite", verifyToken, checkPermissions("createAny", "teamInvite"), (req, res) => {
    const role = req.body.role ?? "projectViewer";
    if (!teamController.isInvitableRole(role)) {
      return res.status(400).send({ error: "Invalid invitation role" });
    }

    const payload = {
      projects: req.body.projects,
      canExport: req.body.canExport,
      role,
      team_id: req.params.id,
      user_id: req.user.id,
    };

    const token = jwt.sign(payload, app.settings.encryptionKey, {
      expiresIn: 2592000 // a month
    }, (err, token) => {
      if (err) throw new Error(err);
      return res.status(200).send({
        url: `${app.settings.client}/invite?token=${token}`,
      });
    });

    return token;
  });
  // --------------------------------------

  // route for adding a team member with invite url
  app.post("/team/user/:user_id", verifyToken, (req, res) => {
    if (!req.params.user_id || !req.body.token) return res.status(400).send("Missing fields");
    if (`${req.params.user_id}` !== `${req.user.id}`) {
      return res.status(400).send("Malformed request");
    }

    let newRole = {};
    return jwt.verify(req.body.token, app.settings.encryptionKey, (err, decoded) => {
      if (err || !decoded?.team_id) {
        return res.status(401).send({ error: "Invalid or expired invitation" });
      }

      return teamController.addTeamRole(decoded.team_id, req.user.id, decoded.role || "projectViewer", decoded.projects, decoded.canExport)
        .then((role) => {
          newRole = role;
          return teamController.findById(newRole.team_id);
        })
        .then((team) => {
          const modTeam = team;
          if (team.Projects) modTeam.setDataValue("Projects", filterProjects(team.Projects, newRole));
          return res.status(200).send(modTeam);
        })
        .catch((error) => {
          return res.status(400).send(error);
        });
    });
  });
  // --------------------------------------

  // route to get all team users
  app.get("/team/:id/members", verifyToken, checkPermissions(), (req, res) => {
    return teamController.getTeamMembersId(req.params.id)
      .then((userIds) => {
        if (userIds.length < 1) return res.status(200).send([]);
        return userController.getUsersById(userIds, req.params.id);
      })
      .then((teamMembers) => {
        return res.status(200).send(teamMembers);
      })
      .catch((error) => {
        if (error.message === "401") return res.status(401).send({ error: "Not authorized" });
        return res.status(400).send(error);
      });
  });
  // --------------------------------------

  // route to update a team role
  app.put("/team/:id/role", verifyToken, checkPermissions("updateAny", "teamRole"), (req, res) => {
    return teamController.updateTeamRole(req.params.id, req.body.user_id, req.body)
      .then((updated) => {
        return res.status(200).send(updated);
      })
      .catch((error) => {
        if (error.message === "401") return res.status(401).send({ error: "Not authorized" });
        return res.status(400).send(error);
      });
  });
  // --------------------------------------

  // route to delete a team member
  app.delete("/team/:id/member/:userId", verifyToken, checkPermissions("deleteAny", "teamRole"), (req, res) => {
    return teamController.getTeamRole(req.params.id, req.params.userId)
      .then(async (teamRole) => {
        if (!teamRole) return res.status(404).send("Did not find a team member");

        const roleToDelete = await teamController.getTeamRole(req.params.id, req.params.userId);

        if (roleToDelete.role === "teamOwner") {
          return new Promise((resolve, reject) => reject("Cannot delete a team owner"));
        }

        return teamController.deleteTeamMember(teamRole.id);
      })
      .then((success) => {
        if (success) {
          return res.status(200).send({ removed: success });
        }
        return new Promise((resolve, reject) => reject(new Error(400)));
      })
      .catch((error) => {
        if (error.message === "401") return res.status(401).send({ error: "Not authorized" });
        return res.status(400).send(error);
      });
  });
  // --------------------------------------

  // route to create a new API key to access the team content
  app.post("/team/:id/apikey", verifyToken, checkPermissions("createAny", "apiKey"), (req, res) => {
    if (!req.body.name) return res.status(400).send({ error: "A key name is required." });

    return teamController.createApiKey(req.params.id, req.user, req.body)
      .then((apiKey) => {
        return res.status(200).send(apiKey);
      })
      .catch((err) => {
        return res.status(400).send({ error: err.message || "The key could not be created." });
      });
  });
  // --------------------------------------

  // route to get an API key
  app.get("/team/:id/apikey", verifyToken, checkPermissions("readAny", "apiKey"), (req, res) => {
    return teamController.getApiKeys(req.params.id)
      .then((apiKey) => {
        return res.status(200).send(apiKey);
      })
      .catch((err) => {
        return res.status(400).send(err);
      });
  });
  // --------------------------------------

  // route to remove an API key
  app.delete("/team/:id/apikey/:keyId", verifyToken, checkPermissions("deleteAny", "apiKey"), (req, res) => {
    return teamController.deleteApiKey(req.params.keyId, req.params.id)
      .then(() => {
        return res.status(200).send({ deleted: true });
      })
      .catch((err) => {
        if (err?.message === "404") {
          return res.status(404).send({ error: "Not Found" });
        }
        return res.status(400).send(err);
      });
  });
  // --------------------------------------

  return (req, res, next) => {
    next();
  };
};