feat: privacy policy + download data & delete account

This commit is contained in:
Face 2025-05-29 20:36:42 +03:00
parent 95df713b06
commit fc5c16e6dd
14 changed files with 4159 additions and 52 deletions

View file

@ -0,0 +1,240 @@
import { auth } from '$lib/auth';
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import {
user,
transaction,
coin,
userPortfolio,
predictionBet,
predictionQuestion,
comment,
commentLike,
promoCodeRedemption,
promoCode,
session
} from '$lib/server/db/schema';
import { eq, and, lte } from 'drizzle-orm';
export async function HEAD({ request }) {
const authSession = await auth.api.getSession({
headers: request.headers
});
if (!authSession?.user) {
throw error(401, 'Not authenticated');
}
const userId = Number(authSession.user.id);
try {
// Quick check to estimate file size without generating full data
// Get counts of major data types to estimate size
const userExists = await db.select({ id: user.id })
.from(user)
.where(eq(user.id, userId))
.limit(1);
if (!userExists.length) {
throw error(404, 'User not found');
}
// Estimate file size based on typical data sizes
// This is a rough estimate - actual size may vary
const estimatedSize = 1024 * 50; // Base 50KB estimate
return new Response(null, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Content-Disposition': `attachment; filename="rugplay-data-${userId}-${new Date().toISOString().split('T')[0]}.json"`,
'Content-Length': estimatedSize.toString(),
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
} catch (err) {
console.error('Data export HEAD error:', err);
throw error(500, 'Failed to check export availability');
}
}
export async function GET({ request }) {
const authSession = await auth.api.getSession({
headers: request.headers
});
if (!authSession?.user) {
throw error(401, 'Not authenticated');
}
const userId = Number(authSession.user.id);
try {
// Get user data
const userData = await db.select()
.from(user)
.where(eq(user.id, userId))
.limit(1);
if (!userData.length) {
throw error(404, 'User not found');
}
// Get all user's transactions
const transactions = await db.select({
id: transaction.id,
coinId: transaction.coinId,
coinName: coin.name,
coinSymbol: coin.symbol,
type: transaction.type,
quantity: transaction.quantity,
pricePerCoin: transaction.pricePerCoin,
totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount,
timestamp: transaction.timestamp
})
.from(transaction)
.leftJoin(coin, eq(transaction.coinId, coin.id))
.where(eq(transaction.userId, userId));
// Get user's portfolio
const portfolio = await db.select({
coinId: userPortfolio.coinId,
coinName: coin.name,
coinSymbol: coin.symbol,
quantity: userPortfolio.quantity,
updatedAt: userPortfolio.updatedAt
})
.from(userPortfolio)
.leftJoin(coin, eq(userPortfolio.coinId, coin.id))
.where(eq(userPortfolio.userId, userId));
// Get user's prediction bets
const predictionBets = await db.select({
id: predictionBet.id,
questionId: predictionBet.questionId,
question: predictionQuestion.question,
side: predictionBet.side,
amount: predictionBet.amount,
actualWinnings: predictionBet.actualWinnings,
createdAt: predictionBet.createdAt,
settledAt: predictionBet.settledAt
})
.from(predictionBet)
.leftJoin(predictionQuestion, eq(predictionBet.questionId, predictionQuestion.id))
.where(eq(predictionBet.userId, userId));
// Get user's comments
const comments = await db.select({
id: comment.id,
coinId: comment.coinId,
coinName: coin.name,
coinSymbol: coin.symbol,
content: comment.content,
likesCount: comment.likesCount,
createdAt: comment.createdAt,
updatedAt: comment.updatedAt,
isDeleted: comment.isDeleted
})
.from(comment)
.leftJoin(coin, eq(comment.coinId, coin.id))
.where(eq(comment.userId, userId));
// Get user's comment likes
const commentLikes = await db.select({
commentId: commentLike.commentId,
createdAt: commentLike.createdAt
})
.from(commentLike)
.where(eq(commentLike.userId, userId));
// Get user's promo code redemptions
const promoRedemptions = await db.select({
id: promoCodeRedemption.id,
promoCodeId: promoCodeRedemption.promoCodeId,
promoCode: promoCode.code,
rewardAmount: promoCodeRedemption.rewardAmount,
redeemedAt: promoCodeRedemption.redeemedAt
})
.from(promoCodeRedemption)
.leftJoin(promoCode, eq(promoCodeRedemption.promoCodeId, promoCode.id))
.where(eq(promoCodeRedemption.userId, userId));
// Get user's sessions (limited to active ones for privacy)
const sessions = await db.select({
id: session.id,
expiresAt: session.expiresAt,
createdAt: session.createdAt,
ipAddress: session.ipAddress,
userAgent: session.userAgent
})
.from(session)
.where(and(
eq(session.userId, userId),
lte(session.expiresAt, new Date())
))
// Get coins created by user
const createdCoins = await db.select({
id: coin.id,
name: coin.name,
symbol: coin.symbol,
icon: coin.icon,
initialSupply: coin.initialSupply,
circulatingSupply: coin.circulatingSupply,
marketCap: coin.marketCap,
price: coin.currentPrice,
change24h: coin.change24h,
poolCoinAmount: coin.poolCoinAmount,
poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount,
createdAt: coin.createdAt,
updatedAt: coin.updatedAt,
isListed: coin.isListed
})
.from(coin)
.where(eq(coin.creatorId, userId));
// Get questions created by user
const createdQuestions = await db.select()
.from(predictionQuestion)
.where(eq(predictionQuestion.creatorId, userId));
// Compile all data
const exportData = {
exportInfo: {
userId: userId,
exportedAt: new Date().toISOString(),
dataType: 'complete_user_data'
},
user: userData[0],
transactions: transactions,
portfolio: portfolio,
predictionBets: predictionBets,
comments: comments,
commentLikes: commentLikes,
promoCodeRedemptions: promoRedemptions,
sessions: sessions,
createdCoins: createdCoins,
createdQuestions: createdQuestions
}; // Serialize the data
const jsonData = JSON.stringify(exportData, null, 2);
const dataSize = new TextEncoder().encode(jsonData).length;
// Return as downloadable JSON with proper headers for streaming download
return new Response(jsonData, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Content-Disposition': `attachment; filename="rugplay-data-${userId}-${new Date().toISOString().split('T')[0]}.json"`,
'Content-Length': dataSize.toString(),
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
} catch (err) {
console.error('Data export error:', err);
throw error(500, 'Failed to export user data');
}
}

View file

@ -0,0 +1,58 @@
import { auth } from '$lib/auth';
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { user, accountDeletionRequest } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
export async function POST({ request }) {
const authSession = await auth.api.getSession({
headers: request.headers
});
if (!authSession?.user) {
throw error(401, 'Not authenticated');
}
const userId = Number(authSession.user.id);
const body = await request.json();
const { confirmationText } = body;
if (confirmationText !== 'DELETE MY ACCOUNT') {
throw error(400, 'Invalid confirmation text');
}
const scheduledDeletionAt = new Date();
scheduledDeletionAt.setDate(scheduledDeletionAt.getDate() + 14);
await db.transaction(async (tx) => {
const existingRequest = await tx.select()
.from(accountDeletionRequest)
.where(eq(accountDeletionRequest.userId, userId))
.limit(1);
if (existingRequest.length > 0) {
throw new Error('Account deletion already requested');
}
await tx.insert(accountDeletionRequest).values({
userId,
scheduledDeletionAt,
reason: 'User requested account deletion'
});
await tx.update(user)
.set({
isBanned: true,
banReason: 'Account deletion requested - scheduled for ' + scheduledDeletionAt.toISOString(),
updatedAt: new Date()
})
.where(eq(user.id, userId));
});
return json({
success: true,
message: `Account deletion has been scheduled for ${scheduledDeletionAt.toLocaleDateString()}. Your account has been temporarily suspended. You can cancel this request by contacting support before the scheduled date.`,
scheduledDeletionAt: scheduledDeletionAt.toISOString()
});
}