├── public
├── ed63a00f-2193-49d1-b0e9-76a4a1a5a17c.txt
├── ads.txt
├── 404.png
├── logo.png
├── doubt.png
├── logo1.png
├── aichatbot.png
├── flagforge.gif
├── aichatbot2.png
├── badges
│ ├── 0x1.png
│ ├── 0x2.png
│ ├── 0x3.png
│ ├── 0x4.png
│ ├── 0x5.png
│ ├── 0x6.png
│ ├── 0x7.png
│ ├── bughunter.png
│ ├── securityresearcher.png
│ ├── custom
│ │ ├── badge-1758177518062-zx3ywujqht.png
│ │ ├── badge-1758177272322-9bloahyyzvl.png
│ │ ├── badge-1758178350831-vnaxnz7ifrg.png
│ │ ├── badge-1758178601608-6z426myfiat.png
│ │ └── badge-1758264629375-0y4fdhdxjy5.png
│ ├── images
│ │ ├── badge-1758563324750-h7zfukwxw7.png
│ │ └── badge-1758563431839-63u7vxws5u.png
│ ├── bounty.svg
│ ├── CTF.svg
│ └── researcher.svg
├── NirmalDahal.jpeg
├── SobitThakuri.jpeg
├── flagforge-logo.png
├── server-working.jpg
├── server-support-header-image.png
├── robots.txt
├── sitemap.xml
├── vercel.svg
├── .well-known
│ └── security.txt
├── next.svg
└── sitemap-0.xml
├── .vscode
└── settings.json
├── app
├── favicon.ico
├── api
│ ├── auth
│ │ ├── [...nextauth]
│ │ │ └── route.ts
│ │ ├── cleanup-tokens
│ │ │ └── route.ts
│ │ ├── revoke-token
│ │ │ └── route.ts
│ │ ├── manual-signout
│ │ │ └── route.ts
│ │ ├── check-admin
│ │ │ └── route.ts
│ │ └── logout
│ │ │ └── route.ts
│ ├── test
│ │ └── route.ts
│ ├── categories
│ │ └── route.ts
│ ├── leaderboard
│ │ └── route.ts
│ ├── admin
│ │ ├── badge-templates
│ │ │ ├── toggle-status
│ │ │ │ └── route.ts
│ │ │ ├── delete
│ │ │ │ └── route.ts
│ │ │ ├── create
│ │ │ │ └── route.ts
│ │ │ ├── update
│ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ ├── users
│ │ │ └── route.ts
│ │ ├── dashboard-stats
│ │ │ └── route.ts
│ │ └── upload-badge-image
│ │ │ └── route.ts
│ ├── chat
│ │ └── stats
│ │ │ └── route.ts
│ ├── user
│ │ ├── recent-solved
│ │ │ └── route.ts
│ │ └── [username]
│ │ │ └── route.ts
│ ├── problems
│ │ ├── completed
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── blogs
│ │ ├── route.ts
│ │ └── [id]
│ │ │ └── route.ts
│ └── forgeacademy
│ │ └── route.ts
├── loading.tsx
├── (main)
│ ├── layout.tsx
│ ├── authentication
│ │ └── page.tsx
│ └── unauthorized
│ │ └── page.tsx
├── page.tsx
├── not-found.tsx
├── error.tsx
├── globals.css
└── layout.tsx
├── makefile
├── .eslintrc.json
├── postcss.config.js
├── lib
├── utils.ts
├── authOptions.ts
└── tokenBlacklist.ts
├── next-sitemap.config.js
├── providers
└── auth-provider.tsx
├── components
├── AuthWrapper.tsx
├── loading.tsx
├── authError.tsx
├── FilterSidebar.tsx
├── QustionCards.tsx
├── ui
│ ├── accordion.tsx
│ └── sheet.tsx
├── CategoryButton.tsx
└── ResourceCard.tsx
├── components.json
├── types
├── next-auth.d.ts
├── assignImage.ts
├── assignBadge.ts
└── badgeImage.ts
├── models
├── badgeTemplateSchema.ts
├── userQuestionSchema.ts
├── badgeTemplate.ts
├── tokenBlacklistSchema.ts
├── Resource.ts
├── badgeImage.ts
├── qustionsSchema.ts
├── AssignedBadge.ts
└── userSchema.ts
├── .gitignore
├── tsconfig.json
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── middleware.ts
├── context
└── ThemeContext.tsx
├── utils
├── db.ts
├── discordNotifier.ts
├── ctfDifficultyCalculator.ts
├── data.ts
└── auth.ts
├── bug_report.md
├── package.json
├── middleware
├── tokenBlacklist.ts
└── adminToken.ts
├── next.config.mjs
├── interfaces
└── index.ts
├── tailwind.config.ts
├── HALL-OF-FAME.md
├── README.md
├── SECURITY.md
└── CODE_OF_CONDUCT.md
/public/ed63a00f-2193-49d1-b0e9-76a4a1a5a17c.txt:
--------------------------------------------------------------------------------
1 | Probely
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "kiroAgent.configureMCP": "Disabled"
3 | }
--------------------------------------------------------------------------------
/public/ads.txt:
--------------------------------------------------------------------------------
1 | google.com, pub-2506540900080142, DIRECT, f08c47fec0942fa0
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
4 | Sitemap: https://flagforge.xyz/sitemap.xml
--------------------------------------------------------------------------------
/public/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/404.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/logo.png
--------------------------------------------------------------------------------
/public/doubt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/doubt.png
--------------------------------------------------------------------------------
/public/logo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/logo1.png
--------------------------------------------------------------------------------
/public/aichatbot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/aichatbot.png
--------------------------------------------------------------------------------
/public/flagforge.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/flagforge.gif
--------------------------------------------------------------------------------
/public/aichatbot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/aichatbot2.png
--------------------------------------------------------------------------------
/public/badges/0x1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/0x1.png
--------------------------------------------------------------------------------
/public/badges/0x2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/0x2.png
--------------------------------------------------------------------------------
/public/badges/0x3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/0x3.png
--------------------------------------------------------------------------------
/public/badges/0x4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/0x4.png
--------------------------------------------------------------------------------
/public/badges/0x5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/0x5.png
--------------------------------------------------------------------------------
/public/badges/0x6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/0x6.png
--------------------------------------------------------------------------------
/public/badges/0x7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/0x7.png
--------------------------------------------------------------------------------
/public/NirmalDahal.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/NirmalDahal.jpeg
--------------------------------------------------------------------------------
/public/SobitThakuri.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/SobitThakuri.jpeg
--------------------------------------------------------------------------------
/public/flagforge-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/flagforge-logo.png
--------------------------------------------------------------------------------
/public/server-working.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/server-working.jpg
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "next/typescript"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/public/badges/bughunter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/bughunter.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/badges/securityresearcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/securityresearcher.png
--------------------------------------------------------------------------------
/public/server-support-header-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/server-support-header-image.png
--------------------------------------------------------------------------------
/public/badges/custom/badge-1758177518062-zx3ywujqht.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/custom/badge-1758177518062-zx3ywujqht.png
--------------------------------------------------------------------------------
/public/badges/images/badge-1758563324750-h7zfukwxw7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/images/badge-1758563324750-h7zfukwxw7.png
--------------------------------------------------------------------------------
/public/badges/images/badge-1758563431839-63u7vxws5u.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/images/badge-1758563431839-63u7vxws5u.png
--------------------------------------------------------------------------------
/public/badges/custom/badge-1758177272322-9bloahyyzvl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/custom/badge-1758177272322-9bloahyyzvl.png
--------------------------------------------------------------------------------
/public/badges/custom/badge-1758178350831-vnaxnz7ifrg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/custom/badge-1758178350831-vnaxnz7ifrg.png
--------------------------------------------------------------------------------
/public/badges/custom/badge-1758178601608-6z426myfiat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/custom/badge-1758178601608-6z426myfiat.png
--------------------------------------------------------------------------------
/public/badges/custom/badge-1758264629375-0y4fdhdxjy5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlagForgeCTF/flagForge/HEAD/public/badges/custom/badge-1758264629375-0y4fdhdxjy5.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # *
2 | User-agent: *
3 | Allow: /
4 |
5 | # Host
6 | Host: https://flagforge.xyz
7 |
8 | # Sitemaps
9 | Sitemap: https://flagforge.xyz/sitemap.xml
10 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://flagforge.xyz/sitemap-0.xml
4 |
--------------------------------------------------------------------------------
/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next-sitemap').IConfig} */
2 | module.exports = {
3 | siteUrl: 'https://flagforge.xyz',
4 | generateRobotsTxt: true,
5 | changefreq: 'monthly',
6 | priority: 0.7,
7 | };
8 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import { authOptions } from "@/lib/authOptions";
3 |
4 | const handler = NextAuth(authOptions);
5 |
6 | export const GET = handler;
7 | export const POST = handler;
8 |
--------------------------------------------------------------------------------
/app/loading.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Loading from "@/components/loading";
3 | import React from "react";
4 |
5 | const loading = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default loading;
14 |
--------------------------------------------------------------------------------
/app/api/test/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 |
3 | export async function GET(req: NextRequest) {
4 | return NextResponse.json({
5 | success: true,
6 | message: "API is working!",
7 | timestamp: new Date().toISOString(),
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/app/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Analytics } from '@vercel/analytics/next';
3 | const layout = ({ children }: { children: React.ReactNode }) => {
4 | return ;
8 | };
9 |
10 | export default layout;
11 |
--------------------------------------------------------------------------------
/providers/auth-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { ReactNode } from "react";
3 | import { SessionProvider } from "next-auth/react";
4 | import { AuthProviderProps } from "@/interfaces";
5 |
6 | const Authprovider: React.FC = ({ children }) => {
7 | return {children} ;
8 | };
9 |
10 | export default Authprovider;
11 |
--------------------------------------------------------------------------------
/components/AuthWrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useSession } from "next-auth/react";
3 | import Loading from "@/components/loading";
4 | import AuthError from "@/components/authError";
5 |
6 | const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
7 | const { status } = useSession();
8 |
9 | if (status === "loading") return ;
10 | if (status === "unauthenticated") return ;
11 | return <>{children}>;
12 | };
13 |
14 | export default AuthWrapper;
15 |
--------------------------------------------------------------------------------
/app/api/auth/cleanup-tokens/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { TokenBlacklistService } from '@/lib/tokenBlacklist';
3 |
4 | export async function POST() {
5 | try {
6 | await TokenBlacklistService.cleanupExpired();
7 | return NextResponse.json({ message: 'Cleanup completed successfully' });
8 | } catch (error) {
9 | console.error('Cleanup error:', error);
10 | return NextResponse.json(
11 | { error: 'Cleanup failed' },
12 | { status: 500 }
13 | );
14 | }
15 | }
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/components/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Triangle } from "react-loader-spinner"; // Using Triangle loader
3 | import Navbar from "./Navbar";
4 |
5 | const Loading = () => {
6 | return (
7 |
8 |
16 |
17 | );
18 | };
19 |
20 | export default Loading;
21 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { DefaultUser, DefaultSession, DefaultJWT } from "next-auth";
2 |
3 | declare module "next-auth" {
4 | interface Session {
5 | user: {
6 | id: string;
7 | totalScore?: number;
8 | role?: string;
9 | } & DefaultSession["user"];
10 | }
11 |
12 | interface User extends DefaultUser {
13 | id: string;
14 | totalScore?: number;
15 | role?: string;
16 | }
17 |
18 | interface JWT extends DefaultJWT {
19 | id: string;
20 | totalScore?: number;
21 | role?: string;
22 | jti?: string;
23 | iat?: number;
24 | exp?: number;
25 | }
26 | }
--------------------------------------------------------------------------------
/models/badgeTemplateSchema.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema, model, models } from "mongoose";
2 |
3 | const badgeTemplateSchema = new Schema({
4 | name: { type: String, required: true, trim: true, unique: true },
5 | description: { type: String, required: true, trim: true },
6 | icon: { type: String, required: true },
7 | color: { type: String, default: "#8B5CF6" },
8 | isActive: { type: Boolean, default: true },
9 | createdBy: { type: String, required: true },
10 | }, {
11 | timestamps: true,
12 | });
13 |
14 | const BadgeTemplate = models.BadgeTemplate || model("BadgeTemplate", badgeTemplateSchema);
15 | export default BadgeTemplate;
16 |
--------------------------------------------------------------------------------
/public/.well-known/security.txt:
--------------------------------------------------------------------------------
1 | Contact: mailto:contact@flagforge.xyz
2 | Contact: https://github.com/flagforge/flagForge1/issues
3 | Expires: 2025-12-31T23:59:59.000Z
4 | Encryption: https://keys.openpgp.org/search?q=contact@flagforge.xyz
5 | Preferred-Languages: en
6 | Canonical: https://flagforge.aryan4.com.np/security.txt
7 | Policy: https://github.com/FlagForgeCTF/flagForge/blob/mainv2/SECURITY.md
8 | Hiring: https://github.com/FlagForgeCTF/flagForge/blob/mainv2/CONTRIBUTING.md
9 | Acknowledgments: https://github.com/FlagForgeCTF/flagForge#contributors
10 | Hall-of-Frame:https://github.com/FlagForgeCTF/flagForge/blob/mainv2/HALL-OF-FRAME.md
--------------------------------------------------------------------------------
/models/userQuestionSchema.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema, model } from "mongoose";
2 | import { UserQuestion } from "@/interfaces";
3 |
4 |
5 | const userQuestionSchema = new Schema(
6 | {
7 | userId: {
8 | type: Schema.Types.ObjectId,
9 | ref: "User",
10 | required: true,
11 | },
12 | questionId: {
13 | type: Schema.Types.ObjectId,
14 | ref: "Question",
15 | required: true,
16 | },
17 | },
18 | { timestamps: true }
19 | );
20 |
21 | const UserQuestionModel =
22 | mongoose.models.UserQuestion || model("UserQuestion", userQuestionSchema);
23 |
24 | export default UserQuestionModel;
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env
30 | .env*.local
31 | .vscode
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 | package-lock.json
39 |
40 | k6-test.js
41 |
42 | bun.lockb
43 | remove-duplicate.js
44 | recalculate.js
45 | sync-badges.js
46 | userroleupdate.js
47 | reset-user-point.js
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useSession } from "next-auth/react";
4 | import { useRouter } from "next/navigation";
5 | import { useEffect,useState } from "react";
6 | import Hero from "@/components/Hero";
7 | import Loading from "@/components/loading";
8 |
9 | export default function Home() {
10 | const { data: session, status } = useSession();
11 | const router = useRouter();
12 |
13 | useEffect(() => {
14 | if (status === "authenticated") {
15 | router.replace("/home");
16 | }
17 | }, [status, router]);
18 |
19 | if (status === "loading") {
20 | return (
21 |
22 | );
23 | }
24 |
25 | if (status === "unauthenticated") {
26 | return (
27 |
28 |
29 |
30 | );
31 | }
32 | return null;
33 | }
--------------------------------------------------------------------------------
/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 | import image from '@/public/404.png'
4 | export default function NotFound() {
5 | return (
6 |
7 |
8 |
9 | OOPS! Page Not Found! 😥
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/models/badgeTemplate.ts:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const badgeTemplateSchema = new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true,
7 | trim: true,
8 | unique: true
9 | },
10 | description: {
11 | type: String,
12 | required: true,
13 | trim: true
14 | },
15 | icon: {
16 | type: String,
17 | required: true,
18 | trim: true
19 | },
20 | color: {
21 | type: String,
22 | default: '#8B5CF6'
23 | },
24 | isActive: {
25 | type: Boolean,
26 | default: true
27 | },
28 | createdBy: {
29 | type: String,
30 | default: 'unknown'
31 | }
32 | }, {
33 | timestamps: true // This automatically adds createdAt and updatedAt
34 | });
35 |
36 | // Prevent re-compilation during development
37 | const BadgeTemplate = mongoose.models.BadgeTemplate || mongoose.model('BadgeTemplate', badgeTemplateSchema);
38 |
39 | export default BadgeTemplate;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "react-jsx",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./*"
27 | ]
28 | },
29 | "target": "ES2017"
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts",
36 | "components/ProfileHeroBanner.tsx",
37 | ".next/dev/types/**/*.ts"
38 | , "next-sitemap.config.js" ],
39 | "exclude": [
40 | "node_modules"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/app/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useEffect } from "react";
3 | import Image from "next/image";
4 | import image from "@/public/404.png";
5 |
6 |
7 | export default function Error({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | useEffect(() => {
15 | }, [error]);
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | Something went wrong!! 😥
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Thank you for suggesting a feature to improve **Flag Forge**! Please provide
4 | as much detail as possible to help us understand your idea.
5 | title: 'Feature Request: [Short Description]'
6 | labels: enhancement
7 | assignees: Chief-spartan-117
8 |
9 | ---
10 |
11 | ## Is your feature request related to a problem? Please describe.
12 | A clear and concise description of what the problem is.
13 | Example: *I'm always frustrated when [...]*
14 |
15 | ---
16 |
17 | ## Describe the solution you'd like
18 | A clear and concise description of what you want to happen.
19 |
20 | ---
21 |
22 | ## Describe alternatives you've considered
23 | A clear and concise description of any alternative solutions or features you've considered.
24 |
25 | ---
26 |
27 | ## Additional Context
28 | Add any other context about the feature request here. This could include:
29 | - Screenshots
30 | - Examples from other tools or projects
31 | - Reasons why this feature is important
32 |
--------------------------------------------------------------------------------
/models/tokenBlacklistSchema.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Document, Schema } from "mongoose";
2 |
3 | interface ITokenBlacklist extends Document {
4 | jti: string; // JWT ID
5 | userId?: string;
6 | expiresAt: Date;
7 | blacklistedAt: Date;
8 | }
9 |
10 | const TokenBlacklistSchema = new Schema({
11 | jti: {
12 | type: String,
13 | required: true,
14 | unique: true,
15 | index: true
16 | },
17 | userId: {
18 | type: String,
19 | required: false
20 | },
21 | expiresAt: {
22 | type: Date,
23 | required: true,
24 | // Auto-delete documents after they expire + grace period (3 months)
25 | expires: 60 * 60 * 24 * 90 // 90 days in seconds
26 | },
27 | blacklistedAt: {
28 | type: Date,
29 | default: Date.now
30 | }
31 | });
32 |
33 | // Compound index for efficient queries
34 | TokenBlacklistSchema.index({ jti: 1, expiresAt: 1 });
35 |
36 | const TokenBlacklistModel = mongoose.models.TokenBlacklist ||
37 | mongoose.model("TokenBlacklist", TokenBlacklistSchema);
38 |
39 | export default TokenBlacklistModel;
--------------------------------------------------------------------------------
/models/Resource.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema, model } from "mongoose";
2 |
3 | export interface Resource {
4 | _id?: string;
5 | title: string;
6 | description: string;
7 | category: string;
8 | resourceLink: string;
9 | uploadedBy: string;
10 | createdAt?: Date;
11 | updatedAt?: Date;
12 | }
13 |
14 | const resourceSchema = new Schema(
15 | {
16 | title: {
17 | type: String,
18 | required: true,
19 | },
20 | description: {
21 | type: String,
22 | required: true,
23 | },
24 | category: {
25 | type: String,
26 | required: true,
27 | },
28 | resourceLink: {
29 | type: String,
30 | required: true,
31 | },
32 | uploadedBy: {
33 | type: String,
34 | required: true,
35 | }
36 | },
37 | { timestamps: true }
38 | );
39 |
40 | // Add indexes for better performance
41 | resourceSchema.index({ category: 1, title: 1 });
42 | resourceSchema.index({ createdAt: -1 });
43 |
44 | const ResourceModel = mongoose.models.Resource || model("Resource", resourceSchema);
45 |
46 | export default ResourceModel;
--------------------------------------------------------------------------------
/types/assignImage.ts:
--------------------------------------------------------------------------------
1 | import { Document, Types } from 'mongoose';
2 |
3 | export interface AssignedBadge {
4 | _id?: string | Types.ObjectId;
5 | userId: string;
6 | badgeId: string | Types.ObjectId;
7 | badgeType: 'template' | 'custom';
8 | assignedBy: string;
9 | assignedAt: Date;
10 | reason?: string;
11 | isActive: boolean;
12 | }
13 |
14 | export interface AssignBadgeRequest {
15 | userId: string;
16 | badgeId: string;
17 | badgeType?: 'template' | 'custom';
18 | reason?: string;
19 | assignedBy: string;
20 | }
21 |
22 | export interface AssignedBadgeDocument extends Omit, Document {
23 | _id: Types.ObjectId;
24 | }
25 |
26 | // Response types
27 | export interface AssignBadgeSuccessResponse {
28 | success: true;
29 | message: string;
30 | assignmentId: string;
31 | badge: {
32 | id: string;
33 | name: string;
34 | type: string;
35 | };
36 | }
37 |
38 | export interface AssignBadgeErrorResponse {
39 | success: false;
40 | error: string;
41 | details?: string;
42 | }
43 |
44 | export type AssignBadgeResponse = AssignBadgeSuccessResponse | AssignBadgeErrorResponse;
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { tokenBlacklistMiddleware } from './middleware/tokenBlacklist';
3 | import { adminMiddleware } from './middleware/adminToken';
4 |
5 | export async function middleware(request: NextRequest) {
6 | const { pathname } = request.nextUrl;
7 |
8 | const blacklistResponse = await tokenBlacklistMiddleware(request);
9 | if (blacklistResponse && blacklistResponse instanceof NextResponse) return blacklistResponse;
10 |
11 | if (
12 | pathname.startsWith('/api/admin') ||
13 | pathname.startsWith('/roles/developers/admins') ||
14 | pathname.startsWith('/api/badges') ||
15 | pathname.startsWith('/api/badge-templates') ||
16 | pathname.startsWith('/resources/upload')
17 | ) {
18 | const adminResponse = await adminMiddleware(request);
19 | if (adminResponse && adminResponse instanceof NextResponse) return adminResponse;
20 | }
21 |
22 | // 3) Continue if all checks passed
23 | return NextResponse.next();
24 | }
25 |
26 | export const config = {
27 | matcher: [
28 | '/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
29 | ],
30 | };
31 |
--------------------------------------------------------------------------------
/components/authError.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import image from "@/public/404.png";
4 | const AuthError = () => {
5 | return (
6 |
7 |
8 | {/*
*/}
9 |
10 |
11 |
12 | Access Denied!
13 |
14 |
15 | Sign In To Enter This Page!
16 | 😥
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default AuthError;
29 |
--------------------------------------------------------------------------------
/app/api/categories/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import QuestionModel from "@/models/qustionsSchema";
4 |
5 | export const runtime = "nodejs";
6 |
7 | export async function GET() {
8 | try {
9 | await connect();
10 |
11 | // Get distinct categories from the database
12 | const categories = await QuestionModel.distinct("category");
13 |
14 | // Filter out null/undefined categories, trim whitespace, and remove duplicates
15 | const validCategories = [
16 | ...new Set(
17 | categories
18 | .filter(
19 | (category) =>
20 | category && typeof category === "string" && category.trim() !== ""
21 | )
22 | .map((category) => category.trim())
23 | .sort((a, b) =>
24 | a.localeCompare(b, undefined, { sensitivity: "base" })
25 | )
26 | ),
27 | ];
28 |
29 | const categoriesWithAll = ["All", ...validCategories];
30 |
31 | return NextResponse.json({
32 | categories: categoriesWithAll,
33 | });
34 | } catch (error) {
35 | console.error("Error fetching categories:", error);
36 | return NextResponse.json(
37 | { message: "Internal server error", categories: ["All"] },
38 | { status: 500 }
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/models/badgeImage.ts:
--------------------------------------------------------------------------------
1 | // models/BadgeImage.ts
2 | import mongoose from 'mongoose';
3 | import { BadgeImageDocument } from '@/types/badgeImage';
4 |
5 | const badgeImageSchema = new mongoose.Schema({
6 | name: {
7 | type: String,
8 | required: true,
9 | trim: true
10 | },
11 | originalName: {
12 | type: String,
13 | required: true
14 | },
15 | filename: {
16 | type: String,
17 | required: true,
18 | unique: true
19 | },
20 | path: {
21 | type: String,
22 | required: true
23 | },
24 | category: {
25 | type: String,
26 | required: true,
27 | default: 'badge-template'
28 | },
29 | size: {
30 | type: Number,
31 | required: true
32 | },
33 | mimeType: {
34 | type: String,
35 | required: true
36 | },
37 | uploadedBy: {
38 | type: String,
39 | default: 'unknown'
40 | }
41 | }, {
42 | timestamps: {
43 | createdAt: 'uploadedAt',
44 | updatedAt: true
45 | }
46 | });
47 |
48 | // Create indexes for better query performance
49 | badgeImageSchema.index({ category: 1, uploadedAt: -1 });
50 | badgeImageSchema.index({ filename: 1 }, { unique: true });
51 |
52 | // Prevent re-compilation during development
53 | const BadgeImageModel = mongoose.models.BadgeImage || mongoose.model('BadgeImage', badgeImageSchema);
54 |
55 | export default BadgeImageModel;
--------------------------------------------------------------------------------
/types/assignBadge.ts:
--------------------------------------------------------------------------------
1 | // types/assignBadge.ts
2 | import { Document, Types } from 'mongoose';
3 |
4 | export interface AssignedBadge {
5 | _id?: string | Types.ObjectId;
6 | userId: string;
7 | badgeId: string | Types.ObjectId;
8 | badgeType: 'template' | 'custom';
9 | assignedBy: string;
10 | assignedAt: Date;
11 | reason?: string;
12 | isActive: boolean;
13 | badgeName: string;
14 | badgeDescription?: string;
15 | badgeIcon?: string;
16 | badgeColor?: string;
17 | }
18 |
19 | export interface AssignBadgeRequest {
20 | userId: string;
21 | badgeId?: string;
22 | badgeType?: 'template' | 'custom';
23 | reason?: string;
24 | assignedBy?: string; // Now optional
25 | badge?: {
26 | name: string;
27 | description: string;
28 | icon: string;
29 | color: string;
30 | assignedBy?: string;
31 | assignedAt: string;
32 | };
33 | }
34 |
35 | export interface AssignedBadgeDocument extends Omit, Document {
36 | _id: Types.ObjectId;
37 | }
38 |
39 | // Response types
40 | export interface AssignBadgeSuccessResponse {
41 | success: true;
42 | message: string;
43 | assignmentId: string;
44 | badge: {
45 | id: string;
46 | name: string;
47 | type: string;
48 | };
49 | }
50 |
51 | export interface AssignBadgeErrorResponse {
52 | success: false;
53 | error: string;
54 | details?: string;
55 | }
56 |
57 | export type AssignBadgeResponse = AssignBadgeSuccessResponse | AssignBadgeErrorResponse;
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Thank you for taking the time to help improve **Flag Forge** by reporting a
4 | bug! Please provide as much detail as possible to help us address the issue efficiently.
5 | title: 'Bug: [Short Description]'
6 | labels: bug
7 | assignees: aryan4859
8 |
9 | ---
10 |
11 | ## Describe the Bug
12 | A clear and concise description of what the bug is.
13 |
14 | ---
15 |
16 | ## To Reproduce
17 | Steps to reproduce the behavior:
18 | 1. Go to '...'
19 | 2. Click on '...'
20 | 3. Scroll down to '...'
21 | 4. See error
22 |
23 | ---
24 |
25 | ## Expected Behavior
26 | A clear and concise description of what you expected to happen.
27 |
28 | ---
29 |
30 | ## Screenshots
31 | If applicable, add screenshots to help explain your problem.
32 |
33 | ---
34 |
35 | ## Desktop (please complete the following information):
36 | - **OS**: [e.g., Windows, macOS, Linux]
37 | - **Browser**: [e.g., Chrome, Firefox, Safari]
38 | - **Version**: [e.g., 22]
39 |
40 | ---
41 |
42 | ## Smartphone (please complete the following information):
43 | - **Device**: [e.g., iPhone X, Samsung Galaxy S10]
44 | - **OS**: [e.g., iOS 15, Android 11]
45 | - **Browser**: [e.g., Safari, Chrome]
46 | - **Version**: [e.g., 22]
47 |
48 | ---
49 |
50 | ## Additional Context
51 | Add any other context about the problem here. This could include:
52 | - Error logs
53 | - Relevant links
54 | - Steps attempted to fix the issue
55 |
56 | ---
57 |
--------------------------------------------------------------------------------
/context/ThemeContext.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { createContext, useContext, useEffect, useState } from "react";
4 |
5 | type Theme = "light" | "dark";
6 |
7 | interface ThemeContextType {
8 | theme: Theme;
9 | toggleTheme: () => void;
10 | }
11 |
12 | const ThemeContext = createContext(undefined);
13 |
14 | export function ThemeProvider({ children }: { children: React.ReactNode }) {
15 | const [theme, setTheme] = useState("light");
16 |
17 | useEffect(() => {
18 | const storedTheme = localStorage.getItem("flagforge-theme") as Theme;
19 | if (storedTheme) {
20 | setTheme(storedTheme);
21 | } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
22 | setTheme("dark");
23 | }
24 | }, []);
25 |
26 | useEffect(() => {
27 | if (theme === "dark") {
28 | document.documentElement.classList.add("dark");
29 | } else {
30 | document.documentElement.classList.remove("dark");
31 | }
32 | localStorage.setItem("flagforge-theme", theme);
33 | }, [theme]);
34 |
35 | const toggleTheme = () => {
36 | setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
37 | };
38 |
39 | return (
40 |
41 | {children}
42 |
43 | );
44 | }
45 |
46 | export function useTheme() {
47 | const context = useContext(ThemeContext);
48 | if (context === undefined) {
49 | throw new Error("useTheme must be used within a ThemeProvider");
50 | }
51 | return context;
52 | }
53 |
--------------------------------------------------------------------------------
/utils/db.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const MONGO_URL = process.env.MONGO_URL!;
4 | if (!MONGO_URL) {
5 | throw new Error("❌ Please define MONGO_URL in .env");
6 | }
7 |
8 | // Global cached connection for Next.js hot reloads
9 | interface MongooseCache {
10 | conn: typeof mongoose | null;
11 | promise: Promise | null;
12 | }
13 |
14 | declare global {
15 | // eslint-disable-next-line no-var
16 | var mongooseCache: MongooseCache;
17 | var sigintListenerAdded: boolean;
18 | }
19 |
20 | const cached: MongooseCache = global.mongooseCache || { conn: null, promise: null };
21 |
22 | async function connect() {
23 | if (cached.conn) return cached.conn;
24 |
25 | if (!cached.promise) {
26 | cached.promise = mongoose.connect(MONGO_URL, {
27 | maxPoolSize: 5,
28 | minPoolSize: 1,
29 | maxIdleTimeMS: 30000,
30 | bufferCommands: false,
31 | }).then(m => m);
32 | }
33 |
34 | try {
35 | cached.conn = await cached.promise;
36 | console.log("✅ MongoDB connected");
37 | } catch (error) {
38 | cached.promise = null;
39 | throw new Error("❌ Error connecting to MongoDB: " + (error as Error).message);
40 | }
41 |
42 | // Add SIGINT listener only once
43 | if (!global.sigintListenerAdded) {
44 | process.on("SIGINT", async () => {
45 | await mongoose.connection.close();
46 | console.log("💤 MongoDB disconnected on app termination");
47 | process.exit(0);
48 | });
49 | global.sigintListenerAdded = true;
50 | }
51 |
52 | global.mongooseCache = cached;
53 |
54 | return cached.conn;
55 | }
56 |
57 | export default connect;
58 |
--------------------------------------------------------------------------------
/components/FilterSidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { IoFilter } from "react-icons/io5";
3 |
4 | export default function FilterSidebar() {
5 | return (
6 |
7 |
8 |
9 |
10 | Filters
11 |
12 |
13 |
14 |
15 |
16 | Topics
17 |
18 |
19 | {[
20 | "All",
21 | "Web Exploitation",
22 | "Cryptography",
23 | "Reverse Engineering",
24 | "Forensics",
25 | "General Skills",
26 | "Binary Exploitation",
27 | ].map((topic) => (
28 |
32 | {topic}
33 |
34 | ))}
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/api/leaderboard/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import User from "@/models/userSchema";
3 | import UserQuestionModel from "@/models/userQuestionSchema";
4 | import connect from "@/utils/db";
5 | export const runtime = "nodejs";
6 |
7 | // GET /api/leaderboard
8 | export async function GET() {
9 | try {
10 | // Connect to the database
11 | await connect();
12 |
13 | // Fetch top 50 users sorted by totalScore in descending order
14 | const users = await User.find({})
15 | .sort({ totalScore: -1 })
16 | .limit(50) // Limit to top 50 users
17 | .select("name totalScore image _id");
18 |
19 | // Calculate roomsCompleted for each user
20 | const leaderboardPromises = users.map(async (user, index) => {
21 | // Count completed questions for this user
22 | // Remove the completion filter for now until we debug it properly
23 | const roomsCompleted = await UserQuestionModel.countDocuments({
24 | userId: user._id,
25 | });
26 |
27 | return {
28 | name: user.name,
29 | totalScore: user.totalScore,
30 | image: user.image,
31 | roomsCompleted,
32 | rank: index + 1, // Rank starts from 1
33 | };
34 | });
35 |
36 | // Wait for all promises to resolve
37 | const leaderboard = await Promise.all(leaderboardPromises);
38 |
39 | // Return the leaderboard as JSON
40 | return NextResponse.json(leaderboard);
41 | } catch (error) {
42 | console.error("Leaderboard API Error:", error);
43 | return NextResponse.json(
44 | { error: "Failed to fetch leaderboard" },
45 | { status: 500 }
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/bug_report.md:
--------------------------------------------------------------------------------
1 | # Bug Report 🐞
2 |
3 | Thank you for taking the time to help improve **Flag Forge** by reporting a bug! Please provide as much detail as possible to help us address the issue efficiently.
4 |
5 | ---
6 |
7 | ## Describe the Bug
8 | A clear and concise description of what the bug is.
9 |
10 | ---
11 |
12 | ## To Reproduce
13 | Steps to reproduce the behavior:
14 | 1. Go to '...'
15 | 2. Click on '...'
16 | 3. Scroll down to '...'
17 | 4. See error
18 |
19 | ---
20 |
21 | ## Expected Behavior
22 | A clear and concise description of what you expected to happen.
23 |
24 | ---
25 |
26 | ## Screenshots
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | ---
30 |
31 | ## Desktop (please complete the following information):
32 | - **OS**: [e.g., Windows, macOS, Linux]
33 | - **Browser**: [e.g., Chrome, Firefox, Safari]
34 | - **Version**: [e.g., 22]
35 |
36 | ---
37 |
38 | ## Smartphone (please complete the following information):
39 | - **Device**: [e.g., iPhone X, Samsung Galaxy S10]
40 | - **OS**: [e.g., iOS 15, Android 11]
41 | - **Browser**: [e.g., Safari, Chrome]
42 | - **Version**: [e.g., 22]
43 |
44 | ---
45 |
46 | ## Additional Context
47 | Add any other context about the problem here. This could include:
48 | - Error logs
49 | - Relevant links
50 | - Steps attempted to fix the issue
51 |
52 | ---
53 |
54 | ## Optional Additional Items
55 | - **Default Issue Title**: `Bug: [Short Description]`
56 | - **Assignees**: (To be assigned by maintainers)
57 | - **Labels**: `bug`, `needs-triage`, or relevant categories
58 |
59 | ---
60 |
61 | Thank you for helping us improve **Flag Forge**! Your report is greatly appreciated. 🚀
62 |
--------------------------------------------------------------------------------
/utils/discordNotifier.ts:
--------------------------------------------------------------------------------
1 | const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;
2 |
3 | const COLORS = {
4 | NEW_CHALLENGE: 0x2ecc71, // green
5 | };
6 |
7 | export async function sendDiscordNotification(
8 | title: string,
9 | description: string,
10 | type: "NEW_CHALLENGE" = "NEW_CHALLENGE",
11 | points?: number,
12 | category?: string,
13 | link?: string
14 | ) {
15 | if (!DISCORD_WEBHOOK_URL) {
16 | console.error("❌ DISCORD_WEBHOOK_URL not set");
17 | return;
18 | }
19 |
20 | const payload = {
21 | embeds: [
22 | {
23 | title,
24 | description,
25 | color: COLORS[type],
26 | timestamp: new Date().toISOString(),
27 | footer: { text: "FlagForge System" },
28 | fields: [
29 | ...(category
30 | ? [{ name: "Category", value: category, inline: true }]
31 | : []),
32 | ...(points !== undefined
33 | ? [{ name: "Points", value: points.toString(), inline: true }]
34 | : []),
35 | ...(link
36 | ? [
37 | {
38 | name: "Challenge Link",
39 | value: `[Click here](${link})`,
40 | inline: false,
41 | },
42 | ]
43 | : []),
44 | ],
45 | },
46 | ],
47 | };
48 |
49 | try {
50 | const res = await fetch(DISCORD_WEBHOOK_URL, {
51 | method: "POST",
52 | headers: { "Content-Type": "application/json" },
53 | body: JSON.stringify(payload),
54 | });
55 |
56 | if (!res.ok)
57 | console.error(`❌ Failed to send Discord message: ${res.statusText}`);
58 | } catch (err) {
59 | console.error("⚠️ Error sending Discord message:", err);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/models/qustionsSchema.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema, model } from "mongoose";
2 | import { Questions } from "@/interfaces";
3 |
4 | const hintSchema = new Schema({
5 | text: {
6 | type: String,
7 | required: true,
8 | },
9 | pointsDeduction: {
10 | type: Number,
11 | required: true,
12 | min: 0,
13 | }
14 | }, { _id: true });
15 |
16 | const questionSchema = new Schema(
17 | {
18 | title: {
19 | type: String,
20 | required: true,
21 | },
22 | description: {
23 | type: String,
24 | required: true,
25 | },
26 | category: {
27 | type: String,
28 | required: true,
29 | },
30 | points: {
31 | type: Number,
32 | required: true,
33 | },
34 | flag: {
35 | type: String,
36 | required: true,
37 | },
38 | addilinks: {
39 | type: String,
40 | },
41 | link: {
42 | type: String,
43 | },
44 | done: {
45 | type: Boolean
46 | },
47 | hints: {
48 | type: [hintSchema],
49 | default: []
50 | },
51 | isTimeLimited: {
52 | type: Boolean,
53 | default: false
54 | },
55 | timeLimit: {
56 | type: Number,
57 | min: 1
58 | },
59 | timeLimitUnit: {
60 | type: String,
61 | enum: ['hours', 'days', 'weeks']
62 | },
63 | expiryDate: {
64 | type: Date,
65 | default: null
66 | },
67 | uploadedBy: {
68 | type: String,
69 | required: true
70 | }
71 | },
72 | { timestamps: true }
73 | );
74 |
75 | questionSchema.index({ expiryDate: 1 });
76 |
77 | questionSchema.index({ category: 1, points: 1 });
78 |
79 | const QuestionModel =
80 | mongoose.models.Question || model("Question", questionSchema);
81 |
82 | export default QuestionModel;
--------------------------------------------------------------------------------
/app/(main)/authentication/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect } from "react";
3 | import { signIn, useSession } from "next-auth/react";
4 | import { FcGoogle } from "react-icons/fc";
5 | import { useRouter } from "next/navigation";
6 |
7 | const AuthPage = () => {
8 | const router = useRouter();
9 | const { data: session, status: sessionStatus } = useSession();
10 |
11 | useEffect(() => {
12 | if (sessionStatus === "authenticated") {
13 | router.replace("/");
14 | }
15 | }, [sessionStatus, router]);
16 |
17 | return (
18 |
19 |
20 | Fly into{" "}
21 |
22 | {" "}
23 | FlagForge
24 |
25 | , where Challenges take Wings!🪽
26 |
27 | signIn("google")}
29 | className="flex items-center gap-[10px] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-md shadow-gray-100 dark:shadow-gray-900 px-8 py-4 text-sm font-medium text-gray-800 dark:text-gray-100
30 | hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:focus:ring-red-400
31 | transition-colors duration-300 ease-in-out"
32 | >
33 |
34 | Continue with Google
35 |
36 |
37 | );
38 | };
39 |
40 | export default AuthPage;
41 |
--------------------------------------------------------------------------------
/app/api/auth/revoke-token/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { getServerSession } from 'next-auth';
3 | import { authOptions } from '@/lib/authOptions';
4 | import { TokenBlacklistService } from '@/lib/tokenBlacklist';
5 |
6 | export async function POST(request: NextRequest) {
7 | try {
8 | const session = await getServerSession(authOptions);
9 |
10 | if (!session) {
11 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12 | }
13 |
14 | const { token, expiresAt } = await request.json();
15 |
16 | if (!token) {
17 | return NextResponse.json({ error: 'Token required' }, { status: 400 });
18 | }
19 |
20 | // Parse expiration date or use default (1 hour from now)
21 | const expiration = expiresAt
22 | ? new Date(expiresAt)
23 | : new Date(Date.now() + 60 * 60 * 1000);
24 |
25 | // Add to blacklist with proper arguments
26 | await TokenBlacklistService.addToBlacklist(
27 | token,
28 | expiration,
29 | session.user.id // userId from session
30 | );
31 |
32 | console.log('✅ Token revoked:', {
33 | userId: session.user.id,
34 | expiresAt: expiration.toISOString(),
35 | });
36 |
37 | return NextResponse.json({
38 | success: true,
39 | message: 'Token revoked successfully'
40 | });
41 | } catch (error) {
42 | console.error('❌ Token revocation error:', error);
43 |
44 | let message = 'Failed to revoke token';
45 | if (error instanceof Error) {
46 | message = error.message;
47 | }
48 |
49 | return NextResponse.json(
50 | {
51 | success: false,
52 | error: 'Failed to revoke token',
53 | details: message
54 | },
55 | { status: 500 }
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/models/AssignedBadge.ts:
--------------------------------------------------------------------------------
1 | // models/AssignedBadge.ts
2 | import mongoose from 'mongoose';
3 | import { AssignedBadgeDocument } from '@/types/assignBadge';
4 |
5 | const assignedBadgeSchema = new mongoose.Schema({
6 | userId: {
7 | type: String,
8 | required: true,
9 | index: true
10 | },
11 | badgeId: {
12 | type: mongoose.Schema.Types.Mixed,
13 | required: true,
14 | refPath: 'badgeType'
15 | },
16 | badgeType: {
17 | type: String,
18 | required: true,
19 | enum: ['template', 'custom'],
20 | default: 'template'
21 | },
22 | assignedBy: {
23 | type: String,
24 | required: true
25 | },
26 | reason: {
27 | type: String,
28 | trim: true
29 | },
30 | isActive: {
31 | type: Boolean,
32 | default: true
33 | },
34 | // Badge details for display purposes
35 | badgeName: {
36 | type: String,
37 | required: true
38 | },
39 | badgeDescription: {
40 | type: String,
41 | default: ''
42 | },
43 | badgeIcon: {
44 | type: String,
45 | default: ''
46 | },
47 | badgeColor: {
48 | type: String,
49 | default: '#000000'
50 | }
51 | }, {
52 | timestamps: {
53 | createdAt: 'assignedAt',
54 | updatedAt: true
55 | }
56 | });
57 |
58 | // Create compound index to prevent duplicate active assignments
59 | assignedBadgeSchema.index({ userId: 1, badgeId: 1, isActive: 1 }, { unique: true, partialFilterExpression: { isActive: true } });
60 |
61 | // Create indexes for better query performance
62 | assignedBadgeSchema.index({ userId: 1, isActive: 1 });
63 | assignedBadgeSchema.index({ assignedAt: -1 });
64 |
65 | // Prevent re-compilation during development
66 | const AssignedBadgeModel = mongoose.models.AssignedBadge || mongoose.model('AssignedBadge', assignedBadgeSchema);
67 |
68 | export default AssignedBadgeModel;
--------------------------------------------------------------------------------
/models/userSchema.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema, model, models } from "mongoose";
2 | import { Users } from "@/interfaces";
3 |
4 | const customBadgeSchema = new Schema({
5 | name: {
6 | type: String,
7 | required: true,
8 | trim: true,
9 | },
10 | description: {
11 | type: String,
12 | required: true,
13 | trim: true,
14 | },
15 | icon: {
16 | type: String,
17 | required: true,
18 | },
19 | color: {
20 | type: String,
21 | required: true,
22 | default: "from-gray-400 to-gray-600", // Kept for potential styling
23 | },
24 | assignedAt: {
25 | type: Date,
26 | default: Date.now,
27 | },
28 | assignedBy: {
29 | type: String,
30 | required: true,
31 | },
32 | }, {
33 | timestamps: false,
34 | });
35 |
36 | // Define User Schema
37 | const userSchema = new Schema(
38 | {
39 | name: {
40 | type: String,
41 | required: true,
42 | trim: true,
43 | },
44 | email: {
45 | type: String,
46 | required: true,
47 | unique: true,
48 | lowercase: true,
49 | trim: true,
50 | },
51 | image: {
52 | type: String,
53 | default: "",
54 | },
55 | role: {
56 | type: String,
57 | default: "User",
58 | enum: ["User", "Admin"],
59 | },
60 | totalScore: {
61 | type: Number,
62 | default: 0,
63 | min: 0,
64 | },
65 | customBadges: {
66 | type: [customBadgeSchema],
67 | default: [],
68 | },
69 | },
70 | {
71 | timestamps: true,
72 | }
73 | );
74 |
75 | userSchema.methods.getBadgeCount = function() {
76 | return this.customBadges.length;
77 | };
78 |
79 | userSchema.statics.findUsersWithBadge = function(badgeName: string) {
80 | return this.find({ "customBadges.name": badgeName });
81 | };
82 |
83 | const User = models.User || model("User", userSchema);
84 | export default User;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flagforge",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postbuild": "next-sitemap"
11 | },
12 | "dependencies": {
13 | "@notionhq/client": "^4.0.2",
14 | "@radix-ui/react-accordion": "^1.2.2",
15 | "@radix-ui/react-dialog": "^1.1.4",
16 | "@radix-ui/react-dropdown-menu": "^2.1.4",
17 | "@types/jsonwebtoken": "^9.0.10",
18 | "@types/react-syntax-highlighter": "^15.5.13",
19 | "@upstash/redis": "^1.35.5",
20 | "@vercel/analytics": "^1.5.0",
21 | "axios": "^1.6.7",
22 | "caniuse-lite": "^1.0.30001692",
23 | "class-variance-authority": "^0.7.1",
24 | "clsx": "^2.1.1",
25 | "jsonwebtoken": "^9.0.2",
26 | "lucide-react": "^0.545.0",
27 | "mongoose": "^8.1.3",
28 | "next": "^16.0.5",
29 | "next-auth": "^4.24.6",
30 | "next-sitemap": "^4.2.3",
31 | "next-theme": "^0.1.5",
32 | "next-videos": "^1.4.1",
33 | "notion-to-md": "^3.1.9",
34 | "p5": "^1.11.2",
35 | "react": "^18",
36 | "react-confetti-boom": "^1.1.2",
37 | "react-dom": "^18",
38 | "react-icons": "^5.0.1",
39 | "react-loader-spinner": "^6.1.6",
40 | "react-markdown": "^10.1.0",
41 | "react-syntax-highlighter": "^16.1.0",
42 | "rehype-highlight": "^7.0.2",
43 | "rehype-raw": "^7.0.0",
44 | "rehype-sanitize": "^6.0.0",
45 | "remark-breaks": "^4.0.0",
46 | "remark-gfm": "^4.0.1",
47 | "sharp": "^0.34.3",
48 | "tailwind-merge": "^2.6.0",
49 | "tailwindcss-animate": "^1.0.7"
50 | },
51 | "devDependencies": {
52 | "@types/node": "^20",
53 | "@types/p5": "^1.7.6",
54 | "@types/react": "^18",
55 | "@types/react-dom": "^18",
56 | "autoprefixer": "^10.0.1",
57 | "postcss": "^8",
58 | "tailwindcss": "^3.3.0",
59 | "typescript": "^5"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/middleware/tokenBlacklist.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import type { NextRequest } from 'next/server';
3 | import { TokenBlacklistService } from '@/lib/tokenBlacklist';
4 |
5 | export async function tokenBlacklistMiddleware(request: NextRequest) {
6 | const { pathname } = request.nextUrl;
7 |
8 | // Skip middleware for NextAuth API routes and static files
9 | if (
10 | pathname.startsWith('/api/auth') ||
11 | pathname.startsWith('/_next') ||
12 | pathname.startsWith('/favicon.ico')
13 | ) {
14 | return null; // Let it pass through
15 | }
16 |
17 | try {
18 | // Get the session token cookie
19 | const sessionToken = request.cookies.get('next-auth.session-token')?.value ||
20 | request.cookies.get('__Secure-next-auth.session-token')?.value;
21 |
22 | // If there's no session token, let NextAuth handle it
23 | if (!sessionToken) {
24 | return null;
25 | }
26 |
27 | // Check if token is blacklisted
28 | const isBlacklisted = await TokenBlacklistService.isBlacklisted(sessionToken);
29 |
30 | if (isBlacklisted) {
31 | console.log('🚫 Blacklisted token detected, clearing session');
32 |
33 | // Clear all auth cookies and redirect
34 | const response = NextResponse.redirect(new URL('/authentication', request.url));
35 | response.cookies.delete('next-auth.session-token');
36 | response.cookies.delete('__Secure-next-auth.session-token');
37 | response.cookies.delete('next-auth.csrf-token');
38 | response.cookies.delete('__Secure-next-auth.csrf-token');
39 | response.cookies.delete('next-auth.callback-url');
40 | response.cookies.delete('__Secure-next-auth.callback-url');
41 |
42 | return response;
43 | }
44 |
45 | return null;
46 |
47 | } catch (error) {
48 | console.error('❌ Token blacklist middleware error:', error);
49 | return null;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/types/badgeImage.ts:
--------------------------------------------------------------------------------
1 | import { Document, Types } from 'mongoose';
2 |
3 | export interface BadgeImage {
4 | _id?: string | Types.ObjectId;
5 | name: string;
6 | originalName: string;
7 | filename: string;
8 | path: string;
9 | category: string;
10 | size: number;
11 | mimeType: string;
12 | uploadedAt: Date;
13 | uploadedBy: string;
14 | }
15 |
16 | // For creating new badge images (without Mongoose-specific fields)
17 | export interface BadgeImageCreate {
18 | name: string;
19 | originalName: string;
20 | filename: string;
21 | path: string;
22 | category: string;
23 | size: number;
24 | mimeType: string;
25 | uploadedAt: Date;
26 | uploadedBy: string;
27 | }
28 |
29 | // For Mongoose document type - properly extends Document without _id conflict
30 | export interface BadgeImageDocument extends Omit, Document {
31 | _id: Types.ObjectId;
32 | }
33 |
34 | // Response types
35 | export interface BadgeImageListResponse {
36 | success: true;
37 | images: BadgeImage[];
38 | count: number;
39 | }
40 |
41 | export interface BadgeImageUploadResponse {
42 | success: true;
43 | imagePath: string;
44 | filename: string;
45 | imageId: string;
46 | message: string;
47 | }
48 |
49 | export interface BadgeImageDeleteResponse {
50 | success: true;
51 | message: string;
52 | }
53 |
54 | export interface BadgeImageErrorResponse {
55 | success: false;
56 | error: string;
57 | details?: string;
58 | }
59 |
60 | export type BadgeImageResponse =
61 | | BadgeImageListResponse
62 | | BadgeImageUploadResponse
63 | | BadgeImageDeleteResponse
64 | | BadgeImageErrorResponse;
65 |
66 | export const ALLOWED_IMAGE_TYPES = [
67 | 'image/png',
68 | 'image/jpeg',
69 | 'image/jpg',
70 | 'image/webp',
71 | 'image/svg+xml',
72 | 'image/gif'
73 | ] as const;
74 |
75 | export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
76 |
77 | export type AllowedImageType = typeof ALLOWED_IMAGE_TYPES[number];
--------------------------------------------------------------------------------
/public/badges/bounty.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* Root theme variables */
6 | @layer base {
7 | :root {
8 | --background: 0 0% 100%;
9 | --foreground: 0 0% 3.9%;
10 | --card: 0 0% 100%;
11 | --card-foreground: 0 0% 3.9%;
12 | --popover: 0 0% 100%;
13 | --popover-foreground: 0 0% 3.9%;
14 | --primary: 0 0% 9%;
15 | --primary-foreground: 0 0% 98%;
16 | --secondary: 0 0% 96.1%;
17 | --secondary-foreground: 0 0% 9%;
18 | --muted: 0 0% 96.1%;
19 | --muted-foreground: 0 0% 45.1%;
20 | --accent: 0 0% 96.1%;
21 | --accent-foreground: 0 0% 9%;
22 | --destructive: 0 84.2% 60.2%;
23 | --destructive-foreground: 0 0% 98%;
24 | --border: 0 0% 89.8%;
25 | --input: 0 0% 89.8%;
26 | --ring: 0 0% 3.9%;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | --radius: 0.5rem;
33 | }
34 |
35 | .dark {
36 | --background: 0 0% 3.9%;
37 | --foreground: 0 0% 98%;
38 | --card: 0 0% 3.9%;
39 | --card-foreground: 0 0% 98%;
40 | --popover: 0 0% 3.9%;
41 | --popover-foreground: 0 0% 98%;
42 | --primary: 0 0% 98%;
43 | --primary-foreground: 0 0% 9%;
44 | --secondary: 0 0% 14.9%;
45 | --secondary-foreground: 0 0% 98%;
46 | --muted: 0 0% 14.9%;
47 | --muted-foreground: 0 0% 63.9%;
48 | --accent: 0 0% 14.9%;
49 | --accent-foreground: 0 0% 98%;
50 | --destructive: 0 62.8% 30.6%;
51 | --destructive-foreground: 0 0% 98%;
52 | --border: 0 0% 14.9%;
53 | --input: 0 0% 14.9%;
54 | --ring: 0 0% 83.1%;
55 | --chart-1: 220 70% 50%;
56 | --chart-2: 160 60% 45%;
57 | --chart-3: 30 80% 55%;
58 | --chart-4: 280 65% 60%;
59 | --chart-5: 340 75% 55%;
60 | }
61 |
62 | * {
63 | @apply border-border;
64 | }
65 |
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 |
70 | /* Custom dark mode styles */
71 | .dark .logo-flame {
72 | filter: brightness(1.2) saturate(1.2);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ['writeup.flagforge.xyz'],
5 | remotePatterns: [
6 | {
7 | protocol: "https",
8 | hostname: "lh3.googleusercontent.com",
9 | },
10 | {
11 | protocol: "https",
12 | hostname: "prod-files-secure.s3.us-west-2.amazonaws.com",
13 | },
14 | ],
15 | },
16 | async headers() {
17 | return [
18 | {
19 | source: "/(.*)",
20 | headers: [
21 | {
22 | key: "Access-Control-Allow-Origin",
23 | value: "https://flagforge.xyz",
24 | },
25 | {
26 | key: "X-Frame-Options",
27 | value: "DENY",
28 | },
29 | /*
30 | {
31 | key: "Content-Security-Policy",
32 | value:
33 | "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' https://lh3.googleusercontent.com;",
34 | },
35 | */
36 | {
37 | key: "X-Content-Type-Options",
38 | value: "nosniff",
39 | },
40 | {
41 | key: "Strict-Transport-Security",
42 | value: "max-age=31536000; includeSubDomains; preload",
43 | },
44 | {
45 | key: "Referrer-Policy",
46 | value: "no-referrer",
47 | },
48 | {
49 | key: "Permissions-Policy",
50 | value: "geolocation=(), microphone=(), camera=(), payment=()",
51 | },
52 | {
53 | key: "Cache-Control",
54 | value: "no-store, no-cache, must-revalidate, proxy-revalidate",
55 | },
56 | {
57 | key: "Pragma",
58 | value: "no-cache",
59 | },
60 | {
61 | key: "Server",
62 | value: "",
63 | },
64 | {
65 | key: "X-XSS-Protection",
66 | value: "1; mode=block",
67 | },
68 | ],
69 | },
70 | ];
71 | },
72 | };
73 |
74 | export default nextConfig;
75 |
--------------------------------------------------------------------------------
/utils/ctfDifficultyCalculator.ts:
--------------------------------------------------------------------------------
1 | type DifficultyFactors = {
2 | depth: number;
3 | knowledge: number;
4 | steps: number;
5 | environment: number;
6 | obfuscation: number;
7 | tools: number;
8 | };
9 |
10 | type DifficultyResult = {
11 | difficultyScore: number;
12 | label: "Easy" | "Medium" | "Hard" | "Insane";
13 | points: number;
14 | };
15 |
16 | /**
17 | * Calculate challenge difficulty and points (max 250)
18 | * @param factors Difficulty factors (0–5 each)
19 | * @param continuous Optional: continuous scaling (true = smooth 50–250)
20 | */
21 | export function calculateDifficulty(
22 | factors: DifficultyFactors,
23 | continuous: boolean = false
24 | ): DifficultyResult {
25 | const { depth, knowledge, steps, environment, obfuscation, tools } = factors;
26 |
27 | const difficultyScore =
28 | ((depth + knowledge + steps + environment + obfuscation + tools) / 30) *
29 | 100;
30 |
31 | let label: DifficultyResult["label"];
32 | let points: number;
33 |
34 | if (continuous) {
35 | // Linear scale between 50 and 250
36 | points = Math.round(50 + (difficultyScore / 100) * 200);
37 | } else {
38 | if (difficultyScore <= 25) {
39 | label = "Easy";
40 | points = 75;
41 | } else if (difficultyScore <= 50) {
42 | label = "Medium";
43 | points = 150;
44 | } else if (difficultyScore <= 75) {
45 | label = "Hard";
46 | points = 210;
47 | } else {
48 | label = "Insane";
49 | points = 250;
50 | }
51 | return { difficultyScore: Math.round(difficultyScore), label, points };
52 | }
53 |
54 | if (difficultyScore <= 25) label = "Easy";
55 | else if (difficultyScore <= 50) label = "Medium";
56 | else if (difficultyScore <= 75) label = "Hard";
57 | else label = "Insane";
58 |
59 | return { difficultyScore: Math.round(difficultyScore), label, points };
60 | }
61 |
62 | // Example test
63 | if (require.main === module) {
64 | console.log(
65 | calculateDifficulty(
66 | {
67 | depth: 4,
68 | knowledge: 5,
69 | steps: 4,
70 | environment: 3,
71 | obfuscation: 4,
72 | tools: 2,
73 | },
74 | true
75 | )
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/app/api/admin/badge-templates/toggle-status/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import BadgeTemplate from "@/models/badgeTemplateSchema";
4 | import mongoose from "mongoose";
5 |
6 | export const runtime = "nodejs";
7 |
8 | export async function PATCH(request: NextRequest) {
9 | try {
10 | await connect();
11 |
12 | const body = await request.json();
13 | const { templateId, isActive } = body;
14 |
15 | // 1️⃣ Validate templateId
16 | if (!templateId) {
17 | return NextResponse.json(
18 | { error: "Template ID is required" },
19 | { status: 400 }
20 | );
21 | }
22 |
23 | if (!mongoose.Types.ObjectId.isValid(templateId)) {
24 | return NextResponse.json(
25 | { error: "Invalid template ID format" },
26 | { status: 400 }
27 | );
28 | }
29 |
30 | // 2️⃣ Validate isActive
31 | if (isActive === undefined || isActive === null) {
32 | return NextResponse.json(
33 | { error: "isActive status is required" },
34 | { status: 400 }
35 | );
36 | }
37 |
38 | // 3️⃣ Find the template
39 | const template = await BadgeTemplate.findById(templateId);
40 | if (!template) {
41 | return NextResponse.json(
42 | { error: "Badge template not found" },
43 | { status: 404 }
44 | );
45 | }
46 |
47 | // 4️⃣ Update the active status
48 | template.isActive = Boolean(isActive);
49 | template.updatedAt = new Date();
50 | await template.save();
51 |
52 | console.log(
53 | `✅ Badge template status updated: ${template.name} - ${
54 | isActive ? "activated" : "deactivated"
55 | }`
56 | );
57 |
58 | // 5️⃣ Return response
59 | return NextResponse.json({
60 | success: true,
61 | template,
62 | message: `Badge template ${
63 | isActive ? "activated" : "deactivated"
64 | } successfully`,
65 | });
66 | } catch (error) {
67 | console.error("❌ Badge template status update error:", error);
68 | return NextResponse.json(
69 | { error: "Failed to update badge template status" },
70 | { status: 500 }
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/components/QustionCards.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Questions } from "@/interfaces";
3 | import Link from "next/link";
4 | import { Check } from "lucide-react";
5 |
6 | const QuestionCards = ({
7 | title,
8 | description,
9 | category,
10 | points,
11 | done,
12 | _id,
13 | }: Questions) => {
14 | const isDone = done.some(
15 | (item: { questionId: string | undefined }) => item.questionId === _id
16 | );
17 |
18 | return (
19 |
27 |
28 |
29 | {title}
30 |
31 |
32 |
33 | Points : {points}
34 |
35 |
36 |
37 | {category}
38 |
39 |
40 |
41 |
42 |
43 | {description}...{" "}
44 | more
45 |
46 |
47 | );
48 | };
49 |
50 | export default QuestionCards;
51 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
59 |
--------------------------------------------------------------------------------
/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | import { Types } from "mongoose";
2 | import { HTMLProps, ReactNode } from "react";
3 |
4 | export interface NavbarItems {
5 | href: string;
6 | tags: string;
7 | onClick?: () => void;
8 | style: HTMLProps["className"];
9 | }
10 |
11 | export interface GoogleProviderConfig {
12 | clientId: string;
13 | clientSecret: string;
14 | }
15 |
16 | export interface AuthProviderProps {
17 | children: ReactNode;
18 | }
19 |
20 | export interface Users {
21 | email: string;
22 | image?: string;
23 | name?: string;
24 | customBadges?:string;
25 | questionsDone?: string[];
26 | totalScore?: number;
27 | role?: string;
28 | }
29 |
30 | export interface Hint {
31 | _id?: string;
32 | text: string;
33 | content:string;
34 | pointsDeduction: number;
35 | description:string;
36 | }
37 |
38 | export interface Questions {
39 | title: string;
40 | description: string;
41 | category: string;
42 | points: number;
43 | flag?: string;
44 | isSolved?: boolean;
45 | addilinks?: string;
46 | done: any;
47 | _id?: string;
48 | link?: string;
49 | hints?: Hint[];
50 | isTimeLimited?: boolean;
51 | timeLimit?: number;
52 | timeLimitUnit?: 'hours' | 'days' | 'weeks';
53 | expiryDate?: string | Date | null;
54 | uploadedBy?: string;
55 | createdAt?: string | Date;
56 | updatedAt?: string | Date;
57 | isExpired?: boolean;
58 | timeRemaining?: number;
59 | }
60 |
61 | export interface UserQuestion {
62 | userId: Types.ObjectId;
63 | questionId: Types.ObjectId;
64 | scoredPoint: number;
65 | _id?: string;
66 | solvedAt?: Date;
67 | pointsEarned?: number;
68 | hintsUsed?: string[];
69 | createdAt?: Date;
70 | updatedAt?: Date;
71 | }
72 |
73 | export interface HintUsage {
74 | _id?: string;
75 | userId: Types.ObjectId;
76 | questionId: Types.ObjectId;
77 | hintId: string;
78 | usedAt: Date;
79 | pointsDeducted: number;
80 | }
81 |
82 | export interface ProblemApiResponse {
83 | question: Questions;
84 | isDone: boolean;
85 | expired?: boolean;
86 | timeRemaining?: number | null;
87 | }
88 |
89 | export interface SubmissionResponse {
90 | success: boolean;
91 | message: string;
92 | pointsEarned?: number;
93 | totalScore?: number;
94 | }
--------------------------------------------------------------------------------
/app/api/admin/badge-templates/delete/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import BadgeTemplate from "@/models/badgeTemplateSchema";
4 | import User from "@/models/userSchema";
5 | import mongoose from "mongoose";
6 |
7 | export const runtime = "nodejs";
8 |
9 | export async function DELETE(request: NextRequest) {
10 | try {
11 | await connect();
12 |
13 | const body = await request.json();
14 | const { templateId } = body;
15 |
16 | if (!templateId) {
17 | return NextResponse.json(
18 | { error: "Template ID is required" },
19 | { status: 400 }
20 | );
21 | }
22 |
23 | if (!mongoose.Types.ObjectId.isValid(templateId)) {
24 | return NextResponse.json(
25 | { error: "Invalid template ID format" },
26 | { status: 400 }
27 | );
28 | }
29 |
30 | // Check if template exists
31 | const template = await BadgeTemplate.findById(templateId);
32 | if (!template) {
33 | return NextResponse.json(
34 | { error: "Badge template not found" },
35 | { status: 404 }
36 | );
37 | }
38 |
39 | // Check if template is being used in any assigned badges
40 | const usageCount = await User.countDocuments({
41 | "customBadges.name": template.name,
42 | });
43 |
44 | if (usageCount > 0) {
45 | return NextResponse.json(
46 | {
47 | error: `Cannot delete template. It is currently used by ${usageCount} assigned badge(s).`,
48 | inUse: true,
49 | usageCount,
50 | },
51 | { status: 400 }
52 | );
53 | }
54 |
55 | // Delete the template
56 | await template.deleteOne();
57 |
58 | console.log(`Badge template deleted: ${template.name}`);
59 |
60 | return NextResponse.json({
61 | success: true,
62 | deletedTemplate: {
63 | id: template._id,
64 | name: template.name,
65 | },
66 | message: "Badge template deleted successfully",
67 | });
68 | } catch (error) {
69 | console.error("Badge template deletion error:", error);
70 | return NextResponse.json(
71 | { error: "Failed to delete badge template" },
72 | { status: 500 }
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/api/auth/manual-signout/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { getToken } from 'next-auth/jwt';
3 | import { TokenBlacklistService } from '@/lib/tokenBlacklist';
4 |
5 | export async function POST(request: NextRequest) {
6 | try {
7 | // Get the current session token
8 | const token = await getToken({
9 | req: request,
10 | secret: process.env.NEXTAUTH_SECRET
11 | });
12 |
13 | const sessionToken = request.cookies.get('next-auth.session-token')?.value ||
14 | request.cookies.get('__Secure-next-auth.session-token')?.value;
15 |
16 | if (token && sessionToken) {
17 | // Calculate expiration date from token
18 | const tokenExp = token.exp as number | undefined;
19 | const expiresAt = tokenExp
20 | ? new Date(tokenExp * 1000)
21 | : new Date(Date.now() + 60 * 60 * 1000); // Default 1 hour
22 |
23 | // Blacklist the token with proper arguments
24 | await TokenBlacklistService.addToBlacklist(
25 | sessionToken,
26 | expiresAt,
27 | token.sub // userId (optional)
28 | );
29 |
30 | console.log('✅ Token blacklisted during manual signout:', {
31 | userId: token.sub,
32 | expiresAt: expiresAt.toISOString(),
33 | });
34 | }
35 |
36 | // Clear all auth cookies
37 | const response = NextResponse.json({
38 | success: true,
39 | message: 'Signed out successfully'
40 | });
41 |
42 | response.cookies.delete('next-auth.session-token');
43 | response.cookies.delete('__Secure-next-auth.session-token');
44 | response.cookies.delete('next-auth.csrf-token');
45 | response.cookies.delete('__Secure-next-auth.csrf-token');
46 | response.cookies.delete('next-auth.callback-url');
47 | response.cookies.delete('__Secure-next-auth.callback-url');
48 |
49 | return response;
50 | } catch (error) {
51 | console.error('❌ Manual signout error:', error);
52 |
53 | let message = 'Signout failed';
54 | if (error instanceof Error) {
55 | message = error.message;
56 | }
57 |
58 | return NextResponse.json(
59 | {
60 | success: false,
61 | error: 'Signout failed',
62 | details: message
63 | },
64 | { status: 500 }
65 | );
66 | }
67 | }
--------------------------------------------------------------------------------
/app/api/admin/badge-templates/create/route.ts:
--------------------------------------------------------------------------------
1 | // File: /api/admin/badge-templates/create/route.ts
2 | import { NextRequest, NextResponse } from "next/server";
3 | import connect from "@/utils/db";
4 | import BadgeTemplate from "@/models/badgeTemplateSchema";
5 |
6 | export const runtime = "nodejs";
7 |
8 | export async function POST(request: NextRequest) {
9 | try {
10 | await connect(); // connect to MongoDB
11 |
12 | const body = await request.json();
13 | const { name, description, icon, color, isActive, createdBy } = body;
14 |
15 | // Validation
16 | if (!name?.trim())
17 | return NextResponse.json(
18 | { error: "Badge name is required" },
19 | { status: 400 }
20 | );
21 | if (!description?.trim())
22 | return NextResponse.json(
23 | { error: "Badge description is required" },
24 | { status: 400 }
25 | );
26 | if (!icon?.trim())
27 | return NextResponse.json(
28 | { error: "Badge icon is required" },
29 | { status: 400 }
30 | );
31 | if (!createdBy?.trim())
32 | return NextResponse.json(
33 | { error: "CreatedBy is required" },
34 | { status: 400 }
35 | );
36 |
37 | // Check for duplicate name
38 | const existing = await BadgeTemplate.findOne({ name: name.trim() });
39 | if (existing)
40 | return NextResponse.json(
41 | { error: "A badge template with this name already exists" },
42 | { status: 400 }
43 | );
44 |
45 | // Create new template
46 | const newTemplate = new BadgeTemplate({
47 | name: name.trim(),
48 | description: description.trim(),
49 | icon: icon.trim(),
50 | color: color || "#8B5CF6",
51 | isActive: isActive !== undefined ? isActive : true,
52 | createdBy: createdBy.trim(),
53 | });
54 |
55 | await newTemplate.save();
56 |
57 | console.log(`Badge template created: ${name} by ${createdBy}`);
58 |
59 | return NextResponse.json({
60 | success: true,
61 | template: newTemplate,
62 | message: "Badge template created successfully",
63 | });
64 | } catch (error) {
65 | console.error("Badge template creation error:", error);
66 | return NextResponse.json(
67 | { error: "Failed to create badge template" },
68 | { status: 500 }
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/middleware/adminToken.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import type { NextRequest } from 'next/server';
3 | import { getToken } from 'next-auth/jwt';
4 |
5 | const protectedRoutes = [
6 | '/api/admin',
7 | '/roles/developers/admins',
8 | '/api/badges',
9 | '/api/badge-templates',
10 | '/resources/upload',
11 | ];
12 |
13 | const adminOnlyRoutes = [
14 | '/api/admin',
15 | '/roles/developers/admins',
16 | '/api/badges',
17 | '/api/badge-templates',
18 | '/resources/upload',
19 | ];
20 |
21 | export async function adminMiddleware(request: NextRequest) {
22 | const { pathname } = request.nextUrl;
23 |
24 | const isProtectedRoute = protectedRoutes.some(route =>
25 | pathname.startsWith(route)
26 | );
27 |
28 | if (!isProtectedRoute) {
29 | return NextResponse.next();
30 | }
31 |
32 | const token = await getToken({
33 | req: request,
34 | secret: process.env.NEXTAUTH_SECRET,
35 | });
36 |
37 | console.log("🧭 Admin Middleware Check:", {
38 | pathname,
39 | hasToken: !!token,
40 | tokenKeys: token ? Object.keys(token) : [],
41 | role: token?.role,
42 | email: token?.email,
43 | });
44 |
45 | if (!token) {
46 | const url = new URL('/authentication', request.url);
47 | url.searchParams.set('callbackUrl', pathname);
48 | return NextResponse.redirect(url);
49 | }
50 |
51 | const isAdminRoute = adminOnlyRoutes.some(route =>
52 | pathname.startsWith(route)
53 | );
54 |
55 | if (isAdminRoute) {
56 | const role = (token.role as string | undefined) || 'User';
57 | const isAdmin = role === 'Admin';
58 |
59 | console.log("🔐 Authorization Check:", {
60 | pathname,
61 | role,
62 | isAdmin,
63 | requiredRole: 'Admin',
64 | });
65 |
66 | if (!isAdmin) {
67 | if (pathname.startsWith('/api/')) {
68 | return NextResponse.json(
69 | {
70 | error: 'Forbidden',
71 | message: 'Admin privileges required',
72 | isAdmin: false,
73 | },
74 | { status: 403 }
75 | );
76 | }
77 | return NextResponse.redirect(new URL('/unauthorized', request.url));
78 | }
79 | }
80 |
81 | const requestHeaders = new Headers(request.headers);
82 | requestHeaders.set('x-user-id', token.sub || '');
83 | requestHeaders.set('x-user-role', (token.role as string) || 'User');
84 | requestHeaders.set('x-user-email', token.email || '');
85 |
86 | return NextResponse.next({
87 | request: {
88 | headers: requestHeaders,
89 | },
90 | });
91 | }
92 |
--------------------------------------------------------------------------------
/app/api/chat/stats/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import userSchema from "@/models/userSchema";
4 | import type { NextRequest } from "next/server";
5 | import { getServerSession } from "next-auth";
6 | import { authOptions } from "@/lib/authOptions";
7 | import mongoose from "mongoose";
8 |
9 | const chatHintSchema = new mongoose.Schema({
10 | userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
11 | questionId: {
12 | type: mongoose.Schema.Types.ObjectId,
13 | ref: "Question",
14 | required: true,
15 | },
16 | hintRequests: [
17 | {
18 | timestamp: { type: Date, default: Date.now },
19 | message: String,
20 | hintLevel: String,
21 | pointsDeducted: { type: Number, default: 0 },
22 | },
23 | ],
24 | totalPointsDeducted: { type: Number, default: 0 },
25 | createdAt: { type: Date, default: Date.now },
26 | updatedAt: { type: Date, default: Date.now },
27 | });
28 |
29 | chatHintSchema.index({ userId: 1, questionId: 1 }, { unique: true });
30 |
31 | const ChatHintModel =
32 | mongoose.models.ChatHint || mongoose.model("ChatHint", chatHintSchema);
33 |
34 | export async function GET(req: NextRequest) {
35 | try {
36 | const { searchParams } = new URL(req.url);
37 | const challengeId = searchParams.get("challengeId");
38 |
39 | if (!challengeId) {
40 | return NextResponse.json(
41 | { message: "Challenge ID is required" },
42 | { status: 400 }
43 | );
44 | }
45 |
46 | const session = await getServerSession(authOptions);
47 | if (!session?.user?.email) {
48 | return NextResponse.json(
49 | { message: "Unauthorized" },
50 | { status: 401 }
51 | );
52 | }
53 |
54 | await connect();
55 |
56 | const user = await userSchema.findOne({ email: session.user.email });
57 | if (!user) {
58 | return NextResponse.json(
59 | { message: "User not found" },
60 | { status: 404 }
61 | );
62 | }
63 |
64 | const chatHint = await ChatHintModel.findOne({
65 | userId: user._id,
66 | questionId: challengeId,
67 | });
68 |
69 | return NextResponse.json({
70 | totalPointsDeducted: chatHint?.totalPointsDeducted || 0,
71 | totalHintsUsed: chatHint?.hintRequests?.length || 0,
72 | });
73 | } catch (error) {
74 | console.error("Error fetching chat stats:", error);
75 | return NextResponse.json(
76 | { message: "Internal server error" },
77 | { status: 500 }
78 | );
79 | }
80 | }
--------------------------------------------------------------------------------
/utils/data.ts:
--------------------------------------------------------------------------------
1 | import { NavbarItems, Questions } from "@/interfaces";
2 |
3 | export const NavbarData: NavbarItems[] = [
4 | {
5 | href: "/home",
6 | tags: "Home",
7 | style: undefined
8 | },
9 | {
10 | href: "/problems",
11 | tags: "Problems",
12 | style: undefined
13 | },
14 | {
15 | href: "/leaderboard",
16 | tags: "Leaderboard",
17 | style: undefined
18 | },
19 | {
20 | href: "/blogs",
21 | tags: "Blogs",
22 | style: undefined
23 | },
24 | ];
25 |
26 |
27 |
28 | const ctfQuestions: Questions[] = [
29 | {
30 | title: "Reverse Engineering",
31 | description:
32 | "Reverse engineer the provided binary and find the hidden flag.",
33 | category: "Binary Exploitation",
34 | points: 300,
35 | flag: "REVERSE_FLAG",
36 | isSolved: false,
37 | done: false
38 | },
39 | {
40 | title: "Web Exploitation",
41 | description: "Find the vulnerability in the provided web to retrieve the flag.",
42 | category: "Web Security",
43 | points: 200,
44 | flag: "WEB_FLAG",
45 | isSolved: false,
46 | done: false
47 | },
48 | {
49 | title: "Forensics",
50 | description: "Analyze the provided image file to extract the hidden flag.",
51 | category: "Digital Forensics",
52 | points: 150,
53 | flag: "FORENSICS_FLAG",
54 | isSolved: false,
55 | done: false
56 | },
57 | {
58 | title: "Crypto Challenge",
59 | description: "Decrypt the provided ciphertext to reveal the flag.",
60 | category: "Cryptography",
61 | points: 250,
62 | flag: "CRYPTO_FLAG",
63 | isSolved: false,
64 | done: false
65 | },
66 | {
67 | title: "Steganography",
68 | description: "Identify and extract the hidden message from the provided image.",
69 | category: "Steganography",
70 | points: 200,
71 | flag: "STEGANOGRAPHY_FLAG",
72 | isSolved: false,
73 | done: false
74 | },
75 | {
76 | title: "Network Security",
77 | description: "Capture and analyze the network traffic to find the flag.",
78 | category: "Network Security",
79 | points: 300,
80 | flag: "NETWORK_FLAG",
81 | isSolved: false,
82 | done: false
83 | },
84 | ];
85 |
86 |
87 | const dummyQuestions: Questions[] = ctfQuestions;
88 |
89 |
90 | export default dummyQuestions;
91 |
92 |
93 | export const initialQuestion: Questions = {
94 | title: "",
95 | description: "",
96 | category: "",
97 | points: 0,
98 | flag: "",
99 | isSolved: false,
100 | done: false
101 | };
--------------------------------------------------------------------------------
/app/api/admin/users/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import { getServerSession } from "next-auth";
4 | import { authOptions } from "@/lib/authOptions";
5 | import UserSchema from "@/models/userSchema";
6 | import UserQuestionModel from "@/models/userQuestionSchema";
7 |
8 | export const runtime = "nodejs";
9 |
10 | // Helper function to create error responses
11 | function createErrorResponse(message: string, status: number) {
12 | return NextResponse.json({ success: false, message }, { status });
13 | }
14 |
15 | async function isAdmin(email: string): Promise {
16 | try {
17 | await connect();
18 |
19 | const adminUser = await UserSchema.findOne({
20 | email: email,
21 | role: "Admin",
22 | }).lean();
23 |
24 | return !!adminUser;
25 | } catch (error) {
26 | console.error("Error checking admin status:", error);
27 | return false;
28 | }
29 | }
30 |
31 | async function getUserCompletionStats(user: any) {
32 | try {
33 | const completedCount = await UserQuestionModel.countDocuments({
34 | userId: user._id,
35 | });
36 |
37 | return {
38 | ...user,
39 | completedQuestions: completedCount,
40 | customBadges: user.customBadges || [],
41 | };
42 | } catch (error) {
43 | console.error(`Error getting stats for user ${user._id}:`, error);
44 | return {
45 | ...user,
46 | completedQuestions: 0,
47 | customBadges: user.customBadges || [],
48 | };
49 | }
50 | }
51 |
52 | export async function GET(req: NextRequest) {
53 | try {
54 | await connect();
55 |
56 | const session = await getServerSession(authOptions);
57 | if (!session || !session.user?.email) {
58 | return createErrorResponse("Unauthorized", 401);
59 | }
60 |
61 | // Check if user is admin (now queries database)
62 | if (!(await isAdmin(session.user.email))) {
63 | return createErrorResponse(
64 | "Access denied. Admin privileges required.",
65 | 403
66 | );
67 | }
68 |
69 | const users = await UserSchema.find({})
70 | .select("name email image totalScore customBadges createdAt")
71 | .sort({ totalScore: -1 })
72 | .lean();
73 |
74 | const usersWithStats = await Promise.all(users.map(getUserCompletionStats));
75 |
76 | return NextResponse.json({
77 | success: true,
78 | users: usersWithStats,
79 | total: usersWithStats.length,
80 | });
81 | } catch (error) {
82 | console.error("Error fetching users:", error);
83 | return createErrorResponse("Failed to fetch users", 500);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/utils/auth.ts:
--------------------------------------------------------------------------------
1 | // auth.ts
2 | import { signOut as nextAuthSignOut } from "next-auth/react";
3 |
4 | export const signOut = async () => {
5 | try {
6 | // Clear any client-side storage that might contain sensitive data
7 | if (typeof window !== 'undefined') {
8 | localStorage.clear();
9 | sessionStorage.clear();
10 |
11 | // Clear any custom cookies you might have set
12 | document.cookie.split(";").forEach((c) => {
13 | const eqPos = c.indexOf("=");
14 | const name = eqPos > -1 ? c.substr(0, eqPos) : c;
15 | document.cookie = `${name.trim()}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${window.location.hostname}`;
16 | });
17 | }
18 |
19 | // Call NextAuth signOut with proper options
20 | await nextAuthSignOut({
21 | callbackUrl: "/",
22 | redirect: true
23 | });
24 |
25 | // Force a hard refresh to clear any cached data
26 | if (typeof window !== 'undefined') {
27 | window.location.href = "/";
28 | }
29 | } catch (error) {
30 | console.error('SignOut error:', error);
31 | // Even if signOut fails, redirect to home to prevent session persistence
32 | if (typeof window !== 'undefined') {
33 | window.location.href = "/";
34 | }
35 | }
36 | };
37 |
38 | // Alternative version with more control
39 | export const secureSignOut = async (redirectTo: string = "/") => {
40 | try {
41 | // Optional: Call a custom API endpoint for server-side cleanup
42 | const response = await fetch('/api/auth/logout', {
43 | method: 'POST',
44 | headers: {
45 | 'Content-Type': 'application/json',
46 | },
47 | credentials: 'include'
48 | });
49 |
50 | if (!response.ok) {
51 | console.warn('Server-side logout failed, continuing with client-side logout');
52 | }
53 |
54 | // Clear client-side data
55 | if (typeof window !== 'undefined') {
56 | localStorage.clear();
57 | sessionStorage.clear();
58 | }
59 |
60 | // NextAuth signOut
61 | await nextAuthSignOut({
62 | callbackUrl: redirectTo,
63 | redirect: false // We'll handle redirect manually for better control
64 | });
65 |
66 | // Manual redirect with cache busting
67 | if (typeof window !== 'undefined') {
68 | // Add cache busting parameter
69 | const separator = redirectTo.includes('?') ? '&' : '?';
70 | window.location.href = `${redirectTo}${separator}_t=${Date.now()}`;
71 | }
72 | } catch (error) {
73 | console.error('Secure signOut error:', error);
74 | // Force redirect even if logout fails
75 | if (typeof window !== 'undefined') {
76 | window.location.href = redirectTo;
77 | }
78 | }
79 | };
--------------------------------------------------------------------------------
/app/(main)/unauthorized/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRouter } from 'next/navigation';
4 | import { signOut } from 'next-auth/react';
5 |
6 | export default function UnauthorizedPage() {
7 | const router = useRouter();
8 |
9 | const handleSignOut = async () => {
10 | await signOut({ redirect: false });
11 | router.push('/auth');
12 | };
13 |
14 | return (
15 |
16 |
17 |
18 |
33 |
34 | Access Denied
35 |
36 |
37 | You don't have permission to access this page.
38 |
39 |
40 |
41 |
42 |
43 | Admin privileges required.
44 | Contact your system administrator to request access.
45 |
46 |
47 |
48 |
49 | router.push('/')}
51 | className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition duration-200"
52 | >
53 | Go to Homepage
54 |
55 |
56 |
60 | Sign Out
61 |
62 |
63 |
64 |
65 | );
66 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"], // enable class-based dark mode
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | backgroundImage: {
13 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
14 | "gradient-conic":
15 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
16 | },
17 | borderRadius: {
18 | lg: "var(--radius)",
19 | md: "calc(var(--radius) - 2px)",
20 | sm: "calc(var(--radius) - 4px)",
21 | },
22 | colors: {
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | card: {
26 | DEFAULT: "hsl(var(--card))",
27 | foreground: "hsl(var(--card-foreground))",
28 | },
29 | popover: {
30 | DEFAULT: "hsl(var(--popover))",
31 | foreground: "hsl(var(--popover-foreground))",
32 | },
33 | primary: {
34 | DEFAULT: "hsl(var(--primary))",
35 | foreground: "hsl(var(--primary-foreground))",
36 | },
37 | secondary: {
38 | DEFAULT: "hsl(var(--secondary))",
39 | foreground: "hsl(var(--secondary-foreground))",
40 | },
41 | muted: {
42 | DEFAULT: "hsl(var(--muted))",
43 | foreground: "hsl(var(--muted-foreground))",
44 | },
45 | accent: {
46 | DEFAULT: "hsl(var(--accent))",
47 | foreground: "hsl(var(--accent-foreground))",
48 | },
49 | destructive: {
50 | DEFAULT: "hsl(var(--destructive))",
51 | foreground: "hsl(var(--destructive-foreground))",
52 | },
53 | border: "hsl(var(--border))",
54 | input: "hsl(var(--input))",
55 | ring: "hsl(var(--ring))",
56 | chart: {
57 | "1": "hsl(var(--chart-1))",
58 | "2": "hsl(var(--chart-2))",
59 | "3": "hsl(var(--chart-3))",
60 | "4": "hsl(var(--chart-4))",
61 | "5": "hsl(var(--chart-5))",
62 | },
63 | },
64 | keyframes: {
65 | "accordion-down": {
66 | from: { height: "0" },
67 | to: { height: "var(--radix-accordion-content-height)" },
68 | },
69 | "accordion-up": {
70 | from: { height: "var(--radix-accordion-content-height)" },
71 | to: { height: "0" },
72 | },
73 | },
74 | animation: {
75 | "accordion-down": "accordion-down 0.2s ease-out",
76 | "accordion-up": "accordion-up 0.2s ease-out",
77 | },
78 | },
79 | },
80 | plugins: [require("tailwindcss-animate")],
81 | };
82 |
83 | export default config;
84 |
--------------------------------------------------------------------------------
/app/api/admin/badge-templates/update/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import BadgeTemplate from "@/models/badgeTemplateSchema";
4 | import mongoose from "mongoose";
5 |
6 | export const runtime = "nodejs";
7 |
8 | export async function PUT(request: NextRequest) {
9 | try {
10 | await connect();
11 |
12 | const body = await request.json();
13 | const { templateId, name, description, icon, color, isActive } = body;
14 |
15 | // 1️⃣ Validate templateId
16 | if (!templateId) {
17 | return NextResponse.json(
18 | { error: "Template ID is required" },
19 | { status: 400 }
20 | );
21 | }
22 |
23 | if (!mongoose.Types.ObjectId.isValid(templateId)) {
24 | return NextResponse.json(
25 | { error: "Invalid template ID format" },
26 | { status: 400 }
27 | );
28 | }
29 |
30 | // 2️⃣ Validate required fields
31 | if (!name || !name.trim()) {
32 | return NextResponse.json(
33 | { error: "Badge name is required" },
34 | { status: 400 }
35 | );
36 | }
37 |
38 | if (!description || !description.trim()) {
39 | return NextResponse.json(
40 | { error: "Badge description is required" },
41 | { status: 400 }
42 | );
43 | }
44 |
45 | if (!icon || !icon.trim()) {
46 | return NextResponse.json(
47 | { error: "Badge icon is required" },
48 | { status: 400 }
49 | );
50 | }
51 |
52 | // 3️⃣ Find the template
53 | const template = await BadgeTemplate.findById(templateId);
54 | if (!template) {
55 | return NextResponse.json(
56 | { error: "Badge template not found" },
57 | { status: 404 }
58 | );
59 | }
60 |
61 | // 4️⃣ Check for duplicate name (excluding current template)
62 | const duplicate = await BadgeTemplate.findOne({
63 | name: name.trim(),
64 | _id: { $ne: templateId },
65 | });
66 | if (duplicate) {
67 | return NextResponse.json(
68 | { error: "A badge template with this name already exists" },
69 | { status: 400 }
70 | );
71 | }
72 |
73 | // 5️⃣ Update the template
74 | template.name = name.trim();
75 | template.description = description.trim();
76 | template.icon = icon.trim();
77 | template.color = color || "#8B5CF6";
78 | template.isActive = isActive !== undefined ? isActive : true;
79 | template.updatedAt = new Date();
80 |
81 | await template.save();
82 |
83 | console.log(`✅ Badge template updated: ${name}`);
84 |
85 | return NextResponse.json({
86 | success: true,
87 | template,
88 | message: "Badge template updated successfully",
89 | });
90 | } catch (error) {
91 | console.error("❌ Badge template update error:", error);
92 | return NextResponse.json(
93 | { error: "Failed to update badge template" },
94 | { status: 500 }
95 | );
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/app/api/user/recent-solved/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import { getServerSession } from "next-auth";
4 | import { authOptions } from "@/lib/authOptions";
5 | import userSchema from "@/models/userSchema";
6 | import UserQuestionModel from "@/models/userQuestionSchema";
7 | import QuestionModel from "@/models/qustionsSchema";
8 | import { HttpStatusCode } from "axios";
9 |
10 | export const runtime = "nodejs";
11 |
12 | export async function GET(req: NextRequest) {
13 | try {
14 | await connect();
15 |
16 | const session = await getServerSession(authOptions);
17 | if (!session) {
18 | return new Response("Unauthorized", { status: 401 });
19 | }
20 |
21 | const user = await userSchema.findOne({ email: session?.user?.email });
22 | if (!user) {
23 | return NextResponse.json(
24 | { success: false, message: "User not found" },
25 | { status: HttpStatusCode.NotFound }
26 | );
27 | }
28 |
29 | // Get user's solved questions
30 | const userQuestions = await UserQuestionModel.find({
31 | userId: user.id,
32 | }).limit(10); // Limit to last 10 solved questions
33 |
34 | if (userQuestions.length === 0) {
35 | return NextResponse.json([]);
36 | }
37 |
38 | // Get the question details for each solved question
39 | const questionIds = userQuestions.map((uq) => uq.questionId);
40 | const questions = await QuestionModel.find({ _id: { $in: questionIds } })
41 | .select("title category points description createdAt") // Don't include flag
42 | .sort({ createdAt: -1 }); // Sort by when questions were created (most recent first)
43 |
44 | // Create a map for quick lookup of user question data
45 | const userQuestionMap = new Map();
46 | userQuestions.forEach((uq) => {
47 | userQuestionMap.set(uq.questionId.toString(), uq);
48 | });
49 |
50 | // Transform the data to match the expected format
51 | const recentSolved = questions
52 | .map((question) => {
53 | const userQuestion = userQuestionMap.get(question._id.toString());
54 | if (!userQuestion) return null;
55 |
56 | return {
57 | _id: question._id,
58 | title: question.title,
59 | category: question.category,
60 | points: question.points,
61 | description: question.description,
62 | solvedAt: userQuestion.createdAt, // When the user solved it
63 | questionCreatedAt: question.createdAt, // When the question was created
64 | };
65 | })
66 | .filter((item) => item !== null); // Remove null entries
67 |
68 | return NextResponse.json(recentSolved);
69 | } catch (error) {
70 | console.error("Error fetching recent solved questions:", error);
71 | return NextResponse.json(
72 | { success: false, message: "Failed to fetch recent solved questions" },
73 | { status: HttpStatusCode.InternalServerError }
74 | );
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/api/user/[username]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import UserSchema from "@/models/userSchema";
4 | import UserQuestionModel from "@/models/userQuestionSchema";
5 |
6 | export const runtime = "nodejs";
7 |
8 | // GET /api/user/[username] - Public user profile endpoint
9 | export async function GET(
10 | request: NextRequest,
11 | { params }: { params: Promise<{ username: string }> }
12 | ) {
13 | try {
14 | await connect();
15 |
16 | // Await the params since it's now a Promise in newer Next.js versions
17 | const { username: rawUsername } = await params;
18 | const username = decodeURIComponent(rawUsername);
19 |
20 | const user = await UserSchema.findOne({
21 | name: { $regex: new RegExp(`^${username}$`, "i") },
22 | }).select("name image totalScore customBadges createdAt role");
23 |
24 | if (!user) {
25 | return NextResponse.json({ error: "User not found" }, { status: 404 });
26 | }
27 |
28 | // Get completion stats
29 | const completedQuestions = await UserQuestionModel.countDocuments({
30 | userId: user._id,
31 | });
32 |
33 | // Calculate rank
34 | const allUsers = await UserSchema.find({})
35 | .sort({ totalScore: -1 })
36 | .select("_id totalScore");
37 | const userRank =
38 | allUsers.findIndex((u) => u._id.toString() === user._id.toString()) + 1;
39 |
40 | // Calculate level
41 | const getLevel = (score: number): string => {
42 | if (score < 200) return "[0x1][Newbie]";
43 | if (score < 500) return "[0x2][Scout]";
44 | if (score < 1000) return "[0x3][Codebreaker]";
45 | if (score < 1500) return "[0x4][Hacker]";
46 | if (score < 2000) return "[0x5][Cipher Hunter]";
47 | if (score < 3000) return "[0x6][Forger]";
48 | return "[0x7][Flag Conqueror]";
49 | };
50 |
51 | // Calculate system badges
52 | const getBadges = (completed: number): number => {
53 | let badges = 0;
54 | if (completed >= 1) badges++;
55 | if (completed >= 5) badges++;
56 | if (completed >= 10) badges++;
57 | if (completed >= 25) badges++;
58 | if (completed >= 50) badges++;
59 | if (completed >= 100) badges++;
60 | return badges;
61 | };
62 |
63 | const profileData = {
64 | name: user.name,
65 | image: user.image,
66 | totalScore: user.totalScore || 0,
67 | rank: userRank,
68 | level: getLevel(user.totalScore || 0),
69 | completedQuestions,
70 | badges: getBadges(completedQuestions),
71 | customBadges: user.customBadges || [],
72 | createdAt: user.createdAt,
73 | memberSince: new Date(user.createdAt).getFullYear(),
74 | };
75 |
76 | return NextResponse.json({
77 | success: true,
78 | user: profileData,
79 | });
80 | } catch (error) {
81 | console.error("Public profile API error:", error);
82 | return NextResponse.json(
83 | { error: "Internal server error" },
84 | { status: 500 }
85 | );
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/api/auth/check-admin/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { getServerSession } from "next-auth";
3 | import connect from "@/utils/db";
4 | import User from "@/models/userSchema";
5 |
6 | export const runtime = "nodejs";
7 |
8 | interface SessionUser {
9 | id?: string;
10 | email?: string;
11 | name?: string;
12 | image?: string;
13 | }
14 |
15 | interface CheckAdminResponse {
16 | isAdmin: boolean;
17 | user?: {
18 | id: string;
19 | email: string;
20 | name?: string;
21 | role?: string;
22 | };
23 | message?: string;
24 | }
25 |
26 | export async function GET(request: NextRequest): Promise {
27 | try {
28 | const session = await getServerSession();
29 |
30 | if (!session || !session.user) {
31 | return NextResponse.json(
32 | {
33 | isAdmin: false,
34 | message: "Not authenticated",
35 | } as CheckAdminResponse,
36 | { status: 401 }
37 | );
38 | }
39 |
40 | const sessionUser = session.user as SessionUser;
41 |
42 | if (!sessionUser.email) {
43 | return NextResponse.json(
44 | {
45 | isAdmin: false,
46 | message: "User email not found in session",
47 | } as CheckAdminResponse,
48 | { status: 400 }
49 | );
50 | }
51 |
52 | console.log(`Checking admin status for user: ${sessionUser.email}`);
53 |
54 | // Connect to database
55 | await connect();
56 |
57 | // Find user in database and cast to proper type
58 | const user = (await User.findOne({
59 | email: sessionUser.email,
60 | }).lean()) as any;
61 |
62 | if (!user) {
63 | console.log(`User ${sessionUser.email} not found in database`);
64 | return NextResponse.json(
65 | {
66 | isAdmin: false,
67 | message: "User not found in database",
68 | } as CheckAdminResponse,
69 | { status: 404 }
70 | );
71 | }
72 |
73 | // Check if user has admin role
74 | const isAdmin = user.role === "Admin";
75 |
76 | console.log(
77 | `Admin check result for ${sessionUser.email}: ${isAdmin} (role: ${user.role})`
78 | );
79 |
80 | const response: CheckAdminResponse = {
81 | isAdmin,
82 | user: {
83 | id: user._id.toString(),
84 | email: user.email,
85 | name: user.name,
86 | role: user.role,
87 | },
88 | message: isAdmin ? "Admin access granted" : "Admin privileges required",
89 | };
90 |
91 | // Return appropriate status code
92 | return NextResponse.json(response, {
93 | status: isAdmin ? 200 : 403,
94 | });
95 | } catch (error) {
96 | console.error("Admin check error:", error);
97 |
98 | const errMsg =
99 | error instanceof Error ? error.message : "Unexpected error occurred";
100 |
101 | return NextResponse.json(
102 | {
103 | isAdmin: false,
104 | message: "Failed to verify admin status",
105 | details: errMsg,
106 | } as CheckAdminResponse,
107 | { status: 500 }
108 | );
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/app/api/problems/completed/route.ts:
--------------------------------------------------------------------------------
1 | import connect from "@/utils/db";
2 | import { NextRequest, NextResponse } from "next/server";
3 | import QuestionModel from "@/models/qustionsSchema";
4 | import { HttpStatusCode } from "axios";
5 | import userSchema from "@/models/userSchema";
6 | import { getServerSession } from "next-auth";
7 | import { authOptions } from "@/lib/authOptions";
8 | import UserQuestionModel from "@/models/userQuestionSchema";
9 |
10 | export const runtime = "nodejs";
11 |
12 | export async function GET(request: NextRequest) {
13 | const session = await getServerSession(authOptions);
14 |
15 | if (!session) {
16 | return NextResponse.json(
17 | { success: false, message: "Unauthorized" },
18 | { status: HttpStatusCode.Unauthorized }
19 | );
20 | }
21 |
22 | try {
23 | await connect();
24 |
25 | // Get pagination parameters from URL
26 | const { searchParams } = new URL(request.url);
27 | const page = parseInt(searchParams.get("page") || "1");
28 | const limit = 8; // Fixed to 8 items per page
29 | const skip = (page - 1) * limit;
30 |
31 | // Find the user
32 | const user = await userSchema.findOne({ email: session?.user?.email });
33 | if (!user) {
34 | return NextResponse.json(
35 | { success: false, message: "User not found" },
36 | { status: HttpStatusCode.NotFound }
37 | );
38 | }
39 |
40 | // Get total count of completed questions for pagination info
41 | const totalCompletedCount = await UserQuestionModel.countDocuments({
42 | userId: user._id,
43 | });
44 |
45 | // Get paginated completed questions by this user, sorted by completion date (newest first)
46 | const completedUserQuestions = await UserQuestionModel.find({
47 | userId: user._id,
48 | })
49 | .sort({ createdAt: -1 }) // Sort by completion date, newest first
50 | .skip(skip)
51 | .limit(limit)
52 | .populate({
53 | path: "questionId",
54 | select: "-flag", // Exclude the flag field for security
55 | model: QuestionModel,
56 | });
57 |
58 | // Extract the populated question data and add completion info
59 | const completedProblems = completedUserQuestions
60 | .filter((userQuestion) => userQuestion.questionId) // Filter out any null/undefined
61 | .map((userQuestion) => ({
62 | ...userQuestion.questionId.toObject(),
63 | completedAt: userQuestion.createdAt, // When they completed it
64 | pointsEarned: userQuestion.questionId.points, // Points they earned
65 | }));
66 |
67 | // Calculate pagination info
68 | const totalPages = Math.ceil(totalCompletedCount / limit);
69 | const hasMore = page < totalPages;
70 | const hasPrevious = page > 1;
71 |
72 | return NextResponse.json({
73 | success: true,
74 | completedProblems,
75 | totalProblems: totalCompletedCount,
76 | currentPage: page,
77 | totalPages,
78 | hasMore,
79 | hasPrevious,
80 | itemsPerPage: limit,
81 | });
82 | } catch (error: any) {
83 | console.error("Error fetching completed problems:", error);
84 | return NextResponse.json(
85 | { success: false, message: "Failed to fetch completed problems" },
86 | { status: HttpStatusCode.InternalServerError }
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { DM_Sans } from "next/font/google";
3 | import "./globals.css";
4 | import Navbar from "@/components/Navbar";
5 | import Footer from "@/components/Footer";
6 | import Authprovider from "@/providers/auth-provider";
7 | import { ThemeProvider } from "@/context/ThemeContext";
8 | import { Analytics } from "@vercel/analytics/react";
9 |
10 | const dmSans = DM_Sans({ subsets: ["latin"] });
11 |
12 | export const metadata: Metadata = {
13 | title: "FlagForge",
14 | description:
15 | "Join FlagForge, the premier Capture The Flag (CTF) platform designed to hone your cybersecurity skills with engaging challenges. Compete, learn, and grow your hacking expertise.",
16 | metadataBase: new URL("https://flagforge.xyz"),
17 | alternates: {
18 | canonical: "/",
19 | languages: {
20 | en: "/en",
21 | hi: "/hi",
22 | bn: "/bn",
23 | },
24 | },
25 | applicationName: "FlagForge CTF",
26 | referrer: "origin-when-cross-origin",
27 | keywords: [
28 | "CTF",
29 | "Capture The Flag",
30 | "Cybersecurity",
31 | "Ethical Hacking",
32 | "FlagForge",
33 | "CTF Challenges",
34 | "Hacking Skills",
35 | "Cybersecurity Platform",
36 | "Online CTF Competitions",
37 | ],
38 | authors: [{ name: "@Aryanstha", url: "https://github.com/aryan4859" }],
39 | openGraph: {
40 | title: "FlagForge - The Ultimate CTF Platform",
41 | description:
42 | "FlagForge is the go-to platform for Capture The Flag (CTF) competitions. Test your hacking skills with thrilling challenges in cybersecurity.",
43 | url: "https://flagforge.xyz",
44 | siteName: "FlagForge",
45 | images: [
46 | {
47 | url: "https://flagforge.xyz/flagforge.gif",
48 | width: 1200,
49 | height: 630,
50 | alt: "FlagForge - Capture The Flag Platform",
51 | },
52 | ],
53 | locale: "en_US",
54 | type: "website",
55 | },
56 | twitter: {
57 | card: "summary_large_image",
58 | site: "@Aryanstha",
59 | title: "FlagForge - The Ultimate CTF Platform",
60 | description:
61 | "Join FlagForge, the leading Capture The Flag platform to enhance your cybersecurity skills. Compete and learn with exciting CTF challenges.",
62 | images: ["https://flagforge.xyz"],
63 | },
64 | };
65 |
66 | export default function RootLayout({
67 | children,
68 | }: {
69 | children: React.ReactNode;
70 | }) {
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
81 |
82 |
83 |
86 |
87 |
88 |
89 |
90 |
{children}
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/lib/authOptions.ts:
--------------------------------------------------------------------------------
1 | import { AuthOptions } from "next-auth";
2 | import GoogleProvider from "next-auth/providers/google";
3 | import connect from "@/utils/db";
4 | import UserModel from "@/models/userSchema";
5 | import { TokenBlacklistService } from "./tokenBlacklist";
6 | import { randomUUID } from "crypto";
7 |
8 | export const authOptions: AuthOptions = {
9 | providers: [
10 | GoogleProvider({
11 | clientId: process.env.GOOGLE_CLIENT_ID!,
12 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
13 | authorization: { params: { scope: "email profile" } },
14 | }),
15 | ],
16 |
17 | session: {
18 | strategy: "jwt",
19 | maxAge: 60 * 60, // 1 hour
20 | updateAge: 15 * 60, // refresh JWT every 15 minutes
21 | },
22 |
23 | jwt: {
24 | secret: process.env.NEXTAUTH_SECRET,
25 | maxAge: 60 * 60,
26 | },
27 |
28 | callbacks: {
29 | async signIn({ user, account }) {
30 | if (account?.provider === "google") {
31 | await connect();
32 | try {
33 | const existingUser = await UserModel.findOne({ email: user.email });
34 | if (!existingUser) {
35 | await new UserModel({
36 | email: user.email,
37 | name: user.name,
38 | image: user.image,
39 | totalScore: 0,
40 | role: "User",
41 | }).save();
42 | }
43 | return true;
44 | } catch (err) {
45 | console.error("❌ Sign-in error:", err);
46 | return false;
47 | }
48 | }
49 | return false;
50 | },
51 |
52 | // 🔥 CRITICAL FIX: Always fetch user data from DB
53 | async jwt({ token, user, trigger }) {
54 | await connect();
55 |
56 | // Generate JTI if missing
57 | if (!token.jti) {
58 | token.jti = randomUUID();
59 | }
60 |
61 | // Determine which email to use
62 | const emailToQuery = user?.email || token.email;
63 |
64 | // ✅ ALWAYS fetch from database to ensure role is present
65 | if (emailToQuery) {
66 | try {
67 | const dbUser = await UserModel.findOne({ email: emailToQuery });
68 |
69 | if (dbUser) {
70 | token.id = dbUser._id.toString();
71 | token.email = dbUser.email;
72 | token.name = dbUser.name;
73 | token.picture = dbUser.image;
74 | token.totalScore = dbUser.totalScore ?? 0;
75 | token.role = dbUser.role ?? "User";
76 | } else {
77 | // Fallback if user not found
78 | token.role = token.role || "User";
79 | }
80 | } catch (err) {
81 | console.error("❌ Error fetching user for JWT:", err);
82 | token.role = token.role || "User";
83 | }
84 | }
85 |
86 | // Timestamps
87 | const now = Math.floor(Date.now() / 1000);
88 | token.iat = now;
89 | token.exp = now + 60 * 60;
90 |
91 | return token;
92 | },
93 |
94 | async session({ session, token }) {
95 | session.user = {
96 | id: token.id as string,
97 | email: token.email ?? null,
98 | name: token.name ?? null,
99 | image: token.picture ?? null,
100 | totalScore: (token.totalScore as number) ?? 0,
101 | role: (token.role as string) ?? "User",
102 | };
103 |
104 | (session as any).tokenInfo = {
105 | jti: token.jti,
106 | exp: token.exp,
107 | iat: token.iat,
108 | };
109 |
110 | return session;
111 | },
112 | },
113 | };
114 |
--------------------------------------------------------------------------------
/public/badges/CTF.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/HALL-OF-FAME.md:
--------------------------------------------------------------------------------
1 | # 🏆 Flag Forge Hall of Fame
2 |
3 |
4 |
5 |
6 | Celebrating the ethical hackers who help keep Flag Forge secure 🛡️
7 |
8 |
9 | ---
10 |
11 | ## 🥇 Security Researchers Spotlight
12 |
13 | _These amazing individuals have responsibly disclosed vulnerabilities, strengthening Flag Forge for everyone._
14 |
15 | ---
16 |
17 | ### 📅 Contributors by Year
18 |
19 |
20 | 2025 Contributors
21 |
22 | | Researcher | Handle | Bugs | Severity (per bug) | Badge | Profile | CVE IDs |
23 | | ------------------- | ------------ | ---- | ------------------------------- | --------- | ---------------------------------------- | ---------------------------------------------- |
24 | | **Aryan Shrestha** | @aryan4859 | 3 | 🔴 Critical, 🔴 High, 🟠 Medium | 🥇 Gold | [GitHub](https://github.com/aryan4859) | CVE-2025-59826, CVE-2025-59833, CVE-2025-59841 |
25 | | **Rijan Poudel** | @At0mXploit | 2 | 🔴 High, 🟠 Medium | 🥈 Silver | [GitHub](https://github.com/At0mXploit) | CVE-2025-59843, CVE-2025-59932 |
26 | | **Sarthak KC** | @sarthakkc36 | 1 | 🔴 High | 🥈 Silver | [GitHub](https://github.com/sarthakkc36) | CVE-2025-59827 |
27 | | **Sarams Rauniyar** | @0x0w1z | 1 | 🔴 Critical | 🥇 Gold | [GitHub](https://github.com/0x0w1z) | CVE-2025-61777 |
28 |
29 |
30 |
31 | ---
32 |
33 | ## 📊 Platform Security Stats
34 |
35 | | **Metric** | **Count** |
36 | | ------------------------------ | --------- |
37 | | Total Bug Hunters | 4 |
38 | | Total Vulnerabilities Reported | 7 |
39 | | Critical | 2 |
40 | | High | 4 |
41 | | Medium | 1 |
42 | | Low | 0 |
43 |
44 | ---
45 |
46 | ## 🎖️ Recognition Badges
47 |
48 | | Badge | Criteria |
49 | | --------------- | -------------------------------------------- |
50 | | 🥉 **Bronze** | Discovered a Medium severity vulnerability |
51 | | 🥈 **Silver** | Discovered a High severity vulnerability |
52 | | 🥇 **Gold** | Discovered a Critical severity vulnerability |
53 | | 🏅 **Platinum** | 5+ valid vulnerabilities across categories |
54 |
55 | ---
56 |
57 | ## 🎖️ Digital Badges
58 |
59 | | Badge | Image | Criteria |
60 | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
61 | | Bug Hunter | | Awarded for submitting **1 valid bug** |
62 | | Security Researcher | | Awarded for submitting **3 or more valid bugs** |
63 |
64 | ---
65 |
66 |
67 | 💡 Want to be featured here?
68 |
69 | Report vulnerabilities responsibly in the GitHub Issues section to earn recognition and badges!
70 |
71 |
72 | ---
73 |
74 | _Last Updated: September 2025_
75 |
--------------------------------------------------------------------------------
/public/sitemap-0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://flagforge.xyz/about 2025-11-27T15:53:33.289Z monthly 0.7
4 | https://flagforge.xyz/contact 2025-11-27T15:53:33.289Z monthly 0.7
5 | https://flagforge.xyz/cookie-consent 2025-11-27T15:53:33.289Z monthly 0.7
6 | https://flagforge.xyz/licensing 2025-11-27T15:53:33.289Z monthly 0.7
7 | https://flagforge.xyz/privacy-policy 2025-11-27T15:53:33.289Z monthly 0.7
8 | https://flagforge.xyz/terms-of-service 2025-11-27T15:53:33.289Z monthly 0.7
9 | https://flagforge.xyz/authentication 2025-11-27T15:53:33.289Z monthly 0.7
10 | https://flagforge.xyz/blogs/introduction 2025-11-27T15:53:33.289Z monthly 0.7
11 | https://flagforge.xyz/blogs 2025-11-27T15:53:33.290Z monthly 0.7
12 | https://flagforge.xyz/home 2025-11-27T15:53:33.290Z monthly 0.7
13 | https://flagforge.xyz/leaderboard 2025-11-27T15:53:33.290Z monthly 0.7
14 | https://flagforge.xyz/problems 2025-11-27T15:53:33.290Z monthly 0.7
15 | https://flagforge.xyz/profile 2025-11-27T15:53:33.290Z monthly 0.7
16 | https://flagforge.xyz/resources 2025-11-27T15:53:33.290Z monthly 0.7
17 | https://flagforge.xyz/resources/upload 2025-11-27T15:53:33.290Z monthly 0.7
18 | https://flagforge.xyz/roles/developers/admins/auth 2025-11-27T15:53:33.290Z monthly 0.7
19 | https://flagforge.xyz/roles/developers/admins/badge-templates 2025-11-27T15:53:33.290Z monthly 0.7
20 | https://flagforge.xyz/roles/developers/admins/badges 2025-11-27T15:53:33.290Z monthly 0.7
21 | https://flagforge.xyz/roles/developers/admins 2025-11-27T15:53:33.290Z monthly 0.7
22 | https://flagforge.xyz/roles/developers/admins/uploads 2025-11-27T15:53:33.290Z monthly 0.7
23 | https://flagforge.xyz/unauthorized 2025-11-27T15:53:33.290Z monthly 0.7
24 | https://flagforge.xyz 2025-11-27T15:53:33.290Z monthly 0.7
25 |
--------------------------------------------------------------------------------
/app/api/admin/dashboard-stats/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { getServerSession } from "next-auth";
3 | import { authOptions } from "@/lib/authOptions";
4 | import connect from "@/utils/db";
5 | import UserSchema from "@/models/userSchema";
6 | import QuestionModel from "@/models/qustionsSchema";
7 | import BadgeTemplate from "@/models/badgeTemplate";
8 | import UserQuestionModel from "@/models/userQuestionSchema";
9 |
10 | export const runtime = "nodejs";
11 |
12 | // Admin authentication check
13 | async function isAdmin(email: string): Promise {
14 | try {
15 | await connect();
16 | const adminUser = await UserSchema.findOne({
17 | email: email,
18 | role: "Admin",
19 | }).lean();
20 | return !!adminUser;
21 | } catch (error) {
22 | console.error("Error checking admin status:", error);
23 | return false;
24 | }
25 | }
26 |
27 | export async function GET(req: NextRequest) {
28 | try {
29 | await connect();
30 |
31 | const session = await getServerSession(authOptions);
32 | if (!session || !session.user?.email) {
33 | return NextResponse.json(
34 | { success: false, message: "Unauthorized" },
35 | { status: 401 }
36 | );
37 | }
38 |
39 | // Check if user is admin
40 | if (!(await isAdmin(session.user.email))) {
41 | return NextResponse.json(
42 | {
43 | success: false,
44 | message: "Access denied. Admin privileges required.",
45 | },
46 | { status: 403 }
47 | );
48 | }
49 |
50 | // Get all statistics in parallel for better performance
51 | const [
52 | totalQuestions,
53 | totalUsers,
54 | totalBadgeTemplates,
55 | activeBadgeTemplates,
56 | recentCompletions,
57 | ] = await Promise.all([
58 | // Total challenges
59 | QuestionModel.countDocuments({}),
60 |
61 | // Total users
62 | UserSchema.countDocuments({}),
63 |
64 | // Total badge templates
65 | BadgeTemplate.countDocuments({}),
66 |
67 | // Active badge templates
68 | BadgeTemplate.countDocuments({ isActive: true }),
69 |
70 | // Recent activity (completed challenges in last 24 hours)
71 | UserQuestionModel.countDocuments({
72 | createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
73 | }),
74 | ]);
75 |
76 | // Calculate active challenges (non-expired)
77 | const now = new Date();
78 | const activeQuestions = await QuestionModel.countDocuments({
79 | $or: [
80 | { expiryDate: { $exists: false } },
81 | { expiryDate: null },
82 | { expiryDate: { $gt: now } },
83 | ],
84 | });
85 |
86 | // Get additional insights
87 | const [topCategories, recentUsers] = await Promise.all([
88 | // Most popular categories
89 | QuestionModel.aggregate([
90 | { $group: { _id: "$category", count: { $sum: 1 } } },
91 | { $sort: { count: -1 } },
92 | { $limit: 5 },
93 | ]),
94 |
95 | // Users registered in last week
96 | UserSchema.countDocuments({
97 | createdAt: { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
98 | }),
99 | ]);
100 |
101 | const stats = {
102 | totalChallenges: totalQuestions,
103 | activeChallenges: activeQuestions,
104 | totalBadgeTemplates: totalBadgeTemplates,
105 | activeBadgeTemplates: activeBadgeTemplates,
106 | totalUsers: totalUsers,
107 | recentActivity: recentCompletions,
108 | newUsersThisWeek: recentUsers,
109 | topCategories: topCategories,
110 | lastUpdated: new Date().toISOString(),
111 | };
112 |
113 | return NextResponse.json({
114 | success: true,
115 | stats: stats,
116 | });
117 | } catch (error) {
118 | console.error("Error fetching dashboard stats:", error);
119 | return NextResponse.json(
120 | { success: false, message: "Failed to fetch dashboard statistics" },
121 | { status: 500 }
122 | );
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/lib/tokenBlacklist.ts:
--------------------------------------------------------------------------------
1 | import { Redis } from '@upstash/redis';
2 | import { randomBytes } from 'crypto';
3 |
4 | // Initialize Redis client
5 | const redis = new Redis({
6 | url: process.env.UPSTASH_REDIS_REST_URL!,
7 | token: process.env.UPSTASH_REDIS_REST_TOKEN!,
8 | });
9 |
10 | interface BlacklistData {
11 | userId?: string;
12 | blacklistedAt: string;
13 | expiresAt: string;
14 | }
15 |
16 | export class TokenBlacklistService {
17 | /**
18 | * Add a session token to the blacklist
19 | * @param sessionToken - The NextAuth session token (plain string, not JWT)
20 | * @param expiresAt - When the token expires
21 | * @param userId - Optional user ID for tracking
22 | */
23 | static async addToBlacklist(
24 | sessionToken: string,
25 | expiresAt: Date,
26 | userId?: string
27 | ): Promise {
28 | try {
29 | const ttl = Math.floor((expiresAt.getTime() - Date.now()) / 1000);
30 |
31 | if (ttl > 0) {
32 | const data: BlacklistData = {
33 | userId,
34 | blacklistedAt: new Date().toISOString(),
35 | expiresAt: expiresAt.toISOString(),
36 | };
37 |
38 | await redis.setex(
39 | `blacklist:${sessionToken}`,
40 | ttl,
41 | JSON.stringify(data)
42 | );
43 |
44 | console.log('✅ Token blacklisted:', {
45 | token: sessionToken.substring(0, 20) + '...',
46 | userId,
47 | expiresIn: `${ttl}s`,
48 | expiresAt: expiresAt.toISOString()
49 | });
50 | } else {
51 | console.log('⏰ Token already expired, not adding to blacklist');
52 | }
53 |
54 | } catch (error) {
55 | console.error('❌ Error adding token to blacklist:', error);
56 | throw error;
57 | }
58 | }
59 |
60 | static async isBlacklisted(sessionToken: string): Promise {
61 | try {
62 | const result = await redis.get(`blacklist:${sessionToken}`);
63 | const isBlacklisted = result !== null;
64 |
65 | if (isBlacklisted) {
66 | console.log('🚫 Token is blacklisted:', sessionToken.substring(0, 20) + '...');
67 | }
68 |
69 | return isBlacklisted;
70 | } catch (error) {
71 | console.error('❌ Error checking token blacklist:', error);
72 | return false;
73 | }
74 | }
75 |
76 | static async removeFromBlacklist(sessionToken: string): Promise {
77 | try {
78 | await redis.del(`blacklist:${sessionToken}`);
79 | console.log('🗑️ Token removed from blacklist:', sessionToken.substring(0, 20) + '...');
80 | } catch (error) {
81 | console.error('❌ Error removing token from blacklist:', error);
82 | throw error;
83 | }
84 | }
85 |
86 | static async getBlacklistInfo(sessionToken: string): Promise {
87 | try {
88 | const data = await redis.get(`blacklist:${sessionToken}`);
89 | return data ? JSON.parse(data) : null;
90 | } catch (error) {
91 | console.error('❌ Error getting blacklist info:', error);
92 | return null;
93 | }
94 | }
95 |
96 | static async getAllBlacklisted(): Promise {
97 | try {
98 | const keys = await redis.keys('blacklist:*');
99 | return keys.map(key => key.replace('blacklist:', ''));
100 | } catch (error) {
101 | console.error('❌ Error getting all blacklisted tokens:', error);
102 | return [];
103 | }
104 | }
105 |
106 | static async getBlacklistCount(): Promise {
107 | try {
108 | const keys = await redis.keys('blacklist:*');
109 | return keys.length;
110 | } catch (error) {
111 | console.error('❌ Error getting blacklist count:', error);
112 | return 0;
113 | }
114 | }
115 |
116 | static async cleanupExpired(): Promise {
117 | console.log('✨ Redis auto-expires tokens, manual cleanup not needed');
118 | }
119 |
120 | /**
121 | * Generate a unique JTI (JWT ID)
122 | */
123 | static generateJTI(): string {
124 | return randomBytes(16).toString('hex');
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/api/blogs/route.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '@notionhq/client';
2 | import { NextResponse } from 'next/server';
3 | export const runtime = "nodejs";
4 |
5 | export async function GET() {
6 | try {
7 | // Check environment variables
8 | const apiKey = process.env.NOTION_API_KEY;
9 | const databaseId = process.env.NOTION_DATABASE_ID;
10 |
11 | if (!apiKey) {
12 | console.error('NOTION_API_KEY is not set');
13 | return NextResponse.json(
14 | { error: 'NOTION_API_KEY is not configured' },
15 | { status: 500 }
16 | );
17 | }
18 |
19 | if (!databaseId) {
20 | console.error('NOTION_DATABASE_ID is not set');
21 | return NextResponse.json(
22 | { error: 'NOTION_DATABASE_ID is not configured' },
23 | { status: 500 }
24 | );
25 | }
26 |
27 | const notion = new Client({
28 | auth: apiKey,
29 | });
30 |
31 | // First, let's get the database to check its structure
32 | let database;
33 | try {
34 | database = await notion.databases.retrieve({ database_id: databaseId });
35 | console.log('Database properties:', Object.keys(database.properties));
36 | } catch (dbError) {
37 | console.error('Database access error:', dbError);
38 | return NextResponse.json(
39 | { error: 'Cannot access Notion database. Check your database ID and permissions.' },
40 | { status: 500 }
41 | );
42 | }
43 |
44 | // Query the database with minimal sorting to avoid property issues
45 | const response = await notion.databases.query({
46 | database_id: databaseId,
47 | });
48 |
49 | console.log(`Found ${response.results.length} pages`);
50 |
51 | const getImageUrl = (property: any) => {
52 | if (!property) return null;
53 |
54 | // Handle different Notion file property formats
55 | if (property.files && property.files.length > 0) {
56 | const file = property.files[0];
57 | return file.external?.url || file.file?.url || null;
58 | }
59 |
60 | // Handle direct URL properties
61 | if (property.url) {
62 | return property.url;
63 | }
64 |
65 | // Handle rich text with URLs
66 | if (property.rich_text && property.rich_text.length > 0) {
67 | return property.rich_text[0].href || property.rich_text[0].plain_text;
68 | }
69 |
70 | return null;
71 | };
72 |
73 |
74 |
75 | const posts = response.results.map((page: any) => {
76 | const properties = page.properties;
77 | console.log('Available properties:', Object.keys(properties));
78 | const thumbnailUrl = getImageUrl(properties.Thumbnail);
79 |
80 | return {
81 | id: page.id,
82 | title: properties.Title?.title?.[0]?.plain_text || 'Untitled',
83 | thumbnail: thumbnailUrl,
84 | slug: properties.Slug?.rich_text?.[0]?.plain_text || page.id,
85 | excerpt: '', // You don't have an excerpt field, we'll use first paragraph from content
86 | tags: [], // You don't have tags, we'll leave empty
87 | status: properties.Status?.select?.name || 'Published',
88 | created: properties['Published Date']?.date?.start || page.created_time,
89 | updated: page.last_edited_time,
90 | cover: properties['File and Media']?.files?.[0]?.external?.url ||
91 | properties['File and Media']?.files?.[0]?.file?.url ||
92 | page.cover?.external?.url ||
93 | page.cover?.file?.url ||
94 | null,
95 | };
96 | });
97 |
98 | // Filter only published posts (but if no Status field, show all)
99 | const publishedPosts = posts.filter((post) =>
100 | post.status === 'Published' || post.status === 'published' || !database.properties.Status
101 | );
102 |
103 | console.log(`Returning ${publishedPosts.length} published posts`);
104 |
105 | return NextResponse.json({ posts: publishedPosts });
106 | } catch (error) {
107 | console.error('Error fetching blogs:', error);
108 | return NextResponse.json(
109 | {
110 | error: 'Failed to fetch blogs',
111 | details: error instanceof Error ? error.message : 'Unknown error'
112 | },
113 | { status: 500 }
114 | );
115 | }
116 | }
--------------------------------------------------------------------------------
/app/api/blogs/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '@notionhq/client';
2 | import { NextResponse } from 'next/server';
3 | export const runtime = "nodejs";
4 |
5 | const notion = new Client({
6 | auth: process.env.NOTION_API_KEY,
7 | });
8 |
9 | export async function GET(
10 | request: Request,
11 | { params }: { params: Promise<{ id: string }> }
12 | ) {
13 | try {
14 | // Await the params since they're now a Promise
15 | const { id } = await params;
16 | const pageId = id;
17 |
18 | // Get page properties
19 | const page = await notion.pages.retrieve({ page_id: pageId });
20 |
21 | // Get page content blocks
22 | const blocks = await notion.blocks.children.list({
23 | block_id: pageId,
24 | });
25 |
26 | const pageData = page as any;
27 | const properties = pageData.properties;
28 |
29 | // Extract excerpt from first paragraph block
30 | const extractExcerpt = (blocks: any[]) => {
31 | const firstParagraph = blocks.find(block => block.type === 'paragraph');
32 | if (firstParagraph?.paragraph?.rich_text?.length > 0) {
33 | const text = firstParagraph.paragraph.rich_text
34 | .map((item: any) => item.plain_text)
35 | .join('');
36 | return text.length > 150 ? text.substring(0, 150) + '...' : text;
37 | }
38 | return '';
39 | };
40 |
41 | // Extract plain text content from blocks for search/preview purposes
42 | const extractContentText = (blocks: any[]) => {
43 | return blocks
44 | .filter(block => ['paragraph', 'heading_1', 'heading_2', 'heading_3'].includes(block.type))
45 | .map(block => {
46 | const blockType = block.type;
47 | const richText = block[blockType]?.rich_text || [];
48 | return richText.map((item: any) => item.plain_text).join('');
49 | })
50 | .filter(text => text.trim().length > 0)
51 | .join('\n\n');
52 | };
53 |
54 | // Helper function to extract image/file URL from Notion property
55 | const getImageUrl = (property: any) => {
56 | if (!property) return null;
57 |
58 | // Handle different Notion file property formats
59 | if (property.files && property.files.length > 0) {
60 | const file = property.files[0];
61 | return file.external?.url || file.file?.url || null;
62 | }
63 |
64 | // Handle direct URL properties
65 | if (property.url) {
66 | return property.url;
67 | }
68 |
69 | // Handle rich text with URLs
70 | if (property.rich_text && property.rich_text.length > 0) {
71 | return property.rich_text[0].href || property.rich_text[0].plain_text;
72 | }
73 |
74 | return null;
75 | };
76 |
77 | // Get block content (preferred) or fall back to Content property
78 | const extractedContent = extractContentText(blocks.results);
79 | const fallbackContent = properties.Content?.rich_text
80 | ?.map((t: any) => t.plain_text)
81 | .join('') || '';
82 |
83 | // Extract thumbnail URL
84 | const thumbnailUrl = getImageUrl(properties.Thumbnail);
85 |
86 | // Extract image URL (separate from thumbnail)
87 | const imageUrl = getImageUrl(properties.Images);
88 |
89 | const post = {
90 | id: pageData.id,
91 | title: properties.Title?.title?.[0]?.plain_text || 'Untitled',
92 | thumbnail: thumbnailUrl,
93 | image: imageUrl,
94 | slug: properties.Slug?.rich_text?.[0]?.plain_text || pageData.id,
95 | excerpt: extractExcerpt(blocks.results) || fallbackContent.substring(0, 150),
96 | tags: properties.Tags?.multi_select?.map((tag: any) => tag.name) || [],
97 | status: properties.Status?.select?.name || 'Published',
98 | created: properties['Publish Date']?.date?.start || pageData.created_time,
99 | updated: pageData.last_edited_time,
100 | content: extractedContent || fallbackContent || "No Content",
101 | cover: properties['Files & media']?.files?.[0]?.external?.url ||
102 | properties['Files & media']?.files?.[0]?.file?.url ||
103 | pageData.cover?.external?.url ||
104 | pageData.cover?.file?.url ||
105 | thumbnailUrl ||
106 | imageUrl ||
107 | null,
108 | blocks: blocks.results,
109 | };
110 |
111 | return NextResponse.json({ post });
112 |
113 | } catch (error) {
114 | console.error('Error fetching blog post:', error);
115 | return NextResponse.json(
116 | { error: 'Failed to fetch blog post' },
117 | { status: 500 }
118 | );
119 | }
120 | }
--------------------------------------------------------------------------------
/public/badges/researcher.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/CategoryButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Wrench,
4 | Flag,
5 | Shield,
6 | Globe,
7 | Terminal,
8 | Eye,
9 | Sword,
10 | Lock,
11 | Search,
12 | Folder
13 | } from 'lucide-react';
14 |
15 | interface CategoryButtonProps {
16 | category: string;
17 | count?: number;
18 | onClick: () => void;
19 | isSelected?: boolean;
20 | }
21 |
22 | const CategoryButton: React.FC = ({
23 | category,
24 | count,
25 | onClick,
26 | isSelected = false
27 | }) => {
28 | const getCategoryIcon = (category: string) => {
29 | const iconMap: { [key: string]: React.ReactNode } = {
30 | 'Tools & Frameworks': ,
31 | 'CTF Resources & Writeups': ,
32 | 'CyberSecurity Essentials': ,
33 | 'Web Essentials': ,
34 | 'Linux Essentials': ,
35 | 'Blue Team': ,
36 | 'Red Team': ,
37 | 'Cryptography': ,
38 | 'Forensics': ,
39 | 'All':
40 | };
41 | return iconMap[category] || ;
42 | };
43 |
44 | const getCategoryColor = (category: string) => {
45 | const colorMap: { [key: string]: string } = {
46 | 'Tools & Frameworks': 'from-blue-400 to-blue-600',
47 | 'CTF Resources & Writeups': 'from-red-400 to-red-600',
48 | 'CyberSecurity Essentials': 'from-green-400 to-green-600',
49 | 'Web Essentials': 'from-purple-400 to-purple-600',
50 | 'Linux Essentials': 'from-yellow-400 to-yellow-600',
51 | 'Blue Team': 'from-indigo-400 to-indigo-600',
52 | 'Red Team': 'from-rose-400 to-rose-600',
53 | 'Cryptography': 'from-cyan-400 to-cyan-600',
54 | 'Forensics': 'from-orange-400 to-orange-600',
55 | 'All': 'from-gray-400 to-gray-600'
56 | };
57 | return colorMap[category] || 'from-gray-400 to-gray-600';
58 | };
59 |
60 | const getHoverColor = (category: string) => {
61 | const colorMap: { [key: string]: string } = {
62 | 'Tools & Frameworks': 'from-blue-500 to-blue-700',
63 | 'CTF Resources & Writeups': 'from-red-500 to-red-700',
64 | 'CyberSecurity Essentials': 'from-green-500 to-green-700',
65 | 'Web Essentials': 'from-purple-500 to-purple-700',
66 | 'Linux Essentials': 'from-yellow-500 to-yellow-700',
67 | 'Blue Team': 'from-indigo-500 to-indigo-700',
68 | 'Red Team': 'from-rose-500 to-rose-700',
69 | 'Cryptography': 'from-cyan-500 to-cyan-700',
70 | 'Forensics': 'from-orange-500 to-orange-700',
71 | 'All': 'from-gray-500 to-gray-700'
72 | };
73 | return colorMap[category] || 'from-gray-500 to-gray-700';
74 | };
75 |
76 | return (
77 |
89 | {/* Background Pattern */}
90 |
93 |
94 | {/* Content */}
95 |
96 | {/* Icon */}
97 |
98 | {getCategoryIcon(category)}
99 |
100 |
101 | {/* Category Name */}
102 |
103 | {category}
104 |
105 |
106 | {/* Count Badge */}
107 | {count !== undefined && count > 0 && (
108 |
109 | {count}
110 |
111 | )}
112 |
113 |
114 | {/* Hover Effect */}
115 |
116 |
117 | {/* Selected Indicator */}
118 | {isSelected && (
119 |
120 | )}
121 |
122 | );
123 | };
124 |
125 | export default CategoryButton;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Flagforge
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Flag Forge is a simple Capture The Flag (CTF) platform designed to host, manage, and participate in CTF challenges. It provides an intuitive interface for participants to solve challenges, submit flags, and track their progress.
15 |
16 | ---
17 |
18 | ## Features
19 |
20 | - **User-Friendly Interface**: A clean and responsive UI for participants and admins.
21 | - **Profile Page**: Deeper progress tracking, achievements, and history.
22 | - **Flag Submission**: Secure and efficient flag validation system.
23 | - **Leaderboard**: Real-time leaderboard to track participant scores.
24 | - **Badges**: Earn recognition as you solve challenges and level up your skills.
25 |
26 | ---
27 |
28 | ## Technologies Used
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | ---
37 |
38 | 🛠️ Installation Steps:
39 |
40 | 1. Clone Repo from github
41 |
42 | ```
43 | git clone https://github.com/FlagForgeCTF/flagForge
44 | ```
45 |
46 | 2. Change directory
47 |
48 | ```
49 | cd flagforge
50 | ```
51 |
52 | 3. Install required dependencies
53 |
54 | ```
55 | npm install
56 | ```
57 |
58 | 4. Configure .env
59 |
60 | ```
61 | NEXT_PUBLIC_STATSIG_CLIENT_KEY=
62 | NEXT_PUBLIC_STATSIG_CLIENT_KEY=
63 | NEXT_PUBLIC_ADMIN_EMAIL=
64 | NEXT_PUBLIC_ADMIN_PASSWORD=
65 | NOTION_API_KEY=
66 | NOTION_DATABASE_ID=
67 | GOOGLE_CLIENT_SECRET=
68 | GOOGLE_CLIENT_ID=
69 | NEXTAUTH_URL=
70 | MONGO_URL=
71 | NEXTAUTH_SECRET=
72 | ```
73 |
74 | 5. Run locally
75 |
76 | ```
77 | npm run dev
78 | ```
79 |
80 | ---
81 |
82 | ## Contributors
83 |
84 |
85 |
86 |
87 |
88 | Contributions are welcome! Please fork the repository, make your changes, and submit a pull request.
89 |
90 | ---
91 |
92 | ## License
93 |
94 | Flag Forge is licensed under the GPL-3.0 License. See the `LICENSE` file for more details.
95 |
96 | ---
97 |
98 | ## Contact
99 |
100 | For questions or support, contact the maintainer:
101 |
102 | - **Email**: lagzen.thakuri@flagforge.xyz, contact@aryan4.com.np
103 | - **GitHub**: [aryan4859](https://github.com/aryan4859)
104 |
105 | ## Stats
106 |
107 | 
108 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # 🔒 Security Policy
2 |
3 | ## About FlagForge Security
4 |
5 | FlagForge is a Capture The Flag (CTF) platform that manages **user authentication, challenge data, and competitive scoring systems**. Given the nature of cybersecurity competitions and the sensitive data we handle, **security is at the core of our platform’s design and operations**.
6 |
7 | ## Supported Versions
8 |
9 | | Version | Supported | Notes |
10 | | ------- | --------- | ---------------------------------- |
11 | | 2.0.0 | ✅ | Current development version |
12 | | < 2.1.0 | ❌ | Pre-release versions not supported |
13 |
14 | ## Security Architecture
15 |
16 | ### 🔐 Authentication & Authorization
17 |
18 | - Google OAuth integration via NextAuth.js
19 | - Session-based authentication with secure cookies
20 | - Role-based access control (user, moderator, admin)
21 |
22 | ### 🗄️ Data Security
23 |
24 | - Encrypted MongoDB connections
25 | - Input validation & sanitization across all entry points
26 | - Secure flag validation with anti-timing attack protections
27 |
28 | ### 🛡️ Infrastructure Security
29 |
30 | - Hosted on Vercel with enforced HTTPS
31 | - Secure CI/CD pipelines via CircleCI
32 | - Secrets and environment variable isolation for sensitive configs
33 |
34 | ---
35 |
36 | ## ⚠️ Responsible Testing Notice
37 |
38 | 🚨 **Do not test on our production domain (`flagforge.xyz`)**.
39 | For all vulnerability testing, please use our **dedicated staging environment**:
40 |
41 | 👉 **[staging.flagforge.xyz](https://staging.flagforge.xyz)**
42 |
43 | This ensures testing does not affect live users or disrupt ongoing competitions.
44 |
45 | ---
46 |
47 | ## Reporting Security Vulnerabilities
48 |
49 | ### ✅ In Scope
50 |
51 | - Authentication & authorization bypasses
52 | - Injection vulnerabilities (SQL/NoSQL injection, XSS, etc.)
53 | - Server-Side Request Forgery (SSRF)
54 | - Information disclosure & sensitive data leaks
55 | - Challenge manipulation / flag extraction
56 | - Leaderboard tampering
57 | - Session management flaws
58 |
59 | ### ❌ Out of Scope
60 |
61 | - Social engineering attacks
62 | - Physical security issues
63 | - Third-party service vulnerabilities (Google OAuth, Vercel, etc.)
64 | - DoS/DDoS or brute-force attacks
65 | - Issues requiring physical access to infrastructure
66 |
67 | ### How to Report
68 |
69 |
70 | **Please include**:
71 |
72 | 1. Description of the vulnerability
73 | 2. Steps to reproduce
74 | 3. Potential impact assessment
75 | 4. Proof of concept (if applicable)
76 | 5. Suggested remediation (optional)
77 |
78 | ---
79 |
80 | ## Response Process
81 |
82 | 1. **Acknowledgment** – within 24 hours
83 | 2. **Initial Assessment** – within 7 days
84 | 3. **Weekly Updates** – provided during investigation
85 | 4. **Resolution** – dependent on severity
86 | 5. **Coordinated Disclosure** – after patch deployment
87 |
88 | ---
89 |
90 | ## Severity Classification
91 |
92 | - **Critical**: Immediate compromise, sensitive data breach, or challenge integrity violation
93 | - **High**: Privilege escalation, authentication bypass, or significant exposure
94 | - **Medium**: Information disclosure, sanitization flaws
95 | - **Low**: Minor misconfigurations, low-impact leaks
96 |
97 | ---
98 |
99 | ## Security Best Practices for Contributors
100 |
101 | ### Code Security
102 |
103 | - Validate and sanitize all inputs
104 | - Use parameterized queries
105 | - Handle errors gracefully (no sensitive debug info)
106 | - Keep dependencies updated
107 | - Follow secure coding guidelines (OWASP Top 10, CWE)
108 |
109 | ### Authentication
110 |
111 | - Never hardcode credentials in code
112 | - Use environment variables for secrets
113 | - Implement strict session handling
114 | - Follow OAuth & cookie best practices
115 |
116 | ### CTF-Specific Security
117 |
118 | - Rate-limit flag submissions
119 | - Harden flag validation against timing attacks
120 | - Store challenge files securely
121 | - Ensure challenge isolation between users
122 |
123 | ---
124 |
125 | ## Incident Response
126 |
127 | In the event of a confirmed incident:
128 |
129 | 1. Immediate containment actions
130 | 2. User notification (if applicable)
131 | 3. Root cause analysis
132 | 4. Patch & remediation
133 | 5. Post-incident review & policy updates
134 |
135 | ---
136 |
137 | ## Security Contact
138 |
139 | 📧 **Primary Contact**: security@flagforge.xyz
140 | 🐙 **GitHub Issues**: For non-sensitive discussions
141 | ⏱ **Response Time**: 24 hours for acknowledgment
142 |
143 | ---
144 |
145 | ## Compliance and Standards
146 |
147 | FlagForge adheres to:
148 |
149 | - **OWASP Secure Coding Guidelines**
150 | - **Secure SDLC practices**
151 | - **Regular penetration testing & assessments**
152 | - **Automated dependency & vulnerability scanning**
153 |
154 | ---
155 |
156 | ## Recognition
157 |
158 | We value the contributions of the security community. Researchers who responsibly disclose vulnerabilities may receive:
159 |
160 | - Public acknowledgment (with consent)
161 | - Credit in release notes
162 | - A permanent spot in the **[Hall of Fame](https://github.com/FlagForgeCTF/flagForge/blob/mainv2/HALL-OF-FAME.md)**
163 |
164 | ---
165 |
166 | _Last Updated: September 2025_
167 | _Next Review: June 2026_
168 |
--------------------------------------------------------------------------------
/app/api/admin/upload-badge-image/route.ts:
--------------------------------------------------------------------------------
1 | // File: /api/admin/badge-images/route.ts
2 | import { NextRequest, NextResponse } from "next/server";
3 | import { writeFile, mkdir, unlink } from "fs/promises";
4 | import path from "path";
5 | import { existsSync } from "fs";
6 | import connect from "@/utils/db";
7 | import { getServerSession } from "next-auth";
8 | import { authOptions } from "@/lib/authOptions";
9 | import BadgeImageModel from "@/models/badgeTemplateSchema";
10 | import userSchema from "@/models/userSchema";
11 |
12 | export const runtime = "nodejs";
13 |
14 | // Admin check helper
15 | async function requireAdmin() {
16 | const session = await getServerSession(authOptions);
17 | if (!session || !session.user?.email) {
18 | return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
19 | }
20 | await connect();
21 | const user = await userSchema.findOne({ email: session.user.email });
22 | if (!user || user.role !== "Admin") {
23 | return NextResponse.json(
24 | { error: "Admin privileges required" },
25 | { status: 403 }
26 | );
27 | }
28 | return null;
29 | }
30 |
31 | // POST /api/admin/badge-images
32 | export async function POST(request: NextRequest) {
33 | const adminCheck = await requireAdmin();
34 | if (adminCheck) return adminCheck;
35 |
36 | try {
37 | const formData = await request.formData();
38 | const file = formData.get("file") as File;
39 | const category = (formData.get("category") as string) || "badge-template";
40 | const name = (formData.get("name") as string) || "unnamed";
41 | const uploadedBy = (formData.get("uploadedBy") as string) || "unknown";
42 |
43 | if (!file)
44 | return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
45 |
46 | const allowedTypes = [
47 | "image/png",
48 | "image/jpeg",
49 | "image/jpg",
50 | "image/webp",
51 | "image/gif",
52 | "image/svg+xml",
53 | ];
54 | if (!allowedTypes.includes(file.type)) {
55 | return NextResponse.json(
56 | { error: "Invalid file type. Allowed: PNG, JPG, WebP, GIF, SVG." },
57 | { status: 400 }
58 | );
59 | }
60 |
61 | const maxSize = 5 * 1024 * 1024; // 5MB
62 | if (file.size > maxSize) {
63 | return NextResponse.json(
64 | { error: "File too large. Max size: 5MB." },
65 | { status: 400 }
66 | );
67 | }
68 |
69 | const timestamp = Date.now();
70 | const randomStr = Math.random().toString(36).substring(2, 15);
71 | const extension = path.extname(file.name).toLowerCase() || ".png";
72 | const filename = `badge-${timestamp}-${randomStr}${extension}`;
73 |
74 | const uploadDir = path.join(process.cwd(), "public", "badges", "images");
75 | if (!existsSync(uploadDir)) await mkdir(uploadDir, { recursive: true });
76 |
77 | const bytes = await file.arrayBuffer();
78 | const buffer = Buffer.from(bytes);
79 | const filePath = path.join(uploadDir, filename);
80 | await writeFile(filePath, buffer);
81 |
82 | const imagePath = `/badges/images/${filename}`;
83 |
84 | await connect(); // Ensure DB connected
85 | const badgeDoc = await BadgeImageModel.create({
86 | name: name || filename.split(".")[0],
87 | path: imagePath,
88 | category,
89 | uploadedAt: new Date(),
90 | uploadedBy,
91 | });
92 |
93 | console.log(`✅ Badge image uploaded: ${imagePath}`);
94 |
95 | return NextResponse.json({
96 | success: true,
97 | imagePath,
98 | filename,
99 | imageId: badgeDoc._id,
100 | message: "Badge image uploaded successfully",
101 | });
102 | } catch (error) {
103 | console.error("❌ Badge image upload error:", error);
104 | return NextResponse.json(
105 | { error: "Failed to upload badge image" },
106 | { status: 500 }
107 | );
108 | }
109 | }
110 |
111 | // DELETE /api/admin/badge-images
112 | export async function DELETE(request: NextRequest) {
113 | const adminCheck = await requireAdmin();
114 | if (adminCheck) return adminCheck;
115 |
116 | try {
117 | const { searchParams } = new URL(request.url);
118 | const imagePath = searchParams.get("path");
119 | if (!imagePath || !imagePath.startsWith("/badges/images/")) {
120 | return NextResponse.json(
121 | { error: "Invalid image path" },
122 | { status: 400 }
123 | );
124 | }
125 |
126 | await connect();
127 | const badgeDoc = await BadgeImageModel.findOne({ path: imagePath });
128 | if (!badgeDoc) {
129 | return NextResponse.json(
130 | { error: "Badge image not found in database" },
131 | { status: 404 }
132 | );
133 | }
134 |
135 | await badgeDoc.deleteOne();
136 |
137 | const fullPath = path.join(process.cwd(), "public", imagePath);
138 | if (existsSync(fullPath)) await unlink(fullPath);
139 |
140 | console.log(`✅ Badge image deleted: ${imagePath}`);
141 |
142 | return NextResponse.json({
143 | success: true,
144 | message: "Badge image deleted successfully",
145 | });
146 | } catch (error) {
147 | console.error("❌ Badge image deletion error:", error);
148 | return NextResponse.json(
149 | { error: "Failed to delete badge image" },
150 | { status: 500 }
151 | );
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/app/api/forgeacademy/route.ts:
--------------------------------------------------------------------------------
1 | // app/api/forgeacademy/route.ts
2 | import { NextRequest, NextResponse } from "next/server";
3 | import connect from "@/utils/db";
4 | import Question from "@/models/qustionsSchema"; // Use the correct model
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 | const { topic, difficulty } = await req.json();
9 | if (!topic) {
10 | return NextResponse.json({ error: "Missing topic" }, { status: 400 });
11 | }
12 |
13 | // Connect to MongoDB
14 | await connect();
15 |
16 | // Fetch candidate challenges from 'question' collection
17 | const regex = new RegExp(topic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
18 | const candidates = await Question.find({
19 | $or: [
20 | { title: regex },
21 | { category: regex },
22 | { description: regex },
23 | { tags: regex },
24 | ],
25 | }).lean();
26 |
27 | // Build AI prompt
28 | const systemMessage = `
29 | You are ForgeAcademy AI — an expert cybersecurity instructor at FlagForge.
30 | Create high-quality, TryHackMe-style lessons formatted exactly as JSON:
31 | {
32 | "title","id","level","estimated_time_minutes","tags","overview",
33 | "learning_objectives","sections[]","mini_lab","quiz[]","references","common_pitfalls"
34 | }
35 | Sections should include Introduction, Concepts, Practical Lab, Challenge, Summary.
36 | Do NOT include any flags in plaintext. If a lab requires a flag, set flag.type="hidden".
37 | Return only valid JSON.
38 | `;
39 |
40 | const userMessage = `Generate a detailed lesson on "${topic}" for ${
41 | difficulty || "Beginner"
42 | } learners.
43 | Constraints:
44 | - At least 3 sections with at least one code example
45 | - One mini_lab (dockerfile + instructions) running on port 8080
46 | - 3 multiple-choice quiz questions with explanations
47 | - 2-4 authoritative references
48 | - Short 'common_pitfalls' array (3 items)
49 | Candidates: ${JSON.stringify(
50 | candidates.map((c) => ({
51 | id: c._id?.toString(),
52 | title: c.title,
53 | category: c.category,
54 | description: c.description,
55 | hints: c.hints?.map((h: { text: string }) => h.text),
56 | }))
57 | )}
58 | Select the most relevant challenge and return only JSON with fields: id, title, slug (or id).
59 | Return only JSON.
60 | `;
61 |
62 | // Call OpenRouter
63 | const response = await fetch(
64 | "https://openrouter.ai/api/v1/chat/completions",
65 | {
66 | method: "POST",
67 | headers: {
68 | Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
69 | "Content-Type": "application/json",
70 | },
71 | body: JSON.stringify({
72 | model: "gpt-4o-mini",
73 | messages: [
74 | { role: "system", content: systemMessage },
75 | { role: "user", content: userMessage },
76 | ],
77 | temperature: 0.1,
78 | max_tokens: 1500,
79 | }),
80 | }
81 | );
82 |
83 | const data = await response.json();
84 | const aiText = data.choices?.[0]?.message?.content?.trim();
85 | if (!aiText) {
86 | return NextResponse.json(
87 | { error: "AI returned no content" },
88 | { status: 500 }
89 | );
90 | }
91 |
92 | // Parse AI JSON safely
93 | let lesson: any = null;
94 | try {
95 | lesson = JSON.parse(aiText);
96 | } catch {
97 | const maybeJson = aiText.match(/\{[\s\S]*\}$/);
98 | lesson = maybeJson ? JSON.parse(maybeJson[0]) : { raw: aiText };
99 | }
100 |
101 | // Pick the best challenge returned by AI, fallback to first candidate
102 | const bestChallenge =
103 | lesson?.id || lesson?.challenge_id ? lesson : candidates[0] || null;
104 |
105 | if (bestChallenge) {
106 | const challengeId = bestChallenge.id || bestChallenge._id?.toString();
107 | const challengeTitle = bestChallenge.title || "Untitled Challenge";
108 | const challengeLink = `https://flagforge.xyz/challenges/${challengeId}`;
109 |
110 | // Inject challenge link into lesson
111 | lesson.challenge_meta = {
112 | challenge_id: challengeId,
113 | challenge_title: challengeTitle,
114 | challenge_slug: challengeId,
115 | challenge_link: challengeLink,
116 | };
117 |
118 | lesson.sections = Array.isArray(lesson.sections) ? lesson.sections : [];
119 | const idx = lesson.sections.findIndex((s: any) =>
120 | /challenge/i.test(s.title || "")
121 | );
122 | const linkText = `Related existing FlagForge challenge: ${challengeTitle} — ${challengeLink}`;
123 | if (idx >= 0) {
124 | lesson.sections[idx].content += "\n\n" + linkText;
125 | } else {
126 | lesson.sections.push({ title: "Related Challenge", content: linkText });
127 | }
128 |
129 | if (!lesson.mini_lab) lesson.mini_lab = {};
130 | lesson.mini_lab.related_challenge = {
131 | id: challengeId,
132 | title: challengeTitle,
133 | url: challengeLink,
134 | };
135 | }
136 |
137 | return NextResponse.json({ success: true, data: lesson });
138 | } catch (err) {
139 | console.error("ForgeAcademy error:", err);
140 | return NextResponse.json(
141 | { error: "Lesson generation failed" },
142 | { status: 500 }
143 | );
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct - Flagforge
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behaviour that contributes to a positive environment for our
15 | community include:
16 |
17 | * Demonstrating empathy and kindness toward other people
18 | * Being respectful of differing opinions, viewpoints, and experiences
19 | * Giving and gracefully accepting constructive feedback
20 | * Accepting responsibility and apologising to those affected by our mistakes,
21 | and learning from the experience
22 | * Focusing on what is best not just for us as individuals, but for the
23 | overall community
24 |
25 | Examples of unacceptable behaviour include:
26 |
27 | * The use of sexualised language or imagery, and sexual attention or advances
28 | * Trolling, insulting or derogatory comments, and personal or political attacks
29 | * Public or private harassment
30 | * Publishing others' private information, such as a physical or email
31 | address, without their explicit permission
32 | * Other conduct which could reasonably be considered inappropriate in a
33 | professional setting
34 |
35 | ## Our Responsibilities
36 |
37 | Project maintainers are responsible for clarifying and enforcing our standards of
38 | acceptable behaviour and will take appropriate and fair corrective action in
39 | response to any behaviour that they deem inappropriate,
40 | threatening, offensive, or harmful.
41 |
42 | Project maintainers have the right and responsibility to remove, edit, or reject
43 | comments, commits, code, wiki edits, issues, and other contributions that are
44 | not aligned to this Code of Conduct, and will
45 | communicate reasons for moderation decisions when appropriate.
46 |
47 | ## Scope
48 |
49 | This Code of Conduct applies within all community spaces, and also applies when
50 | an individual is officially representing the community in public spaces.
51 | Examples of representing our community include using an official e-mail address,
52 | posting via an official social media account, or acting as an appointed
53 | representative at an online or offline event.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be
58 | reported to the community leaders responsible for enforcement at <>.
59 | All complaints will be reviewed and investigated promptly and fairly.
60 |
61 | All community leaders are obligated to respect the privacy and security of the
62 | reporter of any incident.
63 |
64 | ## Enforcement Guidelines
65 |
66 | Community leaders will follow these Community Impact Guidelines in determining
67 | the consequences for any action they deem in violation of this Code of Conduct:
68 |
69 | ### 1. Correction
70 |
71 | **Community Impact**: Use of inappropriate language or other behaviour deemed
72 | unprofessional or unwelcome in the community.
73 |
74 | **Consequence**: A private, written warning from community leaders, providing
75 | clarity around the nature of the violation and an explanation of why the
76 | behaviour was inappropriate. A public apology may be requested.
77 |
78 | ### 2. Warning
79 |
80 | **Community Impact**: A violation through a single incident or series
81 | of actions.
82 |
83 | **Consequence**: A warning with consequences for continued behaviour. No
84 | interaction with the people involved, including unsolicited interaction with
85 | those enforcing the Code of Conduct, for a specified period of time. This
86 | includes avoiding interactions in community spaces as well as external channels
87 | like social media. Violating these terms may lead to a temporary or
88 | permanent ban.
89 |
90 | ### 3. Temporary Ban
91 |
92 | **Community Impact**: A serious violation of community standards, including
93 | sustained inappropriate behaviour.
94 |
95 | **Consequence**: A temporary ban from any sort of interaction or public
96 | communication with the community for a specified period of time. No public or
97 | private interaction with the people involved, including unsolicited interaction
98 | with those enforcing the Code of Conduct, is allowed during this period.
99 | Violating these terms may lead to a permanent ban.
100 |
101 | ### 4. Permanent Ban
102 |
103 | **Community Impact**: Demonstrating a pattern of violation of community
104 | standards, including sustained inappropriate behaviour, harassment of an
105 | individual, or aggression toward or disparagement of classes of individuals.
106 |
107 | **Consequence**: A permanent ban from any sort of public interaction within
108 | the community.
109 |
110 | ## Attribution
111 |
112 | This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version
113 | [1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and
114 | [2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md),
115 | and was generated by [contributing-gen](https://github.com/bttger/contributing-gen).
116 |
--------------------------------------------------------------------------------
/app/api/auth/logout/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { getServerSession } from 'next-auth';
3 | import { getToken } from 'next-auth/jwt';
4 | import { authOptions } from '@/lib/authOptions';
5 | import { TokenBlacklistService } from '@/lib/tokenBlacklist';
6 |
7 | export async function POST(request: NextRequest) {
8 | try {
9 | const session = await getServerSession(authOptions);
10 | const token = await getToken({
11 | req: request,
12 | secret: process.env.NEXTAUTH_SECRET
13 | });
14 |
15 | // Get the raw session cookie for blacklisting
16 | const sessionToken = request.cookies.get('next-auth.session-token')?.value ||
17 | request.cookies.get('__Secure-next-auth.session-token')?.value;
18 |
19 | if (session) {
20 | console.log(`User logout: ${session.user.email} at ${new Date().toISOString()}`);
21 | }
22 |
23 | // CRITICAL: Blacklist the current token before clearing cookies
24 | if (sessionToken && token?.exp) {
25 | try {
26 | // Convert JWT exp (seconds) to Date
27 | const expiryDate = new Date(Number(token.exp) * 1000);
28 |
29 | await TokenBlacklistService.addToBlacklist(
30 | sessionToken,
31 | expiryDate,
32 | token.sub // user ID
33 | );
34 |
35 | console.log(`✅ Token blacklisted for user: ${session?.user?.email || 'unknown'}`);
36 | console.log(` Expires at: ${expiryDate.toISOString()}`);
37 | } catch (blacklistError) {
38 | console.error('❌ Failed to blacklist token during logout:', blacklistError);
39 | // Continue with logout even if blacklisting fails
40 | }
41 | } else if (sessionToken) {
42 | // If no expiry found, blacklist for 24 hours as fallback
43 | try {
44 | const expiryDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
45 | await TokenBlacklistService.addToBlacklist(
46 | sessionToken,
47 | expiryDate,
48 | session?.user?.id
49 | );
50 | console.log(`✅ Token blacklisted (24h default) for user: ${session?.user?.email || 'unknown'}`);
51 | } catch (blacklistError) {
52 | console.error('❌ Failed to blacklist token during logout:', blacklistError);
53 | }
54 | } else {
55 | console.warn('⚠️ No session token found to blacklist during logout');
56 | }
57 |
58 | const response = new NextResponse(
59 | JSON.stringify({
60 | success: true,
61 | message: 'Logged out successfully',
62 | tokenBlacklisted: !!sessionToken
63 | }),
64 | { status: 200, headers: { 'Content-Type': 'application/json' } }
65 | );
66 |
67 | // Clear NextAuth cookies with proper attributes
68 | const cookieOptions = {
69 | httpOnly: true,
70 | secure: process.env.NODE_ENV === 'production',
71 | sameSite: 'lax' as const,
72 | path: '/',
73 | };
74 |
75 | // Clear session token
76 | response.cookies.set('next-auth.session-token', '', {
77 | ...cookieOptions,
78 | expires: new Date(0),
79 | maxAge: 0,
80 | });
81 |
82 | // Also clear the secure variant
83 | response.cookies.set('__Secure-next-auth.session-token', '', {
84 | ...cookieOptions,
85 | secure: true,
86 | expires: new Date(0),
87 | maxAge: 0,
88 | });
89 |
90 | // Clear callback URL
91 | response.cookies.set('next-auth.callback-url', '', {
92 | ...cookieOptions,
93 | expires: new Date(0),
94 | maxAge: 0,
95 | });
96 |
97 | response.cookies.set('__Secure-next-auth.callback-url', '', {
98 | ...cookieOptions,
99 | secure: true,
100 | expires: new Date(0),
101 | maxAge: 0,
102 | });
103 |
104 | // Clear CSRF token
105 | response.cookies.set('next-auth.csrf-token', '', {
106 | ...cookieOptions,
107 | expires: new Date(0),
108 | maxAge: 0,
109 | });
110 |
111 | response.cookies.set('__Secure-next-auth.csrf-token', '', {
112 | ...cookieOptions,
113 | secure: true,
114 | expires: new Date(0),
115 | maxAge: 0,
116 | });
117 |
118 | // If using custom domain, also clear with domain prefix
119 | if (process.env.NODE_ENV === 'production' && process.env.NEXTAUTH_URL) {
120 | try {
121 | const domain = new URL(process.env.NEXTAUTH_URL).hostname;
122 |
123 | response.cookies.set('next-auth.session-token', '', {
124 | ...cookieOptions,
125 | domain,
126 | expires: new Date(0),
127 | maxAge: 0,
128 | });
129 |
130 | response.cookies.set('__Secure-next-auth.session-token', '', {
131 | ...cookieOptions,
132 | domain,
133 | secure: true,
134 | expires: new Date(0),
135 | maxAge: 0,
136 | });
137 | } catch (urlError) {
138 | console.error('Failed to parse NEXTAUTH_URL for domain cookies:', urlError);
139 | }
140 | }
141 |
142 | return response;
143 | } catch (error) {
144 | console.error('Logout API error:', error);
145 | return NextResponse.json(
146 | { success: false, error: 'Logout failed' },
147 | { status: 500 }
148 | );
149 | }
150 | }
151 |
152 | // GET method redirects to login
153 | export async function GET(request: NextRequest) {
154 | return NextResponse.redirect(new URL('/authentication', request.url));
155 | }
--------------------------------------------------------------------------------
/components/ResourceCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ExternalLink, Calendar, User } from 'lucide-react';
3 | import { Resource } from '@/models/Resource';
4 |
5 | interface ResourceCardProps {
6 | resource: Resource;
7 | onView?: (resource: Resource) => void;
8 | }
9 |
10 | // Constants for category colors to reduce duplication
11 | const CATEGORY_COLORS: { [key: string]: string } = {
12 | 'Tools & Frameworks': 'bg-blue-100 text-blue-800 border-blue-200',
13 | 'CTF Resources & Writeups': 'bg-red-100 text-red-800 border-red-200',
14 | 'CyberSecurity Essentials': 'bg-green-100 text-green-800 border-green-200',
15 | 'Web Essentials': 'bg-purple-100 text-purple-800 border-purple-200',
16 | 'Linux Essentials': 'bg-yellow-100 text-yellow-800 border-yellow-200',
17 | 'Blue Team': 'bg-indigo-100 text-indigo-800 border-indigo-200',
18 | 'Red Team': 'bg-rose-100 text-rose-800 border-rose-200',
19 | 'Cryptography': 'bg-cyan-100 text-cyan-800 border-cyan-200',
20 | 'Forensics': 'bg-orange-100 text-orange-800 border-orange-200',
21 | 'All': 'bg-gray-100 text-gray-800 border-gray-200',
22 | } as const;
23 |
24 | const DEFAULT_CATEGORY_COLOR = 'bg-gray-100 text-gray-800 border-gray-200';
25 |
26 | // Utility functions
27 | const getCategoryColor = (category: string): string => {
28 | return CATEGORY_COLORS[category] || DEFAULT_CATEGORY_COLOR;
29 | };
30 |
31 | const formatDate = (dateString?: string): string => {
32 | if (!dateString) return 'Unknown';
33 |
34 | try {
35 | return new Date(dateString).toLocaleDateString('en-US', {
36 | year: 'numeric',
37 | month: 'short',
38 | day: 'numeric'
39 | });
40 | } catch (error) {
41 | return 'Invalid Date';
42 | }
43 | };
44 |
45 | // Sub-components for better organization
46 | const CategoryBadge: React.FC<{ category: string }> = ({ category }) => (
47 |
48 |
52 | {category}
53 |
54 |
55 | );
56 |
57 | const ResourceDescription: React.FC<{ description: string }> = ({ description }) => (
58 |
59 |
60 | {description}
61 |
62 |
63 | );
64 |
65 | const ResourceFooter: React.FC<{ createdAt?: string; uploadedBy?: string }> = ({
66 | createdAt,
67 | uploadedBy
68 | }) => (
69 |
70 |
71 |
72 |
73 | {formatDate(createdAt)}
74 |
75 | {uploadedBy && (
76 |
77 |
78 | {uploadedBy}
79 |
80 | )}
81 |
82 |
83 | );
84 |
85 | const ResourceCard: React.FC = ({ resource, onView }) => {
86 | const handleAction = () => {
87 | if (onView) {
88 | onView(resource);
89 | } else {
90 | window.open(resource.resourceLink, '_blank', 'noopener,noreferrer,nofollow');
91 | }
92 | };
93 |
94 | const handleKeyDown = (event: React.KeyboardEvent) => {
95 | // Handle Enter and Space key presses for accessibility
96 | if (event.key === 'Enter' || event.key === ' ') {
97 | event.preventDefault();
98 | handleAction();
99 | }
100 | };
101 |
102 | // Determine appropriate aria-label based on action
103 | const ariaLabel = onView
104 | ? `View details for ${resource.title}`
105 | : `Open ${resource.title} in new tab`;
106 |
107 | const baseCardClasses = [
108 | "relative bg-white rounded-xl shadow-lg border-2 border-gray-100",
109 | "hover:border-rose-300 hover:shadow-xl hover:bg-gradient-to-br hover:from-rose-50/20 hover:to-rose-100/10",
110 | "focus:border-rose-300 focus:shadow-xl focus:bg-gradient-to-br focus:from-rose-50/20 focus:to-rose-100/10",
111 | "focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2",
112 | "transition-all duration-300 cursor-pointer group p-6 h-full flex flex-col overflow-hidden"
113 | ].join(" ");
114 |
115 | return (
116 |
124 | {/* Header */}
125 |
126 |
127 |
128 | {resource.title}
129 |
130 |
131 |
135 |
136 |
137 |
138 |
139 |
142 |
143 | );
144 | };
145 |
146 | export default ResourceCard;
--------------------------------------------------------------------------------
/app/api/problems/route.ts:
--------------------------------------------------------------------------------
1 | import connect from "@/utils/db";
2 | import { NextRequest, NextResponse } from "next/server";
3 | import QuestionModel from "@/models/qustionsSchema";
4 | import { Questions } from "@/interfaces";
5 | import { HttpStatusCode } from "axios";
6 | import userSchema from "@/models/userSchema";
7 | import { getServerSession } from "next-auth";
8 | import { authOptions } from "@/lib/authOptions";
9 | import UserQuestionModel from "@/models/userQuestionSchema";
10 | import { sendDiscordNotification } from "@/utils/discordNotifier";
11 | export const runtime = "nodejs";
12 |
13 | export async function POST(req: NextRequest) {
14 | try {
15 | await connect();
16 |
17 | //admin check
18 | const session = await getServerSession(authOptions);
19 | if (!session) {
20 | return NextResponse.json(
21 | { message: "You are not authorized to add a question" },
22 | { status: HttpStatusCode.Unauthorized }
23 | );
24 | }
25 |
26 | const user = await userSchema.findOne({ email: session?.user?.email });
27 | if (!user || user.role !== "Admin") {
28 | return NextResponse.json(
29 | { message: "You are not authorized to add a question" },
30 | { status: HttpStatusCode.Unauthorized }
31 | );
32 | }
33 |
34 | //add question
35 | const body: Questions = await req.json();
36 | if (
37 | body.title &&
38 | body.points &&
39 | body.category &&
40 | body.flag &&
41 | body.description
42 | ) {
43 | const product = await QuestionModel.create(body);
44 | await product.save();
45 | const challengeLink = `https://flagforge.xyz/problems/${product._id}`;
46 |
47 | // Send Discord notification
48 | await sendDiscordNotification(
49 | "🧩 New Challenge Released!",
50 | body.description,
51 | "NEW_CHALLENGE",
52 | body.points,
53 | body.category,
54 | challengeLink
55 | );
56 |
57 | return NextResponse.json(
58 | { success: true, message: "Your qustion has been created" },
59 | { status: HttpStatusCode.Created }
60 | );
61 | }
62 | return NextResponse.json(
63 | { message: "Something is missing!" },
64 | { status: HttpStatusCode.BadRequest }
65 | );
66 | } catch (error: any) {
67 | return NextResponse.json(
68 | { message: error?.message },
69 | { status: HttpStatusCode.BadRequest }
70 | );
71 | }
72 | }
73 |
74 | export async function GET(request: NextRequest) {
75 | const searchParams = request.nextUrl.searchParams;
76 | const qpage = parseInt(searchParams.get("page") ?? "1", 10);
77 | const page: number = qpage;
78 |
79 | // Allow custom limit from query params, default to 8 for normal pagination
80 | const requestedLimit = searchParams.get("limit");
81 | const limit = requestedLimit ? parseInt(requestedLimit, 10) : 8;
82 |
83 | // Get category filter from query params
84 | const category = searchParams.get("category");
85 |
86 | const startIndex = (page - 1) * limit;
87 | const session = await getServerSession(authOptions);
88 |
89 | if (!session) {
90 | return new Response("Unauthorized", { status: 401 });
91 | }
92 |
93 | try {
94 | await connect();
95 |
96 | // Build the base query - exclude flag
97 | let baseQuery = {};
98 |
99 | // Add category filter if provided and not "All"
100 | if (category && category !== "All") {
101 | baseQuery = { category: category };
102 | }
103 |
104 | // Build the query with category filter
105 | let query = QuestionModel.find(baseQuery).select("-flag");
106 |
107 | // Add sorting - newest first by default
108 | query = query.sort({ createdAt: -1 });
109 |
110 | // Apply pagination only if limit is reasonable (not trying to get all)
111 | if (limit <= 1000) {
112 | query = query.skip(startIndex).limit(limit);
113 | }
114 |
115 | const questions = await query.exec();
116 |
117 | const user = await userSchema.findOne({ email: session?.user?.email });
118 | if (!user) {
119 | return NextResponse.json(
120 | { success: false, message: "User not found" },
121 | { status: HttpStatusCode.NotFound }
122 | );
123 | }
124 |
125 | const userQuestion = await UserQuestionModel.find({ userId: user.id });
126 |
127 | // Get total count for pagination info (with category filter applied)
128 | const totalQuestions = await QuestionModel.countDocuments(baseQuery);
129 |
130 | // Process questions to add expiry information
131 | const now = new Date();
132 | const processedQuestions = questions.map((question) => {
133 | const questionObj = question.toObject();
134 |
135 | // Check if question has expired
136 | if (questionObj.expiryDate) {
137 | const expiryDate = new Date(questionObj.expiryDate);
138 | questionObj.expired = expiryDate < now;
139 | questionObj.timeRemaining = Math.max(
140 | 0,
141 | expiryDate.getTime() - now.getTime()
142 | );
143 | } else {
144 | questionObj.expired = false;
145 | questionObj.timeRemaining = null;
146 | }
147 |
148 | return questionObj;
149 | });
150 |
151 | return NextResponse.json({
152 | data: processedQuestions,
153 | totalScore: user.totalScore,
154 | questionDone: userQuestion,
155 | pagination: {
156 | page,
157 | limit,
158 | total: totalQuestions,
159 | totalPages: Math.ceil(totalQuestions / limit),
160 | hasNext: page < Math.ceil(totalQuestions / limit),
161 | hasPrev: page > 1,
162 | },
163 | });
164 | } catch (error) {
165 | return NextResponse.json({ error });
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/app/api/admin/badge-templates/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import connect from "@/utils/db";
3 | import BadgeTemplate from "@/models/badgeTemplate";
4 | import { getServerSession } from "next-auth/next";
5 | import UserSchema from "@/models/userSchema";
6 |
7 | export const runtime = "nodejs";
8 |
9 | async function requireAdmin(request: NextRequest) {
10 | const session = await getServerSession();
11 | if (!session || !session.user || !session.user.email) {
12 | return NextResponse.json(
13 | { success: false as false, error: "Not authenticated" },
14 | { status: 401 }
15 | );
16 | }
17 | await connect();
18 | const user = await UserSchema.findOne({ email: session.user.email });
19 | if (!user || user.role !== "Admin") {
20 | return NextResponse.json(
21 | { success: false as false, error: "Admin privileges required" },
22 | { status: 403 }
23 | );
24 | }
25 | return null; // Means admin check passed
26 | }
27 |
28 | // Type definitions
29 | interface BadgeTemplateRequest {
30 | name: string;
31 | description: string;
32 | icon: string;
33 | color?: string;
34 | isActive?: boolean;
35 | createdBy?: string;
36 | }
37 |
38 | interface MongooseError extends Error {
39 | name: string;
40 | errors?: { [key: string]: { message: string } };
41 | code?: number;
42 | }
43 |
44 | // GET - Fetch all badge templates
45 | export async function GET(request: NextRequest): Promise {
46 | try {
47 | await connect();
48 | // Admin check
49 | const adminCheck = await requireAdmin(request);
50 | if (adminCheck) return adminCheck;
51 |
52 | console.log("Fetching badge templates");
53 |
54 | const templates = await BadgeTemplate.find({})
55 | .sort({ createdAt: -1 })
56 | .select("-createdBy")
57 | .lean(); // Use lean() for better performance when you don't need Mongoose document methods
58 |
59 | console.log(`Fetched ${templates.length} badge templates`);
60 |
61 | return NextResponse.json({
62 | success: true,
63 | templates: templates,
64 | count: templates.length,
65 | });
66 | } catch (error) {
67 | console.error("Badge template fetch error:", error);
68 |
69 | const errMsg =
70 | error instanceof Error ? error.message : "Unexpected error occurred";
71 |
72 | return NextResponse.json(
73 | { error: "Failed to fetch badge templates", details: errMsg },
74 | { status: 500 }
75 | );
76 | }
77 | }
78 |
79 | // POST - Create new badge template
80 | export async function POST(request: NextRequest): Promise {
81 | try {
82 | // Admin check
83 | const adminCheck = await requireAdmin(request);
84 | if (adminCheck) return adminCheck;
85 |
86 | // Parse request body
87 | const body: BadgeTemplateRequest = await request.json();
88 | const { name, description, icon, color, isActive, createdBy } = body;
89 |
90 | console.log("Creating badge template:", {
91 | name,
92 | description,
93 | icon,
94 | color,
95 | isActive,
96 | createdBy,
97 | });
98 |
99 | // Validation
100 | if (!name || !name.trim()) {
101 | return NextResponse.json(
102 | { error: "Badge name is required" },
103 | { status: 400 }
104 | );
105 | }
106 |
107 | if (!description || !description.trim()) {
108 | return NextResponse.json(
109 | { error: "Badge description is required" },
110 | { status: 400 }
111 | );
112 | }
113 |
114 | if (!icon || !icon.trim()) {
115 | return NextResponse.json(
116 | { error: "Badge icon is required" },
117 | { status: 400 }
118 | );
119 | }
120 |
121 | await connect();
122 |
123 | // Check if template with same name already exists
124 | const existingTemplate = await BadgeTemplate.findOne({
125 | name: name.trim(),
126 | });
127 |
128 | if (existingTemplate) {
129 | return NextResponse.json(
130 | { error: "A badge template with this name already exists" },
131 | { status: 400 }
132 | );
133 | }
134 |
135 | // Create new template
136 | const templateData = {
137 | name: name.trim(),
138 | description: description.trim(),
139 | icon: icon.trim(),
140 | color: color || "#8B5CF6",
141 | isActive: isActive !== undefined ? isActive : true,
142 | createdBy: createdBy || "unknown",
143 | };
144 |
145 | const createdTemplate = await BadgeTemplate.create(templateData);
146 |
147 | console.log(`Badge template created: ${name} by ${createdBy}`);
148 |
149 | return NextResponse.json({
150 | success: true,
151 | template: createdTemplate,
152 | message: "Badge template created successfully",
153 | });
154 | } catch (error) {
155 | console.error("Badge template creation error:", error);
156 |
157 | const mongooseError = error as MongooseError;
158 |
159 | // Handle Mongoose validation errors
160 | if (mongooseError.name === "ValidationError" && mongooseError.errors) {
161 | const validationErrors = Object.values(mongooseError.errors).map(
162 | (err) => err.message
163 | );
164 | return NextResponse.json(
165 | { error: "Validation failed", details: validationErrors.join(", ") },
166 | { status: 400 }
167 | );
168 | }
169 |
170 | // Handle duplicate key errors
171 | if (mongooseError.code === 11000) {
172 | return NextResponse.json(
173 | { error: "A badge template with this name already exists" },
174 | { status: 400 }
175 | );
176 | }
177 |
178 | const errMsg =
179 | error instanceof Error ? error.message : "Unexpected error occurred";
180 |
181 | return NextResponse.json(
182 | { error: "Failed to create badge template", details: errMsg },
183 | { status: 500 }
184 | );
185 | }
186 | }
187 |
--------------------------------------------------------------------------------