server/api/TeamRoute.js
Rama master · réplica de lectura del repositorio original.
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();
};
};