Skip to content

Commit df1d5f0

Browse files
authored
Merge pull request #3 from ky28059/main
Update fork with tag support
2 parents a1c8950 + 6cb8bc1 commit df1d5f0

16 files changed

+608
-113
lines changed

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ You can then start both the rCTF backend and production frontend instance simult
5555
docker compose up -d --build
5656
```
5757

58+
### Non-standard properties (experimental)
59+
On top of supporting all the standard `Challenge` object fields provided by rCTF, this frontend also supports a subset
60+
of non-standard fields if they are present; see [`b01lers/rctf-deploy-action`](https://github.com/b01lers/rctf-deploy-action)
61+
for a complete list.
62+
63+
The underlying mechanism for this is that:
64+
- Challenges are created via `PUT` requests to `/api/v1/admin/challs/{id}` on the rCTF backend with the challenge
65+
metadata as a JSON body, and the JSON data is stored in the challenge database as-is (extra properties included).
66+
- When non-admin users fetch `/api/v1/challs`, however, challenges are [cleaned to only return rCTF's standard properties](https://github.com/redpwn/rctf/blob/master/server/api/challs/get.js#L15)
67+
(see the clean function [here](https://github.com/redpwn/rctf/blob/master/server/challenges/index.ts#L16)).
68+
69+
Then, this frontend fetches the admin challenges endpoint and manually injects (or otherwise handles) any additional
70+
properties before returning them to the client. To this end, if you want to support `prereqs` or other non-standard rCTF
71+
properties in your deployment, make sure you have an `env` file exporting an admin auth token like so:
72+
```env
73+
ADMIN_TOKEN=...
74+
```
75+
If you want to customize which additional properties are supported, see the [challenges page](https://github.com/ky28059/bctf/blob/main/app/challenges/page.tsx).
76+
5877
### Configuring
5978
Further config options can be edited in `/util/config.ts`:
6079
```ts

app/NavBar.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import LogoutButton from '@/app/LogoutButton';
99
import { AUTH_COOKIE_NAME } from '@/util/config';
1010

1111

12-
export default function NavBar() {
13-
const authed = cookies().has(AUTH_COOKIE_NAME);
12+
export default async function NavBar() {
13+
const c = await cookies();
14+
const authed = c.has(AUTH_COOKIE_NAME);
1415

1516
return (
1617
<NavWrapper>

app/admin/challs/preview/page.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import { getAdminChallenges } from '@/util/admin';
1313

1414

1515
export default async function AdminChallengesPreview() {
16-
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
16+
const c = await cookies();
17+
18+
const token = c.get(AUTH_COOKIE_NAME)?.value;
1719
if (!token) return redirect('/');
1820

1921
const challenges = await getAdminChallenges(token);

app/auth/route.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ const ALLOWED_REDIRECTS = [`${process.env.KLODD_URL}/auth`];
99
* "pseudo-oauth" functionality for Klodd.
1010
* See {@link https://klodd.tjcsec.club/install-guide/prerequisites/}.
1111
*/
12-
export function GET(req: NextRequest) {
13-
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
12+
export async function GET(req: NextRequest) {
13+
const c = await cookies();
14+
const token = c.get(AUTH_COOKIE_NAME)?.value;
1415

1516
const params = req.nextUrl.searchParams;
1617
const state = params.get('state');

app/challenges/GridChallengeModal.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ export default function GridChallengeModal(props: GridChallengeModalProps) {
4040
<h1 className="text-2xl text-center mb-2 [overflow-wrap:anywhere]">
4141
{props.challenge.name}
4242
</h1>
43+
{props.challenge.tags && props.challenge.tags.length > 0 && (
44+
<div className="flex gap-1.5 justify-center mb-1">
45+
{props.challenge.tags.map((t) => (
46+
<span key={t}
47+
className="text-xs bg-theme-bright/30 text-theme-bright rounded-full font-semibold px-2 py-0.5">
48+
{t}
49+
</span>
50+
))}
51+
{/*
52+
{props.challenge.difficulty && (
53+
<span className="text-sm bg-theme-bright/30 text-theme-bright rounded-full font-semibold px-2 py-0.5">
54+
{props.challenge.difficulty}
55+
</span>
56+
)}
57+
*/}
58+
</div>
59+
)}
4360
<p className="text-lg text-center text-primary mb-6">
4461
{props.challenge.points}
4562
</p>

app/challenges/page.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export const metadata: Metadata = {
2020
}
2121

2222
export default async function ChallengesPage() {
23-
const token = cookies().get(AUTH_COOKIE_NAME)!.value;
23+
const c = await cookies();
24+
const token = c.get(AUTH_COOKIE_NAME)!.value;
2425

2526
const challenges = await getChallenges(token);
2627
const profile = await getMyProfile(token);
@@ -41,9 +42,10 @@ export default async function ChallengesPage() {
4142
const solved = new Set(profile.data.solves.map((c) => c.id));
4243
challs = challs.filter((c) => !adminData[c.id].prereqs || adminData[c.id].prereqs!.every((p) => solved.has(p)));
4344

44-
// Inject desired properties back into client challenges
45+
// Inject additional properties back into client challenges
4546
for (const c of challs) {
4647
c.difficulty = adminData[c.id].difficulty;
48+
c.tags = adminData[c.id].tags;
4749
}
4850
}
4951

@@ -67,7 +69,7 @@ async function getAdminChallData() {
6769
if (!process.env.ADMIN_TOKEN) return;
6870

6971
const res = await getAdminChallenges(process.env.ADMIN_TOKEN);
70-
if (res.kind === 'badToken') return;
72+
if (res.kind !== 'goodChallenges') return;
7173

7274
return Object.fromEntries(res.data.map((c) => [c.id, c]));
7375
}

app/logout/route.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { AUTH_COOKIE_NAME } from '@/util/config';
44

55

66
export async function GET(req: Request) {
7-
cookies().delete(AUTH_COOKIE_NAME);
7+
const c = await cookies();
8+
c.delete(AUTH_COOKIE_NAME);
9+
810
return NextResponse.redirect(new URL('/', req.url));
911
}

app/profile/Profile.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import { AUTH_COOKIE_NAME, getConfig } from '@/util/config';
1111

1212

1313
export default async function Profile(props: ProfileData) {
14-
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
14+
const c = await cookies();
15+
const token = c.get(AUTH_COOKIE_NAME)?.value;
16+
1517
const challs = token
1618
? await getChallenges(token)
1719
: null;

app/profile/page.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export const metadata: Metadata = {
1616
}
1717

1818
export default async function ProfilePage() {
19-
const token = cookies().get(AUTH_COOKIE_NAME)!.value;
19+
const c = await cookies();
20+
21+
const token = c.get(AUTH_COOKIE_NAME)!.value;
2022
const data = await getMyProfile(token);
2123

2224
if (data.kind === 'badToken')

app/scoreboard/page.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ export const metadata: Metadata = {
1616
}
1717

1818
export default async function ScoreboardPage() {
19+
const c = await cookies();
20+
1921
const scoreboard = await getScoreboard();
2022
const graph = await getGraph();
2123

22-
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
24+
const token = c.get(AUTH_COOKIE_NAME)?.value;
2325
const profile = token
2426
? await getMyProfile(token)
2527
: undefined;

0 commit comments

Comments
 (0)