diff --git a/.env.example b/.env.example index c44ddcb..80cac69 100644 --- a/.env.example +++ b/.env.example @@ -21,10 +21,6 @@ NEXTAUTH_SECRET="secret" STRIPE_PUBLISHABLE_KEY= STRIPE_PRIVATE_KEY= -# Next Auth Discord Provider -DISCORD_CLIENT_ID= -DISCORD_CLIENT_SECRET= - # Email sending EMAIL_SERVER_USER= EMAIL_SERVER_PASSWORD= diff --git a/package.json b/package.json index 0e21fa9..d38f255 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "lint:prisma": "prisma validate", "lint:fix": "next lint --fix && prisma format", "start": "next start", - "ragequit": "rm -rf .next && npm run db:push && npm run dev" + "ragequit": "rm -rf .next && npm run db:push && npm run dev", + "seed": "prisma db seed" }, "prisma": { "seed": "tsx prisma/seeds/index.ts" diff --git a/src/app/actions/validateUserEmail.ts b/src/app/actions/validateUserEmail.ts index 8b22ea4..801a01f 100644 --- a/src/app/actions/validateUserEmail.ts +++ b/src/app/actions/validateUserEmail.ts @@ -1,21 +1,27 @@ "use server" import { MembershipStatus } from "@prisma/client" - import { db } from "@/services/db" +import { Target } from "@/app/shared/login/types" + export interface ServerActionState { checked: boolean valid: boolean email?: string } + interface FormProps { email: string + target: string } + export async function validateUserEmail( prevState: ServerActionState, data: FormProps, ) { + console.log(data) + const user = await db.user.findUnique({ where: { email: data.email, @@ -29,6 +35,15 @@ export async function validateUserEmail( valid: false, } + // No need for further checks if the target is the admin portal + if (data.target === (Target.AdminPortal as string)) { + return { + checked: true, + email: data.email, + valid: true, + } + } + // User without membership means that is not valid const membership = await db.membership.findFirst({ where: { diff --git a/src/app/admin/@unauthenticated/components/discordSignIn.tsx b/src/app/admin/@unauthenticated/components/discordSignIn.tsx deleted file mode 100644 index 6b17908..0000000 --- a/src/app/admin/@unauthenticated/components/discordSignIn.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client" - -import { signIn } from "next-auth/react" -import { DiscordLogoIcon } from "@radix-ui/react-icons" - -export function DiscordSignIn() { - return ( - - ) -} diff --git a/src/app/admin/@unauthenticated/page.tsx b/src/app/admin/@unauthenticated/page.tsx index d7e3988..e11e3f2 100644 --- a/src/app/admin/@unauthenticated/page.tsx +++ b/src/app/admin/@unauthenticated/page.tsx @@ -1,24 +1,19 @@ -import { DiscordSignIn } from "@/app/admin/@unauthenticated/components/discordSignIn" -import { - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" +"use client" + +import { LoginForm } from "@/app/shared/login/form" +import { Target } from "@/app/shared/login/types" export default function AdminLogin() { return ( - - - Login - - You need to login in order to access the admin - - - - - - +
+
+ +
+
) } diff --git a/src/app/members/@unauthenticated/page.tsx b/src/app/members/@unauthenticated/page.tsx index a9979c4..57c7e46 100644 --- a/src/app/members/@unauthenticated/page.tsx +++ b/src/app/members/@unauthenticated/page.tsx @@ -1,152 +1,18 @@ "use client" -import { zodResolver } from "@hookform/resolvers/zod" -import { ExclamationTriangleIcon } from "@radix-ui/react-icons" -import Link from "next/link" -import { signIn } from "next-auth/react" -import { useEffect, useState } from "react" -import { useFormState } from "react-dom" -import { useForm } from "react-hook-form" -import { z } from "zod" - -import { validateUserEmail } from "@/app/actions/validateUserEmail" -import { StatefulButton } from "@/components/molecules/statefulButton" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" - -const formSchema = z.object({ - email: z.string().email(), -}) +import { LoginForm } from "@/app/shared/login/form" +import { Target } from "@/app/shared/login/types" export default function MembershipPortalLoginPage() { - const form = useForm>({ - defaultValues: { - email: "", - }, - resolver: zodResolver(formSchema), - }) - const [working, setWorking] = useState(false) - const [requestedMagicLink, setRequestedMagicLink] = useState(false) - const [validateUserEmailState, validateUserEmailAction] = useFormState( - validateUserEmail, - { - checked: false, - valid: false, - }, - ) - - useEffect(() => { - const handler = async () => { - if (!requestedMagicLink && validateUserEmailState.valid) { - setWorking(true) - const reply = await signIn("email", { - email: validateUserEmailState.email, - redirect: false, - }) - console.log(reply) - setRequestedMagicLink(true) - setWorking(false) - } - } - - void handler() - }, [requestedMagicLink, validateUserEmailState]) - return (
-
- - - - Login - - Enter your email below, we're going to send you a magic link - - - - {validateUserEmailState.checked && - !validateUserEmailState.valid && ( - - - Error - - This email doesn't have an account. -
- You can create one by{" "} - - requesting a membership - -
-
- )} - - {requestedMagicLink && ( - - - Success - - We've sent you a "magic link" via email. -
- Please check your inbox (or spam folder) and click on the - link to proceed. -
-
- )} - - {!requestedMagicLink && ( - <> -
- ( - - Email - - - - - - )} - /> -
- - - Sign in - - - )} -
-
-
- +
) diff --git a/src/app/shared/login/form.tsx b/src/app/shared/login/form.tsx new file mode 100644 index 0000000..27d0b0c --- /dev/null +++ b/src/app/shared/login/form.tsx @@ -0,0 +1,153 @@ +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { ExclamationTriangleIcon } from "@radix-ui/react-icons" +import Link from "next/link" +import { Input } from "@/components/ui/input" +import { StatefulButton } from "@/components/molecules/statefulButton" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { useEffect, useState } from "react" +import { useFormState } from "react-dom" +import { validateUserEmail } from "@/app/actions/validateUserEmail" +import { signIn } from "next-auth/react" +import { Debug } from "@/components/devtool/debug" +import { type LoginFormProps } from "@/app/shared/login/types" + +const formSchema = z.object({ + email: z.string().email(), + target: z.string(), +}) + +export function LoginForm({ description, target }: LoginFormProps) { + const form = useForm>({ + defaultValues: { + email: "", + target: target, + }, + resolver: zodResolver(formSchema), + }) + const [working, setWorking] = useState(false) + const [requestedMagicLink, setRequestedMagicLink] = useState(false) + const [validateUserEmailState, validateUserEmailAction] = useFormState( + validateUserEmail, + { + checked: false, + valid: false, + }, + ) + + useEffect(() => { + console.log(target) + const handler = async () => { + if (!requestedMagicLink && validateUserEmailState.valid) { + setWorking(true) + const reply = await signIn("email", { + email: validateUserEmailState.email, + redirect: false, + }) + console.log(reply) + setRequestedMagicLink(true) + setWorking(false) + } + } + + void handler() + }, [requestedMagicLink, validateUserEmailState]) + + return ( +
+ + + + Login + {description} + {form.getValues()} + {working} + {requestedMagicLink} + {validateUserEmailState} + + + {validateUserEmailState.checked && + !validateUserEmailState.valid && ( + + + Error + + This email doesn't have an account. +
+ You can create one by{" "} + + requesting a membership + +
+
+ )} + + {requestedMagicLink && ( + + + Success + + We've sent you a "magic link" via email. +
+ Please check your inbox (or spam folder) and click on the link + to proceed. +
+
+ )} + + {!requestedMagicLink && ( + <> +
+ ( + + Email + + + + + + )} + /> +
+ + + Sign in + + + )} +
+
+
+ + ) +} diff --git a/src/app/shared/login/types.tsx b/src/app/shared/login/types.tsx new file mode 100644 index 0000000..bc89b22 --- /dev/null +++ b/src/app/shared/login/types.tsx @@ -0,0 +1,9 @@ +export enum Target { + MembershipPortal = "membership_portal", + AdminPortal = "admin_portal", +} + +export interface LoginFormProps { + description?: string + target: Target +} diff --git a/src/env.js b/src/env.js index c2a5693..6aeaa09 100644 --- a/src/env.js +++ b/src/env.js @@ -23,8 +23,6 @@ export const env = createEnv({ */ runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, - DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID, - DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET, EMAIL_FROM: process.env.EMAIL_FROM, EMAIL_SERVER_HOST: process.env.EMAIL_SERVER_HOST, EMAIL_SERVER_PASSWORD: process.env.EMAIL_SERVER_PASSWORD, @@ -50,8 +48,6 @@ export const env = createEnv({ (str) => !str.includes("YOUR_MYSQL_URL_HERE"), "You forgot to change the default URL", ), - DISCORD_CLIENT_ID: z.string().optional(), - DISCORD_CLIENT_SECRET: z.string().optional(), EMAIL_FROM: z.string().email().optional(), EMAIL_SERVER_HOST: z.string().optional(), diff --git a/src/server/auth.ts b/src/server/auth.ts index f7a91f6..11586ca 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -5,7 +5,6 @@ import { type NextAuthOptions, } from "next-auth" import { type Adapter } from "next-auth/adapters" -import DiscordProvider from "next-auth/providers/discord" import EmailProvider, { type SendVerificationRequestParams, } from "next-auth/providers/email" @@ -60,14 +59,9 @@ if (env.EMAIL_SERVER_HOST) { } }, }) -} - -if (env.DISCORD_CLIENT_ID && env.DISCORD_CLIENT_SECRET) { - providers.push( - DiscordProvider({ - clientId: env.DISCORD_CLIENT_ID, - clientSecret: env.DISCORD_CLIENT_SECRET, - }), +} else { + console.warn( + "Email server not configured, login and emails will not be available.", ) }