Skip to content

Commit 81bcd60

Browse files
authored
improve: API block check (#93)
1 parent a7fda59 commit 81bcd60

File tree

7 files changed

+169
-71
lines changed

7 files changed

+169
-71
lines changed

components/WarningApiBlock.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async function checkApiBlock() {
4444
}
4545
4646
working.value = true;
47-
const blocked = await $fetch<true | false>('/api/user/scroll/check');
47+
const blocked = await $fetch<true | false>('/api/checks/blocked');
4848
4949
if (!blocked) {
5050
reloadNuxtApp();

pages/index.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@
4343
/>
4444
</div>
4545

46-
<form v-else class="flex flex-col space-y-4" @submit.prevent="saveScroll()">
46+
<form
47+
v-else-if="!authData.user.apiBlocked"
48+
class="flex flex-col space-y-4"
49+
@submit.prevent="saveScroll()"
50+
>
4751
<div class="*:max-w-prose order-1">
4852
<div
4953
v-if="fetchScrollError"

server/api/checks/blocked.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { userTable } from '~/database/schema';
2+
import { db } from '~/server/db';
3+
import type { DragonData } from '~/types/DragonTypes';
4+
import { getToken, getServerSession } from '#auth';
5+
import { eq } from 'drizzle-orm';
6+
import { z } from 'zod';
7+
import { decrypt } from '~/utils/accessTokenHandling';
8+
import type { JWT } from 'next-auth/jwt';
9+
10+
export default defineEventHandler(async (event) => {
11+
const { clientSecret, accessTokenPassword } = useRuntimeConfig();
12+
13+
let username: string | null = null;
14+
let accessToken: string | null = null;
15+
let userId: number | null = null;
16+
17+
const querySchema = z.object({
18+
userId: z.coerce.number().optional(),
19+
clientSecret: z.string().optional(),
20+
});
21+
22+
const query = await querySchema.parseAsync(getQuery(event));
23+
24+
if (query.userId && query.clientSecret) {
25+
if (query.clientSecret !== clientSecret) {
26+
return;
27+
}
28+
const [user] = await db
29+
.select({
30+
username: userTable.username,
31+
accessToken: userTable.accessToken,
32+
})
33+
.from(userTable)
34+
.where(eq(userTable.id, query.userId));
35+
if (!user.username || !user.accessToken) {
36+
return;
37+
}
38+
39+
username = user.username;
40+
accessToken = decrypt(user.accessToken, accessTokenPassword);
41+
userId = query.userId;
42+
} else {
43+
const [t, s] = await Promise.all([
44+
getToken({ event }) as Promise<JWT>,
45+
getServerSession(event),
46+
]);
47+
48+
if (!t || !s) {
49+
return;
50+
}
51+
52+
accessToken = t.sessionToken as string;
53+
username = s.user.username;
54+
userId = s.user.id;
55+
}
56+
57+
// First, we'll grab a single dragon from the user with their private token.
58+
// That way, we know the dragon definitely exists.
59+
// We'll then test it again using the client secret.
60+
const privateResponse = await dragCaveFetch()<
61+
DragCaveApiResponse<{ hasNextPage: boolean; endCursor: null | number }> & {
62+
dragons: Record<string, DragonData>;
63+
}
64+
>('/user', {
65+
query: {
66+
username,
67+
limit: 1,
68+
},
69+
headers: {
70+
Authorization: `Bearer ${accessToken}`,
71+
},
72+
});
73+
74+
const singleDragon = Object.keys(privateResponse?.dragons ?? {})[0];
75+
76+
let blocked = false;
77+
78+
if (singleDragon) {
79+
const publicResponse = await dragCaveFetch()<{
80+
errors: unknown[];
81+
dragons?: DragonData;
82+
}>(`/dragon/${singleDragon}`, {
83+
timeout: 20000,
84+
retry: 3,
85+
retryDelay: 1000 * 5,
86+
headers: {
87+
Authorization: `Bearer ${clientSecret}`,
88+
},
89+
});
90+
91+
blocked = publicResponse.errors.length > 0;
92+
}
93+
94+
await db
95+
.update(userTable)
96+
.set({
97+
apiBlocked: blocked,
98+
})
99+
.where(eq(userTable.id, userId));
100+
101+
return blocked;
102+
});

server/api/user/scroll/check.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

server/clean-up.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import chunkArray from '~/utils/chunkArray';
22
import { db } from '~/server/db';
3-
import { hatcheryTable, recordingsTable, userTable } from '~/database/schema';
3+
import { hatcheryTable, recordingsTable } from '~/database/schema';
44
import { inArray } from 'drizzle-orm';
55
import { DateTime } from 'luxon';
66
import { dragCaveFetch } from '~/server/utils/dragCaveFetch';
77
import { isIncubated, isStunned } from '~/utils/calculations';
88
import type { DragonData } from '~/types/DragonTypes';
9+
import { blockedApiQueue } from './queue';
910

1011
export async function cleanUp() {
1112
const { clientSecret } = useRuntimeConfig();
@@ -27,7 +28,7 @@ export async function cleanUp() {
2728
const removeFromHatchery: string[] = [];
2829
const updateIncubated: string[] = [];
2930
const updateStunned: string[] = [];
30-
const apiBlocked: Set<number> = new Set();
31+
const apiBlockedTest: Set<number> = new Set();
3132
let hatchlings = 0;
3233
let eggs = 0;
3334
let adults = 0;
@@ -66,7 +67,7 @@ export async function cleanUp() {
6667
// Sucks for them. We'll remove it, and add a note to the user.
6768
if (code in apiResponse.dragons === false) {
6869
removeFromHatchery.push(code);
69-
apiBlocked.add(hatcheryDragon.userId);
70+
apiBlockedTest.add(hatcheryDragon.userId);
7071
continue;
7172
}
7273

@@ -171,14 +172,27 @@ export async function cleanUp() {
171172
)
172173
);
173174

174-
await Promise.allSettled(
175-
chunkArray(Array.from(apiBlocked), 200).map(async (chunk) =>
176-
db
177-
.update(userTable)
178-
.set({ apiBlocked: true })
179-
.where(inArray(userTable.id, chunk))
180-
)
181-
);
175+
// We can't totally be sure that just because we couldn't find one of their dragons
176+
// that they're blocking us. For example, maybe they transferred it to an account
177+
// that does have the Garden blocked. To be thorough, we'll find something
178+
// on their scroll and check against that.
179+
for (const userId of apiBlockedTest) {
180+
console.log('Adding to blockedApiQueue', userId);
181+
await blockedApiQueue.add(
182+
'blockedApiQueue',
183+
{
184+
userId,
185+
},
186+
{
187+
removeOnComplete: {
188+
age: 1000 * 60 * 60 * 24 * 7,
189+
},
190+
removeOnFail: {
191+
age: 1000 * 60 * 60 * 24 * 14,
192+
},
193+
}
194+
);
195+
}
182196

183197
const end = new Date().getTime();
184198

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Worker as BullWorker } from 'bullmq';
2+
3+
const {
4+
redis: { host, port },
5+
clientSecret,
6+
} = useRuntimeConfig();
7+
8+
export default defineNitroPlugin(async () => {
9+
new BullWorker<{ userId: number }>(
10+
'blockedApiQueue',
11+
async (job) => {
12+
await $fetch('/api/checks/blocked', {
13+
query: {
14+
userId: job.data.userId,
15+
clientSecret,
16+
},
17+
});
18+
},
19+
{
20+
connection: {
21+
host,
22+
port: Number(port),
23+
},
24+
concurrency: 10,
25+
}
26+
);
27+
28+
console.info('API block check worker started');
29+
});

server/queue/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,10 @@ export const shareScrollQueue = new Queue('shareScrollQueue', {
1717
port: Number(port),
1818
},
1919
});
20+
21+
export const blockedApiQueue = new Queue('blockedApiQueue', {
22+
connection: {
23+
host,
24+
port: Number(port),
25+
},
26+
});

0 commit comments

Comments
 (0)