feat: privacy policy + download data & delete account
This commit is contained in:
parent
95df713b06
commit
fc5c16e6dd
14 changed files with 4159 additions and 52 deletions
70
website/drizzle/0012_glamorous_white_tiger.sql
Normal file
70
website/drizzle/0012_glamorous_white_tiger.sql
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS "account_deletion_request" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"requested_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"scheduled_deletion_at" timestamp with time zone NOT NULL,
|
||||||
|
"reason" text,
|
||||||
|
"is_processed" boolean DEFAULT false NOT NULL,
|
||||||
|
CONSTRAINT "account_deletion_request_user_id_unique" UNIQUE("user_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "comment" DROP CONSTRAINT "comment_user_id_user_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "prediction_bet" DROP CONSTRAINT "prediction_bet_user_id_user_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "prediction_question" DROP CONSTRAINT "prediction_question_creator_id_user_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "promo_code" DROP CONSTRAINT "promo_code_created_by_user_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "promo_code_redemption" DROP CONSTRAINT "promo_code_redemption_user_id_user_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "transaction" DROP CONSTRAINT "transaction_user_id_user_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "comment" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "prediction_bet" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "prediction_question" ALTER COLUMN "creator_id" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "promo_code_redemption" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "transaction" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "account_deletion_request" ADD CONSTRAINT "account_deletion_request_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "account_deletion_request_user_id_idx" ON "account_deletion_request" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "account_deletion_request_scheduled_deletion_idx" ON "account_deletion_request" USING btree ("scheduled_deletion_at");--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "comment" ADD CONSTRAINT "comment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "prediction_bet" ADD CONSTRAINT "prediction_bet_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "prediction_question" ADD CONSTRAINT "prediction_question_creator_id_user_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "promo_code" ADD CONSTRAINT "promo_code_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "promo_code_redemption" ADD CONSTRAINT "promo_code_redemption_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "transaction" ADD CONSTRAINT "transaction_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
1
website/drizzle/0013_big_champions.sql
Normal file
1
website/drizzle/0013_big_champions.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
CREATE INDEX IF NOT EXISTS "account_deletion_request_open_idx" ON "account_deletion_request" USING btree ("user_id") WHERE is_processed = false;
|
||||||
1503
website/drizzle/meta/0012_snapshot.json
Normal file
1503
website/drizzle/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1519
website/drizzle/meta/0013_snapshot.json
Normal file
1519
website/drizzle/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -85,6 +85,20 @@
|
||||||
"when": 1748528211995,
|
"when": 1748528211995,
|
||||||
"tag": "0011_broken_risque",
|
"tag": "0011_broken_risque",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1748537205986,
|
||||||
|
"tag": "0012_glamorous_white_tiger",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 13,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1748540176485,
|
||||||
|
"tag": "0013_big_champions",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { auth } from "$lib/auth";
|
import { auth } from "$lib/auth";
|
||||||
import { resolveExpiredQuestions } from "$lib/server/job";
|
import { resolveExpiredQuestions, processAccountDeletions } from "$lib/server/job";
|
||||||
import { svelteKitHandler } from "better-auth/svelte-kit";
|
import { svelteKitHandler } from "better-auth/svelte-kit";
|
||||||
import { redis } from "$lib/server/redis";
|
import { redis } from "$lib/server/redis";
|
||||||
import { building } from '$app/environment';
|
import { building } from '$app/environment';
|
||||||
|
|
@ -38,9 +38,11 @@ async function initializeScheduler() {
|
||||||
}, (lockTTL / 2) * 1000); // Renew at half the TTL
|
}, (lockTTL / 2) * 1000); // Renew at half the TTL
|
||||||
|
|
||||||
resolveExpiredQuestions().catch(console.error);
|
resolveExpiredQuestions().catch(console.error);
|
||||||
|
processAccountDeletions().catch(console.error);
|
||||||
|
|
||||||
const schedulerInterval = setInterval(() => {
|
const schedulerInterval = setInterval(() => {
|
||||||
resolveExpiredQuestions().catch(console.error);
|
resolveExpiredQuestions().catch(console.error);
|
||||||
|
processAccountDeletions().catch(console.error);
|
||||||
}, 5 * 60 * 1000);
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
// Cleanup on process exit
|
// Cleanup on process exit
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export const userPortfolio = pgTable("user_portfolio", {
|
||||||
|
|
||||||
export const transaction = pgTable("transaction", {
|
export const transaction = pgTable("transaction", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
userId: integer("user_id").references(() => user.id, { onDelete: "set null" }),
|
||||||
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
||||||
type: transactionTypeEnum("type").notNull(),
|
type: transactionTypeEnum("type").notNull(),
|
||||||
quantity: decimal("quantity", { precision: 30, scale: 8 }).notNull(),
|
quantity: decimal("quantity", { precision: 30, scale: 8 }).notNull(),
|
||||||
|
|
@ -121,7 +121,7 @@ export const priceHistory = pgTable("price_history", {
|
||||||
|
|
||||||
export const comment = pgTable("comment", {
|
export const comment = pgTable("comment", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
userId: integer("user_id").references(() => user.id, { onDelete: "set null" }),
|
||||||
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
||||||
content: varchar("content", { length: 500 }).notNull(),
|
content: varchar("content", { length: 500 }).notNull(),
|
||||||
likesCount: integer("likes_count").notNull().default(0),
|
likesCount: integer("likes_count").notNull().default(0),
|
||||||
|
|
@ -154,12 +154,12 @@ export const promoCode = pgTable('promo_code', {
|
||||||
isActive: boolean('is_active').notNull().default(true),
|
isActive: boolean('is_active').notNull().default(true),
|
||||||
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
createdBy: integer('created_by').references(() => user.id),
|
createdBy: integer('created_by').references(() => user.id, { onDelete: "set null" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const promoCodeRedemption = pgTable('promo_code_redemption', {
|
export const promoCodeRedemption = pgTable('promo_code_redemption', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
userId: integer('user_id').notNull().references(() => user.id),
|
userId: integer('user_id').references(() => user.id, { onDelete: "cascade" }),
|
||||||
promoCodeId: integer('promo_code_id').notNull().references(() => promoCode.id),
|
promoCodeId: integer('promo_code_id').notNull().references(() => promoCode.id),
|
||||||
rewardAmount: decimal('reward_amount', { precision: 20, scale: 8 }).notNull(),
|
rewardAmount: decimal('reward_amount', { precision: 20, scale: 8 }).notNull(),
|
||||||
redeemedAt: timestamp('redeemed_at', { withTimezone: true }).notNull().defaultNow(),
|
redeemedAt: timestamp('redeemed_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
|
@ -169,7 +169,7 @@ export const promoCodeRedemption = pgTable('promo_code_redemption', {
|
||||||
|
|
||||||
export const predictionQuestion = pgTable("prediction_question", {
|
export const predictionQuestion = pgTable("prediction_question", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
creatorId: integer("creator_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
creatorId: integer("creator_id").references(() => user.id, { onDelete: "set null" }),
|
||||||
question: varchar("question", { length: 200 }).notNull(),
|
question: varchar("question", { length: 200 }).notNull(),
|
||||||
status: predictionMarketEnum("status").notNull().default("ACTIVE"),
|
status: predictionMarketEnum("status").notNull().default("ACTIVE"),
|
||||||
resolutionDate: timestamp("resolution_date", { withTimezone: true }).notNull(),
|
resolutionDate: timestamp("resolution_date", { withTimezone: true }).notNull(),
|
||||||
|
|
@ -192,7 +192,7 @@ export const predictionQuestion = pgTable("prediction_question", {
|
||||||
|
|
||||||
export const predictionBet = pgTable("prediction_bet", {
|
export const predictionBet = pgTable("prediction_bet", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
userId: integer("user_id").references(() => user.id, { onDelete: "set null" }),
|
||||||
questionId: integer("question_id").notNull().references(() => predictionQuestion.id, { onDelete: "cascade" }),
|
questionId: integer("question_id").notNull().references(() => predictionQuestion.id, { onDelete: "cascade" }),
|
||||||
side: boolean("side").notNull(), // true = YES, false = NO
|
side: boolean("side").notNull(), // true = YES, false = NO
|
||||||
amount: decimal("amount", { precision: 20, scale: 8 }).notNull(),
|
amount: decimal("amount", { precision: 20, scale: 8 }).notNull(),
|
||||||
|
|
@ -208,3 +208,20 @@ export const predictionBet = pgTable("prediction_bet", {
|
||||||
amountCheck: check("amount_positive", sql`amount > 0`),
|
amountCheck: check("amount_positive", sql`amount > 0`),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const accountDeletionRequest = pgTable("account_deletion_request", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }).unique(),
|
||||||
|
requestedAt: timestamp("requested_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
scheduledDeletionAt: timestamp("scheduled_deletion_at", { withTimezone: true }).notNull(),
|
||||||
|
reason: text("reason"),
|
||||||
|
isProcessed: boolean("is_processed").notNull().default(false),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
userIdIdx: index("account_deletion_request_user_id_idx").on(table.userId),
|
||||||
|
scheduledDeletionIdx: index("account_deletion_request_scheduled_deletion_idx").on(table.scheduledDeletionAt),
|
||||||
|
oneOpenRequest: index("account_deletion_request_open_idx")
|
||||||
|
.on(table.userId)
|
||||||
|
.where(sql`is_processed = false`),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { predictionQuestion, predictionBet, user } from '$lib/server/db/schema';
|
import { predictionQuestion, predictionBet, user, accountDeletionRequest, session, account, promoCodeRedemption, userPortfolio, commentLike, comment, transaction, coin } from '$lib/server/db/schema';
|
||||||
import { eq, and, lte, isNull } from 'drizzle-orm';
|
import { eq, and, lte, isNull } from 'drizzle-orm';
|
||||||
import { resolveQuestion, getRugplayData } from '$lib/server/ai';
|
import { resolveQuestion, getRugplayData } from '$lib/server/ai';
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ export async function resolveExpiredQuestions() {
|
||||||
})
|
})
|
||||||
.where(eq(predictionBet.id, bet.id));
|
.where(eq(predictionBet.id, bet.id));
|
||||||
|
|
||||||
if (won && winnings > 0) {
|
if (won && winnings > 0 && bet.userId !== null) {
|
||||||
const [userData] = await tx
|
const [userData] = await tx
|
||||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||||
.from(user)
|
.from(user)
|
||||||
|
|
@ -114,3 +114,73 @@ export async function resolveExpiredQuestions() {
|
||||||
console.error('Error in resolveExpiredQuestions:', error);
|
console.error('Error in resolveExpiredQuestions:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function processAccountDeletions() {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const expiredRequests = await db.select()
|
||||||
|
.from(accountDeletionRequest)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
lte(accountDeletionRequest.scheduledDeletionAt, now),
|
||||||
|
eq(accountDeletionRequest.isProcessed, false)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`🗑️ Processing ${expiredRequests.length} expired account deletion requests`);
|
||||||
|
|
||||||
|
for (const request of expiredRequests) {
|
||||||
|
try {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const userId = request.userId;
|
||||||
|
|
||||||
|
await tx.update(transaction)
|
||||||
|
.set({ userId: null })
|
||||||
|
.where(eq(transaction.userId, userId));
|
||||||
|
|
||||||
|
await tx.update(comment)
|
||||||
|
.set({ userId: null, content: "[deleted]", isDeleted: true })
|
||||||
|
.where(eq(comment.userId, userId));
|
||||||
|
|
||||||
|
await tx.update(predictionBet)
|
||||||
|
.set({ userId: null })
|
||||||
|
.where(eq(predictionBet.userId, userId));
|
||||||
|
|
||||||
|
await tx.update(predictionQuestion)
|
||||||
|
.set({ creatorId: null })
|
||||||
|
.where(eq(predictionQuestion.creatorId, userId));
|
||||||
|
|
||||||
|
await tx.update(coin)
|
||||||
|
.set({ creatorId: null })
|
||||||
|
.where(eq(coin.creatorId, userId));
|
||||||
|
|
||||||
|
await tx.delete(session).where(eq(session.userId, userId));
|
||||||
|
await tx.delete(account).where(eq(account.userId, userId));
|
||||||
|
await tx.delete(promoCodeRedemption).where(eq(promoCodeRedemption.userId, userId));
|
||||||
|
await tx.delete(userPortfolio).where(eq(userPortfolio.userId, userId));
|
||||||
|
await tx.delete(commentLike).where(eq(commentLike.userId, userId));
|
||||||
|
|
||||||
|
await tx.update(accountDeletionRequest)
|
||||||
|
.set({ isProcessed: true })
|
||||||
|
.where(eq(accountDeletionRequest.id, request.id));
|
||||||
|
|
||||||
|
await tx.delete(user).where(eq(user.id, userId));
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Successfully processed account deletion for user ID: ${request.userId}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ Failed to process account deletion for user ID: ${request.userId}`, error);
|
||||||
|
|
||||||
|
await db.update(accountDeletionRequest)
|
||||||
|
.set({
|
||||||
|
isProcessed: true, // Mark as processed to avoid retries, but log the failure
|
||||||
|
reason: request.reason ? `${request.reason} - FAILED: ${error.message}` : `FAILED: ${error.message}`
|
||||||
|
})
|
||||||
|
.where(eq(accountDeletionRequest.id, request.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing account deletions:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,16 +26,17 @@ export interface PredictionQuestion {
|
||||||
estimatedYesWinnings?: number;
|
estimatedYesWinnings?: number;
|
||||||
estimatedNoWinnings?: number;
|
estimatedNoWinnings?: number;
|
||||||
};
|
};
|
||||||
|
// fuck gdpr and all that fucking shit
|
||||||
recentBets?: Array<{
|
recentBets?: Array<{
|
||||||
id: number;
|
id?: number;
|
||||||
side: boolean;
|
side: boolean;
|
||||||
amount: number;
|
amount: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
user: {
|
user?: {
|
||||||
id: number;
|
id?: number;
|
||||||
name: string;
|
name?: string;
|
||||||
username: string;
|
username?: string;
|
||||||
image: string;
|
image?: string;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
240
website/src/routes/api/settings/data-download/+server.ts
Normal file
240
website/src/routes/api/settings/data-download/+server.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
58
website/src/routes/api/settings/delete-account/+server.ts
Normal file
58
website/src/routes/api/settings/delete-account/+server.ts
Normal 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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -570,28 +570,36 @@
|
||||||
<Card.Content class="pb-6">
|
<Card.Content class="pb-6">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each question.recentBets as bet}
|
{#each question.recentBets as bet}
|
||||||
|
{#if bet.user}
|
||||||
<div class="flex items-center justify-between rounded-xl border p-4">
|
<div class="flex items-center justify-between rounded-xl border p-4">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<HoverCard.Root>
|
<HoverCard.Root>
|
||||||
<HoverCard.Trigger>
|
<HoverCard.Trigger>
|
||||||
<button
|
<button
|
||||||
class="flex cursor-pointer items-center gap-3 text-left"
|
class="flex cursor-pointer items-center gap-3 text-left"
|
||||||
onclick={() => goto(`/user/${bet.user.username}`)}
|
onclick={() => goto(`/user/${bet.user?.username}`)}
|
||||||
>
|
>
|
||||||
<Avatar.Root class="h-10 w-10">
|
<Avatar.Root class="h-10 w-10">
|
||||||
<Avatar.Image src={getPublicUrl(bet.user.image)} alt={bet.user.name} />
|
<Avatar.Image
|
||||||
|
src={getPublicUrl(bet.user?.image || null)}
|
||||||
|
alt={bet.user?.name || bet.user?.username}
|
||||||
|
/>
|
||||||
<Avatar.Fallback class="text-sm"
|
<Avatar.Fallback class="text-sm"
|
||||||
>{bet.user.name.charAt(0)}</Avatar.Fallback
|
>{(bet.user?.name || bet.user?.username || 'U').charAt(
|
||||||
|
0
|
||||||
|
)}</Avatar.Fallback
|
||||||
>
|
>
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold hover:underline">{bet.user.name}</div>
|
<div class="font-semibold hover:underline">{bet.user?.name || "Deleted User"}</div>
|
||||||
<div class="text-muted-foreground text-sm">@{bet.user.username}</div>
|
<div class="text-muted-foreground text-sm">@{bet.user?.username || "deleted_user"}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</HoverCard.Trigger>
|
</HoverCard.Trigger>
|
||||||
<HoverCard.Content class="w-80">
|
<HoverCard.Content class="w-80">
|
||||||
<UserProfilePreview userId={bet.user.id} />
|
{#if bet.user?.id}
|
||||||
|
<UserProfilePreview userId={bet.user?.id} />
|
||||||
|
{/if}
|
||||||
</HoverCard.Content>
|
</HoverCard.Content>
|
||||||
</HoverCard.Root>
|
</HoverCard.Root>
|
||||||
<Badge variant="destructive" class={bet.side ? 'bg-success/80!' : ''}>
|
<Badge variant="destructive" class={bet.side ? 'bg-success/80!' : ''}>
|
||||||
|
|
@ -605,6 +613,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
|
||||||
413
website/src/routes/legal/privacy/+page.svelte
Normal file
413
website/src/routes/legal/privacy/+page.svelte
Normal file
|
|
@ -0,0 +1,413 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import * as Alert from '$lib/components/ui/alert';
|
||||||
|
import ShieldCheck from 'lucide-svelte/icons/shield-check';
|
||||||
|
import AlertTriangle from 'lucide-svelte/icons/alert-triangle';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
|
const LAST_UPDATED = 'May 29, 2025';
|
||||||
|
const CONTACT_EMAIL = 'privacy@outpoot.com';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Privacy Policy - Rugplay</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Learn how we protect your privacy on Rugplay and what data we retain after account deletion."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="privacy, policy, data protection, crypto, trading, GDPR, rugplay"
|
||||||
|
/>
|
||||||
|
<meta property="og:title" content="Privacy Policy - Rugplay" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Learn how we protect your privacy on Rugplay and what data we retain after account deletion."
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content={page.url.href} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container mx-auto max-w-4xl py-10">
|
||||||
|
<Card.Root class="p-6">
|
||||||
|
<div class="mb-8 flex items-center gap-3">
|
||||||
|
<ShieldCheck class="text-primary h-10 w-10" />
|
||||||
|
<div>
|
||||||
|
<h1 class="text-4xl font-bold">Privacy Policy</h1>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
Last updated: {LAST_UPDATED}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert.Root class="mb-6">
|
||||||
|
<AlertTriangle class="h-4 w-4" />
|
||||||
|
<Alert.Title>Important Notice About Account Deletion</Alert.Title>
|
||||||
|
<Alert.Description>
|
||||||
|
When you delete your account, all personal information is permanently removed, but some
|
||||||
|
anonymized data may be retained for platform integrity. This is explained in detail below.
|
||||||
|
</Alert.Description>
|
||||||
|
</Alert.Root>
|
||||||
|
|
||||||
|
<Separator class="my-6" />
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">1. Our Privacy Commitment</h2>
|
||||||
|
<p class="mb-4">
|
||||||
|
We are committed to protecting your privacy while providing a secure and functional crypto
|
||||||
|
trading simulation platform. This policy explains exactly what data we collect, how we use
|
||||||
|
it, and what happens when you delete your account.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Platform Note:</strong> Rugplay is a simulated trading environment using virtual currency
|
||||||
|
("*BUSS" or "$") with no real monetary value.
|
||||||
|
</p>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">2. Information We Collect</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-medium">2.1 Account Information</h3>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>Email address</li>
|
||||||
|
<li>Username</li>
|
||||||
|
<li>Profile information (name, bio, profile picture)</li>
|
||||||
|
<li>Account preferences and settings</li>
|
||||||
|
<li>Authentication tokens and session data</li>
|
||||||
|
<li>
|
||||||
|
IP addresses and browser/user agent information (for security and session
|
||||||
|
management)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-medium">2.2 Trading and Financial Data (Simulated)</h3>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>Transaction history (buy/sell orders, amounts, prices, timestamps)</li>
|
||||||
|
<li>Portfolio holdings and balances</li>
|
||||||
|
<li>Trading patterns and preferences</li>
|
||||||
|
<li>Prediction market bets and outcomes</li>
|
||||||
|
<li>Reward claims and promotional code usage</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-medium">2.3 Platform Activity</h3>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>Comments and posts on coin pages</li>
|
||||||
|
<li>Likes and interactions with content</li>
|
||||||
|
<li>Login activity and streaks</li>
|
||||||
|
<!-- IP addresses and browser information already covered in 2.1 -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">3. How We Use Your Data</h2>
|
||||||
|
<p class="mb-4">We use your data for:</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>Providing trading platform functionality</li>
|
||||||
|
<li>Maintaining accurate portfolio and transaction records</li>
|
||||||
|
<li>Calculating market prices and liquidity</li>
|
||||||
|
<li>Fraud prevention and security monitoring</li>
|
||||||
|
<li>Platform analytics and improvements</li>
|
||||||
|
<li>Resolving disputes and maintaining system integrity</li>
|
||||||
|
<li>
|
||||||
|
Operating and resolving prediction markets, which may involve automated decision-making
|
||||||
|
as detailed below.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 class="mb-2 mt-4 text-lg font-medium">
|
||||||
|
3.1 Automated Decision-Making (Prediction Markets)
|
||||||
|
</h3>
|
||||||
|
<p class="mb-2">
|
||||||
|
Our prediction markets are entirely AI-automated systems that determine outcomes of
|
||||||
|
prediction questions without human intervention. The AI processes publicly available
|
||||||
|
information and platform data to settle expired prediction questions automatically.
|
||||||
|
</p>
|
||||||
|
<p class="mb-2">
|
||||||
|
<strong>No Guarantee of Accuracy:</strong> AI decisions are not guaranteed to be correct or
|
||||||
|
accurate. The automated system may make errors in interpreting data or determining outcomes.
|
||||||
|
</p>
|
||||||
|
<p class="mb-2">
|
||||||
|
<strong>Significance and Envisaged Consequences:</strong> These automated decisions directly
|
||||||
|
determine whether your prediction bets are won or lost and the corresponding virtual payouts.
|
||||||
|
All outcomes are final once processed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Limitation of Liability:</strong> We are not responsible for any losses, incorrect
|
||||||
|
outcomes, or disputes arising from automated AI decisions in prediction markets. By participating,
|
||||||
|
you acknowledge and accept the risks of AI-automated resolution systems.
|
||||||
|
</p>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="text-destructive mb-4 text-2xl font-semibold">
|
||||||
|
4. Account Deletion and Data Retention
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Alert.Root class="mb-4">
|
||||||
|
<AlertTriangle class="h-4 w-4" />
|
||||||
|
<Alert.Title>14-Day Deletion Process</Alert.Title>
|
||||||
|
<Alert.Description>
|
||||||
|
Account deletion is scheduled 14 days after your request. During this period, your
|
||||||
|
account is suspended but you can contact support to cancel the deletion.
|
||||||
|
</Alert.Description>
|
||||||
|
</Alert.Root>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-medium" style="color: oklch(var(--success))">
|
||||||
|
4.1 What Gets Permanently Deleted
|
||||||
|
</h3>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>Your personal information (name, email, username, bio, profile picture)</li>
|
||||||
|
<li>
|
||||||
|
Authentication sessions (including IP address and user agent history) and login
|
||||||
|
tokens
|
||||||
|
</li>
|
||||||
|
<li>OAuth account connections</li>
|
||||||
|
<li>Account preferences and settings</li>
|
||||||
|
<li>Portfolio holdings and balances (*BUSS ("$") and simulated coins)</li>
|
||||||
|
<li>Records of your likes on comments</li>
|
||||||
|
<li>Promo code redemption records linked to your account</li>
|
||||||
|
<li>Your user account record itself</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-lg border-2 p-4"
|
||||||
|
style="border-color: oklch(0.828 0.189 84.429); background-color: oklch(0.828 0.189 84.429 / 0.1);"
|
||||||
|
>
|
||||||
|
<h3 class="mb-2 text-lg font-medium" style="color: oklch(0.828 0.189 84.429)">
|
||||||
|
4.2 What May Remain (Fully Anonymized)
|
||||||
|
</h3>
|
||||||
|
<p class="mb-3" style="color: oklch(0.828 0.189 84.429 / 0.8)">
|
||||||
|
<strong
|
||||||
|
>The following data may remain but is completely disconnected from your identity
|
||||||
|
(your User ID is removed):</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-2" style="color: oklch(0.828 0.189 84.429 / 0.8)">
|
||||||
|
<li>
|
||||||
|
<strong>Transaction records:</strong> Trading history (buys/sells, amounts, prices, timestamps)
|
||||||
|
with your User ID removed.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Comment content:</strong> Your comments will have their User ID removed, the
|
||||||
|
content replaced with "[deleted]", and marked as deleted. Timestamps and like counts
|
||||||
|
(aggregated on the comment itself) are preserved.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Prediction bets:</strong> Records of bets placed, with your User ID removed.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Created coins:</strong> Simulated coins you created will remain, but your User
|
||||||
|
ID as creator will be removed.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Created prediction questions:</strong> Prediction questions you created will
|
||||||
|
remain, but your User ID as creator will be removed.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-medium">4.3 Why Some Data Remains Anonymized</h3>
|
||||||
|
<p class="mb-3">Anonymized data serves these legitimate purposes:</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>
|
||||||
|
<strong>Market Integrity:</strong> Historical transaction data is needed to maintain
|
||||||
|
accurate price charts and trading history
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Platform Function:</strong> Coins and prediction markets must remain functional
|
||||||
|
even after creators delete their accounts
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Community Content:</strong> Comments remain available to preserve discussion
|
||||||
|
threads, but with anonymous authorship
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>System Balance:</strong> Transaction records ensure the platform's virtual economy
|
||||||
|
remains mathematically consistent
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-muted-foreground mt-3 text-sm">
|
||||||
|
<strong>Important:</strong> This anonymized data cannot be linked back to you in any way
|
||||||
|
as all personal identifiers are permanently removed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-medium">4.4 Deletion Timeline</h3>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li><strong>Immediate:</strong> Account suspended and login disabled</li>
|
||||||
|
<li>
|
||||||
|
<strong>14 days later:</strong> Complete deletion process executed automatically
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cancellation:</strong> Contact support within 14 days to cancel deletion
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">5. Data Security</h2>
|
||||||
|
<p class="mb-4">We implement industry-standard security measures including:</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>Encryption of sensitive data in transit and at rest</li>
|
||||||
|
<li>Regular security audits and monitoring</li>
|
||||||
|
<li>Access controls and authentication requirements</li>
|
||||||
|
<li>Secure database configurations and backups</li>
|
||||||
|
</ul>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">6. Your Data Protection Rights</h2>
|
||||||
|
<p class="mb-4">
|
||||||
|
Depending on your location, you may have certain rights regarding your personal data,
|
||||||
|
including:
|
||||||
|
</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li><strong>Access:</strong> The right to access the personal data we hold about you.</li>
|
||||||
|
<li>
|
||||||
|
<strong>Rectification:</strong> The right to correct inaccurate or incomplete data.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Erasure (Deletion):</strong> The right to request deletion of your personal data
|
||||||
|
(subject to our retention policy and legal obligations, as described in Section 4).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Portability:</strong> The right to export your data in a structured, commonly used,
|
||||||
|
and machine-readable format.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Restrict Processing:</strong> The right to request restriction of processing your
|
||||||
|
personal data under certain conditions.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Object to Processing:</strong> The right to object to processing of your personal
|
||||||
|
data under certain conditions, including for direct marketing purposes (which we do not currently
|
||||||
|
engage in).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Rights related to automated decision-making:</strong> The right not to be subject
|
||||||
|
to a decision based solely on automated processing, including profiling, which produces legal
|
||||||
|
effects concerning you or similarly significantly affects you, except under certain conditions.
|
||||||
|
As described in Section 3.1, we use automated decision-making for prediction market resolution,
|
||||||
|
and we provide information and recourse options.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mt-3">
|
||||||
|
To exercise these rights, please contact us at <a
|
||||||
|
href="mailto:{CONTACT_EMAIL}"
|
||||||
|
class="text-primary underline">{CONTACT_EMAIL}</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">7. Consent and Legal Basis</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-medium">7.1 Explicit Consent Required</h3>
|
||||||
|
<p class="mb-3">By creating an account, you explicitly consent to:</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>Our data collection and processing practices</li>
|
||||||
|
<li>The anonymization and retention policy described in Section 4</li>
|
||||||
|
<li>Processing of your trading data for platform functionality</li>
|
||||||
|
<li>14-day deletion process with suspension during the waiting period</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-medium">7.2 Withdrawal of Consent</h3>
|
||||||
|
<p>You can withdraw consent at any time by deleting your account.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">8. Data Sharing</h2>
|
||||||
|
<p class="mb-4">We do not sell or share your personal data with third parties, except:</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>When required by law or legal process</li>
|
||||||
|
<li>To prevent fraud or protect platform security</li>
|
||||||
|
<li>
|
||||||
|
With service providers who assist in platform operations (under strict data processing
|
||||||
|
agreements)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">9. International Data Transfers</h2>
|
||||||
|
<p class="mb-4">
|
||||||
|
Your data may be processed outside the EU. We ensure adequate protection through standard
|
||||||
|
contractual clauses and appropriate safeguards as required by GDPR.
|
||||||
|
</p>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">10. Cookies and Tracking Technologies</h2>
|
||||||
|
<p class="mb-4">
|
||||||
|
We use cookies solely for authentication and session management purposes. These essential
|
||||||
|
cookies are necessary to maintain your login state and provide secure access to your
|
||||||
|
account.
|
||||||
|
</p>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">11. Contact and Complaints</h2>
|
||||||
|
<p class="mb-4">For privacy-related questions or to exercise your rights:</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
<li>
|
||||||
|
Email: <a href="mailto:{CONTACT_EMAIL}" class="text-primary underline"
|
||||||
|
>{CONTACT_EMAIL}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>To cancel account deletion: Contact us immediately at the above email</li>
|
||||||
|
<li>You have the right to lodge a complaint with your local data protection authority</li>
|
||||||
|
</ul>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">12. Policy Updates</h2>
|
||||||
|
<p>
|
||||||
|
We will notify you of material changes to this policy via email and platform
|
||||||
|
notifications. Continued use after changes constitutes acceptance of the updated policy.
|
||||||
|
</p>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
<div class="rounded-lg p-4 text-sm" style="background-color: oklch(var(--primary) / 0.1);">
|
||||||
|
<p class="mb-2"><strong>Last Updated:</strong> {LAST_UPDATED}</p>
|
||||||
|
<p class="mb-2">
|
||||||
|
<strong>Contact:</strong>
|
||||||
|
<a href="mailto:{CONTACT_EMAIL}" class="text-primary underline">{CONTACT_EMAIL}</a>
|
||||||
|
</p>
|
||||||
|
<p><strong>Platform:</strong> Rugplay - virtual cryptocurrency trading simulation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator class="my-8" />
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-4">
|
||||||
|
<Button variant="outline" size="lg" onclick={() => goto('/legal/terms')}>
|
||||||
|
Terms of Service →
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="lg" onclick={() => goto('/settings')}>
|
||||||
|
Export My Data
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
@ -9,11 +9,12 @@
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Slider } from '$lib/components/ui/slider';
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { CheckIcon, Volume2Icon, VolumeXIcon } from 'lucide-svelte';
|
import { CheckIcon, Volume2Icon, VolumeXIcon, DownloadIcon, Trash2Icon } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { MAX_FILE_SIZE } from '$lib/data/constants';
|
import { MAX_FILE_SIZE } from '$lib/data/constants';
|
||||||
import { volumeSettings } from '$lib/stores/volume-settings';
|
import { volumeSettings } from '$lib/stores/volume-settings';
|
||||||
import { USER_DATA } from '$lib/stores/user-data';
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
|
|
||||||
let name = $state($USER_DATA?.name || '');
|
let name = $state($USER_DATA?.name || '');
|
||||||
let bio = $state($USER_DATA?.bio ?? '');
|
let bio = $state($USER_DATA?.bio ?? '');
|
||||||
|
|
@ -37,10 +38,14 @@
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let usernameAvailable: boolean | null = $state(null);
|
let usernameAvailable: boolean | null = $state(null);
|
||||||
let checkingUsername = $state(false);
|
let checkingUsername = $state(false);
|
||||||
|
|
||||||
let masterVolume = $state(($USER_DATA?.volumeMaster || 0) * 100);
|
let masterVolume = $state(($USER_DATA?.volumeMaster || 0) * 100);
|
||||||
let isMuted = $state($USER_DATA?.volumeMuted || false);
|
let isMuted = $state($USER_DATA?.volumeMuted || false);
|
||||||
|
|
||||||
|
let deleteDialogOpen = $state(false);
|
||||||
|
let deleteConfirmationText = $state('');
|
||||||
|
let isDeleting = $state(false);
|
||||||
|
let isDownloading = $state(false);
|
||||||
|
|
||||||
function beforeUnloadHandler(e: BeforeUnloadEvent) {
|
function beforeUnloadHandler(e: BeforeUnloadEvent) {
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -155,12 +160,103 @@
|
||||||
volumeSettings.setMaster(normalizedValue);
|
volumeSettings.setMaster(normalizedValue);
|
||||||
saveVolumeToServer({ master: normalizedValue, muted: isMuted });
|
saveVolumeToServer({ master: normalizedValue, muted: isMuted });
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMute() {
|
function toggleMute() {
|
||||||
isMuted = !isMuted;
|
isMuted = !isMuted;
|
||||||
volumeSettings.setMuted(isMuted);
|
volumeSettings.setMuted(isMuted);
|
||||||
saveVolumeToServer({ master: masterVolume / 100, muted: isMuted });
|
saveVolumeToServer({ master: masterVolume / 100, muted: isMuted });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadUserData() {
|
||||||
|
isDownloading = true;
|
||||||
|
try {
|
||||||
|
const headResponse = await fetch('/api/settings/data-download', {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!headResponse.ok) {
|
||||||
|
throw new Error('Download service unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLength = headResponse.headers.get('Content-Length');
|
||||||
|
if (contentLength) {
|
||||||
|
const sizeInMB = parseInt(contentLength) / (1024 * 1024);
|
||||||
|
if (sizeInMB > 50) {
|
||||||
|
const proceed = confirm(
|
||||||
|
`Your data export is ${sizeInMB.toFixed(1)}MB. This may take a while to download. Continue?`
|
||||||
|
);
|
||||||
|
if (!proceed) {
|
||||||
|
isDownloading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadUrl = '/api/settings/data-download';
|
||||||
|
|
||||||
|
const downloadWindow = window.open(downloadUrl, '_blank');
|
||||||
|
|
||||||
|
if (!downloadWindow || downloadWindow.closed) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.target = '_blank';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
downloadWindow.close();
|
||||||
|
} catch (e) {}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Your data download has started');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download error:', error);
|
||||||
|
toast.error('Failed to start data download: ' + (error as Error).message);
|
||||||
|
} finally {
|
||||||
|
isDownloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAccount() {
|
||||||
|
if (deleteConfirmationText !== 'DELETE MY ACCOUNT') {
|
||||||
|
toast.error('Please type "DELETE MY ACCOUNT" to confirm');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDeleting = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/delete-account', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
confirmationText: deleteConfirmationText
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
throw new Error(result.message || 'Failed to delete account');
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Account deleted successfully. You will be logged out shortly.');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
}, 2000);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Delete account error:', error);
|
||||||
|
toast.error('Failed to delete account: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
isDeleting = false;
|
||||||
|
deleteDialogOpen = false;
|
||||||
|
deleteConfirmationText = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto max-w-2xl p-6">
|
<div class="container mx-auto max-w-2xl p-6">
|
||||||
|
|
@ -289,5 +385,99 @@
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>Data & Privacy</Card.Title>
|
||||||
|
<Card.Description>Manage your personal data and account</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="space-y-4">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h4 class="text-sm font-medium">Download Your Data</h4>
|
||||||
|
<p class="text-muted-foreground text-xs">
|
||||||
|
Export a complete copy of your account data including transactions, bets, and
|
||||||
|
profile information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={downloadUserData}
|
||||||
|
disabled={isDownloading}
|
||||||
|
class="ml-4"
|
||||||
|
>
|
||||||
|
<DownloadIcon class="h-4 w-4" />
|
||||||
|
{isDownloading ? 'Downloading...' : 'Download Data'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border-destructive/20 bg-destructive/5 flex items-center justify-between rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h4 class="text-destructive text-sm font-medium">Delete Account</h4>
|
||||||
|
<p class="text-muted-foreground text-xs">
|
||||||
|
Permanently delete your account. This will anonymize your data while preserving
|
||||||
|
transaction records for compliance.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => (deleteDialogOpen = true)}
|
||||||
|
class="ml-4"
|
||||||
|
>
|
||||||
|
<Trash2Icon class="h-4 w-4" />
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open={deleteDialogOpen}>
|
||||||
|
<Dialog.Content class="sm:max-w-md">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title class="text-destructive">Delete Account</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
This action cannot be undone. Your account will be permanently deleted and your data will be
|
||||||
|
anonymized.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="bg-destructive/10 rounded-lg p-4">
|
||||||
|
<h4 class="mb-2 text-sm font-medium">What happens when you delete your account:</h4>
|
||||||
|
<ul class="text-muted-foreground space-y-1 text-xs">
|
||||||
|
<li>• Your profile information will be permanently removed</li>
|
||||||
|
<li>• You will be logged out from all devices</li>
|
||||||
|
<li>• Your comments will be anonymized</li>
|
||||||
|
<li>• Transaction history will be preserved for compliance (anonymized)</li>
|
||||||
|
<li>• You will not be able to recover this account</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="delete-confirmation">Type "DELETE MY ACCOUNT" to confirm:</Label>
|
||||||
|
<Input
|
||||||
|
id="delete-confirmation"
|
||||||
|
bind:value={deleteConfirmationText}
|
||||||
|
placeholder="DELETE MY ACCOUNT"
|
||||||
|
class="font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="outline" onclick={() => (deleteDialogOpen = false)}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onclick={deleteAccount}
|
||||||
|
disabled={isDeleting || deleteConfirmationText !== 'DELETE MY ACCOUNT'}
|
||||||
|
>
|
||||||
|
{isDeleting ? 'Deleting...' : 'Delete Account'}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
|
||||||
Reference in a new issue