feat: authentication improvements

This commit is contained in:
Kirill Siukhin 2025-07-10 16:22:12 +05:00
parent 9d0e447350
commit b1979aa8bb
7 changed files with 96 additions and 27 deletions

View File

@ -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>
);
}

View File

@ -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>
</>

View File

@ -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",
};

View File

@ -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");

View File

@ -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} />} />}

View File

@ -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
View 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*",
};