+3
-2
Dockerfile
+3
-2
Dockerfile
···
10
10
ARG BUILD_DATE
11
11
ARG SHA
12
12
ARG VERSION
13
+
ARG ATPROTO_HANDLE
13
14
14
15
FROM base AS build
15
16
COPY --chown=1000:1000 apps/bot /app
···
36
37
EXPOSE 8002
37
38
CMD ["node", "dist/index.js"]
38
39
LABEL org.opencontainers.image.authors="Dane Miller 'me@dane.computer'" \
39
-
org.opencontainers.image.source="https://tangled.org/dane.is.extraordinarily.cool/tealfmbot" \
40
+
org.opencontainers.image.source="https://tangled.org/${ATPROTO_HANDLE}/tealfmbot" \
40
41
org.opencontainers.image.title="discostuweb" \
41
42
org.opencontainers.image.description="The web service for authentication for the disco stu discord bot" \
42
43
org.opencontainers.image.version=$VERSION \
···
50
51
WORKDIR /prod/bot
51
52
CMD ["node", "dist/main.js"]
52
53
LABEL org.opencontainers.image.authors="Dane Miller 'me@dane.computer'" \
53
-
org.opencontainers.image.source="https://tangled.org/dane.is.extraordinarily.cool/tealfmbot" \
54
+
org.opencontainers.image.source="https://tangled.org/${ATPROTO_HANDLE}/tealfmbot" \
54
55
org.opencontainers.image.title="discostubot" \
55
56
org.opencontainers.image.description="A discord bot that displays your music listens based on your teal.fm records" \
56
57
org.opencontainers.image.version=$VERSION \
+7
-2
apps/bot/commands/auth.ts
+7
-2
apps/bot/commands/auth.ts
···
2
2
import { logger } from "@tealfmbot/common/logger";
3
3
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
4
4
5
+
const LOGIN_URL =
6
+
process.env.NODE_ENV === "development" ? `127.0.0.1:${env.WEB_SERVICE_PORT}` : env.PUBLIC_URL;
7
+
5
8
export default {
6
9
data: new SlashCommandBuilder()
7
10
.setName("login")
···
9
12
"Authenticate with your Atmosphere account and request your listens to be tracked",
10
13
),
11
14
async execute(interaction: ChatInputCommandInteraction) {
12
-
await interaction.reply(`Log in here and request to be tracked: ${env.WEB_SERVICE_URL}:${env.WEB_SERVICE_PORT}/login`);
13
-
logger.info(`starting authentication process for ${interaction.user.globalName} at ${new Date().toJSON()}`);
15
+
await interaction.reply(`Log in here and request to be tracked: ${LOGIN_URL}/login`);
16
+
logger.info(
17
+
`starting authentication process for ${interaction.user.username} in guild ${interaction.guildId} at ${new Date().toJSON()}`,
18
+
);
14
19
},
15
20
};
+1
-3
apps/bot/deploy-commands.ts
+1
-3
apps/bot/deploy-commands.ts
+1
-1
apps/bot/package.json
+1
-1
apps/bot/package.json
+10
-14
apps/web/client.ts
+10
-14
apps/web/client.ts
···
8
8
import { env } from "@tealfmbot/common/constants";
9
9
import { db } from "@tealfmbot/database/db";
10
10
import assert from "node:assert";
11
+
11
12
import { SessionStore, StateStore } from "./storage.js";
12
13
13
14
const loadJwk = async () => {
14
-
const raw = env.PRIVATE_KEYS;
15
-
if (!raw) return undefined;
16
-
const json = JSON.parse(raw);
17
-
if (!json) return undefined;
18
-
const keys = await Promise.all(
19
-
json.map((jwk: string | Record<string, unknown>) => JoseKey.fromJWK(jwk)),
20
-
);
21
-
return new Keyset(keys);
15
+
const raw = env.PRIVATE_KEYS;
16
+
if (!raw) return undefined;
17
+
const json = JSON.parse(raw);
18
+
if (!json) return undefined;
19
+
const keys = await Promise.all(
20
+
json.map((jwk: string | Record<string, unknown>) => JoseKey.fromJWK(jwk)),
21
+
);
22
+
return new Keyset(keys);
22
23
};
23
24
24
-
const keyset =
25
-
env.PUBLIC_URL && env.PRIVATE_KEYS
26
-
? await loadJwk()
27
-
: undefined;
28
-
29
-
25
+
const keyset = env.PUBLIC_URL && env.PRIVATE_KEYS ? await loadJwk() : undefined;
30
26
31
27
assert(!env.PUBLIC_URL || keyset?.size, "PRIVATE_KEYS environment variable must be set");
32
28
+28
-15
apps/web/index.ts
+28
-15
apps/web/index.ts
···
8
8
import pinoHttpLogger from "pino-http";
9
9
10
10
import { client } from "./client.js";
11
-
import { createSession, getSessionAgent, MAX_AGE, validateIdentifier } from "./utils.js";
11
+
import {
12
+
createSession,
13
+
getSessionAgent,
14
+
MAX_AGE,
15
+
validateIdentifier,
16
+
getSession,
17
+
} from "./utils.js";
12
18
13
19
type Variables = {
14
20
logger: typeof logger;
···
29
35
});
30
36
31
37
app.get("/dashboard", async (c) => {
32
-
const agent = await getSessionAgent(c);
33
-
if (agent?.assertAuthenticated()) {
34
-
return c.redirect("/login");
38
+
const session = await getSession(c, env);
39
+
if (session) {
40
+
const agent = await getSessionAgent(c);
41
+
return c.html(html`
42
+
<h1>Dashboard</h1>
43
+
<p>DID: ${agent?.assertDid}</p>
44
+
<form method="post" action="/logout">
45
+
<button type="submit">Log out</button>
46
+
</form>
47
+
`);
35
48
}
36
-
return c.html(html`
37
-
<h1>Dashboard</h1>
38
-
<p>DID: ${agent?.assertDid}</p>
39
-
<form method="post" action="/logout">
40
-
<button type="submit">Log out</button>
41
-
</form>
42
-
`);
49
+
return c.redirect("/login");
43
50
});
44
51
45
52
app.get("/health", (c) => {
···
61
68
const params = new URLSearchParams(c.req.url.split("?")[1]);
62
69
63
70
try {
64
-
const session = await getSignedCookie(c, env.COOKIE_SECRET, "__teal_fm_bot_session");
71
+
const session = await getSession(c, env);
65
72
if (session) {
66
73
try {
67
74
const oauthSession = await client.restore(session);
···
80
87
return c.redirect("/dashboard");
81
88
});
82
89
83
-
app.get("/login", (c) => {
90
+
app.get("/login", async (c) => {
91
+
const session = await getSession(c, env);
92
+
if (session) {
93
+
return c.redirect("/dashboard")
94
+
}
95
+
84
96
return c.html(
85
97
html`
86
98
<form action="/login" method="post">
···
90
102
</form>
91
103
`,
92
104
);
105
+
93
106
});
94
107
95
108
app.post(
···
114
127
});
115
128
return c.redirect(url.href.toString());
116
129
} catch (error) {
117
-
logger.error({error}, "oauth authorize failed");
130
+
logger.error({ error }, "oauth authorize failed");
118
131
return c.json({ message: "oauth authorize failed" }, 500);
119
132
}
120
133
},
···
122
135
123
136
app.post("/logout", async (c) => {
124
137
c.header("Cache-Control", "no-store");
125
-
const session = await getSignedCookie(c, env.COOKIE_SECRET, "__teal_fm_bot_session");
138
+
const session = await getSession(c, env);
126
139
if (session) {
127
140
try {
128
141
const oauthSession = await client.restore(session);
+5
apps/web/utils.ts
+5
apps/web/utils.ts
···
39
39
}
40
40
}
41
41
42
+
export async function getSession(c: Context, e: typeof env) {
43
+
const session = await getSignedCookie(c, e.COOKIE_SECRET, "__teal_fm_bot_session");
44
+
return session;
45
+
}
46
+
42
47
export function isValidUrl(url: string) {
43
48
if (URL.canParse(url)) {
44
49
const pdsUrl = new URL(url);
+2
build-and-publish-images.sh
+2
build-and-publish-images.sh
···
2
2
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
3
3
VERSION=$(git describe --tags --abbrev=0)
4
4
REGISTRY=atcr.io/besaid.zone
5
+
ATPROTO_HANDLE=besaid.zone
5
6
6
7
services=(
7
8
web
···
17
18
--target $svc \
18
19
--build-arg VERSION=${VERSION#v} \
19
20
--build-arg SHA=$SHA \
21
+
--build-arg ATPROTO_HANDLE=$ATPROTO_HANDLE \
20
22
--build-arg BUILD_DATE=$BUILD_DATE \
21
23
--pull \
22
24
--no-cache \
+1
-1
docker-compose.dev.yml
+1
-1
docker-compose.dev.yml
+2
-3
packages/common/constants.ts
+2
-3
packages/common/constants.ts
···
13
13
TAP_ADMIN_PASSWORD: str(),
14
14
DATABASE_URL: str({ devDefault: "postgres://postgres:password@localhost:5432/tealfmbotdb" }),
15
15
PUBLIC_URL: str(),
16
-
COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }),
16
+
COOKIE_SECRET: str({ devDefault: "00000000000000000000000000000000" }),
17
17
PRIVATE_KEYS: str(),
18
-
WEB_SERVICE_URL: str({ devDefault: "http://localhost" }),
19
-
WEB_SERVICE_PORT: num({devDefault: 8002})
18
+
WEB_SERVICE_PORT: num({ devDefault: 8002 }),
20
19
});