Skip to content

Commit e12d5bf

Browse files
authored
Merge pull request #18 from will-lp1/improve-auth
Improve auth
2 parents 7995fbe + 89e9103 commit e12d5bf

File tree

5 files changed

+189
-38
lines changed

5 files changed

+189
-38
lines changed

apps/snow-leopard/app/(auth)/login/page.tsx

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ import { toast } from '@/components/toast';
88
import { AuthForm } from '@/components/auth-form';
99
import { SubmitButton } from '@/components/submit-button';
1010

11+
// Client-side check for enabled providers
12+
const googleEnabled = process.env.NEXT_PUBLIC_GOOGLE_ENABLED === 'true';
13+
const githubEnabled = process.env.NEXT_PUBLIC_GITHUB_ENABLED === 'true';
14+
1115
export default function LoginPage() {
1216
const router = useRouter();
13-
const [isLoading, setIsLoading] = useState(false);
17+
const [isEmailLoading, setIsEmailLoading] = useState(false);
18+
const [isSocialLoading, setIsSocialLoading] = useState<string | null>(null);
1419
const [isSuccessful, setIsSuccessful] = useState(false);
1520
const [email, setEmail] = useState('');
1621

@@ -25,11 +30,12 @@ export default function LoginPage() {
2530
callbackURL: "/documents"
2631
}, {
2732
onRequest: () => {
28-
setIsLoading(true);
33+
setIsEmailLoading(true);
2934
setIsSuccessful(false);
35+
setIsSocialLoading(null);
3036
},
3137
onSuccess: (ctx) => {
32-
setIsLoading(false);
38+
setIsEmailLoading(false);
3339
setIsSuccessful(true);
3440
toast({
3541
type: 'success',
@@ -38,7 +44,7 @@ export default function LoginPage() {
3844
router.refresh();
3945
},
4046
onError: (ctx) => {
41-
setIsLoading(false);
47+
setIsEmailLoading(false);
4248
setIsSuccessful(false);
4349
console.error("Email Login Error:", ctx.error);
4450
toast({
@@ -49,6 +55,29 @@ export default function LoginPage() {
4955
});
5056
};
5157

58+
const handleSocialLogin = async (provider: 'google' | 'github') => {
59+
await authClient.signIn.social({
60+
provider,
61+
callbackURL: "/documents",
62+
errorCallbackURL: "/login?error=social_signin_failed",
63+
}, {
64+
onRequest: () => {
65+
setIsSocialLoading(provider);
66+
setIsSuccessful(false);
67+
setIsEmailLoading(false);
68+
},
69+
onError: (ctx) => {
70+
setIsSocialLoading(null);
71+
setIsSuccessful(false);
72+
console.error(`Social Login Error (${provider}):`, ctx.error);
73+
toast({
74+
type: 'error',
75+
description: ctx.error.message || `Failed to sign in with ${provider}.`,
76+
});
77+
},
78+
});
79+
};
80+
5281
return (
5382
<div className="flex h-dvh w-screen items-start pt-12 md:pt-0 md:items-center justify-center bg-background">
5483
<div className="w-full max-w-md overflow-hidden rounded-2xl flex flex-col gap-12">
@@ -60,7 +89,16 @@ export default function LoginPage() {
6089
</div>
6190

6291
<div className="px-8">
63-
<AuthForm action={handleEmailLogin} defaultEmail={email}>
92+
<AuthForm
93+
action={handleEmailLogin}
94+
defaultEmail={email}
95+
showSocialLogins={true}
96+
googleEnabled={googleEnabled}
97+
githubEnabled={githubEnabled}
98+
onSocialLogin={handleSocialLogin}
99+
isSocialLoading={isSocialLoading}
100+
isEmailLoading={isEmailLoading}
101+
>
64102
<SubmitButton
65103
isSuccessful={isSuccessful}
66104
>

apps/snow-leopard/app/(auth)/register/page.tsx

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ import { useRouter } from 'next/navigation';
55
import { useState } from 'react';
66
import { authClient } from '@/lib/auth-client';
77
import { toast } from '@/components/toast';
8-
import { AuthForm } from '@/components/auth-form';
8+
import { AuthForm } from '@/components/auth-form';
99
import { SubmitButton } from '@/components/submit-button';
1010

11+
const googleEnabled = process.env.NEXT_PUBLIC_GOOGLE_ENABLED === 'true';
12+
const githubEnabled = process.env.NEXT_PUBLIC_GITHUB_ENABLED === 'true';
13+
const emailVerificationEnabled = process.env.NEXT_PUBLIC_EMAIL_VERIFY_ENABLED === 'true';
14+
1115
export default function RegisterPage() {
1216
const router = useRouter();
13-
const [isLoading, setIsLoading] = useState(false);
17+
const [isEmailLoading, setIsEmailLoading] = useState(false);
18+
const [isSocialLoading, setIsSocialLoading] = useState<string | null>(null);
1419
const [isSuccessful, setIsSuccessful] = useState(false);
1520
const [email, setEmail] = useState('');
1621

@@ -19,8 +24,9 @@ export default function RegisterPage() {
1924
const password = formData.get('password') as string;
2025
const name = emailValue.split('@')[0] || 'User';
2126
setEmail(emailValue);
22-
setIsLoading(true);
27+
setIsEmailLoading(true);
2328
setIsSuccessful(false);
29+
setIsSocialLoading(null);
2430

2531
await authClient.signUp.email({
2632
email: emailValue,
@@ -30,11 +36,8 @@ export default function RegisterPage() {
3036
onRequest: () => {
3137
},
3238
onSuccess: (ctx) => {
33-
setIsLoading(false);
34-
35-
const isVerificationEnabled = process.env.NEXT_PUBLIC_EMAIL_VERIFY_ENABLED === 'true';
36-
37-
if (isVerificationEnabled) {
39+
setIsEmailLoading(false);
40+
if (emailVerificationEnabled) {
3841
setIsSuccessful(false);
3942
toast({
4043
type: 'success',
@@ -47,11 +50,11 @@ export default function RegisterPage() {
4750
type: 'success',
4851
description: 'Account created! Redirecting...'
4952
});
50-
router.push('/documents');
53+
router.push('/documents');
5154
}
5255
},
5356
onError: (ctx) => {
54-
setIsLoading(false);
57+
setIsEmailLoading(false);
5558
setIsSuccessful(false);
5659
console.error("Email Signup Error:", ctx.error);
5760
toast({
@@ -62,6 +65,29 @@ export default function RegisterPage() {
6265
});
6366
};
6467

68+
const handleSocialLogin = async (provider: 'google' | 'github') => {
69+
await authClient.signIn.social({
70+
provider,
71+
callbackURL: "/documents",
72+
errorCallbackURL: "/register?error=social_signin_failed",
73+
}, {
74+
onRequest: () => {
75+
setIsSocialLoading(provider);
76+
setIsSuccessful(false);
77+
setIsEmailLoading(false);
78+
},
79+
onError: (ctx) => {
80+
setIsSocialLoading(null);
81+
setIsSuccessful(false);
82+
console.error(`Social Sign Up/In Error (${provider}):`, ctx.error);
83+
toast({
84+
type: 'error',
85+
description: ctx.error.message || `Failed to sign up/in with ${provider}.`,
86+
});
87+
},
88+
});
89+
};
90+
6591
return (
6692
<div className="flex h-dvh w-screen items-start pt-12 md:pt-0 md:items-center justify-center bg-background">
6793
<div className="w-full max-w-md overflow-hidden rounded-2xl flex flex-col gap-12">
@@ -72,9 +98,18 @@ export default function RegisterPage() {
7298
</p>
7399
</div>
74100

75-
<div className="px-8">
76-
<AuthForm action={handleEmailSignup} defaultEmail={email}>
77-
<SubmitButton
101+
<div className="px-8 flex flex-col gap-6">
102+
<AuthForm
103+
action={handleEmailSignup}
104+
defaultEmail={email}
105+
showSocialLogins={true}
106+
googleEnabled={googleEnabled}
107+
githubEnabled={githubEnabled}
108+
onSocialLogin={handleSocialLogin}
109+
isSocialLoading={isSocialLoading}
110+
isEmailLoading={isEmailLoading}
111+
>
112+
<SubmitButton
78113
isSuccessful={isSuccessful}
79114
>
80115
Sign Up

apps/snow-leopard/components/auth-form.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,36 @@
22

33
import { Input } from '@/components/ui/input';
44
import { Label } from '@/components/ui/label';
5+
import { Button } from '@/components/ui/button';
6+
import { LogoGoogle, GitIcon, LoaderIcon } from '@/components/icons';
57
import type { ReactNode } from 'react';
68

79
interface AuthFormProps {
810
action: (formData: FormData) => void;
911
defaultEmail?: string;
1012
children: ReactNode;
13+
showSocialLogins?: boolean;
14+
googleEnabled?: boolean;
15+
githubEnabled?: boolean;
16+
onSocialLogin?: (provider: 'google' | 'github') => void;
17+
isSocialLoading?: string | null;
18+
isEmailLoading?: boolean;
1119
}
1220

1321
export function AuthForm({
1422
action,
1523
defaultEmail = '',
1624
children,
25+
showSocialLogins = false,
26+
googleEnabled = false,
27+
githubEnabled = false,
28+
onSocialLogin = () => {},
29+
isSocialLoading = null,
30+
isEmailLoading = false,
1731
}: AuthFormProps) {
32+
const anySocialEnabled = googleEnabled || githubEnabled;
33+
const isLoading = !!isSocialLoading || isEmailLoading;
34+
1835
return (
1936
<form action={action} className="flex flex-col gap-6 px-4 sm:px-16">
2037
<div className="flex flex-col gap-2">
@@ -39,6 +56,46 @@ export function AuthForm({
3956
/>
4057
</div>
4158
{children}
59+
60+
{showSocialLogins && anySocialEnabled && (
61+
<>
62+
<div className="relative">
63+
<div className="absolute inset-0 flex items-center">
64+
<span className="w-full border-t" />
65+
</div>
66+
<div className="relative flex justify-center text-xs uppercase">
67+
<span className="bg-background px-2 text-muted-foreground">
68+
Or continue with
69+
</span>
70+
</div>
71+
</div>
72+
73+
<div className="flex flex-col sm:flex-row gap-2">
74+
{googleEnabled && (
75+
<Button
76+
type="button"
77+
variant="outline"
78+
className="w-full"
79+
onClick={() => onSocialLogin('google')}
80+
disabled={isLoading}
81+
>
82+
{isSocialLoading === 'google' ? (<span className="mr-2 h-4 w-4"><LoaderIcon size={16} /></span>) : (<span className="mr-2 h-4 w-4"><LogoGoogle size={16} /></span>)} Google
83+
</Button>
84+
)}
85+
{githubEnabled && (
86+
<Button
87+
type="button"
88+
variant="outline"
89+
className="w-full"
90+
onClick={() => onSocialLogin('github')}
91+
disabled={isLoading}
92+
>
93+
{isSocialLoading === 'github' ? (<span className="mr-2 h-4 w-4"><LoaderIcon size={16} /></span>) : (<span className="mr-2 h-4 w-4"><GitIcon /></span>)} GitHub
94+
</Button>
95+
)}
96+
</div>
97+
</>
98+
)}
4299
</form>
43100
);
44101
}

apps/snow-leopard/components/submit-button.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ export function SubmitButton({ isSuccessful, children }: SubmitButtonProps) {
2222
Done
2323
</>
2424
) : (
25-
<>
26-
<LoaderIcon size={16} />
27-
{children}
28-
</>
25+
children
2926
)}
3027
</Button>
3128
);

apps/snow-leopard/lib/auth.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,38 @@
11
import 'server-only';
22
import { betterAuth } from 'better-auth';
3-
// Use the adapter import path potentially provided by the core library or a plugin
43
import { drizzleAdapter } from "better-auth/adapters/drizzle";
54
import { nextCookies } from 'better-auth/next-js';
6-
import { db, user as schemaUser } from '@snow-leopard/db'; // Use the actual package name
7-
import * as schema from '@snow-leopard/db'; // Import all exports if needed, or specific tables
8-
import Stripe from "stripe"; // Import Stripe SDK
9-
import { stripe } from "@better-auth/stripe"; // Import Better Auth Stripe plugin
10-
import { Resend } from 'resend'; // Import Resend SDK
11-
// import { count } from 'drizzle-orm'; // No longer needed for test query
12-
13-
// Check for required environment variables
5+
import { db, user as schemaUser } from '@snow-leopard/db';
6+
import * as schema from '@snow-leopard/db';
7+
import Stripe from "stripe";
8+
import { stripe } from "@better-auth/stripe";
9+
import { Resend } from 'resend';
10+
11+
const googleEnabled = process.env.GOOGLE_ENABLED === 'true';
12+
const githubEnabled = process.env.GITHUB_ENABLED === 'true';
13+
1414
if (!process.env.BETTER_AUTH_SECRET) {
1515
throw new Error('Missing BETTER_AUTH_SECRET environment variable');
1616
}
1717
if (!process.env.BETTER_AUTH_URL) {
1818
throw new Error('Missing BETTER_AUTH_URL environment variable');
1919
}
2020

21-
// Only check Stripe keys if Stripe is explicitly enabled
2221
if (process.env.STRIPE_ENABLED === 'true') {
2322
if (!process.env.STRIPE_SECRET_KEY) throw new Error('Missing STRIPE_SECRET_KEY but STRIPE_ENABLED is true');
2423
if (!process.env.STRIPE_WEBHOOK_SECRET) throw new Error('Missing STRIPE_WEBHOOK_SECRET but STRIPE_ENABLED is true');
2524
if (!process.env.STRIPE_PRO_MONTHLY_PRICE_ID) throw new Error('Missing STRIPE_PRO_MONTHLY_PRICE_ID but STRIPE_ENABLED is true'); // Example Price ID env var
2625
if (!process.env.STRIPE_PRO_YEARLY_PRICE_ID) throw new Error('Missing STRIPE_PRO_YEARLY_PRICE_ID but STRIPE_ENABLED is true'); // Add check for yearly price ID
2726
}
2827

29-
// ---> Add check for email verification enabled <---
3028
const emailVerificationEnabled = process.env.EMAIL_VERIFY_ENABLED === 'true';
3129
console.log(`Email Verification Enabled: ${emailVerificationEnabled}`);
3230

33-
// Check Resend/Email From keys ONLY if verification is enabled
3431
if (emailVerificationEnabled) {
3532
if (!process.env.RESEND_API_KEY) throw new Error('Missing RESEND_API_KEY because EMAIL_VERIFY_ENABLED is true');
3633
if (!process.env.EMAIL_FROM) throw new Error('Missing EMAIL_FROM because EMAIL_VERIFY_ENABLED is true');
3734
}
3835

39-
// Environment Variable Checks (Ensure all required vars are present)
4036
if (process.env.STRIPE_ENABLED === 'true') {
4137
if (!process.env.STRIPE_SECRET_KEY) {
4238
throw new Error("STRIPE_SECRET_KEY environment variable is missing but STRIPE_ENABLED is true.");
@@ -52,6 +48,17 @@ if (process.env.STRIPE_ENABLED === 'true') {
5248
}
5349
}
5450

51+
if (googleEnabled) {
52+
if (!process.env.GOOGLE_CLIENT_ID) throw new Error('Missing GOOGLE_CLIENT_ID because GOOGLE_ENABLED is true');
53+
if (!process.env.GOOGLE_CLIENT_SECRET) throw new Error('Missing GOOGLE_CLIENT_SECRET because GOOGLE_ENABLED is true');
54+
console.log('Google OAuth ENABLED');
55+
}
56+
57+
if (githubEnabled) {
58+
if (!process.env.GITHUB_CLIENT_ID) throw new Error('Missing GITHUB_CLIENT_ID because GITHUB_ENABLED is true');
59+
if (!process.env.GITHUB_CLIENT_SECRET) throw new Error('Missing GITHUB_CLIENT_SECRET because GITHUB_ENABLED is true');
60+
console.log('GitHub OAuth ENABLED');
61+
}
5562

5663
console.log('--- Checking env vars in lib/auth.ts ---');
5764
console.log('DATABASE_URL:', process.env.DATABASE_URL ? 'Set' : 'MISSING!');
@@ -115,14 +122,31 @@ if (process.env.STRIPE_ENABLED === 'true') {
115122
console.log('Stripe is DISABLED. Skipping Stripe plugin for Better Auth.');
116123
}
117124

118-
// Ensure the nextCookies plugin is always last in the chain
119125
authPlugins.push(nextCookies());
120126

127+
const socialProviders: Record<string, any> = {};
128+
129+
if (googleEnabled) {
130+
socialProviders.google = {
131+
clientId: process.env.GOOGLE_CLIENT_ID!,
132+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
133+
};
134+
}
135+
136+
if (githubEnabled) {
137+
socialProviders.github = {
138+
clientId: process.env.GITHUB_CLIENT_ID!,
139+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
140+
};
141+
}
142+
121143
export const auth = betterAuth({
122144
database: drizzleAdapter(db, {
123145
provider: 'pg',
124-
}),
125-
146+
}),
147+
148+
socialProviders,
149+
126150
emailAndPassword: {
127151
enabled: true,
128152
requireEmailVerification: emailVerificationEnabled,

0 commit comments

Comments
 (0)