refactor: db-only credits and admin controls

Remove blockchain dependence from credit flow:
- watch credits now DB-only
- webhook earnings now DB-only
- wallet balance now DB-only
- admin bonus/adjustment routes operate on DB totals

Add admin adjustment API for account control.
Keep Solana service file unused for now; safe to delete later.
This commit is contained in:
2026-04-22 13:05:48 -04:00
parent 86a8c7d89a
commit 8ec91acc25
5 changed files with 327 additions and 470 deletions
+59 -49
View File
@@ -6,18 +6,13 @@ import {
requireAdmin,
} from "../middleware/auth";
import { asyncHandler } from "../middleware/errorHandler";
import {
createWallet,
getTokenBalance,
requestAirdrop,
} from "../services/solana";
import { createWallet } from "../services/solana";
import { prisma } from "../utils/prisma";
const router = Router();
const ENCRYPTION_KEY =
process.env.ENCRYPTION_KEY || "default-key-32-chars-long!!!!!";
// Encrypt private key
function encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest();
@@ -28,7 +23,6 @@ function encrypt(text: string): string {
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
}
// Decrypt private key
function decrypt(encryptedData: string): string {
const parts = encryptedData.split(":");
const iv = Buffer.from(parts[0], "hex");
@@ -42,7 +36,6 @@ function decrypt(encryptedData: string): string {
return decrypted;
}
// Get user's wallet info
router.get(
"/",
authenticate,
@@ -65,24 +58,18 @@ router.get(
});
}
// Get on-chain balance (may be 0 if using DB-only credits)
const onChainBalance = await getTokenBalance(user.walletAddress);
// Usable credits are tracked in DB (works even when blockchain is down)
const dbBalance = user.totalEarned - user.totalSpent;
res.json({
hasWallet: true,
address: user.walletAddress,
balance: dbBalance,
onChainBalance,
totalEarned: user.totalEarned,
totalSpent: user.totalSpent,
explorerUrl: `https://explorer.solana.com/address/${user.walletAddress}?cluster=devnet`,
});
}),
);
// Create new wallet
router.post(
"/create",
authenticate,
@@ -95,7 +82,6 @@ router.post(
return res.status(400).json({ error: "Wallet already exists" });
}
// Create new Solana wallet
const wallet = createWallet();
const encryptedKey = encrypt(wallet.secretKey);
@@ -107,20 +93,13 @@ router.post(
},
});
// Request airdrop for testing
await requestAirdrop(wallet.publicKey);
res.json({
success: true,
address: wallet.publicKey,
message:
"Wallet created successfully. Funded with 2 SOL for transaction fees.",
warning:
"Please backup your recovery phrase if shown. This is the only time it will be displayed.",
});
}),
);
// Connect existing wallet
router.post(
"/connect",
authenticate,
@@ -128,21 +107,15 @@ router.post(
const { address } = req.body;
if (!address) {
return res.status(400).json({ error: "Wallet address required" });
return res.status(400).json({ error: "Address required" });
}
// Check if address is already connected to another user
const existing = await prisma.user.findFirst({
where: {
walletAddress: address,
NOT: { id: req.user!.id },
},
where: { walletAddress: address },
});
if (existing) {
return res
.status(400)
.json({ error: "Wallet already connected to another account" });
if (existing && existing.id !== req.user!.id) {
return res.status(400).json({ error: "Wallet already in use" });
}
await prisma.user.update({
@@ -150,14 +123,10 @@ router.post(
data: { walletAddress: address },
});
res.json({
address,
message: "Wallet connected successfully",
});
res.json({ success: true, address });
}),
);
// Get recovery phrase (only shown once at creation)
router.post(
"/backup",
authenticate,
@@ -167,20 +136,14 @@ router.post(
});
if (!user?.encryptedPrivateKey) {
return res.status(400).json({ error: "No wallet found" });
return res.status(400).json({ error: "No wallet backup available" });
}
// Decrypt and return private key for backup
const privateKey = decrypt(user.encryptedPrivateKey);
res.json({
privateKey,
warning: "Store this securely. Never share it with anyone.",
});
res.json({ success: true, privateKey });
}),
);
// Admin: Get user's wallet
router.get(
"/admin/:userId",
authenticate,
@@ -191,9 +154,14 @@ router.get(
select: {
id: true,
plexUsername: true,
email: true,
isAdmin: true,
isActive: true,
walletAddress: true,
totalEarned: true,
totalSpent: true,
watchTimeMinutes: true,
createdAt: true,
},
});
@@ -201,13 +169,55 @@ router.get(
return res.status(404).json({ error: "User not found" });
}
const dbBalance = user.totalEarned - user.totalSpent;
res.json({
...user,
balance: dbBalance,
balance: user.totalEarned - user.totalSpent,
});
}),
);
router.post(
"/admin/:userId/adjust",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { amount, reason } = req.body;
if (!amount || Number.isNaN(Number(amount)) || Number(amount) === 0) {
return res.status(400).json({ error: "Valid amount required" });
}
const user = await prisma.user.findUnique({
where: { id: req.params.userId },
});
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const delta = Number(amount);
const type = delta > 0 ? "BONUS" : "ADJUSTMENT";
await prisma.transaction.create({
data: {
userId: user.id,
type,
amount: Math.abs(delta),
description: reason || "Admin adjustment",
contentTitle: reason || "Admin adjustment",
},
});
await prisma.user.update({
where: { id: user.id },
data: {
totalEarned: delta > 0 ? { increment: delta } : undefined,
totalSpent: delta < 0 ? { increment: Math.abs(delta) } : undefined,
},
});
res.json({ success: true });
}),
);
export { router as walletRouter };