Skip to content
45 changes: 41 additions & 4 deletions classes/Project/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,16 @@ export default class Project {
let message = `You have been invited to the TPEN project ${projectTitle}.
View project <a href='${process.env.TPENINTERFACES}project?projectID=${this.data._id}'>here</a>.`
if (user) {
// FIXME this does not have the functionality of an 'invite'.
await this.inviteExistingTPENUser(user._id, roles)
}
else {
const inviteData = await this.inviteNewTPENUser(email, roles)
const returnTo = encodeURIComponent(`${process.env.TPENINTERFACES}project?projectID=${this.data._id}&inviteCode=${inviteData.tpenUserID}`)
// Signup starting at the TPEN3 public site
const signup = `${process.env.TPENTHREE}login?inviteCode=${inviteData.tpenUserID}&returnTo=${returnTo}`
// TODO decline endpoint in TPEN Services
const decline = `${process.env.TPENINTERFACES}project/decline?inviteCode=${inviteData.tpenUserID}&groupID=${inviteData.tpenGroupID}`
// Decline endpoint in TPEN Services
const decline = `${process.env.TPENINTERFACES}project/decline?email=${encodeURIComponent(email)}&user=${inviteData.tpenUserID}&project=${this.data._id}&projectTitle=${encodeURIComponent(projectTitle)}`
message += `
<p>
Click the button below to get started with your project</p>
Expand Down Expand Up @@ -138,12 +139,19 @@ export default class Project {
return roles
}

/**
* Invite an existing TPEN3 User to the project.
* FIXME this does not have the functionality of an 'invite'. The User is added to the project.
* There is no step for them to accept or decline.
*/
async inviteExistingTPENUser(userId, roles) {
const group = new Group(this.data.group)
await group.addMember(userId, roles)
await this.addMember(userId, roles)
return this
}

/**
* Add a new temporary user to the users collection and send the invite E-mail.
*/
async inviteNewTPENUser(email, roles) {
const user = new User()
const inviteCode = user._id
Expand All @@ -152,15 +160,44 @@ export default class Project {
const _sub = `temp-${user._id}` // This is a temporary sub for the user until they verify their email
user.data = { email, _sub, profile, agent, inviteCode }
await user.save()
// FIXME this does not have the functionality of an 'invite'.
await this.inviteExistingTPENUser(user._id, roles)
return { "tpenUserID":user._id, "tpenGroupID":this.data.group }
}

/**
* Add a member to the Project Group.
*
* @param userId The User/member _id to add to the Group.
*/
async addMember(userId, roles) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only adds the catch and is only used once. It's a bit odd, but it looks fine

try {
const group = new Group(this.data.group)
await group.addMember(userId, roles)
} catch (error) {
throw {
status: error.status || 500,
message: error.message || "An error occurred while adding the member."
}
}

}

/**
* Remove a member from the Project Group.
* If the member is an invitee (temporary) User, delete that User from the db.
*
* @param userId The User/member _id to remove from the Group and perhaps delete from the db.
*/
async removeMember(userId) {
try {
const group = new Group(this.data.group)
await group.removeMember(userId)
await group.update()
// Don't leave orphaned invitees in the db.
const member = new User(userId)
const memberData = await member.getSelf()
if(memberData?.inviteCode) member.delete()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a member with an inviteCode has not successfully joined and accepted yet. This makes sense.

return this
} catch (error) {
throw {
Expand Down
2 changes: 2 additions & 0 deletions project/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import hotkeysRouter from "./hotkeysRouter.js"
import metadataRouter from "./metadataRouter.js"
import projectToolsRouter from "./projectToolsRouter.js"
import memberUpgradeRouter from "./memberUpgradeRouter.js"
import memberDeclineInviteRouter from "./memberDeclineInviteRouter.js"

const router = express.Router({ mergeParams: true })
router.use(cors(common_cors))

// Use split routers
router.use(memberUpgradeRouter) // Contains unauthenticated route!
router.use(memberDeclineInviteRouter) // Contains unauthenticated route!
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these comments are odd.

router.use(projectCreateRouter)
router.use(import28Router)
router.use(projectReadRouter)
Expand Down
41 changes: 41 additions & 0 deletions project/memberDeclineInviteRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import express from "express"
import { validateID, respondWithError } from "../utilities/shared.js"
import Project from "../classes/Project/Project.js"
import User from "../classes/User/User.js"

const router = express.Router({ mergeParams: true })

/**
* A user is declining from the E-mail they recieved. It is unauthenticated.
* Their member entry should be removed from the Group.
* Their temporary user should be removed from the db.
*
* @param projectId - The project which contains the temporary TPEN3 User as a member.
* @param collaboratorId - The temporary TPEN3 User declining the invitation.
*/
router.route("/:projectId/collaborator/:collaboratorId/decline").get(async (req, res) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this organization

const { projectId, collaboratorId } = req.params
if (!projectId || !collaboratorId) return respondWithError(res, 400, "Not all data was provided.")
try {
const project = await new Project(projectId)
const projectData = await project.loadProject()
if (!projectData) return respondWithError(res, 404, "Project does not exist or the project id is invalid.")
const invitedUser = new User(collaboratorId)
const userData = await invitedUser.getSelf()
if (!userData?.profile) return respondWithError(res, 404, "This user has already declined or the user id is invalid.")
if (!userData?.inviteCode) return respondWithError(res, 400, "This user has already accepted the invitation.")
await project.removeMember(collaboratorId)
const name = userData.email ?? userData.profile.displayName ?? collaboratorId
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getting this name seems extra tot he request, but it's fine

res.status(200).send(`User '${name}' successfully declined the invitation.`)
} catch (error) {
return respondWithError(
res,
error.status || error.code || 500,
error.message ?? "There was an error declining the invitation."
)
}
}).all((_, res) => {
respondWithError(res, 405, "Improper request method. Use GET instead")
})

export default router
14 changes: 7 additions & 7 deletions project/memberUpgradeRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,20 @@ router.route("/:projectId/collaborator/:collaboratorId/agent/:agentId").get(asyn
const tempData = await tempUser.getSelf()
if(!tempData?.profile) return respondWithError(res, 404, "Temporary user does not exist")
if(!tempData?.inviteCode) return respondWithError(res, 400, "Temporary user provided is not a temporary user.")
const project = await new Project(projectId).loadProject()
if(!project) return respondWithError(res, 404, "Project does not exist.")
const group = new Group(project.group)
const project = new Project(projectId)
const projectData = await project.loadProject()
if(!projectData) return respondWithError(res, 404, "Project does not exist.")
const group = new Group(projectData.group)
let tempRoles = await group.getMemberRoles(tempData._id)
if(!tempRoles) tempRoles = {"VIEWER":[]}
group.addMember(agentId, Object.keys(tempRoles))
await project.addMember(agentId, Object.keys(tempRoles))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love using the project.addMember here when you already have the Group.

try {
group.removeMember(tempData._id)
// This will also delete the temporary User from the users collection.
await project.removeMember(tempData._id)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This abstraction makes more sense on the client, but since it is just a kind of alias, I can accept it

}
catch (err) {
// keep going.
}
await group.update()
tempUser.delete()
res.status(200).send(`Temporary user '${collaboratorId}' upgraded to user '${agentId}'.`)
} catch (error) {
return respondWithError(
Expand Down
Loading