implement final login flow
All checks were successful
Build and Push Docker Image / build_and_push (push) Successful in 1m19s

This commit is contained in:
Aidan 2025-01-26 00:07:56 -05:00
parent 818023e5c1
commit a76527d839
7 changed files with 357 additions and 36 deletions

View File

@ -0,0 +1,119 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Key } from "lucide-react"
import Cookies from "js-cookie"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert"
export default function Login() {
const [magicCode, setMagicCode] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
useEffect(() => {
const checkStatus = async () => {
setErrorMessage("")
setIsLoading(true)
try {
const response = await fetch('http://localhost:3001/auth/validateStageTwo', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: Cookies.get("email"), stageTwoKey: Cookies.get("stageTwoKey") }),
})
const data = await response.json()
if (!response.ok || !data.success) {
router.push("/account/login")
}
} catch (error) {
console.error("There was a problem with checking the status of your request:", error)
setErrorMessage("An unexpected error occurred. Please try again.")
} finally {
setIsLoading(false)
}
}
checkStatus()
}, [magicCode, router])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setErrorMessage("")
try {
const response = await fetch('http://localhost:3001/auth/validateMagicCode', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: Cookies.get("email"), stageTwoKey: Cookies.get("stageTwoKey"), magicCode }),
})
const data = await response.json()
if (response.ok && data.success) {
Cookies.set("key", data.key)
Cookies.remove("email")
Cookies.remove("stageTwoKey")
router.push("/account/dashboard")
} else {
setErrorMessage(data.error || "An unknown error occurred.")
}
} catch (error) {
console.error("There was a problem with checking the magic code:", error)
setErrorMessage("An unexpected error occurred. Please try again.")
} finally {
setIsLoading(false)
}
}
return (
<div className="flex h-screen items-center justify-center">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Enter Your Magic Code</CardTitle>
<CardDescription>Check your email for the code we sent you.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Input
type="text"
placeholder="Magic Code"
value={magicCode}
onChange={(e) => setMagicCode(e.target.value)}
required
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? (
"Checking..."
) : (
<>
<Key className="mr-2 h-4 w-4" />
Submit
</>
)}
</Button>
{errorMessage && (
<Alert variant="destructive">
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
</form>
</CardContent>
</Card>
</div>
)
}

View File

@ -2,49 +2,92 @@
import Link from "next/link"
import { useState } from "react"
import { TextField, Button, Flex, Text, Card } from "@radix-ui/themes"
import { useRouter } from "next/navigation"
import { Mail } from "lucide-react"
import Cookies from "js-cookie"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert"
export default function Login() {
const [email, setEmail] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setErrorMessage("")
try {
const response = await fetch('http://localhost:3001/auth/login', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
})
const data = await response.json()
if (response.ok && data.success) {
Cookies.set("stageTwoKey", data.stageTwoKey)
Cookies.set("email", email)
router.push("/account/login/code")
} else {
setErrorMessage(data.error || "An unknown error occurred.")
}
} catch (error) {
console.error("There was a problem with requesting a magic code:", error)
setErrorMessage("An unexpected error occurred. Please try again.")
} finally {
setIsLoading(false)
}
}
return (
<Flex className="h-screen" align="center" justify="center">
<Card className="w-full max-w-md p-6">
<form onSubmit={handleSubmit}>
<Text size="5" weight="bold">
Log in to your account
</Text>
<Flex direction="column" gap="4" className="mt-6">
<TextField.Root
placeholder="Email"
type="email"
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
required
>
<TextField.Slot>
<Mail height="16" width="16" />
</TextField.Slot>
</TextField.Root>
<Button color="gray" variant="outline" type="submit" highContrast>
<Mail height="16" width="16" />
Send Magic Code
<div className="flex h-screen items-center justify-center">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Log in to your account</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? (
"Sending..."
) : (
<>
<Mail className="mr-2 h-4 w-4" />
Send Magic Code
</>
)}
</Button>
<Link
href="https://user.pontusmail.org/admin/user/signup"
className="text-sm underline text-center"
>
I don&apos;t have an account
</Link>
</Flex>
</form>
{errorMessage && (
<Alert variant="destructive">
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
<div className="text-center">
<Link href="https://user.pontusmail.org/admin/user/signup" className="text-sm underline">
I don&apos;t have an account
</Link>
</div>
</form>
</CardContent>
</Card>
</Flex>
</div>
)
}

BIN
bun.lockb

Binary file not shown.

59
components/ui/alert.tsx Normal file
View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

76
components/ui/card.tsx Normal file
View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

22
components/ui/input.tsx Normal file
View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -13,6 +13,7 @@
"@radix-ui/themes": "^3.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"js-cookie": "^3.0.5",
"lucide-react": "^0.474.0",
"next": "15.1.6",
"react": "^19.0.0",
@ -22,14 +23,15 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"@eslint/eslintrc": "^3"
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}