example restaurant review app on atproto
at main 4.9 kB view raw
1import { OAuthResolverError } from "@atproto/oauth-client-node"; 2import { serve } from "@hono/node-server"; 3import { Hono } from "hono"; 4import { generateSignedCookie, getSignedCookie } from "hono/cookie"; 5import { client } from "./atproto/client"; 6import { resolveMiniDoc } from "./utils"; 7import "dotenv/config"; 8import { jetstream } from "./ingester"; 9import { Agent } from "@atproto/api"; 10import { TID } from "@atproto/common"; 11import { AppFooodzReview } from "./lexicons"; 12import { is } from "@atcute/lexicons"; 13import { Layout } from "./components"; 14import { db } from "./drizzle/db"; 15import { users } from "./drizzle/schema"; 16import { eq } from "drizzle-orm"; 17 18const app = new Hono(); 19 20app.get("/client-metadata.json", (c) => { 21 return c.json(client.clientMetadata); 22}); 23 24app.get("/oauth/callback", async (c) => { 25 const params = new URLSearchParams(c.req.url.split("?")[1]); 26 27 try { 28 const { session } = await client.callback(params); 29 30 // check if user is already added to db 31 const user = await db.query.users.findFirst({ 32 where: eq(users.did, session.did), 33 }); 34 35 if (!user) { 36 console.log("user not found in db, inserting"); 37 await db.insert(users).values({ 38 did: session.did, 39 }); 40 } 41 42 const cookie = await generateSignedCookie( 43 "__foooodz_session", 44 session.did, 45 process.env.COOKIE_SECRET as string, 46 { 47 path: "/", 48 httpOnly: true, 49 sameSite: "lax", 50 maxAge: 60 * 60 * 24 * 7, 51 secure: process.env.NODE_ENV === "production", 52 }, 53 ); 54 return new Response(null, { 55 status: 302, 56 headers: { 57 "Set-Cookie": cookie, 58 Location: "/", 59 }, 60 }); 61 } catch (error) { 62 if (error instanceof Error) { 63 throw error; 64 } 65 } 66}); 67 68app.post("/login", async (c) => { 69 try { 70 const formData = await c.req.formData(); 71 const handle = formData.get("handle"); 72 if (!handle || typeof handle !== "string") { 73 return c.json( 74 { 75 ok: false, 76 message: "Invalid handle", 77 }, 78 400, 79 ); 80 } 81 82 const { did } = await resolveMiniDoc(handle); 83 84 const ac = new AbortController(); 85 const redirectUrl = await client.authorize(did, { 86 signal: ac.signal, 87 }); 88 return c.redirect(redirectUrl.toString()); 89 } catch (error) { 90 if (error instanceof OAuthResolverError) { 91 return c.json({ ok: false, message: error.message }, 500); 92 } 93 console.log(error); 94 return c.json({ ok: false, message: "something went wrong" }, 500); 95 } 96}); 97 98app.get("/", async (c) => { 99 return c.html( 100 <Layout> 101 <form method="post" action="/login"> 102 <label htmlFor="handle">Handle</label> 103 <input type="text" name="handle" id="handle" /> 104 <button type="submit">Login</button> 105 </form> 106 </Layout>, 107 ); 108}); 109 110app.get("/review", async (c) => { 111 const reviews = await db.query.reviews.findMany({ 112 limit: 10, 113 }); 114 return c.html( 115 <Layout> 116 <form method="post" action="/review"> 117 <label htmlFor="place">place</label> 118 <input type="text" name="place" id="place" maxlength={200} required /> 119 <label htmlFor="review">review</label> 120 <textarea name="review" id="review" maxlength={3000} required /> 121 <label htmlFor="rating">rating</label> 122 <select name="rating" id="rating" required> 123 {Array.from({ length: 5 }, (_, index) => ( 124 <option key={index} value={index + 1}> 125 {index + 1} 126 </option> 127 ))} 128 </select> 129 <button type="submit">submit review</button> 130 </form> 131 <ul> 132 {reviews.map((review) => ( 133 <li key={review.rkey}> 134 <article> 135 <h2>{review.place}</h2> 136 <p>{review.review}</p> 137 <span>{review.rating}</span> 138 <time datetime={review.createdAt}>{review.createdAt}</time> 139 </article> 140 </li> 141 ))} 142 </ul> 143 </Layout>, 144 ); 145}); 146 147app.post("/review", async (c) => { 148 const cookie = await getSignedCookie(c, process.env.COOKIE_SECRET as string); 149 if (cookie.__foooodz_session) { 150 try { 151 const oauthSession = await client.restore(cookie.__foooodz_session); 152 const agent = new Agent(oauthSession); 153 const formData = await c.req.formData(); 154 const place = formData.get("place") as string; 155 const review = formData.get("review") as string; 156 const rating = formData.get("rating") as string; 157 158 const rkey = TID.nextStr(); 159 160 const record: AppFooodzReview.Main = { 161 $type: "app.fooodz.review", 162 place, 163 rating: +rating, 164 review, 165 createdAt: new Date().toISOString(), 166 }; 167 168 if (!is(AppFooodzReview.mainSchema, record)) { 169 return c.json({ ok: false, message: "invalid review record" }, 400); 170 } 171 172 await agent.com.atproto.repo.putRecord({ 173 repo: agent.assertDid, 174 rkey, 175 record, 176 collection: "app.fooodz.review", 177 validate: false, 178 }); 179 180 return c.redirect("/review"); 181 } catch (error) { 182 return c.json({ ok: false, message: "failed to write record" }, 500); 183 } 184 } 185}); 186 187serve( 188 { 189 fetch: app.fetch, 190 port: 3000, 191 }, 192 (info) => { 193 console.log(`Server is running @ http://localhost:${info.port}`); 194 jetstream.start(); 195 }, 196);