a tool for shared writing and social publishing
1"use server"; 2 3import { randomBytes } from "crypto"; 4import { drizzle } from "drizzle-orm/node-postgres"; 5import postgres from "postgres"; 6import { email_auth_tokens, identities } from "drizzle/schema"; 7import { and, eq } from "drizzle-orm"; 8import { cookies } from "next/headers"; 9import { createIdentity } from "./createIdentity"; 10import { setAuthToken } from "src/auth"; 11import { pool } from "supabase/pool"; 12 13async function sendAuthCode(email: string, code: string) { 14 if (process.env.NODE_ENV === "development") { 15 console.log("Auth code:", code); 16 return; 17 } 18 19 let res = await fetch("https://api.postmarkapp.com/email", { 20 method: "POST", 21 headers: { 22 "Content-Type": "application/json", 23 "X-Postmark-Server-Token": process.env.POSTMARK_API_KEY!, 24 }, 25 body: JSON.stringify({ 26 From: "Leaflet <accounts@leaflet.pub>", 27 Subject: `Your authentication code for Leaflet is ${code}`, 28 To: email, 29 TextBody: `Paste this code to login to Leaflet: 30 31${code} 32 `, 33 HtmlBody: ` 34 <html> 35 <body> 36 <p>Paste this code to login to Leaflet: <strong>${code}</strong></p> 37 </body> 38 </html> 39 `, 40 }), 41 }); 42} 43 44export async function requestAuthEmailToken(emailNonNormalized: string) { 45 let email = emailNonNormalized.toLowerCase(); 46 const client = await pool.connect(); 47 const db = drizzle(client); 48 49 const code = randomBytes(3).toString("hex").toUpperCase(); 50 51 const [token] = await db 52 .insert(email_auth_tokens) 53 .values({ 54 email, 55 confirmation_code: code, 56 confirmed: false, 57 }) 58 .returning({ 59 id: email_auth_tokens.id, 60 }); 61 62 await sendAuthCode(email, code); 63 64 client.release(); 65 return token.id; 66} 67 68export async function confirmEmailAuthToken(tokenId: string, code: string) { 69 const client = await pool.connect(); 70 const db = drizzle(client); 71 72 const [token] = await db 73 .select() 74 .from(email_auth_tokens) 75 .where(eq(email_auth_tokens.id, tokenId)); 76 77 if (!token || !token.email) { 78 client.release(); 79 return null; 80 } 81 82 if (token.confirmation_code !== code) { 83 client.release(); 84 return null; 85 } 86 87 if (token.confirmed) { 88 client.release(); 89 return null; 90 } 91 let authToken = (await cookies()).get("auth_token"); 92 if (authToken) { 93 let [existingToken] = await db 94 .select() 95 .from(email_auth_tokens) 96 .rightJoin(identities, eq(identities.id, email_auth_tokens.identity)) 97 .where(eq(email_auth_tokens.id, authToken.value)); 98 99 if (existingToken) { 100 if (existingToken.identities?.email) { 101 } 102 await db 103 .update(identities) 104 .set({ email: token.email }) 105 .where(eq(identities.id, existingToken.identities.id)); 106 client.release(); 107 return existingToken; 108 } 109 } 110 111 let identityID; 112 let [identity] = await db 113 .select() 114 .from(identities) 115 .where(eq(identities.email, token.email)); 116 if (!identity) { 117 let newIdentity = await createIdentity(db, { email: token.email }); 118 identityID = newIdentity.id; 119 } else { 120 identityID = identity.id; 121 } 122 123 const [confirmedToken] = await db 124 .update(email_auth_tokens) 125 .set({ 126 confirmed: true, 127 identity: identityID, 128 }) 129 .where( 130 and( 131 eq(email_auth_tokens.id, tokenId), 132 eq(email_auth_tokens.confirmation_code, code), 133 ), 134 ) 135 .returning(); 136 137 await setAuthToken(confirmedToken.id); 138 139 client.release(); 140 return confirmedToken; 141}