Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update fork with tag support #3

Merged
merged 3 commits into from
Nov 18, 2024
Merged
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
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
Loading