From af10837072a44ee758499509be9d775ac29c8dbd Mon Sep 17 00:00:00 2001 From: Chneemann Date: Sun, 30 Aug 2026 16:25:11 +0200 Subject: [PATCH] refactor(auth): remove SessionProvider and sync user status on login/logout --- app/(app)/layout.tsx | 28 ++++++++++----- app/api/auth/logout/route.ts | 41 ++++++++++++++++++++++ app/layout.tsx | 7 ++-- auth.ts | 60 ++++++++++++++++++++------------ components/layout/AppSidebar.tsx | 36 ++++++++++++++++--- components/sidebar/UserPanel.tsx | 35 +++++++++++++++---- lib/types/next-auth.d.ts | 25 +++++-------- 7 files changed, 166 insertions(+), 66 deletions(-) create mode 100644 app/api/auth/logout/route.ts diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index 5f07f57..b24d2ef 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -4,20 +4,15 @@ */ import { auth } from "@/auth"; +import { db } from "@/db"; +import { users } from "@/db/schema"; +import { eq } from "drizzle-orm"; import { getUserServers } from "@/lib/services/server.service"; import { AppSidebar } from "@/components/layout/AppSidebar"; import { MemberSidebar } from "@/components/layout/MemberSidebar"; import { ServerProvider } from "@/lib/context/ServerContext"; import { redirect } from "next/navigation"; -/** - * Server component layout wrapper for authenticated application views. - * Handles session verification, fetches user servers, and renders global layout components. - * - * @param {Object} props - The component props. - * @param {React.ReactNode} props.children - The child page content to render inside the main viewport layout. - * @returns {Promise} The rendered application layout hierarchy with provider contexts. - */ export default async function AppLayout({ children, }: { @@ -29,12 +24,27 @@ export default async function AppLayout({ redirect("/login"); } + const [currentUser] = await db + .select({ + id: users.id, + username: users.username, + color: users.color, + status: users.status, + }) + .from(users) + .where(eq(users.id, session.user.id)) + .limit(1); + + if (!currentUser) { + redirect("/login"); + } + const userServers = await getUserServers(session.user.id); return (
- +
{children}
diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..e30b18e --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,41 @@ +/** + * @file app/api/auth/logout/route.ts + * @description API route handler for logging out authenticated users and updating their online presence. + */ + +import { auth } from "@/auth"; +import { db } from "@/db"; +import { users } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { NextResponse } from "next/server"; + +/** + * Handles POST requests to log out a user by updating their online status and last active timestamp. + * + * @returns {Promise} JSON response indicating success or error status. + */ +export async function POST(): Promise { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + await db + .update(users) + .set({ + status: "OFFLINE", + lastSeenAt: new Date(), + }) + .where(eq(users.id, session.user.id)); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Logout error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 72ff832..cc2caba 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,8 @@ /** * @file app/layout.tsx - * @description Root layout component that wraps the entire application, providing global CSS styles, base HTML structure, and page metadata. + * @description Root layout component providing the global HTML structure and application metadata. */ -import { SessionProvider } from "next-auth/react"; import "./globals.css"; /** @@ -28,9 +27,7 @@ export default function RootLayout({ }) { return ( - - {children} - + {children} ); } diff --git a/auth.ts b/auth.ts index ef5e3f3..3c5df77 100644 --- a/auth.ts +++ b/auth.ts @@ -1,6 +1,6 @@ /** * @file auth.ts - * @description NextAuth configuration defining authentication providers, credentials verification, JWT callbacks, and session handling. + * @description NextAuth configuration handling authentication and lightweight ID-only session management. */ import NextAuth from "next-auth"; @@ -21,10 +21,11 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ password: { label: "Password", type: "password" }, }, /** - * Authorizes user credentials by checking against database records and verifying the password. + * Authorizes user credentials against database records. * + * @function authorize * @param {Record | undefined} credentials - The incoming sign-in credentials containing email and password. - * @returns {Promise} The authenticated user object containing id, name, email, and color, or null if validation fails. + * @returns {Promise<{ id: string } | null>} The authenticated user object containing only the user ID, or null if validation fails. */ authorize: async (credentials) => { const email = credentials?.email as string | undefined; @@ -51,50 +52,63 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ return { id: user.id, - username: user.username, - email: user.email, - color: user.color, - status: user.status, }; }, }), ], callbacks: { /** - * Callback triggered when a JSON Web Token is created or updated. + * Populates the JWT token with the user ID upon initial sign in. * - * @param {Object} params - Callback parameters. - * @param {Object} params.token - The current JWT token payload. - * @param {Object} [params.user] - The authenticated user object available on initial sign in. - * @returns {Object} The updated JWT token containing custom user claims. + * @function jwt + * @param {Object} params - The callback parameters. + * @param {import("next-auth/jwt").JWT} params.token - The current JSON Web Token. + * @param {import("next-auth").User} [params.user] - The authenticated user object (available on first sign in). + * @returns {import("next-auth/jwt").JWT} The updated JWT token. */ jwt({ token, user }) { if (user) { token.id = user.id; - token.username = user.username; - token.color = user.color; - token.status = user.status; } return token; }, /** - * Callback triggered whenever a session is checked or accessed. + * Attaches the user ID from the JWT token to the active session. * - * @param {Object} params - Callback parameters. - * @param {Object} params.session - The current session object. - * @param {Object} params.token - The decoded JWT token payload. - * @returns {Object} The updated session object populated with custom token attributes. + * @function session + * @param {Object} params - The callback parameters. + * @param {import("next-auth").Session} params.session - The current user session object. + * @param {import("next-auth/jwt").JWT} params.token - The active JSON Web Token. + * @returns {import("next-auth").Session} The updated session object. */ session({ session, token }) { if (token && session.user) { session.user.id = token.id as string; - session.user.username = token.username as string; - session.user.color = token.color as string; - session.user.status = token.status as any; } return session; }, }, + events: { + /** + * Updates the user's status to ONLINE and refreshes lastSeenAt upon successful sign in. + * + * @function signIn + * @param {Object} params - The event parameters. + * @param {import("next-auth").User} params.user - The signed-in user object. + * @returns {Promise} + */ + async signIn({ user }) { + if (user?.id) { + await db + .update(users) + .set({ + status: "ONLINE", + lastSeenAt: new Date(), + }) + .where(eq(users.id, user.id)); + } + }, + }, pages: { signIn: "/login", }, diff --git a/components/layout/AppSidebar.tsx b/components/layout/AppSidebar.tsx index 9e0be55..b8a86bc 100644 --- a/components/layout/AppSidebar.tsx +++ b/components/layout/AppSidebar.tsx @@ -10,16 +10,42 @@ import { ServerSidebar } from "@/components/sidebar/ServerSidebar"; import { ChannelSidebar } from "@/components/sidebar/ChannelSidebar"; import { UserPanel } from "@/components/sidebar/UserPanel"; import type { ServerWithChannels } from "@/lib/context/ServerContext"; +import type { UserStatus } from "@/db/schema"; import { clsx } from "clsx"; +/** + * Properties representing the user in the sidebar. + * + * @interface SidebarUser + * @property {string} username - The display name of the user. + * @property {string} color - The custom color assigned to the user's avatar or profile. + * @property {UserStatus} status - The current online status of the user. + */ +export interface SidebarUser { + username: string; + color: string; + status: UserStatus; +} + +/** + * Properties for the AppSidebar component. + * + * @interface AppSidebarProps + * @property {ServerWithChannels[]} servers - List of available servers including their channels. + * @property {SidebarUser} user - Information about the currently authenticated user. + */ +interface AppSidebarProps { + servers: ServerWithChannels[]; + user: SidebarUser; +} + /** * Renders the responsive application sidebar containing server navigation, channel lists, and user profile. * - * @param {Object} props - The component props. - * @param {ServerWithChannels[]} props.servers - Array of server objects with channels to display in the server navigation bar. + * @param {AppSidebarProps} props - The component props. * @returns {JSX.Element} The rendered mobile overlay and responsive sidebar structure. */ -export function AppSidebar({ servers }: { servers: ServerWithChannels[] }) { +export function AppSidebar({ servers, user }: AppSidebarProps) { const { isNavOpen, closeAll } = useSidebarStore(); return ( @@ -36,7 +62,7 @@ export function AppSidebar({ servers }: { servers: ServerWithChannels[] }) {
- +
@@ -58,7 +84,7 @@ export function AppSidebar({ servers }: { servers: ServerWithChannels[] }) { - + ); diff --git a/components/sidebar/UserPanel.tsx b/components/sidebar/UserPanel.tsx index d2da64b..3f19c28 100644 --- a/components/sidebar/UserPanel.tsx +++ b/components/sidebar/UserPanel.tsx @@ -6,15 +6,36 @@ "use client"; import { LogOut, Settings } from "lucide-react"; -import { signOut, useSession } from "next-auth/react"; +import { signOut } from "next-auth/react"; import { UserAvatar } from "@/components/ui/UserAvatar"; +import { UserStatus } from "@/db/schema"; -export function UserPanel() { - const { data: session } = useSession(); +/** + * Props for the UserPanel component. + */ +interface UserPanelProps { + /** User details including username, avatar color, and online status. */ + user: { + username: string; + color: string; + status: UserStatus; + }; +} - if (!session?.user) return null; - - const { user } = session; +/** + * Renders the user panel footer containing the user's avatar, status, and action buttons. + * + * @param {UserPanelProps} props - Component properties. + * @returns {JSX.Element} The rendered user panel component. + */ +export function UserPanel({ user }: UserPanelProps) { + /** + * Handles user logout by invalidating the local session and redirecting to the login page. + */ + const handleLogout = async () => { + await fetch("/api/auth/logout", { method: "POST" }); + await signOut({ callbackUrl: "/login" }); + }; return (
@@ -47,7 +68,7 @@ export function UserPanel() {