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({ const session = await prisma.session.create({
data: { data: {
userId: user.id, 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), expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
}, },
}); });
+65 -53
View File
@@ -1,39 +1,48 @@
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
createBurnInstruction,
createMintToInstruction,
getAccount,
getOrCreateAssociatedTokenAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { import {
Connection, Connection,
PublicKey,
Keypair, Keypair,
Transaction, PublicKey,
SystemProgram, SystemProgram,
sendAndConfirmTransaction sendAndConfirmTransaction,
} from '@solana/web3.js'; Transaction,
import { } from "@solana/web3.js";
getOrCreateAssociatedTokenAccount, import bs58 from "bs58";
createMintToInstruction, import { prisma } from "../utils/prisma";
createBurnInstruction,
getAccount,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
} from '@solana/spl-token';
import bs58 from 'bs58';
import { prisma } from '../utils/prisma';
const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com'; 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 PROGRAM_ID = new PublicKey(
const DECIMALS = parseInt(process.env.SOLANA_TOKEN_DECIMALS || '6'); process.env.SOLANA_PROGRAM_ID ||
"CoopCredits111111111111111111111111111111111",
);
const DECIMALS = parseInt(process.env.SOLANA_TOKEN_DECIMALS || "6");
// Backend mint authority keypair (stored securely) // Backend mint authority keypair (stored securely)
let mintAuthority: Keypair | null = null; let mintAuthority: Keypair | null = null;
try { try {
if (process.env.SOLANA_MINT_AUTHORITY_KEYPAIR) { if (process.env.SOLANA_MINT_AUTHORITY_KEYPAIR) {
const secretKey = bs58.decode(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); mintAuthority = Keypair.fromSecretKey(secretKey);
} }
} catch (error) { } 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 // Get token mint address from program state
export async function getTokenMint(): Promise<PublicKey | null> { export async function getTokenMint(): Promise<PublicKey | null> {
@@ -47,7 +56,7 @@ export async function getTokenMint(): Promise<PublicKey | null> {
} }
return null; return null;
} catch (error) { } catch (error) {
console.error('Failed to get token mint:', error); console.error("Failed to get token mint:", error);
return null; return null;
} }
} }
@@ -58,20 +67,20 @@ export function createWallet(): { publicKey: string; secretKey: string } {
return { return {
publicKey: keypair.publicKey.toBase58(), publicKey: keypair.publicKey.toBase58(),
secretKey: bs58.encode(keypair.secretKey) secretKey: bs58.encode(keypair.secretKey),
}; };
} }
// Get or create token account for user // Get or create token account for user
export async function getOrCreateTokenAccount( export async function getOrCreateTokenAccount(
userPublicKey: PublicKey, userPublicKey: PublicKey,
mint: PublicKey mint: PublicKey,
): Promise<PublicKey> { ): Promise<PublicKey> {
const tokenAccount = await getOrCreateAssociatedTokenAccount( const tokenAccount = await getOrCreateAssociatedTokenAccount(
connection, connection,
mintAuthority!, // payer mintAuthority!, // payer
mint, mint,
userPublicKey userPublicKey,
); );
return tokenAccount.address; return tokenAccount.address;
@@ -85,10 +94,10 @@ export async function mintTokens(
sessionId: string; sessionId: string;
contentTitle: string; contentTitle: string;
watchDurationMinutes: number; watchDurationMinutes: number;
} },
): Promise<string | null> { ): Promise<string | null> {
if (!mintAuthority) { if (!mintAuthority) {
throw new Error('Mint authority not configured'); throw new Error("Mint authority not configured");
} }
try { try {
@@ -96,36 +105,34 @@ export async function mintTokens(
const mint = await getTokenMint(); const mint = await getTokenMint();
if (!mint) { if (!mint) {
throw new Error('Token mint not found'); throw new Error("Token mint not found");
} }
// Get or create user's token account // Get or create user's token account
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint); const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
// Calculate amount with decimals // Calculate amount with decimals
const amountWithDecimals = amount * Math.pow(10, DECIMALS); const amountWithDecimals = amount * 10 ** DECIMALS;
// Create mint instruction // Create mint instruction
const mintInstruction = createMintToInstruction( const mintInstruction = createMintToInstruction(
mint, mint,
tokenAccount, tokenAccount,
mintAuthority.publicKey, mintAuthority.publicKey,
BigInt(Math.floor(amountWithDecimals)) BigInt(Math.floor(amountWithDecimals)),
); );
// Create and send transaction // Create and send transaction
const transaction = new Transaction().add(mintInstruction); const transaction = new Transaction().add(mintInstruction);
const signature = await sendAndConfirmTransaction( const signature = await sendAndConfirmTransaction(connection, transaction, [
connection, mintAuthority,
transaction, ]);
[mintAuthority]
);
console.log(`Minted ${amount} COOP to ${userWalletAddress}: ${signature}`); console.log(`Minted ${amount} COOP to ${userWalletAddress}: ${signature}`);
return signature; return signature;
} catch (error) { } catch (error) {
console.error('Failed to mint tokens:', error); console.error("Failed to mint tokens:", error);
return null; return null;
} }
} }
@@ -134,14 +141,14 @@ export async function mintTokens(
export async function burnTokens( export async function burnTokens(
userWalletAddress: string, userWalletAddress: string,
userSecretKey: string, userSecretKey: string,
amount: number amount: number,
): Promise<string | null> { ): Promise<string | null> {
try { try {
const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey)); const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey));
const mint = await getTokenMint(); const mint = await getTokenMint();
if (!mint) { if (!mint) {
throw new Error('Token mint not found'); throw new Error("Token mint not found");
} }
const userPublicKey = new PublicKey(userWalletAddress); const userPublicKey = new PublicKey(userWalletAddress);
@@ -149,10 +156,10 @@ export async function burnTokens(
// Check balance // Check balance
const accountInfo = await getAccount(connection, tokenAccount); const accountInfo = await getAccount(connection, tokenAccount);
const amountWithDecimals = BigInt(Math.floor(amount * Math.pow(10, DECIMALS))); const amountWithDecimals = BigInt(Math.floor(amount * 10 ** DECIMALS));
if (accountInfo.amount < amountWithDecimals) { if (accountInfo.amount < amountWithDecimals) {
throw new Error('Insufficient balance'); throw new Error("Insufficient balance");
} }
// Create burn instruction // Create burn instruction
@@ -160,21 +167,21 @@ export async function burnTokens(
tokenAccount, tokenAccount,
mint, mint,
userKeypair.publicKey, userKeypair.publicKey,
amountWithDecimals amountWithDecimals,
); );
const transaction = new Transaction().add(burnInstruction); const transaction = new Transaction().add(burnInstruction);
const signature = await sendAndConfirmTransaction( const signature = await sendAndConfirmTransaction(connection, transaction, [
connection, userKeypair,
transaction, ]);
[userKeypair]
);
console.log(`Burned ${amount} COOP from ${userWalletAddress}: ${signature}`); console.log(
`Burned ${amount} COOP from ${userWalletAddress}: ${signature}`,
);
return signature; return signature;
} catch (error) { } catch (error) {
console.error('Failed to burn tokens:', error); console.error("Failed to burn tokens:", error);
return null; return null;
} }
} }
@@ -192,29 +199,34 @@ export async function getTokenBalance(walletAddress: string): Promise<number> {
connection, connection,
mintAuthority!, mintAuthority!,
mint, mint,
userPublicKey userPublicKey,
); );
const accountInfo = await getAccount(connection, tokenAccount.address); const accountInfo = await getAccount(connection, tokenAccount.address);
return Number(accountInfo.amount) / Math.pow(10, DECIMALS); return Number(accountInfo.amount) / 10 ** DECIMALS;
} catch { } catch {
return 0; return 0;
} }
} catch (error) { } catch (error) {
console.error('Failed to get token balance:', error); console.error("Failed to get token balance:", error);
return 0; return 0;
} }
} }
// Request airdrop for testing (devnet only) // Request airdrop for testing (devnet only)
export async function requestAirdrop(walletAddress: string): Promise<string | null> { export async function requestAirdrop(
walletAddress: string,
): Promise<string | null> {
try { try {
const publicKey = new PublicKey(walletAddress); const publicKey = new PublicKey(walletAddress);
const signature = await connection.requestAirdrop(publicKey, 2 * 1000000000); // 2 SOL const signature = await connection.requestAirdrop(
publicKey,
2 * 1000000000,
); // 2 SOL
await connection.confirmTransaction(signature); await connection.confirmTransaction(signature);
return signature; return signature;
} catch (error) { } catch (error) {
console.error('Airdrop failed:', error); console.error("Airdrop failed:", error);
return null; return null;
} }
} }