mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-07-07 14:00:28 +02:00
Handle missing Discord email sooner. Make sure emails are verified.
This commit is contained in:
@ -88,9 +88,9 @@
|
|||||||
+ configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
|
+ configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
|
||||||
+ }
|
+ }
|
||||||
},
|
},
|
||||||
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
|
|
||||||
onAuthFailedRedirectTo: "/login",
|
onAuthFailedRedirectTo: "/login",
|
||||||
@@ -87,11 +83,11 @@
|
onAuthSucceededRedirectTo: "/demo-app",
|
||||||
|
@@ -86,11 +82,11 @@
|
||||||
// NOTE: "Dummy" provider is just for local development purposes.
|
// 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)!
|
// 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/ .
|
// 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 {
|
api paymentsWebhook {
|
||||||
|
@ -1,46 +1,64 @@
|
|||||||
--- template/app/src/auth/userSignupFields.ts
|
--- template/app/src/auth/userSignupFields.ts
|
||||||
+++ opensaas-sh/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 { z } from 'zod';
|
||||||
import { defineUserSignupFields } from 'wasp/auth/providers/types';
|
import { defineUserSignupFields } from 'wasp/auth/providers/types';
|
||||||
|
|
||||||
-const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
|
-const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
|
||||||
-
|
-
|
||||||
export const getEmailUserFields = defineUserSignupFields({
|
const emailDataSchema = z.object({
|
||||||
username: (data: any) => data.email,
|
email: z.string(),
|
||||||
- isAdmin: (data: any) => adminEmails.includes(data.email),
|
});
|
||||||
email: (data: any) => data.email,
|
@@ -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);
|
const githubData = githubDataSchema.parse(data);
|
||||||
return githubData.profile.login;
|
return githubData.profile.login;
|
||||||
},
|
},
|
||||||
- isAdmin: (data) => {
|
- isAdmin: (data) => {
|
||||||
- const githubData = githubDataSchema.parse(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"]
|
// We are using the first email from the list of emails returned by GitHub.
|
||||||
@@ -58,10 +51,6 @@
|
@@ -85,13 +71,6 @@
|
||||||
const googleData = googleDataSchema.parse(data);
|
const googleData = googleDataSchema.parse(data);
|
||||||
return googleData.profile.email;
|
return googleData.profile.email;
|
||||||
},
|
},
|
||||||
- isAdmin: (data) => {
|
- isAdmin: (data) => {
|
||||||
- const googleData = googleDataSchema.parse(data);
|
- const googleData = googleDataSchema.parse(data);
|
||||||
|
- if (!googleData.profile.email_verified) {
|
||||||
|
- return false;
|
||||||
|
- }
|
||||||
- return adminEmails.includes(googleData.profile.email);
|
- return adminEmails.includes(googleData.profile.email);
|
||||||
- },
|
- },
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getGoogleAuthConfig() {
|
export function getGoogleAuthConfig() {
|
||||||
@@ -86,10 +75,6 @@
|
@@ -121,13 +100,6 @@
|
||||||
const discordData = discordDataSchema.parse(data);
|
const discordData = discordDataSchema.parse(data);
|
||||||
return discordData.profile.username;
|
return discordData.profile.username;
|
||||||
},
|
},
|
||||||
- isAdmin: (data) => {
|
- isAdmin: (data) => {
|
||||||
- const email = discordDataSchema.parse(data).profile.email;
|
- const discordData = discordDataSchema.parse(data);
|
||||||
- return !!email && adminEmails.includes(email);
|
- if (!discordData.profile.email || !discordData.profile.verified) {
|
||||||
|
- return false;
|
||||||
|
- }
|
||||||
|
- return adminEmails.includes(discordData.profile.email);
|
||||||
- },
|
- },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8,16 +8,7 @@
|
|||||||
|
|
||||||
interface PaymentPlanCard {
|
interface PaymentPlanCard {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -82,7 +83,7 @@
|
@@ -105,16 +106,24 @@
|
||||||
}
|
|
||||||
|
|
||||||
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 @@
|
|
||||||
Pick your <span className='text-yellow-500'>pricing</span>
|
Pick your <span className='text-yellow-500'>pricing</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</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>
|
+ <span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
|
||||||
+ </p>
|
+ </p>
|
||||||
+ </div>
|
+ </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'>
|
<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) => (
|
{Object.values(PaymentPlanId).map((planId) => (
|
||||||
<div
|
<div
|
||||||
@@ -187,7 +195,7 @@
|
@@ -201,7 +210,7 @@
|
||||||
)}
|
)}
|
||||||
disabled={isPaymentLoading}
|
disabled={isPaymentLoading}
|
||||||
>
|
>
|
||||||
|
@ -66,7 +66,6 @@ app OpenSaaS {
|
|||||||
// configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
|
// configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
|
||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
|
|
||||||
onAuthFailedRedirectTo: "/login",
|
onAuthFailedRedirectTo: "/login",
|
||||||
onAuthSucceededRedirectTo: "/demo-app",
|
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 adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
|
||||||
|
|
||||||
|
const emailDataSchema = z.object({
|
||||||
|
email: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const getEmailUserFields = defineUserSignupFields({
|
export const getEmailUserFields = defineUserSignupFields({
|
||||||
username: (data: any) => data.email,
|
email: (data) => {
|
||||||
isAdmin: (data: any) => adminEmails.includes(data.email),
|
const emailData = emailDataSchema.parse(data);
|
||||||
email: (data: any) => data.email,
|
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({
|
const githubDataSchema = z.object({
|
||||||
profile: z.object({
|
profile: z.object({
|
||||||
emails: z.array(
|
emails: z
|
||||||
z.object({
|
.array(
|
||||||
email: z.string(),
|
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(),
|
login: z.string(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -23,7 +39,7 @@ const githubDataSchema = z.object({
|
|||||||
export const getGitHubUserFields = defineUserSignupFields({
|
export const getGitHubUserFields = defineUserSignupFields({
|
||||||
email: (data) => {
|
email: (data) => {
|
||||||
const githubData = githubDataSchema.parse(data);
|
const githubData = githubDataSchema.parse(data);
|
||||||
return githubData.profile.emails[0].email;
|
return getGithubEmailInfo(githubData).email;
|
||||||
},
|
},
|
||||||
username: (data) => {
|
username: (data) => {
|
||||||
const githubData = githubDataSchema.parse(data);
|
const githubData = githubDataSchema.parse(data);
|
||||||
@ -31,10 +47,20 @@ export const getGitHubUserFields = defineUserSignupFields({
|
|||||||
},
|
},
|
||||||
isAdmin: (data) => {
|
isAdmin: (data) => {
|
||||||
const githubData = githubDataSchema.parse(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"]
|
// 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
|
// instead of ["user"] and access args.profile.username instead
|
||||||
export function getGitHubAuthConfig() {
|
export function getGitHubAuthConfig() {
|
||||||
@ -46,6 +72,7 @@ export function getGitHubAuthConfig() {
|
|||||||
const googleDataSchema = z.object({
|
const googleDataSchema = z.object({
|
||||||
profile: z.object({
|
profile: z.object({
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
|
email_verified: z.boolean(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -60,6 +87,9 @@ export const getGoogleUserFields = defineUserSignupFields({
|
|||||||
},
|
},
|
||||||
isAdmin: (data) => {
|
isAdmin: (data) => {
|
||||||
const googleData = googleDataSchema.parse(data);
|
const googleData = googleDataSchema.parse(data);
|
||||||
|
if (!googleData.profile.email_verified) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return adminEmails.includes(googleData.profile.email);
|
return adminEmails.includes(googleData.profile.email);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -74,12 +104,17 @@ const discordDataSchema = z.object({
|
|||||||
profile: z.object({
|
profile: z.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
email: z.string().email().nullable(),
|
email: z.string().email().nullable(),
|
||||||
|
verified: z.boolean().nullable(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getDiscordUserFields = defineUserSignupFields({
|
export const getDiscordUserFields = defineUserSignupFields({
|
||||||
email: (data) => {
|
email: (data) => {
|
||||||
const discordData = discordDataSchema.parse(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;
|
return discordData.profile.email;
|
||||||
},
|
},
|
||||||
username: (data) => {
|
username: (data) => {
|
||||||
@ -87,8 +122,11 @@ export const getDiscordUserFields = defineUserSignupFields({
|
|||||||
return discordData.profile.username;
|
return discordData.profile.username;
|
||||||
},
|
},
|
||||||
isAdmin: (data) => {
|
isAdmin: (data) => {
|
||||||
const email = discordDataSchema.parse(data).profile.email;
|
const discordData = discordDataSchema.parse(data);
|
||||||
return !!email && adminEmails.includes(email);
|
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 PricingPage = () => {
|
||||||
const [isPaymentLoading, setIsPaymentLoading] = useState<boolean>(false);
|
const [isPaymentLoading, setIsPaymentLoading] = useState<boolean>(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: user } = useAuth();
|
const { data: user } = useAuth();
|
||||||
const isUserSubscribed = !!user && !!user.subscriptionStatus && user.subscriptionStatus !== SubscriptionStatus.Deleted;
|
const isUserSubscribed =
|
||||||
|
!!user && !!user.subscriptionStatus && user.subscriptionStatus !== SubscriptionStatus.Deleted;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: customerPortalUrl,
|
data: customerPortalUrl,
|
||||||
@ -65,8 +67,13 @@ const PricingPage = () => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('Error generating checkout session URL');
|
throw new Error('Error generating checkout session URL');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error(error);
|
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
|
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) {
|
if (customerPortalUrlError) {
|
||||||
console.error('Error fetching customer portal url');
|
setErrorMessage('Error fetching Customer Portal URL');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!customerPortalUrl) {
|
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');
|
window.open(customerPortalUrl, '_blank');
|
||||||
@ -101,6 +110,11 @@ const PricingPage = () => {
|
|||||||
out below with test credit card number <br />
|
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>
|
<span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
|
||||||
</p>
|
</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'>
|
<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) => (
|
{Object.values(PaymentPlanId).map((planId) => (
|
||||||
<div
|
<div
|
||||||
|
@ -25,10 +25,8 @@ export const generateCheckoutSession: GenerateCheckoutSession<
|
|||||||
const userId = context.user.id;
|
const userId = context.user.id;
|
||||||
const userEmail = context.user.email;
|
const userEmail = context.user.email;
|
||||||
if (!userEmail) {
|
if (!userEmail) {
|
||||||
throw new HttpError(
|
// If using the usernameAndPassword Auth method, switch to an Auth method that provides an email.
|
||||||
403,
|
throw new HttpError(403, 'User needs an email to make a payment.');
|
||||||
'User needs an email to make a payment. If using the usernameAndPassword Auth method, switch to an Auth method that provides an email.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentPlan = paymentPlans[paymentPlanId];
|
const paymentPlan = paymentPlans[paymentPlanId];
|
||||||
|
Reference in New Issue
Block a user