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}