Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(multi-tenant): fix accept invitation for multi-tenant #518

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions packages/multi-tenant/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export { default as tenantMigrationPlugin } from "./migratePlugin";

export { default as thirdPartyEmailPassword } from "./supertokens/recipes";

export { default as invitationResolver } from "./invitations/resolver";

export { default as tenantResolver } from "./model/tenants/resolver";

export { default as tenantRoutes } from "./model/tenants/controller";
Expand Down
131 changes: 131 additions & 0 deletions packages/multi-tenant/src/invitations/handler/acceptInvitation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { formatDate } from "@dzangolab/fastify-slonik";
import {
isInvitationValid,
validateEmail,
validatePassword,
InvitationService,
} from "@dzangolab/fastify-user";
import { createNewSession } from "supertokens-node/recipe/session";
import { emailPasswordSignUp } from "supertokens-node/recipe/thirdpartyemailpassword";

import type {
User,
Invitation,
InvitationCreateInput,
InvitationUpdateInput,
} from "@dzangolab/fastify-user";
import type { FastifyReply, FastifyRequest } from "fastify";
import type { QueryResultRow } from "slonik";

interface FieldInput {
email: string;
password: string;
}

const acceptInvitation = async (
request: FastifyRequest,
reply: FastifyReply
) => {
const { body, config, dbSchema, log, params, slonik } =
request as FastifyRequest<{
Body: FieldInput;
}>;

const { token } = params as { token: string };

try {
const { email, password } = body;

// check if the email is valid
const emailResult = validateEmail(email, config);

if (!emailResult.success) {
return reply.send({
status: "ERROR",
message: emailResult.message,
});
}

// password strength validation
const passwordStrength = validatePassword(password, config);

if (!passwordStrength.success) {
return reply.send({
status: "ERROR",
message: passwordStrength.message,
});
}

const service = new InvitationService<
Invitation & QueryResultRow,
InvitationCreateInput,
InvitationUpdateInput
>(config, slonik, dbSchema);

const invitation = await service.findByToken(token);

// validate the invitation
if (!invitation || !isInvitationValid(invitation)) {
return reply.send({
status: "ERROR",
message: "Invitation is invalid or has expired",
});
}

// compare the FieldInput email to the invitation email
if (invitation.email != email) {
return reply.send({
status: "ERROR",
message: "Email do not match with the invitation",
});
}

// signup
const signUpResponse = await emailPasswordSignUp(email, password, {
roles: [invitation.role],
autoVerifyEmail: true,
tenant: request.tenant,
});

if (signUpResponse.status !== "OK") {
return reply.send(signUpResponse);
}

// update invitation's acceptedAt value with current time
await service.update(invitation.id, {
acceptedAt: formatDate(new Date(Date.now())),
});

// run post accept hook
try {
await config.user.invitation?.postAccept?.(
request,
invitation,
signUpResponse.user as unknown as User
);
} catch (error) {
log.error(error);
}

// create new session so the user be logged in on signup
await createNewSession(request, reply, signUpResponse.user.id);

reply.send({
...signUpResponse,
user: {
...signUpResponse.user,
roles: [invitation.role],
},
});
} catch (error) {
log.error(error);
reply.status(500);

reply.send({
status: "ERROR",
message: "Oops! Something went wrong",
});
}
};

export default acceptInvitation;
139 changes: 139 additions & 0 deletions packages/multi-tenant/src/invitations/resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { formatDate } from "@dzangolab/fastify-slonik";
import {
isInvitationValid,
validateEmail,
validatePassword,
InvitationService as Service,
} from "@dzangolab/fastify-user";
import mercurius from "mercurius";
import { createNewSession } from "supertokens-node/recipe/session";
import { emailPasswordSignUp } from "supertokens-node/recipe/thirdpartyemailpassword";

import type {
User,
Invitation,
InvitationCreateInput,
InvitationUpdateInput,
} from "@dzangolab/fastify-user";
import type { MercuriusContext } from "mercurius";
import type { QueryResultRow } from "slonik";

const Mutation = {
acceptInvitation: async (
parent: unknown,
arguments_: {
data: {
email: string;
password: string;
};
token: string;
},
context: MercuriusContext
) => {
const { app, config, database, dbSchema, reply } = context;

const { token, data } = arguments_;

try {
const { email, password } = data;

// check if the email is valid
const emailResult = validateEmail(email, config);

if (!emailResult.success && emailResult.message) {
const mercuriusError = new mercurius.ErrorWithProps(
emailResult.message
);

return mercuriusError;
}

// password strength validation
const passwordStrength = validatePassword(password, config);

if (!passwordStrength.success && passwordStrength.message) {
const mercuriusError = new mercurius.ErrorWithProps(
passwordStrength.message
);

return mercuriusError;
}

const service = new Service<
Invitation & QueryResultRow,
InvitationCreateInput,
InvitationUpdateInput
>(config, database, dbSchema);

const invitation = await service.findByToken(token);

// validate the invitation
if (!invitation || !isInvitationValid(invitation)) {
const mercuriusError = new mercurius.ErrorWithProps(
"Invitation is invalid or has expired"
);

return mercuriusError;
}

// compare the FieldInput email to the invitation email
if (invitation.email != email) {
const mercuriusError = new mercurius.ErrorWithProps(
"Email do not match with the invitation"
);

return mercuriusError;
}

// signup
const signUpResponse = await emailPasswordSignUp(email, password, {
roles: [invitation.role],
autoVerifyEmail: true,
tenant: reply.request.tenant,
});

if (signUpResponse.status !== "OK") {
return signUpResponse;
}

// update invitation's acceptedAt value with current time
await service.update(invitation.id, {
acceptedAt: formatDate(new Date(Date.now())),
});

// run post accept hook
try {
await config.user.invitation?.postAccept?.(
reply.request,
invitation,
signUpResponse.user as unknown as User
);
} catch (error) {
app.log.error(error);
}

// create new session so the user be logged in on signup
await createNewSession(reply.request, reply, signUpResponse.user.id);

return {
...signUpResponse,
user: {
...signUpResponse.user,
roles: [invitation.role],
},
};
} catch (error) {
app.log.error(error);

const mercuriusError = new mercurius.ErrorWithProps(
"Oops! Something went wrong"
);

mercuriusError.statusCode = 500;

return mercuriusError;
}
},
};

export default { Mutation };
10 changes: 10 additions & 0 deletions packages/multi-tenant/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FastifyPlugin from "fastify-plugin";
import merge from "lodash.merge";

import acceptInvitation from "./invitations/handler/acceptInvitation";
import createTenantOwnerRole from "./lib/createTenantOwnerRole";
import updateContext from "./lib/updateContext";
import recipes from "./supertokens/recipes";
Expand All @@ -26,6 +27,15 @@ const plugin = async (
// merge supertokens config
config.user.supertokens = merge(supertokensConfig, config.user.supertokens);

const handlers = {
invitation: {
accept: acceptInvitation,
},
};

// merge handlers
config.user.handlers = merge(handlers, config.user.handlers);

fastify.addHook("onReady", async () => {
await createTenantOwnerRole();
});
Expand Down
Loading