fix(backend): session dedupe + mint authority JSON array parsing

1. Auth callback now deletes old sessions before creating new one,
   and adds Date.now() nonce to JWT to prevent unique constraint
   violations on duplicate login attempts.

2. Solana mint authority now accepts both JSON array and base58
   formats for SOLANA_MINT_AUTHORITY_KEYPAIR env var.
This commit is contained in:
2026-04-21 14:26:21 -04:00
parent 5db61212dc
commit 2215eb900b
2 changed files with 195 additions and 177 deletions
+8 -2
View File
@@ -108,11 +108,17 @@ router.post(
});
}
// Create session
// 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() },
JWT_SECRET,
{ expiresIn: "7d" },
);
const session = await prisma.session.create({
data: {
userId: user.id,
token: jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: "7d" }),
token: sessionToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
+187 -175
View File
@@ -1,220 +1,232 @@
import {
Connection,
PublicKey,
Keypair,
Transaction,
SystemProgram,
sendAndConfirmTransaction
} from '@solana/web3.js';
ASSOCIATED_TOKEN_PROGRAM_ID,
createBurnInstruction,
createMintToInstruction,
getAccount,
getOrCreateAssociatedTokenAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
getOrCreateAssociatedTokenAccount,
createMintToInstruction,
createBurnInstruction,
getAccount,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
} from '@solana/spl-token';
import bs58 from 'bs58';
import { prisma } from '../utils/prisma';
Connection,
Keypair,
PublicKey,
SystemProgram,
sendAndConfirmTransaction,
Transaction,
} from "@solana/web3.js";
import bs58 from "bs58";
import { prisma } from "../utils/prisma";
const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com';
const PROGRAM_ID = new PublicKey(process.env.SOLANA_PROGRAM_ID || 'CoopCredits111111111111111111111111111111111');
const DECIMALS = parseInt(process.env.SOLANA_TOKEN_DECIMALS || '6');
const RPC_URL = process.env.SOLANA_RPC_URL || "https://api.devnet.solana.com";
const PROGRAM_ID = new PublicKey(
process.env.SOLANA_PROGRAM_ID ||
"CoopCredits111111111111111111111111111111111",
);
const DECIMALS = parseInt(process.env.SOLANA_TOKEN_DECIMALS || "6");
// Backend mint authority keypair (stored securely)
let mintAuthority: Keypair | null = null;
try {
if (process.env.SOLANA_MINT_AUTHORITY_KEYPAIR) {
const secretKey = bs58.decode(process.env.SOLANA_MINT_AUTHORITY_KEYPAIR);
mintAuthority = Keypair.fromSecretKey(secretKey);
}
if (process.env.SOLANA_MINT_AUTHORITY_KEYPAIR) {
const raw = process.env.SOLANA_MINT_AUTHORITY_KEYPAIR.trim();
let secretKey: Uint8Array;
if (raw.startsWith("[")) {
secretKey = new Uint8Array(JSON.parse(raw));
} else {
secretKey = bs58.decode(raw);
}
mintAuthority = Keypair.fromSecretKey(secretKey);
}
} catch (error) {
console.warn('Mint authority not configured');
console.warn("Mint authority not configured");
}
export const connection = new Connection(RPC_URL, 'confirmed');
export const connection = new Connection(RPC_URL, "confirmed");
// Get token mint address from program state
export async function getTokenMint(): Promise<PublicKey | null> {
try {
// In a real implementation, you'd derive this from the program state
// For now, return from environment or stored config
const config = await prisma.systemSettings.findFirst();
if (config) {
// Store/retrieve mint address from config
return null; // Placeholder
}
return null;
} catch (error) {
console.error('Failed to get token mint:', error);
return null;
}
try {
// In a real implementation, you'd derive this from the program state
// For now, return from environment or stored config
const config = await prisma.systemSettings.findFirst();
if (config) {
// Store/retrieve mint address from config
return null; // Placeholder
}
return null;
} catch (error) {
console.error("Failed to get token mint:", error);
return null;
}
}
// Create a new Solana wallet for a user
export function createWallet(): { publicKey: string; secretKey: string } {
const keypair = Keypair.generate();
return {
publicKey: keypair.publicKey.toBase58(),
secretKey: bs58.encode(keypair.secretKey)
};
const keypair = Keypair.generate();
return {
publicKey: keypair.publicKey.toBase58(),
secretKey: bs58.encode(keypair.secretKey),
};
}
// Get or create token account for user
export async function getOrCreateTokenAccount(
userPublicKey: PublicKey,
mint: PublicKey
userPublicKey: PublicKey,
mint: PublicKey,
): Promise<PublicKey> {
const tokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
mintAuthority!, // payer
mint,
userPublicKey
);
return tokenAccount.address;
const tokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
mintAuthority!, // payer
mint,
userPublicKey,
);
return tokenAccount.address;
}
// Mint tokens to user (called by backend after watch event)
export async function mintTokens(
userWalletAddress: string,
amount: number,
metadata: {
sessionId: string;
contentTitle: string;
watchDurationMinutes: number;
}
userWalletAddress: string,
amount: number,
metadata: {
sessionId: string;
contentTitle: string;
watchDurationMinutes: number;
},
): Promise<string | null> {
if (!mintAuthority) {
throw new Error('Mint authority not configured');
}
if (!mintAuthority) {
throw new Error("Mint authority not configured");
}
try {
const userPublicKey = new PublicKey(userWalletAddress);
const mint = await getTokenMint();
if (!mint) {
throw new Error('Token mint not found');
}
try {
const userPublicKey = new PublicKey(userWalletAddress);
const mint = await getTokenMint();
// Get or create user's token account
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
// Calculate amount with decimals
const amountWithDecimals = amount * Math.pow(10, DECIMALS);
// Create mint instruction
const mintInstruction = createMintToInstruction(
mint,
tokenAccount,
mintAuthority.publicKey,
BigInt(Math.floor(amountWithDecimals))
);
// Create and send transaction
const transaction = new Transaction().add(mintInstruction);
const signature = await sendAndConfirmTransaction(
connection,
transaction,
[mintAuthority]
);
console.log(`Minted ${amount} COOP to ${userWalletAddress}: ${signature}`);
return signature;
} catch (error) {
console.error('Failed to mint tokens:', error);
return null;
}
if (!mint) {
throw new Error("Token mint not found");
}
// Get or create user's token account
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
// Calculate amount with decimals
const amountWithDecimals = amount * 10 ** DECIMALS;
// Create mint instruction
const mintInstruction = createMintToInstruction(
mint,
tokenAccount,
mintAuthority.publicKey,
BigInt(Math.floor(amountWithDecimals)),
);
// Create and send transaction
const transaction = new Transaction().add(mintInstruction);
const signature = await sendAndConfirmTransaction(connection, transaction, [
mintAuthority,
]);
console.log(`Minted ${amount} COOP to ${userWalletAddress}: ${signature}`);
return signature;
} catch (error) {
console.error("Failed to mint tokens:", error);
return null;
}
}
// Burn tokens from user (called when content request is approved)
export async function burnTokens(
userWalletAddress: string,
userSecretKey: string,
amount: number
userWalletAddress: string,
userSecretKey: string,
amount: number,
): Promise<string | null> {
try {
const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey));
const mint = await getTokenMint();
if (!mint) {
throw new Error('Token mint not found');
}
try {
const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey));
const mint = await getTokenMint();
const userPublicKey = new PublicKey(userWalletAddress);
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
// Check balance
const accountInfo = await getAccount(connection, tokenAccount);
const amountWithDecimals = BigInt(Math.floor(amount * Math.pow(10, DECIMALS)));
if (accountInfo.amount < amountWithDecimals) {
throw new Error('Insufficient balance');
}
// Create burn instruction
const burnInstruction = createBurnInstruction(
tokenAccount,
mint,
userKeypair.publicKey,
amountWithDecimals
);
const transaction = new Transaction().add(burnInstruction);
const signature = await sendAndConfirmTransaction(
connection,
transaction,
[userKeypair]
);
console.log(`Burned ${amount} COOP from ${userWalletAddress}: ${signature}`);
return signature;
} catch (error) {
console.error('Failed to burn tokens:', error);
return null;
}
if (!mint) {
throw new Error("Token mint not found");
}
const userPublicKey = new PublicKey(userWalletAddress);
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
// Check balance
const accountInfo = await getAccount(connection, tokenAccount);
const amountWithDecimals = BigInt(Math.floor(amount * 10 ** DECIMALS));
if (accountInfo.amount < amountWithDecimals) {
throw new Error("Insufficient balance");
}
// Create burn instruction
const burnInstruction = createBurnInstruction(
tokenAccount,
mint,
userKeypair.publicKey,
amountWithDecimals,
);
const transaction = new Transaction().add(burnInstruction);
const signature = await sendAndConfirmTransaction(connection, transaction, [
userKeypair,
]);
console.log(
`Burned ${amount} COOP from ${userWalletAddress}: ${signature}`,
);
return signature;
} catch (error) {
console.error("Failed to burn tokens:", error);
return null;
}
}
// Get token balance for user
export async function getTokenBalance(walletAddress: string): Promise<number> {
try {
const mint = await getTokenMint();
if (!mint) return 0;
try {
const mint = await getTokenMint();
if (!mint) return 0;
const userPublicKey = new PublicKey(walletAddress);
try {
const tokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
mintAuthority!,
mint,
userPublicKey
);
const accountInfo = await getAccount(connection, tokenAccount.address);
return Number(accountInfo.amount) / Math.pow(10, DECIMALS);
} catch {
return 0;
}
} catch (error) {
console.error('Failed to get token balance:', error);
return 0;
}
const userPublicKey = new PublicKey(walletAddress);
try {
const tokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
mintAuthority!,
mint,
userPublicKey,
);
const accountInfo = await getAccount(connection, tokenAccount.address);
return Number(accountInfo.amount) / 10 ** DECIMALS;
} catch {
return 0;
}
} catch (error) {
console.error("Failed to get token balance:", error);
return 0;
}
}
// Request airdrop for testing (devnet only)
export async function requestAirdrop(walletAddress: string): Promise<string | null> {
try {
const publicKey = new PublicKey(walletAddress);
const signature = await connection.requestAirdrop(publicKey, 2 * 1000000000); // 2 SOL
await connection.confirmTransaction(signature);
return signature;
} catch (error) {
console.error('Airdrop failed:', error);
return null;
}
export async function requestAirdrop(
walletAddress: string,
): Promise<string | null> {
try {
const publicKey = new PublicKey(walletAddress);
const signature = await connection.requestAirdrop(
publicKey,
2 * 1000000000,
); // 2 SOL
await connection.confirmTransaction(signature);
return signature;
} catch (error) {
console.error("Airdrop failed:", error);
return null;
}
}