a tool for shared writing and social publishing
at update/reader 145 lines 3.6 kB view raw
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 { setAuthToken } from "src/auth"; 10import { pool } from "supabase/pool"; 11import { supabaseServerClient } from "supabase/serverClient"; 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 const { data: newIdentity } = await supabaseServerClient 118 .from("identities") 119 .insert({ email: token.email }) 120 .select() 121 .single(); 122 identityID = newIdentity!.id; 123 } else { 124 identityID = identity.id; 125 } 126 127 const [confirmedToken] = await db 128 .update(email_auth_tokens) 129 .set({ 130 confirmed: true, 131 identity: identityID, 132 }) 133 .where( 134 and( 135 eq(email_auth_tokens.id, tokenId), 136 eq(email_auth_tokens.confirmation_code, code), 137 ), 138 ) 139 .returning(); 140 141 await setAuthToken(confirmedToken.id); 142 143 client.release(); 144 return confirmedToken; 145}