multi tenancy stuff

This commit is contained in:
m5r
2021-08-06 01:07:15 +08:00
parent b54f9ef43c
commit d20eeb0617
51 changed files with 907 additions and 2542 deletions

View File

@ -1,13 +0,0 @@
import { useSession, useQuery } from "blitz";
import getCurrentCustomer from "../../customers/queries/get-current-customer";
export default function useCurrentCustomer() {
const session = useSession();
const [customer] = useQuery(getCurrentCustomer, null, { enabled: Boolean(session.userId) });
return {
customer,
hasFilledTwilioCredentials: Boolean(customer && customer.accountSid && customer.authToken),
hasCompletedOnboarding: session.hasCompletedOnboarding,
};
}

View File

@ -0,0 +1,11 @@
import { useQuery } from "blitz";
import getCurrentPhoneNumber from "../../phone-numbers/queries/get-current-phone-number";
import useCurrentUser from "./use-current-user";
export default function useUserPhoneNumber() {
const { hasFilledTwilioCredentials } = useCurrentUser();
const [phoneNumber] = useQuery(getCurrentPhoneNumber, {}, { enabled: hasFilledTwilioCredentials });
return phoneNumber;
}

View File

@ -0,0 +1,15 @@
import { useSession, useQuery } from "blitz";
import getCurrentUser from "../../users/queries/get-current-user";
export default function useCurrentUser() {
const session = useSession();
const [user] = useQuery(getCurrentUser, null, { enabled: Boolean(session.userId) });
const organization = user?.memberships[0]!.organization;
return {
user,
organization,
hasFilledTwilioCredentials: Boolean(user && organization?.twilioAccountSid && organization?.twilioAuthToken),
hasCompletedOnboarding: session.hasCompletedOnboarding,
};
}

View File

@ -1,12 +0,0 @@
import { useQuery } from "blitz";
import getCurrentCustomerPhoneNumber from "../../phone-numbers/queries/get-current-customer-phone-number";
import useCurrentCustomer from "./use-current-customer";
export default function useCustomerPhoneNumber() {
const { customer } = useCurrentCustomer();
const hasFilledTwilioCredentials = Boolean(customer && customer.accountSid && customer.authToken);
const [customerPhoneNumber] = useQuery(getCurrentCustomerPhoneNumber, {}, { enabled: hasFilledTwilioCredentials });
return customerPhoneNumber;
}

View File

@ -2,6 +2,7 @@ import { getConfig, useMutation } from "blitz";
import { useEffect, useState } from "react";
import setNotificationSubscription from "../mutations/set-notification-subscription";
import useCurrentPhoneNumber from "./use-current-phone-number";
const { publicRuntimeConfig } = getConfig();
@ -9,6 +10,7 @@ export default function useNotifications() {
const isServiceWorkerSupported = "serviceWorker" in navigator;
const [subscription, setSubscription] = useState<PushSubscription | null>(null);
const [setNotificationSubscriptionMutation] = useMutation(setNotificationSubscription);
const phoneNumber = useCurrentPhoneNumber();
useEffect(() => {
(async () => {
@ -23,7 +25,7 @@ export default function useNotifications() {
}, [isServiceWorkerSupported]);
async function subscribe() {
if (!isServiceWorkerSupported) {
if (!isServiceWorkerSupported || !phoneNumber) {
return;
}
@ -33,7 +35,10 @@ export default function useNotifications() {
applicationServerKey: urlBase64ToUint8Array(publicRuntimeConfig.webPush.publicKey),
});
setSubscription(subscription);
await setNotificationSubscriptionMutation({ subscription: subscription.toJSON() as any }); // TODO remove as any
await setNotificationSubscriptionMutation({
phoneNumberId: phoneNumber.id,
subscription: subscription.toJSON() as any,
}); // TODO remove as any
}
async function unsubscribe() {

View File

@ -1,12 +1,12 @@
import { Routes, useRouter } from "blitz";
import useCurrentCustomer from "./use-current-customer";
import useCustomerPhoneNumber from "./use-customer-phone-number";
import useCurrentUser from "./use-current-user";
import useCurrentPhoneNumber from "./use-current-phone-number";
export default function useRequireOnboarding() {
const router = useRouter();
const { hasFilledTwilioCredentials, hasCompletedOnboarding } = useCurrentCustomer();
const customerPhoneNumber = useCustomerPhoneNumber();
const { hasFilledTwilioCredentials, hasCompletedOnboarding } = useCurrentUser();
const phoneNumber = useCurrentPhoneNumber();
if (hasCompletedOnboarding) {
return;
@ -16,12 +16,12 @@ export default function useRequireOnboarding() {
throw router.push(Routes.StepTwo());
}
/*if (!customer.paddleCustomerId || !customer.paddleSubscriptionId) {
/*if (!user.paddleCustomerId || !user.paddleSubscriptionId) {
throw router.push(Routes.StepTwo());
return;
}*/
if (!customerPhoneNumber) {
if (!phoneNumber) {
throw router.push(Routes.StepThree());
}
}

View File

@ -77,6 +77,8 @@ const ErrorBoundary = withRouter(
// let Blitz ErrorBoundary handle this one
throw error;
}
// if network error and connection lost, display the auto-reload page with countdown
}
public render() {

View File

@ -3,10 +3,13 @@ import { z } from "zod";
import db from "../../../db";
import appLogger from "../../../integrations/logger";
import { enforceSuperAdminIfNotCurrentOrganization, setDefaultOrganizationId } from "../utils";
const logger = appLogger.child({ mutation: "set-notification-subscription" });
const Body = z.object({
organizationId: z.string().optional(),
phoneNumberId: z.string(),
subscription: z.object({
endpoint: z.string(),
expirationTime: z.number().nullable(),
@ -17,22 +20,36 @@ const Body = z.object({
}),
});
export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({ subscription }, context) => {
const customerId = context.session.userId;
try {
await db.notificationSubscription.create({
data: {
customerId,
endpoint: subscription.endpoint,
expirationTime: subscription.expirationTime,
keys_p256dh: subscription.keys.p256dh,
keys_auth: subscription.keys.auth,
},
export default resolver.pipe(
resolver.zod(Body),
resolver.authorize(),
setDefaultOrganizationId,
enforceSuperAdminIfNotCurrentOrganization,
async ({ organizationId, phoneNumberId, subscription }) => {
const phoneNumber = await db.phoneNumber.findFirst({
where: { id: phoneNumberId, organizationId },
include: { organization: true },
});
} catch (error) {
if (error.code !== "P2002") {
logger.error(error);
// we might want to `throw error`;
if (!phoneNumber) {
return;
}
}
});
try {
await db.notificationSubscription.create({
data: {
organizationId,
phoneNumberId,
endpoint: subscription.endpoint,
expirationTime: subscription.expirationTime,
keys_p256dh: subscription.keys.p256dh,
keys_auth: subscription.keys.auth,
},
});
} catch (error) {
if (error.code !== "P2002") {
logger.error(error);
// we might want to `throw error`;
}
}
},
);

35
app/core/utils.ts Normal file
View File

@ -0,0 +1,35 @@
import type { Ctx } from "blitz";
import type { Prisma } from "../../db";
import { GlobalRole } from "../../db";
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message);
}
export function setDefaultOrganizationId<T extends Record<any, any>>(
input: T,
{ session }: Ctx,
): T & { organizationId: Prisma.StringNullableFilter | string } {
assert(session.orgId, "Missing session.orgId in setDefaultOrganizationId");
if (input.organizationId) {
// Pass through the input
return input as T & { organizationId: string };
} else if (session.roles?.includes(GlobalRole.SUPERADMIN)) {
// Allow viewing any organization
return { ...input, organizationId: { not: "" } };
} else {
// Set organizationId to session.orgId
return { ...input, organizationId: session.orgId };
}
}
export function enforceSuperAdminIfNotCurrentOrganization<T extends Record<any, any>>(input: T, ctx: Ctx): T {
assert(ctx.session.orgId, "missing session.orgId");
assert(input.organizationId, "missing input.organizationId");
if (input.organizationId !== ctx.session.orgId) {
ctx.session.$authorize(GlobalRole.SUPERADMIN);
}
return input;
}