-
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 all 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 |
|---|---|---|
|
|
@@ -57,4 +57,6 @@ drizzle/meta/ | |
| .supabase/ | ||
|
|
||
|
|
||
| .agents/ | ||
| .agents/ | ||
| .playwright-mcp/ | ||
| .mcp.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export default async function DashboardPage() { | ||
| 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"> | ||
| You are admin | ||
| </p> | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,37 +2,66 @@ | |
|
|
||
| import { redirect } from "next/navigation"; | ||
| import { createClient } from "@/utils/supabase/server"; | ||
| import { loginSchema, signupSchema } from "./schema"; | ||
|
|
||
| export async function login(formData: FormData) { | ||
| const supabase = await createClient(); | ||
| export type ActionState = { | ||
| errors: Partial<Record<string, string[]>>; | ||
| } | null; | ||
|
|
||
| const { error } = await supabase.auth.signInWithPassword({ | ||
| email: formData.get("email") as string, | ||
| password: formData.get("password") as string, | ||
| export async function login( | ||
| _prevState: ActionState, | ||
| formData: FormData | ||
| ): Promise<ActionState> { | ||
| const result = loginSchema.safeParse({ | ||
| email: formData.get("email"), | ||
| password: formData.get("password"), | ||
| }); | ||
|
|
||
| if (!result.success) { | ||
| return { errors: result.error.flatten().fieldErrors }; | ||
| } | ||
|
|
||
| const supabase = await createClient(); | ||
| const { error } = await supabase.auth.signInWithPassword(result.data); | ||
|
|
||
| if (error) { | ||
| redirect(`/login?error=${encodeURIComponent(error.message)}`); | ||
| return { errors: { email: [error.message] } }; | ||
| } | ||
|
|
||
| redirect("/"); | ||
| } | ||
|
|
||
| export async function signup(formData: FormData) { | ||
| const supabase = await createClient(); | ||
| export async function signup( | ||
| _prevState: ActionState, | ||
| formData: FormData | ||
| ): Promise<ActionState> { | ||
| const result = signupSchema.safeParse({ | ||
| firstName: formData.get("first_name"), | ||
| lastName: formData.get("last_name"), | ||
| email: formData.get("email"), | ||
| password: formData.get("password"), | ||
| }); | ||
|
|
||
| if (!result.success) { | ||
| return { errors: result.error.flatten().fieldErrors }; | ||
| } | ||
|
|
||
| const { firstName, lastName, email, password } = result.data; | ||
|
|
||
| const supabase = await createClient(); | ||
| const { error } = await supabase.auth.signUp({ | ||
| email: formData.get("email") as string, | ||
| password: formData.get("password") as string, | ||
| email, | ||
| password, | ||
| options: { | ||
| data: { first_name: firstName, last_name: lastName }, | ||
| }, | ||
| }); | ||
|
|
||
| if (error) { | ||
| redirect(`/login?error=${encodeURIComponent(error.message)}`); | ||
| return { errors: { email: [error.message] } }; | ||
| } | ||
|
|
||
| redirect( | ||
| `/login?message=${encodeURIComponent("Check your email to confirm your account.")}` | ||
| ); | ||
| redirect("/login?message=Check+your+email+to+confirm+your+account."); | ||
|
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. Should we make a enum file for strings like this? something like constants.js, and maybe we would put it in a constants folder maybe in lib/
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. For now, we don't have that much strings, let's try to not over complicate the project for nothing. Later on, if we do have a lot of strings we could yes. Usually having a lib for strings is useful when you have locales translation in multiple languages. |
||
| } | ||
|
|
||
| export async function signout() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| "use client"; | ||
|
|
||
| import { login, signup } from "./actions"; | ||
| import { useActionState, useState, Suspense } from "react"; | ||
| import { useSearchParams } from "next/navigation"; | ||
| import { Suspense } from "react"; | ||
| import { login, signup, type ActionState } from "./actions"; | ||
| import { | ||
| Card, | ||
| CardContent, | ||
|
|
@@ -14,61 +14,117 @@ import { Input } from "@/components/ui/input"; | |
| import { Label } from "@/components/ui/label"; | ||
| import { Button } from "@/components/ui/button"; | ||
|
|
||
| function FieldError({ errors }: { errors?: string[] }) { | ||
| if (!errors?.length) return null; | ||
| return <p className="text-sm text-destructive">{errors[0]}</p>; | ||
| } | ||
|
|
||
| function LoginForm() { | ||
| const searchParams = useSearchParams(); | ||
| const message = searchParams.get("message"); | ||
| const error = searchParams.get("error"); | ||
| const [mode, setMode] = useState<"login" | "signup">("login"); | ||
|
|
||
| const [loginState, loginAction] = useActionState<ActionState, FormData>( | ||
| login, | ||
| null | ||
| ); | ||
| const [signupState, signupAction] = useActionState<ActionState, FormData>( | ||
| signup, | ||
| null | ||
| ); | ||
|
|
||
| const state = mode === "login" ? loginState : signupState; | ||
| const action = mode === "login" ? loginAction : signupAction; | ||
|
|
||
| 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> | ||
| {error && ( | ||
| <div className="mb-4 rounded-md bg-destructive/10 p-3 text-sm text-destructive"> | ||
| {error} | ||
| </div> | ||
| )} | ||
| {message && ( | ||
| <div className="mb-4 rounded-md bg-green-500/10 p-3 text-sm text-green-700 dark:text-green-400"> | ||
| {message} | ||
| </div> | ||
| )} | ||
|
|
||
| <form className="space-y-4"> | ||
| <div className="space-y-2"> | ||
| <form action={action} className="space-y-4" noValidate> | ||
| {mode === "signup" && ( | ||
| <div className="flex gap-3"> | ||
| <div className="space-y-1 flex-1"> | ||
| <Label htmlFor="first_name">First name</Label> | ||
| <Input | ||
| id="first_name" | ||
| name="first_name" | ||
| type="text" | ||
| placeholder="Jane" | ||
| /> | ||
| <FieldError errors={signupState?.errors?.firstName} /> | ||
| </div> | ||
| <div className="space-y-1 flex-1"> | ||
| <Label htmlFor="last_name">Last name</Label> | ||
| <Input | ||
| id="last_name" | ||
| name="last_name" | ||
| type="text" | ||
| placeholder="Doe" | ||
| /> | ||
| <FieldError errors={signupState?.errors?.lastName} /> | ||
| </div> | ||
| </div> | ||
| )} | ||
| <div className="space-y-1"> | ||
| <Label htmlFor="email">Email</Label> | ||
| <Input | ||
| id="email" | ||
| name="email" | ||
| type="email" | ||
| required | ||
| placeholder="you@example.com" | ||
| /> | ||
| <FieldError errors={state?.errors?.email} /> | ||
| </div> | ||
| <div className="space-y-2"> | ||
| <div className="space-y-1"> | ||
| <Label htmlFor="password">Password</Label> | ||
| <Input | ||
| id="password" | ||
| name="password" | ||
| type="password" | ||
| required | ||
| minLength={6} | ||
| placeholder="••••••••" | ||
| /> | ||
| <FieldError errors={state?.errors?.password} /> | ||
| </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 type="submit">Log in</Button> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| onClick={() => setMode("signup")} | ||
| > | ||
| Create an account | ||
| </Button> | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <Button type="submit">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 |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { z } from "zod"; | ||
|
|
||
| export const loginSchema = z.object({ | ||
| email: z.string().email("Invalid email address"), | ||
| password: z.string().min(6, "Password must be at least 6 characters"), | ||
| }); | ||
|
|
||
| export const signupSchema = z.object({ | ||
| firstName: z.string().min(1, "First name is required"), | ||
| lastName: z.string().min(1, "Last name is required"), | ||
| email: z.string().email("Invalid email address"), | ||
| password: z.string().min(6, "Password must be at least 6 characters"), | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export const ROLES = { | ||
| ADMIN: "admin", | ||
| COACH: "coach", | ||
| USER: "user", | ||
| } as const; | ||
|
|
||
| export type Role = (typeof ROLES)[keyof typeof ROLES]; |
Uh oh!
There was an error while loading. Please reload this page.