Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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"
]
}
11 changes: 9 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@
"github-actions",
"issue-templates",
"issue-forms",
"global"
"global",
"vscode",
"coderabbit"
],
"conventionalCommits.promptCI": true,
"conventionalCommits.emojiFormat": "code"
"conventionalCommits.emojiFormat": "code",
"editor.colorDecoratorsLimit": 50000,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"dotenv.enableAutocloaking": false
}
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Contributing guidelines

Thanks for your interest in contributing into the project! If you want to contribute, here
are the documentation you need to get you set up.

## Pre-flight

- [ ] Agree to [the Community Code of Conduct](https://policies.recaptime.dev/code-of-conduct)
and [Developer Certificate of Origin](https://developercertificate.org/)
- [ ] Install Deno 2 and Node.js LTS 22.x on your machine (or use Dev Containers / Codespaces instead)
- [ ] You might also need to install dotenvx via npm: `npm i -g @dotenvx/dotenvx`

## Commit message style

Our commit message style uses Conventional Commits with the scopes being defined under the
`conventionalCommits.scopes` VS Code workspace configuration. We'll be working on setting up
Commitlint + Commitizen CLI integration soon.

## Sending patches

Since we automatically deploy the main branch to production via Deno Deploy, we use GitHub to
accept merge requests although we can also accept on its GitLab mirror and via sourcehut lists.

### Via sourcehut lists

> **New to sending email patches?** Visit <https://git-send-email.io/> for a guided tutorial.

For the subject prefix, please set `format.subjectPrefix` local Git config to `PATCH badges-api` if you are sending
it to dedicated dev mailing list for Community Lorebooks. Otherwise, set it to `PATCH lorebooks-wiki/bdges-api`
for the main dev mailing list at Recap Time Squad.

Before sending over email, set `sendemail.to` to either `~recaptime-dev/[email protected]`
or `~recaptime-dev/[email protected]` and enable `--annotate` by default with setting `sendemail.annotate`
to `yes` globally.

```bash
git config sendemail.to ~recaptime-dev/[email protected]
git config format.subjectPrefix "PATH badges-api"
git config format.signOff yes # don't forget to sign-off your commits
```
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
# Badges API for lorebooks.wiki
# Community Lorebooks Badges API

[![Built during Arcade 2024](https://badges.api.lorebooks.wiki/badges/hackclub/arcade?style=flat-square)](https://hackclub.com/arcade)
[![Donate to RecapTime.dev](https://badges.api.lorebooks.wiki/hcb/donate?org=recaptime-dev&style=flat-square)](https://hcb.hackclub.com/donations/start/recaptime-dev)

It's like our own instance of `img.shields.io`, but hosted in Deno Deploy with:

* `hono` and `chanfana` for API docs, validation and routing
* Deno KV for storing badge metadata
* `badges-maker` npm library for generating badges on the fly
- `hono` and `chanfana` for API docs, validation and routing
- Deno KV for storing badge metadata, lessening the URL parameter spaghetti
- `badges-maker` npm library for generating badges on the fly
- Edge caching with the Web Cache API (enabled as a Hono middleware)

You can see the API docs at <https://badges.api.lorebooks.wiki/docs> or
[explore our docs here](./docs/)

## Want to use Hack Club Arcade 2024 badge(s) in your README?
## Want to use Hack Club badge(s) in your README?

See [`docs/hackclub-badges.md`](./docs/hackclub-badges.md) for more details!
See [`docs/hackclub-badges.md`](./docs/hackclub-badges.md) for more details! We'll be adding
more YSWS badges alongside HCB and Hack Club community badges in the future, but you can
request one today.

## Running locally

> **:warning: Warning**: You need to either reset the `.env*` files and provide your own
> secrets or ask @ajhalili2006 for the contents of `.env.keys` to decrypt them via
> `dotenvx` in order to locally run the server.

```bash
deno task dev
dotenvx run -f .env -- deno task dev
```

It will listen on `localhost:8080` by default unless overriden by `PORT`
environment variable.

[See the docs](./docs/self-hosting.md) for a more detailed instructions on self-hosting
and more.

## License

AGPL
157 changes: 157 additions & 0 deletions api/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Bool, Num, OpenAPIRoute, Str } from "chanfana";
import { Context } from "hono";
import { z } from "zod";
import { github, handleGitHubAuth, hashToken, UserDataOps } from "../lib/githubAuth.ts";
import { kv } from "../lib/db.ts";
import { config } from "../lib/config.ts";

const { authServiceToken, org, team_slug } = config.github;
const ghApi = github(authServiceToken)

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=true` URL parameter.",
security: [
{
BearerAuth: [],
},
],
request: {
query: z.object({
force: Bool({
description: "Force checking permissions for authenticated user even if cached.",
default: false,
required: false
})
})
}
}

override async handle(c: Context) {
const { query } = await this.getValidatedData<typeof this.schema>();
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: {
code: "MISSING_AUTH",
message: "Authorization header is required"
}
}, 401)
}

const result = await handleGitHubAuth(parsedAuthHeader[1], true, query?.force ?? false)

try {
const dbMeta = await (await kv(config.kvUrl)).get<UserDataOps>(key)
return c.json({
ok: result,
result: dbMeta.value ?? 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)
}
}
}

export class grantAdminAccess extends OpenAPIRoute {
override schema = {
tags: ["admin"],
summary: "Add a GitHub user to the API admins team",
description: `\
Grant a GitHub user access to Badges API admin endpoints by adding them into [the API admins team](https://github.com/orgs/${config.github.org}/teams/${config.github.team_slug}).

If they are not yet in the \`${config.github.org}\` GitHub organization, they'll be needed to accept the organization invite first.`,
request: {
query: z.object({
username: Str({
description: "The GitHub user to grant admin permissions",
required: true
}),
force: Bool({
description: "Force checking permissions for authenticated user even if cached.",
default: false,
required: false
})
})
},
responses: {
"200": {
description: "Successfully added/invited a user to the team",
content: {
"application/json": {
schema: z.object({
ok: Bool({ default: true }).default(true),
result: z.object({
message: Str({ default: "Successfully added" }),
ghApiResult: z.object({
url: Str({ example: "https://api.github.com/organizations/78218015/team/10816194/memberships/username" }),
role: Str({ example: "member" }),
state: Str({ example: "pending" })
}),
ghApiStatus: Num({ default: 200 })
})
})
}
}
}
}
};

override async handle(c: Context) {
try {
const { query } = await this.getValidatedData<typeof this.schema>();
const apiResult = await ghApi.teams.addOrUpdateMembershipForUserInOrg({
org,
team_slug,
username: query.username
})

return c.json({
ok: true,
result: {
message: "Successfully added",
ghApiResult: apiResult.data,
ghApiStatus: apiResult.status
}
})
} catch (error) {
return c.json({
ok: false,
error: {
code: "GITHUB_API_ERROR",
message: "Something went wrong while adding into the API admins team.",
ghApiResult: error.response.data,
ghApiStatusCode: error.status
}
})
}
}
}

export class revokeAdminAccess extends OpenAPIRoute {
// TODO: Implement this
}

export class getAdminUserInfo extends OpenAPIRoute {
// TODO: Implement this
}

export class listAdmins extends OpenAPIRoute {
// TODO: Implement this
}
Loading