diff --git a/README.md b/README.md index 38eea22..759008b 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,10 @@ The project uses Next.js Route Groups without a `src/` directory to maintain a c - `components/` — Modular UI components (Chat, Navigation, Sidebars) - `db/` — Database schema definitions, migrations, and Drizzle configuration (`drizzle.config.ts`) - `lib/` — Centralized core logic folder containing: - - `store/` — State management stores + - `stores/` — State management stores + - `schemas/` — Zod validation schemas - `types/` — Global TypeScript interfaces and type definitions + - `services/` — Business logic layers and external API integration services - `public/` — Static assets (images, icons, fonts) ## 🎯 Current Status diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index 0512816..a19ade6 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -5,7 +5,7 @@ "use client"; -import { useSidebarStore } from "@/lib/store/useSidebarStore"; +import { useSidebarStore } from "@/lib/stores/useSidebarStore"; import { ServerSidebar } from "@/components/sidebar/ServerSidebar"; import { ChannelSidebar } from "@/components/sidebar/ChannelSidebar"; import { MemberSidebar } from "@/components/sidebar/MemberSidebar"; diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..5817d11 --- /dev/null +++ b/app/(auth)/layout.tsx @@ -0,0 +1,16 @@ +/** + * @file app/(auth)/layout.tsx + * @description Layout component wrapping authentication views with a centered container structure and fixed bottom footer. + */ + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
{children}
+
+ ); +} diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..6ba79f3 --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,144 @@ +/** + * @file app/(auth)/login/page.tsx + * @description Client component rendering the login page, handling form submission, authentication requests, error messaging, and redirection. + */ + +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; +import { loginUser } from "@/lib/services/auth.service"; + +/** + * Renders the user login interface and manages authentication form state. + * + * @returns {JSX.Element} The rendered login page component. + */ +export default function LoginPage() { + const router = useRouter(); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + /** + * Form submit handler extracting credentials from FormData and delegating to handleSignIn. + * + * @param {React.SubmitEvent} e - The form submission event. + */ + const handleSubmit = async (e: React.SubmitEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const email = formData.get("email") as string; + const password = formData.get("password") as string; + await handleSignIn(email, password); + }; + + /** + * Authenticates the user with provided credentials and redirects upon success. + * + * @param {string} email - The user's email address. + * @param {string} password - The user's password. + */ + const handleSignIn = async (email: string, password: string) => { + setError(null); + setLoading(true); + + const result = await loginUser(email, password); + + if (result.error) { + setError(result.error); + setLoading(false); + } else { + router.push("/"); + } + }; + + return ( +
+
+ {/* Header / Logo */} +
+
+ Waveform Logo +
+

Welcome back!

+

+ We're looking forward to seeing you again. +

+
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Form */} +
+
+ + +
+ +
+ + +
+ + +
+ + {/* Footer Link */} +

+ Don't have an account yet?{" "} + + Register + +

+
+
+ ); +} diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx new file mode 100644 index 0000000..26e860f --- /dev/null +++ b/app/(auth)/register/page.tsx @@ -0,0 +1,171 @@ +/** + * @file app/(auth)/register/page.tsx + * @description Client component rendering the registration page, handling form submissions, account creation logic, error handling, and navigation. + */ + +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; +import { registerUser } from "@/lib/services/auth.service"; + +/** + * Renders the user registration interface and manages sign-up form state. + * + * @returns {JSX.Element} The rendered registration page component. + */ +export default function RegisterPage() { + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + /** + * Handles user registration form submission, validates form data via auth service, + * and navigates to summary page on successful registration. + * + * @param {React.SubmitEvent} event - The form submission event. + * @returns {Promise} + */ + async function handleSubmit(event: React.SubmitEvent) { + event.preventDefault(); + setError(null); + setLoading(true); + + const formData = new FormData(event.currentTarget); + const payload = Object.fromEntries(formData.entries()); + + const result = await registerUser(payload); + + if (result.error) { + setError(result.error); + setLoading(false); + } else { + router.push("/"); + } + } + + return ( +
+
+
+
+ Waveform Logo +
+

+ Create an Account +

+

+ Get started with Waveform now. +

+
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Form */} +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ + +
+ +

+ Already have an account?{" "} + + Sign In + +

+
+
+ ); +} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts new file mode 100644 index 0000000..745a9f6 --- /dev/null +++ b/app/api/auth/register/route.ts @@ -0,0 +1,72 @@ +/** + * @file app/api/auth/register/route.ts + * @description API route handler for user registration, validating request payloads, checking duplicate accounts, hashing passwords, and storing new user records. + */ + +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { users } from "@/db/schema"; +import { hashPassword } from "@/lib/password"; +import { registerSchema } from "@/lib/schemas/auth.schema"; +import { eq, or } from "drizzle-orm"; + +/** + * Handles HTTP POST requests for registering a new user. + * + * @param {Request} req - The incoming HTTP request object containing the JSON registration payload. + * @returns {Promise} JSON response indicating successful creation (201), validation failure (400), conflict (409), or server error (500). + */ +export async function POST(req: Request) { + try { + const body = await req.json(); + const validationResult = registerSchema.safeParse(body); + + if (!validationResult.success) { + return NextResponse.json( + { message: validationResult.error.issues[0].message }, + { status: 400 }, + ); + } + + const { username, email, password } = validationResult.data; + + // Check whether the email address or username already exists + const [existingUser] = await db + .select() + .from(users) + .where(or(eq(users.email, email), eq(users.username, username))) + .limit(1); + + if (existingUser) { + if (existingUser.email === email) { + return NextResponse.json( + { message: "This email address is already in use." }, + { status: 409 }, + ); + } + + if (existingUser.username === username) { + return NextResponse.json( + { message: "This username is already taken." }, + { status: 409 }, + ); + } + } + + // Hash a Password & Create a User + const hashedPassword = await hashPassword(password); + + await db.insert(users).values({ + username, + email, + password: hashedPassword, + }); + + return NextResponse.json({ success: true }, { status: 201 }); + } catch { + return NextResponse.json( + { message: "Fehler beim Erstellen des Benutzers." }, + { status: 500 }, + ); + } +} diff --git a/app/globals.css b/app/globals.css index 6799211..62c1b9a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -7,6 +7,8 @@ --color-accent: hsl(235 86% 65%); --color-accent-hover: hsl(235 86% 58%); + --color-destructive: hsl(0 84% 60%); + --color-foreground: hsl(210 11% 93%); --color-muted: hsl(215 8% 55%); diff --git a/app/layout.tsx b/app/layout.tsx index a440573..72ff832 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ * @description Root layout component that wraps the entire application, providing global CSS styles, base HTML structure, and page metadata. */ +import { SessionProvider } from "next-auth/react"; import "./globals.css"; /** @@ -27,7 +28,9 @@ export default function RootLayout({ }) { return ( - {children} + + {children} + ); } diff --git a/components/chat/ChatHeader.tsx b/components/chat/ChatHeader.tsx index 4561a1f..9492958 100644 --- a/components/chat/ChatHeader.tsx +++ b/components/chat/ChatHeader.tsx @@ -6,7 +6,7 @@ "use client"; import { Menu, Hash, Users } from "lucide-react"; -import { useSidebarStore } from "@/lib/store/useSidebarStore"; +import { useSidebarStore } from "@/lib/stores/useSidebarStore"; /** * Renders the top navigation header for the chat view, providing toggle triggers for mobile navigation and member list sidebars. diff --git a/components/sidebar/UserProfile.tsx b/components/sidebar/UserProfile.tsx index 0150ad2..0fcb811 100644 --- a/components/sidebar/UserProfile.tsx +++ b/components/sidebar/UserProfile.tsx @@ -1,31 +1,42 @@ /** * @file components/sidebar/UserProfile.tsx - * @description User profile component rendered at the bottom of the sidebar, displaying user avatar, status indicator, username, and settings action trigger. + * @description User profile component displaying current session info, status, settings, and logout action. */ -import { Settings } from "lucide-react"; +"use client"; + +import { LogOut, Settings } from "lucide-react"; +import { signOut, useSession } from "next-auth/react"; /** - * Renders the user profile card with user information, online status, and quick access settings. + * Renders the user profile card with user information, online status, settings, and logout trigger. * * @returns {JSX.Element} The rendered user profile footer element. */ export function UserProfile() { + const { data: session } = useSession(); + + const handleLogout = () => { + signOut({ callbackUrl: "/login" }); + }; + + const username = session?.user?.name || "User"; + const userInitial = username.charAt(0).toUpperCase(); + return (
- U + {userInitial}
- {/* Online Status Indicator */}

- Username + {username}

Online @@ -33,12 +44,22 @@ export function UserProfile() {

- +
+ + + +
); diff --git a/lib/schemas/auth.schema.ts b/lib/schemas/auth.schema.ts new file mode 100644 index 0000000..67f7c45 --- /dev/null +++ b/lib/schemas/auth.schema.ts @@ -0,0 +1,44 @@ +/** + * @file app/lib/schemas/auth.schema.ts + * @description Zod validation schemas for authentication forms, including login and user registration data constraints. + */ + +import { z } from "zod"; + +/** + * Zod validation schema for user login credentials. + */ +export const loginSchema = z.object({ + email: z + .string() + .max(255, "Email is too long") + .pipe(z.email("Please provide a valid email address")), + password: z + .string() + .min(1, "Password is required") + .max(72, "Password is too long"), +}); + +/** + * Zod validation schema for new user registration payloads, including password confirmation matching. + */ +export const registerSchema = z + .object({ + username: z + .string() + .min(1, "Username is required") + .max(50, "Username is too long"), + email: z + .string() + .max(255, "Email is too long") + .pipe(z.email("Please provide a valid email address")), + password: z + .string() + .min(8, "Password must be at least 8 characters long") + .max(72, "Password is too long (max 72 characters)"), + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + message: "Passwords do not match", + path: ["confirmPassword"], + }); diff --git a/lib/services/auth.service.ts b/lib/services/auth.service.ts new file mode 100644 index 0000000..cf44e1a --- /dev/null +++ b/lib/services/auth.service.ts @@ -0,0 +1,134 @@ +/** + * @file app/lib/services/auth.service.ts + * @description Authentication service providing helper functions for sign-in, and registration. + */ + +import { loginSchema, registerSchema } from "@/lib/schemas/auth.schema"; +import { signIn } from "next-auth/react"; + +/** + * Authenticates a user using email and password credentials, performing client-side validation first. + * + * @async + * @param {string} email - The user's email address. + * @param {string} password - The user's account password. + * @returns {Promise<{ success?: boolean; error?: string }>} An object indicating sign-in success or an error message. + */ +export async function loginUser(email: string, password: string) { + const validationResult = loginSchema.safeParse({ email, password }); + + if (!validationResult.success) { + return { error: validationResult.error.issues[0].message }; + } + + try { + const res = await signIn("credentials", { + email: validationResult.data.email, + password: validationResult.data.password, + redirect: false, + }); + + if (res?.error) { + if (res.error === "CredentialsSignin") { + return { error: "Incorrect email address or password." }; + } + return { error: "An unexpected error has occurred." }; + } + + return { success: true }; + } catch (err: any) { + if ( + err?.message?.includes("fetch") || + err?.name === "TypeError" || + err?.message?.includes("network") + ) { + return { error: "Server error: API endpoint not available" }; + } + return { error: err?.message || "An unexpected error has occurred." }; + } +} + +/** + * Registers a new user account by validating the form data and submitting it to the backend registration route. + * + * @async + * @param {Record} payload - The user registration form payload containing credentials and user details. + * @returns {Promise<{ success?: boolean; error?: string }>} An object indicating successful registration or an error message. + */ +export async function registerUser(payload: Record) { + const validationResult = registerSchema.safeParse(payload); + + if (!validationResult.success) { + return { error: validationResult.error.issues[0].message }; + } + + try { + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(validationResult.data), + }); + + const contentType = response.headers.get("content-type"); + let data; + + if (contentType && contentType.includes("application/json")) { + try { + data = await response.json(); + } catch (jsonErr) { + return { error: "Server error: Invalid response format" }; + } + } else { + throw new Error("Server error: API endpoint not available"); + } + + if (!response.ok) { + return { error: data.message || "Something went wrong." }; + } + + const loginResult = await autoLogin( + validationResult.data.email, + validationResult.data.password, + ); + + if (!loginResult.success) { + return { error: loginResult.error }; + } + + return { success: true }; + } catch (err: any) { + return { error: err.message || "Something went wrong." }; + } +} + +/** + * Performs client-side automatic login using user credentials. + * + * @async + * @param {string} email - The user's email address. + * @param {string} password - The user's password. + * @returns {Promise<{ success: boolean; error?: string }>} The result of the sign-in operation. + */ +export async function autoLogin(email: string, password: string) { + try { + const signInResult = await signIn("credentials", { + email, + password, + redirect: false, + }); + + if (signInResult?.error) { + return { + success: false, + error: "Automatic sign-in failed. Please log in manually.", + }; + } + + return { success: true }; + } catch (err: any) { + return { + success: false, + error: err?.message || "An unexpected error occurred during auto-login.", + }; + } +} diff --git a/lib/store/useSidebarStore.ts b/lib/stores/useSidebarStore.ts similarity index 97% rename from lib/store/useSidebarStore.ts rename to lib/stores/useSidebarStore.ts index 4426ccb..ffb176e 100644 --- a/lib/store/useSidebarStore.ts +++ b/lib/stores/useSidebarStore.ts @@ -1,5 +1,5 @@ /** - * @file lib/store/useSidebarStore.ts + * @file lib/stores/useSidebarStore.ts * @description Condition state management store for controlling the visibility and mutual exclusion of navigation and members sidebars. */