├── 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
5 | {children} 6 | 7 |
; 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 | 404image 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 | 404image 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 | 404image 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 | 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 |
19 | 25 | 31 | 32 |
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 | 55 | 56 | 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 | Hall of Fame 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/about2025-11-27T15:53:33.289Zmonthly0.7 4 | https://flagforge.xyz/contact2025-11-27T15:53:33.289Zmonthly0.7 5 | https://flagforge.xyz/cookie-consent2025-11-27T15:53:33.289Zmonthly0.7 6 | https://flagforge.xyz/licensing2025-11-27T15:53:33.289Zmonthly0.7 7 | https://flagforge.xyz/privacy-policy2025-11-27T15:53:33.289Zmonthly0.7 8 | https://flagforge.xyz/terms-of-service2025-11-27T15:53:33.289Zmonthly0.7 9 | https://flagforge.xyz/authentication2025-11-27T15:53:33.289Zmonthly0.7 10 | https://flagforge.xyz/blogs/introduction2025-11-27T15:53:33.289Zmonthly0.7 11 | https://flagforge.xyz/blogs2025-11-27T15:53:33.290Zmonthly0.7 12 | https://flagforge.xyz/home2025-11-27T15:53:33.290Zmonthly0.7 13 | https://flagforge.xyz/leaderboard2025-11-27T15:53:33.290Zmonthly0.7 14 | https://flagforge.xyz/problems2025-11-27T15:53:33.290Zmonthly0.7 15 | https://flagforge.xyz/profile2025-11-27T15:53:33.290Zmonthly0.7 16 | https://flagforge.xyz/resources2025-11-27T15:53:33.290Zmonthly0.7 17 | https://flagforge.xyz/resources/upload2025-11-27T15:53:33.290Zmonthly0.7 18 | https://flagforge.xyz/roles/developers/admins/auth2025-11-27T15:53:33.290Zmonthly0.7 19 | https://flagforge.xyz/roles/developers/admins/badge-templates2025-11-27T15:53:33.290Zmonthly0.7 20 | https://flagforge.xyz/roles/developers/admins/badges2025-11-27T15:53:33.290Zmonthly0.7 21 | https://flagforge.xyz/roles/developers/admins2025-11-27T15:53:33.290Zmonthly0.7 22 | https://flagforge.xyz/roles/developers/admins/uploads2025-11-27T15:53:33.290Zmonthly0.7 23 | https://flagforge.xyz/unauthorized2025-11-27T15:53:33.290Zmonthly0.7 24 | https://flagforge.xyz2025-11-27T15:53:33.290Zmonthly0.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 | 122 | ); 123 | }; 124 | 125 | export default CategoryButton; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Flagforge

2 | 3 |

project-image

4 | 5 |

6 | shields 7 | shields 8 | shields 9 | shields 10 | shields 11 | shields 12 | shields

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 | javascript typescript 32 | mongodb 33 | nextjs 34 | circleci

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 | ![Alt](https://repobeats.axiom.co/api/embed/02af8e8621d7a600aa56c45db6612f56af820bc4.svg "Repobeats analytics image") 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 |
75 | {uploadedBy && ( 76 |
77 |
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 |
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 | --------------------------------------------------------------------------------