mirror of
https://github.com/System-End/scraps.git
synced 2026-04-20 00:25:18 +00:00
Merge branch 'main' of https://github.com/hackclub/scraps
This commit is contained in:
commit
13eed0aaac
3 changed files with 65 additions and 13 deletions
|
|
@ -1671,8 +1671,8 @@ admin.post('/fix-negative-balances', async ({ headers, status }) => {
|
|||
}
|
||||
})
|
||||
|
||||
// CSV export of shipped projects for YSWS
|
||||
admin.get('/export/shipped-csv', async ({ headers, status }) => {
|
||||
// CSV export of projects under review for YSWS
|
||||
admin.get('/export/review-csv', async ({ headers, status }) => {
|
||||
try {
|
||||
const user = await requireReviewer(headers as Record<string, string>)
|
||||
if (!user) {
|
||||
|
|
@ -1690,7 +1690,7 @@ admin.get('/export/shipped-csv', async ({ headers, status }) => {
|
|||
.from(projectsTable)
|
||||
.innerJoin(usersTable, eq(projectsTable.userId, usersTable.id))
|
||||
.where(and(
|
||||
eq(projectsTable.status, 'shipped'),
|
||||
or(eq(projectsTable.status, 'waiting_for_review'), eq(projectsTable.status, 'pending_admin_approval')),
|
||||
or(eq(projectsTable.deleted, 0), isNull(projectsTable.deleted))
|
||||
))
|
||||
.orderBy(desc(projectsTable.updatedAt))
|
||||
|
|
@ -1717,16 +1717,16 @@ admin.get('/export/shipped-csv', async ({ headers, status }) => {
|
|||
return new Response(rows.join('\n'), {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="scraps-shipped-projects.csv"'
|
||||
'Content-Disposition': 'attachment; filename="scraps-review-projects.csv"'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return status(500, { error: 'Failed to export CSV' })
|
||||
return status(500, { error: 'Failed to export review CSV' })
|
||||
}
|
||||
})
|
||||
|
||||
admin.get('/export/ysws-json', async ({ headers, status }) => {
|
||||
admin.get('/export/review-json', async ({ headers, status }) => {
|
||||
try {
|
||||
const user = await requireReviewer(headers as Record<string, string>)
|
||||
if (!user) {
|
||||
|
|
@ -1744,7 +1744,7 @@ admin.get('/export/ysws-json', async ({ headers, status }) => {
|
|||
.from(projectsTable)
|
||||
.innerJoin(usersTable, eq(projectsTable.userId, usersTable.id))
|
||||
.where(and(
|
||||
eq(projectsTable.status, 'shipped'),
|
||||
or(eq(projectsTable.status, 'waiting_for_review'), eq(projectsTable.status, 'pending_admin_approval')),
|
||||
or(eq(projectsTable.deleted, 0), isNull(projectsTable.deleted))
|
||||
))
|
||||
.orderBy(desc(projectsTable.updatedAt))
|
||||
|
|
@ -1764,7 +1764,7 @@ admin.get('/export/ysws-json', async ({ headers, status }) => {
|
|||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return status(500, { error: 'Failed to export YSWS JSON' })
|
||||
return status(500, { error: 'Failed to export review JSON' })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -612,15 +612,17 @@ shop.post('/items/:id/upgrade-probability', async ({ params, headers }) => {
|
|||
|
||||
const item = items[0]
|
||||
|
||||
if (item.count <= 0) {
|
||||
return { error: 'Item is out of stock' }
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// Lock the user row to serialize spend operations and prevent race conditions
|
||||
await tx.execute(sql`SELECT 1 FROM users WHERE id = ${user.id} FOR UPDATE`)
|
||||
|
||||
// Lock the item row and check stock atomically
|
||||
const stockCheck = await tx.execute(sql`SELECT count FROM shop_items WHERE id = ${itemId} FOR UPDATE`)
|
||||
if (!stockCheck.rows[0] || (stockCheck.rows[0] as { count: number }).count <= 0) {
|
||||
throw { type: 'out_of_stock' }
|
||||
}
|
||||
|
||||
const boostResult = await tx
|
||||
.select({
|
||||
boostPercent: sql<number>`COALESCE(SUM(${refineryOrdersTable.boostAmount}), 0)`
|
||||
|
|
@ -697,6 +699,9 @@ shop.post('/items/:id/upgrade-probability', async ({ params, headers }) => {
|
|||
return result
|
||||
} catch (e) {
|
||||
const err = e as { type?: string; balance?: number; cost?: number }
|
||||
if (err.type === 'out_of_stock') {
|
||||
return { error: 'Item is out of stock' }
|
||||
}
|
||||
if (err.type === 'max_probability') {
|
||||
return { error: 'Already at maximum probability' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Users, FolderKanban, Clock, Scale, Hourglass, ShieldAlert, Coins, XCircle } from '@lucide/svelte';
|
||||
import { Users, FolderKanban, Clock, Scale, Hourglass, ShieldAlert, Coins, XCircle, Download } from '@lucide/svelte';
|
||||
import { getUser } from '$lib/auth-client';
|
||||
import { API_URL } from '$lib/config';
|
||||
import { t } from '$lib/i18n';
|
||||
|
|
@ -146,6 +146,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function downloadExport(endpoint: string, filename: string) {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/admin/export/${endpoint}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error('Export failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const user = await getUser();
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'reviewer')) {
|
||||
|
|
@ -281,6 +299,35 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6 pb-12 md:px-12">
|
||||
<h2 class="mb-4 text-2xl font-bold">exports</h2>
|
||||
<div class="rounded-2xl border-4 border-black p-6">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="flex items-center gap-2 text-lg font-bold">
|
||||
<Download size={20} />
|
||||
YSWS review export
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500">download projects under review for YSWS fraud checking</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
onclick={() => downloadExport('review-csv', 'scraps-review-projects.csv')}
|
||||
class="cursor-pointer rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
|
||||
>
|
||||
CSV
|
||||
</button>
|
||||
<button
|
||||
onclick={() => downloadExport('review-json', 'scraps-review.json')}
|
||||
class="cursor-pointer rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
|
||||
>
|
||||
JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isAdmin}
|
||||
<div class="mx-auto max-w-4xl px-6 pb-24 md:px-12">
|
||||
<h2 class="mb-4 text-2xl font-bold">admin actions</h2>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue