From e775aeb20e2e785ab6c1a75de7e7bf2fd6dc3031 Mon Sep 17 00:00:00 2001 From: Divyanshgupta030 <145568562+Divyanshgupta030@users.noreply.github.com> Date: Sun, 10 Nov 2024 20:25:27 +0530 Subject: [PATCH] add auth --- apps/api/app/api/[[...route]]/hello.ts | 46 + apps/api/app/api/[[...route]]/mail.ts | 62 + apps/api/app/api/[[...route]]/route.ts | 124 +- apps/api/package.json | 8 +- apps/api/tsconfig.json | 10 +- apps/app/package.json | 7 +- apps/www/app/(auth)/dashboard/page.tsx | 14 + apps/www/app/(auth)/sign-in/page.tsx | 9 + apps/www/app/(auth)/sign-up/page.tsx | 9 + .../www/app/sign-in/[[...sign-in]]/layout.tsx | 16 - apps/www/app/sign-in/[[...sign-in]]/page.tsx | 63 - .../components/custom/account-switcher.tsx | 37 + .../www/components/custom/signInComponent.tsx | 75 + .../www/components/custom/signUpComponent.tsx | 90 + apps/www/components/ui/button.tsx | 26 +- apps/www/components/ui/form.tsx | 178 ++ apps/www/components/ui/label.tsx | 26 + apps/www/lib/auth-client.ts | 13 + apps/www/lib/server.ts | 16 + apps/www/package.json | 9 +- packages/auth/package.json | 18 + packages/auth/src/auth.ts | 36 + .../20241110144439_auth/migration.sql | 57 + .../prisma/migrations/migration_lock.toml | 3 + packages/database/prisma/schema.prisma | 55 +- packages/mail/.gitignore | 3 + packages/mail/package.json | 24 + packages/mail/src/constants.ts | 0 packages/mail/src/index.ts | 39 + packages/mail/src/template/template.tsx | 127 ++ packages/mail/tsconfig.json | 10 + packages/types/package.json | 14 + packages/types/src/mail.ts | 13 + pnpm-lock.yaml | 1870 ++++++++++++++++- 34 files changed, 2944 insertions(+), 163 deletions(-) create mode 100644 apps/api/app/api/[[...route]]/hello.ts create mode 100644 apps/api/app/api/[[...route]]/mail.ts create mode 100644 apps/www/app/(auth)/dashboard/page.tsx create mode 100644 apps/www/app/(auth)/sign-in/page.tsx create mode 100644 apps/www/app/(auth)/sign-up/page.tsx delete mode 100644 apps/www/app/sign-in/[[...sign-in]]/layout.tsx delete mode 100644 apps/www/app/sign-in/[[...sign-in]]/page.tsx create mode 100644 apps/www/components/custom/account-switcher.tsx create mode 100644 apps/www/components/custom/signInComponent.tsx create mode 100644 apps/www/components/custom/signUpComponent.tsx create mode 100644 apps/www/components/ui/form.tsx create mode 100644 apps/www/components/ui/label.tsx create mode 100644 apps/www/lib/auth-client.ts create mode 100644 apps/www/lib/server.ts create mode 100644 packages/auth/package.json create mode 100644 packages/auth/src/auth.ts create mode 100644 packages/database/prisma/migrations/20241110144439_auth/migration.sql create mode 100644 packages/database/prisma/migrations/migration_lock.toml create mode 100644 packages/mail/.gitignore create mode 100644 packages/mail/package.json create mode 100644 packages/mail/src/constants.ts create mode 100644 packages/mail/src/index.ts create mode 100644 packages/mail/src/template/template.tsx create mode 100644 packages/mail/tsconfig.json create mode 100644 packages/types/package.json create mode 100644 packages/types/src/mail.ts diff --git a/apps/api/app/api/[[...route]]/hello.ts b/apps/api/app/api/[[...route]]/hello.ts new file mode 100644 index 0000000..9788240 --- /dev/null +++ b/apps/api/app/api/[[...route]]/hello.ts @@ -0,0 +1,46 @@ +import { Hono } from "hono"; +const app = new Hono(); +import { prisma } from "@repo/db"; +app + .get("/", async (c) => { + const user = await prisma.user.findMany(); + return c.json({ + user, + }); + }) + .patch(async (c) => { + const name = await c.req.json(); + const test = await prisma.user.update({ + where: { + id: "123", + }, + data: { + name: name.name, + }, + }); + return c.json({ + test, + }); + }) + .delete(async (c) => { + const test = await prisma.user.delete({ + where: { + id: "2", + }, + }); + return c.json({ + test, + }); + }) + .post(async (c) => { + const body = await c.req.json(); + console.log(body); + const test = await prisma.user.create({ + data: body, + }); + return c.json({ + test, + }); + }); + +export default app; diff --git a/apps/api/app/api/[[...route]]/mail.ts b/apps/api/app/api/[[...route]]/mail.ts new file mode 100644 index 0000000..1db0248 --- /dev/null +++ b/apps/api/app/api/[[...route]]/mail.ts @@ -0,0 +1,62 @@ +import { Hono } from "hono"; +import { sendBatchEmail, sendEmail } from "@repo/mail"; +import { mailBatchSchema, mailSchema } from "@repo/types"; +import { zValidator } from "@hono/zod-validator"; + +const app = new Hono(); + +app + .post("/send", zValidator("json", mailSchema), async (c) => { + const { email, subject } = c.req.valid("json"); + const { data, error } = await sendEmail(email, subject); + if (error) { + return c.json( + { + message: "Email sent failed", + }, + 400, + ); + } + return c.json( + { + message: "Email sent successfully", + data, + }, + 200, + ); + }) + .get((c) => { + return c.json({ + message: "mail api is alive", + status: 200, + }); + }); + +app + .post("/send-batch", zValidator("json", mailBatchSchema), async (c) => { + const { emails, subject } = c.req.valid("json"); + const { data, error } = await sendBatchEmail(emails, subject); + if (error) { + return c.json( + { + message: "Email sent failed", + }, + 400, + ); + } + return c.json( + { + message: "All Emails sent successfully", + data, + }, + 200, + ); + }) + .get((c) => { + return c.json({ + message: "all mail api is alive", + status: 200, + }); + }); + +export default app; diff --git a/apps/api/app/api/[[...route]]/route.ts b/apps/api/app/api/[[...route]]/route.ts index d9ad22f..f26676f 100644 --- a/apps/api/app/api/[[...route]]/route.ts +++ b/apps/api/app/api/[[...route]]/route.ts @@ -1,52 +1,69 @@ -import { prisma } from "@repo/db"; -import { Hono } from "hono"; import { handle } from "hono/vercel"; +import { Hono } from "hono"; +import { auth } from "@repo/auth"; +import { cors } from "hono/cors"; +import mail from "./mail"; +import hello from "./hello"; + +const allowedOrigins = [ + "http://localhost:3003", + "https://www.plura.pro", + "http://app.plura.pro", +]; export const runtime = "nodejs"; -const app = new Hono().basePath("/api"); +const app = new Hono<{ + Variables: { + user: typeof auth.$Infer.Session.user | null; + session: typeof auth.$Infer.Session.session | null; + }; +}>().basePath("/api"); -app - .get("/hello", async (c) => { - const test = await prisma.user.findMany(); - return c.json({ - test, - }); - }) - .patch(async (c) => { - const name = await c.req.json(); - const test = await prisma.user.update({ - where: { - id: "123", - }, - data: { - name: name.name, - }, - }); - return c.json({ - test, - }); - }) - .delete(async (c) => { - const test = await prisma.user.delete({ - where: { - id: "2", +app.use( + "/auth/**", + cors({ + origin: allowedOrigins, + allowHeaders: ["Content-Type", "Authorization"], + allowMethods: ["POST", "GET", "OPTIONS"], + exposeHeaders: ["Content-Length"], + maxAge: 600, + credentials: true, + }), +); +app.options("/auth/**", (c) => { + const origin = c.req.raw.headers.get("origin") ?? ""; + + if (allowedOrigins.includes(origin)) { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "600", }, }); - return c.json({ - test, - }); - }) - .post(async (c) => { - const body = await c.req.json(); - console.log(body); - const test = await prisma.user.create({ - data: body, - }); - return c.json({ - test, - }); + } + + return new Response("Forbidden", { + status: 403, }); +}); +app.use("*", async (c, next) => { + const session = await auth.api.getSession({ headers: c.req.raw.headers }); + + if (!session) { + c.set("user", null); + c.set("session", null); + return next(); + } + + c.set("user", session.user); + c.set("session", session.session); + return next(); +}); app.get("/health", async (c) => { return c.json({ @@ -54,10 +71,33 @@ app.get("/health", async (c) => { status: 200, }); }); +app.get("/session", async (c) => { + const session = c.get("session"); + const user = c.get("user"); + + if (!user) return c.body(null, 401); + + return c.json({ + session, + user, + }); +}); +app.route("/hello", hello); +app.route("/mail", mail); +app.on(["POST", "GET"], "/auth/**", (c) => { + return auth.handler(c.req.raw); +}); +app.get("/multi-sessions", async (c) => { + const res = await auth.api.listDeviceSessions({ + headers: c.req.raw.headers, + }); + return c.json(res); +}); const GET = handle(app); const POST = handle(app); const PATCH = handle(app); const DELETE = handle(app); +const OPTIONS = handle(app); -export { GET, PATCH, POST, DELETE }; +export { GET, PATCH, POST, DELETE, OPTIONS }; diff --git a/apps/api/package.json b/apps/api/package.json index 8ef407c..50bc7fd 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -13,17 +13,21 @@ "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache" }, "dependencies": { + "@hono/node-server": "^1.13.5", + "@hono/zod-validator": "^0.4.1", + "@repo/auth": "workspace:*", "@repo/db": "workspace:*", - "contentlayer2": "^0.5.3", "hono": "^4.6.9", "next": "15.0.2", "react": "19.0.0-rc-02c0e824-20241028", - "react-dom": "19.0.0-rc-02c0e824-20241028" + "react-dom": "19.0.0-rc-02c0e824-20241028", + "zod": "^3.23.8" }, "devDependencies": { "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.10", + "dotenv": "^16.4.5", "typescript": "^5" } } \ No newline at end of file diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index f614f33..85ea767 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -12,8 +12,8 @@ "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "ESNext", + "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", @@ -34,8 +34,10 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts" - ], + ".next/types/**/*.ts", + "src/**/*.ts", + "src/**/*.d.ts" +, "../../packages/types/auth.ts" ], "exclude": [ "node_modules" ] diff --git a/apps/app/package.json b/apps/app/package.json index 7d1b5ef..3b67683 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -47,7 +47,10 @@ "sonner": "^1.7.0", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", - "zod": "^3.23.8" + "zod": "^3.23.8", + "@repo/auth": "workspace:*", + "@repo/db": "workspace:*", + "@repo/types": "workspace:*" }, "devDependencies": { "@types/node": "^20", @@ -60,4 +63,4 @@ "tailwindcss": "^3.4.1", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/apps/www/app/(auth)/dashboard/page.tsx b/apps/www/app/(auth)/dashboard/page.tsx new file mode 100644 index 0000000..0df0b68 --- /dev/null +++ b/apps/www/app/(auth)/dashboard/page.tsx @@ -0,0 +1,14 @@ +import AccountSwitcher from "@/components/custom/account-switcher"; +import { getMultipleSessions, getSession } from "@/lib/server"; + +export default async function page() { + const session = await getSession(); + const multipleSessions = await getMultipleSessions(); + return ( +
+ +
{JSON.stringify(session, null, 1)}
+
{JSON.stringify(multipleSessions,null,2)}
+
+ ); +} diff --git a/apps/www/app/(auth)/sign-in/page.tsx b/apps/www/app/(auth)/sign-in/page.tsx new file mode 100644 index 0000000..bd81345 --- /dev/null +++ b/apps/www/app/(auth)/sign-in/page.tsx @@ -0,0 +1,9 @@ +import SignInComponent from "@/components/custom/signInComponent"; + +export default function page() { + return ( +
+ +
+ ); +} diff --git a/apps/www/app/(auth)/sign-up/page.tsx b/apps/www/app/(auth)/sign-up/page.tsx new file mode 100644 index 0000000..ebdb563 --- /dev/null +++ b/apps/www/app/(auth)/sign-up/page.tsx @@ -0,0 +1,9 @@ +import SignUpComponent from "@/components/custom/signUpComponent"; + +export default function page() { + return ( +
+ +
+ ); +} diff --git a/apps/www/app/sign-in/[[...sign-in]]/layout.tsx b/apps/www/app/sign-in/[[...sign-in]]/layout.tsx deleted file mode 100644 index 6425796..0000000 --- a/apps/www/app/sign-in/[[...sign-in]]/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { SiteFooter } from "@/components/custom/site/footer"; -import { SiteHeader } from "@/components/custom/site/header"; - -interface AppLayoutProps { - children: React.ReactNode; -} - -export default function AppLayout({ children }: AppLayoutProps) { - return ( -
- -
{children}
- -
- ); -} diff --git a/apps/www/app/sign-in/[[...sign-in]]/page.tsx b/apps/www/app/sign-in/[[...sign-in]]/page.tsx deleted file mode 100644 index 76e5c85..0000000 --- a/apps/www/app/sign-in/[[...sign-in]]/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { PiGithubLogoBold } from "react-icons/pi"; -import { SiDiscord } from "react-icons/si"; -import { FcGoogle } from "react-icons/fc"; -import { Input } from "@/components/ui/input"; -import Link from "next/link"; -export default function SignInPage() { - return ( -
-
-
-
-

- Transform how you work. -

-
- Log In Your Plura Account -
-
-
-
- - - -
-
-
-
- - Email - - - - - already a member - -
-
- {/* experiment here*/} -
-
-
- ); -} diff --git a/apps/www/components/custom/account-switcher.tsx b/apps/www/components/custom/account-switcher.tsx new file mode 100644 index 0000000..9bcfb90 --- /dev/null +++ b/apps/www/components/custom/account-switcher.tsx @@ -0,0 +1,37 @@ +"use client"; +import { authClient } from "@/lib/auth-client"; +import { Session } from "@repo/auth"; +import { useRouter } from "next/navigation"; +interface Props { + session: Session[]; + activeSession: Session; +} +export default function AccountSwitcher({ session, activeSession }: Props) { + const router = useRouter(); + const onSelect = async (sessionId: string) => { + console.log(sessionId); + const active = await authClient.multiSession.setActive({ + sessionId: sessionId, + }); + + console.log(active); + router.refresh(); + }; + return ( +
+ +
+ ); +} diff --git a/apps/www/components/custom/signInComponent.tsx b/apps/www/components/custom/signInComponent.tsx new file mode 100644 index 0000000..ad587ea --- /dev/null +++ b/apps/www/components/custom/signInComponent.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { authClient } from "@/lib/auth-client"; + +const formSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), +}); + +export default function SignInComponent() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + password: "", + }, + }); + const onSubmit = async (SignInData: z.infer) => { + const { data, error } = await authClient.signIn.email( + { + email: SignInData.email, + password: SignInData.password, + callbackURL: "/dashboard", + }, + ); + }; + + return ( +
+ + ( + + email + + + + + + )} + /> + ( + + password + + + + + + )} + /> + + + + ); +} diff --git a/apps/www/components/custom/signUpComponent.tsx b/apps/www/components/custom/signUpComponent.tsx new file mode 100644 index 0000000..7d4c90f --- /dev/null +++ b/apps/www/components/custom/signUpComponent.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { authClient } from "@/lib/auth-client"; + +const formSchema = z.object({ + name: z.string(), + email: z.string().email(), + password: z.string().min(8), +}); + +export default function SignUpComponent() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + password: "", + }, + }); + const onSubmit = async (SignInData: z.infer) => { + const { data, error } = await authClient.signUp.email( + { + name: SignInData.name, + email: SignInData.email, + password: SignInData.password, + } + ); + }; + + return ( +
+ + ( + + name + + + + + + )} + /> + ( + + email + + + + + + )} + /> + ( + + password + + + + + + )} + /> + + + + ); +} diff --git a/apps/www/components/ui/button.tsx b/apps/www/components/ui/button.tsx index d09a695..65d4fcd 100644 --- a/apps/www/components/ui/button.tsx +++ b/apps/www/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", @@ -31,27 +31,27 @@ const buttonVariants = cva( variant: "default", size: "default", }, - }, -); + } +) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean; + asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; + const Comp = asChild ? Slot : "button" return ( - ); - }, -); -Button.displayName = "Button"; + ) + } +) +Button.displayName = "Button" -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/apps/www/components/ui/form.tsx b/apps/www/components/ui/form.tsx new file mode 100644 index 0000000..b6daa65 --- /dev/null +++ b/apps/www/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +