diff --git a/app/account/signup/page.tsx b/app/account/signup/page.tsx index 950ef60..b8560c5 100644 --- a/app/account/signup/page.tsx +++ b/app/account/signup/page.tsx @@ -10,12 +10,13 @@ import { Button } from "@/components/ui/button" import { Switch } from "@/components/ui/switch" import Link from "next/link" import { motion, AnimatePresence } from "motion/react" -import { UserPlus, UserCog, Heart, AlertCircle, CheckCircle2, Mail, Lock, User, Bot, Loader, ArrowLeft } from "lucide-react" +import { UserPlus, UserCog, Heart, AlertCircle, CheckCircle2, Mail, Lock, User, Bot, Loader2, ArrowLeft } from "lucide-react" import { useRouter } from "next/navigation" -import { validateEmail, validatePassword } from "@/lib/utils" +import { validateEmail, validatePassword, validateName } from "@/lib/utils" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import EmailField from "@/components/custom/signup/EmailField" import Altcha from "@/components/custom/Altcha" +import { useSession } from "next-auth/react" export default function Signup() { const router = useRouter() @@ -76,9 +77,11 @@ export default function Signup() { useEffect(() => { if (formType === "create") { const { name, emailUsername, emailDomain, password, terms } = formData - if (name.length < 2) { + + const nameValidation = validateName(name) + if (!nameValidation.valid) { setIsValid(false) - setValidationMessage("Enter your name") + setValidationMessage(nameValidation.message) return } @@ -112,9 +115,11 @@ export default function Signup() { setValidationMessage("Create Account") } else if (formType === "migrate") { const { emailUsername, emailDomain, migratePassword, migrateTerms, migrateName } = formData - if (migrateName.length < 2) { + + const nameValidation = validateName(migrateName) + if (!nameValidation.valid) { setIsValid(false) - setValidationMessage("Enter your name") + setValidationMessage(nameValidation.message) return } @@ -151,11 +156,12 @@ export default function Signup() { const getButtonIcon = () => { if (isValid) return - if (validationMessage.includes("name")) return + if (validationMessage.includes("name") || validationMessage.includes("Name")) return if (validationMessage.includes("Email") || validationMessage.includes("email")) return if (validationMessage.includes("Password") || validationMessage.includes("password")) return if (validationMessage.includes("terms")) return if (validationMessage.includes("robot") || validationMessage.includes("Security")) return + if (validationMessage.includes("special characters")) return return null } @@ -219,12 +225,54 @@ export default function Signup() { } } + const { data: session } = useSession() + + useEffect(() => { + if (session) { + router.push("/account/dashboard") + } + }, [session, router]) + return (
- Account Setup - Create a new account or migrate an existing one. + + {formType === "initial" ? ( + + Account Setup + Create a new account or migrate an existing one. + + ) : formType === "create" ? ( + + Create New Account + Set up your new LibreCloud account. + + ) : ( + + Migrate Account + Transfer your p0ntus mail account to LibreCloud. + + )} + {errorAlert && ( @@ -301,11 +349,11 @@ export default function Signup() { /> @@ -378,11 +426,11 @@ export default function Signup() { /> @@ -416,7 +464,7 @@ export default function Signup() { onClick={handleSubmit} > {isSubmitting ? ( - + ) : ( getButtonIcon() )} @@ -445,5 +493,4 @@ export default function Signup() {
) -} - +} \ No newline at end of file diff --git a/app/account/signup/success/page.tsx b/app/account/signup/success/page.tsx index 1ad52d4..df7fa60 100644 --- a/app/account/signup/success/page.tsx +++ b/app/account/signup/success/page.tsx @@ -140,7 +140,7 @@ const WelcomePage = () => { From here, you can proceed to sign in to your newly created account with Authentik. It will handle all the sign-ins for your account except for Pass (Vaultwarden).

- + diff --git a/app/api/auth/password/route.ts b/app/api/auth/password/route.ts index 7ac92d2..cec0612 100644 --- a/app/api/auth/password/route.ts +++ b/app/api/auth/password/route.ts @@ -1,6 +1,7 @@ import { auth } from "@/auth" import axios from "axios" import { NextResponse } from "next/server" +import { validatePassword } from "@/lib/utils" export async function POST(request: Request) { try { @@ -14,6 +15,11 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Invalid password" }, { status: 400 }) } + const passwordValidation = validatePassword(password) + if (!passwordValidation.valid) { + return NextResponse.json({ error: passwordValidation.message }, { status: 400 }) + } + // Get user ID from email const user = await axios.request({ method: "get", diff --git a/app/api/mail/password/route.ts b/app/api/mail/password/route.ts index 29040c4..80e311b 100644 --- a/app/api/mail/password/route.ts +++ b/app/api/mail/password/route.ts @@ -1,5 +1,6 @@ import { auth } from "@/auth" import { NextResponse } from "next/server" +import { validatePassword } from "@/lib/utils" export async function POST(request: Request) { try { @@ -13,6 +14,11 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Invalid password" }, { status: 400 }) } + const passwordValidation = validatePassword(password) + if (!passwordValidation.valid) { + return NextResponse.json({ error: passwordValidation.message }, { status: 400 }) + } + const { email } = session.user const response = await fetch(`${process.env.MAIL_CONNECT_API_URL}/accounts/update/password`, { diff --git a/app/api/users/create/route.ts b/app/api/users/create/route.ts index 6c22d9e..e2781fc 100644 --- a/app/api/users/create/route.ts +++ b/app/api/users/create/route.ts @@ -1,6 +1,7 @@ import axios from "axios" import { NextResponse } from "next/server" -import { validateToken } from "@/lib/utils" +import { validateToken, validateName, validateEmail } from "@/lib/utils" +import { auth } from "@/auth" // This endpoint has two functions: // (1) Create a new LibreCloud user (Authentik, Email) @@ -68,15 +69,33 @@ export async function POST(request: Request) { let atkCreated = false let userID = "" + const session = await auth() + if (session) { + return NextResponse.json({ success: false, message: "You are already logged in" }, { status: 403 }) + } + try { const body = await request.json() const { name, email, password, migrate, token } = body - // Validate fields + // Make sure all fields are present if (!name || !email || !password) { return NextResponse.json({ success: false, message: "The form you submitted is incomplete" }, { status: 400 }) } + // Validate name again + const nameValidation = validateName(name) + if (!nameValidation.valid) { + return NextResponse.json({ success: false, message: nameValidation.message }, { status: 400 }) + } + + // and email + const [emailUsername, emailDomain] = email.split('@') + const emailValidation = validateEmail(emailUsername, emailDomain) + if (!emailValidation.valid) { + return NextResponse.json({ success: false, message: emailValidation.message }, { status: 400 }) + } + const tokenValidation = await validateToken(token) if (!tokenValidation.success) { console.error("Altcha validation failed:", tokenValidation.error) diff --git a/components/cards/dashboard/Settings/ChangeEmailPassword.tsx b/components/cards/dashboard/Settings/ChangeEmailPassword.tsx index d32fa5a..ff5a3b7 100644 --- a/components/cards/dashboard/Settings/ChangeEmailPassword.tsx +++ b/components/cards/dashboard/Settings/ChangeEmailPassword.tsx @@ -4,7 +4,7 @@ import React, { useState, useRef, useEffect, useCallback } from "react" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Label } from "@/components/ui/label" -import { CheckCircleIcon, Key, Loader2, XCircleIcon } from "lucide-react" +import { CheckCircleIcon, Key, Loader2, XCircleIcon, AlertCircle } from "lucide-react" import Link from "next/link" import { Dialog, @@ -17,6 +17,7 @@ import { } from "@/components/ui/dialog" import { toast } from "sonner" import { motion, useAnimationControls } from "framer-motion" +import { validatePassword } from "@/lib/utils" export function ChangeEmailPassword() { const [newPassword, setNewPassword] = useState("") @@ -28,6 +29,7 @@ export function ChangeEmailPassword() { const [isHolding, setIsHolding] = useState(false) const holdDuration = 10 const [remainingTime, setRemainingTime] = useState(holdDuration) + const [passwordError, setPasswordError] = useState(null) const submitPasswordChange = async () => { setLoading(true) @@ -86,17 +88,16 @@ export function ChangeEmailPassword() { }, }) controls.set({ "--progress": "0%" }) - } finally { - setLoading(false) - setIsHolding(false) - if (holdTimeoutRef.current) { - clearTimeout(holdTimeoutRef.current) - holdTimeoutRef.current = null - } - if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null - } + } + setLoading(false) + setIsHolding(false) + if (holdTimeoutRef.current) { + clearTimeout(holdTimeoutRef.current) + holdTimeoutRef.current = null + } + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null } } @@ -104,10 +105,23 @@ export function ChangeEmailPassword() { e.preventDefault() } + const handlePasswordChange = (e: React.ChangeEvent) => { + const value = e.target.value + setNewPassword(value) + + // Validate password + const validation = validatePassword(value) + if (!validation.valid) { + setPasswordError(validation.message) + } else { + setPasswordError(null) + } + } + const holdDurationMs = holdDuration * 1000 const handleHoldStart = () => { - if (loading || newPassword.length < 8) return + if (loading || newPassword.length < 8 || passwordError) return setIsHolding(true) controls.set({ "--progress": "0%" }) @@ -205,12 +219,19 @@ export function ChangeEmailPassword() { id="new-password" type="password" value={newPassword} - onChange={(e) => setNewPassword(e.target.value)} + onChange={handlePasswordChange} className="mt-1.5" /> -

- Password must be at least 8 characters long. -

+ {passwordError ? ( +

+ + {passwordError} +

+ ) : ( +

+ Password must be 8-128 characters long, include letters and digits, and not contain spaces. +

+ )} {message &&

{message}

} diff --git a/lib/utils.ts b/lib/utils.ts index bd2d57a..febdd73 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -37,6 +37,11 @@ export function validateEmail(username: string, domain: string) { return { valid: false, message: "Email is required" } } + const specialCharsRegex = /[<>()[\]\\,;:{}"']/ + if (specialCharsRegex.test(username)) { + return { valid: false, message: "Email contains special characters" } + } + const email = `${username}@${domain}` const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ @@ -47,6 +52,28 @@ export function validateEmail(username: string, domain: string) { return { valid: true, message: "" } } +// name validation +export function validateName(name: string) { + if (!name) { + return { valid: false, message: "Name is required" } + } + + if (name.length < 2) { + return { valid: false, message: "Name must be at least 2 characters" } + } + + if (name.length > 64) { + return { valid: false, message: "Name must be less than 64 characters" } + } + + const specialCharsRegex = /[<>()[\]\\,;:{}"']/ + if (specialCharsRegex.test(name)) { + return { valid: false, message: "Name contains special characters" } + } + + return { valid: true, message: "" } +} + // Password validation export function validatePassword(password: string) { if (!password) {