-
Notifications
You must be signed in to change notification settings - Fork 0
feat(auth): roles and admin dashboard protection #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
34bfc5f
1ac5d67
c2ebadf
3c3b438
971fb30
d7f06b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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") { | ||
| 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> | ||
|
achneerov marked this conversation as resolved.
|
||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above, maybe in a constants file?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment. See previous |
||
| </CardDescription> | ||
| </CardHeader> | ||
| <CardContent> | ||
|
|
@@ -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 | ||
|
|
@@ -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> | ||
|
|
||
| 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"), | ||
|
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"), | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
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
|
||
|
|
||
| return supabaseResponse; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.