From 010722a1cd42c11b69c159858a99d986b4e44515 Mon Sep 17 00:00:00 2001 From: TarekHassan1 Date: Sun, 16 Aug 2026 23:48:39 +0300 Subject: [PATCH 1/2] feat: build admin API (#61) --- apps/api/drizzle.config.ts | 12 +- .../api/src/controllers/webhook.Controller.ts | 21 +- apps/api/src/db/schema.ts | 9 +- apps/api/src/index.ts | 10 +- apps/api/src/middleware/adminAuth.ts | 29 +++ apps/api/src/middleware/auth.ts | 21 +- apps/api/src/routes/admin.route.ts | 179 ++++++++++++++++++ apps/api/src/routes/user.route.ts | 18 +- apps/api/src/types.ts | 6 +- apps/api/src/utils/user.ts | 36 ++++ apps/api/wrangler.toml | 4 + 11 files changed, 321 insertions(+), 24 deletions(-) create mode 100644 apps/api/src/middleware/adminAuth.ts create mode 100644 apps/api/src/routes/admin.route.ts create mode 100644 apps/api/src/utils/user.ts diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts index 36f97ba..44e7728 100644 --- a/apps/api/drizzle.config.ts +++ b/apps/api/drizzle.config.ts @@ -1,11 +1,7 @@ -import type { Config } from 'drizzle-kit'; +import { defineConfig } from 'drizzle-kit' -export default { +export default defineConfig({ schema: './src/db/schema.ts', out: './migrations', - driver: 'd1', - dbCredentials: { - wranglerConfigPath: 'wrangler.toml', - dbName: 'voting-system-db', - }, -} satisfies Config; + dialect: 'sqlite', +}) \ No newline at end of file diff --git a/apps/api/src/controllers/webhook.Controller.ts b/apps/api/src/controllers/webhook.Controller.ts index a721c66..7c95975 100644 --- a/apps/api/src/controllers/webhook.Controller.ts +++ b/apps/api/src/controllers/webhook.Controller.ts @@ -3,6 +3,7 @@ import { Webhook } from 'svix' import type { AppEnv } from '../types' import { getDb } from '../db/client' import { users } from '../db/schema' +import { ensureUserExists } from '../utils/user' export const clerkWebhook = async (c: Context) => { const webhookSecret = c.env.CLERK_WEBHOOK_SECRET @@ -29,15 +30,21 @@ export const clerkWebhook = async (c: Context) => { return c.json({ error: 'Invalid webhook signature' }, 401) } - if (event.type === 'user.created') { + if (event.type === 'user.created' || event.type === 'user.updated') { const { id, email_addresses, first_name, last_name } = event.data - const email = email_addresses?.[0]?.email_address ?? null - const name = `${first_name ?? ''} ${last_name ?? ''}`.trim() || null - - const db = getDb(c.env.DB) - await db.insert(users).values({ id, email, name }).onConflictDoNothing() + const email = email_addresses?.[0]?.email_address ?? '' + const name = `${first_name ?? ''} ${last_name ?? ''}`.trim() || '' + + // Replaces raw insert — now checks ADMIN_EMAILS for first-admin bootstrap + await ensureUserExists( + c.env.DB, + id, + email, + name, + c.env.ADMIN_EMAILS + ) } return c.json({ success: true }) -} +} \ No newline at end of file diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 68bfe06..5b593df 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -6,6 +6,7 @@ export const users = sqliteTable('users', { name: text('name'), completed: integer('completed', { mode: 'boolean' }).default(false), createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + is_admin:integer('is_admin', { mode: 'boolean' }).default(0), }); export const items = sqliteTable('items', { @@ -21,7 +22,12 @@ export const items = sqliteTable('items', { qualifiedAvgRating: real('qualified_avg_rating').default(0), createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), }); - + export const settings = sqliteTable('settings', { + id: integer('id').primaryKey({ autoIncrement: true }), + key: text('key').unique().notNull(), + value: text('value'), + updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), +}); export const ratings = sqliteTable('ratings', { id: integer('id').primaryKey({ autoIncrement: true }), userId: text('user_id').notNull().references(() => users.id), @@ -29,6 +35,7 @@ export const ratings = sqliteTable('ratings', { rating: integer('rating').notNull(), createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), }, + (table) => ({ unq: uniqueIndex('unique_vote').on(table.userId, table.itemId), }) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index bebd0d3..561e96e 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono' -import { clerkMiddleware } from '@hono/clerk-auth' +import { clerkMiddleware } from '@clerk/hono' import type { AppEnv } from './types' import voteRoutes from './routes/vote.route' @@ -10,7 +10,9 @@ import resultsRoutes from './routes/results.route' import userRoutes from './routes/user.route' import webhookRoutes from './routes/webhook.route' import { cors } from 'hono/cors' +import adminRoutes from './routes/admin.route' +// Mount under /api/v1/admin const app = new Hono() const port = 8000; @@ -18,6 +20,7 @@ const port = 8000; //using cors and clerk middleware app.use('*', cors()) app.use('*', clerkMiddleware()) + app.all('/__clerk/*', async (c) => { const url = new URL(c.req.url); @@ -72,8 +75,6 @@ app.all('/__clerk/*', async (c) => { // Webhook route must come before clerkMiddleware app.route('/webhooks', webhookRoutes) -app.use('*', clerkMiddleware()) - //protected api group const api = app.basePath('/api/v1') @@ -83,7 +84,8 @@ api.route('/progress', progressRoutes) api.route('/items', itemsRoutes) api.route('/results', resultsRoutes) api.route('/user', userRoutes) - +api.route('/admin', adminRoutes) +app.get('/', (c) => c.json({ ok: true })) export default { port, fetch: app.fetch, diff --git a/apps/api/src/middleware/adminAuth.ts b/apps/api/src/middleware/adminAuth.ts new file mode 100644 index 0000000..9298a2e --- /dev/null +++ b/apps/api/src/middleware/adminAuth.ts @@ -0,0 +1,29 @@ +import { eq } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/d1' +import { getAuth } from '@clerk/hono' +import { createMiddleware } from 'hono/factory' +import type { AppEnv } from '../types' +import { users } from '../db/schema' + +export const requireAdmin = createMiddleware(async (c, next) => { + const auth = getAuth(c) + + if (!auth?.userId) { + return c.json({ success: false, error: 'Unauthorized' }, 401) + } + + const db = drizzle(c.env.DB) + const user = await db + .select() + .from(users) + .where(eq(users.id, auth.userId)) + .get() + + if (!user || !user.is_admin) { + return c.json({ success: false, error: 'Forbidden' }, 403) + } + + c.set('userId', auth.userId) + c.set('isAdmin', true) + await next() +}) \ No newline at end of file diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts index c555e61..3620db6 100644 --- a/apps/api/src/middleware/auth.ts +++ b/apps/api/src/middleware/auth.ts @@ -1,6 +1,8 @@ -import { getAuth } from '@hono/clerk-auth' +import { getAuth } from '@clerk/hono' import { createMiddleware } from 'hono/factory' +import { createClerkClient } from '@clerk/backend' // FIX: was clerkClient import type { AppEnv } from '../types' +import { ensureUserExists } from '../utils/user' export const requireAuth = createMiddleware(async (c, next) => { const auth = getAuth(c) @@ -9,6 +11,21 @@ export const requireAuth = createMiddleware(async (c, next) => { return c.json({ success: false, error: 'Unauthorized' }, 401) } + // FIX: create the client instance + const clerk = createClerkClient({ secretKey: c.env.CLERK_SECRET_KEY }) + const clerkUser = await clerk.users.getUser(auth.userId) + + const email = clerkUser.emailAddresses[0]?.emailAddress + const name = `${clerkUser.firstName || ''} ${clerkUser.lastName || ''}`.trim() + + await ensureUserExists( + c.env.DB, + auth.userId, + email || '', + name, + c.env.ADMIN_EMAILS + ) + c.set('userId', auth.userId) await next() -}) +}) \ No newline at end of file diff --git a/apps/api/src/routes/admin.route.ts b/apps/api/src/routes/admin.route.ts new file mode 100644 index 0000000..aa100eb --- /dev/null +++ b/apps/api/src/routes/admin.route.ts @@ -0,0 +1,179 @@ +import { Hono } from 'hono' +import { eq, count } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/d1' +import { requireAdmin } from '../middleware/adminAuth' +import { items, ratings, users, settings } from '../db/schema' +import type { AppEnv } from '../types' +import { requireAuth } from './middleware/auth' + +const app = new Hono() + +// Everything behind requireAdmin +app.use('*', requireAdmin) + +// GET /items — list all with aggregates +app.get('/items', async (c) => { + const db = drizzle(c.env.DB) + const allItems = await db.select().from(items).all() + return c.json({ success: true, data: allItems }) +}) + +// POST /items — validate name, validate unique qrSlug, return 409 on duplicate +app.post('/items', async (c) => { + const db = drizzle(c.env.DB) + const body = await c.req.json() + + if (!body.name?.trim()) { + return c.json({ success: false, error: 'Name is required' }, 400) + } + if (!body.qrSlug?.trim()) { + return c.json({ success: false, error: 'qrSlug is required' }, 400) + } + + const existing = await db.select().from(items).where(eq(items.qrSlug, body.qrSlug)).get() + if (existing) { + return c.json({ success: false, error: 'Duplicate qrSlug' }, 409) + } + + const result = await db.insert(items).values({ + name: body.name, + description: body.description, + logo: body.logo, + qrSlug: body.qrSlug, + }).returning().get() + + return c.json({ success: true, data: result }, 201) +}) + +// PUT /items/:id +app.put('/items/:id', async (c) => { + const db = drizzle(c.env.DB) + const id = Number(c.req.param('id')) + if (isNaN(id)) return c.json({ success: false, error: 'Invalid ID' }, 400) + + const body = await c.req.json() + const updateData: Record = {} + + if (body.name !== undefined) updateData.name = body.name + if (body.description !== undefined) updateData.description = body.description + if (body.logo !== undefined) updateData.logo = body.logo + if (body.qrSlug !== undefined) { + const existing = await db.select().from(items).where(eq(items.qrSlug, body.qrSlug)).get() + if (existing && existing.id !== id) { + return c.json({ success: false, error: 'Duplicate qrSlug' }, 409) + } + updateData.qrSlug = body.qrSlug + } + + const result = await db.update(items).set(updateData).where(eq(items.id, id)).returning().get() + if (!result) return c.json({ success: false, error: 'Not found' }, 404) + + return c.json({ success: true, data: result }) +}) + +// DELETE /items/:id — ratings cascade, then delete item +app.delete('/items/:id', async (c) => { + const db = drizzle(c.env.DB) + const id = Number(c.req.param('id')) + if (isNaN(id)) return c.json({ success: false, error: 'Invalid ID' }, 400) + + // Delete ratings first (cascade behavior) + await db.delete(ratings).where(eq(ratings.itemId, id)) + + const result = await db.delete(items).where(eq(items.id, id)).returning().get() + if (!result) return c.json({ success: false, error: 'Not found' }, 404) + + return c.json({ success: true }) +}) + +// POST /items/bulk — accept JSON array for seeding +app.post('/items/bulk', async (c) => { + const db = drizzle(c.env.DB) + const body = await c.req.json() + + if (!Array.isArray(body)) { + return c.json({ success: false, error: 'Expected array' }, 400) + } + + for (const item of body) { + if (!item.name?.trim() || !item.qrSlug?.trim()) { + return c.json({ success: false, error: 'Each item needs name and qrSlug' }, 400) + } + const existing = await db.select().from(items).where(eq(items.qrSlug, item.qrSlug)).get() + if (existing) { + return c.json({ success: false, error: `Duplicate qrSlug: ${item.qrSlug}` }, 409) + } + } + + await db.insert(items).values(body) + return c.json({ success: true, count: body.length }, 201) +}) + +// GET /settings +app.get('/settings', async (c) => { + const db = drizzle(c.env.DB) + const allSettings = await db.select().from(settings).all() + const settingsMap = Object.fromEntries(allSettings.map(s => [s.key, s.value])) + return c.json({ success: true, data: settingsMap }) +}) + +// PUT /settings +app.put('/settings', async (c) => { + const db = drizzle(c.env.DB) + const body = await c.req.json() + + if (typeof body !== 'object' || body === null) { + return c.json({ success: false, error: 'Invalid body' }, 400) + } + + for (const [key, value] of Object.entries(body)) { + await db.insert(settings).values({ key, value: String(value) }) + .onConflictDoUpdate({ target: settings.key, set: { value: String(value) } }) + } + + return c.json({ success: true }) +}) + +// GET /users — with vote counts and completion status +app.get('/users', async (c) => { + const db = drizzle(c.env.DB) + + const allUsers = await db.select().from(users).all() + const voteCounts = await db + .select({ userId: ratings.userId, count: count() }) + .from(ratings) + .groupBy(ratings.userId) + .all() + + const countMap = new Map(voteCounts.map(v => [v.userId, v.count])) + + const result = allUsers.map(u => ({ + id: u.id, + email: u.email, + name: u.name, + completed: u.completed, + isAdmin: u.is_admin, + voteCount: countMap.get(u.id) || 0, + })) + + return c.json({ success: true, data: result }) +}) + +// PUT /users/:id/admin — promote or demote +app.put('/users/:id/admin', async (c) => { + const db = drizzle(c.env.DB) + const id = c.req.param('id') + const body = await c.req.json() + + const result = await db.update(users) + .set({ is_admin: body.isAdmin === true }) + .where(eq(users.id, id)) + .returning() + .get() + + if (!result) return c.json({ success: false, error: 'Not found' }, 404) + + return c.json({ success: true, data: result }) +}) + +export default app \ No newline at end of file diff --git a/apps/api/src/routes/user.route.ts b/apps/api/src/routes/user.route.ts index 2367856..bbba0b0 100644 --- a/apps/api/src/routes/user.route.ts +++ b/apps/api/src/routes/user.route.ts @@ -1,10 +1,26 @@ import { Hono } from 'hono' +import { drizzle } from 'drizzle-orm/d1' +import { eq } from 'drizzle-orm' import type { AppEnv } from '../types' import { requireAuth } from '../middleware/auth' +import { users } from '../db/schema' import { syncUser } from '../controllers/user.Controller' const user = new Hono() +// GET /api/v1/user/me — returns { isAdmin } for frontend route guard +user.get('/me', requireAuth, async (c) => { + const db = drizzle(c.env.DB) + const userId = c.get('userId') + + const row = await db.select().from(users).where(eq(users.id, userId)).get() + + return c.json({ + success: true, + data: { isAdmin: row?.is_admin ?? false } + }) +}) + user.post('/sync', requireAuth, syncUser) -export default user +export default user \ No newline at end of file diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index cdc1a54..f452f60 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -1,15 +1,19 @@ +import type { D1Database } from '@cloudflare/workers-types' + export type Bindings = { DB: D1Database CLERK_PUBLISHABLE_KEY: string CLERK_SECRET_KEY: string CLERK_WEBHOOK_SECRET: string + ADMIN_EMAILS?: string } export type Variables = { userId: string + isAdmin?: boolean } export type AppEnv = { Bindings: Bindings Variables: Variables -} +} \ No newline at end of file diff --git a/apps/api/src/utils/user.ts b/apps/api/src/utils/user.ts new file mode 100644 index 0000000..8226fb6 --- /dev/null +++ b/apps/api/src/utils/user.ts @@ -0,0 +1,36 @@ +import { drizzle } from 'drizzle-orm/d1' +import { eq } from 'drizzle-orm' +import type { D1Database } from '@cloudflare/workers-types' +import { users } from '../db/schema' + +export async function ensureUserExists( + dbD1: D1Database, + userId: string, + email: string, + name: string, + adminEmails?: string +) { + const db = drizzle(dbD1) + + const isAdmin = adminEmails + ? adminEmails.split(',').map(e => e.trim().toLowerCase()).includes(email.toLowerCase()) + : false + + await db + .insert(users) + .values({ + id: userId, + email, + name, + is_admin: isAdmin, + }) + .onConflictDoUpdate({ + target: users.id, + set: { + email, + name, + // Only promote to admin via ADMIN_EMAILS; never demote existing admins + is_admin: isAdmin ? true : undefined, + }, + }) +} \ No newline at end of file diff --git a/apps/api/wrangler.toml b/apps/api/wrangler.toml index 91300c4..59dd4e9 100644 --- a/apps/api/wrangler.toml +++ b/apps/api/wrangler.toml @@ -5,7 +5,11 @@ compatibility_flags = ["nodejs_compat"] [dev] port = 8000 + [[d1_databases]] binding = "DB" database_name = "vote-system" database_id = "832b59da-1bf5-45cf-b5d1-b4935b625394" + +[vars] +ADMIN_EMAILS = "you@example.com" \ No newline at end of file From 2a3a8a137469c115a856668ed7b7a7651f8ebf61 Mon Sep 17 00:00:00 2001 From: TarekHassan1 Date: Mon, 17 Aug 2026 06:47:19 +0300 Subject: [PATCH 2/2] feat: build admin API + security fixes --- README.md | 19 +- .../src/controllers/progress.Controller.ts | 14 +- apps/api/src/middleware/auth.ts | 2 +- apps/api/src/routes/health.route.ts | 40 +++- apps/api/src/routes/progress.route.ts | 5 +- apps/api/src/routes/results.route.ts | 49 +++- apps/web/README.md | 22 +- package-lock.json | 132 ++++++++++ package.json | 5 +- pnpm-lock.yaml | 226 +++++++++++------- 10 files changed, 385 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 779b84a..5f7a712 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,14 @@ voting-system/ ## 🚀 Quick Start (Local Development) 1. **Clone the repository:** + ```bash git clone https://github.com/Kaushik4141/voting-system.git cd voting-system ``` 2. **Install dependencies:** + ```bash pnpm install ``` @@ -44,7 +46,6 @@ voting-system/ - Frontend (`apps/web`): Copy `.env.example` to `.env` - Backend (`apps/api`): Copy `.dev.vars.example` to `.dev.vars` - 4. **Start development servers:** ```bash pnpm dev @@ -54,11 +55,25 @@ voting-system/ ## 🌐 Self-Deployment & Infrastructure Guide -Planning to deploy your own instance of the Voting System? +Planning to deploy your own instance of the Voting System? Check out our comprehensive **[Self-Deployment & Infrastructure Guide](./DEPLOYMENT.md)** for detailed instructions on: + - Cloudflare Pages & Workers deployment - Custom Domain & DNS configuration - Cloudflare D1 Database migrations - Clerk Authentication setup & Webhooks user synchronization +### Manual admin promotion (fallback) + +If `ADMIN_EMAILS` wasn't set at deploy time: + +```bash +npx wrangler d1 execute vote-system --command "UPDATE users SET is_admin = 1 WHERE email = 'user@example.com'" +``` + +## Admin setup + +### First admin (automatic) + +Set `ADMIN_EMAILS` as a comma-separated list in your Worker environment: diff --git a/apps/api/src/controllers/progress.Controller.ts b/apps/api/src/controllers/progress.Controller.ts index e8127d5..b0f1488 100644 --- a/apps/api/src/controllers/progress.Controller.ts +++ b/apps/api/src/controllers/progress.Controller.ts @@ -3,17 +3,23 @@ import type { AppEnv } from '../types' import { fetchProgress } from '../services/progress.Service' export const getProgress = async (c: Context) => { - const userId = c.req.param('userId') + const paramUserId = c.req.param('userId') + const tokenUserId = c.get('userId') // set by requireAuth middleware - if (!userId) { + if (!paramUserId) { return c.json({ success: false, message: 'userId is required' }, 400) } + // Reject IDOR: param must match the authenticated user + if (tokenUserId !== paramUserId) { + return c.json({ success: false, error: 'Forbidden' }, 403) + } + try { - const data = await fetchProgress(c.env.DB, userId) + const data = await fetchProgress(c.env.DB, paramUserId) return c.json(data) } catch (e: any) { console.error('Progress error:', e) return c.json({ success: false, message: 'Internal Server Error' }, 500) } -} +} \ No newline at end of file diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts index 3620db6..218da21 100644 --- a/apps/api/src/middleware/auth.ts +++ b/apps/api/src/middleware/auth.ts @@ -1,6 +1,6 @@ import { getAuth } from '@clerk/hono' import { createMiddleware } from 'hono/factory' -import { createClerkClient } from '@clerk/backend' // FIX: was clerkClient +import { createClerkClient } from '@clerk/backend' import type { AppEnv } from '../types' import { ensureUserExists } from '../utils/user' diff --git a/apps/api/src/routes/health.route.ts b/apps/api/src/routes/health.route.ts index e7431d7..fc1fc93 100644 --- a/apps/api/src/routes/health.route.ts +++ b/apps/api/src/routes/health.route.ts @@ -1,19 +1,39 @@ import { Hono } from 'hono' +import { getDb } from '../db/client' import type { AppEnv } from '../types' -import { requireAuth } from '../middleware/auth' - +import { sql } from 'drizzle-orm' const health = new Hono() -//test route -health.get('/', requireAuth, (c) => { - const userId = c.get('userId') +// Public — uptime monitor hit this. No auth, no DB. +health.get('/', (c) => { return c.json({ success: true, - data: { - message: 'Hello World', - userId - } + status: 'ok', + timestamp: new Date().toISOString() }) }) -export default health +// Public — pings D1 to verify connectivity. No auth. +health.get('/db', async (c) => { + try { + const db = getDb(c.env.DB) + await db.run(sql`SELECT 1`) + + return c.json({ + success: true, + status: 'ok', + db: 'connected', + timestamp: new Date().toISOString() + }) + } catch (error) { + console.error('Health DB check failed:', error) + return c.json({ + success: false, + status: 'error', + db: 'unreachable', + error: 'Database connection failed' + }, 500) + } +}) + +export default health \ No newline at end of file diff --git a/apps/api/src/routes/progress.route.ts b/apps/api/src/routes/progress.route.ts index b5a7b8c..acc58e0 100644 --- a/apps/api/src/routes/progress.route.ts +++ b/apps/api/src/routes/progress.route.ts @@ -1,9 +1,10 @@ import { Hono } from 'hono' import type { AppEnv } from '../types' import { getProgress } from '../controllers/progress.Controller' +import { requireAuth } from '../middleware/auth' // ADD const progress = new Hono() -progress.get('/:userId', getProgress) +progress.get('/:userId', requireAuth, getProgress) -export default progress +export default progress \ No newline at end of file diff --git a/apps/api/src/routes/results.route.ts b/apps/api/src/routes/results.route.ts index e7014a2..09c1b5a 100644 --- a/apps/api/src/routes/results.route.ts +++ b/apps/api/src/routes/results.route.ts @@ -1,35 +1,60 @@ import { Hono } from 'hono'; +import { getAuth } from '@clerk/hono' import { getDb } from '../db/client'; -import { items, ratings } from '../db/schema'; +import { items, settings, users } from '../db/schema'; // add settings, users import { eq } from 'drizzle-orm'; import type { AppEnv } from '../types'; import { refreshItemAggregates } from '../services/items.Service'; + const resultsRoutes = new Hono(); resultsRoutes.get('/', async (c) => { + const db = getDb(c.env.DB); + + // 1. Check if results are deliberately made public + const publicSetting = await db + .select() + .from(settings) + .where(eq(settings.key, 'results_public')) + .get(); + + const isPublic = publicSetting?.value === 'true'; + + // 2. If NOT public, require admin + if (!isPublic) { + const auth = getAuth(c); + if (!auth?.userId) { + return c.json({ success: false, error: 'Unauthorized' }, 401); + } + + const user = await db + .select() + .from(users) + .where(eq(users.id, auth.userId)) + .get(); + + if (!user?.is_admin) { + return c.json({ success: false, error: 'Forbidden' }, 403); + } + + c.set('userId', auth.userId); + } + + // 3. Fetch and return results try { - const db = getDb(c.env.DB); const allItems = await db.select().from(items); const itemIds = allItems.map(i => i.id); - // Refresh all items to ensure consistency (optional but good for a "Results" view) - // In a high-traffic app, we might skip this and just read, but for this event, real-time-consistent view is best. await refreshItemAggregates(c.env.DB, itemIds); - // Fetch the updated data const finalResults = await db.select().from(items); - - // Sort by qualifiedRatingSum descending for the leaderboard finalResults.sort((a, b) => (b.qualifiedRatingSum || 0) - (a.qualifiedRatingSum || 0)); - return c.json({ - success: true, - data: finalResults - }); + return c.json({ success: true, data: finalResults }); } catch (error) { console.error('Error fetching results:', error); return c.json({ success: false, error: 'Failed to fetch results' }, 500); } }); -export default resultsRoutes; +export default resultsRoutes; \ No newline at end of file diff --git a/apps/web/README.md b/apps/web/README.md index c3c21da..08412ab 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -19,9 +19,9 @@ If you are developing a production application, we recommend updating the config ```js export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ // Other configs... @@ -36,40 +36,40 @@ export default defineConfig([ ], languageOptions: { parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], + project: ["./tsconfig.node.json", "./tsconfig.app.json"], tsconfigRootDir: import.meta.dirname, }, // other options... }, }, -]) +]); ``` You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: ```js // eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +import reactX from "eslint-plugin-react-x"; +import reactDom from "eslint-plugin-react-dom"; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ // Other configs... // Enable lint rules for React - reactX.configs['recommended-typescript'], + reactX.configs["recommended-typescript"], // Enable lint rules for React DOM reactDom.configs.recommended, ], languageOptions: { parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], + project: ["./tsconfig.node.json", "./tsconfig.app.json"], tsconfigRootDir: import.meta.dirname, }, // other options... }, }, -]) +]); ``` diff --git a/package-lock.json b/package-lock.json index 9209090..05416b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,10 +5,86 @@ "packages": { "": { "name": "voting-system", + "dependencies": { + "@clerk/hono": "^0.1.67" + }, "devDependencies": { "turbo": "latest" } }, + "node_modules/@clerk/backend": { + "version": "3.16.6", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-3.16.6.tgz", + "integrity": "sha512-8xyGRhMnfDOZhXJMa50qSG6RQYFNLtoXGVNwPRurcgS/0fBJNQq+CTMrw98GmswZRzUe6Sr2dvCxgWdLh/LjGA==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^4.29.1", + "standardwebhooks": "^1.0.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.9.0" + } + }, + "node_modules/@clerk/hono": { + "version": "0.1.67", + "resolved": "https://registry.npmjs.org/@clerk/hono/-/hono-0.1.67.tgz", + "integrity": "sha512-bvqJNRgj52nGlSAU8iSDXKjhAI0A3CQPbnn3dwNINLCzaHs679+4nbiLwTl/J+rhBjydC035V610yIcmcr3iRg==", + "license": "MIT", + "dependencies": { + "@clerk/backend": "^3.16.6", + "@clerk/shared": "^4.29.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "peerDependencies": { + "hono": ">=4" + } + }, + "node_modules/@clerk/shared": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.29.1.tgz", + "integrity": "sha512-vl53gfMKHGhOskQgYfz7Qdkw0E/TYMCRuBkKRibi+8/Mz0JvR1yyVKA9k/w9CRvGfGLL9m/4/YDr1IlUJjx8ag==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "^5.100.6", + "dequal": "2.0.3", + "glob-to-regexp": "0.4.1", + "js-cookie": "3.0.7" + }, + "engines": { + "node": ">=20.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", + "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@turbo/darwin-64": { "version": "2.8.20", "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.8.20.tgz", @@ -93,6 +169,62 @@ "win32" ] }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/hono": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz", + "integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/js-cookie": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz", + "integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/turbo": { "version": "2.8.20", "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.20.tgz", diff --git a/package.json b/package.json index 2d565a6..6430b78 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ }, "devDependencies": { "turbo": "latest" + }, + "dependencies": { + "@clerk/hono": "^0.1.67" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7485e5..2592576 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@clerk/hono': + specifier: ^0.1.67 + version: 0.1.67(hono@4.12.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) devDependencies: turbo: specifier: latest @@ -81,7 +85,7 @@ importers: version: 9.39.4 '@rolldown/plugin-babel': specifier: ^0.2.1 - version: 0.2.2(@babel/core@7.29.0)(rolldown@1.0.0-rc.12)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)) + version: 0.2.2(@babel/core@7.29.0)(rolldown@1.2.4)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)) '@tailwindcss/vite': specifier: ^4.2.2 version: 4.2.2(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)) @@ -99,7 +103,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(rolldown@1.0.0-rc.12)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)) + version: 6.0.1(@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(rolldown@1.2.4)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -216,6 +220,16 @@ packages: resolution: {integrity: sha512-DRwmFu6gEmzHRUeXXB5y02QxMihHDEgetSQrb0ME6KaYe29+LnenBUQAmlASXmsovIi9cBqk4hE4WHWNRXX+Bw==} engines: {node: '>=18.17.0'} + '@clerk/backend@3.16.6': + resolution: {integrity: sha512-8xyGRhMnfDOZhXJMa50qSG6RQYFNLtoXGVNwPRurcgS/0fBJNQq+CTMrw98GmswZRzUe6Sr2dvCxgWdLh/LjGA==} + engines: {node: '>=20.9.0'} + + '@clerk/hono@0.1.67': + resolution: {integrity: sha512-bvqJNRgj52nGlSAU8iSDXKjhAI0A3CQPbnn3dwNINLCzaHs679+4nbiLwTl/J+rhBjydC035V610yIcmcr3iRg==} + engines: {node: '>=20.9.0'} + peerDependencies: + hono: '>=4' + '@clerk/react@6.1.2': resolution: {integrity: sha512-hqghETUo8LLG+7pJQoRylFadwGNDrnLzM8n36SXr8T/ZyFqEmS1XzmiEZRi9xnKit1iFExpyiyA2nQW3HSCcvA==} engines: {node: '>=20.9.0'} @@ -235,6 +249,18 @@ packages: react-dom: optional: true + '@clerk/shared@4.29.1': + resolution: {integrity: sha512-vl53gfMKHGhOskQgYfz7Qdkw0E/TYMCRuBkKRibi+8/Mz0JvR1yyVKA9k/w9CRvGfGLL9m/4/YDr1IlUJjx8ag==} + engines: {node: '>=20.9.0'} + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@clerk/shared@4.3.2': resolution: {integrity: sha512-tYYzdY4Fxb02TO4RHoLRFzEjXJn0iFDfoKhWtGyqf2AaIgkprTksunQtX0hnVssHMr3XD/E2S00Vrb+PzX3jCQ==} engines: {node: '>=20.9.0'} @@ -953,8 +979,8 @@ packages: '@oxc-project/types@0.120.0': resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} - '@oxc-project/types@0.122.0': - resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@oxc-project/types@0.144.0': + resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} '@rolldown/binding-android-arm64@1.0.0-rc.10': resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} @@ -962,8 +988,8 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + '@rolldown/binding-android-arm64@1.2.4': + resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -974,8 +1000,8 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + '@rolldown/binding-darwin-arm64@1.2.4': + resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -986,8 +1012,8 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + '@rolldown/binding-darwin-x64@1.2.4': + resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -998,8 +1024,8 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + '@rolldown/binding-freebsd-x64@1.2.4': + resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -1010,8 +1036,8 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -1023,8 +1049,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + '@rolldown/binding-linux-arm64-gnu@1.2.4': + resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1036,8 +1062,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + '@rolldown/binding-linux-arm64-musl@1.2.4': + resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1049,8 +1075,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] @@ -1062,8 +1088,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + '@rolldown/binding-linux-s390x-gnu@1.2.4': + resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -1075,8 +1101,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + '@rolldown/binding-linux-x64-gnu@1.2.4': + resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1088,8 +1114,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + '@rolldown/binding-linux-x64-musl@1.2.4': + resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1100,8 +1126,8 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + '@rolldown/binding-openharmony-arm64@1.2.4': + resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -1111,19 +1137,14 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + '@rolldown/binding-win32-arm64-msvc@1.2.4': + resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -1134,8 +1155,8 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + '@rolldown/binding-win32-x64-msvc@1.2.4': + resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1160,12 +1181,12 @@ packages: '@rolldown/pluginutils@1.0.0-rc.10': resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} - '@rolldown/pluginutils@1.0.0-rc.12': - resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} - '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} @@ -1259,6 +1280,9 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} + '@tanstack/query-core@5.90.16': resolution: {integrity: sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==} @@ -1953,6 +1977,10 @@ packages: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} + js-cookie@3.0.7: + resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==} + engines: {node: '>=20'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2250,8 +2278,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.0-rc.12: - resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + rolldown@1.2.4: + resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2664,6 +2692,24 @@ snapshots: - react - react-dom + '@clerk/backend@3.16.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@clerk/shared': 4.29.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + standardwebhooks: 1.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/hono@0.1.67(hono@4.12.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@clerk/backend': 3.16.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@clerk/shared': 4.29.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + hono: 4.12.8 + transitivePeerDependencies: + - react + - react-dom + '@clerk/react@6.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@clerk/shared': 4.3.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2683,6 +2729,16 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@clerk/shared@4.29.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/query-core': 5.101.4 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.7 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@clerk/shared@4.3.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/query-core': 5.90.16 @@ -3161,78 +3217,78 @@ snapshots: '@oxc-project/types@0.120.0': {} - '@oxc-project/types@0.122.0': {} + '@oxc-project/types@0.144.0': {} '@rolldown/binding-android-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.12': + '@rolldown/binding-android-arm64@1.2.4': optional: true '@rolldown/binding-darwin-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + '@rolldown/binding-darwin-arm64@1.2.4': optional: true '@rolldown/binding-darwin-x64@1.0.0-rc.10': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.12': + '@rolldown/binding-darwin-x64@1.2.4': optional: true '@rolldown/binding-freebsd-x64@1.0.0-rc.10': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + '@rolldown/binding-freebsd-x64@1.2.4': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-arm64-gnu@1.2.4': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + '@rolldown/binding-linux-arm64-musl@1.2.4': optional: true '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-ppc64-gnu@1.2.4': optional: true '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-s390x-gnu@1.2.4': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-x64-gnu@1.2.4': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + '@rolldown/binding-linux-x64-musl@1.2.4': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + '@rolldown/binding-openharmony-arm64@1.2.4': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': @@ -3240,37 +3296,32 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - dependencies: - '@napi-rs/wasm-runtime': 1.1.1 - optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + '@rolldown/binding-win32-arm64-msvc@1.2.4': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + '@rolldown/binding-win32-x64-msvc@1.2.4': optional: true - '@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(rolldown@1.0.0-rc.12)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1))': + '@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(rolldown@1.2.4)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1))': dependencies: '@babel/core': 7.29.0 picomatch: 4.0.3 - rolldown: 1.0.0-rc.12 + rolldown: 1.2.4 optionalDependencies: vite: 8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1) '@rolldown/pluginutils@1.0.0-rc.10': {} - '@rolldown/pluginutils@1.0.0-rc.12': {} - '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rolldown/pluginutils@1.0.1': {} + '@stablelib/base64@1.0.1': {} '@tailwindcss/node@4.2.2': @@ -3341,6 +3392,8 @@ snapshots: tailwindcss: 4.2.2 vite: 8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1) + '@tanstack/query-core@5.101.4': {} + '@tanstack/query-core@5.90.16': {} '@turbo/darwin-64@2.8.20': @@ -3494,12 +3547,12 @@ snapshots: '@typescript-eslint/types': 8.57.1 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.0.1(@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(rolldown@1.0.0-rc.12)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1))': + '@vitejs/plugin-react@6.0.1(@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(rolldown@1.2.4)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1) optionalDependencies: - '@rolldown/plugin-babel': 0.2.2(@babel/core@7.29.0)(rolldown@1.0.0-rc.12)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)) + '@rolldown/plugin-babel': 0.2.2(@babel/core@7.29.0)(rolldown@1.2.4)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.19.12)(jiti@2.6.1)) babel-plugin-react-compiler: 1.0.0 acorn-jsx@5.3.2(acorn@8.16.0): @@ -4044,6 +4097,8 @@ snapshots: js-cookie@3.0.5: {} + js-cookie@3.0.7: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -4318,26 +4373,25 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 - rolldown@1.0.0-rc.12: + rolldown@1.2.4: dependencies: - '@oxc-project/types': 0.122.0 - '@rolldown/pluginutils': 1.0.0-rc.12 + '@oxc-project/types': 0.144.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-x64': 1.0.0-rc.12 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + '@rolldown/binding-android-arm64': 1.2.4 + '@rolldown/binding-darwin-arm64': 1.2.4 + '@rolldown/binding-darwin-x64': 1.2.4 + '@rolldown/binding-freebsd-x64': 1.2.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 + '@rolldown/binding-linux-arm64-gnu': 1.2.4 + '@rolldown/binding-linux-arm64-musl': 1.2.4 + '@rolldown/binding-linux-ppc64-gnu': 1.2.4 + '@rolldown/binding-linux-s390x-gnu': 1.2.4 + '@rolldown/binding-linux-x64-gnu': 1.2.4 + '@rolldown/binding-linux-x64-musl': 1.2.4 + '@rolldown/binding-openharmony-arm64': 1.2.4 + '@rolldown/binding-win32-arm64-msvc': 1.2.4 + '@rolldown/binding-win32-x64-msvc': 1.2.4 rollup-plugin-inject@3.0.2: dependencies: