Public
HTTP
stevekrouse-spotify.web.val.run
January 12, 2024
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { generateRandomString } from "https://esm.town/v/greg0r/generateRandomString?v=1";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { home, scopesRoute } from "https://esm.town/v/stevekrouse/spotify_helpers";
import { spotifyRefreshToken } from "https://esm.town/v/stevekrouse/spotifyRefreshToken";
import { spotifyRequestToken } from "https://esm.town/v/stevekrouse/spotifyRequestToken";
import { thisWebURL } from "https://esm.town/v/stevekrouse/thisWebURL";
import { eq, sql } from "npm:drizzle-orm";
import { drizzle } from "npm:drizzle-orm/libsql";
import { integer, sqliteTable, text } from "npm:drizzle-orm/sqlite-core";
import { getCookie, setCookie } from "npm:hono/cookie";
import { Hono } from "npm:hono@3";
// await sqlite.execute("CREATE TABLE spot (id text primary key, data text)")
export const db = drizzle(sqlite as any);
export const table = sqliteTable("SPOTIFY_AUTH", {
id: text("id").primaryKey(),
data: text("data"),
});
const thisURL = thisWebURL();
const redirect_uri = thisURL + "/callback";
const app = new Hono();
app.use("*", async (c, next) => {
const state = getCookie(c, "state") ?? generateRandomString(16);
c.set("state", state);
await next();
setCookie(c, "state", state);
});
app.get("/", home);
app.get("/scopes", scopesRoute);
app.get("/login", async (c) => {
const state = c.get("state");
const scopes = c.req.queries("scopes[]").join(" ");
await db.insert(table).values([{ id: state }]);
return c.redirect(
`https://accounts.spotify.com/authorize?response_type=code&client_id=${
encodeURIComponent(Deno.env.get("SPOTIFY_CLIENT_ID"))
}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(redirect_uri)}&state=${state}`,
);
});
app.get("/callback", async (c) => {
const code = c.req.query("code");
const state = c.req.query("state") as string;
if (state !== c.get("state")) {
return c.html("State mismatch 1");
}
const rowInitialResult = await db.select().from(table).where(eq(table.id, state)).limit(1);
const rowInitial = rowInitialResult[0];
if (!rowInitial) {
return c.html("State mismatch 2");
}
if (rowInitial.data) {
return c.html("Already authenticated for that state");
}
const data = await spotifyRequestToken({
code,
client_id: Deno.env.get("SPOTIFY_CLIENT_ID"),
client_secret: Deno.env.get("SPOTIFY_CLIENT_SECRET"),
redirect_uri,
});
data.now = Date.now(); // to help us calculate when to refresh
if (data.error) return c.json(data.error);
if (!data.access_token) return c.json("No access token");
await db.update(table).set({ data: JSON.stringify(data) }).where(eq(table.id, state));
return c.redirect("/token?state=" + state);
});
app.get("/token", async (c) => {
const state = c.req.query("state") as string;
const authData = await db.select().from(table).where(eq(table.id, state)).limit(1);
if (!authData.length) return c.json("No auth data found for supplied state");
const data = JSON.parse(authData[0].data);
let expiresAt = data.now + (data.expires_in * 1000);
if (expiresAt < Date.now()) {
let refresh = await spotifyRefreshToken({
refresh_token: data.refresh_token,
client_id: Deno.env.get("SPOTIFY_CLIENT_ID"),
client_secret: Deno.env.get("SPOTIFY_CLIENT_SECRET"),
});
if (refresh.error) return c.json("Refresh error: " + refresh.error);
if (!refresh.access_token) return c.json("No refresh access token");
refresh.now = Date.now();
await db.update(table).set({ data: JSON.stringify(refresh) }).where(eq(table.id, state));
}
return c.json(data.access_token);
});
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Comments
Nobody has commented on this val yet: be the first!
January 12, 2024