Handle missing Discord email sooner. Make sure emails are verified.

This commit is contained in:
Mihovil Ilakovac 2025-02-26 15:25:17 +01:00
parent dd334e28c6
commit 66d1155a0d
8 changed files with 112 additions and 64 deletions

View File

@ -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 {

View File

@ -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);
- },
});

View File

@ -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}
>

View File

@ -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",
},

View File

@ -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');
}
};

View File

@ -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);
},
});

View File

@ -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

View File

@ -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];