mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
migrate: replace NextAuth.js with Better Auth
- Install better-auth and @better-auth/drizzle-adapter - Create src/lib/auth.ts with Better Auth configuration using bcrypt - Update database schema: change auth table IDs from uuid to text - Update route handler from /api/auth/[...nextauth] to /api/auth/[...all] - Update tRPC context and middleware for Better Auth session handling - Update client components to use Better Auth APIs (signIn, signOut) - Update seed script with text-based IDs and correct account schema - Fix type errors in wizard components (robotId, optional chaining) - Fix API paths: api.robots.initialize -> api.robots.plugins.initialize - Update auth router to use text IDs for Better Auth compatibility Note: Auth tables were reset - users will need to re-register.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { cookies } from "next/headers";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "~/components/ui/sidebar";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import { AppSidebar } from "~/components/dashboard/app-sidebar";
|
||||
import { auth } from "~/server/auth";
|
||||
import { auth } from "~/lib/auth";
|
||||
import {
|
||||
BreadcrumbProvider,
|
||||
BreadcrumbDisplay,
|
||||
@@ -22,16 +22,15 @@ interface DashboardLayoutProps {
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: DashboardLayoutProps) {
|
||||
const session = await auth();
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/auth/signin");
|
||||
}
|
||||
|
||||
const userRole =
|
||||
typeof session.user.roles?.[0] === "string"
|
||||
? session.user.roles[0]
|
||||
: (session.user.roles?.[0]?.role ?? "observer");
|
||||
const userRole = "researcher"; // Default role for dashboard access
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true";
|
||||
|
||||
@@ -16,19 +16,10 @@ import { Separator } from "~/components/ui/separator";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { formatRole, getRoleDescription } from "~/lib/auth-client";
|
||||
import {
|
||||
User,
|
||||
Shield,
|
||||
Download,
|
||||
Trash2,
|
||||
ExternalLink,
|
||||
Lock,
|
||||
UserCog,
|
||||
Mail,
|
||||
Fingerprint,
|
||||
} from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { User, Shield, Download, Trash2, Lock, UserCog } from "lucide-react";
|
||||
import { useSession } from "~/lib/auth-client";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
interface ProfileUser {
|
||||
id: string;
|
||||
@@ -37,7 +28,8 @@ interface ProfileUser {
|
||||
image: string | null;
|
||||
roles?: Array<{
|
||||
role: "administrator" | "researcher" | "wizard" | "observer";
|
||||
grantedAt: string | Date;
|
||||
grantedAt: Date;
|
||||
grantedBy: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -213,14 +205,20 @@ function ProfileContent({ user }: { user: ProfileUser }) {
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { data: session, status } = useSession();
|
||||
const { data: session, isPending } = useSession();
|
||||
const { data: userData, isPending: isUserPending } = api.auth.me.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: !!session?.user,
|
||||
},
|
||||
);
|
||||
|
||||
useBreadcrumbsEffect([
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Profile" },
|
||||
]);
|
||||
|
||||
if (status === "loading") {
|
||||
if (isPending || isUserPending) {
|
||||
return (
|
||||
<div className="text-muted-foreground animate-pulse p-8">
|
||||
Loading profile...
|
||||
@@ -232,7 +230,13 @@ export default function ProfilePage() {
|
||||
redirect("/auth/signin");
|
||||
}
|
||||
|
||||
const user = session.user;
|
||||
const user: ProfileUser = {
|
||||
id: session.user.id,
|
||||
name: userData?.name ?? session.user.name ?? null,
|
||||
email: userData?.email ?? session.user.email,
|
||||
image: userData?.image ?? session.user.image ?? null,
|
||||
roles: userData?.systemRoles as ProfileUser["roles"],
|
||||
};
|
||||
|
||||
return <ProfileContent user={user} />;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from "~/components/ui/entity-view";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSession } from "~/lib/auth-client";
|
||||
import { useStudyManagement } from "~/hooks/useStudyManagement";
|
||||
|
||||
interface ExperimentDetailPageProps {
|
||||
@@ -99,6 +99,9 @@ export default function ExperimentDetailPage({
|
||||
params,
|
||||
}: ExperimentDetailPageProps) {
|
||||
const { data: session } = useSession();
|
||||
const { data: userData } = api.auth.me.useQuery(undefined, {
|
||||
enabled: !!session?.user,
|
||||
});
|
||||
const [experiment, setExperiment] = useState<Experiment | null>(null);
|
||||
const [trials, setTrials] = useState<Trial[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -181,7 +184,7 @@ export default function ExperimentDetailPage({
|
||||
const description = experiment.description;
|
||||
|
||||
// Check if user can edit this experiment
|
||||
const userRoles = session?.user?.roles?.map((r) => r.role) ?? [];
|
||||
const userRoles = userData?.roles ?? [];
|
||||
const canEdit =
|
||||
userRoles.includes("administrator") || userRoles.includes("researcher");
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSession } from "~/lib/auth-client";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
FileText,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "~/components/ui/entity-view";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSession } from "~/lib/auth-client";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
interface StudyDetailPageProps {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { WizardView } from "~/components/trials/views/WizardView";
|
||||
import { ObserverView } from "~/components/trials/views/ObserverView";
|
||||
import { ParticipantView } from "~/components/trials/views/ParticipantView";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSession } from "~/lib/auth-client";
|
||||
|
||||
function WizardPageContent() {
|
||||
const params = useParams();
|
||||
@@ -25,6 +25,11 @@ function WizardPageContent() {
|
||||
const { study } = useSelectedStudyDetails();
|
||||
const { data: session } = useSession();
|
||||
|
||||
// Get user roles
|
||||
const { data: userData } = api.auth.me.useQuery(undefined, {
|
||||
enabled: !!session?.user,
|
||||
});
|
||||
|
||||
// Get trial data
|
||||
const {
|
||||
data: trial,
|
||||
@@ -67,7 +72,7 @@ function WizardPageContent() {
|
||||
}
|
||||
|
||||
// Default role logic based on user
|
||||
const userRole = session.user.roles?.[0]?.role ?? "observer";
|
||||
const userRole = userData?.roles?.[0] ?? "observer";
|
||||
if (userRole === "administrator" || userRole === "researcher") {
|
||||
return "wizard";
|
||||
}
|
||||
@@ -188,6 +193,7 @@ function WizardPageContent() {
|
||||
name: trial.experiment.name,
|
||||
description: trial.experiment.description,
|
||||
studyId: trial.experiment.studyId,
|
||||
robotId: trial.experiment.robotId,
|
||||
},
|
||||
participant: {
|
||||
id: trial.participant.id,
|
||||
|
||||
4
src/app/api/auth/[...all]/route.ts
Executable file
4
src/app/api/auth/[...all]/route.ts
Executable file
@@ -0,0 +1,4 @@
|
||||
import { auth } from "~/lib/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth);
|
||||
@@ -1,3 +0,0 @@
|
||||
import { handlers } from "~/server/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
generateFileKey,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
uploadFile,
|
||||
validateFile,
|
||||
} from "~/lib/storage/minio";
|
||||
import { auth } from "~/server/auth";
|
||||
import { auth } from "~/lib/auth";
|
||||
import { db } from "~/server/db";
|
||||
import {
|
||||
experiments,
|
||||
@@ -28,7 +29,9 @@ const uploadSchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const session = await auth();
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
@@ -181,7 +184,9 @@ export async function POST(request: NextRequest) {
|
||||
// Generate presigned upload URL for direct client uploads
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
import { signIn } from "~/lib/auth-client";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
@@ -37,22 +37,21 @@ export default function SignInPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await signIn("credentials", {
|
||||
const result = await signIn.email({
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
setError("Invalid email or password");
|
||||
if (result.error) {
|
||||
setError(result.error.message || "Invalid email or password");
|
||||
} else {
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
} catch (err: unknown) {
|
||||
setError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "An error occurred. Please try again.",
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { signOut, useSession } from "~/lib/auth-client";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -14,33 +14,29 @@ import {
|
||||
} from "~/components/ui/card";
|
||||
|
||||
export default function SignOutPage() {
|
||||
const { data: session, status } = useSession();
|
||||
const { data: session, isPending } = useSession();
|
||||
const router = useRouter();
|
||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// If user is not logged in, redirect to home
|
||||
if (status === "loading") return; // Still loading
|
||||
if (!session) {
|
||||
if (!isPending && !session) {
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
}, [session, status, router]);
|
||||
}, [session, isPending, router]);
|
||||
|
||||
const handleSignOut = async () => {
|
||||
setIsSigningOut(true);
|
||||
try {
|
||||
await signOut({
|
||||
callbackUrl: "/",
|
||||
redirect: true,
|
||||
});
|
||||
await signOut();
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error("Error signing out:", error);
|
||||
setIsSigningOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "loading") {
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100">
|
||||
<div className="text-center">
|
||||
@@ -52,7 +48,7 @@ export default function SignOutPage() {
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return null; // Will redirect via useEffect
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -80,7 +76,7 @@ export default function SignOutPage() {
|
||||
<div className="rounded-md bg-blue-50 p-3 text-sm text-blue-700">
|
||||
<p className="font-medium">
|
||||
Currently signed in as:{" "}
|
||||
{session.user.name ?? session.user.email}
|
||||
{session.user?.name ?? session.user?.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +99,8 @@ export default function SignOutPage() {
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center text-xs text-slate-500">
|
||||
<p>
|
||||
© 2024 HRIStudio. A platform for Human-Robot Interaction research.
|
||||
© {new Date().getFullYear()} HRIStudio. A platform for Human-Robot
|
||||
Interaction research.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ import { Badge } from "~/components/ui/badge";
|
||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useTour } from "~/components/onboarding/TourProvider";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSession } from "~/lib/auth-client";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { startTour } = useTour();
|
||||
|
||||
@@ -3,7 +3,6 @@ import "~/styles/globals.css";
|
||||
import { type Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { TRPCReactProvider } from "~/trpc/react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -24,9 +23,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable}`}>
|
||||
<body>
|
||||
<SessionProvider>
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
</SessionProvider>
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Logo } from "~/components/ui/logo";
|
||||
import { auth } from "~/server/auth";
|
||||
import { auth } from "~/lib/auth";
|
||||
import {
|
||||
ArrowRight,
|
||||
Beaker,
|
||||
@@ -20,7 +21,9 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
export default async function Home() {
|
||||
const session = await auth();
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
if (session?.user) {
|
||||
redirect("/dashboard");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { headers } from "next/headers";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -7,10 +8,12 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { auth } from "~/server/auth";
|
||||
import { auth } from "~/lib/auth";
|
||||
|
||||
export default async function UnauthorizedPage() {
|
||||
const session = await auth();
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4">
|
||||
@@ -60,13 +63,6 @@ export default async function UnauthorizedPage() {
|
||||
<div className="rounded-md bg-blue-50 p-3 text-sm text-blue-700">
|
||||
<p className="font-medium">Current User:</p>
|
||||
<p>{session.user.name ?? session.user.email}</p>
|
||||
{session.user.roles && session.user.roles.length > 0 ? (
|
||||
<p className="mt-1">
|
||||
Roles: {session.user.roles.map((r) => r.role).join(", ")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-1">No roles assigned</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user