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