mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 22:05:09 +00:00
838 lines
28 KiB
TypeScript
838 lines
28 KiB
TypeScript
import { Elysia } from 'elysia'
|
|
import { eq, and, inArray, sql, desc, or } from 'drizzle-orm'
|
|
import { db } from '../db'
|
|
import { usersTable, userBonusesTable } from '../schemas/users'
|
|
import { projectsTable } from '../schemas/projects'
|
|
import { reviewsTable } from '../schemas/reviews'
|
|
import { shopItemsTable, shopOrdersTable, shopHeartsTable } from '../schemas/shop'
|
|
import { newsTable } from '../schemas/news'
|
|
import { activityTable } from '../schemas/activity'
|
|
import { getUserFromSession } from '../lib/auth'
|
|
import { calculateScrapsFromHours, getUserScrapsBalance } from '../lib/scraps'
|
|
|
|
const admin = new Elysia({ prefix: '/admin' })
|
|
|
|
async function requireReviewer(headers: Record<string, string>) {
|
|
const user = await getUserFromSession(headers)
|
|
if (!user) return null
|
|
if (user.role !== 'reviewer' && user.role !== 'admin') return null
|
|
return user
|
|
}
|
|
|
|
async function requireAdmin(headers: Record<string, string>) {
|
|
const user = await getUserFromSession(headers)
|
|
if (!user) return null
|
|
if (user.role !== 'admin') return null
|
|
return user
|
|
}
|
|
|
|
// Get all users (reviewers see limited info)
|
|
admin.get('/users', async ({ headers, query }) => {
|
|
try {
|
|
const user = await requireReviewer(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const page = parseInt(query.page as string) || 1
|
|
const limit = Math.min(parseInt(query.limit as string) || 20, 100)
|
|
const offset = (page - 1) * limit
|
|
const search = (query.search as string)?.trim() || ''
|
|
|
|
const searchCondition = search
|
|
? or(
|
|
sql`${usersTable.username} ILIKE ${'%' + search + '%'}`,
|
|
sql`${usersTable.email} ILIKE ${'%' + search + '%'}`,
|
|
sql`${usersTable.slackId} ILIKE ${'%' + search + '%'}`
|
|
)
|
|
: undefined
|
|
|
|
const [users, countResult] = await Promise.all([
|
|
db.select({
|
|
id: usersTable.id,
|
|
username: usersTable.username,
|
|
avatar: usersTable.avatar,
|
|
slackId: usersTable.slackId,
|
|
role: usersTable.role,
|
|
internalNotes: usersTable.internalNotes,
|
|
createdAt: usersTable.createdAt,
|
|
scrapsEarned: sql<number>`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id}), 0)`.as('scraps_earned'),
|
|
scrapsSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_spent')
|
|
}).from(usersTable).where(searchCondition).orderBy(desc(usersTable.createdAt)).limit(limit).offset(offset),
|
|
db.select({ count: sql<number>`count(*)` }).from(usersTable).where(searchCondition)
|
|
])
|
|
|
|
const total = Number(countResult[0]?.count || 0)
|
|
|
|
return {
|
|
data: users.map(u => ({
|
|
id: u.id,
|
|
username: u.username,
|
|
avatar: u.avatar,
|
|
slackId: u.slackId,
|
|
scraps: Number(u.scrapsEarned) - Number(u.scrapsSpent),
|
|
role: u.role,
|
|
internalNotes: u.internalNotes,
|
|
createdAt: u.createdAt
|
|
})),
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to fetch users' }
|
|
}
|
|
})
|
|
|
|
// Get single user details (for admin/users/[id] page)
|
|
admin.get('/users/:id', async ({ params, headers }) => {
|
|
try {
|
|
const user = await requireReviewer(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const targetUserId = parseInt(params.id)
|
|
|
|
const targetUser = await db
|
|
.select({
|
|
id: usersTable.id,
|
|
username: usersTable.username,
|
|
avatar: usersTable.avatar,
|
|
slackId: usersTable.slackId,
|
|
role: usersTable.role,
|
|
internalNotes: usersTable.internalNotes,
|
|
createdAt: usersTable.createdAt
|
|
})
|
|
.from(usersTable)
|
|
.where(eq(usersTable.id, targetUserId))
|
|
.limit(1)
|
|
|
|
if (!targetUser[0]) return { error: 'User not found' }
|
|
|
|
const projects = await db
|
|
.select()
|
|
.from(projectsTable)
|
|
.where(eq(projectsTable.userId, targetUserId))
|
|
.orderBy(desc(projectsTable.updatedAt))
|
|
|
|
const projectStats = {
|
|
total: projects.length,
|
|
shipped: projects.filter(p => p.status === 'shipped').length,
|
|
inProgress: projects.filter(p => p.status === 'in_progress').length,
|
|
waitingForReview: projects.filter(p => p.status === 'waiting_for_review').length,
|
|
rejected: projects.filter(p => p.status === 'permanently_rejected').length
|
|
}
|
|
|
|
const totalHours = projects.reduce((sum, p) => sum + (p.hoursOverride ?? p.hours ?? 0), 0)
|
|
|
|
const scrapsBalance = await getUserScrapsBalance(targetUserId) || 0;
|
|
|
|
return {
|
|
user: {
|
|
id: targetUser[0].id,
|
|
username: targetUser[0].username,
|
|
avatar: targetUser[0].avatar,
|
|
slackId: targetUser[0].slackId,
|
|
scraps: scrapsBalance.balance,
|
|
role: targetUser[0].role,
|
|
internalNotes: targetUser[0].internalNotes,
|
|
createdAt: targetUser[0].createdAt
|
|
},
|
|
projects,
|
|
stats: {
|
|
...projectStats,
|
|
totalHours
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to fetch user details' }
|
|
}
|
|
})
|
|
|
|
// Update user role (admin only)
|
|
admin.put('/users/:id/role', async ({ params, body, headers }) => {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const { role } = body as { role: string }
|
|
if (!['member', 'reviewer', 'admin', 'banned'].includes(role)) {
|
|
return { error: 'Invalid role' }
|
|
}
|
|
|
|
if (user.id === parseInt(params.id)) {
|
|
return { error: 'Cannot change your own role' }
|
|
}
|
|
|
|
try {
|
|
const updated = await db
|
|
.update(usersTable)
|
|
.set({ role, updatedAt: new Date() })
|
|
.where(eq(usersTable.id, parseInt(params.id)))
|
|
.returning()
|
|
|
|
return (updated[0] && { success: true }) || { error: "Not Found" }
|
|
} catch (err) {
|
|
console.error(err);
|
|
return { error: "Failed to update user role" };
|
|
}
|
|
})
|
|
|
|
// Update user internal notes
|
|
admin.put('/users/:id/notes', async ({ params, body, headers }) => {
|
|
const user = await requireReviewer(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const { internalNotes } = body as { internalNotes: string }
|
|
|
|
if (typeof internalNotes != "string" || internalNotes.length > 2500) {
|
|
return { error: "Note is too long or it's malformed!" };
|
|
}
|
|
|
|
try {
|
|
const updated = await db
|
|
.update(usersTable)
|
|
.set({ internalNotes, updatedAt: new Date() })
|
|
.where(eq(usersTable.id, parseInt(params.id)))
|
|
.returning()
|
|
|
|
return (updated[0] && { success: true }) || { error: "Not Found" }
|
|
} catch (err) {
|
|
console.error(err);
|
|
return { error: "Failed to update user internal notes" };
|
|
}
|
|
})
|
|
|
|
// Give bonus scraps to user (admin only)
|
|
admin.post('/users/:id/bonus', async ({ params, body, headers }) => {
|
|
try {
|
|
const admin = await requireAdmin(headers as Record<string, string>)
|
|
if (!admin) return { error: 'Unauthorized' }
|
|
|
|
const { amount, reason } = body as { amount: number; reason: string }
|
|
|
|
if (!amount || typeof amount !== 'number') {
|
|
return { error: 'Amount is required and must be a number' }
|
|
}
|
|
|
|
if (Number(amount))
|
|
|
|
if (!reason || typeof reason !== 'string' || reason.trim().length === 0) {
|
|
return { error: 'Reason is required' }
|
|
}
|
|
|
|
if (reason.length > 500) {
|
|
return { error: 'Reason is too long (max 500 characters)' }
|
|
}
|
|
|
|
const targetUserId = parseInt(params.id)
|
|
|
|
const targetUser = await db
|
|
.select({ id: usersTable.id })
|
|
.from(usersTable)
|
|
.where(eq(usersTable.id, targetUserId))
|
|
.limit(1)
|
|
|
|
if (!targetUser[0]) return { error: 'User not found' }
|
|
|
|
const bonus = await db
|
|
.insert(userBonusesTable)
|
|
.values({
|
|
userId: targetUserId,
|
|
amount,
|
|
reason: reason.trim(),
|
|
givenBy: admin.id
|
|
})
|
|
.returning({
|
|
id: userBonusesTable.id,
|
|
amount: userBonusesTable.amount,
|
|
reason: userBonusesTable.reason,
|
|
givenBy: userBonusesTable.givenBy,
|
|
createdAt: userBonusesTable.createdAt
|
|
})
|
|
|
|
return bonus[0]
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to create user bonus' }
|
|
}
|
|
})
|
|
|
|
// Get user bonuses (admin only)
|
|
admin.get('/users/:id/bonuses', async ({ params, headers }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const targetUserId = parseInt(params.id)
|
|
|
|
const bonuses = await db
|
|
.select({
|
|
id: userBonusesTable.id,
|
|
amount: userBonusesTable.amount,
|
|
reason: userBonusesTable.reason,
|
|
givenBy: userBonusesTable.givenBy,
|
|
givenByUsername: usersTable.username,
|
|
createdAt: userBonusesTable.createdAt
|
|
})
|
|
.from(userBonusesTable)
|
|
.leftJoin(usersTable, eq(userBonusesTable.givenBy, usersTable.id))
|
|
.where(eq(userBonusesTable.userId, targetUserId))
|
|
.orderBy(desc(userBonusesTable.createdAt))
|
|
|
|
return bonuses
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to fetch user bonuses' }
|
|
}
|
|
})
|
|
|
|
// Get projects waiting for review
|
|
admin.get('/reviews', async ({ headers, query }) => {
|
|
try {
|
|
const user = await requireReviewer(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const page = parseInt(query.page as string) || 1
|
|
const limit = Math.min(parseInt(query.limit as string) || 20, 100)
|
|
const offset = (page - 1) * limit
|
|
|
|
const [projects, countResult] = await Promise.all([
|
|
db.select().from(projectsTable)
|
|
.where(eq(projectsTable.status, 'waiting_for_review'))
|
|
.orderBy(desc(projectsTable.updatedAt))
|
|
.limit(limit)
|
|
.offset(offset),
|
|
db.select({ count: sql<number>`count(*)` }).from(projectsTable)
|
|
.where(eq(projectsTable.status, 'waiting_for_review'))
|
|
])
|
|
|
|
const total = Number(countResult[0]?.count || 0)
|
|
|
|
return {
|
|
data: projects,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to fetch reviews' }
|
|
}
|
|
})
|
|
|
|
// Get single project for review (with user info and previous reviews)
|
|
admin.get('/reviews/:id', async ({ params, headers }) => {
|
|
const user = await requireReviewer(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
try {
|
|
const project = await db
|
|
.select()
|
|
.from(projectsTable)
|
|
.where(eq(projectsTable.id, parseInt(params.id)))
|
|
.limit(1)
|
|
|
|
if (project.length <= 0) return { error: "Project not found!" };
|
|
|
|
const projectUser = await db
|
|
.select({
|
|
id: usersTable.id,
|
|
username: usersTable.username,
|
|
avatar: usersTable.avatar,
|
|
internalNotes: usersTable.internalNotes
|
|
})
|
|
.from(usersTable)
|
|
.where(eq(usersTable.id, project[0].userId))
|
|
.limit(1)
|
|
|
|
const reviews = await db
|
|
.select()
|
|
.from(reviewsTable)
|
|
.where(eq(reviewsTable.projectId, parseInt(params.id)))
|
|
|
|
const reviewerIds = reviews.map(r => r.reviewerId)
|
|
let reviewers: { id: number; username: string | null; avatar: string | null }[] = []
|
|
if (reviewerIds.length > 0) {
|
|
reviewers = await db
|
|
.select({ id: usersTable.id, username: usersTable.username, avatar: usersTable.avatar })
|
|
.from(usersTable)
|
|
.where(inArray(usersTable.id, reviewerIds))
|
|
}
|
|
|
|
return {
|
|
project: project[0],
|
|
user: projectUser[0] ? {
|
|
id: projectUser[0].id,
|
|
username: projectUser[0].username,
|
|
avatar: projectUser[0].avatar,
|
|
internalNotes: projectUser[0].internalNotes
|
|
} : null,
|
|
reviews: reviews.map(r => {
|
|
const reviewer = reviewers.find(rv => rv.id === r.reviewerId)
|
|
return {
|
|
...r,
|
|
reviewerName: reviewer?.username,
|
|
reviewerAvatar: reviewer?.avatar,
|
|
reviewerId: r.reviewerId
|
|
}
|
|
})
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
return { error: "Something went wrong while trying to get project" }
|
|
}
|
|
})
|
|
|
|
// Submit a review
|
|
admin.post('/reviews/:id', async ({ params, body, headers }) => {
|
|
try {
|
|
const user = await requireReviewer(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const { action, feedbackForAuthor, internalJustification, hoursOverride, userInternalNotes } = body as {
|
|
action: 'approved' | 'denied' | 'permanently_rejected'
|
|
feedbackForAuthor: string
|
|
internalJustification?: string
|
|
hoursOverride?: number
|
|
userInternalNotes?: string
|
|
}
|
|
|
|
if (!['approved', 'denied', 'permanently_rejected'].includes(action)) {
|
|
return { error: 'Invalid action' }
|
|
}
|
|
|
|
if (!feedbackForAuthor?.trim()) {
|
|
return { error: 'Feedback for author is required' }
|
|
}
|
|
|
|
const projectId = parseInt(params.id)
|
|
|
|
// Get project to find user
|
|
const project = await db
|
|
.select()
|
|
.from(projectsTable)
|
|
.where(eq(projectsTable.id, projectId))
|
|
.limit(1)
|
|
|
|
if (!project[0]) return { error: 'Project not found' }
|
|
|
|
// Reject if project is deleted or not waiting for review
|
|
if (project[0].deleted) {
|
|
return { error: 'Cannot review a deleted project' }
|
|
}
|
|
if (project[0].status !== 'waiting_for_review') {
|
|
return { error: 'Project is not marked for review' }
|
|
}
|
|
|
|
// Create review record
|
|
await db.insert(reviewsTable).values({
|
|
projectId,
|
|
reviewerId: user.id,
|
|
action,
|
|
feedbackForAuthor,
|
|
internalJustification
|
|
})
|
|
|
|
// Update project status
|
|
let newStatus = 'in_progress'
|
|
|
|
switch (action) {
|
|
case "approved":
|
|
newStatus = "shipped";
|
|
break;
|
|
case "denied":
|
|
newStatus = "in_progress";
|
|
break;
|
|
case "permanently_rejected":
|
|
newStatus = "permanently_rejected";
|
|
break;
|
|
default:
|
|
newStatus = "in_progress";
|
|
}
|
|
|
|
const updateData: Record<string, unknown> = {
|
|
status: newStatus,
|
|
updatedAt: new Date()
|
|
}
|
|
|
|
if (hoursOverride !== undefined) {
|
|
updateData.hoursOverride = hoursOverride
|
|
}
|
|
|
|
let scrapsAwarded = 0
|
|
if (action === 'approved') {
|
|
const hours = hoursOverride ?? project[0].hours ?? 0
|
|
scrapsAwarded = calculateScrapsFromHours(hours)
|
|
updateData.scrapsAwarded = scrapsAwarded
|
|
}
|
|
|
|
await db
|
|
.update(projectsTable)
|
|
.set(updateData)
|
|
.where(eq(projectsTable.id, projectId))
|
|
|
|
if (action === 'approved' && scrapsAwarded > 0) {
|
|
await db.insert(activityTable).values({
|
|
userId: project[0].userId,
|
|
projectId,
|
|
action: `earned ${scrapsAwarded} scraps`
|
|
})
|
|
}
|
|
|
|
// Update user internal notes if provided
|
|
if (userInternalNotes !== undefined) {
|
|
if (userInternalNotes.length <= 2500) {
|
|
await db
|
|
.update(usersTable)
|
|
.set({ internalNotes: userInternalNotes, updatedAt: new Date() })
|
|
.where(eq(usersTable.id, project[0].userId))
|
|
}
|
|
}
|
|
|
|
return { success: true }
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to submit review' }
|
|
}
|
|
})
|
|
|
|
// Shop admin endpoints (admin only)
|
|
admin.get('/shop/items', async ({ headers }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const items = await db
|
|
.select()
|
|
.from(shopItemsTable)
|
|
.orderBy(desc(shopItemsTable.createdAt))
|
|
|
|
return items
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to fetch shop items' }
|
|
}
|
|
})
|
|
|
|
admin.post('/shop/items', async ({ headers, body }) => {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const { name, image, description, price, category, count, baseProbability, baseUpgradeCost, costMultiplier, boostAmount } = body as {
|
|
name: string
|
|
image: string
|
|
description: string
|
|
price: number
|
|
category: string
|
|
count: number
|
|
baseProbability?: number
|
|
baseUpgradeCost?: number
|
|
costMultiplier?: number
|
|
boostAmount?: number
|
|
}
|
|
|
|
if (!name?.trim() || !image?.trim() || !description?.trim() || !category?.trim()) {
|
|
return { error: 'All fields are required' }
|
|
}
|
|
|
|
if (typeof price !== 'number' || price < 0) {
|
|
return { error: 'Invalid price' }
|
|
}
|
|
|
|
if (baseProbability !== undefined && (typeof baseProbability !== 'number' || baseProbability < 0 || baseProbability > 100)) {
|
|
return { error: 'baseProbability must be between 0 and 100' }
|
|
}
|
|
|
|
try {
|
|
await db
|
|
.insert(shopItemsTable)
|
|
.values({
|
|
name: name.trim(),
|
|
image: image.trim(),
|
|
description: description.trim(),
|
|
price,
|
|
category: category.trim(),
|
|
count: count || 0,
|
|
baseProbability: baseProbability ?? 50,
|
|
baseUpgradeCost: baseUpgradeCost ?? 10,
|
|
costMultiplier: costMultiplier ?? 115,
|
|
boostAmount: boostAmount ?? 1
|
|
});
|
|
|
|
return { success: true };
|
|
} catch (err) {
|
|
console.error(err);
|
|
throw "Something went wrong while trying to create an shop item"
|
|
}
|
|
})
|
|
|
|
admin.put('/shop/items/:id', async ({ params, headers, body }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const { name, image, description, price, category, count, baseProbability, baseUpgradeCost, costMultiplier, boostAmount } = body as {
|
|
name?: string
|
|
image?: string
|
|
description?: string
|
|
price?: number
|
|
category?: string
|
|
count?: number
|
|
baseProbability?: number
|
|
baseUpgradeCost?: number
|
|
costMultiplier?: number
|
|
boostAmount?: number
|
|
}
|
|
|
|
if (baseProbability !== undefined && (typeof baseProbability !== 'number' || baseProbability < 0 || baseProbability > 100)) {
|
|
return { error: 'baseProbability must be between 0 and 100' }
|
|
}
|
|
|
|
const updateData: Record<string, unknown> = { updatedAt: new Date() }
|
|
|
|
if (name !== undefined) updateData.name = name.trim()
|
|
if (image !== undefined) updateData.image = image.trim()
|
|
if (description !== undefined) updateData.description = description.trim()
|
|
if (price !== undefined) updateData.price = price
|
|
if (category !== undefined) updateData.category = category.trim()
|
|
if (count !== undefined) updateData.count = count
|
|
if (baseProbability !== undefined) updateData.baseProbability = baseProbability
|
|
if (baseUpgradeCost !== undefined) updateData.baseUpgradeCost = baseUpgradeCost
|
|
if (costMultiplier !== undefined) updateData.costMultiplier = costMultiplier
|
|
if (boostAmount !== undefined) updateData.boostAmount = boostAmount
|
|
|
|
const updated = await db
|
|
.update(shopItemsTable)
|
|
.set(updateData)
|
|
.where(eq(shopItemsTable.id, parseInt(params.id)))
|
|
.returning()
|
|
|
|
if (!updated[0]) return { error: 'Not found' }
|
|
return { success: true }
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to update shop item' }
|
|
}
|
|
})
|
|
|
|
admin.delete('/shop/items/:id', async ({ params, headers }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const itemId = parseInt(params.id)
|
|
|
|
await db
|
|
.delete(shopHeartsTable)
|
|
.where(eq(shopHeartsTable.shopItemId, itemId))
|
|
|
|
await db
|
|
.delete(shopItemsTable)
|
|
.where(eq(shopItemsTable.id, itemId))
|
|
|
|
return { success: true }
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to delete shop item' }
|
|
}
|
|
})
|
|
|
|
// News admin endpoints (admin only)
|
|
admin.get('/news', async ({ headers }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const items = await db
|
|
.select()
|
|
.from(newsTable)
|
|
.orderBy(desc(newsTable.createdAt))
|
|
|
|
return items
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to fetch news' }
|
|
}
|
|
})
|
|
|
|
admin.post('/news', async ({ headers, body }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const { title, content, active } = body as {
|
|
title: string
|
|
content: string
|
|
active?: boolean
|
|
}
|
|
|
|
if (!title?.trim() || !content?.trim()) {
|
|
return { error: 'Title and content are required' }
|
|
}
|
|
|
|
const inserted = await db
|
|
.insert(newsTable)
|
|
.values({
|
|
title: title.trim(),
|
|
content: content.trim(),
|
|
active: active ?? true
|
|
})
|
|
.returning({
|
|
id: newsTable.id,
|
|
title: newsTable.title,
|
|
content: newsTable.content,
|
|
active: newsTable.active,
|
|
createdAt: newsTable.createdAt,
|
|
updatedAt: newsTable.updatedAt
|
|
})
|
|
|
|
return inserted[0]
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to create news' }
|
|
}
|
|
})
|
|
|
|
admin.put('/news/:id', async ({ params, headers, body }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const { title, content, active } = body as {
|
|
title?: string
|
|
content?: string
|
|
active?: boolean
|
|
}
|
|
|
|
const updateData: Record<string, unknown> = { updatedAt: new Date() }
|
|
|
|
if (title !== undefined) updateData.title = title.trim()
|
|
if (content !== undefined) updateData.content = content.trim()
|
|
if (active !== undefined) updateData.active = active
|
|
|
|
const updated = await db
|
|
.update(newsTable)
|
|
.set(updateData)
|
|
.where(eq(newsTable.id, parseInt(params.id)))
|
|
.returning({
|
|
id: newsTable.id,
|
|
title: newsTable.title,
|
|
content: newsTable.content,
|
|
active: newsTable.active,
|
|
createdAt: newsTable.createdAt,
|
|
updatedAt: newsTable.updatedAt
|
|
})
|
|
|
|
if (!updated[0]) return { error: 'Not found' }
|
|
return updated[0]
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to update news' }
|
|
}
|
|
})
|
|
|
|
admin.delete('/news/:id', async ({ params, headers }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
await db
|
|
.delete(newsTable)
|
|
.where(eq(newsTable.id, parseInt(params.id)))
|
|
|
|
return { success: true }
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to delete news' }
|
|
}
|
|
})
|
|
|
|
admin.get('/orders', async ({ headers, query }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const status = query.status as string | undefined
|
|
|
|
let ordersQuery = db
|
|
.select({
|
|
id: shopOrdersTable.id,
|
|
quantity: shopOrdersTable.quantity,
|
|
pricePerItem: shopOrdersTable.pricePerItem,
|
|
totalPrice: shopOrdersTable.totalPrice,
|
|
status: shopOrdersTable.status,
|
|
orderType: shopOrdersTable.orderType,
|
|
notes: shopOrdersTable.notes,
|
|
isFulfilled: shopOrdersTable.isFulfilled,
|
|
shippingAddress: shopOrdersTable.shippingAddress,
|
|
createdAt: shopOrdersTable.createdAt,
|
|
itemId: shopItemsTable.id,
|
|
itemName: shopItemsTable.name,
|
|
itemImage: shopItemsTable.image,
|
|
userId: usersTable.id,
|
|
username: usersTable.username
|
|
})
|
|
.from(shopOrdersTable)
|
|
.innerJoin(shopItemsTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id))
|
|
.innerJoin(usersTable, eq(shopOrdersTable.userId, usersTable.id))
|
|
.orderBy(desc(shopOrdersTable.createdAt))
|
|
|
|
if (status) {
|
|
ordersQuery = ordersQuery.where(eq(shopOrdersTable.status, status)) as typeof ordersQuery
|
|
}
|
|
|
|
return await ordersQuery
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to fetch orders' }
|
|
}
|
|
})
|
|
|
|
admin.patch('/orders/:id', async ({ params, body, headers }) => {
|
|
try {
|
|
const user = await requireAdmin(headers as Record<string, string>)
|
|
if (!user) return { error: 'Unauthorized' }
|
|
|
|
const { status, notes, isFulfilled } = body as { status?: string; notes?: string; isFulfilled?: boolean }
|
|
|
|
const validStatuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled']
|
|
if (status && !validStatuses.includes(status)) {
|
|
return { error: 'Invalid status' }
|
|
}
|
|
|
|
const updateData: Record<string, unknown> = { updatedAt: new Date() }
|
|
if (status) updateData.status = status
|
|
if (notes !== undefined) updateData.notes = notes
|
|
if (isFulfilled !== undefined) updateData.isFulfilled = isFulfilled
|
|
|
|
const updated = await db
|
|
.update(shopOrdersTable)
|
|
.set(updateData)
|
|
.where(eq(shopOrdersTable.id, parseInt(params.id)))
|
|
.returning({
|
|
id: shopOrdersTable.id,
|
|
quantity: shopOrdersTable.quantity,
|
|
pricePerItem: shopOrdersTable.pricePerItem,
|
|
totalPrice: shopOrdersTable.totalPrice,
|
|
status: shopOrdersTable.status,
|
|
orderType: shopOrdersTable.orderType,
|
|
notes: shopOrdersTable.notes,
|
|
isFulfilled: shopOrdersTable.isFulfilled,
|
|
shippingAddress: shopOrdersTable.shippingAddress,
|
|
createdAt: shopOrdersTable.createdAt
|
|
})
|
|
|
|
if (!updated[0]) return { error: 'Not found' }
|
|
return updated[0]
|
|
} catch (err) {
|
|
console.error(err)
|
|
return { error: 'Failed to update order' }
|
|
}
|
|
})
|
|
|
|
export default admin
|