add keycrow feature ideas, exclude features/ from biome
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
|
||||
"files": {
|
||||
"ignore": ["src/client/routeTree.gen.ts", "drizzle/"]
|
||||
"ignore": ["src/client/routeTree.gen.ts", "drizzle/", "dist/", "features/"]
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
|
||||
80
features/keycrow/README.md
Normal file
80
features/keycrow/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# KeyCrow - Steam Key Trading Platform with Escrow
|
||||
|
||||
Technical foundation for a automated Steam key trading platform with escrow system.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client/App │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Backend API (Express) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Routes: auth | listings | transactions | theoretical │
|
||||
└──────┬──────────────┬──────────────────┬───────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐
|
||||
│ Store │ │ Encryption │ │ Services │
|
||||
│ (In-Memory) │ │ Service │ │ - PaymentProvider (Mock) │
|
||||
│ │ │ (AES) │ │ - KeyActivationProvider │
|
||||
└──────────────┘ └──────────────┘ └──────────────────────────┘
|
||||
```
|
||||
|
||||
## What's Implemented
|
||||
|
||||
### Realistic Flow (Production-Ready Pattern)
|
||||
1. **Seller** creates a listing with encrypted Steam key
|
||||
2. **Buyer** purchases via escrow (payment held)
|
||||
3. **Platform** delivers decrypted key to buyer
|
||||
4. **Buyer** confirms key works → money released to seller
|
||||
5. **Buyer** reports failure → dispute, refund initiated
|
||||
|
||||
### Theoretica/Ideal Flow (Mock Only)
|
||||
- Automated server-side key activation on buyer's Steam account
|
||||
- **DISABLED by default** - requires `ALLOW_THEORETICAL_ACTIVATION=true`
|
||||
- Clearly marked as potentially violating Steam ToS
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
- `POST /auth/register` - Register user
|
||||
- `GET /auth/me` - Get current user
|
||||
- `POST /auth/auth/steam/login` - Steam login (mock)
|
||||
|
||||
### Listings
|
||||
- `POST /listings` - Create listing
|
||||
- `GET /listings` - Get active listings
|
||||
- `GET /listings/:id` - Get listing by ID
|
||||
- `GET /listings/seller/me` - Get seller's listings
|
||||
- `DELETE /listings/:id` - Cancel listing
|
||||
|
||||
### Transactions
|
||||
- `POST /transactions` - Create purchase (escrow hold)
|
||||
- `GET /transactions/:id` - Get transaction
|
||||
- `GET /transactions/:id/key` - Get decrypted key (buyer only)
|
||||
- `POST /transactions/:id/confirm` - Confirm key works/failed
|
||||
- `GET /transactions/buyer/me` - Buyer's transactions
|
||||
- `GET /transactions/seller/me` - Seller's transactions
|
||||
|
||||
### Theoretical (Mock)
|
||||
- `POST /theoretical/activate` - Attempt automated activation
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
PORT=3000
|
||||
ENCRYPTION_KEY=your-256-bit-key
|
||||
STEAM_API_KEY=your-steam-api-key
|
||||
STEAM_REDIRECT_URI=http://localhost:3000/auth/steam/callback
|
||||
ALLOW_THEORETICAL_ACTIVATION=false
|
||||
```
|
||||
|
||||
## Legal Notice
|
||||
|
||||
This implementation is a **technical proof-of-concept**. Automated Steam key activation is likely to violate Steam's Terms of Service unless you have an official partnership with Valve.
|
||||
|
||||
The "theoretical" module is clearly marked and disabled by default. Use at your own risk.
|
||||
5617
features/keycrow/package-lock.json
generated
Normal file
5617
features/keycrow/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
features/keycrow/package.json
Normal file
34
features/keycrow/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "keycrow-opencode",
|
||||
"version": "2026.02.18",
|
||||
"description": "Steam Key Trading Platform with Escrow - Technical Foundation",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc && cp -r src/gui dist/",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^7.2.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
16
features/keycrow/src/config/index.ts
Normal file
16
features/keycrow/src/config/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const config = {
|
||||
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
|
||||
encryption: {
|
||||
key: process.env.ENCRYPTION_KEY || 'default-dev-key-change-in-prod',
|
||||
},
|
||||
steam: {
|
||||
apiKey: process.env.STEAM_API_KEY || '',
|
||||
redirectUri: process.env.STEAM_REDIRECT_URI || 'http://localhost:3000/auth/steam/callback',
|
||||
},
|
||||
escrow: {
|
||||
holdDurationDays: 7,
|
||||
},
|
||||
features: {
|
||||
allowTheoreticalActivation: process.env.ALLOW_THEORETICAL_ACTIVATION === 'true',
|
||||
},
|
||||
};
|
||||
246
features/keycrow/src/gui/index.html
Normal file
246
features/keycrow/src/gui/index.html
Normal file
@@ -0,0 +1,246 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KeyCrow Admin</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
h1 { color: #333; margin-bottom: 20px; }
|
||||
h2 { color: #555; margin: 20px 0 10px; }
|
||||
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
label { display: block; margin-bottom: 5px; color: #666; }
|
||||
input, select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
|
||||
button { background: #007AFF; color: white; border: none; padding: 12px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; }
|
||||
button:hover { background: #0056b3; }
|
||||
button.secondary { background: #6c757d; }
|
||||
button.danger { background: #dc3545; }
|
||||
button.success { background: #28a745; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
|
||||
th { background: #f8f9fa; font-weight: 600; }
|
||||
.status { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; }
|
||||
.status.ACTIVE { background: #d4edda; color: #155724; }
|
||||
.status.SOLD { background: #cce5ff; color: #004085; }
|
||||
.status.HELD { background: #fff3cd; color: #856404; }
|
||||
.status.RELEASED { background: #d4edda; color: #155724; }
|
||||
.status.REFUNDED { background: #f8d7da; color: #721c24; }
|
||||
.status.PENDING { background: #fff3cd; color: #856404; }
|
||||
.tabs { display: flex; gap: 10px; margin-bottom: 20px; }
|
||||
.tab { padding: 10px 20px; background: white; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; }
|
||||
.tab.active { background: #007AFF; color: white; border-color: #007AFF; }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.alert { padding: 15px; border-radius: 6px; margin-bottom: 15px; }
|
||||
.alert.error { background: #f8d7da; color: #721c24; }
|
||||
.alert.success { background: #d4edda; color: #155724; }
|
||||
.user-info { background: #e9ecef; padding: 10px; border-radius: 4px; margin-bottom: 20px; }
|
||||
pre { background: #f8f9fa; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>KeyCrow Admin</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2>Session</h2>
|
||||
<div class="form-group">
|
||||
<label>User ID (for testing)</label>
|
||||
<input type="text" id="userId" placeholder="Enter user ID from registration">
|
||||
</div>
|
||||
<button onclick="setUser()">Set User</button>
|
||||
<span id="currentUser" style="margin-left: 15px; color: #666;"></span>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="showTab('listings')">Listings</div>
|
||||
<div class="tab" onclick="showTab('transactions')">Transactions</div>
|
||||
<div class="tab" onclick="showTab('create')">Create Listing</div>
|
||||
</div>
|
||||
|
||||
<div id="listings" class="tab-content active">
|
||||
<div class="card">
|
||||
<h2>Active Listings</h2>
|
||||
<button class="secondary" onclick="loadListings()">Refresh</button>
|
||||
<table style="margin-top: 15px;">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Game</th><th>Platform</th><th>Price</th><th>Status</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="listingsTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transactions" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>Transactions</h2>
|
||||
<button class="secondary" onclick="loadTransactions()">Refresh</button>
|
||||
<table style="margin-top: 15px;">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Listing</th><th>Amount</th><th>Escrow</th><th>Status</th><th>Key</th><th>Confirm</th></tr>
|
||||
</thead>
|
||||
<tbody id="transactionsTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="create" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>Create New Listing</h2>
|
||||
<div class="form-group">
|
||||
<label>Game Title</label>
|
||||
<input type="text" id="gameTitle" placeholder="e.g., Cyberpunk 2077">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Platform</label>
|
||||
<select id="platform">
|
||||
<option value="STEAM">Steam</option>
|
||||
<option value="GOG">GOG</option>
|
||||
<option value="EPIC">Epic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Price</label>
|
||||
<input type="number" id="price" step="0.01" placeholder="9.99">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Steam Key</label>
|
||||
<input type="text" id="key" placeholder="XXXX-XXXX-XXXX">
|
||||
</div>
|
||||
<button onclick="createListing()">Create Listing</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert" class="alert" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentUser = null;
|
||||
|
||||
function showTab(tabId) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
}
|
||||
|
||||
function showAlert(message, type = 'success') {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.className = `alert ${type}`;
|
||||
alert.textContent = message;
|
||||
alert.style.display = 'block';
|
||||
setTimeout(() => alert.style.display = 'none', 5000);
|
||||
}
|
||||
|
||||
function setUser() {
|
||||
currentUser = document.getElementById('userId').value;
|
||||
if (currentUser) {
|
||||
document.getElementById('currentUser').textContent = `Current user: ${currentUser.substring(0, 8)}...`;
|
||||
}
|
||||
}
|
||||
|
||||
async function api(method, endpoint, body = null) {
|
||||
const options = { method, headers: { 'Content-Type': 'application/json' } };
|
||||
if (currentUser) options.headers['x-user-id'] = currentUser;
|
||||
if (body) options.body = JSON.stringify(body);
|
||||
const res = await fetch(endpoint, options);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function loadListings() {
|
||||
const result = await api('GET', '/listings');
|
||||
const tbody = document.getElementById('listingsTable');
|
||||
if (!result.data?.listings?.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">No listings found</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = result.data.listings.map(l => `
|
||||
<tr>
|
||||
<td>${l.id.substring(0, 8)}...</td>
|
||||
<td>${l.gameTitle}</td>
|
||||
<td>${l.platform}</td>
|
||||
<td>${l.price} ${l.currency}</td>
|
||||
<td><span class="status ${l.status}">${l.status}</span></td>
|
||||
<td><button class="secondary" onclick="deleteListing('${l.id}')">Cancel</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function createListing() {
|
||||
const data = {
|
||||
gameTitle: document.getElementById('gameTitle').value,
|
||||
platform: document.getElementById('platform').value,
|
||||
price: parseFloat(document.getElementById('price').value),
|
||||
currency: 'EUR',
|
||||
key: document.getElementById('key').value
|
||||
};
|
||||
const result = await api('POST', '/listings', data);
|
||||
if (result.success) {
|
||||
showAlert('Listing created!');
|
||||
document.getElementById('gameTitle').value = '';
|
||||
document.getElementById('price').value = '';
|
||||
document.getElementById('key').value = '';
|
||||
loadListings();
|
||||
} else {
|
||||
showAlert(result.error || 'Failed to create listing', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteListing(id) {
|
||||
if (!confirm('Cancel this listing?')) return;
|
||||
const result = await api('DELETE', `/listings/${id}`);
|
||||
if (result.success) {
|
||||
showAlert('Listing cancelled');
|
||||
loadListings();
|
||||
} else {
|
||||
showAlert(result.error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTransactions() {
|
||||
const result = await api('GET', '/transactions/buyer/me');
|
||||
const tbody = document.getElementById('transactionsTable');
|
||||
if (!result.data?.transactions?.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7">No transactions found</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = result.data.transactions.map(t => `
|
||||
<tr>
|
||||
<td>${t.id.substring(0, 8)}...</td>
|
||||
<td>${t.listingId.substring(0, 8)}...</td>
|
||||
<td>${t.amount} ${t.currency}</td>
|
||||
<td><span class="status ${t.escrowStatus}">${t.escrowStatus}</span></td>
|
||||
<td><span class="status ${t.transactionStatus}">${t.transactionStatus}</span></td>
|
||||
<td>${t.keyDelivered ? '✓' : '<button class="secondary" onclick="getKey(\'' + t.id + '\')">Get Key</button>'}</td>
|
||||
<td>${t.transactionStatus === 'PENDING' ? '<button class="success" onclick="confirmTx(\'' + t.id + '\', \'SUCCESS\')">✓</button> <button class="danger" onclick="confirmTx(\'' + t.id + '\', \'FAILED\')">✗</button>' : '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function getKey(txId) {
|
||||
const result = await api('GET', `/transactions/${txId}/key`);
|
||||
if (result.success && result.data.key) {
|
||||
showAlert(`Key: ${result.data.key}`);
|
||||
loadTransactions();
|
||||
} else {
|
||||
showAlert(result.error || 'Failed to get key', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmTx(txId, status) {
|
||||
const result = await api('POST', `/transactions/${txId}/confirm`, { status });
|
||||
if (result.success) {
|
||||
showAlert(`Transaction ${status === 'SUCCESS' ? 'confirmed' : 'disputed'}`);
|
||||
loadTransactions();
|
||||
} else {
|
||||
showAlert(result.error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadListings();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
43
features/keycrow/src/index.ts
Normal file
43
features/keycrow/src/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import express, { Express } from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { config } from './config';
|
||||
import authRoutes from './routes/auth';
|
||||
import listingsRoutes from './routes/listings';
|
||||
import transactionsRoutes from './routes/transactions';
|
||||
import theoreticalRoutes from './routes/theoretical';
|
||||
|
||||
const app: Express = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'gui')));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'gui', 'index.html'));
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
app.use('/auth', authRoutes);
|
||||
app.use('/listings', listingsRoutes);
|
||||
app.use('/transactions', transactionsRoutes);
|
||||
app.use('/theoretical', theoreticalRoutes);
|
||||
|
||||
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error('Unhandled error:', err);
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
});
|
||||
|
||||
const startServer = () => {
|
||||
app.listen(config.port, () => {
|
||||
console.log(`Server running on port ${config.port}`);
|
||||
console.log(`Theoretical activation: ${config.features.allowTheoreticalActivation ? 'ENABLED' : 'DISABLED'}`);
|
||||
});
|
||||
};
|
||||
|
||||
startServer();
|
||||
|
||||
export default app;
|
||||
50
features/keycrow/src/middleware/auth.ts
Normal file
50
features/keycrow/src/middleware/auth.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { store } from '../services/Store';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
userId?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export const mockAuthMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const user = store.getUserById(userId);
|
||||
if (!user) {
|
||||
return res.status(401).json({ success: false, error: 'User not found' });
|
||||
}
|
||||
|
||||
req.userId = userId;
|
||||
next();
|
||||
};
|
||||
|
||||
export const requireSteamAuth = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const user = store.getUserById(userId);
|
||||
if (!user) {
|
||||
return res.status(401).json({ success: false, error: 'User not found' });
|
||||
}
|
||||
|
||||
if (!user.steamId) {
|
||||
return res.status(403).json({ success: false, error: 'Steam authentication required' });
|
||||
}
|
||||
|
||||
req.userId = userId;
|
||||
next();
|
||||
};
|
||||
65
features/keycrow/src/models/index.ts
Normal file
65
features/keycrow/src/models/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
steamId?: string;
|
||||
username: string;
|
||||
email: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type ListingStatus = 'ACTIVE' | 'SOLD' | 'CANCELLED' | 'EXPIRED';
|
||||
export type Platform = 'STEAM' | 'GOG' | 'EPIC' | 'OTHER';
|
||||
|
||||
export interface Listing {
|
||||
id: string;
|
||||
sellerId: string;
|
||||
gameTitle: string;
|
||||
platform: Platform;
|
||||
price: number;
|
||||
currency: string;
|
||||
keyEncrypted: string;
|
||||
status: ListingStatus;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type EscrowStatus = 'PENDING' | 'HELD' | 'RELEASED' | 'REFUNDED';
|
||||
export type TransactionStatus = 'PENDING' | 'COMPLETED' | 'DISPUTED' | 'CANCELLED';
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
listingId: string;
|
||||
buyerId: string;
|
||||
sellerId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
escrowStatus: EscrowStatus;
|
||||
transactionStatus: TransactionStatus;
|
||||
holdId?: string;
|
||||
keyDelivered: boolean;
|
||||
confirmedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateListingDto {
|
||||
gameTitle: string;
|
||||
platform: Platform;
|
||||
price: number;
|
||||
currency?: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface CreateTransactionDto {
|
||||
listingId: string;
|
||||
}
|
||||
|
||||
export interface ConfirmTransactionDto {
|
||||
status: 'SUCCESS' | 'FAILED';
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
75
features/keycrow/src/routes/auth.ts
Normal file
75
features/keycrow/src/routes/auth.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { store } from '../services/Store';
|
||||
import { ApiResponse } from '../models';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/register', (req: Request, res: Response) => {
|
||||
const { username, email } = req.body;
|
||||
|
||||
if (!username || !email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Username and email required'
|
||||
});
|
||||
}
|
||||
|
||||
const user = store.createUser({ username, email });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user: { id: user.id, username: user.username, email: user.email } },
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/me', (req: Request, res: Response) => {
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const user = store.getUserById(userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: 'User not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user },
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/auth/steam/login', (req: Request, res: Response) => {
|
||||
const { steamId, username } = req.body;
|
||||
|
||||
if (!steamId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Steam ID required'
|
||||
});
|
||||
}
|
||||
|
||||
let user = store.getUserBySteamId(steamId);
|
||||
|
||||
if (!user) {
|
||||
user = store.createUser({
|
||||
username: username || `SteamUser_${steamId}`,
|
||||
email: '',
|
||||
steamId,
|
||||
});
|
||||
} else {
|
||||
store.updateUser(user.id, { steamId });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: { id: user.id, username: user.username, steamId: user.steamId },
|
||||
message: 'Steam login successful (mock)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
121
features/keycrow/src/routes/listings.ts
Normal file
121
features/keycrow/src/routes/listings.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { store } from '../services/Store';
|
||||
import { encryptionService } from '../utils/encryption';
|
||||
import { mockAuthMiddleware } from '../middleware/auth';
|
||||
import { CreateListingDto } from '../models';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/', mockAuthMiddleware, (req, res: Response) => {
|
||||
const { gameTitle, platform, price, currency = 'EUR', key } = req.body as CreateListingDto;
|
||||
|
||||
if (!gameTitle || !platform || !price || !key) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'gameTitle, platform, price, and key are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (price <= 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Price must be greater than 0',
|
||||
});
|
||||
}
|
||||
|
||||
const keyEncrypted = encryptionService.encrypt(key);
|
||||
|
||||
const listing = store.createListing({
|
||||
sellerId: req.userId!,
|
||||
gameTitle,
|
||||
platform,
|
||||
price,
|
||||
currency,
|
||||
keyEncrypted,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
listing: {
|
||||
id: listing.id,
|
||||
gameTitle: listing.gameTitle,
|
||||
platform: listing.platform,
|
||||
price: listing.price,
|
||||
currency: listing.currency,
|
||||
status: listing.status,
|
||||
createdAt: listing.createdAt,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', (req, res: Response) => {
|
||||
const listings = store.getActiveListings();
|
||||
|
||||
const safeListings = listings.map(l => ({
|
||||
id: l.id,
|
||||
gameTitle: l.gameTitle,
|
||||
platform: l.platform,
|
||||
price: l.price,
|
||||
currency: l.currency,
|
||||
sellerId: l.sellerId,
|
||||
status: l.status,
|
||||
createdAt: l.createdAt,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { listings: safeListings },
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:id', (req, res: Response) => {
|
||||
const listing = store.getListingById(req.params.id);
|
||||
|
||||
if (!listing) {
|
||||
return res.status(404).json({ success: false, error: 'Listing not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { listing },
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/seller/me', mockAuthMiddleware, (req, res: Response) => {
|
||||
const listings = store.getListingsBySeller(req.userId!);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { listings },
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/:id', mockAuthMiddleware, (req, res: Response) => {
|
||||
const listing = store.getListingById(req.params.id);
|
||||
|
||||
if (!listing) {
|
||||
return res.status(404).json({ success: false, error: 'Listing not found' });
|
||||
}
|
||||
|
||||
if (listing.sellerId !== req.userId) {
|
||||
return res.status(403).json({ success: false, error: 'Forbidden' });
|
||||
}
|
||||
|
||||
if (listing.status !== 'ACTIVE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Can only cancel active listings'
|
||||
});
|
||||
}
|
||||
|
||||
store.updateListing(listing.id, { status: 'CANCELLED' });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { message: 'Listing cancelled' },
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
35
features/keycrow/src/routes/theoretical.ts
Normal file
35
features/keycrow/src/routes/theoretical.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Router, Response } from 'express';
|
||||
import { keyActivationProvider } from '../services/KeyActivationProvider';
|
||||
import { mockAuthMiddleware, requireSteamAuth } from '../middleware/auth';
|
||||
import { config } from '../config';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/activate', requireSteamAuth, async (req, res: Response) => {
|
||||
if (!config.features.allowTheoreticalActivation) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Automated key activation is disabled. This feature is for demonstration only.',
|
||||
});
|
||||
}
|
||||
|
||||
const { key } = req.body;
|
||||
|
||||
if (!key) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Key is required',
|
||||
});
|
||||
}
|
||||
|
||||
const user = req.userId;
|
||||
|
||||
const result = await keyActivationProvider.activateKey(user!, key);
|
||||
|
||||
res.json({
|
||||
success: result.success,
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
225
features/keycrow/src/routes/transactions.ts
Normal file
225
features/keycrow/src/routes/transactions.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Router, Response } from 'express';
|
||||
import { store } from '../services/Store';
|
||||
import { paymentProvider } from '../services/PaymentProvider';
|
||||
import { keyActivationProvider } from '../services/KeyActivationProvider';
|
||||
import { encryptionService } from '../utils/encryption';
|
||||
import { mockAuthMiddleware, requireSteamAuth } from '../middleware/auth';
|
||||
import { config } from '../config';
|
||||
import { CreateTransactionDto } from '../models';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/', mockAuthMiddleware, async (req, res: Response) => {
|
||||
const { listingId } = req.body as CreateTransactionDto;
|
||||
|
||||
if (!listingId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'listingId is required',
|
||||
});
|
||||
}
|
||||
|
||||
const listing = store.getListingById(listingId);
|
||||
|
||||
if (!listing) {
|
||||
return res.status(404).json({ success: false, error: 'Listing not found' });
|
||||
}
|
||||
|
||||
if (listing.status !== 'ACTIVE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Listing is not available',
|
||||
});
|
||||
}
|
||||
|
||||
if (listing.sellerId === req.userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Cannot buy your own listing',
|
||||
});
|
||||
}
|
||||
|
||||
const holdResult = await paymentProvider.createHold(listing.price, listing.currency);
|
||||
|
||||
if (!holdResult.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: holdResult.error || 'Payment failed',
|
||||
});
|
||||
}
|
||||
|
||||
const transaction = store.createTransaction({
|
||||
listingId: listing.id,
|
||||
buyerId: req.userId!,
|
||||
sellerId: listing.sellerId,
|
||||
amount: listing.price,
|
||||
currency: listing.currency,
|
||||
escrowStatus: 'HELD',
|
||||
transactionStatus: 'PENDING',
|
||||
holdId: holdResult.holdId,
|
||||
keyDelivered: false,
|
||||
});
|
||||
|
||||
store.updateListing(listing.id, { status: 'SOLD' });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
transaction: {
|
||||
id: transaction.id,
|
||||
listingId: transaction.listingId,
|
||||
amount: transaction.amount,
|
||||
currency: transaction.currency,
|
||||
escrowStatus: transaction.escrowStatus,
|
||||
transactionStatus: transaction.transactionStatus,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:id', mockAuthMiddleware, (req, res: Response) => {
|
||||
const transaction = store.getTransactionById(req.params.id);
|
||||
|
||||
if (!transaction) {
|
||||
return res.status(404).json({ success: false, error: 'Transaction not found' });
|
||||
}
|
||||
|
||||
if (transaction.buyerId !== req.userId && transaction.sellerId !== req.userId) {
|
||||
return res.status(403).json({ success: false, error: 'Forbidden' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { transaction },
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:id/key', mockAuthMiddleware, (req, res: Response) => {
|
||||
const transaction = store.getTransactionById(req.params.id);
|
||||
|
||||
if (!transaction) {
|
||||
return res.status(404).json({ success: false, error: 'Transaction not found' });
|
||||
}
|
||||
|
||||
if (transaction.buyerId !== req.userId) {
|
||||
return res.status(403).json({ success: false, error: 'Forbidden' });
|
||||
}
|
||||
|
||||
if (transaction.escrowStatus !== 'HELD') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Key only available when payment is held in escrow',
|
||||
});
|
||||
}
|
||||
|
||||
if (transaction.keyDelivered) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
keyAlreadyDelivered: true,
|
||||
message: 'Key was already delivered in this transaction',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const listing = store.getListingById(transaction.listingId);
|
||||
if (!listing) {
|
||||
return res.status(404).json({ success: false, error: 'Listing not found' });
|
||||
}
|
||||
|
||||
const key = encryptionService.decrypt(listing.keyEncrypted);
|
||||
|
||||
store.updateTransaction(transaction.id, { keyDelivered: true });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { key },
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:id/confirm', requireSteamAuth, async (req, res: Response) => {
|
||||
const { status } = req.body;
|
||||
|
||||
if (!status || !['SUCCESS', 'FAILED'].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'status must be SUCCESS or FAILED',
|
||||
});
|
||||
}
|
||||
|
||||
const transaction = store.getTransactionById(req.params.id);
|
||||
|
||||
if (!transaction) {
|
||||
return res.status(404).json({ success: false, error: 'Transaction not found' });
|
||||
}
|
||||
|
||||
if (transaction.buyerId !== req.userId) {
|
||||
return res.status(403).json({ success: false, error: 'Forbidden' });
|
||||
}
|
||||
|
||||
if (transaction.transactionStatus !== 'PENDING') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Transaction already confirmed or disputed',
|
||||
});
|
||||
}
|
||||
|
||||
if (status === 'SUCCESS') {
|
||||
if (transaction.holdId) {
|
||||
await paymentProvider.release(transaction.holdId);
|
||||
}
|
||||
|
||||
store.updateTransaction(transaction.id, {
|
||||
escrowStatus: 'RELEASED',
|
||||
transactionStatus: 'COMPLETED',
|
||||
confirmedAt: new Date(),
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Key confirmed. Payment released to seller.',
|
||||
escrowStatus: 'RELEASED',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (transaction.holdId) {
|
||||
await paymentProvider.refund(transaction.holdId);
|
||||
}
|
||||
|
||||
store.updateTransaction(transaction.id, {
|
||||
escrowStatus: 'REFUNDED',
|
||||
transactionStatus: 'DISPUTED',
|
||||
confirmedAt: new Date(),
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Key marked as failed. Payment refunded to buyer.',
|
||||
escrowStatus: 'REFUNDED',
|
||||
transactionStatus: 'DISPUTED',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/buyer/me', mockAuthMiddleware, (req, res: Response) => {
|
||||
const transactions = store.getTransactionsByBuyer(req.userId!);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { transactions },
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/seller/me', mockAuthMiddleware, (req, res: Response) => {
|
||||
const transactions = store.getTransactionsBySeller(req.userId!);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { transactions },
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
49
features/keycrow/src/services/KeyActivationProvider.ts
Normal file
49
features/keycrow/src/services/KeyActivationProvider.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export interface ActivationResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
productId?: string;
|
||||
purchaseId?: string;
|
||||
}
|
||||
|
||||
export interface KeyActivationProvider {
|
||||
activateKey(steamId: string, key: string): Promise<ActivationResult>;
|
||||
}
|
||||
|
||||
export class MockKeyActivationProvider implements KeyActivationProvider {
|
||||
async activateKey(steamId: string, key: string): Promise<ActivationResult> {
|
||||
await this.simulateDelay();
|
||||
|
||||
if (!key || key.length < 5) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid key format',
|
||||
};
|
||||
}
|
||||
|
||||
if (key.toLowerCase().includes('invalid')) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Key has already been redeemed',
|
||||
};
|
||||
}
|
||||
|
||||
if (key.toLowerCase().includes('expired')) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Key has expired',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
productId: `prod_${Math.random().toString(36).substr(2, 9)}`,
|
||||
purchaseId: `purch_${Math.random().toString(36).substr(2, 9)}`,
|
||||
};
|
||||
}
|
||||
|
||||
private simulateDelay(): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
export const keyActivationProvider = new MockKeyActivationProvider();
|
||||
53
features/keycrow/src/services/PaymentProvider.ts
Normal file
53
features/keycrow/src/services/PaymentProvider.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type PaymentResult = {
|
||||
success: boolean;
|
||||
holdId?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export interface PaymentProvider {
|
||||
createHold(amount: number, currency: string): Promise<PaymentResult>;
|
||||
release(holdId: string): Promise<{ success: boolean; error?: string }>;
|
||||
refund(holdId: string): Promise<{ success: boolean; error?: string }>;
|
||||
}
|
||||
|
||||
export class MockPaymentProvider implements PaymentProvider {
|
||||
private holds: Map<string, { amount: number; currency: string; released: boolean }> = new Map();
|
||||
|
||||
async createHold(amount: number, currency: string): Promise<PaymentResult> {
|
||||
const holdId = `hold_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.holds.set(holdId, { amount, currency, released: false });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
holdId,
|
||||
};
|
||||
}
|
||||
|
||||
async release(holdId: string): Promise<{ success: boolean; error?: string }> {
|
||||
const hold = this.holds.get(holdId);
|
||||
|
||||
if (!hold) {
|
||||
return { success: false, error: 'Hold not found' };
|
||||
}
|
||||
|
||||
if (hold.released) {
|
||||
return { success: false, error: 'Hold already released' };
|
||||
}
|
||||
|
||||
hold.released = true;
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async refund(holdId: string): Promise<{ success: boolean; error?: string }> {
|
||||
const hold = this.holds.get(holdId);
|
||||
|
||||
if (!hold) {
|
||||
return { success: false, error: 'Hold not found' };
|
||||
}
|
||||
|
||||
this.holds.delete(holdId);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export const paymentProvider = new MockPaymentProvider();
|
||||
103
features/keycrow/src/services/Store.ts
Normal file
103
features/keycrow/src/services/Store.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { User, Listing, Transaction } from '../models';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class InMemoryStore {
|
||||
users: Map<string, User> = new Map();
|
||||
listings: Map<string, Listing> = new Map();
|
||||
transactions: Map<string, Transaction> = new Map();
|
||||
|
||||
createUser(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): User {
|
||||
const user: User = {
|
||||
...data,
|
||||
id: uuidv4(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.users.set(user.id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
getUserById(id: string): User | undefined {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
getUserBySteamId(steamId: string): User | undefined {
|
||||
return Array.from(this.users.values()).find(u => u.steamId === steamId);
|
||||
}
|
||||
|
||||
updateUser(id: string, data: Partial<User>): User | undefined {
|
||||
const user = this.users.get(id);
|
||||
if (!user) return undefined;
|
||||
|
||||
const updated = { ...user, ...data, updatedAt: new Date() };
|
||||
this.users.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
createListing(data: Omit<Listing, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Listing {
|
||||
const listing: Listing = {
|
||||
...data,
|
||||
id: uuidv4(),
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.listings.set(listing.id, listing);
|
||||
return listing;
|
||||
}
|
||||
|
||||
getListingById(id: string): Listing | undefined {
|
||||
return this.listings.get(id);
|
||||
}
|
||||
|
||||
getActiveListings(): Listing[] {
|
||||
return Array.from(this.listings.values()).filter(l => l.status === 'ACTIVE');
|
||||
}
|
||||
|
||||
getListingsBySeller(sellerId: string): Listing[] {
|
||||
return Array.from(this.listings.values()).filter(l => l.sellerId === sellerId);
|
||||
}
|
||||
|
||||
updateListing(id: string, data: Partial<Listing>): Listing | undefined {
|
||||
const listing = this.listings.get(id);
|
||||
if (!listing) return undefined;
|
||||
|
||||
const updated = { ...listing, ...data, updatedAt: new Date() };
|
||||
this.listings.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
createTransaction(data: Omit<Transaction, 'id' | 'createdAt' | 'updatedAt'>): Transaction {
|
||||
const transaction: Transaction = {
|
||||
...data,
|
||||
id: uuidv4(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.transactions.set(transaction.id, transaction);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
getTransactionById(id: string): Transaction | undefined {
|
||||
return this.transactions.get(id);
|
||||
}
|
||||
|
||||
getTransactionsByBuyer(buyerId: string): Transaction[] {
|
||||
return Array.from(this.transactions.values()).filter(t => t.buyerId === buyerId);
|
||||
}
|
||||
|
||||
getTransactionsBySeller(sellerId: string): Transaction[] {
|
||||
return Array.from(this.transactions.values()).filter(t => t.sellerId === sellerId);
|
||||
}
|
||||
|
||||
updateTransaction(id: string, data: Partial<Transaction>): Transaction | undefined {
|
||||
const transaction = this.transactions.get(id);
|
||||
if (!transaction) return undefined;
|
||||
|
||||
const updated = { ...transaction, ...data, updatedAt: new Date() };
|
||||
this.transactions.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
export const store = new InMemoryStore();
|
||||
33
features/keycrow/src/utils/encryption.ts
Normal file
33
features/keycrow/src/utils/encryption.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { config } from '../config';
|
||||
|
||||
export class EncryptionService {
|
||||
private static instance: EncryptionService;
|
||||
private readonly key: string;
|
||||
|
||||
private constructor() {
|
||||
this.key = config.encryption.key;
|
||||
}
|
||||
|
||||
static getInstance(): EncryptionService {
|
||||
if (!EncryptionService.instance) {
|
||||
EncryptionService.instance = new EncryptionService();
|
||||
}
|
||||
return EncryptionService.instance;
|
||||
}
|
||||
|
||||
encrypt(plainText: string): string {
|
||||
return CryptoJS.AES.encrypt(plainText, this.key).toString();
|
||||
}
|
||||
|
||||
decrypt(cipherText: string): string {
|
||||
const bytes = CryptoJS.AES.decrypt(cipherText, this.key);
|
||||
return bytes.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
hash(data: string): string {
|
||||
return CryptoJS.SHA256(data).toString();
|
||||
}
|
||||
}
|
||||
|
||||
export const encryptionService = EncryptionService.getInstance();
|
||||
94
features/keycrow/tests/api.test.ts
Normal file
94
features/keycrow/tests/api.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import request from 'supertest';
|
||||
import app from '../src/index';
|
||||
|
||||
describe('KeyCrow API', () => {
|
||||
let sellerId: string;
|
||||
let buyerId: string;
|
||||
let listingId: string;
|
||||
let transactionId: string;
|
||||
const testKey = 'ABCD-EFGH-IJKL-MNOP';
|
||||
|
||||
describe('Auth Flow', () => {
|
||||
it('should register a seller', async () => {
|
||||
const res = await request(app)
|
||||
.post('/auth/register')
|
||||
.send({ username: 'seller1', email: 'seller@test.com' });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
sellerId = res.body.data.user.id;
|
||||
});
|
||||
|
||||
it('should register a buyer with steam', async () => {
|
||||
const res = await request(app)
|
||||
.post('/auth/auth/steam/login')
|
||||
.send({ steamId: '76561198000000001', username: 'buyer1' });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
buyerId = res.body.data.user.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Listings Flow', () => {
|
||||
it('should create a listing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/listings')
|
||||
.set('x-user-id', sellerId)
|
||||
.send({
|
||||
gameTitle: 'Test Game',
|
||||
platform: 'STEAM',
|
||||
price: 9.99,
|
||||
currency: 'EUR',
|
||||
key: testKey,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
listingId = res.body.data.listing.id;
|
||||
});
|
||||
|
||||
it('should get active listings', async () => {
|
||||
const res = await request(app).get('/listings');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.listings.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transaction Flow (Escrow)', () => {
|
||||
it('should create a purchase with escrow hold', async () => {
|
||||
const res = await request(app)
|
||||
.post('/transactions')
|
||||
.set('x-user-id', buyerId)
|
||||
.send({ listingId });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.transaction.escrowStatus).toBe('HELD');
|
||||
transactionId = res.body.data.transaction.id;
|
||||
});
|
||||
|
||||
it('should deliver key to buyer', async () => {
|
||||
const res = await request(app)
|
||||
.get(`/transactions/${transactionId}/key`)
|
||||
.set('x-user-id', buyerId);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.key).toBe(testKey);
|
||||
});
|
||||
|
||||
it('should confirm success and release escrow', async () => {
|
||||
const res = await request(app)
|
||||
.post(`/transactions/${transactionId}/confirm`)
|
||||
.set('x-user-id', buyerId)
|
||||
.send({ status: 'SUCCESS' });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.escrowStatus).toBe('RELEASED');
|
||||
});
|
||||
});
|
||||
});
|
||||
19
features/keycrow/tsconfig.json
Normal file
19
features/keycrow/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
Reference in New Issue
Block a user