this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(auth): working

+170 -121
+1 -1
.vscode/settings.json
··· 3 3 "source.organizeImports.biome": "explicit", 4 4 "source.fixAll.biome": "explicit" 5 5 } 6 - } 6 + }
-94
auth-schema.ts
··· 1 - import { relations } from "drizzle-orm"; 2 - import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core"; 3 - 4 - export const user = pgTable("user", { 5 - id: text("id").primaryKey(), 6 - name: text("name").notNull(), 7 - email: text("email").notNull().unique(), 8 - emailVerified: boolean("email_verified").default(false).notNull(), 9 - image: text("image"), 10 - createdAt: timestamp("created_at").defaultNow().notNull(), 11 - updatedAt: timestamp("updated_at") 12 - .defaultNow() 13 - .$onUpdate(() => /* @__PURE__ */ new Date()) 14 - .notNull(), 15 - isAnonymous: boolean("is_anonymous").default(false), 16 - }); 17 - 18 - export const session = pgTable( 19 - "session", 20 - { 21 - id: text("id").primaryKey(), 22 - expiresAt: timestamp("expires_at").notNull(), 23 - token: text("token").notNull().unique(), 24 - createdAt: timestamp("created_at").defaultNow().notNull(), 25 - updatedAt: timestamp("updated_at") 26 - .$onUpdate(() => /* @__PURE__ */ new Date()) 27 - .notNull(), 28 - ipAddress: text("ip_address"), 29 - userAgent: text("user_agent"), 30 - userId: text("user_id") 31 - .notNull() 32 - .references(() => user.id, { onDelete: "cascade" }), 33 - }, 34 - (table) => [index("session_userId_idx").on(table.userId)], 35 - ); 36 - 37 - export const account = pgTable( 38 - "account", 39 - { 40 - id: text("id").primaryKey(), 41 - accountId: text("account_id").notNull(), 42 - providerId: text("provider_id").notNull(), 43 - userId: text("user_id") 44 - .notNull() 45 - .references(() => user.id, { onDelete: "cascade" }), 46 - accessToken: text("access_token"), 47 - refreshToken: text("refresh_token"), 48 - idToken: text("id_token"), 49 - accessTokenExpiresAt: timestamp("access_token_expires_at"), 50 - refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), 51 - scope: text("scope"), 52 - password: text("password"), 53 - createdAt: timestamp("created_at").defaultNow().notNull(), 54 - updatedAt: timestamp("updated_at") 55 - .$onUpdate(() => /* @__PURE__ */ new Date()) 56 - .notNull(), 57 - }, 58 - (table) => [index("account_userId_idx").on(table.userId)], 59 - ); 60 - 61 - export const verification = pgTable( 62 - "verification", 63 - { 64 - id: text("id").primaryKey(), 65 - identifier: text("identifier").notNull(), 66 - value: text("value").notNull(), 67 - expiresAt: timestamp("expires_at").notNull(), 68 - createdAt: timestamp("created_at").defaultNow().notNull(), 69 - updatedAt: timestamp("updated_at") 70 - .defaultNow() 71 - .$onUpdate(() => /* @__PURE__ */ new Date()) 72 - .notNull(), 73 - }, 74 - (table) => [index("verification_identifier_idx").on(table.identifier)], 75 - ); 76 - 77 - export const userRelations = relations(user, ({ many }) => ({ 78 - sessions: many(session), 79 - accounts: many(account), 80 - })); 81 - 82 - export const sessionRelations = relations(session, ({ one }) => ({ 83 - user: one(user, { 84 - fields: [session.userId], 85 - references: [user.id], 86 - }), 87 - })); 88 - 89 - export const accountRelations = relations(account, ({ one }) => ({ 90 - user: one(user, { 91 - fields: [account.userId], 92 - references: [user.id], 93 - }), 94 - }));
+5
biome.json
··· 33 33 "organizeImports": "on" 34 34 } 35 35 } 36 + }, 37 + "css": { 38 + "parser": { 39 + "tailwindDirectives": true 40 + } 36 41 } 37 42 }
+2 -1
package.json
··· 21 21 "drizzle-orm": "^0.45.1", 22 22 "next": "16.1.6", 23 23 "next-themes": "^0.4.6", 24 + "nodemailer": "^8.0.1", 24 25 "react": "19.2.4", 25 26 "react-dom": "19.2.4", 26 27 "shadcn": "^3.8.4", ··· 39 40 "tsx": "^4.21.0", 40 41 "typescript": "^5.9.3" 41 42 } 42 - } 43 + }
+9
pnpm-lock.yaml
··· 41 41 next-themes: 42 42 specifier: ^0.4.6 43 43 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 44 + nodemailer: 45 + specifier: ^8.0.1 46 + version: 8.0.1 44 47 react: 45 48 specifier: 19.2.4 46 49 version: 19.2.4 ··· 2270 2273 2271 2274 node-releases@2.0.27: 2272 2275 resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} 2276 + 2277 + nodemailer@8.0.1: 2278 + resolution: {integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==} 2279 + engines: {node: '>=6.0.0'} 2273 2280 2274 2281 npm-run-path@4.0.1: 2275 2282 resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} ··· 4588 4595 formdata-polyfill: 4.0.10 4589 4596 4590 4597 node-releases@2.0.27: {} 4598 + 4599 + nodemailer@8.0.1: {} 4591 4600 4592 4601 npm-run-path@4.0.1: 4593 4602 dependencies:
+2
pnpm-workspace.yaml
··· 1 1 ignoredBuiltDependencies: 2 2 - sharp 3 3 - unrs-resolver 4 + - esbuild 5 + - msw
+70 -15
src/app/auth/page.tsx
··· 1 - import { 2 - AppleIcon, 3 - GoogleIcon, 4 - TicketStarIcon, 5 - } from "@hugeicons/core-free-icons"; 1 + "use client"; 2 + 3 + import { TicketStarIcon } from "@hugeicons/core-free-icons"; 6 4 import { HugeiconsIcon } from "@hugeicons/react"; 7 5 import { Button } from "@/components/ui/button"; 8 6 import { ··· 10 8 FieldDescription, 11 9 FieldGroup, 12 10 FieldLabel, 13 - FieldSeparator, 14 11 } from "@/components/ui/field"; 15 12 import { Input } from "@/components/ui/input"; 13 + import { useRef, useState } from "react"; 14 + import { authClient } from "@/lib/auth-client"; 15 + import { 16 + Dialog, 17 + DialogContent, 18 + DialogDescription, 19 + DialogHeader, 20 + DialogTitle, 21 + } from "@/components/ui/dialog"; 22 + import { Spinner } from "@/components/ui/spinner"; 23 + 24 + type ModalState = { 25 + title: string; 26 + message: string; 27 + } | null; 16 28 17 29 export default function Auth() { 30 + const email = useRef<HTMLInputElement>(null); 31 + const [isLoading, setIsLoading] = useState(false); 32 + const [modalContent, setModalContent] = useState<ModalState>(null); 33 + 34 + const handleLogin = async (e: React.FormEvent) => { 35 + e.preventDefault(); 36 + if (!email.current) return; 37 + setIsLoading(true); 38 + 39 + const { data, error } = await authClient.signIn.magicLink({ 40 + email: email.current?.value || "", 41 + callbackURL: "/", 42 + }); 43 + 44 + email.current.value = ""; 45 + setIsLoading(false); 46 + 47 + if (error) 48 + setModalContent({ 49 + title: "Connection Failed", 50 + message: error.message || "An error occurred while sending the link.", 51 + }); 52 + else 53 + setModalContent({ 54 + title: "Check your email", 55 + message: "We've sent you a magic link to sign in.", 56 + }); 57 + }; 58 + 18 59 return ( 19 60 <div className="flex flex-col h-screen justify-center gap-6"> 20 - <form> 61 + <form onSubmit={handleLogin}> 21 62 <FieldGroup> 22 63 <div className="flex flex-col items-center gap-2"> 23 64 <HugeiconsIcon icon={TicketStarIcon} className="size-6" /> 24 65 <h1 className="text-xl font-bold">Welcome to Vouch</h1> 25 - <FieldDescription> 26 - Sign in to continue 27 - </FieldDescription> 66 + <FieldDescription>Sign in to continue</FieldDescription> 28 67 </div> 29 68 <Field> 30 69 <FieldLabel htmlFor="email">Email</FieldLabel> 31 70 <Input 32 71 id="email" 33 72 type="email" 73 + ref={email} 34 74 placeholder="alexandre@hallaine.com" 35 75 required 36 76 /> 37 77 </Field> 38 78 <Field> 39 - <Button type="submit">Login</Button> 79 + <Button type="submit" disabled={isLoading}> 80 + {isLoading && <Spinner />} 81 + Login 82 + </Button> 40 83 </Field> 41 - <FieldSeparator>Or</FieldSeparator> 84 + {/* <FieldSeparator>Or</FieldSeparator> 42 85 <Field className="grid gap-4 sm:grid-cols-2"> 43 86 <Button variant="outline" type="button"> 44 87 <HugeiconsIcon icon={AppleIcon} className="size-6" /> ··· 48 91 <HugeiconsIcon icon={GoogleIcon} className="size-6" /> 49 92 Continue with Google 50 93 </Button> 51 - </Field> 94 + </Field> */} 52 95 </FieldGroup> 53 96 </form> 54 - <FieldDescription className="px-6 text-center"> 97 + {/* <FieldDescription className="px-6 text-center"> 55 98 By clicking continue, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>. 56 - </FieldDescription> 99 + </FieldDescription> */} 100 + 101 + <Dialog 102 + open={!!modalContent} 103 + onOpenChange={(open) => !open && setModalContent(null)} 104 + > 105 + <DialogContent> 106 + <DialogHeader> 107 + <DialogTitle>{modalContent?.title}</DialogTitle> 108 + <DialogDescription>{modalContent?.message}</DialogDescription> 109 + </DialogHeader> 110 + </DialogContent> 111 + </Dialog> 57 112 </div> 58 113 ); 59 114 }
+6 -2
src/app/layout.tsx
··· 1 1 import type { Metadata } from "next"; 2 2 import "./globals.css"; 3 3 import { Figtree } from "next/font/google"; 4 - import { ThemeProvider } from "@/components/theme-provider" 4 + import { ThemeProvider } from "@/components/theme-provider"; 5 5 6 6 const figtree = Figtree({ subsets: ["latin"], variable: "--font-sans" }); 7 7 ··· 16 16 children: React.ReactNode; 17 17 }>) { 18 18 return ( 19 - <html lang="en" className={`${figtree.variable} max-w-md mx-auto`} suppressHydrationWarning> 19 + <html 20 + lang="en" 21 + className={`${figtree.variable} max-w-md mx-auto`} 22 + suppressHydrationWarning 23 + > 20 24 <body> 21 25 <ThemeProvider 22 26 attribute="class"
+11 -3
src/app/page.tsx
··· 1 - "use client"; 2 - 3 1 import { 4 2 Card, 5 3 CardAction, ··· 29 27 } from "@/components/ui/dialog"; 30 28 import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 31 29 import { Input } from "@/components/ui/input"; 30 + import { auth } from "@/lib/auth"; 31 + import { headers } from "next/headers"; 32 + import { redirect } from "next/navigation"; 32 33 33 - export default function Page() { 34 + export default async function Groups() { 35 + const session = await auth.api.getSession({ 36 + headers: await headers() 37 + }) 38 + 39 + if (!session) 40 + redirect("/auth") 41 + 34 42 const groups = [ 35 43 { 36 44 id: 1,
+11
src/components/ui/spinner.tsx
··· 1 + import { cn } from "@/lib/utils" 2 + import { HugeiconsIcon } from "@hugeicons/react" 3 + import { Loading03Icon } from "@hugeicons/core-free-icons" 4 + 5 + function Spinner({ className, ...props }: React.ComponentProps<"svg">) { 6 + return ( 7 + <HugeiconsIcon icon={Loading03Icon} strokeWidth={2} role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} /> 8 + ) 9 + } 10 + 11 + export { Spinner }
-1
src/db/auth-schema.ts
··· 12 12 .defaultNow() 13 13 .$onUpdate(() => /* @__PURE__ */ new Date()) 14 14 .notNull(), 15 - isAnonymous: boolean("is_anonymous").default(false), 16 15 }); 17 16 18 17 export const session = pgTable(
+2 -2
src/lib/auth-client.ts
··· 1 - import { anonymousClient } from "better-auth/client/plugins"; 1 + import { magicLinkClient } from "better-auth/client/plugins"; 2 2 import { createAuthClient } from "better-auth/react"; 3 3 4 4 export const authClient = createAuthClient({ 5 5 baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL, 6 - plugins: [anonymousClient()], 6 + plugins: [magicLinkClient()], 7 7 });
+18 -2
src/lib/auth.ts
··· 1 1 import { betterAuth } from "better-auth"; 2 2 import { drizzleAdapter } from "better-auth/adapters/drizzle"; 3 - import { anonymous } from "better-auth/plugins"; 3 + import { magicLink } from "better-auth/plugins"; 4 4 5 5 import * as schema from "@/db/schema"; 6 6 import { db } from "@/lib/db"; 7 + import { sendEmail } from "@/lib/email"; 7 8 8 9 export const auth = betterAuth({ 9 10 database: drizzleAdapter(db, { 10 11 provider: "pg", 11 12 schema, 12 13 }), 13 - plugins: [anonymous()], 14 + plugins: [ 15 + magicLink({ 16 + sendMagicLink: async ({ email, token, url }, request) => { 17 + await sendEmail({ 18 + to: email, 19 + subject: "Sign in to Vouch", 20 + html: ` 21 + <h1>Welcome to Vouch</h1> 22 + <p>Click the link below to sign in:</p> 23 + <a href="${url}">Sign in now</a> 24 + <p>This link expires in 5 minutes.</p> 25 + `, 26 + }); 27 + }, 28 + }), 29 + ], 14 30 });
+33
src/lib/email.ts
··· 1 + import nodemailer from "nodemailer"; 2 + 3 + interface SendEmailParams { 4 + to: string; 5 + subject: string; 6 + html: string; 7 + } 8 + 9 + const transporter = nodemailer.createTransport({ 10 + host: process.env.SMTP_HOST, 11 + port: Number(process.env.SMTP_PORT), 12 + secure: true, 13 + auth: { 14 + user: process.env.SMTP_USER, 15 + pass: process.env.SMTP_PASS, 16 + }, 17 + }); 18 + 19 + export async function sendEmail({ to, subject, html }: SendEmailParams) { 20 + try { 21 + const info = await transporter.sendMail({ 22 + from: process.env.SMTP_FROM, 23 + to, 24 + subject, 25 + html, 26 + }); 27 + console.log("Message sent: %s", info.messageId); 28 + return { success: true, messageId: info.messageId }; 29 + } catch (error) { 30 + console.error("Error sending email:", error); 31 + return { success: false, error }; 32 + } 33 + }