Skip to content

Commit

Permalink
Merge pull request #3 from ky28059/main
Browse files Browse the repository at this point in the history
Update fork with tag support
  • Loading branch information
ky28059 authored Nov 18, 2024
2 parents a1c8950 + 6cb8bc1 commit df1d5f0
Show file tree
Hide file tree
Showing 16 changed files with 608 additions and 113 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ You can then start both the rCTF backend and production frontend instance simult
docker compose up -d --build
```

### Non-standard properties (experimental)
On top of supporting all the standard `Challenge` object fields provided by rCTF, this frontend also supports a subset
of non-standard fields if they are present; see [`b01lers/rctf-deploy-action`](https://github.com/b01lers/rctf-deploy-action)
for a complete list.

The underlying mechanism for this is that:
- Challenges are created via `PUT` requests to `/api/v1/admin/challs/{id}` on the rCTF backend with the challenge
metadata as a JSON body, and the JSON data is stored in the challenge database as-is (extra properties included).
- 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)
(see the clean function [here](https://github.com/redpwn/rctf/blob/master/server/challenges/index.ts#L16)).

Then, this frontend fetches the admin challenges endpoint and manually injects (or otherwise handles) any additional
properties before returning them to the client. To this end, if you want to support `prereqs` or other non-standard rCTF
properties in your deployment, make sure you have an `env` file exporting an admin auth token like so:
```env
ADMIN_TOKEN=...
```
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).

### Configuring
Further config options can be edited in `/util/config.ts`:
```ts
Expand Down
5 changes: 3 additions & 2 deletions app/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import LogoutButton from '@/app/LogoutButton';
import { AUTH_COOKIE_NAME } from '@/util/config';


export default function NavBar() {
const authed = cookies().has(AUTH_COOKIE_NAME);
export default async function NavBar() {
const c = await cookies();
const authed = c.has(AUTH_COOKIE_NAME);

return (
<NavWrapper>
Expand Down
4 changes: 3 additions & 1 deletion app/admin/challs/preview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { getAdminChallenges } from '@/util/admin';


export default async function AdminChallengesPreview() {
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
const c = await cookies();

const token = c.get(AUTH_COOKIE_NAME)?.value;
if (!token) return redirect('/');

const challenges = await getAdminChallenges(token);
Expand Down
5 changes: 3 additions & 2 deletions app/auth/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ const ALLOWED_REDIRECTS = [`${process.env.KLODD_URL}/auth`];
* "pseudo-oauth" functionality for Klodd.
* See {@link https://klodd.tjcsec.club/install-guide/prerequisites/}.
*/
export function GET(req: NextRequest) {
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
export async function GET(req: NextRequest) {
const c = await cookies();
const token = c.get(AUTH_COOKIE_NAME)?.value;

const params = req.nextUrl.searchParams;
const state = params.get('state');
Expand Down
17 changes: 17 additions & 0 deletions app/challenges/GridChallengeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ export default function GridChallengeModal(props: GridChallengeModalProps) {
<h1 className="text-2xl text-center mb-2 [overflow-wrap:anywhere]">
{props.challenge.name}
</h1>
{props.challenge.tags && props.challenge.tags.length > 0 && (
<div className="flex gap-1.5 justify-center mb-1">
{props.challenge.tags.map((t) => (
<span key={t}
className="text-xs bg-theme-bright/30 text-theme-bright rounded-full font-semibold px-2 py-0.5">
{t}
</span>
))}
{/*
{props.challenge.difficulty && (
<span className="text-sm bg-theme-bright/30 text-theme-bright rounded-full font-semibold px-2 py-0.5">
{props.challenge.difficulty}
</span>
)}
*/}
</div>
)}
<p className="text-lg text-center text-primary mb-6">
{props.challenge.points}
</p>
Expand Down
8 changes: 5 additions & 3 deletions app/challenges/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const metadata: Metadata = {
}

export default async function ChallengesPage() {
const token = cookies().get(AUTH_COOKIE_NAME)!.value;
const c = await cookies();
const token = c.get(AUTH_COOKIE_NAME)!.value;

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

// Inject desired properties back into client challenges
// Inject additional properties back into client challenges
for (const c of challs) {
c.difficulty = adminData[c.id].difficulty;
c.tags = adminData[c.id].tags;
}
}

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

const res = await getAdminChallenges(process.env.ADMIN_TOKEN);
if (res.kind === 'badToken') return;
if (res.kind !== 'goodChallenges') return;

return Object.fromEntries(res.data.map((c) => [c.id, c]));
}
4 changes: 3 additions & 1 deletion app/logout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { AUTH_COOKIE_NAME } from '@/util/config';


export async function GET(req: Request) {
cookies().delete(AUTH_COOKIE_NAME);
const c = await cookies();
c.delete(AUTH_COOKIE_NAME);

return NextResponse.redirect(new URL('/', req.url));
}
4 changes: 3 additions & 1 deletion app/profile/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { AUTH_COOKIE_NAME, getConfig } from '@/util/config';


export default async function Profile(props: ProfileData) {
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
const c = await cookies();
const token = c.get(AUTH_COOKIE_NAME)?.value;

const challs = token
? await getChallenges(token)
: null;
Expand Down
4 changes: 3 additions & 1 deletion app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export const metadata: Metadata = {
}

export default async function ProfilePage() {
const token = cookies().get(AUTH_COOKIE_NAME)!.value;
const c = await cookies();

const token = c.get(AUTH_COOKIE_NAME)!.value;
const data = await getMyProfile(token);

if (data.kind === 'badToken')
Expand Down
4 changes: 3 additions & 1 deletion app/scoreboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ export const metadata: Metadata = {
}

export default async function ScoreboardPage() {
const c = await cookies();

const scoreboard = await getScoreboard();
const graph = await getGraph();

const token = cookies().get(AUTH_COOKIE_NAME)?.value;
const token = c.get(AUTH_COOKIE_NAME)?.value;
const profile = token
? await getMyProfile(token)
: undefined;
Expand Down
Loading

0 comments on commit df1d5f0

Please sign in to comment.