Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ DOTENV_PUBLIC_KEY="032dda4912c6faddddab573094801f500e92c6877e2a2b4716e4d7261c298
# .env
DENO_KV_URL="encrypted:BGHRIa2PJZtGSBFqRZRSilqltfsc/wbnLNWcrV8Uz1nyH8Oj8U8Q99xwocM2lOgXqjT5j92Y7I3jRMQqwe6azVac4yBBc84M4lcVObij5ATjl2hf/RtBfOH17gMturGIfJiMXkGG5gIPbC5qFtgjQr0isU5WAVYW+lNhnlRxtOK+rcFjUurfwVu8/P3M/0pjsdm06qRdkh2SHKZpqUKeAE5/ncY0OA9LCTiNSQ=="
DENO_KV_ACCESS_TOKEN="encrypted:BE2fQGbkSA5cuXvOXHkdJCPlKJGIF3kzyg06u5cXnVgSJT3CxQiDnHRpEF0s/IdldO4SY1iHONGHEahBWjDGk4Wt41oj/Az316WbTzhqc6kS/AFiy1szEBWFyu+6jl724SxmPk9VTIw8axXkjaZfINBc1wxfV/GZrmXZfnKA/WxaqRWDj6rcZO4="
GITHUB_TOKEN="encrypted:BDZxA0lxUyhV0Vdx/4dOmpQhya8CIWuI0qpnuD3Kx3yLRv8v46/FIqabPLY3TSJ1D1QWC0k4W8F5RkiHa1H6p1Fv4SJX+iJpLBMNZLiAHyI2wnITPTQl68LdN79dkEfga5WjMhTG8RsOZLZapJJb4Vvha85nhSmbiy1g/L4c2F1gvV58G8nreBY2Xp3DUuBv/S8qWD+Elj/ZgdWG9R9bPh1fC3dljqobNvJPLFxSUCtbPoCnVot5jmyFSNOTjA=="
3 changes: 2 additions & 1 deletion .env.prod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
DOTENV_PUBLIC_KEY_PROD="02a0e747641689038dd186f2ff44b0dcfab2e56c7908d3d2a214ac4b6d9a0350ac"

DENO_KV_URL="encrypted:BFE7P49Dn8+d4HzL/iUUiAf4YVUnILVGWqxGeVXyX/BTYrvMfoIHdkK2U4zxoEOJTzAkrhg4GmhzlkyYIZ2QWKcNEd+S7JXkavrnQs5leI+ZxVMaF3UNFuXP10tLQs0Ez87XPT1FRK+NZhgn950m4G8pAjoiG7h/3+wQ8I/Ps+z82Yl09p8zFJfkIySsKLnqO+rMWd/c6jI54WMJGPv9abAduM5zphOvE5GSoA=="
DENO_KV_ACCESS_TOKEN="encrypted:BJBaDO65KbLOVB0G/XeiwS4BMZiIxkUnUkJMJsZ5H0eOHTfXEj91YX8xcpGM7f6b4EKnV7CHFINkplZUqI1BhbXb+598DYkl/Mg3YJ1BXi2dF/NGfurEzQKFii+F0UmitbBe2Z10cJ54GoG3tLTlmudDf2EYe0HPESBFiCrOjlrXQfNmx3Nyeio="
DENO_KV_ACCESS_TOKEN="encrypted:BJBaDO65KbLOVB0G/XeiwS4BMZiIxkUnUkJMJsZ5H0eOHTfXEj91YX8xcpGM7f6b4EKnV7CHFINkplZUqI1BhbXb+598DYkl/Mg3YJ1BXi2dF/NGfurEzQKFii+F0UmitbBe2Z10cJ54GoG3tLTlmudDf2EYe0HPESBFiCrOjlrXQfNmx3Nyeio="
GITHUB_TOKEN="encrypted:BIUCNVVVwehMW3+hmvN+SszVo+8pQB3wu/Gg1csjgcbf1XlEhe5ZTFdsUtzcZQFuJC5hd9wmGRrg71xPMP5ntrpUVmItps9mB+OdMEBmQPtexZjHBE1z6Ti2+oBYE4ghVszJV6TSnjRCydAEs0sNKM3FtPsPHd0xQlqt0xgV3WUdDZVpqPkn6AsJETjUrvEpvUcsoe+aF+KnpFjR39EWmJVUHJYiwmjlZXAmuAh5Z/0xql5BAr1Dv6d8S6HYXA=="
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
node_modules
.env.keys
secrets/
.env.decrypted
*.decrypted
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
deno 2.0.4
10 changes: 5 additions & 5 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"recommendations": [
"denoland.vscode-deno",
"vivaxy.vscode-conventional-commits"
]
}
"recommendations": [
"denoland.vscode-deno",
"vivaxy.vscode-conventional-commits"
]
}
12 changes: 9 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@
"github-actions",
"issue-templates",
"issue-forms",
"global"
"global",
"vscode"
],
"conventionalCommits.promptCI": true,
"conventionalCommits.emojiFormat": "code"
}
"conventionalCommits.emojiFormat": "code",
"editor.colorDecoratorsLimit": 50000,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"dotenv.enableAutocloaking": false
}
41 changes: 41 additions & 0 deletions api/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Bool, OpenAPIRoute, Str } from "chanfana";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unused imports.

The following imports are not used in the code:

  • Bool, Str from "chanfana"
  • z from "zod"
-import { Bool, OpenAPIRoute, Str } from "chanfana";
+import { OpenAPIRoute } from "chanfana";
-import { z } from "zod";

Also applies to: 3-3

import { Context } from "hono";
import { z } from "zod";
import { handleGitHubAuth, hashToken } from "../lib/githubAuth.ts";
import { kv } from "../lib/db.ts";
import { config } from "../lib/config.ts";

export class testGitHubAuth extends OpenAPIRoute {
override schema = {
tags: ["admin"],
summary: "Check if you are authenticated or not",
description: "To avoid wasting GitHub API requests, we'll cache the API results on KV for 5 minutes. You can also use this endpoint to clear the cache by add `?force=1` URL parameter.",
security: [
{
BearerAuth: [],
},
],
}

override async handle(c: Context) {
const authHeader = c.req.header("Authorization")
const parsedAuthHeader = authHeader?.split(" ") || ["bearer", "null"];
const tokHash = await hashToken(parsedAuthHeader[1]);
const key = ["cachedGitHubTokenHash", tokHash];

if (parsedAuthHeader[1] == "null") {
return c.json({
ok: false,
error: "missing auth key"
}, 418)
}

const result = await handleGitHubAuth(parsedAuthHeader[1], true)

const dbMeta = await (await kv(config.kvUrl)).get(key)
return c.json({
ok: result,
result: dbMeta
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for KV operations.

The KV store operations could fail, but there's no error handling in place. Also, consider adding type safety for the stored metadata.

-  const dbMeta = await (await kv(config.kvUrl)).get(key)
-  return c.json({
-    ok: result,
-    result: dbMeta
-  })
+  try {
+    const dbMeta = await (await kv(config.kvUrl)).get(key)
+    return c.json({
+      ok: result,
+      result: dbMeta ?? null
+    })
+  } catch (error) {
+    console.error('KV store error:', error);
+    return c.json({
+      ok: result,
+      result: null,
+      error: {
+        code: "KV_ERROR",
+        message: "Failed to retrieve cached data"
+      }
+    }, 500)
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dbMeta = await (await kv(config.kvUrl)).get(key)
return c.json({
ok: result,
result: dbMeta
})
try {
const dbMeta = await (await kv(config.kvUrl)).get(key)
return c.json({
ok: result,
result: dbMeta ?? null
})
} catch (error) {
console.error('KV store error:', error);
return c.json({
ok: result,
result: null,
error: {
code: "KV_ERROR",
message: "Failed to retrieve cached data"
}
}, 500)
}

}
}
122 changes: 84 additions & 38 deletions api/badges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@ import { Context } from "hono";
import { z } from "zod";
import { BadgeData, getBadgeData, resolveBadgeIcon } from "../lib/db.ts";
import { Format, makeBadge } from "badge-maker";
import { makeLogo } from "../lib/logos.ts";
import { getOrgData } from "../lib/hcb.ts";
import { validateBadgeStyle } from "../lib/utils.ts";

/**
* Get a HCB balance badge
*/
export class hcbBalanceOps extends OpenAPIRoute {
schema = {
override schema = {
tags: ["hcb"],
summary: "Generate a SVG badge of a HCB organization's balances",
descritpion: `\
By default without the \`org\` query parameter, it will uses data from [Hack Club HQ](https://hcb.hackclub.com/api/v3/organizations/hq),
but it will change to either \`recaptime-dev\` or \`lorebooks-wiki\` in the future.
description: `\
By default without the \`org\` query parameter, it will uses data from [Hack Club HQ](https://hcb.hackclub.com/api/v3/organizations/hq), \
but it will change to either \`recaptime-dev\` or \`lorebooks-wiki\` in the future.

The generated badge includes your organization's balance after dividing \`balances.balance_cents\` from API to 100 to show up the cents \
in USD.
`,
request: {
query: z.object({
Expand All @@ -30,6 +36,7 @@ but it will change to either \`recaptime-dev\` or \`lorebooks-wiki\` in the futu
},
responses: {
"200": {
description: "Generate a HCB badge with your organization balances",
content: {
"image/svg+xml": {
schema: {
Expand All @@ -41,36 +48,54 @@ but it will change to either \`recaptime-dev\` or \`lorebooks-wiki\` in the futu
},
};

async handle(c: Context) {
override async handle(c: Context) {
const apiReqData = await this.getValidatedData<typeof this.schema>();
const { org, style } = apiReqData.query;
const { org, style } = apiReqData?.query;
const { result, code } = await getOrgData(org || "hq");
console.log(`API result: ${JSON.stringify(result)} | code is ${code}`);

if (code == 200) {
const bal = result.balances.balance_cents / 100;
const badge: Format = {
label: `HCB balance for ${org}`,
label: `HCB balance for ${result.name}`,
labelColor: "EC3750",
message: `USD ${bal}`,
logoBase64: await resolveBadgeIcon("hcb-dark"),
style: style || "flat",
style: validateBadgeStyle(style),
};
console.log(badge);
const badgeSvg = makeBadge(badge);
return c.newResponse(badgeSvg, 200, {
"Content-Type": "image/svg+xml",
"Cache-Control": "max-age=300",
});
} else if (code == 404) {
const badgeSvg = makeBadge({
label: `HCB balance for unknown organization`,
labelColor: "EC3750",
message: `USD 0`,
logoBase64: await resolveBadgeIcon("hcb-dark"),
style: validateBadgeStyle(style)
});
return c.newResponse(badgeSvg, 404, {
"Content-Type": "image/svg+xml",
"Cache-Control": "max-age=300",
});
}
}
}

export class hcbDonateButton extends OpenAPIRoute {
schema = {
override schema = {
tags: ["hcb"],
summary: "Generate a SVG badge of a HCB organization's balances",
descritpion: `\
summary: "Generate a SVG badge for HCB donate badges.",
description: `\
By default without the \`org\` query parameter, it will uses data from [Hack Club HQ](https://hcb.hackclub.com/api/v3/organizations/hq),
but it will change to either \`recaptime-dev\` or \`lorebooks-wiki\` in the future.
but it will change to either \`recaptime-dev\` or \`lorebooks-wiki\` in the future.

The generated badge includes your organization name and embeds the donation page URL and your organization URL (if transparency mode is enabled)
so it is easily clickable when added as a SVG object.

`,
request: {
query: z.object({
Expand Down Expand Up @@ -99,29 +124,37 @@ but it will change to either \`recaptime-dev\` or \`lorebooks-wiki\` in the futu
},
};

async handle(c: Context) {
override async handle(c: Context) {
const apiReqData = await this.getValidatedData<typeof this.schema>();
const { org, style } = apiReqData.query;
const dbData = (await getBadgeData("hcb", "donate")).result?.data;
const badgeData: Format = {
label: dbData?.label,
labelColor: dbData?.labelColor,
logoBase64: await resolveBadgeIcon("hcb-dark"),
message: `Donate to ${org || "HQ"}`,
links: [
`https://hcb.hackclub.com/donations/start/${org || "hq"}`,
`https://hcb.hackclub.com/donations/start/${org || "hq"}`,
],
};
const badge = makeBadge(badgeData);
return c.newResponse(badge, 200, {
"Content-Type": "image/svg+xml",
});
const { result, code } = await getOrgData(org || "hq");
if (code == 200) {
const badgeData: Format = {
label: "Donate on HCB",
labelColor: "EC3750",
logoBase64: await resolveBadgeIcon("hcb-dark"),
message: `@${org || "hq"}`,
links: [
`https://hcb.hackclub.com/donations/start/${org || "hq"}`,
`https://hcb.hackclub.com/${org || "hq"}`,
],
style: validateBadgeStyle(style),
};
console.log(badgeData);
const badge = makeBadge(badgeData);
return c.newResponse(badge, 200, {
"Content-Type": "image/svg+xml",
"Cache-Control": "max-age=300",
});
} else if (code == 404) {
return c.notFound();
}
}
}

export class generateSvg extends OpenAPIRoute {
schema = {
override schema = {
tags: ["badges"],
summary: "Generate a SVG badge based on stored badge data on Deno KV.",
description: `\
Expand Down Expand Up @@ -171,7 +204,7 @@ including \`logo\` (not \`logoBase64\` for abuse prevention) and \`style\`.
},
};

async handle(c: Context) {
override async handle(c: Context) {
const apiReqData = await this.getValidatedData<typeof this.schema>();
const reqUrl = new URL(c.req.url);
const { origin } = reqUrl;
Expand All @@ -181,7 +214,7 @@ including \`logo\` (not \`logoBase64\` for abuse prevention) and \`style\`.
const acceptCT = c.req.header("Accept");
const dbData = await getBadgeData(
apiReqData.params.project,
apiReqData.params.badgeName
apiReqData.params.badgeName,
);
console.log(dbData);

Expand All @@ -197,7 +230,7 @@ including \`logo\` (not \`logoBase64\` for abuse prevention) and \`style\`.
versionStamp: null,
error: "project and badge name combination not found",
},
404
404,
);
}
return c.json(dbData);
Expand All @@ -211,6 +244,7 @@ including \`logo\` (not \`logoBase64\` for abuse prevention) and \`style\`.
});
return c.newResponse(Badge404, 404, {
"Content-Type": "image/svg+xml",
"Cache-Control": "max-age=900",
});
}

Expand All @@ -225,8 +259,9 @@ including \`logo\` (not \`logoBase64\` for abuse prevention) and \`style\`.
if (type == "redirect") {
if (typeof data?.redirectUrl == "string") {
let baseString = data.redirectUrl;
if (baseString.startsWith("/badges/"))
if (baseString.startsWith("/badges/")) {
baseString = `${origin}${data.redirectUrl}`;
}
const urlParamsOps = new URL(baseString);
for (const param in apiReqData.query) {
if (param == "style" && apiReqData.query.style == undefined) {
Expand All @@ -238,23 +273,34 @@ including \`logo\` (not \`logoBase64\` for abuse prevention) and \`style\`.
return c.redirect(urlParamsOps.toString());
}
return c.redirect(
"https://badges.api.lorebooks.wiki/badges/notfound/notfound"
"https://badges.api.lorebooks.wiki/badges/notfound/notfound",
);
} else if (type == "badge") {
console.log(`logo name: ${data?.logo || null}`);
const logoData = await resolveBadgeIcon(data?.logo);
console.log(`logo data - ${logoData}`);
let badgeData: Format = {
message: data.message,
color: data?.color || "gray",
color: color || data?.color || "gray",
style: validateBadgeStyle(style || data?.color),
};
if (typeof data?.label == "string") {
Object.assign(badgeData, {
label: data?.label,
labelColor: data?.labelColor,
});
}
if (logoData != null) {
if (
logoData != null &&
(style == "social" || data?.style == "social") &&
data?.logo?.endsWith("-light")
) {
Object.assign(badgeData, {
logoBase64: await resolveBadgeIcon(
data.logo.replace(/-light/gm, "-dark"),
),
});
} else {
Object.assign(badgeData, {
logoBase64: logoData,
});
Expand All @@ -264,10 +310,10 @@ including \`logo\` (not \`logoBase64\` for abuse prevention) and \`style\`.
links: data.links,
});
}

const badge = makeBadge(badgeData);
return c.newResponse(badge, 200, {
"Content-Type": "image/svg+xml",
"Cache-Control": "max-age=900",
});
}
} catch (error) {
Expand All @@ -276,7 +322,7 @@ including \`logo\` (not \`logoBase64\` for abuse prevention) and \`style\`.
label: "error",
message: "something went wrong",
color: "red",
style: apiReqData.query.style || "flat",
style: validateBadgeStyle(style),
});
return c.newResponse(resultSvgError, 500, {
"Content-Type": "image/svg+xml",
Expand Down
7 changes: 4 additions & 3 deletions api/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@ import { Context } from "hono";
import { z } from "zod";

export class ping extends OpenAPIRoute {
schema = {
override schema = {
description: "Pings the server if still up.",
tags: ["meta"],
responses: {
"200": {
description: "Everything seems to be up",
content: {
"application/json": {
schema: z.object({
ok: Bool({default: true}).default(true),
ok: Bool({ default: true }).default(true),
}),
},
},
},
},
};

async handle(c: Context) {
override handle(c: Context) {
return c.json({
ok: true,
result: "Everything is up",
Expand Down
Loading