Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();

if (!user || user.app_metadata?.role !== "admin") {
Comment thread
martin0024 marked this conversation as resolved.
Outdated
redirect("/");
}

return (
<main className="flex min-h-screen flex-col p-8">
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
<p className="mt-2 text-muted-foreground">
Signed in as <span className="font-medium">{user.email}</span>
</p>
Comment thread
achneerov marked this conversation as resolved.
</main>
);
}
6 changes: 6 additions & 0 deletions app/login/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export async function signup(formData: FormData) {
const { error } = await supabase.auth.signUp({
email: formData.get("email") as string,
password: formData.get("password") as string,
options: {
data: {
first_name: formData.get("first_name") as string,
last_name: formData.get("last_name") as string,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signup casts formData.get("first_name"/"last_name") to string without validating presence/type. If these fields are missing (e.g., crafted request, or future UI change), formData.get(...) is null and Supabase will receive null metadata values. Add server-side validation (ensure values are non-empty strings, trim), and redirect with a clear error when invalid before calling auth.signUp.

Suggested change
const { error } = await supabase.auth.signUp({
email: formData.get("email") as string,
password: formData.get("password") as string,
options: {
data: {
first_name: formData.get("first_name") as string,
last_name: formData.get("last_name") as string,
const rawFirstName = formData.get("first_name");
const rawLastName = formData.get("last_name");
if (typeof rawFirstName !== "string" || typeof rawLastName !== "string") {
redirect(
`/login?error=${encodeURIComponent("First name and last name are required.")}`
);
}
const firstName = rawFirstName.trim();
const lastName = rawLastName.trim();
if (!firstName || !lastName) {
redirect(
`/login?error=${encodeURIComponent("First name and last name are required.")}`
);
}
const { error } = await supabase.auth.signUp({
email: formData.get("email") as string,
password: formData.get("password") as string,
options: {
data: {
first_name: firstName,
last_name: lastName,

Copilot uses AI. Check for mistakes.
},
},
});

if (error) {
Expand Down
66 changes: 56 additions & 10 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { login, signup } from "./actions";
import { useSearchParams } from "next/navigation";
import { Suspense } from "react";
import { Suspense, useState } from "react";
import {
Card,
CardContent,
Expand All @@ -18,14 +18,19 @@ function LoginForm() {
const searchParams = useSearchParams();
const message = searchParams.get("message");
const error = searchParams.get("error");
const [mode, setMode] = useState<"login" | "signup">("login");

return (
<div className="flex min-h-screen items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl">Welcome</CardTitle>
<CardTitle className="text-2xl">
{mode === "login" ? "Welcome back" : "Create an account"}
</CardTitle>
<CardDescription>
Sign in to your account or create a new one
{mode === "login"
? "Sign in to your account"
: "Fill in your details to get started"}
Comment on lines +48 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, maybe in a constants file?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment. See previous

</CardDescription>
</CardHeader>
<CardContent>
Expand All @@ -41,6 +46,30 @@ function LoginForm() {
)}

<form className="space-y-4">
{mode === "signup" && (
<div className="flex gap-3">
<div className="space-y-2 flex-1">
<Label htmlFor="first_name">First name</Label>
<Input
id="first_name"
name="first_name"
type="text"
required
placeholder="Jane"
/>
</div>
<div className="space-y-2 flex-1">
<Label htmlFor="last_name">Last name</Label>
<Input
id="last_name"
name="last_name"
type="text"
required
placeholder="Doe"
/>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
Expand All @@ -62,13 +91,30 @@ function LoginForm() {
placeholder="••••••••"
/>
</div>
<div className="flex gap-2 pt-2">
<Button formAction={login} className="flex-1">
Log in
</Button>
<Button formAction={signup} variant="outline" className="flex-1">
Sign up
</Button>
<div className="flex flex-col gap-2 pt-2">
{mode === "login" ? (
<>
<Button formAction={login}>Log in</Button>
<Button
type="button"
variant="outline"
onClick={() => setMode("signup")}
>
Create an account
</Button>
</>
) : (
<>
<Button formAction={signup}>Sign up</Button>
<Button
type="button"
variant="outline"
onClick={() => setMode("login")}
>
Already have an account? Log in
</Button>
</>
)}
</div>
</form>
</CardContent>
Expand Down
10 changes: 8 additions & 2 deletions lib/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";

export const userRoleEnum = pgEnum("user_role", ["admin", "coach", "user"]);

export const profiles = pgTable("profiles", {
id: uuid("id").primaryKey(),
fullName: text("full_name"),
firstName: text("first_name"),
lastName: text("last_name"),
avatarUrl: text("avatar_url"),
Comment thread
Jxl-s marked this conversation as resolved.
Outdated
stripeCustomerId: text("stripe_customer_id"),
role: userRoleEnum("role").default("user").notNull(),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at"),
});
10 changes: 10 additions & 0 deletions utils/supabase/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,15 @@ export async function updateSession(request: NextRequest) {
return NextResponse.redirect(url);
}

// Protect /dashboard/* — admin only
if (request.nextUrl.pathname.startsWith("/dashboard")) {
const role = user?.app_metadata?.role;
Comment thread
martin0024 marked this conversation as resolved.
Outdated
if (role !== "admin") {
const url = request.nextUrl.clone();
url.pathname = "/";
return NextResponse.redirect(url);
}
}
Comment on lines +48 to +57
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Role checks use the string literal "admin" in multiple places (middleware + dashboard page). To reduce drift if role values change, consider centralizing role names/types in a shared module (e.g., a UserRole union/constant) and reusing it for comparisons.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, read prev comments


return supabaseResponse;
}
Loading