A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fff48ea3213bb11efcfcb7db85be1dfcd2bebc5e 393 lines 9.2 kB view raw
1import axios from "axios"; 2import { consola } from "consola"; 3import { ctx } from "context"; 4import { eq } from "drizzle-orm"; 5import { Hono } from "hono"; 6import jwt from "jsonwebtoken"; 7import { encrypt } from "lib/crypto"; 8import { env } from "lib/env"; 9import { requestCounter } from "metrics"; 10import tables from "schema"; 11import { emailSchema } from "types/email"; 12 13const app = new Hono(); 14 15app.get("/login", async (c) => { 16 requestCounter.add(1, { method: "GET", route: "/dropbox/login" }); 17 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 18 19 if (!bearer || bearer === "null") { 20 c.status(401); 21 return c.text("Unauthorized"); 22 } 23 24 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 25 ignoreExpiration: true, 26 }); 27 28 const user = await ctx.db 29 .select() 30 .from(tables.users) 31 .where(eq(tables.users.did, did)) 32 .limit(1) 33 .execute() 34 .then((res) => res[0]); 35 36 if (!user) { 37 c.status(401); 38 return c.text("Unauthorized"); 39 } 40 41 const clientId = env.DROPBOX_CLIENT_ID; 42 const redirectUri = `https://www.dropbox.com/oauth2/authorize?client_id=${clientId}&redirect_uri=${env.DROPBOX_REDIRECT_URI}&response_type=code&token_access_type=offline&state=${user.id}`; 43 return c.json({ redirectUri }); 44}); 45 46app.get("/oauth/callback", async (c) => { 47 requestCounter.add(1, { method: "GET", route: "/dropbox/oauth/callback" }); 48 const params = new URLSearchParams(c.req.url.split("?")[1]); 49 const entries = Object.fromEntries(params.entries()); 50 // entries.code 51 const response = await axios.postForm( 52 "https://api.dropboxapi.com/oauth2/token", 53 { 54 code: entries.code, 55 grant_type: "authorization_code", 56 client_id: env.DROPBOX_CLIENT_ID, 57 client_secret: env.DROPBOX_CLIENT_SECRET, 58 redirect_uri: env.DROPBOX_REDIRECT_URI, 59 }, 60 ); 61 62 const { dropbox, dropbox_tokens } = await ctx.db 63 .select() 64 .from(tables.dropbox) 65 .where(eq(tables.dropbox.userId, entries.state)) 66 .leftJoin( 67 tables.dropboxTokens, 68 eq(tables.dropboxTokens.id, tables.dropbox.dropboxTokenId), 69 ) 70 .limit(1) 71 .execute() 72 .then((res) => res[0]); 73 74 const newDropboxToken = await ctx.db 75 .insert(tables.dropboxTokens) 76 .values({ 77 id: dropbox_tokens?.id, 78 refreshToken: encrypt( 79 response.data.refresh_token, 80 env.SPOTIFY_ENCRYPTION_KEY, 81 ), 82 }) 83 .onConflictDoUpdate({ 84 target: tables.dropboxTokens.id, // specify the conflict column (primary key) 85 set: { 86 refreshToken: encrypt( 87 response.data.refresh_token, 88 env.SPOTIFY_ENCRYPTION_KEY, 89 ), 90 }, 91 }) 92 .returning() 93 .execute() 94 .then((res) => res[0]); 95 96 await ctx.db 97 .insert(tables.dropbox) 98 .values({ 99 id: dropbox?.id, 100 dropboxTokenId: newDropboxToken.id, 101 userId: entries.state, 102 }) 103 .onConflictDoUpdate({ 104 target: tables.dropbox.id, 105 set: { 106 dropboxTokenId: newDropboxToken.id, 107 userId: entries.state, 108 }, 109 }) 110 .execute(); 111 112 return c.redirect(`${env.FRONTEND_URL}/dropbox`); 113}); 114 115app.post("/join", async (c) => { 116 requestCounter.add(1, { method: "POST", route: "/dropbox/join" }); 117 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 118 119 if (!bearer || bearer === "null") { 120 c.status(401); 121 return c.text("Unauthorized"); 122 } 123 124 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 125 ignoreExpiration: true, 126 }); 127 128 const user = await ctx.db 129 .select() 130 .from(tables.users) 131 .where(eq(tables.users.did, did)) 132 .limit(1) 133 .execute() 134 .then((res) => res[0]); 135 if (!user) { 136 c.status(401); 137 return c.text("Unauthorized"); 138 } 139 140 const body = await c.req.json(); 141 const parsed = emailSchema.safeParse(body); 142 143 if (parsed.error) { 144 c.status(400); 145 return c.text("Invalid email: " + parsed.error.message); 146 } 147 148 const { email } = parsed.data; 149 150 try { 151 await ctx.db 152 .insert(tables.dropboxAccounts) 153 .values({ 154 userId: user.id, 155 email, 156 isBetaUser: false, 157 }) 158 .execute(); 159 } catch (e) { 160 if ( 161 !e.message.includes("invalid record: column [user_id]: is not unique") 162 ) { 163 consola.error(e.message); 164 } else { 165 throw e; 166 } 167 } 168 169 await fetch("https://beta.rocksky.app", { 170 method: "POST", 171 headers: { 172 "Content-Type": "application/json", 173 Authorization: `Bearer ${env.ROCKSKY_BETA_TOKEN}`, 174 }, 175 body: JSON.stringify({ email }), 176 }); 177 178 return c.json({ status: "ok" }); 179}); 180 181app.get("/files", async (c) => { 182 requestCounter.add(1, { method: "GET", route: "/dropbox/files" }); 183 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 184 185 if (!bearer || bearer === "null") { 186 c.status(401); 187 return c.text("Unauthorized"); 188 } 189 190 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 191 ignoreExpiration: true, 192 }); 193 194 const [user] = await ctx.db 195 .select() 196 .from(tables.users) 197 .where(eq(tables.users.did, did)) 198 .limit(1) 199 .execute(); 200 if (!user) { 201 c.status(401); 202 return c.text("Unauthorized"); 203 } 204 205 const path = c.req.query("path"); 206 207 if (!path) { 208 try { 209 const { data } = await ctx.dropbox.post("dropbox.getFiles", { 210 did, 211 }); 212 213 return c.json(data); 214 } catch { 215 await ctx.dropbox.post("dropbox.createMusicFolder", { 216 did, 217 }); 218 const response = await ctx.dropbox.post("dropbox.getFiles", { 219 did, 220 }); 221 return c.json(response.data); 222 } 223 } 224 225 const { data } = await ctx.dropbox.post("dropbox.getFilesAt", { 226 did, 227 path, 228 }); 229 230 return c.json(data); 231}); 232 233app.get("/temporary-link", async (c) => { 234 requestCounter.add(1, { method: "GET", route: "/dropbox/temporary-link" }); 235 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 236 237 if (!bearer || bearer === "null") { 238 c.status(401); 239 return c.text("Unauthorized"); 240 } 241 242 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 243 ignoreExpiration: true, 244 }); 245 246 const [user] = await ctx.db 247 .select() 248 .from(tables.users) 249 .where(eq(tables.users.did, did)) 250 .limit(1) 251 .execute(); 252 if (!user) { 253 c.status(401); 254 return c.text("Unauthorized"); 255 } 256 257 const path = c.req.query("path"); 258 if (!path) { 259 c.status(400); 260 return c.text("Bad Request, path is required"); 261 } 262 263 const { data } = await ctx.dropbox.post("dropbox.getTemporaryLink", { 264 did, 265 path, 266 }); 267 268 return c.json(data); 269}); 270 271app.get("/files/:id", async (c) => { 272 requestCounter.add(1, { method: "GET", route: "/dropbox/files/:id" }); 273 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 274 275 if (!bearer || bearer === "null") { 276 c.status(401); 277 return c.text("Unauthorized"); 278 } 279 280 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 281 ignoreExpiration: true, 282 }); 283 284 const [user] = await ctx.db 285 .select() 286 .from(tables.users) 287 .where(eq(tables.users.did, did)) 288 .limit(1) 289 .execute(); 290 if (!user) { 291 c.status(401); 292 return c.text("Unauthorized"); 293 } 294 295 const path = c.req.param("id"); 296 297 const response = await ctx.dropbox.post("dropbox.getMetadata", { 298 did, 299 path, 300 }); 301 302 return c.json(response.data); 303}); 304 305app.get("/file", async (c) => { 306 requestCounter.add(1, { method: "GET", route: "/dropbox/file" }); 307 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 308 309 if (!bearer || bearer === "null") { 310 c.status(401); 311 return c.text("Unauthorized"); 312 } 313 314 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 315 ignoreExpiration: true, 316 }); 317 318 const [user] = await ctx.db 319 .select() 320 .from(tables.users) 321 .where(eq(tables.users.did, did)) 322 .limit(1) 323 .execute(); 324 if (!user) { 325 c.status(401); 326 return c.text("Unauthorized"); 327 } 328 329 const path = c.req.query("path"); 330 331 if (!path) { 332 c.status(400); 333 return c.text("Bad Request, path is required"); 334 } 335 336 const response = await ctx.dropbox.post("dropbox.getMetadata", { 337 did, 338 path, 339 }); 340 341 return c.json(response.data); 342}); 343 344app.get("/download", async (c) => { 345 requestCounter.add(1, { method: "GET", route: "/dropbox/download" }); 346 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 347 348 if (!bearer || bearer === "null") { 349 c.status(401); 350 return c.text("Unauthorized"); 351 } 352 353 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 354 ignoreExpiration: true, 355 }); 356 357 const [user] = await ctx.db 358 .select() 359 .from(tables.users) 360 .where(eq(tables.users.did, did)) 361 .limit(1) 362 .execute(); 363 if (!user) { 364 c.status(401); 365 return c.text("Unauthorized"); 366 } 367 368 const path = c.req.query("path"); 369 if (!path) { 370 c.status(400); 371 return c.text("Bad Request, path is required"); 372 } 373 374 const response = await ctx.dropbox.post("dropbox.downloadFile", { 375 did, 376 path, 377 }); 378 379 c.header( 380 "Content-Type", 381 response.headers["content-type"] || "application/octet-stream", 382 ); 383 c.header( 384 "Content-Disposition", 385 response.headers["content-disposition"] || "attachment", 386 ); 387 388 return new Response(response.data, { 389 headers: c.res.headers, 390 }); 391}); 392 393export default app;