mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-03-29 11:12:19 +01:00
Handle missing Discord email sooner. Make sure emails are verified.
This commit is contained in:
parent
dd334e28c6
commit
66d1155a0d
@ -88,9 +88,9 @@
|
||||
+ configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
|
||||
+ }
|
||||
},
|
||||
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
@@ -87,11 +83,11 @@
|
||||
onAuthSucceededRedirectTo: "/demo-app",
|
||||
@@ -86,11 +82,11 @@
|
||||
// NOTE: "Dummy" provider is just for local development purposes.
|
||||
// Make sure to check the server logs for the email confirmation url (it will not be sent to an address)!
|
||||
// Once you are ready for production, switch to e.g. "SendGrid" or "Mailgun" providers. Check out https://docs.opensaas.sh/guides/email-sending/ .
|
||||
@ -104,7 +104,7 @@
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -207,9 +203,9 @@
|
||||
@@ -206,9 +202,9 @@
|
||||
}
|
||||
|
||||
api paymentsWebhook {
|
||||
|
@ -1,46 +1,64 @@
|
||||
--- template/app/src/auth/userSignupFields.ts
|
||||
+++ opensaas-sh/app/src/auth/userSignupFields.ts
|
||||
@@ -1,11 +1,8 @@
|
||||
@@ -1,8 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { defineUserSignupFields } from 'wasp/auth/providers/types';
|
||||
|
||||
-const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
|
||||
-
|
||||
export const getEmailUserFields = defineUserSignupFields({
|
||||
username: (data: any) => data.email,
|
||||
- isAdmin: (data: any) => adminEmails.includes(data.email),
|
||||
email: (data: any) => data.email,
|
||||
const emailDataSchema = z.object({
|
||||
email: z.string(),
|
||||
});
|
||||
@@ -16,10 +14,6 @@
|
||||
const emailData = emailDataSchema.parse(data);
|
||||
return emailData.email;
|
||||
},
|
||||
- isAdmin: (data) => {
|
||||
- const emailData = emailDataSchema.parse(data);
|
||||
- return adminEmails.includes(emailData.email);
|
||||
- },
|
||||
});
|
||||
|
||||
@@ -29,10 +26,6 @@
|
||||
const githubDataSchema = z.object({
|
||||
@@ -45,14 +39,6 @@
|
||||
const githubData = githubDataSchema.parse(data);
|
||||
return githubData.profile.login;
|
||||
},
|
||||
- isAdmin: (data) => {
|
||||
- const githubData = githubDataSchema.parse(data);
|
||||
- return adminEmails.includes(githubData.profile.emails[0].email);
|
||||
- const emailInfo = getGithubEmailInfo(githubData);
|
||||
- if (!emailInfo.verified) {
|
||||
- return false;
|
||||
- }
|
||||
- return adminEmails.includes(emailInfo.email);
|
||||
- },
|
||||
});
|
||||
|
||||
// NOTE: if we don't want to access users' emails, we can use scope ["user:read"]
|
||||
@@ -58,10 +51,6 @@
|
||||
// We are using the first email from the list of emails returned by GitHub.
|
||||
@@ -85,13 +71,6 @@
|
||||
const googleData = googleDataSchema.parse(data);
|
||||
return googleData.profile.email;
|
||||
},
|
||||
- isAdmin: (data) => {
|
||||
- const googleData = googleDataSchema.parse(data);
|
||||
- if (!googleData.profile.email_verified) {
|
||||
- return false;
|
||||
- }
|
||||
- return adminEmails.includes(googleData.profile.email);
|
||||
- },
|
||||
});
|
||||
|
||||
export function getGoogleAuthConfig() {
|
||||
@@ -86,10 +75,6 @@
|
||||
@@ -121,13 +100,6 @@
|
||||
const discordData = discordDataSchema.parse(data);
|
||||
return discordData.profile.username;
|
||||
},
|
||||
- isAdmin: (data) => {
|
||||
- const email = discordDataSchema.parse(data).profile.email;
|
||||
- return !!email && adminEmails.includes(email);
|
||||
- const discordData = discordDataSchema.parse(data);
|
||||
- if (!discordData.profile.email || !discordData.profile.verified) {
|
||||
- return false;
|
||||
- }
|
||||
- return adminEmails.includes(discordData.profile.email);
|
||||
- },
|
||||
});
|
||||
|
||||
|
@ -8,16 +8,7 @@
|
||||
|
||||
interface PaymentPlanCard {
|
||||
name: string;
|
||||
@@ -82,7 +83,7 @@
|
||||
}
|
||||
|
||||
if (!customerPortalUrl) {
|
||||
- throw new Error(`Customer Portal does not exist for user ${user.id}`)
|
||||
+ throw new Error(`Customer Portal does not exist for user ${user.id}`);
|
||||
}
|
||||
|
||||
window.open(customerPortalUrl, '_blank');
|
||||
@@ -96,11 +97,18 @@
|
||||
@@ -105,16 +106,24 @@
|
||||
Pick your <span className='text-yellow-500'>pricing</span>
|
||||
</h2>
|
||||
</div>
|
||||
@ -37,11 +28,17 @@
|
||||
+ <span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
|
||||
+ </p>
|
||||
+ </div>
|
||||
+
|
||||
{errorMessage && (
|
||||
<div className='mt-8 p-4 bg-red-100 text-red-600 rounded-md dark:bg-red-200 dark:text-red-800'>
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
+
|
||||
<div className='isolate mx-auto mt-16 grid max-w-md grid-cols-1 gap-y-8 lg:gap-x-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3'>
|
||||
{Object.values(PaymentPlanId).map((planId) => (
|
||||
<div
|
||||
@@ -187,7 +195,7 @@
|
||||
@@ -201,7 +210,7 @@
|
||||
)}
|
||||
disabled={isPaymentLoading}
|
||||
>
|
||||
|
@ -66,7 +66,6 @@ app OpenSaaS {
|
||||
// configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
|
||||
// }
|
||||
},
|
||||
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
onAuthSucceededRedirectTo: "/demo-app",
|
||||
},
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { HttpError } from 'wasp/server';
|
||||
import type { OnAfterSignupHook } from 'wasp/server/auth';
|
||||
|
||||
export const onAfterSignup: OnAfterSignupHook = async ({ providerId, user, prisma }) => {
|
||||
// For Stripe to function correctly, we need a valid email associated with the user.
|
||||
// Discord allows an email address to be optional. If this is the case, we delete the user
|
||||
// from our DB and throw an error.
|
||||
if (providerId.providerName === 'discord' && !user.email) {
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
throw new HttpError(403, 'Discord user needs a valid email to sign up');
|
||||
}
|
||||
};
|
@ -3,19 +3,35 @@ import { defineUserSignupFields } from 'wasp/auth/providers/types';
|
||||
|
||||
const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
|
||||
|
||||
const emailDataSchema = z.object({
|
||||
email: z.string(),
|
||||
});
|
||||
|
||||
export const getEmailUserFields = defineUserSignupFields({
|
||||
username: (data: any) => data.email,
|
||||
isAdmin: (data: any) => adminEmails.includes(data.email),
|
||||
email: (data: any) => data.email,
|
||||
email: (data) => {
|
||||
const emailData = emailDataSchema.parse(data);
|
||||
return emailData.email;
|
||||
},
|
||||
username: (data) => {
|
||||
const emailData = emailDataSchema.parse(data);
|
||||
return emailData.email;
|
||||
},
|
||||
isAdmin: (data) => {
|
||||
const emailData = emailDataSchema.parse(data);
|
||||
return adminEmails.includes(emailData.email);
|
||||
},
|
||||
});
|
||||
|
||||
const githubDataSchema = z.object({
|
||||
profile: z.object({
|
||||
emails: z.array(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
})
|
||||
),
|
||||
emails: z
|
||||
.array(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
verified: z.boolean(),
|
||||
})
|
||||
)
|
||||
.min(1, 'You need to have an email address associated with your GitHub account to sign up.'),
|
||||
login: z.string(),
|
||||
}),
|
||||
});
|
||||
@ -23,7 +39,7 @@ const githubDataSchema = z.object({
|
||||
export const getGitHubUserFields = defineUserSignupFields({
|
||||
email: (data) => {
|
||||
const githubData = githubDataSchema.parse(data);
|
||||
return githubData.profile.emails[0].email;
|
||||
return getGithubEmailInfo(githubData).email;
|
||||
},
|
||||
username: (data) => {
|
||||
const githubData = githubDataSchema.parse(data);
|
||||
@ -31,10 +47,20 @@ export const getGitHubUserFields = defineUserSignupFields({
|
||||
},
|
||||
isAdmin: (data) => {
|
||||
const githubData = githubDataSchema.parse(data);
|
||||
return adminEmails.includes(githubData.profile.emails[0].email);
|
||||
const emailInfo = getGithubEmailInfo(githubData);
|
||||
if (!emailInfo.verified) {
|
||||
return false;
|
||||
}
|
||||
return adminEmails.includes(emailInfo.email);
|
||||
},
|
||||
});
|
||||
|
||||
// We are using the first email from the list of emails returned by GitHub.
|
||||
// If you want to use a different email, you can modify this function.
|
||||
function getGithubEmailInfo(githubData: z.infer<typeof githubDataSchema>) {
|
||||
return githubData.profile.emails[0];
|
||||
}
|
||||
|
||||
// NOTE: if we don't want to access users' emails, we can use scope ["user:read"]
|
||||
// instead of ["user"] and access args.profile.username instead
|
||||
export function getGitHubAuthConfig() {
|
||||
@ -46,6 +72,7 @@ export function getGitHubAuthConfig() {
|
||||
const googleDataSchema = z.object({
|
||||
profile: z.object({
|
||||
email: z.string(),
|
||||
email_verified: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -60,6 +87,9 @@ export const getGoogleUserFields = defineUserSignupFields({
|
||||
},
|
||||
isAdmin: (data) => {
|
||||
const googleData = googleDataSchema.parse(data);
|
||||
if (!googleData.profile.email_verified) {
|
||||
return false;
|
||||
}
|
||||
return adminEmails.includes(googleData.profile.email);
|
||||
},
|
||||
});
|
||||
@ -74,12 +104,17 @@ const discordDataSchema = z.object({
|
||||
profile: z.object({
|
||||
username: z.string(),
|
||||
email: z.string().email().nullable(),
|
||||
verified: z.boolean().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const getDiscordUserFields = defineUserSignupFields({
|
||||
email: (data) => {
|
||||
const discordData = discordDataSchema.parse(data);
|
||||
// Users need to have an email for payment processing.
|
||||
if (!discordData.profile.email) {
|
||||
throw new Error('You need to have an email address associated with your Discord account to sign up.');
|
||||
}
|
||||
return discordData.profile.email;
|
||||
},
|
||||
username: (data) => {
|
||||
@ -87,8 +122,11 @@ export const getDiscordUserFields = defineUserSignupFields({
|
||||
return discordData.profile.username;
|
||||
},
|
||||
isAdmin: (data) => {
|
||||
const email = discordDataSchema.parse(data).profile.email;
|
||||
return !!email && adminEmails.includes(email);
|
||||
const discordData = discordDataSchema.parse(data);
|
||||
if (!discordData.profile.email || !discordData.profile.verified) {
|
||||
return false;
|
||||
}
|
||||
return adminEmails.includes(discordData.profile.email);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -38,9 +38,11 @@ export const paymentPlanCards: Record<PaymentPlanId, PaymentPlanCard> = {
|
||||
|
||||
const PricingPage = () => {
|
||||
const [isPaymentLoading, setIsPaymentLoading] = useState<boolean>(false);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const { data: user } = useAuth();
|
||||
const isUserSubscribed = !!user && !!user.subscriptionStatus && user.subscriptionStatus !== SubscriptionStatus.Deleted;
|
||||
const isUserSubscribed =
|
||||
!!user && !!user.subscriptionStatus && user.subscriptionStatus !== SubscriptionStatus.Deleted;
|
||||
|
||||
const {
|
||||
data: customerPortalUrl,
|
||||
@ -65,8 +67,13 @@ const PricingPage = () => {
|
||||
} else {
|
||||
throw new Error('Error generating checkout session URL');
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
setErrorMessage(error.message);
|
||||
} else {
|
||||
setErrorMessage('Error processing payment. Please try again later.');
|
||||
}
|
||||
setIsPaymentLoading(false); // We only set this to false here and not in the try block because we redirect to the checkout url within the same window
|
||||
}
|
||||
}
|
||||
@ -78,11 +85,13 @@ const PricingPage = () => {
|
||||
}
|
||||
|
||||
if (customerPortalUrlError) {
|
||||
console.error('Error fetching customer portal url');
|
||||
setErrorMessage('Error fetching Customer Portal URL');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customerPortalUrl) {
|
||||
throw new Error(`Customer Portal does not exist for user ${user.id}`)
|
||||
setErrorMessage(`Customer Portal does not exist for user ${user.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(customerPortalUrl, '_blank');
|
||||
@ -101,6 +110,11 @@ const PricingPage = () => {
|
||||
out below with test credit card number <br />
|
||||
<span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
|
||||
</p>
|
||||
{errorMessage && (
|
||||
<div className='mt-8 p-4 bg-red-100 text-red-600 rounded-md dark:bg-red-200 dark:text-red-800'>
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<div className='isolate mx-auto mt-16 grid max-w-md grid-cols-1 gap-y-8 lg:gap-x-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3'>
|
||||
{Object.values(PaymentPlanId).map((planId) => (
|
||||
<div
|
||||
|
@ -25,10 +25,8 @@ export const generateCheckoutSession: GenerateCheckoutSession<
|
||||
const userId = context.user.id;
|
||||
const userEmail = context.user.email;
|
||||
if (!userEmail) {
|
||||
throw new HttpError(
|
||||
403,
|
||||
'User needs an email to make a payment. If using the usernameAndPassword Auth method, switch to an Auth method that provides an email.'
|
||||
);
|
||||
// If using the usernameAndPassword Auth method, switch to an Auth method that provides an email.
|
||||
throw new HttpError(403, 'User needs an email to make a payment.');
|
||||
}
|
||||
|
||||
const paymentPlan = paymentPlans[paymentPlanId];
|
||||
|
Loading…
x
Reference in New Issue
Block a user