Skip to content

Commit 76a3011

Browse files
committed
feat(auth): implement instant passwordless flow using hidden passwords
1 parent c7005c6 commit 76a3011

3 files changed

Lines changed: 44 additions & 117 deletions

File tree

app/login/page.tsx

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,10 @@ import { Input } from "@/components/ui/input"
88
import { Label } from "@/components/ui/label"
99
import Link from "next/link"
1010
import { motion, AnimatePresence } from "framer-motion"
11-
import { signIn, checkRememberMe } from "@/lib/features/auth"
11+
import { signInOrSignUpWithEmailOnly, checkRememberMe } from "@/lib/features/auth"
1212
import { getTranslations, getUserLanguage } from "@/lib/config"
1313
import { LanguageSwitcher } from "@/components/language-switcher"
1414
import {
15-
EyeIcon,
16-
EyeSlashIcon,
17-
LockClosedIcon,
1815
EnvelopeIcon,
1916
ArrowRightIcon,
2017
SparklesIcon,
@@ -26,10 +23,8 @@ import {
2623
export default function LoginPage() {
2724
const router = useRouter()
2825
const [email, setEmail] = useState("")
29-
const [password, setPassword] = useState("")
3026
const [rememberMe, setRememberMe] = useState(false)
3127
const [error, setError] = useState("")
32-
const [showPassword, setShowPassword] = useState(false)
3328
const [t, setT] = useState(getTranslations("en"))
3429

3530
useEffect(() => {
@@ -49,12 +44,12 @@ export default function LoginPage() {
4944
e.preventDefault()
5045
setError("")
5146

52-
if (!email || !password) {
53-
setError("Email and password are required")
47+
if (!email) {
48+
setError(t.auth.email + " is required")
5449
return
5550
}
5651

57-
const result = await signIn(email, password, rememberMe)
52+
const result = await signInOrSignUpWithEmailOnly(email, rememberMe)
5853

5954
if (result.success) {
6055
router.push("/dashboard")
@@ -200,33 +195,6 @@ export default function LoginPage() {
200195
/>
201196
</div>
202197
</div>
203-
204-
<div className="space-y-2">
205-
<Label htmlFor="password" className="text-xs font-bold uppercase tracking-widest text-slate-500 ml-1">
206-
{t.auth.password}
207-
</Label>
208-
<div className="relative group">
209-
<div className="absolute left-4 top-1/2 -translate-y-1/2 flex items-center justify-center">
210-
<LockClosedIcon className="h-5 w-5 text-slate-500 group-focus-within:text-primary transition-colors" />
211-
</div>
212-
<Input
213-
id="password"
214-
type={showPassword ? "text" : "password"}
215-
placeholder="••••••••"
216-
value={password}
217-
onChange={(e) => setPassword(e.target.value)}
218-
className="pl-12 pr-12 h-14 bg-white/[0.03] border-white/5 hover:border-white/10 focus:border-primary/50 transition-all rounded-2xl text-lg placeholder:text-slate-600 focus:ring-0 focus:bg-white/[0.05]"
219-
required
220-
/>
221-
<button
222-
type="button"
223-
onClick={() => setShowPassword(!showPassword)}
224-
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 hover:text-white transition-colors"
225-
>
226-
{showPassword ? <EyeSlashIcon className="h-5 w-5" /> : <EyeIcon className="h-5 w-5" />}
227-
</button>
228-
</div>
229-
</div>
230198
</div>
231199

232200
<div className="flex items-center justify-between px-1">
@@ -244,16 +212,13 @@ export default function LoginPage() {
244212
{t.auth.rememberMe}
245213
</span>
246214
</label>
247-
<Link href="#" className="text-sm text-primary hover:text-white font-bold transition-colors">
248-
Forgot password?
249-
</Link>
250215
</div>
251216

252217
<Button
253218
type="submit"
254219
className="w-full h-14 text-lg font-black uppercase tracking-widest bg-primary hover:bg-primary/90 text-slate-950 shadow-[0_0_20px_rgba(var(--primary-rgb),0.3)] hover:shadow-[0_0_30px_rgba(var(--primary-rgb),0.5)] transition-all group rounded-2xl"
255220
>
256-
Sign In
221+
Log In
257222
<ArrowRightIcon className="ml-3 h-5 w-5 group-hover:translate-x-1.5 transition-transform" />
258223
</Button>
259224
</form>

app/signup/page.tsx

Lines changed: 5 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input"
88
import { Label } from "@/components/ui/label"
99
import Link from "next/link"
1010
import { motion, AnimatePresence } from "framer-motion"
11-
import { signUp } from "@/lib/features/auth"
11+
import { signInOrSignUpWithEmailOnly } from "@/lib/features/auth"
1212
import { getTranslations, getUserLanguage } from "@/lib/config"
1313
import { LanguageSwitcher } from "@/components/language-switcher"
1414
import {
@@ -17,20 +17,13 @@ import {
1717
ArrowRightIcon,
1818
BoltIcon,
1919
CheckCircleIcon,
20-
ShieldCheckIcon,
21-
LockClosedIcon,
22-
EyeIcon,
23-
EyeSlashIcon
20+
ShieldCheckIcon
2421
} from "@heroicons/react/24/outline"
2522

2623
export default function SignUpPage() {
2724
const router = useRouter()
2825
const [email, setEmail] = useState("")
29-
const [password, setPassword] = useState("")
30-
const [confirmPassword, setConfirmPassword] = useState("")
3126
const [error, setError] = useState("")
32-
const [showPassword, setShowPassword] = useState(false)
33-
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
3427
const [t, setT] = useState(getTranslations("en"))
3528

3629
useEffect(() => {
@@ -41,23 +34,12 @@ export default function SignUpPage() {
4134
e.preventDefault()
4235
setError("")
4336

44-
if (!email || !password) {
45-
setError("Email and password are required")
37+
if (!email) {
38+
setError(t.auth.email + " is required")
4639
return
4740
}
4841

49-
if (password !== confirmPassword) {
50-
setError("Passwords do not match")
51-
return
52-
}
53-
54-
if (password.length < 6) {
55-
setError("Password must be at least 6 characters")
56-
return
57-
}
58-
59-
// Name is optional in our new flow, passing undefined
60-
const result = await signUp(email, password)
42+
const result = await signInOrSignUpWithEmailOnly(email, true)
6143

6244
if (result.success) {
6345
router.push("/dashboard")
@@ -203,60 +185,6 @@ export default function SignUpPage() {
203185
/>
204186
</div>
205187
</div>
206-
207-
<div className="space-y-2">
208-
<Label htmlFor="password" className="text-xs font-bold uppercase tracking-widest text-slate-500 ml-1">
209-
{t.auth.password}
210-
</Label>
211-
<div className="relative group">
212-
<div className="absolute left-4 top-1/2 -translate-y-1/2 flex items-center justify-center">
213-
<LockClosedIcon className="h-5 w-5 text-slate-500 group-focus-within:text-primary transition-colors" />
214-
</div>
215-
<Input
216-
id="password"
217-
type={showPassword ? "text" : "password"}
218-
placeholder="••••••••"
219-
value={password}
220-
onChange={(e) => setPassword(e.target.value)}
221-
className="pl-12 pr-12 h-14 bg-white/[0.03] border-white/5 hover:border-white/10 focus:border-primary/50 transition-all rounded-2xl text-lg placeholder:text-slate-600 focus:ring-0 focus:bg-white/[0.05]"
222-
required
223-
/>
224-
<button
225-
type="button"
226-
onClick={() => setShowPassword(!showPassword)}
227-
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 hover:text-white transition-colors"
228-
>
229-
{showPassword ? <EyeSlashIcon className="h-5 w-5" /> : <EyeIcon className="h-5 w-5" />}
230-
</button>
231-
</div>
232-
</div>
233-
234-
<div className="space-y-2">
235-
<Label htmlFor="confirmPassword" className="text-xs font-bold uppercase tracking-widest text-slate-500 ml-1">
236-
Confirm Password
237-
</Label>
238-
<div className="relative group">
239-
<div className="absolute left-4 top-1/2 -translate-y-1/2 flex items-center justify-center">
240-
<LockClosedIcon className="h-5 w-5 text-slate-500 group-focus-within:text-primary transition-colors" />
241-
</div>
242-
<Input
243-
id="confirmPassword"
244-
type={showConfirmPassword ? "text" : "password"}
245-
placeholder="••••••••"
246-
value={confirmPassword}
247-
onChange={(e) => setConfirmPassword(e.target.value)}
248-
className="pl-12 pr-12 h-14 bg-white/[0.03] border-white/5 hover:border-white/10 focus:border-primary/50 transition-all rounded-2xl text-lg placeholder:text-slate-600 focus:ring-0 focus:bg-white/[0.05]"
249-
required
250-
/>
251-
<button
252-
type="button"
253-
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
254-
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 hover:text-white transition-colors"
255-
>
256-
{showConfirmPassword ? <EyeSlashIcon className="h-5 w-5" /> : <EyeIcon className="h-5 w-5" />}
257-
</button>
258-
</div>
259-
</div>
260188
</div>
261189

262190
<div className="text-sm text-slate-500 font-medium px-1">

lib/features/auth/auth-service.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,40 @@ export async function signIn(
200200
}
201201
}
202202

203+
// Instant Passwordless Authentication (Hidden Password)
204+
// This orchestrates a seamless login/signup flow using just an email.
205+
export async function signInOrSignUpWithEmailOnly(
206+
email: string,
207+
rememberMe = true,
208+
): Promise<{ success: boolean; error?: string; user?: User }> {
209+
try {
210+
// Generate a consistent, structurally strong "hidden" password
211+
// In a real production app, this should ideally use a pepper/HMAC on the backend,
212+
// but for this instant-auth lab requirement we create it deterministically here.
213+
const hiddenPassword = `L@b68!${email.length}${email}SecureHash`;
214+
215+
// 1. Attempt to sign in first (assuming the user already exists)
216+
const signInResult = await signIn(email, hiddenPassword, rememberMe);
217+
218+
if (signInResult.success) {
219+
return signInResult; // Successfully logged in
220+
}
221+
222+
// 2. If sign in fails (likely because user doesn't exist), attempt to sign up
223+
const signUpResult = await signUp(email, hiddenPassword);
224+
225+
if (signUpResult.success) {
226+
// Supabase signUp auto-logs you in if email confirmation is disabled,
227+
// but to ensure the `rememberMe` and cache states are identical to a normal login,
228+
// we'll run a clean signIn immediately after a successful signUp.
229+
return await signIn(email, hiddenPassword, rememberMe);
230+
}
231+
232+
return { success: false, error: signUpResult.error || "Instant authentication failed" };
233+
} catch (error: any) {
234+
return { success: false, error: error.message || "Instant authentication encountered an error" };
235+
}
236+
}
203237

204238
// Sign out user
205239
export async function signOut(): Promise<void> {

0 commit comments

Comments
 (0)