Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,31 @@ async function bootstrap(): Promise<void> {
exclude: ['docs', 'docs-json', 'docs/(.*)'],
});

const corsOrigin = configService.get<string>('appConfig.cors.origin') ?? '*';
const credentials = configService.get<boolean>('appConfig.cors.credentials') ?? true;

app.enableCors({
origin: configService.get<string>('appConfig.cors.origin') ?? '*',
origin: (origin, callback) => {
if (credentials && corsOrigin === '*') {
const allowlist = process.env.FRONTEND_URL ? process.env.FRONTEND_URL.split(',') : [];
if (!origin || allowlist.includes(origin) || origin === 'http://localhost:3000') {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
} else {
if (corsOrigin === '*') {
callback(null, true);
} else {
const allowlist = corsOrigin.split(',');
if (!origin || allowlist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}
},
methods: configService.get<string[]>('appConfig.cors.methods') ?? [
'GET',
'POST',
Expand All @@ -51,8 +74,7 @@ async function bootstrap(): Promise<void> {
allowedHeaders: configService.get<string[]>(
'appConfig.cors.allowedHeaders',
) ?? ['Content-Type', 'Authorization'],
credentials:
configService.get<boolean>('appConfig.cors.credentials') ?? true,
credentials,
});

app.use(helmet({
Expand Down
5 changes: 4 additions & 1 deletion backend/src/puzzle/puzzle.service.ts
Original file line number Diff line number Diff line change
@@ -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 { Puzzle } from './puzzle.entity';
Expand Down Expand Up @@ -34,6 +34,9 @@ export class PuzzleService {

async update(id: string, updatePuzzleDto: UpdatePuzzleDto): Promise<Puzzle> {
const puzzle = await this.findOneAdmin(id);
if (puzzle.isActive && updatePuzzleDto.solution && updatePuzzleDto.solution !== puzzle.solution) {
throw new BadRequestException('Cannot edit the solution of an active/published puzzle in place. Use versioned answer data.');
}
Object.assign(puzzle, updatePuzzleDto);
puzzle.title = sanitizeText(puzzle.title);
puzzle.description = sanitizeText(puzzle.description);
Expand Down
2 changes: 2 additions & 0 deletions onchain/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions onchain/contracts/stellar_hunts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub enum DataKey {
PlayerProgress(Address),
PlayerLevelProgress(Address, Levels),
SchemaVersion,
Paused,
}

// ---------------------------------------------------------------------
Expand Down Expand Up @@ -102,6 +103,7 @@ pub enum Error {
MissingNftContract = 9,
AttemptTooSoon = 10,
LevelImmutable = 11,
ContractPaused = 12,
}

// ---------------------------------------------------------------------
Expand Down Expand Up @@ -333,6 +335,9 @@ impl StellarHunts {
// -----------------------------------------------------------------

pub fn submit_answer(env: Env, caller: Address, question_id: u64, answer: Bytes) -> bool {
if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) {
panic_with_error!(&env, Error::ContractPaused);
}
caller.require_auth();

if !env
Expand Down Expand Up @@ -467,6 +472,9 @@ impl StellarHunts {
}

pub fn claim_level_completion_nft(env: Env, caller: Address, level: Levels) {
if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) {
panic_with_error!(&env, Error::ContractPaused);
}
caller.require_auth();

if !env
Expand Down Expand Up @@ -592,6 +600,20 @@ impl StellarHunts {
level.next()
}

pub fn pause(env: Env) {
require_admin(&env);
env.storage().instance().set(&DataKey::Paused, &true);
}

pub fn unpause(env: Env) {
require_admin(&env);
env.storage().instance().set(&DataKey::Paused, &false);
}

pub fn is_paused(env: Env) -> bool {
env.storage().instance().get(&DataKey::Paused).unwrap_or(false)
}

pub fn get_schema_version(e: Env) -> u32 {
get_schema_version(&e)
}
Expand Down
17 changes: 17 additions & 0 deletions onchain/contracts/stellar_hunts/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,3 +691,20 @@ fn test_schema_version() {

assert_eq!(client.get_schema_version(), 1);
}

#[test]
fn test_unauthorized_add_question_fails() {
let env = Env::default();
let admin = Address::generate(&env);
let user = Address::generate(&env);
let contract_id = env.register_contract(None, StellarHunts);
let client = StellarHuntsClient::new(&env, &contract_id);
client.init(&admin);

env.mock_all_auths();
// Call as normal user
let should_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.add_question(&Levels::Easy, &Bytes::from_slice(&env, b"q"), &Bytes::from_slice(&env, b"a"), &Bytes::from_slice(&env, b"h"));
}));
assert!(should_panic.is_err());
}
21 changes: 21 additions & 0 deletions onchain/contracts/stellar_hunts_nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub use stellar_hunts_types::Levels;
#[derive(Clone)]
pub enum NftDataKey {
Admin,
Paused,
Minters(Address),
Badge(Address, Levels),
BadgeData(Address, Levels),
Expand Down Expand Up @@ -52,6 +53,7 @@ pub enum Error {
AlreadyInitialized = 3,
InvalidBaseUri = 4,
MetadataTooLarge = 5,
ContractPaused = 6,
}

const MAX_BASE_URI_LEN: usize = 200;
Expand Down Expand Up @@ -120,8 +122,27 @@ impl StellarHuntsNft {
/// authorises the mint. This is the v22 replacement for the previous
/// `env.invoker()`-based check: in normal operation the StellarHunts
/// game contract passes its own contract address as `minter`.
pub fn pause(env: Env) {
let admin: Address = env.storage().instance().get(&NftDataKey::Admin).unwrap();
admin.require_auth();
env.storage().instance().set(&NftDataKey::Paused, &true);
}

pub fn unpause(env: Env) {
let admin: Address = env.storage().instance().get(&NftDataKey::Admin).unwrap();
admin.require_auth();
env.storage().instance().set(&NftDataKey::Paused, &false);
}

pub fn is_paused(env: Env) -> bool {
env.storage().instance().get(&NftDataKey::Paused).unwrap_or(false)
}

pub fn mint_level_badge(env: Env, minter: Address, recipient: Address, level: Levels) {
minter.require_auth();
if env.storage().instance().get(&NftDataKey::Paused).unwrap_or(false) {
panic_with_error!(&env, Error::ContractPaused);
}

if !Self::has_minter_role(env.clone(), minter.clone()) {
panic_with_error!(&env, Error::NotAuthorized);
Expand Down
8 changes: 1 addition & 7 deletions onchain/contracts/stellar_hunts_nft/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Loading