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