forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
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;