diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56f8b905..458e65ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: name: Build contracts runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # Cache Scarb / cargo artifacts and any (future) root-level # node_modules. Keyed on the lockfile hash so a dependency change @@ -32,7 +32,7 @@ jobs: # included so that when npm-based jobs are added in the future, # the cache key already covers them. - name: Cache Scarb, Cargo and node_modules - uses: actions/cache@v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: | ~/.cargo/registry @@ -46,9 +46,9 @@ jobs: restore-keys: | ${{ runner.os }}-scarb-cargo- - - uses: software-mansion/setup-scarb@v1 + - uses: software-mansion/setup-scarb@2a96b748888e3329ee44ac9ac073d930e692b3cd # v1.6.2 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: targets: wasm32-unknown-unknown components: rustfmt @@ -66,7 +66,7 @@ jobs: # Audit dependencies for security advisories, license compliance, # and duplicate crate versions. - name: Install cargo-deny - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2.87.2 with: tool: cargo-deny @@ -78,10 +78,10 @@ jobs: name: Test contracts runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Cache Scarb, Cargo and node_modules - uses: actions/cache@v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: | ~/.cargo/registry @@ -95,14 +95,14 @@ jobs: restore-keys: | ${{ runner.os }}-scarb-cargo- - - uses: software-mansion/setup-scarb@v1 + - uses: software-mansion/setup-scarb@2a96b748888e3329ee44ac9ac073d930e692b3cd # v1.6.2 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: toolchain: stable - name: Install cargo-deny - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2.87.2 with: tool: cargo-deny @@ -127,7 +127,7 @@ jobs: run: cargo test --workspace --locked -- bench_ --nocapture 2>&1 | tee bench-output.txt - name: Upload bench artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: bench-output path: onchain/bench-output.txt @@ -141,10 +141,10 @@ jobs: name: Backend lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 cache: npm @@ -176,10 +176,10 @@ jobs: name: Backend tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 cache: npm @@ -204,10 +204,10 @@ jobs: name: Frontend lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 cache: npm @@ -240,10 +240,10 @@ jobs: name: Frontend build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 cache: npm @@ -265,10 +265,10 @@ jobs: runs-on: ubuntu-latest needs: frontend-build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 cache: npm @@ -307,10 +307,10 @@ jobs: name: Frontend tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f94f50d4..3aaeddb4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: # Fetch all history so git-cliff / auto-changelog can read commits fetch-depth: 0 @@ -36,12 +36,12 @@ jobs: # ── Install toolchains ──────────────────────────────── - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: targets: wasm32-unknown-unknown - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 cache: npm @@ -139,7 +139,7 @@ jobs: # ── Create GitHub Release ───────────────────────────── - name: Create Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: tag_name: ${{ github.ref_name }} name: v${{ steps.tag.outputs.version }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 33de5b9c..057f0827 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -26,18 +26,18 @@ jobs: language: [javascript-typescript] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@a35ac6e6798d72df5475948b28efb89edc2e19ca # v4.37.9 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v4 + uses: github/codeql-action/autobuild@a35ac6e6798d72df5475948b28efb89edc2e19ca # v4.37.9 - name: Analyze - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@a35ac6e6798d72df5475948b28efb89edc2e19ca # v4.37.9 with: category: /language:${{ matrix.language }} @@ -50,7 +50,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 # Requires the repository's "Dependency graph" setting to be enabled # (Settings → Code security and analysis). Until that is flipped, the @@ -59,7 +59,7 @@ jobs: # blocking gate as soon as the setting is enabled. - name: Review dependency changes continue-on-error: true - uses: actions/dependency-review-action@v5 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: fail-on-severity: high fail-on-scopes: runtime, development @@ -102,7 +102,7 @@ jobs: security-events: write steps: - name: Checkout full history - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 0 @@ -119,13 +119,13 @@ jobs: - name: Upload SARIF to code scanning if: always() - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@a35ac6e6798d72df5475948b28efb89edc2e19ca # v4.37.9 with: sarif_file: gitleaks.sarif - name: Upload SARIF artifact if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: gitleaks-sarif path: gitleaks.sarif diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 71d80d40..d75f7fba 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,4 +1,5 @@ -import { Module } from '@nestjs/common'; +import { CsrfMiddleware } from './common/security/csrf.middleware'; +import { NestModule, MiddlewareConsumer } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { join } from 'path'; @@ -147,4 +148,8 @@ import { GracefulShutdownService } from './graceful-shutdown.service'; controllers: [AppController], providers: [AppService, GracefulShutdownService], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CsrfMiddleware).forRoutes('*'); + } +} diff --git a/backend/src/common/security/csrf.middleware.ts b/backend/src/common/security/csrf.middleware.ts new file mode 100644 index 00000000..6a9e7be7 --- /dev/null +++ b/backend/src/common/security/csrf.middleware.ts @@ -0,0 +1,17 @@ +import { Injectable, NestMiddleware, ForbiddenException } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class CsrfMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + const method = req.method; + if (['GET', 'HEAD', 'OPTIONS'].includes(method)) { + return next(); + } + const csrfToken = req.headers['x-csrf-token']; + if (!csrfToken) { + throw new ForbiddenException('CSRF token missing'); + } + next(); + } +} diff --git a/backend/src/puzzle-draft/draft-puzzle.service.ts b/backend/src/puzzle-draft/draft-puzzle.service.ts index b867b11b..e9c7374f 100644 --- a/backend/src/puzzle-draft/draft-puzzle.service.ts +++ b/backend/src/puzzle-draft/draft-puzzle.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { DraftPuzzle } from './entities/draft-puzzle.entity'; @@ -29,6 +29,23 @@ export class DraftPuzzleService { async update(id: string, updateDto: UpdateDraftDto) { const draft = await this.findOne(id); + if (draft.status === 'published') { + throw new BadRequestException('Cannot edit a published draft.'); + } + + if (updateDto.status) { + const allowedTransitions: Record = { + draft: ['review'], + review: ['draft', 'approved'], + approved: ['review', 'published'], + published: [] + }; + const allowed = allowedTransitions[draft.status] || []; + if (!allowed.includes(updateDto.status)) { + throw new BadRequestException(`Invalid status transition from ${draft.status} to ${updateDto.status}`); + } + } + Object.assign(draft, updateDto); return this.draftRepo.save(draft); } @@ -40,7 +57,11 @@ export class DraftPuzzleService { async publish(id: string) { const draft = await this.findOne(id); - // Emit event or return structured data for publishing module to handle + if (draft.status !== 'approved') { + throw new BadRequestException('Only approved drafts can be published.'); + } + draft.status = 'published'; + await this.draftRepo.save(draft); return { event: 'PUZZLE_DRAFT_PUBLISHED', data: draft, diff --git a/backend/src/puzzle-draft/entities/draft-puzzle.entity.ts b/backend/src/puzzle-draft/entities/draft-puzzle.entity.ts index 6d7c561b..0b2717c4 100644 --- a/backend/src/puzzle-draft/entities/draft-puzzle.entity.ts +++ b/backend/src/puzzle-draft/entities/draft-puzzle.entity.ts @@ -23,6 +23,9 @@ export class DraftPuzzle { @Column() createdBy: string; + @Column({ default: 'draft' }) + status: string; + @CreateDateColumn() createdAt: Date; diff --git a/frontend/app/ref/[referralId]/page.js b/frontend/app/ref/[referralId]/page.js index 3c97e317..01f74e5e 100644 --- a/frontend/app/ref/[referralId]/page.js +++ b/frontend/app/ref/[referralId]/page.js @@ -1,221 +1,221 @@ -"use client"; -import React, { useState, useEffect } from "react"; -import { useParams, useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { - Gift, - Star, - Users, - ArrowRight, - CheckCircle, - Sparkles, - Trophy -} from "lucide-react"; - -export default function ReferralLandingPage() { - const params = useParams(); - const router = useRouter(); - const [referrer, setReferrer] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - // In a real app, you would fetch referrer data from the backend - // For now, we'll simulate it - setTimeout(() => { - setReferrer({ - username: "crypto_explorer", - avatar: "/placeholder.svg", - totalInvites: 8, - level: 15 - }); - setLoading(false); - }, 1000); - }, []); - - const handleGetStarted = () => { - // Store referral info in localStorage or state management - localStorage.setItem("referralId", params.referralId); - router.push("/register"); - }; - - const referralBonuses = [ - { - icon: Gift, - title: "Welcome NFT", - description: "Get a free NFT just for joining through referral", - color: "purple" - }, - { - icon: Star, - title: "50 XP Bonus", - description: "Start your journey with extra experience points", - color: "yellow" - }, - { - icon: Trophy, - title: "Exclusive Badge", - description: "Show off your referral status with a special badge", - color: "pink" - } - ]; - - if (loading) { - return ( -
-
-
-

Loading referral...

-
-
- ); - } - - return ( -
-
- {/* Header */} -
-
-
- -
+"use client"; +import React, { useState, useEffect } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { + Gift, + Star, + Users, + ArrowRight, + CheckCircle, + Sparkles, + Trophy +} from "lucide-react"; + +export default function ReferralLandingPage() { + const params = useParams(); + const router = useRouter(); + const [referrer, setReferrer] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // In a real app, you would fetch referrer data from the backend + // For now, we'll simulate it + setTimeout(() => { + setReferrer({ + username: "crypto_explorer", + avatar: "/placeholder.svg", + totalInvites: 8, + level: 15 + }); + setLoading(false); + }, 1000); + }, []); + + const handleGetStarted = () => { + // Store referral info in localStorage or state management + localStorage.setItem("referralId", params.referralId); + router.push("/register"); + }; + + const referralBonuses = [ + { + icon: Gift, + title: "Welcome NFT", + description: "Get a free NFT just for joining through referral", + color: "purple" + }, + { + icon: Star, + title: "50 XP Bonus", + description: "Start your journey with extra experience points", + color: "yellow" + }, + { + icon: Trophy, + title: "Exclusive Badge", + description: "Show off your referral status with a special badge", + color: "pink" + } + ]; + + if (loading) { + return ( +
+
+
+

Loading referral...

+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+
+ +

You've Been Invited!

-

- {referrer?.username} invited you to join StellarHunts! -

-
-
- - {/* Referrer Info */} - -
-
- - {referrer?.username?.charAt(0).toUpperCase()} - -
-
-

{referrer?.username}

-

Level {referrer?.level} Explorer

-
-
-
-
-

{referrer?.totalInvites}

-

Friends Invited

-
-
-

8

-

Puzzles Solved

-
-
-

3

-

NFTs Collected

-
-
-
- - {/* Special Bonuses */} -
-

- - Special Referral Bonuses -

-
- {referralBonuses.map((bonus, index) => ( - -
- -
-

{bonus.title}

-

{bonus.description}

-
- ))} -
-
- - {/* Game Preview */} - -
-

What Awaits You

+

+ {referrer?.username} invited you to join StellarHunts! +

+
+
+ + {/* Referrer Info */} + +
+
+ + {referrer?.username?.charAt(0).toUpperCase()} + +
+
+

{referrer?.username}

+

Level {referrer?.level} Explorer

+
+
+
+
+

{referrer?.totalInvites}

+

Friends Invited

+
+
+

8

+

Puzzles Solved

+
+
+

3

+

NFTs Collected

+
+
+
+ + {/* Special Bonuses */} +
+

+ + Special Referral Bonuses +

+
+ {referralBonuses.map((bonus, index) => ( + +
+ +
+

{bonus.title}

+

{bonus.description}

+
+ ))} +
+
+ + {/* Game Preview */} + +
+

What Awaits You

Embark on an epic digital treasure hunt where you'll solve cryptographic puzzles, collect rare NFTs, and compete with players worldwide!

-
- -
-
-
- - Solve challenging cryptographic puzzles -
-
- - Collect exclusive NFT rewards -
-
- - Compete on global leaderboards -
-
-
-
- - Earn XP and level up -
-
- - Join a vibrant community -
-
- - Unlock special achievements -
-
-
-
- - {/* CTA Section */} -
- -

- Ready to Start Your Adventure? -

+
+ +
+
+
+ + Solve challenging cryptographic puzzles +
+
+ + Collect exclusive NFT rewards +
+
+ + Compete on global leaderboards +
+
+
+
+ + Earn XP and level up +
+
+ + Join a vibrant community +
+
+ + Unlock special achievements +
+
+
+ + + {/* CTA Section */} +
+ +

+ Ready to Start Your Adventure? +

Join thousands of players in the ultimate StellarHunts challenge. Your friend's referral gives you exclusive bonuses to get started!

-
- - -
- - {/* Referral Code Display */} -
-

Referral Code:

-

{params.referralId}

-
-
-
-
-
- ); +
+ + +
+ + {/* Referral Code Display */} +
+

Referral Code:

+

{params.referralId}

+
+ +
+
+
+ ); } diff --git a/onchain/Cargo.lock b/onchain/Cargo.lock index 16ed8f8e..05ba5cf4 100644 --- a/onchain/Cargo.lock +++ b/onchain/Cargo.lock @@ -1429,6 +1429,8 @@ name = "stellar-hunts-receiver" version = "0.1.0" dependencies = [ "soroban-sdk", + "stellar-hunts-nft", + "stellar-hunts-types", ] [[package]] diff --git a/onchain/contracts/stellar_hunts/src/lib.rs b/onchain/contracts/stellar_hunts/src/lib.rs index 6a3648e5..9dd107d1 100644 --- a/onchain/contracts/stellar_hunts/src/lib.rs +++ b/onchain/contracts/stellar_hunts/src/lib.rs @@ -531,11 +531,12 @@ impl StellarHunts { // ----------------------------------------------------------------- pub fn get_question(env: Env, question_id: u64) -> Question { - env.storage() + match env.storage() .persistent() - .get(&DataKey::Question(question_id)) - .ok_or(Error::QuestionNotFound) - .unwrap() + .get(&DataKey::Question(question_id)) { + Some(q) => q, + None => panic_with_error!(&env, Error::QuestionNotFound), + } } pub fn get_question_per_level(env: Env) -> u32 { @@ -551,11 +552,13 @@ impl StellarHunts { .persistent() .get(&DataKey::QuestionsByLevel(level, index)) .unwrap_or(0u64); - let q: Question = env + let q: Question = match env .storage() .persistent() - .get(&DataKey::Question(question_id)) - .unwrap(); + .get(&DataKey::Question(question_id)) { + Some(q) => q, + None => panic_with_error!(&env, Error::QuestionNotFound), + }; q.question } @@ -564,12 +567,18 @@ impl StellarHunts { if !env.storage().persistent().has(&pp_key) { return Levels::Easy; } - let pp: PlayerProgress = env.storage().persistent().get(&pp_key).unwrap(); + let pp: PlayerProgress = match env.storage().persistent().get(&pp_key) { + Some(pp) => pp, + None => panic_with_error!(&env, Error::NotInitialized), + }; pp.current_level } pub fn get_nft_contract_address(env: Env) -> Address { - env.storage().instance().get(&DataKey::NftContract).unwrap() + match env.storage().instance().get(&DataKey::NftContract) { + Some(addr) => addr, + None => panic_with_error!(&env, Error::MissingNftContract), + } } pub fn get_player_level_progress(env: Env, player: Address, level: Levels) -> LevelProgress { diff --git a/onchain/contracts/stellar_hunts_nft/src/test.rs b/onchain/contracts/stellar_hunts_nft/src/test.rs index dfb3db41..1e27830e 100644 --- a/onchain/contracts/stellar_hunts_nft/src/test.rs +++ b/onchain/contracts/stellar_hunts_nft/src/test.rs @@ -84,13 +84,7 @@ fn test_double_mint_rejected() { game.mint(&nft_id, &r, &crate::Levels::Easy); // Second mint must fail (already-has-badge error). let should_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - client.init( - &admin, - &game, - &String::from_str(&env, &long_uri), - &String::from_str(&env, "StellarHuntsBadge"), - &String::from_str(&env, "SHB"), - ); + game.mint(&nft_id, &r, &crate::Levels::Easy); })); assert!(should_panic.is_err()); }