feat: authentication improvements
This commit is contained in:
parent
9d0e447350
commit
b1979aa8bb
@ -13,10 +13,12 @@ export default async function Home() {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
<Editor />
|
||||
<i className="text-center text-sm text-neutral-400">
|
||||
Changes are not saved!<br />
|
||||
<Link href="/login" className="font-bold hover:underline">Log in</Link> to save your notes.
|
||||
</i>
|
||||
{!auth && (
|
||||
<i className="text-center text-sm text-neutral-400">
|
||||
Changes are not saved!<br />
|
||||
<Link href="/login" className="font-bold hover:underline">Log in</Link> to save your notes.
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,12 +11,14 @@ export default function About() {
|
||||
<>
|
||||
<div className="m-4">
|
||||
<h1 className="font-bold text-xl mb-2">Authenticate</h1>
|
||||
<p className="mb-2">Please, use this form to log in/register:</p>
|
||||
<AuthForm />
|
||||
<div className="flex gap-4 flex-col md:flex-row">
|
||||
<AuthForm />
|
||||
<AuthForm isRegister />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center text-sm text-neutral-400">
|
||||
<p>Welcome to rhyme!</p>
|
||||
<p>Free service for writing and saving notes, lyrics, poetry, etc...</p>
|
||||
<p>Welcome to Rhyme!</p>
|
||||
<p>Free service for writing and saving notes, lyrics, poetry, etc</p>
|
||||
<p>Made with ❤️ by <a href="https://misterkirill.com" className="font-bold hover:underline">Kirill Siukhin</a></p>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "My notes - Rhyme",
|
||||
title: "Notes - Rhyme",
|
||||
description: "View, create and edit your notes",
|
||||
};
|
||||
|
||||
|
@ -10,7 +10,7 @@ import bcrypt from "bcrypt";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET!;
|
||||
|
||||
export async function authenticate(_prevState: unknown, formData: FormData) {
|
||||
export async function login(_prevState: unknown, formData: FormData) {
|
||||
const username = formData.get("username") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
@ -25,12 +25,9 @@ export async function authenticate(_prevState: unknown, formData: FormData) {
|
||||
.where(eq(usersTable.username, username));
|
||||
|
||||
if (users.length === 0) {
|
||||
await db.insert(usersTable).values({
|
||||
username,
|
||||
password: bcrypt.hashSync(password, bcrypt.genSaltSync()),
|
||||
});
|
||||
return { error: "Invalid password or username!" };
|
||||
} else if (!bcrypt.compareSync(password, users[0].password)) {
|
||||
return { error: "Invalid password or username is already taken" };
|
||||
return { error: "Invalid password or username!" };
|
||||
}
|
||||
|
||||
const token = jwt.sign({ sub: username }, JWT_SECRET, { expiresIn: "7d" });
|
||||
@ -45,6 +42,46 @@ export async function authenticate(_prevState: unknown, formData: FormData) {
|
||||
redirect("/notes");
|
||||
}
|
||||
|
||||
export async function register(_prevState: unknown, formData: FormData) {
|
||||
const username = formData.get("username") as string;
|
||||
const password = formData.get("password") as string;
|
||||
const passwordConfirm = formData.get("password_confirm") as string;
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
return { error: "Passwords do not match!" };
|
||||
}
|
||||
|
||||
if (username.length < 3) {
|
||||
return { error: "Username is too short!" };
|
||||
} else if (password.length < 8) {
|
||||
return { error: "Password is too short!" };
|
||||
}
|
||||
|
||||
const users = await db.select()
|
||||
.from(usersTable)
|
||||
.where(eq(usersTable.username, username));
|
||||
|
||||
if (users.length !== 0) {
|
||||
return { error: "Username is already taken!" };
|
||||
}
|
||||
|
||||
await db.insert(usersTable).values({
|
||||
username,
|
||||
password: bcrypt.hashSync(password, bcrypt.genSaltSync()),
|
||||
});
|
||||
|
||||
const token = jwt.sign({ sub: username }, JWT_SECRET, { expiresIn: "7d" });
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set("session", token, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
redirect("/notes");
|
||||
}
|
||||
|
||||
export async function logOut() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete("session");
|
||||
|
@ -1,8 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRightFromLine, CircleQuestionMark, List, Plus, UserRound, UserRoundMinus } from "lucide-react";
|
||||
import { logOut } from "@/app/actions";
|
||||
import { getAuth } from "@/lib/auth";
|
||||
import HeaderButton from "./HeaderButton";
|
||||
import { logOut } from "@/app/actions";
|
||||
|
||||
export default async function Header({ showToolbar = false }: { showToolbar?: boolean }) {
|
||||
const auth = await getAuth();
|
||||
@ -16,8 +16,12 @@ export default async function Header({ showToolbar = false }: { showToolbar?: bo
|
||||
<div className="flex gap-2">
|
||||
{auth && (
|
||||
<>
|
||||
<HeaderButton title="new" icon={<Plus size={20} />} />
|
||||
{showToolbar && <HeaderButton title="list" icon={<List size={20} />} />}
|
||||
<Link href="/notes/new">
|
||||
<HeaderButton title="new" icon={<Plus size={20} />} />
|
||||
</Link>
|
||||
<Link href="/notes">
|
||||
<HeaderButton title="list" icon={<List size={20} />} />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{showToolbar && <HeaderButton title="export" icon={<ArrowRightFromLine size={20} />} />}
|
||||
|
@ -1,17 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState } from "react";
|
||||
import { authenticate } from "@/app/actions";
|
||||
import { login, register } from "@/app/actions";
|
||||
|
||||
export default function AuthForm() {
|
||||
const [state, formAction] = useActionState(authenticate, null);
|
||||
export default function AuthForm({ isRegister = false }: { isRegister?: boolean }) {
|
||||
const [state, formAction] = useActionState(isRegister ? register : login, null);
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-2" action={formAction}>
|
||||
<input name="username" className="p-2 bg-neutral-800 focus:outline-none focus:bg-neutral-700" placeholder="Username" required />
|
||||
<input name="password" className="p-2 bg-neutral-800 focus:outline-none focus:bg-neutral-700" placeholder="Password" type="password" required />
|
||||
<button type="submit" className="p-2 bg-neutral-800 hover:bg-neutral-700 cursor-pointer">Authenticate</button>
|
||||
{state?.error && <p>{state.error}</p>}
|
||||
</form>
|
||||
<div className="w-full">
|
||||
<h1 className="font-bold mb-2">{isRegister ? "Register" : "Login"}</h1>
|
||||
<form className="flex flex-col gap-2" action={formAction}>
|
||||
<input name="username" className="p-2 bg-neutral-800 focus:outline-none focus:bg-neutral-700" placeholder="Username" required />
|
||||
<input name="password" className="p-2 bg-neutral-800 focus:outline-none focus:bg-neutral-700" placeholder="Password" type="password" required />
|
||||
{isRegister && (
|
||||
<input name="password_confirm" className="p-2 bg-neutral-800 focus:outline-none focus:bg-neutral-700" placeholder="Confirm password" type="password" required />
|
||||
)}
|
||||
<button type="submit" className="p-2 bg-neutral-800 hover:bg-neutral-700 cursor-pointer">
|
||||
{isRegister ? "Register" : "Login"}
|
||||
</button>
|
||||
{state?.error && <p>{state.error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
16
src/middleware.ts
Normal file
16
src/middleware.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAuth } from "./lib/auth";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const auth = await getAuth();
|
||||
|
||||
if (auth) {
|
||||
return NextResponse.next();
|
||||
} else {
|
||||
return NextResponse.redirect(new URL("/auth", request.url));
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: "/notes/:path*",
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user