Better auth
This commit is contained in:
+19
-30
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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: "" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user