A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 320 lines 8.0 kB view raw
1import { equals } from "@xata.io/client"; 2import axios from "axios"; 3import { ctx } from "context"; 4import { Hono } from "hono"; 5import jwt from "jsonwebtoken"; 6import { encrypt } from "lib/crypto"; 7import { env } from "lib/env"; 8import { requestCounter } from "metrics"; 9import { emailSchema } from "types/email"; 10 11const app = new Hono(); 12 13app.get("/login", async (c) => { 14 requestCounter.add(1, { method: "GET", route: "/dropbox/login" }); 15 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 16 17 if (!bearer || bearer === "null") { 18 c.status(401); 19 return c.text("Unauthorized"); 20 } 21 22 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 23 ignoreExpiration: true, 24 }); 25 26 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 27 if (!user) { 28 c.status(401); 29 return c.text("Unauthorized"); 30 } 31 32 const clientId = env.DROPBOX_CLIENT_ID; 33 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.xata_id}`; 34 return c.json({ redirectUri }); 35}); 36 37app.get("/oauth/callback", async (c) => { 38 requestCounter.add(1, { method: "GET", route: "/dropbox/oauth/callback" }); 39 const params = new URLSearchParams(c.req.url.split("?")[1]); 40 const entries = Object.fromEntries(params.entries()); 41 // entries.code 42 const response = await axios.postForm( 43 "https://api.dropboxapi.com/oauth2/token", 44 { 45 code: entries.code, 46 grant_type: "authorization_code", 47 client_id: env.DROPBOX_CLIENT_ID, 48 client_secret: env.DROPBOX_CLIENT_SECRET, 49 redirect_uri: env.DROPBOX_REDIRECT_URI, 50 }, 51 ); 52 53 const dropbox = await ctx.client.db.dropbox 54 .select(["*", "user_id.*", "dropbox_token_id.*"]) 55 .filter("user_id.xata_id", equals(entries.state)) 56 .getFirst(); 57 58 const newDropboxToken = await ctx.client.db.dropbox_tokens.createOrUpdate( 59 dropbox?.dropbox_token_id?.xata_id, 60 { 61 refresh_token: encrypt( 62 response.data.refresh_token, 63 env.SPOTIFY_ENCRYPTION_KEY, 64 ), 65 }, 66 ); 67 68 await ctx.client.db.dropbox.createOrUpdate(dropbox?.xata_id, { 69 dropbox_token_id: newDropboxToken.xata_id, 70 user_id: entries.state, 71 }); 72 73 return c.redirect(`${env.FRONTEND_URL}/dropbox`); 74}); 75 76app.post("/join", async (c) => { 77 requestCounter.add(1, { method: "POST", route: "/dropbox/join" }); 78 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 79 80 if (!bearer || bearer === "null") { 81 c.status(401); 82 return c.text("Unauthorized"); 83 } 84 85 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 86 ignoreExpiration: true, 87 }); 88 89 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 90 if (!user) { 91 c.status(401); 92 return c.text("Unauthorized"); 93 } 94 95 const body = await c.req.json(); 96 const parsed = emailSchema.safeParse(body); 97 98 if (parsed.error) { 99 c.status(400); 100 return c.text("Invalid email: " + parsed.error.message); 101 } 102 103 const { email } = parsed.data; 104 105 try { 106 await ctx.client.db.dropbox_accounts.create({ 107 user_id: user.xata_id, 108 email, 109 is_beta_user: false, 110 }); 111 } catch (e) { 112 if ( 113 !e.message.includes("invalid record: column [user_id]: is not unique") 114 ) { 115 console.error(e.message); 116 } else { 117 throw e; 118 } 119 } 120 121 await fetch("https://beta.rocksky.app", { 122 method: "POST", 123 headers: { 124 "Content-Type": "application/json", 125 Authorization: `Bearer ${env.ROCKSKY_BETA_TOKEN}`, 126 }, 127 body: JSON.stringify({ email }), 128 }); 129 130 return c.json({ status: "ok" }); 131}); 132 133app.get("/files", async (c) => { 134 requestCounter.add(1, { method: "GET", route: "/dropbox/files" }); 135 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 136 137 if (!bearer || bearer === "null") { 138 c.status(401); 139 return c.text("Unauthorized"); 140 } 141 142 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 143 ignoreExpiration: true, 144 }); 145 146 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 147 if (!user) { 148 c.status(401); 149 return c.text("Unauthorized"); 150 } 151 152 const path = c.req.query("path"); 153 154 if (!path) { 155 try { 156 const { data } = await ctx.dropbox.post("dropbox.getFiles", { 157 did, 158 }); 159 160 return c.json(data); 161 } catch { 162 await ctx.dropbox.post("dropbox.createMusicFolder", { 163 did, 164 }); 165 const response = await ctx.dropbox.post("dropbox.getFiles", { 166 did, 167 }); 168 return c.json(response.data); 169 } 170 } 171 172 const { data } = await ctx.dropbox.post("dropbox.getFilesAt", { 173 did, 174 path, 175 }); 176 177 return c.json(data); 178}); 179 180app.get("/temporary-link", async (c) => { 181 requestCounter.add(1, { method: "GET", route: "/dropbox/temporary-link" }); 182 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 183 184 if (!bearer || bearer === "null") { 185 c.status(401); 186 return c.text("Unauthorized"); 187 } 188 189 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 190 ignoreExpiration: true, 191 }); 192 193 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 194 if (!user) { 195 c.status(401); 196 return c.text("Unauthorized"); 197 } 198 199 const path = c.req.query("path"); 200 if (!path) { 201 c.status(400); 202 return c.text("Bad Request, path is required"); 203 } 204 205 const { data } = await ctx.dropbox.post("dropbox.getTemporaryLink", { 206 did, 207 path, 208 }); 209 210 return c.json(data); 211}); 212 213app.get("/files/:id", async (c) => { 214 requestCounter.add(1, { method: "GET", route: "/dropbox/files/:id" }); 215 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 216 217 if (!bearer || bearer === "null") { 218 c.status(401); 219 return c.text("Unauthorized"); 220 } 221 222 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 223 ignoreExpiration: true, 224 }); 225 226 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 227 if (!user) { 228 c.status(401); 229 return c.text("Unauthorized"); 230 } 231 232 const path = c.req.param("id"); 233 234 const response = await ctx.dropbox.post("dropbox.getMetadata", { 235 did, 236 path, 237 }); 238 239 return c.json(response.data); 240}); 241 242app.get("/file", async (c) => { 243 requestCounter.add(1, { method: "GET", route: "/dropbox/file" }); 244 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 245 246 if (!bearer || bearer === "null") { 247 c.status(401); 248 return c.text("Unauthorized"); 249 } 250 251 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 252 ignoreExpiration: true, 253 }); 254 255 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 256 if (!user) { 257 c.status(401); 258 return c.text("Unauthorized"); 259 } 260 261 const path = c.req.query("path"); 262 263 if (!path) { 264 c.status(400); 265 return c.text("Bad Request, path is required"); 266 } 267 268 const response = await ctx.dropbox.post("dropbox.getMetadata", { 269 did, 270 path, 271 }); 272 273 return c.json(response.data); 274}); 275 276app.get("/download", async (c) => { 277 requestCounter.add(1, { method: "GET", route: "/dropbox/download" }); 278 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 279 280 if (!bearer || bearer === "null") { 281 c.status(401); 282 return c.text("Unauthorized"); 283 } 284 285 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 286 ignoreExpiration: true, 287 }); 288 289 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 290 if (!user) { 291 c.status(401); 292 return c.text("Unauthorized"); 293 } 294 295 const path = c.req.query("path"); 296 if (!path) { 297 c.status(400); 298 return c.text("Bad Request, path is required"); 299 } 300 301 const response = await ctx.dropbox.post("dropbox.downloadFile", { 302 did, 303 path, 304 }); 305 306 c.header( 307 "Content-Type", 308 response.headers["content-type"] || "application/octet-stream", 309 ); 310 c.header( 311 "Content-Disposition", 312 response.headers["content-disposition"] || "attachment", 313 ); 314 315 return new Response(response.data, { 316 headers: c.res.headers, 317 }); 318}); 319 320export default app;