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

feat(user): tenant owner can sign in to his/her tenant app #610

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { formatDate } from "@dzangolab/fastify-slonik";
import { ROLE_USER } from "@dzangolab/fastify-user";
import UserRoles from "supertokens-node/recipe/userroles";

import { ROLE_TENANT_OWNER } from "../../../constants";
import getUserService from "../../../lib/getUserService";
import Email from "../../utils/email";
import isTenantOwnerEmail from "../../utils/isTenantOwnerEmail";

import type { AuthUser } from "@dzangolab/fastify-user";
import type { AuthUser, User, UserCreateInput } from "@dzangolab/fastify-user";
import type { FastifyInstance } from "fastify";
import type { QueryResultRow } from "slonik";
import type { RecipeInterface } from "supertokens-node/recipe/thirdpartyemailpassword";

const emailPasswordSignIn = (
Expand All @@ -14,11 +19,21 @@ const emailPasswordSignIn = (
const { config, log, slonik } = fastify;

return async (input) => {
input.email = Email.addTenantPrefix(
config,
input.email,
input.userContext.tenant
);
if (
input.userContext.tenant &&
!(await isTenantOwnerEmail(
config,
slonik,
input.email,
input.userContext.tenant
))
) {
input.email = Email.addTenantPrefix(
config,
input.email,
input.userContext.tenant
);
}

const originalResponse = await originalImplementation.emailPasswordSignIn(
input
Expand All @@ -31,15 +46,43 @@ const emailPasswordSignIn = (
const userService = getUserService(
config,
slonik,
input.userContext.dbSchema
input.userContext.tenant
);

const user = await userService.findById(originalResponse.user.id);
let user = await userService.findById(originalResponse.user.id);

if (!user) {
log.error(`User record not found for userId ${originalResponse.user.id}`);
const { roles } = await UserRoles.getRolesForUser(
originalResponse.user.id
);

if (input.userContext.tenant && roles.includes(ROLE_TENANT_OWNER)) {
// This is the first time the tenant owner is signing in to the tenant app.
if (input.userContext.tenant) {
await UserRoles.addRoleToUser(originalResponse.user.id, ROLE_USER);
}

// Get user details from from default schema
const userDetails = (await getUserService(config, slonik).findById(
originalResponse.user.id
)) as (User & UserCreateInput) | null;

if (!userDetails) {
throw new Error("Unable to find user");
}

delete userDetails.roles;
delete userDetails.lastLoginAt;
delete userDetails.signedUpAt;

user = (await userService.create(userDetails)) as User & QueryResultRow;
} else {
log.error(
`User record not found for userId ${originalResponse.user.id}`
);

return { status: "WRONG_CREDENTIALS_ERROR" };
return { status: "WRONG_CREDENTIALS_ERROR" };
}
}

user.lastLoginAt = Date.now();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getUserByThirdPartyInfo } from "supertokens-node/recipe/thirdpartyemail
import UserRoles from "supertokens-node/recipe/userroles";

import getMultiTenantConfig from "../../../lib/getMultiTenantConfig";
import isTenantOwnerEmail from "../../utils/isTenantOwnerEmail";

import type { Tenant } from "../../../types";
import type { FastifyInstance, FastifyError } from "fastify";
Expand All @@ -13,13 +14,21 @@ const thirdPartySignInUp = (
originalImplementation: RecipeInterface,
fastify: FastifyInstance
): RecipeInterface["thirdPartySignInUp"] => {
const { config, log } = fastify;
const { config, log, slonik } = fastify;

return async (input) => {
const roles = (input.userContext.roles || []) as string[];
const tenant: Tenant | undefined = input.userContext.tenant;

if (tenant) {
if (
tenant &&
!(await isTenantOwnerEmail(
config,
slonik,
input.email,
input.userContext.tenant
))
) {
const tenantId = tenant[getMultiTenantConfig(config).table.columns.id];

input.thirdPartyUserId = tenantId + "_" + input.thirdPartyUserId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { formatDate } from "@dzangolab/fastify-slonik";
import { ROLE_USER } from "@dzangolab/fastify-user";
import { deleteUser } from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

import { ROLE_TENANT_OWNER } from "../../../constants";
import getHost from "../../../lib/getHost";
import getMultiTenantConfig from "../../../lib/getMultiTenantConfig";
import getUserService from "../../../lib/getUserService";

import type { User } from "@dzangolab/fastify-user";
import type { User, UserCreateInput } from "@dzangolab/fastify-user";
import type { FastifyInstance, FastifyRequest } from "fastify";
import type { QueryResultRow } from "slonik";
import type { APIInterface } from "supertokens-node/recipe/thirdpartyemailpassword/types";

const thirdPartySignInUpPOST = (
Expand Down Expand Up @@ -98,14 +100,44 @@ const thirdPartySignInUpPOST = (
user = await userService.findById(originalResponse.user.id);

if (!user) {
log.error(
`User record not found for userId ${originalResponse.user.id}`
const { roles } = await UserRoles.getRolesForUser(
originalResponse.user.id
);

return {
status: "GENERAL_ERROR",
message: "Something went wrong",
};
if (input.userContext.tenant && roles.includes(ROLE_TENANT_OWNER)) {
// This is the first time the tenant owner is signing in to the tenant app.
if (input.userContext.tenant) {
await UserRoles.addRoleToUser(
originalResponse.user.id,
ROLE_USER
);
}

// Get user details from from default schema
const userDetails = (await getUserService(config, slonik).findById(
originalResponse.user.id
)) as (User & UserCreateInput) | null;

if (!userDetails) {
throw new Error("Unable to find user");
}

delete userDetails.roles;
delete userDetails.lastLoginAt;
delete userDetails.signedUpAt;

user = (await userService.create(userDetails)) as User &
QueryResultRow;
} else {
log.error(
`User record not found for userId ${originalResponse.user.id}`
);

return {
status: "GENERAL_ERROR",
message: "Something went wrong",
};
}
}

user.lastLoginAt = Date.now();
Expand Down
41 changes: 41 additions & 0 deletions packages/multi-tenant/src/supertokens/utils/isTenantOwnerEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { UserService } from "@dzangolab/fastify-user";
import humps from "humps";

import getMultiTenantConfig from "../../lib/getMultiTenantConfig";

import type { Tenant } from "../../types/tenant";
import type { ApiConfig } from "@dzangolab/fastify-config";
import type { Database } from "@dzangolab/fastify-slonik";
import type {
User,
UserCreateInput,
UserUpdateInput,
} from "@dzangolab/fastify-user";
import type { QueryResultRow } from "slonik";

const isTenantOwnerEmail = async (
config: ApiConfig,
database: Database,
email: string,
tenant: Tenant
) => {
const userService = new UserService<
User & QueryResultRow,
UserCreateInput,
UserUpdateInput
>(config, database);

const multiTenantConfig = getMultiTenantConfig(config);

const owner = await userService.findById(
tenant[humps.camelize(multiTenantConfig.table.columns.ownerId)]
);

if (owner) {
return email === owner.email;
}

return false;
};

export default isTenantOwnerEmail;
Loading