Skip to content

Commit b372b9f

Browse files
authored
Merge pull request #427 from demilade18-git/feat/issue-279-295-301-316-cors-oracle-blockchain-bridge-animations
feat: implement CORS middleware, Oracle role, and BlockchainService score bridge
2 parents 13b333d + c40f071 commit b372b9f

6 files changed

Lines changed: 143 additions & 7 deletions

File tree

middleware/src/auth/rbac.middleware.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import { Request, Response, NextFunction } from 'express';
44
export enum UserRole {
55
USER = 'USER',
66
MODERATOR = 'MODERATOR',
7+
/** Trusted score submitter — can call blockchain score endpoints (Issue #295). */
8+
ORACLE = 'ORACLE',
79
ADMIN = 'ADMIN',
810
}
911

1012
/**
11-
* ADMIN inherits all MODERATOR and USER permissions.
12-
* MODERATOR inherits all USER permissions.
13+
* ADMIN inherits all permissions.
14+
* ORACLE can submit trusted scores (USER-level API access + score submission).
15+
* MODERATOR inherits USER permissions.
1316
*/
1417
const ROLE_HIERARCHY: Record<UserRole, UserRole[]> = {
15-
[UserRole.ADMIN]: [UserRole.ADMIN, UserRole.MODERATOR, UserRole.USER],
18+
[UserRole.ADMIN]: [UserRole.ADMIN, UserRole.ORACLE, UserRole.MODERATOR, UserRole.USER],
19+
[UserRole.ORACLE]: [UserRole.ORACLE, UserRole.USER],
1620
[UserRole.MODERATOR]: [UserRole.MODERATOR, UserRole.USER],
1721
[UserRole.USER]: [UserRole.USER],
1822
};

middleware/src/blockchain/blockchain.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { RegisterPlayerProvider } from './providers/register-player.provider';
66
import { SubmitPuzzleProvider } from './providers/submit-puzzle.provider';
77
import { SyncXpMilestoneProvider } from './providers/sync-xp-milestone.provider';
88
import { SyncStreakProvider } from './providers/sync-streak.provider';
9+
import { ScoreSubmissionBridge } from './score-submission.bridge';
910

1011
/**
1112
* BlockchainModule — Issue #307
@@ -37,7 +38,8 @@ import { SyncStreakProvider } from './providers/sync-streak.provider';
3738
SubmitPuzzleProvider,
3839
SyncXpMilestoneProvider,
3940
SyncStreakProvider,
41+
ScoreSubmissionBridge,
4042
],
41-
exports: [BlockchainService],
43+
exports: [BlockchainService, ScoreSubmissionBridge],
4244
})
4345
export class BlockchainModule {}

middleware/src/blockchain/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ export * from './providers/sync-streak.provider';
1111

1212
// Issue #308 — Wallet linking provider and its interfaces
1313
export * from './link-wallet.provider';
14+
15+
// Issue #301 — Score submission bridge (Oracle/Admin → Stellar contract)
16+
export * from './score-submission.bridge';
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Injectable, Logger, ForbiddenException } from '@nestjs/common';
2+
import { UserRole } from '../auth/rbac.middleware';
3+
import { BlockchainService } from './blockchain.service';
4+
5+
export interface TrustedScorePayload {
6+
stellarWallet: string;
7+
puzzleId: string;
8+
categoryId: string;
9+
/** Normalized 0–100 score */
10+
score: number;
11+
submittedBy: string;
12+
submitterRole: UserRole;
13+
}
14+
15+
export interface TrustedScoreResult {
16+
success: boolean;
17+
stellarWallet: string;
18+
puzzleId: string;
19+
}
20+
21+
/**
22+
* ScoreSubmissionBridge — Issue #301
23+
*
24+
* Bridges the backend score verification layer and the Stellar smart contract.
25+
* Only ORACLE or ADMIN roles may submit trusted scores on-chain.
26+
*
27+
* Usage:
28+
* await this.scoreSubmissionBridge.submitTrustedScore({
29+
* stellarWallet, puzzleId, categoryId, score,
30+
* submittedBy: req.user.sub,
31+
* submitterRole: req.user.userRole,
32+
* });
33+
*/
34+
@Injectable()
35+
export class ScoreSubmissionBridge {
36+
private readonly logger = new Logger(ScoreSubmissionBridge.name);
37+
38+
constructor(private readonly blockchainService: BlockchainService) {}
39+
40+
async submitTrustedScore(payload: TrustedScorePayload): Promise<TrustedScoreResult> {
41+
const { stellarWallet, puzzleId, categoryId, score, submittedBy, submitterRole } = payload;
42+
43+
if (submitterRole !== UserRole.ORACLE && submitterRole !== UserRole.ADMIN) {
44+
throw new ForbiddenException('Only ORACLE or ADMIN roles may submit trusted scores.');
45+
}
46+
47+
if (score < 0 || score > 100) {
48+
throw new Error(`Score must be 0–100, received ${score}`);
49+
}
50+
51+
this.logger.log(
52+
`Trusted score: ${submittedBy} (${submitterRole}) → wallet=${stellarWallet} puzzle=${puzzleId} score=${score}`,
53+
);
54+
55+
await this.blockchainService.submitPuzzleOnChain(stellarWallet, puzzleId, categoryId, score);
56+
57+
return { success: true, stellarWallet, puzzleId };
58+
}
59+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Injectable, NestMiddleware } from '@nestjs/common';
2+
import { Request, Response, NextFunction } from 'express';
3+
4+
export interface CorsOptions {
5+
origins?: string | string[];
6+
methods?: string[];
7+
allowedHeaders?: string[];
8+
exposedHeaders?: string[];
9+
credentials?: boolean;
10+
maxAge?: number;
11+
}
12+
13+
const DEFAULTS: Required<CorsOptions> = {
14+
origins: '*',
15+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
16+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Correlation-ID', 'X-Idempotency-Key'],
17+
exposedHeaders: ['X-Correlation-ID'],
18+
credentials: false,
19+
maxAge: 86400,
20+
};
21+
22+
@Injectable()
23+
export class CorsMiddleware implements NestMiddleware {
24+
private readonly opts: Required<CorsOptions>;
25+
26+
constructor(options: CorsOptions = {}) {
27+
this.opts = { ...DEFAULTS, ...options };
28+
}
29+
30+
use(req: Request, res: Response, next: NextFunction): void {
31+
const { origins, methods, allowedHeaders, exposedHeaders, credentials, maxAge } = this.opts;
32+
const reqOrigin = req.headers.origin;
33+
34+
if (Array.isArray(origins)) {
35+
if (reqOrigin && origins.includes(reqOrigin)) {
36+
res.setHeader('Access-Control-Allow-Origin', reqOrigin);
37+
res.setHeader('Vary', 'Origin');
38+
}
39+
} else {
40+
res.setHeader('Access-Control-Allow-Origin', origins);
41+
}
42+
43+
res.setHeader('Access-Control-Allow-Methods', methods.join(', '));
44+
res.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(', '));
45+
46+
if (exposedHeaders.length) {
47+
res.setHeader('Access-Control-Expose-Headers', exposedHeaders.join(', '));
48+
}
49+
50+
if (credentials) {
51+
res.setHeader('Access-Control-Allow-Credentials', 'true');
52+
}
53+
54+
if (req.method === 'OPTIONS') {
55+
res.setHeader('Access-Control-Max-Age', String(maxAge));
56+
res.status(204).end();
57+
return;
58+
}
59+
60+
next();
61+
}
62+
}
63+
64+
/** Factory for use with NestJS consumer.apply() */
65+
export function corsMiddleware(options?: CorsOptions) {
66+
const mw = new CorsMiddleware(options);
67+
return (req: Request, res: Response, next: NextFunction) => mw.use(req, res, next);
68+
}

middleware/src/security/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
// Placeholder: security middleware exports will live here.
2-
3-
export const __securityPlaceholder = true;
1+
export * from './cors.middleware';
2+
export * from './security-headers.middleware';
3+
export * from './security-headers.config';

0 commit comments

Comments
 (0)