Skip to content

Update fork with tag support #3

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

Merged
merged 3 commits into from
Nov 18, 2024
Merged
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
@@ -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
5 changes: 3 additions & 2 deletions app/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 3 additions & 1 deletion app/admin/challs/preview/page.tsx
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 3 additions & 2 deletions app/auth/route.ts
Original file line number Diff line number Diff line change
@@ -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');
17 changes: 17 additions & 0 deletions app/challenges/GridChallengeModal.tsx
Original file line number Diff line number Diff line change
@@ -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>
8 changes: 5 additions & 3 deletions app/challenges/page.tsx
Original file line number Diff line number Diff line change
@@ -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);
@@ -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;
}
}

@@ -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
@@ -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
@@ -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;
4 changes: 3 additions & 1 deletion app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -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')
4 changes: 3 additions & 1 deletion app/scoreboard/page.tsx
Original file line number Diff line number Diff line change
@@ -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;
614 changes: 523 additions & 91 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -13,10 +13,10 @@
"@headlessui/tailwindcss": "^0.2.0",
"autoprefixer": "^10.4.20",
"luxon": "^3.5.0",
"next": "^14.2.5",
"next": "^15.0.3",
"postcss": "^8.4.41",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
"recharts": "^2.12.2",
10 changes: 7 additions & 3 deletions util/auth.ts
Original file line number Diff line number Diff line change
@@ -47,7 +47,8 @@ export async function register(email: string, name: string) {
if (res.kind !== 'goodRegister')
return { error: res.message };

cookies().set(AUTH_COOKIE_NAME, res.data.authToken);
const c = await cookies();
c.set(AUTH_COOKIE_NAME, res.data.authToken);

return { ok: true };
}
@@ -116,13 +117,16 @@ export async function login(token: string) {
if (res.kind !== 'goodLogin')
return { error: res.message };

cookies().set(AUTH_COOKIE_NAME, res.data.authToken);
const c = await cookies();
c.set(AUTH_COOKIE_NAME, res.data.authToken);

return { ok: true };
}

export async function logout() {
cookies().delete(AUTH_COOKIE_NAME);
const c = await cookies();
c.delete(AUTH_COOKIE_NAME);

return redirect('/');
}

5 changes: 4 additions & 1 deletion util/challenges.ts
Original file line number Diff line number Diff line change
@@ -11,8 +11,11 @@ export type Challenge = {
sortWeight: number,
solves: number,
points: number,
} & Partial<NonStandardChallProps>

difficulty?: string, // Non-standard
type NonStandardChallProps = {
difficulty: string,
tags: string[],
}

type FileData = {
4 changes: 3 additions & 1 deletion util/flags.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,9 @@ export async function attemptSubmit(
id: string,
flag: string
) {
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
const c = await cookies();
const token = c.get(AUTH_COOKIE_NAME)?.value;

if (!token) return { kind: 'badToken', message: 'Missing token' }; // TODO: hacky?

const res: GoodFlagResponse | BadFlagResponse | CTFEndedResponse = await (await fetch(`${process.env.API_BASE}/challs/${id}/submit`, {
8 changes: 6 additions & 2 deletions util/profile.ts
Original file line number Diff line number Diff line change
@@ -69,7 +69,9 @@ export type UpdateProfilePayload = {
division?: string
}
export async function updateProfile(payload: UpdateProfilePayload) {
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
const c = await cookies();

const token = c.get(AUTH_COOKIE_NAME)?.value;
if (!token)
return { error: 'Not authenticated.' };

@@ -101,7 +103,9 @@ type DivisionNotAllowedResponse = {
}

export async function updateEmail(email: string) {
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
const c = await cookies();

const token = c.get(AUTH_COOKIE_NAME)?.value;
if (!token)
return { error: 'Not authenticated.' };