chore: remove solana wallet stack
This commit is contained in:
+28
-155
@@ -3,7 +3,6 @@ import crypto from "crypto";
|
||||
import { Router } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { asyncHandler } from "../middleware/errorHandler";
|
||||
import { createWallet } from "../services/solana";
|
||||
import { prisma } from "../utils/prisma";
|
||||
|
||||
const ENCRYPTION_KEY =
|
||||
@@ -27,18 +26,11 @@ const PLEX_CLIENT_ID = process.env.PLEX_CLIENT_ID || "";
|
||||
const PLEX_REDIRECT_URI = process.env.PLEX_REDIRECT_URI || "";
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "secret";
|
||||
|
||||
if (!PLEX_CLIENT_ID) {
|
||||
console.error("ERROR: PLEX_CLIENT_ID not set");
|
||||
}
|
||||
|
||||
// Step 1: Generate Plex PIN and return auth URL
|
||||
router.get(
|
||||
"/plex/url",
|
||||
asyncHandler(async (_req, res) => {
|
||||
if (!PLEX_CLIENT_ID) {
|
||||
if (!PLEX_CLIENT_ID)
|
||||
return res.status(500).json({ error: "Plex not configured" });
|
||||
}
|
||||
|
||||
const pinResponse = await axios.post(
|
||||
"https://plex.tv/api/v2/pins?strong=true",
|
||||
null,
|
||||
@@ -53,25 +45,18 @@ router.get(
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const pin = pinResponse.data;
|
||||
const authUrl = `https://app.plex.tv/auth#?clientID=${encodeURIComponent(PLEX_CLIENT_ID)}&code=${pin.code}&forwardUrl=${encodeURIComponent(PLEX_REDIRECT_URI)}`;
|
||||
|
||||
res.json({ authUrl, pinId: pin.id });
|
||||
}),
|
||||
);
|
||||
|
||||
// Step 2: Exchange PIN for Plex token and create user session
|
||||
router.post(
|
||||
"/plex/callback",
|
||||
asyncHandler(async (req, res) => {
|
||||
const { pinId } = req.body;
|
||||
if (!pinId) return res.status(400).json({ error: "PIN ID required" });
|
||||
|
||||
if (!pinId) {
|
||||
return res.status(400).json({ error: "PIN ID required" });
|
||||
}
|
||||
|
||||
// Check PIN status to get auth token
|
||||
const pinResponse = await axios.get(
|
||||
`https://plex.tv/api/v2/pins/${pinId}`,
|
||||
{
|
||||
@@ -81,33 +66,23 @@ router.post(
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const pin = pinResponse.data;
|
||||
|
||||
if (!pin.authToken) {
|
||||
if (!pin.authToken)
|
||||
return res.status(400).json({ error: "Authentication not completed" });
|
||||
}
|
||||
|
||||
const plexToken = pin.authToken;
|
||||
|
||||
// Get user info from Plex
|
||||
const userResponse = await axios.get("https://plex.tv/api/v2/user", {
|
||||
headers: {
|
||||
"X-Plex-Token": plexToken,
|
||||
"X-Plex-Token": pin.authToken,
|
||||
"X-Plex-Client-Identifier": PLEX_CLIENT_ID,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const plexUser = userResponse.data;
|
||||
|
||||
// Find or create user
|
||||
let user = await prisma.user.findUnique({
|
||||
where: { plexId: String(plexUser.id) },
|
||||
});
|
||||
|
||||
const isNewUser = !user;
|
||||
|
||||
if (!user) {
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
@@ -127,7 +102,6 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
// Delete old sessions and create new one
|
||||
await prisma.session.deleteMany({ where: { userId: user.id } });
|
||||
const sessionToken = jwt.sign(
|
||||
{ userId: user.id, nonce: Date.now() },
|
||||
@@ -142,22 +116,13 @@ router.post(
|
||||
},
|
||||
});
|
||||
|
||||
// Backfill 30 days of watch history for new users
|
||||
if (isNewUser) {
|
||||
await backfillUserHistory(user);
|
||||
}
|
||||
if (isNewUser) await backfillUserHistory(user);
|
||||
|
||||
// Generate JWT
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
plexId: user.plexId,
|
||||
isAdmin: user.isAdmin,
|
||||
},
|
||||
{ userId: user.id, plexId: user.plexId, isAdmin: user.isAdmin },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: "7d" },
|
||||
);
|
||||
|
||||
res.json({
|
||||
token,
|
||||
sessionToken: session.token,
|
||||
@@ -167,7 +132,6 @@ router.post(
|
||||
plexUsername: user.plexUsername,
|
||||
email: user.email,
|
||||
isAdmin: user.isAdmin,
|
||||
walletAddress: user.walletAddress,
|
||||
totalEarned: user.totalEarned,
|
||||
totalSpent: user.totalSpent,
|
||||
},
|
||||
@@ -179,24 +143,15 @@ router.get(
|
||||
"/verify",
|
||||
asyncHandler(async (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
if (!authHeader?.startsWith("Bearer "))
|
||||
return res.status(401).json({ error: "No token provided" });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
|
||||
const decoded = jwt.verify(authHeader.substring(7), JWT_SECRET) as any;
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
if (!user || !user.isActive)
|
||||
return res.status(401).json({ error: "User not found or inactive" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
@@ -204,12 +159,11 @@ router.get(
|
||||
plexUsername: user.plexUsername,
|
||||
email: user.email,
|
||||
isAdmin: user.isAdmin,
|
||||
walletAddress: user.walletAddress,
|
||||
totalEarned: user.totalEarned,
|
||||
totalSpent: user.totalSpent,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
}),
|
||||
@@ -219,21 +173,12 @@ router.post(
|
||||
"/logout",
|
||||
asyncHandler(async (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
|
||||
await prisma.session.deleteMany({
|
||||
where: { userId: decoded.userId },
|
||||
});
|
||||
} catch {
|
||||
// Ignore invalid tokens on logout
|
||||
}
|
||||
const decoded = jwt.verify(authHeader.substring(7), JWT_SECRET) as any;
|
||||
await prisma.session.deleteMany({ where: { userId: decoded.userId } });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
res.json({ message: "Logged out successfully" });
|
||||
}),
|
||||
);
|
||||
@@ -242,38 +187,12 @@ async function backfillUserHistory(user: {
|
||||
id: string;
|
||||
plexId: string;
|
||||
plexUsername: string;
|
||||
walletAddress: string | null;
|
||||
}) {
|
||||
if (!TAUTULLI_URL || !TAUTULLI_API_KEY) {
|
||||
console.log("Tautulli not configured, skipping backfill");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TAUTULLI_URL || !TAUTULLI_API_KEY) return;
|
||||
try {
|
||||
// Create wallet if user doesn't have one
|
||||
if (!user.walletAddress) {
|
||||
const wallet = createWallet();
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
walletAddress: wallet.publicKey,
|
||||
encryptedPrivateKey: encrypt(wallet.secretKey),
|
||||
},
|
||||
});
|
||||
user.walletAddress = wallet.publicKey;
|
||||
console.log(
|
||||
`Created wallet for new user ${user.plexUsername}: ${wallet.publicKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Map Plex username to Tautulli user ID
|
||||
const usersResponse = await axios.get(`${TAUTULLI_URL}/api/v2`, {
|
||||
params: {
|
||||
apikey: TAUTULLI_API_KEY,
|
||||
cmd: "get_users",
|
||||
},
|
||||
params: { apikey: TAUTULLI_API_KEY, cmd: "get_users" },
|
||||
});
|
||||
|
||||
const tautulliUsers = usersResponse.data?.response?.data || [];
|
||||
const tautulliUser = tautulliUsers.find(
|
||||
(u: any) =>
|
||||
@@ -281,44 +200,25 @@ async function backfillUserHistory(user: {
|
||||
u.email?.toLowerCase() === user.plexUsername.toLowerCase() ||
|
||||
u.friendly_name?.toLowerCase() === user.plexUsername.toLowerCase(),
|
||||
);
|
||||
if (!tautulliUser) return;
|
||||
|
||||
if (!tautulliUser) {
|
||||
console.log(
|
||||
`Tautulli user not found for Plex username ${user.plexUsername}, skipping backfill`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tautulliUserId = tautulliUser.user_id;
|
||||
console.log(
|
||||
`Mapped ${user.plexUsername} to Tautulli user ID ${tautulliUserId}`,
|
||||
);
|
||||
|
||||
// Calculate 30 days ago
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
const startDate = thirtyDaysAgo.toISOString().split("T")[0];
|
||||
|
||||
// Fetch history from Tautulli
|
||||
const startDate = (() => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 30);
|
||||
return d.toISOString().split("T")[0];
|
||||
})();
|
||||
const historyResponse = await axios.get(`${TAUTULLI_URL}/api/v2`, {
|
||||
params: {
|
||||
apikey: TAUTULLI_API_KEY,
|
||||
cmd: "get_history",
|
||||
user_id: tautulliUserId,
|
||||
user_id: tautulliUser.user_id,
|
||||
start_date: startDate,
|
||||
length: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
const historyData = historyResponse.data?.response?.data?.data || [];
|
||||
if (!Array.isArray(historyData) || historyData.length === 0) {
|
||||
console.log(
|
||||
`No watch history found for user ${user.plexUsername} in last 30 days`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(historyData) || historyData.length === 0) return;
|
||||
|
||||
// Get system settings for credit calculation
|
||||
const settings = await prisma.systemSettings.findFirst();
|
||||
const creditsPerMinute = settings?.creditsPerMinute || 2;
|
||||
const minWatchPercent = settings?.minWatchPercent || 80;
|
||||
@@ -326,38 +226,21 @@ async function backfillUserHistory(user: {
|
||||
|
||||
let totalCredits = 0;
|
||||
let totalMinutes = 0;
|
||||
let processedCount = 0;
|
||||
|
||||
for (const item of historyData) {
|
||||
const sessionId = item.reference_id?.toString() || item.id?.toString();
|
||||
if (!sessionId) continue;
|
||||
|
||||
// Skip duplicates
|
||||
const existing = await prisma.watchEvent.findUnique({
|
||||
where: { sessionId },
|
||||
});
|
||||
if (existing) continue;
|
||||
|
||||
if (await prisma.watchEvent.findUnique({ where: { sessionId } }))
|
||||
continue;
|
||||
const percentComplete = item.percent_complete || 0;
|
||||
const watchDurationMinutes = Math.floor(
|
||||
(item.stopped - item.started) / 60,
|
||||
);
|
||||
|
||||
if (
|
||||
percentComplete < minWatchPercent ||
|
||||
watchDurationMinutes < minWatchMinutes
|
||||
) {
|
||||
)
|
||||
continue;
|
||||
}
|
||||
|
||||
let creditsEarned = watchDurationMinutes * creditsPerMinute;
|
||||
if (settings?.newReleaseMultiplier && item.is_new) {
|
||||
creditsEarned = Math.floor(
|
||||
creditsEarned * Number(settings.newReleaseMultiplier),
|
||||
);
|
||||
}
|
||||
|
||||
// Create watch event
|
||||
const creditsEarned = watchDurationMinutes * creditsPerMinute;
|
||||
const watchEvent = await prisma.watchEvent.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
@@ -373,8 +256,6 @@ async function backfillUserHistory(user: {
|
||||
watchedAt: new Date(item.stopped * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
// DB-only credit record
|
||||
await prisma.transaction.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
@@ -385,14 +266,10 @@ async function backfillUserHistory(user: {
|
||||
contentTitle: item.title || item.full_title || "Unknown",
|
||||
},
|
||||
});
|
||||
|
||||
totalCredits += creditsEarned;
|
||||
totalMinutes += watchDurationMinutes;
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
// Update user stats
|
||||
if (totalCredits > 0) {
|
||||
if (totalCredits > 0)
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
@@ -400,10 +277,6 @@ async function backfillUserHistory(user: {
|
||||
watchTimeMinutes: { increment: totalMinutes },
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`Backfilled ${processedCount} watch events for ${user.plexUsername}: ${totalCredits} $COOP, ${totalMinutes} minutes`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to backfill history for ${user.plexUsername}:`,
|
||||
|
||||
+29
-210
@@ -1,223 +1,42 @@
|
||||
import crypto from "crypto";
|
||||
import { Router } from "express";
|
||||
import {
|
||||
type AuthenticatedRequest,
|
||||
authenticate,
|
||||
requireAdmin,
|
||||
} from "../middleware/auth";
|
||||
import { type AuthenticatedRequest, authenticate, requireAdmin } from "../middleware/auth";
|
||||
import { asyncHandler } from "../middleware/errorHandler";
|
||||
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!!!!!";
|
||||
|
||||
function encrypt(text: string): string {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest();
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
let encrypted = cipher.update(text, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
const authTag = cipher.getAuthTag();
|
||||
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
||||
}
|
||||
router.get("/", authenticate, asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({ where: { id: req.user!.id }, select: { totalEarned: true, totalSpent: true } });
|
||||
res.json({ hasWallet: false, balance: (user?.totalEarned || 0) - (user?.totalSpent || 0), totalEarned: user?.totalEarned || 0, totalSpent: user?.totalSpent || 0 });
|
||||
}));
|
||||
|
||||
function decrypt(encryptedData: string): string {
|
||||
const parts = encryptedData.split(":");
|
||||
const iv = Buffer.from(parts[0], "hex");
|
||||
const authTag = Buffer.from(parts[1], "hex");
|
||||
const encrypted = parts[2];
|
||||
const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest();
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
return decrypted;
|
||||
}
|
||||
router.post("/create", authenticate, asyncHandler(async (_req: AuthenticatedRequest, res) => {
|
||||
res.status(410).json({ error: "Wallets removed" });
|
||||
}));
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
authenticate,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id },
|
||||
select: {
|
||||
walletAddress: true,
|
||||
totalEarned: true,
|
||||
totalSpent: true,
|
||||
},
|
||||
});
|
||||
router.post("/connect", authenticate, asyncHandler(async (_req: AuthenticatedRequest, res) => {
|
||||
res.status(410).json({ error: "Wallets removed" });
|
||||
}));
|
||||
|
||||
if (!user?.walletAddress) {
|
||||
return res.json({
|
||||
hasWallet: false,
|
||||
balance: 0,
|
||||
totalEarned: user?.totalEarned || 0,
|
||||
totalSpent: user?.totalSpent || 0,
|
||||
});
|
||||
}
|
||||
router.post("/backup", authenticate, asyncHandler(async (_req: AuthenticatedRequest, res) => {
|
||||
res.status(410).json({ error: "Wallets removed" });
|
||||
}));
|
||||
|
||||
const dbBalance = user.totalEarned - user.totalSpent;
|
||||
router.get("/admin/:userId", authenticate, requireAdmin, asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({ where: { id: req.params.userId }, select: { id: true, plexUsername: true, email: true, isAdmin: true, isActive: true, totalEarned: true, totalSpent: true, watchTimeMinutes: true, createdAt: true } });
|
||||
if (!user) return res.status(404).json({ error: "User not found" });
|
||||
res.json({ ...user, balance: user.totalEarned - user.totalSpent });
|
||||
}));
|
||||
|
||||
res.json({
|
||||
hasWallet: true,
|
||||
address: user.walletAddress,
|
||||
balance: dbBalance,
|
||||
totalEarned: user.totalEarned,
|
||||
totalSpent: user.totalSpent,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/create",
|
||||
authenticate,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id },
|
||||
});
|
||||
|
||||
if (user?.walletAddress) {
|
||||
return res.status(400).json({ error: "Wallet already exists" });
|
||||
}
|
||||
|
||||
const wallet = createWallet();
|
||||
const encryptedKey = encrypt(wallet.secretKey);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: req.user!.id },
|
||||
data: {
|
||||
walletAddress: wallet.publicKey,
|
||||
encryptedPrivateKey: encryptedKey,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
address: wallet.publicKey,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/connect",
|
||||
authenticate,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const { address } = req.body;
|
||||
|
||||
if (!address) {
|
||||
return res.status(400).json({ error: "Address required" });
|
||||
}
|
||||
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: { walletAddress: address },
|
||||
});
|
||||
|
||||
if (existing && existing.id !== req.user!.id) {
|
||||
return res.status(400).json({ error: "Wallet already in use" });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: req.user!.id },
|
||||
data: { walletAddress: address },
|
||||
});
|
||||
|
||||
res.json({ success: true, address });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/backup",
|
||||
authenticate,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id },
|
||||
});
|
||||
|
||||
if (!user?.encryptedPrivateKey) {
|
||||
return res.status(400).json({ error: "No wallet backup available" });
|
||||
}
|
||||
|
||||
const privateKey = decrypt(user.encryptedPrivateKey);
|
||||
res.json({ success: true, privateKey });
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/admin/:userId",
|
||||
authenticate,
|
||||
requireAdmin,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.params.userId },
|
||||
select: {
|
||||
id: true,
|
||||
plexUsername: true,
|
||||
email: true,
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
walletAddress: true,
|
||||
totalEarned: true,
|
||||
totalSpent: true,
|
||||
watchTimeMinutes: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: "User not found" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
...user,
|
||||
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 });
|
||||
}),
|
||||
);
|
||||
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);
|
||||
await prisma.transaction.create({ data: { userId: user.id, type: "ADJUSTMENT", amount: Math.abs(delta), description: reason || "Admin adjustment", contentTitle: reason || "Admin adjustment" } });
|
||||
await prisma.user.update({ where: { id: user.id }, data: delta > 0 ? { totalEarned: { increment: delta } } : { totalSpent: { increment: Math.abs(delta) } } });
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
export { router as walletRouter };
|
||||
|
||||
Reference in New Issue
Block a user