Compare commits

..

No commits in common. "ce31e35426b28d7d3c64f7b3f51ea7952620cd00" and "177a5e2c66c83ee6ca74380356efeaabe2544fd1" have entirely different histories.

4 changed files with 131 additions and 434 deletions

View File

@ -2,7 +2,7 @@
import type React from "react" import type React from "react"
import { useState, useEffect, useRef } from "react" import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardFooter, CardTitle, CardDescription } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardFooter, CardTitle, CardDescription } from "@/components/ui/card"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
@ -10,19 +10,11 @@ import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import Link from "next/link" import Link from "next/link"
import { motion, AnimatePresence } from "framer-motion" import { motion, AnimatePresence } from "framer-motion"
import { UserPlus, UserCog, Heart, AlertCircle, CheckCircle2, Mail, Lock, User, Bot, Loader, ArrowLeft } from "lucide-react" import { UserPlus, UserCog, Heart, AlertCircle, CheckCircle2, Mail, Lock, User } from "lucide-react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import validator from "validator"
import PasswordValidator from "password-validator"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Turnstile } from "next-turnstile"
import { validateEmail, validatePassword } from "@/lib/utils"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
declare global {
interface Window {
onTurnstileSuccess?: (token: string) => void
onloadTurnstileCallback?: () => void
}
}
export default function Signup() { export default function Signup() {
const router = useRouter() const router = useRouter()
@ -37,13 +29,10 @@ export default function Signup() {
migrateTerms: false, migrateTerms: false,
migrateName: "", migrateName: "",
}) })
const [isValid, setIsValid] = useState(false) const [isValid, setIsValid] = useState(false)
const [validationMessage, setValidationMessage] = useState("") const [validationMessage, setValidationMessage] = useState("")
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [turnstileStatus, setTurnstileStatus] = useState<"success" | "error" | "expired" | "required">("required")
const formRef = useRef<HTMLFormElement>(null)
const [errorAlert, setErrorAlert] = useState<string | null>(null)
const [forceRefresh, setForceRefresh] = useState(false)
const fadeInOut = { const fadeInOut = {
initial: { opacity: 0, y: 20 }, initial: { opacity: 0, y: 20 },
@ -58,133 +47,51 @@ export default function Signup() {
...prev, ...prev,
[name]: type === "checkbox" ? checked : value, [name]: type === "checkbox" ? checked : value,
})) }))
if (errorAlert) {
setErrorAlert(null)
}
} }
const turnstileCallback = () => {
console.log("[i] Turnstile token received")
}
useEffect(() => {
window.onTurnstileSuccess = turnstileCallback
return () => {
delete window.onTurnstileSuccess
}
}, [])
useEffect(() => { useEffect(() => {
if (formType === "create") { if (formType === "create") {
const { name, emailUsername, emailDomain, password, terms } = formData const { name, emailUsername, emailDomain, password, terms } = formData
if (name.length < 2) { const isNameValid = name.length >= 2
setIsValid(false) const isEmailValid = validateEmail(emailUsername, emailDomain)
setValidationMessage("Enter your name") const isPasswordValid = validatePassword(password)
return setIsValid(isNameValid && isEmailValid && isPasswordValid && terms)
}
const emailValidation = validateEmail(emailUsername, emailDomain) if (!isNameValid) setValidationMessage("Enter your name")
if (!emailValidation.valid) { else if (!isEmailValid) setValidationMessage("Enter a valid email address")
setIsValid(false) else if (!isPasswordValid) setValidationMessage("Weak Password")
setValidationMessage(emailValidation.message) else if (!terms) setValidationMessage("Accept the terms")
return else setValidationMessage("Create Account")
}
const passwordValidation = validatePassword(password)
if (!passwordValidation.valid) {
setIsValid(false)
setValidationMessage(passwordValidation.message)
return
}
if (!terms) {
setIsValid(false)
setValidationMessage("Accept the terms")
return
}
if (turnstileStatus !== "success") {
setIsValid(false)
setValidationMessage("Please verify you are not a robot")
return
}
setIsValid(true)
setValidationMessage("Create Account")
} else if (formType === "migrate") { } else if (formType === "migrate") {
const { emailUsername, emailDomain, migratePassword, migrateTerms, migrateName } = formData const { emailUsername, emailDomain, migratePassword, migrateTerms, migrateName } = formData
if (migrateName.length < 2) { const isEmailValid = validateEmail(emailUsername, emailDomain)
setIsValid(false) const isPasswordValid = validatePassword(migratePassword)
setValidationMessage("Enter your name") const isNameValid = migrateName.length >= 2
return setIsValid(isEmailValid && isPasswordValid && migrateTerms && isNameValid)
}
const emailValidation = validateEmail(emailUsername, emailDomain) if (!isNameValid) setValidationMessage("Enter your name")
if (!emailValidation.valid) { else if (!isEmailValid) setValidationMessage("Enter a valid email address")
setIsValid(false) else if (!isPasswordValid) setValidationMessage("Weak Password")
setValidationMessage(emailValidation.message) else if (!migrateTerms) setValidationMessage("Accept the terms")
return else setValidationMessage("Migrate Account")
}
const passwordValidation = validatePassword(migratePassword)
if (!passwordValidation.valid) {
setIsValid(false)
setValidationMessage(passwordValidation.message)
return
}
if (!migrateTerms) {
setIsValid(false)
setValidationMessage("Accept the terms")
return
}
if (turnstileStatus !== "success") {
setIsValid(false)
setValidationMessage("Please verify you are not a robot")
return
}
setIsValid(true)
setValidationMessage("Migrate Account")
} }
}, [formData, formType, turnstileStatus]) }, [formData, formType])
const getButtonIcon = () => { const getButtonIcon = () => {
if (isValid) return <CheckCircle2 size={30} /> if (isValid) return <CheckCircle2 className="mr-2 h-4 w-4" />
if (validationMessage.includes("name")) return <User size={30} /> if (validationMessage.includes("name")) return <User className="mr-2 h-4 w-4" />
if (validationMessage.includes("Email") || validationMessage.includes("email")) return <Mail size={30} /> if (validationMessage.includes("email")) return <Mail className="mr-2 h-4 w-4" />
if (validationMessage.includes("Password") || validationMessage.includes("password")) return <Lock size={30} /> if (validationMessage.includes("Password")) return <Lock className="mr-2 h-4 w-4" />
if (validationMessage.includes("terms")) return <AlertCircle size={30} /> if (validationMessage.includes("terms")) return <AlertCircle className="mr-2 h-4 w-4" />
if (validationMessage.includes("robot") || validationMessage.includes("Security")) return <Bot size={30} />
return null return null
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setIsSubmitting(true) setIsSubmitting(true)
setErrorAlert(null)
try { try {
if (turnstileStatus !== "success") {
setValidationMessage("Please verify you are not a robot")
setIsSubmitting(false)
return
}
const email = `${formData.emailUsername}@${formData.emailDomain}` const email = `${formData.emailUsername}@${formData.emailDomain}`
const formDataObj = new FormData(formRef.current as HTMLFormElement)
const token = formDataObj.get("cf-turnstile-response") as string
if (!token) {
setErrorAlert("Security verification token is missing. Please refresh")
setIsSubmitting(false)
setForceRefresh(true)
return
}
const response = await fetch("/api/users/create", { const response = await fetch("/api/users/create", {
method: "POST", method: "POST",
headers: { headers: {
@ -195,64 +102,58 @@ export default function Signup() {
email: email, email: email,
password: formType === "create" ? formData.password : formData.migratePassword, password: formType === "create" ? formData.password : formData.migratePassword,
migrate: formType === "migrate", migrate: formType === "migrate",
token: token,
}), }),
}) })
const data = await response.json() const data = await response.json()
if (!response.ok) {
console.error("API error:", response.status, data)
setErrorAlert(data.message || `Error ${response.status}: Failed to create account`)
setIsSubmitting(false)
setForceRefresh(true)
return
}
if (data.success) { if (data.success) {
router.push("/account/signup/success") router.push("/account/signup/success")
} else { } else {
setErrorAlert(data.message || "Failed to create account.") setValidationMessage(data.message || "Failed to create account. Please try again.")
} }
} catch (error) { } catch (error) {
console.error("Form submission error:", error) console.log("[!] " + error)
setErrorAlert("An unexpected error occurred. Please try again later.") setValidationMessage("An error occurred. Please contact us (see the sidebar)")
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
} }
function validateEmail(username: string, domain: string) {
return username.length > 0 && domain.length > 0 && validator.isEmail(`${username}@${domain}`)
}
function validatePassword(password: string) {
const passwordSchema = new PasswordValidator()
passwordSchema.is().min(8).is().max(128).has().letters().has().digits().has().not().spaces()
return passwordSchema.validate(password)
}
return ( return (
<div className="flex h-screen items-center justify-center p-4"> <div className="flex h-screen items-center justify-center p-4">
<Card className="w-full max-w-md overflow-hidden"> <Card className="w-full max-w-md overflow-hidden">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl">Account Setup</CardTitle> <CardTitle>Account Setup</CardTitle>
<CardDescription>Create a new account or migrate an existing one.</CardDescription> <CardDescription>Create a new account or migrate an existing one.</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{errorAlert && (
<Alert variant="destructive" className="text-red-500 mb-4">
<AlertCircle color={"#EF4444" /* this is text-red-500 btw */} size={18} />
<AlertTitle className="text-lg font-bold">Oops! Something went wrong.</AlertTitle>
<AlertDescription>{errorAlert}</AlertDescription>
</Alert>
)}
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{formType === "initial" && ( {formType === "initial" && (
<motion.div key="initial" {...fadeInOut} className="space-y-4"> <motion.div key="initial" {...fadeInOut} className="space-y-4">
<Button onClick={() => setFormType("create")} className="w-full h-16 text-lg"> <Button onClick={() => setFormType("create")} className="w-full h-16 text-lg">
<UserPlus className="mr-2" /> <UserPlus className="mr-2 h-6 w-6" />
Create New Account Create New Account
</Button> </Button>
<Button onClick={() => setFormType("migrate")} className="w-full h-16 text-lg"> <Button onClick={() => setFormType("migrate")} className="w-full h-16 text-lg">
<UserCog className="mr-2" /> <UserCog className="mr-2 h-6 w-6" />
Migrate p0ntus mail Account Migrate p0ntus mail Account
</Button> </Button>
</motion.div> </motion.div>
)} )}
{formType === "create" && ( {formType === "create" && (
<motion.form key="create" ref={formRef} {...fadeInOut} className="space-y-4" onSubmit={handleSubmit}> <motion.form key="create" {...fadeInOut} className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="name">Name</Label> <Label htmlFor="name">Name</Label>
<Input <Input
@ -281,10 +182,7 @@ export default function Signup() {
<Select <Select
name="emailDomain" name="emailDomain"
value={formData.emailDomain} value={formData.emailDomain}
onValueChange={(value) => { onValueChange={(value) => setFormData((prev) => ({ ...prev, emailDomain: value }))}
setFormData((prev) => ({ ...prev, emailDomain: value }))
if (errorAlert) setErrorAlert(null)
}}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select domain" /> <SelectValue placeholder="Select domain" />
@ -314,7 +212,7 @@ export default function Signup() {
onChange={handleInputChange} onChange={handleInputChange}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Password must be 8-128 characters long, include letters and digits, and not contain spaces. Password must be 8-64 characters long, include letters and digits, and not contain spaces.
</p> </p>
</div> </div>
<div className="flex items-center space-x-4 py-2"> <div className="flex items-center space-x-4 py-2">
@ -323,10 +221,7 @@ export default function Signup() {
name="terms" name="terms"
required required
checked={formData.terms} checked={formData.terms}
onCheckedChange={(checked) => { onCheckedChange={(checked) => setFormData((prev) => ({ ...prev, terms: checked }))}
setFormData((prev) => ({ ...prev, terms: checked }))
if (errorAlert) setErrorAlert(null)
}}
/> />
<Label htmlFor="terms" className="text-sm"> <Label htmlFor="terms" className="text-sm">
I agree to the{" "} I agree to the{" "}
@ -339,35 +234,10 @@ export default function Signup() {
</Link> </Link>
</Label> </Label>
</div> </div>
{!forceRefresh && (
<Turnstile
siteKey={process.env.NEXT_PUBLIC_CF_SITEKEY!}
retry="auto"
refreshExpired="auto"
onError={() => {
setTurnstileStatus("error")
setValidationMessage("Security check failed. Please try again.")
console.error("[!] Turnstile error occurred")
}}
onExpire={() => {
setTurnstileStatus("expired")
setValidationMessage("Security check expired. Please verify again.")
console.warn("[!] Turnstile token expired")
}}
onLoad={() => {
setTurnstileStatus("required")
}}
onVerify={() => {
setTurnstileStatus("success")
console.log("[S] Turnstile verification successful")
}}
className="flex justify-center"
/>
)}
</motion.form> </motion.form>
)} )}
{formType === "migrate" && ( {formType === "migrate" && (
<motion.form key="migrate" ref={formRef} {...fadeInOut} className="space-y-4" onSubmit={handleSubmit}> <motion.form key="migrate" {...fadeInOut} className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="migrateName">Name</Label> <Label htmlFor="migrateName">Name</Label>
<Input <Input
@ -396,10 +266,7 @@ export default function Signup() {
<Select <Select
name="emailDomain" name="emailDomain"
value={formData.emailDomain} value={formData.emailDomain}
onValueChange={(value) => { onValueChange={(value) => setFormData((prev) => ({ ...prev, emailDomain: value }))}
setFormData((prev) => ({ ...prev, emailDomain: value }))
if (errorAlert) setErrorAlert(null)
}}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select domain" /> <SelectValue placeholder="Select domain" />
@ -441,10 +308,7 @@ export default function Signup() {
name="migrateTerms" name="migrateTerms"
required required
checked={formData.migrateTerms} checked={formData.migrateTerms}
onCheckedChange={(checked) => { onCheckedChange={(checked) => setFormData((prev) => ({ ...prev, migrateTerms: checked }))}
setFormData((prev) => ({ ...prev, migrateTerms: checked }))
if (errorAlert) setErrorAlert(null)
}}
/> />
<Label htmlFor="migrateTerms" className="text-sm"> <Label htmlFor="migrateTerms" className="text-sm">
I agree to the{" "} I agree to the{" "}
@ -457,70 +321,40 @@ export default function Signup() {
</Link> </Link>
</Label> </Label>
</div> </div>
{!forceRefresh && (
<Turnstile
siteKey={process.env.NEXT_PUBLIC_CF_SITEKEY!}
retry="auto"
refreshExpired="auto"
onError={() => {
setTurnstileStatus("error")
setValidationMessage("Security check failed. Please try again.")
console.error("[!] Turnstile error occurred")
}}
onExpire={() => {
setTurnstileStatus("expired")
setValidationMessage("Security check expired. Please verify again.")
console.warn("[!] Turnstile token expired")
}}
onLoad={() => {
setTurnstileStatus("required")
}}
onVerify={() => {
setTurnstileStatus("success")
console.log("[S] Turnstile verification successful")
}}
className="flex justify-center"
/>
)}
</motion.form> </motion.form>
)} )}
</AnimatePresence> </AnimatePresence>
</CardContent> </CardContent>
<CardFooter> <CardFooter>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{!forceRefresh ? ( {formType !== "initial" ? (
formType !== "initial" ? (
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
<Button
type="submit"
className="w-full mb-4"
disabled={!isValid || isSubmitting}
onClick={handleSubmit}
>
{isSubmitting ? (
<Loader size={30} className="animate-spin" />
) : (
getButtonIcon()
)}
{isSubmitting ? "Submitting..." : validationMessage}
</Button>
<Button variant="outline" className="w-full" onClick={() => setFormType("initial")}>
Back
</Button>
</motion.div>
) : (
<motion.div key="welcome" {...fadeInOut} className="flex w-full justify-center items-center">
<span className="text-sm text-center">Welcome to the LibreCloud family!</span>
<Heart size={16} className="ml-1" />
</motion.div>
)
) : (
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2"> <motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
<Button className="w-full" onClick={() => { setFormType("initial"); setForceRefresh(false); setErrorAlert(null) }}> <Button
<ArrowLeft size={30} /> type="submit"
className="w-full mb-4"
disabled={!isValid || isSubmitting}
onClick={handleSubmit}
>
{isSubmitting ? (
<motion.div
className="h-5 w-5 animate-spin rounded-full border-b-2 border-white"
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Number.POSITIVE_INFINITY, ease: "linear" }}
/>
) : (
getButtonIcon()
)}
{isSubmitting ? "Submitting..." : validationMessage}
</Button>
<Button variant="outline" className="w-full" onClick={() => setFormType("initial")}>
Back Back
</Button> </Button>
</motion.div> </motion.div>
) : (
<motion.div key="welcome" {...fadeInOut} className="flex w-full justify-center items-center">
<span className="text-sm text-center">Welcome to the LibreCloud family!</span>
<Heart className="h-4 w-4 ml-1" />
</motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</CardFooter> </CardFooter>

View File

@ -1,42 +1,23 @@
import axios from "axios" import axios from "axios"
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { validateToken } from "@/lib/utils"
// This endpoint has two functions: // This endpoint has two functions:
// (1) Create a new LibreCloud user (Authentik, Email) // (1) Create a new LibreCloud user (Authentik, Email)
// (2) Migrate a p0ntus mail account to a LibreCloud account (creates/migrates Authentik/Email) // (2) Migrate a p0ntus mail account to a LibreCloud account (creates/migrates Authentik/Email)
async function createEmail(email: string, password: string, migrate: boolean) { async function createEmail(email: string, password: string, migrate: boolean) {
try { const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, {
if (!process.env.MAIL_CONNECT_API_URL) { email,
console.error("[!] Missing MAIL_CONNECT_API_URL environment variable") password,
return { success: false, message: "Server configuration error" } migrate,
} })
const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, { const responseData = response.data
email, if (responseData.success) {
password, return response.data
migrate, } else if (responseData.error) {
}) return { success: false, message: responseData.error }
} else {
const responseData = response.data
if (responseData.success) {
return response.data
} else if (responseData.error) {
console.error("[!] Email creation failed:", responseData.error)
return { success: false, message: responseData.error }
} else {
console.error("[!] Email creation failed with unknown error")
return { success: false, message: "Failed to create email account" }
}
} catch (error) {
console.error("[!] Email creation error:", error)
if (axios.isAxiosError(error)) {
return {
success: false,
message: error.response?.data?.error || "Failed to connect to email service",
}
}
return { success: false, message: "Failed to create email account" } return { success: false, message: "Failed to create email account" }
} }
} }
@ -44,22 +25,10 @@ async function createEmail(email: string, password: string, migrate: boolean) {
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json() const body = await request.json()
const { name, email, password, migrate, token } = body const { name, email, password, migrate } = body
// Validate fields
if (!name || !email || !password) { if (!name || !email || !password) {
return NextResponse.json({ success: false, message: "The form you submitted is incomplete" }, { status: 400 }) return NextResponse.json({ success: false, message: "Request is incomplete" })
}
const tokenValidation = await validateToken(token)
if (!tokenValidation.success) {
console.error("Turnstile validation failed:", tokenValidation.error)
return NextResponse.json({ success: false, message: "Robot check failed, try refreshing" }, { status: 400 })
}
if (!process.env.AUTHENTIK_API_URL || !process.env.AUTHENTIK_API_KEY) {
console.error("Missing Authentik environment variables")
return NextResponse.json({ success: false, message: "Server configuration error" }, { status: 500 })
} }
// Create Authentik user // Create Authentik user
@ -74,9 +43,6 @@ export async function POST(request: Request) {
email, email,
type: "internal", type: "internal",
} }
console.log("[i] Creating user in Authentik:", { username: genUser, email })
const response = await axios.request({ const response = await axios.request({
method: "post", method: "post",
maxBodyLength: Infinity, maxBodyLength: Infinity,
@ -91,82 +57,56 @@ export async function POST(request: Request) {
}) })
if (response.data?.detail) { if (response.data?.detail) {
console.error("[!] Authentik user creation issue:", response.data.detail) console.log(response.data.detail)
} }
if (response.status !== 201) { if (response.status !== 201) {
if (response.data.username && response.data.username[0] === "This field must be unique.") { if (response.data.username && response.data.username[0] === "This field must be unique.") {
return NextResponse.json({ success: false, message: "Username already exists" }, { status: 409 }) return NextResponse.json({ success: false, message: "Username already exists" })
} }
return NextResponse.json({ success: false, message: "Failed to create user in Authentik" })
console.error("Failed to create user in Authentik:", response.status, response.data)
return NextResponse.json({ success: false, message: "Failed to create user account" }, { status: 500 })
}
// User created successfully, now set password
const userID = response.data.pk
const updData = {
password,
}
console.log("[i] Setting password for user:", userID)
const updCfg = await axios.request({
method: "post",
maxBodyLength: Number.POSITIVE_INFINITY,
url: `${process.env.AUTHENTIK_API_URL}/core/users/${userID}/set_password/`,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: updData,
validateStatus: () => true, // capture response even for error status codes
})
if (updCfg.data?.detail) {
console.error("[!] Password setting issue:", updCfg.data.detail)
}
if (updCfg.status === 204) {
// account created successfully, now create email
console.log("[i] Creating email account for:", email)
const emailRes = await createEmail(email, password, migrate)
if (emailRes.success) {
console.log("[S] Account creation successful for:", email)
} else {
console.error("[!] Email creation failed for:", email, emailRes.message)
}
return NextResponse.json(emailRes, {
status: emailRes.success ? 200 : 500,
})
} else if (updCfg.status === 400) {
console.error("[!] Failed to set password:", updCfg.data)
return NextResponse.json({ success: false, message: "Invalid password format" }, { status: 400 })
} else { } else {
console.error("[!] Unknown error setting password:", updCfg.status, updCfg.data) const userID = response.data.pk
return NextResponse.json({ success: false, message: "Failed to complete account setup" }, { status: 500 }) const updData = {
password,
}
const updCfg = await axios.request({
method: "post",
maxBodyLength: Infinity,
url: `${process.env.AUTHENTIK_API_URL}/core/users/${userID}/set_password/`,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: updData,
validateStatus: () => true, // capture response even for error status codes
})
if (updCfg.data?.detail) {
console.log(updCfg.data.detail)
}
if (updCfg.status === 204) {
// account created successfully, now create email
const emailRes = await createEmail(email, password, migrate)
return NextResponse.json(emailRes)
} else if (updCfg.status === 400) {
return NextResponse.json({ success: false, message: "Bad Request - Failed to set Authentik password" })
} else {
return NextResponse.json({ success: false, message: "Internal Server Error - Failed to set Authentik password" })
}
} }
} catch (error: unknown) { } catch (error: unknown) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error) && error.response?.data?.detail) {
if (error.response?.data?.detail) { console.log("Error:", error.response.data.detail)
console.error("[!] Request error with detail:", error.response.data.detail) return NextResponse.json({ success: false, message: "Internal server error - Failed to create user" })
return NextResponse.json({ success: false, message: "Server error - Failed to create user" }, { status: 500 }) } else if (axios.isAxiosError(error) && error.response?.data?.error) {
} else if (error.response?.data?.error) { console.log("Error:", error.response.data.error)
console.error("[!] Request error (passed from Authentik):", error.response.data.error) return NextResponse.json({ success: false, message: error.response.data.error })
return NextResponse.json({ success: false, message: error.response.data.error }, { status: 500 }) } else {
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") { console.log(error)
console.error("[!] Connection error:", error.message) return NextResponse.json({ success: false, message: "Internal server error - Failed to create user" })
return NextResponse.json(
{ success: false, message: "Failed to connect to authentication service" },
{ status: 503 },
)
}
} }
console.error("[!] Unhandled error while creating user:", error)
return NextResponse.json({ success: false, message: "An unexpected error occurred" }, { status: 500 })
} }
} }

View File

@ -1,82 +1,6 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { validateTurnstileToken } from "next-turnstile"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export async function validateToken(token: string) {
try {
if (!token) {
console.error("Validation failed: No token provided")
return { success: false, error: "No token provided" }
}
if (!process.env.CF_SECRETKEY) {
console.error("Validation failed: Missing CF_SECRETKEY environment variable")
return { success: false, error: "Server configuration error" }
}
const result = await validateTurnstileToken({
token,
secretKey: process.env.CF_SECRETKEY,
})
if (result.success) {
return { success: true }
} else {
console.error("Validation failed:", result)
return { success: false, error: "Invalid token" }
}
} catch (error) {
console.error("Turnstile validation error:", error)
return { success: false, error: "Validation service error" }
}
}
// Email validation
export function validateEmail(username: string, domain: string) {
if (!username || !domain) {
return { valid: false, message: "Email is required" }
}
const email = `${username}@${domain}`
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
return { valid: false, message: "Invalid email format" }
}
return { valid: true, message: "" }
}
// Password validation
export function validatePassword(password: string) {
if (!password) {
return { valid: false, message: "Password is required" }
}
if (password.length < 8) {
return { valid: false, message: "Password must be at least 8 characters" }
}
if (password.length > 128) {
return { valid: false, message: "Password must be less than 128 characters" }
}
if (!/[A-Za-z]/.test(password)) {
return { valid: false, message: "Password must contain letters" }
}
if (!/\d/.test(password)) {
return { valid: false, message: "Password must contain digits" }
}
if (/\s/.test(password)) {
return { valid: false, message: "Password cannot contain spaces" }
}
return { valid: true, message: "" }
}

View File

@ -36,7 +36,6 @@
"next": "^15.2.1", "next": "^15.2.1",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.5", "next-themes": "^0.4.5",
"next-turnstile": "^1.0.2",
"password-validator": "^5.3.0", "password-validator": "^5.3.0",
"prisma": "^6.4.1", "prisma": "^6.4.1",
"react": "^19.0.0", "react": "^19.0.0",