diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index eaf2659..17dab93 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -5,111 +5,29 @@ import { signIn } from "next-auth/react"; import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Loader2, Fingerprint } from "lucide-react"; import { toast } from "sonner"; +import { EncryptIDClient } from "@encryptid/sdk/client"; const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || "https://encryptid.jeffemmett.com"; +const encryptid = new EncryptIDClient(ENCRYPTID_SERVER); function SignInForm() { const router = useRouter(); const searchParams = useSearchParams(); const callbackUrl = searchParams.get("callbackUrl") || "/"; - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); const [isPasskeyLoading, setIsPasskeyLoading] = useState(false); - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setIsLoading(true); - - try { - const result = await signIn("credentials", { - email, - password, - redirect: false, - }); - - if (result?.error) { - toast.error("Invalid email or password"); - } else { - toast.success("Signed in successfully!"); - router.push(callbackUrl); - router.refresh(); - } - } catch (error) { - toast.error("An error occurred. Please try again."); - } finally { - setIsLoading(false); - } - } - async function handlePasskeySignIn() { setIsPasskeyLoading(true); try { - // Step 1: Get authentication options from EncryptID server - const startRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/start`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - }); - const { options } = await startRes.json(); + // SDK handles the full WebAuthn ceremony + const authResult = await encryptid.authenticate(); - // Step 2: Trigger WebAuthn ceremony in the browser - const challenge = Uint8Array.from(atob(options.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)); - - const publicKeyOptions: PublicKeyCredentialRequestOptions = { - challenge: challenge.buffer as ArrayBuffer, - rpId: options.rpId, - userVerification: options.userVerification as UserVerificationRequirement, - timeout: options.timeout, - allowCredentials: options.allowCredentials?.map((c: { type: string; id: string; transports?: string[] }) => ({ - type: c.type as PublicKeyCredentialType, - id: Uint8Array.from(atob(c.id.replace(/-/g, "+").replace(/_/g, "/")), ch => ch.charCodeAt(0)).buffer as ArrayBuffer, - transports: c.transports as AuthenticatorTransport[], - })), - }; - - const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions }) as PublicKeyCredential; - if (!assertion) throw new Error("Passkey authentication cancelled"); - - const response = assertion.response as AuthenticatorAssertionResponse; - - // Helper to convert ArrayBuffer to base64url - function toBase64url(buffer: ArrayBuffer): string { - return btoa(String.fromCharCode(...new Uint8Array(buffer))) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); - } - - // Step 3: Complete authentication with EncryptID server - const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/complete`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - challenge: options.challenge, - credential: { - credentialId: assertion.id, - authenticatorData: toBase64url(response.authenticatorData), - clientDataJSON: toBase64url(response.clientDataJSON), - signature: toBase64url(response.signature), - userHandle: response.userHandle ? toBase64url(response.userHandle) : null, - }, - }), - }); - - const authResult = await completeRes.json(); - if (!authResult.success) { - throw new Error(authResult.error || "Authentication failed"); - } - - // Step 4: Exchange EncryptID token for NextAuth session + // Exchange EncryptID token for NextAuth session const result = await signIn("encryptid", { token: authResult.token, redirect: false, @@ -124,7 +42,8 @@ function SignInForm() { } } catch (error: any) { if (error.name === "NotAllowedError") { - toast.error("Passkey authentication was cancelled"); + toast.error("No passkey found. Create an account first."); + router.push("/auth/signup"); } else { toast.error(error.message || "Passkey authentication failed"); } @@ -133,24 +52,20 @@ function SignInForm() { } } - const anyLoading = isLoading || isPasskeyLoading; - return ( Sign in - Sign in with a passkey or your email and password + Sign in with your passkey to continue - {/* Passkey sign-in button */} - -
-
- -
-
- Or continue with email -
-
- - {/* Email/password form */} -
-
- - setEmail(e.target.value)} - required - disabled={anyLoading} - /> -
-
- - setPassword(e.target.value)} - required - disabled={anyLoading} - /> -
- -

diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index 855ea80..15fdbc2 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -10,139 +10,27 @@ import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Loader2, Fingerprint } from "lucide-react"; import { toast } from "sonner"; +import { EncryptIDClient } from "@encryptid/sdk/client"; const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || "https://encryptid.jeffemmett.com"; +const encryptid = new EncryptIDClient(ENCRYPTID_SERVER); export default function SignUpPage() { const router = useRouter(); const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); const [isPasskeyLoading, setIsPasskeyLoading] = useState(false); - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - - if (password !== confirmPassword) { - toast.error("Passwords do not match"); - return; - } - - if (password.length < 8) { - toast.error("Password must be at least 8 characters"); - return; - } - - setIsLoading(true); - - try { - // Register the user - const res = await fetch("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, email, password }), - }); - - const data = await res.json(); - - if (!res.ok) { - throw new Error(data.error || "Failed to create account"); - } - - // Sign in automatically - const result = await signIn("credentials", { - email, - password, - redirect: false, - }); - - if (result?.error) { - toast.error("Account created but failed to sign in. Please try signing in manually."); - router.push("/auth/signin"); - } else { - toast.success("Account created! Welcome to rVote."); - router.push("/"); - router.refresh(); - } - } catch (error) { - toast.error(error instanceof Error ? error.message : "An error occurred"); - } finally { - setIsLoading(false); - } - } - async function handlePasskeyRegister() { setIsPasskeyLoading(true); try { - const username = name || `user-${Date.now().toString(36)}`; + const username = name.trim() || `user-${Date.now().toString(36)}`; - // Step 1: Get registration options from EncryptID server - const startRes = await fetch(`${ENCRYPTID_SERVER}/api/register/start`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, displayName: name || username }), - }); - const { options, userId } = await startRes.json(); + // SDK handles the full WebAuthn registration ceremony + const regResult = await encryptid.register(username, name || username); - // Step 2: Trigger WebAuthn registration ceremony in the browser - function fromBase64url(str: string): Uint8Array { - return Uint8Array.from(atob(str.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)); - } - - const publicKeyOptions: PublicKeyCredentialCreationOptions = { - challenge: fromBase64url(options.challenge).buffer as ArrayBuffer, - rp: options.rp, - user: { - id: fromBase64url(options.user.id).buffer as ArrayBuffer, - name: options.user.name, - displayName: options.user.displayName, - }, - pubKeyCredParams: options.pubKeyCredParams, - authenticatorSelection: options.authenticatorSelection, - timeout: options.timeout, - attestation: options.attestation as AttestationConveyancePreference, - }; - - const credential = await navigator.credentials.create({ publicKey: publicKeyOptions }) as PublicKeyCredential; - if (!credential) throw new Error("Passkey registration cancelled"); - - const response = credential.response as AuthenticatorAttestationResponse; - - function toBase64url(buffer: ArrayBuffer): string { - return btoa(String.fromCharCode(...new Uint8Array(buffer))) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); - } - - // Step 3: Complete registration with EncryptID server - const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/register/complete`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - challenge: options.challenge, - userId, - username, - credential: { - credentialId: credential.id, - publicKey: toBase64url(response.getPublicKey?.() || response.attestationObject), - attestationObject: toBase64url(response.attestationObject), - clientDataJSON: toBase64url(response.clientDataJSON), - transports: (response as any).getTransports?.() || [], - }, - }), - }); - - const regResult = await completeRes.json(); - if (!regResult.success) { - throw new Error(regResult.error || "Registration failed"); - } - - // Step 4: Exchange EncryptID token for NextAuth session + // Exchange EncryptID token for NextAuth session const result = await signIn("encryptid", { token: regResult.token, redirect: false, @@ -167,8 +55,6 @@ export default function SignUpPage() { } } - const anyLoading = isLoading || isPasskeyLoading; - return (

@@ -179,7 +65,6 @@ export default function SignUpPage() { - {/* Passkey registration */}
setName(e.target.value)} - disabled={anyLoading} + disabled={isPasskeyLoading} />
- -
-
- -
-
- Or sign up with email -
-
- - {/* Email/password form */} -
-
- - setEmail(e.target.value)} - required - disabled={anyLoading} - /> -
-
- - setPassword(e.target.value)} - required - disabled={anyLoading} - /> -
-
- - setConfirmPassword(e.target.value)} - required - disabled={anyLoading} - /> -
-

- You'll start with 50 credits and earn 10 more each day. -

- -
+

+ You'll start with 50 credits and earn 10 more each day. +

diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 5c2489a..c140e18 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -2,7 +2,6 @@ import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; import { PrismaAdapter } from "@auth/prisma-adapter"; import { prisma } from "./prisma"; -import bcrypt from "bcryptjs"; import { verifyEncryptIDToken } from "./encryptid"; export const { handlers, signIn, signOut, auth } = NextAuth({ @@ -16,45 +15,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ error: "/auth/error", }, providers: [ - // Email + password login - Credentials({ - id: "credentials", - name: "credentials", - credentials: { - email: { label: "Email", type: "email" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials) { - if (!credentials?.email || !credentials?.password) { - return null; - } - - const user = await prisma.user.findUnique({ - where: { email: credentials.email as string }, - }); - - if (!user || !user.passwordHash) { - return null; - } - - const isValid = await bcrypt.compare( - credentials.password as string, - user.passwordHash - ); - - if (!isValid) { - return null; - } - - return { - id: user.id, - email: user.email, - name: user.name, - }; - }, - }), - - // EncryptID passkey login + // EncryptID passkey login — sole auth provider Credentials({ id: "encryptid", name: "EncryptID Passkey",