diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 40f25fe..11e57fd 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -5,10 +5,10 @@ import { supabase } from "../supa"; /** * POST /api/user * Register new user - * Body: { name, username, email, password } + * Body: { username, email, password } */ export async function POST(req: NextRequest) { - const { name, username, email, password } = await req.json(); + const { username, email, password } = await req.json(); if (!email || !password || !username) return NextResponse.json( @@ -28,29 +28,35 @@ export async function POST(req: NextRequest) { { status: StatusCodes.CONFLICT } ); - const { data: authData, error: authError } = - await supabase.auth.admin.createUser({ - email, - password, - email_confirm: true, - user_metadata: { name, username }, - }); + const { data: authData, error: authError } = await supabase.auth.signUp({ + email, + password, + }); if (authError) return NextResponse.json( { message: authError.message }, - { status: StatusCodes.BAD_REQUEST } + { status: StatusCodes.INTERNAL_SERVER_ERROR } + ); + + if (!authData.user) + return NextResponse.json( + { message: "User is null" }, + { status: StatusCodes.INTERNAL_SERVER_ERROR } ); await supabase.from("profiles").insert({ id: authData.user.id, - name, username, + display_name: username, + avatar_url: "", + node_address: "", }); return NextResponse.json({ - message: "ok", - user_id: authData.user.id, + message: "User registered. Please check your email to verify your account.", + user_id: authData.user?.id, + email_sent: true, }); } @@ -61,30 +67,14 @@ export async function POST(req: NextRequest) { */ export async function GET(req: NextRequest) { const params = new URL(req.url).searchParams; - const [username, password] = [params.get("username"), params.get("password")]; + const [email, password] = [params.get("email"), params.get("password")]; - if (!username || !password) + if (!email || !password) return NextResponse.json( - { message: "Params username and password are required" }, + { message: "Params email and password are required" }, { status: StatusCodes.BAD_REQUEST } ); - let email = username; - if (!username.includes("@")) { - const { data } = await supabase - .from("profiles") - .select("email") - .eq("email", username) - .maybeSingle(); - email = data?.email; - } - - if (!email) - return NextResponse.json( - { message: "Invalid username" }, - { status: StatusCodes.UNAUTHORIZED } - ); - const { data, error } = await supabase.auth.signInWithPassword({ email, password, @@ -104,7 +94,6 @@ export async function GET(req: NextRequest) { user_id: session?.user?.id, }); - // Optionally set cookie res.cookies.set("token", session?.access_token ?? "", { httpOnly: true }); return res; diff --git a/src/app/api/verify/route.ts b/src/app/api/verify/route.ts new file mode 100644 index 0000000..223f423 --- /dev/null +++ b/src/app/api/verify/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabase } from "../supa"; +import { StatusCodes } from "http-status-codes"; + +export async function GET(req: NextRequest) { + const email = req.nextUrl.searchParams.get("email"); + const password = req.nextUrl.searchParams.get("password"); + + if (!email || !password) + return NextResponse.json( + { message: "Missing email or password" }, + { status: StatusCodes.BAD_REQUEST } + ); + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error) + return NextResponse.json( + { message: error.message }, + { status: StatusCodes.INTERNAL_SERVER_ERROR } + ); + + if (!data) + return NextResponse.json( + { verified: false, message: "User not found" }, + { status: StatusCodes.NOT_FOUND } + ); + + const res = NextResponse.json({ + verified: !!data.user.email_confirmed_at, + user_id: data.user.id, + access_token: data.session.access_token, + refresh_token: data.session.refresh_token, + }); + + return res; +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index bd5fefa..66e0830 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -106,9 +106,7 @@ export default function AppSidebar({ ]; setServers(updatedServers); - Cookies.set("servers", updatedServers.map((x) => x.join("@")).join(","), { - expires: 7, - }); + Cookies.set("servers", updatedServers.map((x) => x.join("@")).join(",")); setNewServer({ name: "", ip: "" }); }; diff --git a/src/components/auth/CreateAccount.tsx b/src/components/auth/CreateAccount.tsx index e0055de..7a0e77d 100644 --- a/src/components/auth/CreateAccount.tsx +++ b/src/components/auth/CreateAccount.tsx @@ -3,12 +3,13 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import axios from "axios"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Cookies from "js-cookie"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { CardContent, CardHeader, CardTitle } from "../ui/card"; import { Label } from "../ui/label"; +import { toast } from "sonner"; export default function CreateAccount() { const [username, setUsername] = useState(""); @@ -20,9 +21,40 @@ export default function CreateAccount() { const [feedback, setFeedback] = useState< undefined | { kind: "error" | "info"; message: string } >(); + const [verify, setVerify] = useState(); const router = useRouter(); + // Verify interval + useEffect(() => { + if (verify) { + const i = setInterval(async () => { + const { data } = await axios.get("/api/verify", { + params: { email, password }, + }); + const response = data as { + verified: boolean; + user_id: string; + access_token?: string; + refresh_token?: string; + }; + if (response.verified) { + clearInterval(i); + setFeedback({ + message: "Email verified, redirecting...", + kind: "info", + }); + if (response.access_token) { + Cookies.set("token", response.access_token); + setTimeout(() => router.push("/chat"), 500); + } else { + console.log(response); + } + } + }, 8000); + } + }, [verify]); + const validateInput = (): | { kind: "error" | "info"; message: string } | undefined => { @@ -36,7 +68,7 @@ export default function CreateAccount() { return { kind: "error", message: - "Username must be 3–20 characters and only letters, numbers, or underscores.", + "Username must be 3-20 characters and only letters, numbers, or underscores.", }; } if (password.length < 6) { @@ -67,11 +99,25 @@ export default function CreateAccount() { try { setLoading(true); - const res = await axios.post("/api/user", { username, password, email }); - Cookies.set("token", (res.data as any).token); - - setFeedback({ kind: "info", message: "Account created! Redirecting..." }); - setTimeout(() => router.push("/chat"), 1000); + const { data } = await axios.post("/api/user", { + username, + password, + email, + }); + const response = data as { + message: string; + email_sent: boolean; + user_id: string; + }; + if (response.email_sent) { + toast.info( + "We sent you a verification email. Please verify to continue." + ); + setFeedback(undefined); + setVerify(response.user_id); + } else { + setFeedback({ message: response.message, kind: "error" }); + } } catch (err: any) { setFeedback({ kind: "error", @@ -87,69 +133,77 @@ export default function CreateAccount() { return ( <> - Create an Account + + {verify ? "Verify" : "Create an Account"} + - - Email - setEmail(e.target.value)} - placeholder="john@example.com" - type="email" - /> - - - Username - setUsername(e.target.value)} - placeholder="john_doe" - /> - - - Password - setPassword(e.target.value)} - type="password" - /> - - - Confirm Password - setConfirmPassword(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") signup(); - }} - type="password" - /> - + {verify ? ( + `We sent you a verification email at (${email}). Please verify to continue.` + ) : ( + <> + + Email + setEmail(e.target.value)} + placeholder="john@example.com" + type="email" + /> + + + Username + setUsername(e.target.value)} + placeholder="john_doe" + /> + + + Password + setPassword(e.target.value)} + type="password" + /> + + + Confirm Password + setConfirmPassword(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") signup(); + }} + type="password" + /> + - + - {feedback && ( -
- {feedback.message} -
+ {feedback && ( +
+ {feedback.message} +
+ )} + + + )} - -
); diff --git a/src/components/auth/Login.tsx b/src/components/auth/Login.tsx index 72075c9..ed410b3 100644 --- a/src/components/auth/Login.tsx +++ b/src/components/auth/Login.tsx @@ -6,18 +6,10 @@ import axios from "axios"; import { useState } from "react"; import Cookies from "js-cookie"; import { useRouter } from "next/navigation"; -import { Label } from "@/components/ui/label"; -import Link from "next/link"; -import { - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "../ui/card"; +import { CardContent, CardHeader, CardTitle } from "../ui/card"; export default function Login() { - const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [feedback, setFeedback] = useState< undefined | { kind: "error" | "info"; message: string } @@ -29,7 +21,7 @@ export default function Login() { setFeedback(undefined); try { const res = await axios.get("/api/user", { - params: { username, password }, + params: { email, password }, }); Cookies.set("token", (res.data as any).token); @@ -57,7 +49,7 @@ export default function Login() { Email - setUsername(e.target.value)} type="email" /> + setEmail(e.target.value)} type="email" /> Password diff --git a/src/hooks/get-user.ts b/src/hooks/get-user.ts index 9379a1e..c3770c5 100644 --- a/src/hooks/get-user.ts +++ b/src/hooks/get-user.ts @@ -17,12 +17,6 @@ export default function useUser() { useEffect(() => { async function fetchUser() { - const token = Cookies.get("token"); - if (!token) { - router.push("/login"); - return; - } - try { setUser((await get("/api/profile")).data); } catch (e: any) {