Skip to content

Commit 3d71370

Browse files
authored
improve UX/DX of auth flow (#127)
* fix: docker setup * improve UX/DX of auth flow * refactor(auth): clean up verification page logic
1 parent 5e11c75 commit 3d71370

File tree

14 files changed

+897
-560
lines changed

14 files changed

+897
-560
lines changed

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ WORKDIR /app
66
# Copy the entire sim directory
77
COPY sim/ ./
88

9+
# Create the .env file if it doesn't exist
10+
RUN touch .env
11+
912
# Install dependencies
1013
RUN npm install
1114

docker-compose.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ services:
1717
- POSTGRES_URL=postgresql://postgres:postgres@db:5432/simstudio
1818
- BETTER_AUTH_URL=http://localhost:3000
1919
- NEXT_PUBLIC_APP_URL=http://localhost:3000
20+
- BETTER_AUTH_SECRET=your_auth_secret_here
21+
- ENCRYPTION_KEY=your_encryption_key_here
22+
- GOOGLE_CLIENT_ID=placeholder
23+
- GOOGLE_CLIENT_SECRET=placeholder
24+
- GITHUB_CLIENT_ID=placeholder
25+
- GITHUB_CLIENT_SECRET=placeholder
26+
- RESEND_API_KEY=placeholder
27+
- WEBCONTAINER_CLIENT_ID=placeholder
2028
depends_on:
2129
db:
2230
condition: service_healthy
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use server'
2+
3+
export async function getOAuthProviderStatus() {
4+
const githubAvailable = !!(
5+
process.env.GITHUB_CLIENT_ID &&
6+
process.env.GITHUB_CLIENT_SECRET &&
7+
process.env.GITHUB_CLIENT_ID !== 'placeholder' &&
8+
process.env.GITHUB_CLIENT_SECRET !== 'placeholder'
9+
)
10+
11+
const googleAvailable = !!(
12+
process.env.GOOGLE_CLIENT_ID &&
13+
process.env.GOOGLE_CLIENT_SECRET &&
14+
process.env.GOOGLE_CLIENT_ID !== 'placeholder' &&
15+
process.env.GOOGLE_CLIENT_SECRET !== 'placeholder'
16+
)
17+
18+
const isProduction = process.env.NODE_ENV === 'production'
19+
20+
return { githubAvailable, googleAvailable, isProduction }
21+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { GithubIcon, GoogleIcon } from '@/components/icons'
5+
import { Button } from '@/components/ui/button'
6+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
7+
import { client } from '@/lib/auth-client'
8+
import { useNotificationStore } from '@/stores/notifications/store'
9+
10+
interface SocialLoginButtonsProps {
11+
githubAvailable: boolean
12+
googleAvailable: boolean
13+
callbackURL?: string
14+
isProduction: boolean
15+
}
16+
17+
export function SocialLoginButtons({
18+
githubAvailable,
19+
googleAvailable,
20+
callbackURL = '/w',
21+
isProduction,
22+
}: SocialLoginButtonsProps) {
23+
const [isGithubLoading, setIsGithubLoading] = useState(false)
24+
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
25+
const { addNotification } = useNotificationStore()
26+
27+
async function signInWithGithub() {
28+
if (!githubAvailable) return
29+
30+
setIsGithubLoading(true)
31+
try {
32+
await client.signIn.social({ provider: 'github', callbackURL })
33+
} catch (err: any) {
34+
let errorMessage = 'Failed to sign in with GitHub'
35+
36+
if (err.message?.includes('account exists')) {
37+
errorMessage = 'An account with this email already exists. Please sign in instead.'
38+
} else if (err.message?.includes('cancelled')) {
39+
errorMessage = 'GitHub sign in was cancelled. Please try again.'
40+
} else if (err.message?.includes('network')) {
41+
errorMessage = 'Network error. Please check your connection and try again.'
42+
} else if (err.message?.includes('rate limit')) {
43+
errorMessage = 'Too many attempts. Please try again later.'
44+
}
45+
46+
addNotification('error', errorMessage, null)
47+
} finally {
48+
setIsGithubLoading(false)
49+
}
50+
}
51+
52+
async function signInWithGoogle() {
53+
if (!googleAvailable) return
54+
55+
setIsGoogleLoading(true)
56+
try {
57+
await client.signIn.social({ provider: 'google', callbackURL })
58+
} catch (err: any) {
59+
let errorMessage = 'Failed to sign in with Google'
60+
61+
if (err.message?.includes('account exists')) {
62+
errorMessage = 'An account with this email already exists. Please sign in instead.'
63+
} else if (err.message?.includes('cancelled')) {
64+
errorMessage = 'Google sign in was cancelled. Please try again.'
65+
} else if (err.message?.includes('network')) {
66+
errorMessage = 'Network error. Please check your connection and try again.'
67+
} else if (err.message?.includes('rate limit')) {
68+
errorMessage = 'Too many attempts. Please try again later.'
69+
}
70+
71+
addNotification('error', errorMessage, null)
72+
} finally {
73+
setIsGoogleLoading(false)
74+
}
75+
}
76+
77+
const githubButton = (
78+
<Button
79+
variant="outline"
80+
className="w-full"
81+
disabled={!githubAvailable || isGithubLoading}
82+
onClick={signInWithGithub}
83+
>
84+
<GithubIcon className="mr-2 h-4 w-4" />
85+
{isGithubLoading ? 'Connecting...' : 'Continue with GitHub'}
86+
</Button>
87+
)
88+
89+
const googleButton = (
90+
<Button
91+
variant="outline"
92+
className="w-full"
93+
disabled={!googleAvailable || isGoogleLoading}
94+
onClick={signInWithGoogle}
95+
>
96+
<GoogleIcon className="mr-2 h-4 w-4" />
97+
{isGoogleLoading ? 'Connecting...' : 'Continue with Google'}
98+
</Button>
99+
)
100+
101+
// Early return for production mode
102+
if (isProduction) return null
103+
104+
const renderGithubButton = () => {
105+
if (githubAvailable) return githubButton
106+
107+
return (
108+
<TooltipProvider>
109+
<Tooltip>
110+
<TooltipTrigger asChild>
111+
<div>{githubButton}</div>
112+
</TooltipTrigger>
113+
<TooltipContent>
114+
<p>
115+
GitHub login requires OAuth credentials to be configured. Add the following
116+
environment variables:
117+
</p>
118+
<ul className="mt-2 text-xs space-y-1">
119+
<li>• GITHUB_CLIENT_ID</li>
120+
<li>• GITHUB_CLIENT_SECRET</li>
121+
</ul>
122+
</TooltipContent>
123+
</Tooltip>
124+
</TooltipProvider>
125+
)
126+
}
127+
128+
const renderGoogleButton = () => {
129+
if (googleAvailable) return googleButton
130+
131+
return (
132+
<TooltipProvider>
133+
<Tooltip>
134+
<TooltipTrigger asChild>
135+
<div>{googleButton}</div>
136+
</TooltipTrigger>
137+
<TooltipContent>
138+
<p>
139+
Google login requires OAuth credentials to be configured. Add the following
140+
environment variables:
141+
</p>
142+
<ul className="mt-2 text-xs space-y-1">
143+
<li>• GOOGLE_CLIENT_ID</li>
144+
<li>• GOOGLE_CLIENT_SECRET</li>
145+
</ul>
146+
</TooltipContent>
147+
</Tooltip>
148+
</TooltipProvider>
149+
)
150+
}
151+
152+
return (
153+
<div className="grid gap-2">
154+
{renderGithubButton()}
155+
{renderGoogleButton()}
156+
</div>
157+
)
158+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import Link from 'next/link'
5+
import { useRouter } from 'next/navigation'
6+
import { Button } from '@/components/ui/button'
7+
import {
8+
Card,
9+
CardContent,
10+
CardDescription,
11+
CardFooter,
12+
CardHeader,
13+
CardTitle,
14+
} from '@/components/ui/card'
15+
import { Input } from '@/components/ui/input'
16+
import { Label } from '@/components/ui/label'
17+
import { client } from '@/lib/auth-client'
18+
import { useNotificationStore } from '@/stores/notifications/store'
19+
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
20+
import { NotificationList } from '@/app/w/[id]/components/notifications/notifications'
21+
22+
export default function LoginPage({
23+
githubAvailable,
24+
googleAvailable,
25+
isProduction,
26+
}: {
27+
githubAvailable: boolean
28+
googleAvailable: boolean
29+
isProduction: boolean
30+
}) {
31+
const router = useRouter()
32+
const [isLoading, setIsLoading] = useState(false)
33+
const [mounted, setMounted] = useState(false)
34+
const { addNotification } = useNotificationStore()
35+
36+
useEffect(() => {
37+
setMounted(true)
38+
}, [])
39+
40+
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
41+
e.preventDefault()
42+
setIsLoading(true)
43+
44+
const formData = new FormData(e.currentTarget)
45+
const email = formData.get('email') as string
46+
const password = formData.get('password') as string
47+
48+
try {
49+
const result = await client.signIn.email({
50+
email,
51+
password,
52+
callbackURL: '/w',
53+
})
54+
55+
if (!result || result.error) {
56+
throw new Error(result?.error?.message || 'Authentication failed')
57+
}
58+
} catch (err: any) {
59+
let errorMessage = 'Invalid email or password'
60+
61+
if (err.message?.includes('not verified')) {
62+
// Redirect to verification page directly without asking for confirmation
63+
try {
64+
// Send a new verification OTP
65+
await client.emailOtp.sendVerificationOtp({
66+
email,
67+
type: 'email-verification',
68+
})
69+
70+
// Redirect to the verify page
71+
router.push(`/verify?email=${encodeURIComponent(email)}`)
72+
return
73+
} catch (verifyErr) {
74+
errorMessage = 'Failed to send verification code. Please try again later.'
75+
}
76+
} else if (err.message?.includes('not found')) {
77+
errorMessage = 'No account found with this email. Please sign up first.'
78+
} else if (err.message?.includes('invalid password')) {
79+
errorMessage = 'Invalid password. Please try again or use the forgot password link.'
80+
} else if (err.message?.includes('too many attempts')) {
81+
errorMessage = 'Too many login attempts. Please try again later or reset your password.'
82+
} else if (err.message?.includes('account locked')) {
83+
errorMessage = 'Your account has been locked for security. Please reset your password.'
84+
} else if (err.message?.includes('network')) {
85+
errorMessage = 'Network error. Please check your connection and try again.'
86+
} else if (err.message?.includes('rate limit')) {
87+
errorMessage = 'Too many requests. Please wait a moment before trying again.'
88+
}
89+
90+
addNotification('error', errorMessage, null)
91+
// Prevent navigation on error
92+
return
93+
} finally {
94+
setIsLoading(false)
95+
}
96+
}
97+
98+
return (
99+
<main className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
100+
{mounted && <NotificationList />}
101+
<div className="sm:mx-auto sm:w-full sm:max-w-md">
102+
<h1 className="text-2xl font-bold text-center mb-8">Sim Studio</h1>
103+
<Card className="w-full">
104+
<CardHeader>
105+
<CardTitle>Welcome back</CardTitle>
106+
<CardDescription>Enter your credentials to access your account</CardDescription>
107+
</CardHeader>
108+
<CardContent>
109+
<div className="grid gap-6">
110+
<SocialLoginButtons
111+
githubAvailable={githubAvailable}
112+
googleAvailable={googleAvailable}
113+
callbackURL="/w"
114+
isProduction={isProduction}
115+
/>
116+
<div className="relative">
117+
<div className="absolute inset-0 flex items-center">
118+
<span className="w-full border-t" />
119+
</div>
120+
<div className="relative flex justify-center text-xs uppercase">
121+
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
122+
</div>
123+
</div>
124+
<form onSubmit={onSubmit}>
125+
<div className="space-y-4">
126+
<div className="space-y-2">
127+
<Label htmlFor="email">Email</Label>
128+
<Input
129+
id="email"
130+
name="email"
131+
type="email"
132+
placeholder="name@example.com"
133+
required
134+
/>
135+
</div>
136+
<div className="space-y-2">
137+
<Label htmlFor="password">Password</Label>
138+
<Input
139+
id="password"
140+
name="password"
141+
type="password"
142+
placeholder="Enter your password"
143+
required
144+
/>
145+
</div>
146+
<Button type="submit" className="w-full" disabled={isLoading}>
147+
{isLoading ? 'Signing in...' : 'Sign in'}
148+
</Button>
149+
</div>
150+
</form>
151+
</div>
152+
</CardContent>
153+
<CardFooter>
154+
<p className="text-sm text-gray-500 text-center w-full">
155+
Don't have an account?{' '}
156+
<Link href="/signup" className="text-primary hover:underline">
157+
Sign up
158+
</Link>
159+
</p>
160+
</CardFooter>
161+
</Card>
162+
</div>
163+
</main>
164+
)
165+
}

0 commit comments

Comments
 (0)