diff --git a/api/graphql/resolvers/team.resolver.js b/api/graphql/resolvers/team.resolver.js index 6e649005..c8ee673e 100644 --- a/api/graphql/resolvers/team.resolver.js +++ b/api/graphql/resolvers/team.resolver.js @@ -3,6 +3,7 @@ import { getAllTeams, findTeamByAlias, createTeam, inviteTeamMember } from '~/se import { verifyInvitationToken } from '~/services/teams/verifyInvitationToken.service'; import { acceptInvitation } from '~/services/teams/acceptInvitation'; import { isAuthenticated } from './authorization.resolver'; +import { cancelInvitation } from '../../services/teams/cancelInvitation'; const resolvers = { Query: { @@ -29,6 +30,10 @@ const resolvers = { isAuthenticated, (_, { token }, { user }) => acceptInvitation(token, user), ), + cancelInvitation: combineResolvers( + isAuthenticated, + (_, { userId, teamId }) => cancelInvitation(userId, teamId), + ), }, }; diff --git a/api/graphql/schemas/team.schema.js b/api/graphql/schemas/team.schema.js index b4d54e0a..f6c803dc 100644 --- a/api/graphql/schemas/team.schema.js +++ b/api/graphql/schemas/team.schema.js @@ -25,6 +25,12 @@ export const TeamSchema = gql` email: String isOwner: Boolean status: TeamMemberType + teamId: ID + } + + type VerifyTokenResponse{ + teamName: String! + owner: String! } type VerifyTokenResponse{ @@ -42,5 +48,6 @@ export const TeamSchema = gql` createTeam(name: String!, alias: String!): Team, inviteMember(email: String!, alias: String!): TeamMember joinTeam(type: JoinTeamType!, token: String!): Boolean! + cancelInvitation(userId: String!, teamId: String!): Boolean! } `; diff --git a/api/migrations/20201130225822_create_teams.js b/api/migrations/20201130225822_create_teams.js index 471ca05b..12f6df49 100644 --- a/api/migrations/20201130225822_create_teams.js +++ b/api/migrations/20201130225822_create_teams.js @@ -15,10 +15,10 @@ export function up(knex) { table.integer('created_by').unsigned().notNullable(); }), knex.schema.createTable('team_invitations', (table) => { + table.increments('id'); table.string('email').notNullable(); table.integer('team_id').unsigned(); table.foreign('team_id').references('id').inTable('teams'); - table.unique(['email', 'team_id']); table.unique('token'); table.string('token').notNullable(); table.enu('status', ['active', 'inactive']).notNullable(); @@ -31,11 +31,11 @@ export function up(knex) { .defaultTo(knex.raw('CURRENT_TIMESTAMP')); }), knex.schema.createTable('team_members', (table) => { + table.increments('id'); table.integer('user_id').unsigned().notNullable(); table.integer('team_id').unsigned().notNullable(); table.foreign('user_id').references('id').inTable('users'); table.foreign('team_id').references('id').inTable('teams'); - table.unique(['user_id', 'team_id']); table.enu('status', ['pending', 'active', 'inactive', 'decline']).notNullable(); table.dateTime('created_at') .notNullable() diff --git a/api/repository/team_invitations.repository.js b/api/repository/team_invitations.repository.js index b2bf2eb1..3521123c 100644 --- a/api/repository/team_invitations.repository.js +++ b/api/repository/team_invitations.repository.js @@ -56,13 +56,10 @@ export async function getDetailTeamInvitation(token) { }); } -export async function updateTeamInvitationByToken(token, data) { - return database(TABLE).where({ token }).update(data); +export async function updateTeamInvitation(condition, data, transaction = null) { + const query = database(TABLE).where(condition).update(data); + if (!transaction) { + return query; + } + return query.transacting(transaction); } -// select * from team_invitations -// inner join teams -// on teams.id = team_invitations.team_id -// inner join users on users.id = team_invitations.invited_by -// where team_invitations.token = '3235f68696b46196c00e5aa359273c909613b3514ce9fe819f594f0033421a6c' - -// AND(team_invitations.token = '3235f68696b46196c00e5aa359273c909613b3514ce9fe819f594f0033421a6c') \ No newline at end of file diff --git a/api/repository/team_members.repository.js b/api/repository/team_members.repository.js index bc8fbed2..d77fe4db 100644 --- a/api/repository/team_members.repository.js +++ b/api/repository/team_members.repository.js @@ -5,7 +5,7 @@ import database from '~/config/database.config'; import { TABLES } from '~/constants/database.constant'; import { teamsColumns } from './team.repository'; import { usersColumns } from './user.repository'; -import { createTeamInvitation, VALID_PERIOD_DAYS } from './team_invitations.repository'; +import { createTeamInvitation, updateTeamInvitation, VALID_PERIOD_DAYS } from './team_invitations.repository'; const TABLE = TABLES.teamMembers; @@ -25,8 +25,10 @@ export async function getListTeamMemberByAliasTeam({ alias }) { .whereIn(teamMembersColumns.teamId, function subQuery() { this.select('id').from(TABLES.teams).where({ alias }); }).join(TABLES.users, teamMembersColumns.userId, usersColumns.id) + .whereNot({ [teamMembersColumns.status]: 'decline' }) .select({ userName: usersColumns.name, + teamId: teamMembersColumns.teamId, userId: usersColumns.id, email: usersColumns.email, status: teamMembersColumns.status, @@ -63,6 +65,44 @@ export async function createMemberAndInviteToken({ userId, teamId, memberId, ema } } +export async function updateTeamMember(condition, data, transaction = null) { + const query = database(TABLE).where(condition).update(data); + if (!transaction) { + return query; + } + return query.transacting(transaction); +} + +export async function reInviteMemberHasDecline({ teamId, memberId, email, token, userId }) { + let transaction; + const queries = []; + try { + transaction = await database.transaction(); + + queries.push(updateTeamMember({ + team_id: teamId, + user_id: memberId, + }, { status: 'pending' }, transaction)); + + queries.push(updateTeamInvitation({ email }, { status: 'inactive' }, transaction)); + + queries.push(createTeamInvitation({ + email, + token, + invited_by: userId, + team_id: teamId, + valid_until: formatDateDB(dayjs().add(VALID_PERIOD_DAYS, 'days')), + status: 'active', + }, transaction)); + + await Promise.all(queries); + await transaction.commit(); + } catch (error) { + transaction.rollback(); + throw new Error(error); + } +} + export async function updateTeamMember(condition, data) { return database(TABLE).where(condition).update(data); } diff --git a/api/repository/user.repository.js b/api/repository/user.repository.js index 449d0c03..2f5b37e4 100644 --- a/api/repository/user.repository.js +++ b/api/repository/user.repository.js @@ -5,6 +5,7 @@ import { TABLES } from '~/constants/database.constant'; import { insertUserPlan } from './user_plans.repository'; import { insertMultiPermission } from './user_permission.repository'; import { PERMISSION_PLAN } from '~/constants/billing.constant'; +import { teamMembersColumns } from './team_members.repository'; const TABLE = TABLES.users; @@ -82,3 +83,16 @@ export async function getUserByIdAndJoinUserToken(id, type) { export async function activeUser(id) { return database(TABLE).where({ id }).update({ is_active: true }); } + +export async function getUserAndTeamInfo({ email }) { + return database(TABLE) + .join(TABLES.teamMembers, usersColumns.id, teamMembersColumns.userId) + .where({ [usersColumns.email]: email }) + .select({ + id: usersColumns.id, + email: usersColumns.email, + name: usersColumns.name, + teamStatus: teamMembersColumns.status, + }) + .first(); +} diff --git a/api/services/teams/acceptInvitation.js b/api/services/teams/acceptInvitation.js index 688f6779..7d4d1fbc 100644 --- a/api/services/teams/acceptInvitation.js +++ b/api/services/teams/acceptInvitation.js @@ -1,9 +1,9 @@ -import { updateTeamInvitationByToken } from '../../repository/team_invitations.repository'; +import { updateTeamInvitation } from '../../repository/team_invitations.repository'; import { updateTeamMember } from '../../repository/team_members.repository'; export async function acceptInvitation(token, type) { const status = type === 'accept' ? 'active' : 'decline'; - await updateTeamInvitationByToken(token, { status: 'inactive' }); + await updateTeamInvitation({ token }, { status: 'inactive' }); await updateTeamMember({ invitation_token: token }, { status }); return true; } diff --git a/api/services/teams/cancelInvitation.js b/api/services/teams/cancelInvitation.js new file mode 100644 index 00000000..8e1ffc49 --- /dev/null +++ b/api/services/teams/cancelInvitation.js @@ -0,0 +1,25 @@ +import { updateTeamInvitation } from "../../repository/team_invitations.repository"; +import { updateTeamMember } from "../../repository/team_members.repository"; +import { findUser } from "../../repository/user.repository"; +import database from '~/config/database.config'; + +export async function cancelInvitation(userId, teamId) { + let transaction; + try { + transaction = await database.transaction(); + const user = await findUser({ id: userId }); + const queries = []; + queries.push(updateTeamMember({ + team_id: teamId, + user_id: userId, + }, { status: 'inactive' }, transaction)); + + queries.push(updateTeamInvitation({ email: user.email }, { status: 'inactive' }, transaction)); + await Promise.all(queries); + await transaction.commit(); + return true; + } catch (error) { + transaction.rollback(); + throw new Error(error); + } +} diff --git a/api/services/teams/teams.service.js b/api/services/teams/teams.service.js index b51a34af..7f59a0bd 100644 --- a/api/services/teams/teams.service.js +++ b/api/services/teams/teams.service.js @@ -9,8 +9,8 @@ import generateRandomKey from '~/helpers/genarateRandomkey'; import { normalizeEmail, stringToSlug } from '~/helpers/string.helper'; import logger from '~/utils/logger'; import sendMail from '~/libs/mail'; -import { createMemberAndInviteToken, getListTeamMemberByAliasTeam } from '../../repository/team_members.repository'; -import { findUser } from '../../repository/user.repository'; +import { createMemberAndInviteToken, getListTeamMemberByAliasTeam, reInviteMemberHasDecline } from '../../repository/team_members.repository'; +import { getUserAndTeamInfo } from '../../repository/user.repository'; /** * Function to get all team @@ -38,6 +38,7 @@ export async function findTeamByAlias(alias) { email: member.email, status: member.status, isOwner: member.userId === member.owner, + teamId: member.teamId, })); } @@ -88,7 +89,10 @@ export async function inviteTeamMember(user, alias, inviteeEmail) { throw new ApolloError('Team not found'); } - member = await findUser({ email: inviteeEmail }); + member = await getUserAndTeamInfo({ email: inviteeEmail }); + if (member.id === team.created_by) { + throw new ApolloError('Invalid Email'); + } const token = await generateRandomKey(); const subject = 'Team invitation'; @@ -99,16 +103,38 @@ export async function inviteTeamMember(user, alias, inviteeEmail) { url: `${process.env.FRONTEND_URL}/teams/invitation/${token}`, }, }); - const queries = [sendMail(normalizeEmail(inviteeEmail), subject, template)]; if (member) { - queries.push(createMemberAndInviteToken({ - email: inviteeEmail, - token, - teamId: team.id, - memberId: member.id, - userId: user.id, - })); + switch (member.teamStatus) { + case 'decline': + queries.push(reInviteMemberHasDecline({ + teamId: team.id, + memberId: member.id, + email: member.email, + token, + userId: user.id, + })); + break; + + case 'active': + throw new ApolloError('Account have been actived'); + + case 'inactive': + throw new ApolloError('Account has deactived'); + + case 'pending': + throw new ApolloError('Account have been invited'); + + default: + queries.push(createMemberAndInviteToken({ + email: inviteeEmail, + token, + teamId: team.id, + memberId: member.id, + userId: user.id, + })); + break; + } } else { queries.push(createTeamInvitation({ email: inviteeEmail, @@ -129,6 +155,6 @@ export async function inviteTeamMember(user, alias, inviteeEmail) { }; } catch (error) { logger.error(error); - throw new ApolloError('Something went wrong!'); + throw new ApolloError(error); } } diff --git a/app/src/components/Team/InviteMemberForm.jsx b/app/src/components/Team/InviteMemberForm.jsx index 3e992e94..9aee5efd 100644 --- a/app/src/components/Team/InviteMemberForm.jsx +++ b/app/src/components/Team/InviteMemberForm.jsx @@ -6,9 +6,11 @@ InviteMemberForm.propTypes = { onSubmit: PropTypes.func.isRequired, register: PropTypes.func.isRequired, formErrors: PropTypes.object, + apiError: PropTypes.string, + loading: PropTypes.bool }; -function InviteMemberForm({ onSubmit, register, formErrors }) { +function InviteMemberForm({ onSubmit, register, formErrors, apiError, loading }) { return (