Better auth

This commit is contained in:
2025-10-17 10:34:57 +02:00
parent 6868894a1b
commit 4876b57389
6 changed files with 186 additions and 119 deletions
+19 -30
View File
@@ -5,10 +5,10 @@ import { supabase } from "../supa";
/** /**
* POST /api/user * POST /api/user
* Register new user * Register new user
* Body: { name, username, email, password } * Body: { username, email, password }
*/ */
export async function POST(req: NextRequest) { 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) if (!email || !password || !username)
return NextResponse.json( return NextResponse.json(
@@ -28,29 +28,35 @@ export async function POST(req: NextRequest) {
{ status: StatusCodes.CONFLICT } { status: StatusCodes.CONFLICT }
); );
const { data: authData, error: authError } = const { data: authData, error: authError } = await supabase.auth.signUp({
await supabase.auth.admin.createUser({
email, email,
password, password,
email_confirm: true,
user_metadata: { name, username },
}); });
if (authError) if (authError)
return NextResponse.json( return NextResponse.json(
{ message: authError.message }, { 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({ await supabase.from("profiles").insert({
id: authData.user.id, id: authData.user.id,
name,
username, username,
display_name: username,
avatar_url: "",
node_address: "",
}); });
return NextResponse.json({ return NextResponse.json({
message: "ok", message: "User registered. Please check your email to verify your account.",
user_id: authData.user.id, user_id: authData.user?.id,
email_sent: true,
}); });
} }
@@ -61,30 +67,14 @@ export async function POST(req: NextRequest) {
*/ */
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const params = new URL(req.url).searchParams; 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( return NextResponse.json(
{ message: "Params username and password are required" }, { message: "Params email and password are required" },
{ status: StatusCodes.BAD_REQUEST } { 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({ const { data, error } = await supabase.auth.signInWithPassword({
email, email,
password, password,
@@ -104,7 +94,6 @@ export async function GET(req: NextRequest) {
user_id: session?.user?.id, user_id: session?.user?.id,
}); });
// Optionally set cookie
res.cookies.set("token", session?.access_token ?? "", { httpOnly: true }); res.cookies.set("token", session?.access_token ?? "", { httpOnly: true });
return res; return res;
+40
View File
@@ -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;
}
+1 -3
View File
@@ -106,9 +106,7 @@ export default function AppSidebar({
]; ];
setServers(updatedServers); setServers(updatedServers);
Cookies.set("servers", updatedServers.map((x) => x.join("@")).join(","), { Cookies.set("servers", updatedServers.map((x) => x.join("@")).join(","));
expires: 7,
});
setNewServer({ name: "", ip: "" }); setNewServer({ name: "", ip: "" });
}; };
+62 -8
View File
@@ -3,12 +3,13 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import axios from "axios"; import axios from "axios";
import { useState } from "react"; import { useEffect, useState } from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { CardContent, CardHeader, CardTitle } from "../ui/card"; import { CardContent, CardHeader, CardTitle } from "../ui/card";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import { toast } from "sonner";
export default function CreateAccount() { export default function CreateAccount() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@@ -20,9 +21,40 @@ export default function CreateAccount() {
const [feedback, setFeedback] = useState< const [feedback, setFeedback] = useState<
undefined | { kind: "error" | "info"; message: string } undefined | { kind: "error" | "info"; message: string }
>(); >();
const [verify, setVerify] = useState<string | undefined>();
const router = useRouter(); 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 = (): const validateInput = ():
| { kind: "error" | "info"; message: string } | { kind: "error" | "info"; message: string }
| undefined => { | undefined => {
@@ -36,7 +68,7 @@ export default function CreateAccount() {
return { return {
kind: "error", kind: "error",
message: 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) { if (password.length < 6) {
@@ -67,11 +99,25 @@ export default function CreateAccount() {
try { try {
setLoading(true); setLoading(true);
const res = await axios.post("/api/user", { username, password, email }); const { data } = await axios.post("/api/user", {
Cookies.set("token", (res.data as any).token); username,
password,
setFeedback({ kind: "info", message: "Account created! Redirecting..." }); email,
setTimeout(() => router.push("/chat"), 1000); });
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) { } catch (err: any) {
setFeedback({ setFeedback({
kind: "error", kind: "error",
@@ -87,9 +133,15 @@ export default function CreateAccount() {
return ( return (
<> <>
<CardHeader> <CardHeader>
<CardTitle className="text-3xl mx-auto">Create an Account</CardTitle> <CardTitle className="text-3xl mx-auto">
{verify ? "Verify" : "Create an Account"}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="gap-3 flex flex-col"> <CardContent className="gap-3 flex flex-col">
{verify ? (
`We sent you a verification email at (${email}). Please verify to continue.`
) : (
<>
<span> <span>
Email Email
<Input <Input
@@ -150,6 +202,8 @@ export default function CreateAccount() {
<Button onClick={signup} disabled={loading}> <Button onClick={signup} disabled={loading}>
{loading ? "Signing Up..." : "Sign Up"} {loading ? "Signing Up..." : "Sign Up"}
</Button> </Button>
</>
)}
</CardContent> </CardContent>
</> </>
); );
+4 -12
View File
@@ -6,18 +6,10 @@ import axios from "axios";
import { useState } from "react"; import { useState } from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Label } from "@/components/ui/label"; import { CardContent, CardHeader, CardTitle } from "../ui/card";
import Link from "next/link";
import {
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
export default function Login() { export default function Login() {
const [username, setUsername] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [feedback, setFeedback] = useState< const [feedback, setFeedback] = useState<
undefined | { kind: "error" | "info"; message: string } undefined | { kind: "error" | "info"; message: string }
@@ -29,7 +21,7 @@ export default function Login() {
setFeedback(undefined); setFeedback(undefined);
try { try {
const res = await axios.get("/api/user", { const res = await axios.get("/api/user", {
params: { username, password }, params: { email, password },
}); });
Cookies.set("token", (res.data as any).token); Cookies.set("token", (res.data as any).token);
@@ -57,7 +49,7 @@ export default function Login() {
<CardContent className="gap-3 flex flex-col"> <CardContent className="gap-3 flex flex-col">
<span> <span>
Email Email
<Input onChange={(e) => setUsername(e.target.value)} type="email" /> <Input onChange={(e) => setEmail(e.target.value)} type="email" />
</span> </span>
<span> <span>
Password Password
-6
View File
@@ -17,12 +17,6 @@ export default function useUser() {
useEffect(() => { useEffect(() => {
async function fetchUser() { async function fetchUser() {
const token = Cookies.get("token");
if (!token) {
router.push("/login");
return;
}
try { try {
setUser((await get("/api/profile")).data); setUser((await get("/api/profile")).data);
} catch (e: any) { } catch (e: any) {