Add Hono backend with PostgreSQL, auth, cloud saves, and leaderboard
Server app (apps/server) with Hono framework and Drizzle ORM: - PostgreSQL schema: users, saves, leaderboard, achievements tables - Anonymous auth with UUID tokens, optional email/password linking - Cloud save API: list, get, upsert, delete with auto-save hook - Leaderboard API: per-category rankings with score submission - CORS configured for dev server ports - Typed middleware with Hono env variables Frontend cloud save integration: - API client with auth token management in localStorage - useCloudSave hook auto-saves every 300 ticks when authenticated - Vite env type declarations for VITE_API_URL Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { Hono } from 'hono';
|
||||
import { eq, and, desc } from 'drizzle-orm';
|
||||
import { db } from '../db';
|
||||
import { saves } from '../db/schema';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import type { AppEnv } from '../types';
|
||||
|
||||
const savesRouter = new Hono<AppEnv>();
|
||||
|
||||
savesRouter.use('*', authMiddleware);
|
||||
|
||||
savesRouter.get('/', async (c) => {
|
||||
const userId = c.get('userId') as string;
|
||||
|
||||
const userSaves = await db
|
||||
.select({
|
||||
id: saves.id,
|
||||
companyName: saves.companyName,
|
||||
era: saves.era,
|
||||
tickCount: saves.tickCount,
|
||||
updatedAt: saves.updatedAt,
|
||||
})
|
||||
.from(saves)
|
||||
.where(eq(saves.userId, userId))
|
||||
.orderBy(desc(saves.updatedAt))
|
||||
.limit(10);
|
||||
|
||||
return c.json({ saves: userSaves });
|
||||
});
|
||||
|
||||
savesRouter.get('/:id', async (c) => {
|
||||
const userId = c.get('userId') as string;
|
||||
const saveId = c.req.param('id');
|
||||
|
||||
const [save] = await db
|
||||
.select()
|
||||
.from(saves)
|
||||
.where(and(eq(saves.id, saveId), eq(saves.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!save) {
|
||||
return c.json({ error: 'Save not found' }, 404);
|
||||
}
|
||||
|
||||
return c.json({ save });
|
||||
});
|
||||
|
||||
savesRouter.put('/', async (c) => {
|
||||
const userId = c.get('userId') as string;
|
||||
const body = await c.req.json<{
|
||||
companyName: string;
|
||||
saveVersion: number;
|
||||
gameData: unknown;
|
||||
tickCount: number;
|
||||
era: string;
|
||||
}>();
|
||||
|
||||
const existing = await db
|
||||
.select({ id: saves.id })
|
||||
.from(saves)
|
||||
.where(eq(saves.userId, userId))
|
||||
.orderBy(desc(saves.updatedAt))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db
|
||||
.update(saves)
|
||||
.set({
|
||||
companyName: body.companyName,
|
||||
saveVersion: body.saveVersion,
|
||||
gameData: body.gameData,
|
||||
tickCount: body.tickCount,
|
||||
era: body.era,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(saves.id, existing[0].id));
|
||||
|
||||
return c.json({ id: existing[0].id, updated: true });
|
||||
}
|
||||
|
||||
const [newSave] = await db
|
||||
.insert(saves)
|
||||
.values({
|
||||
userId,
|
||||
companyName: body.companyName,
|
||||
saveVersion: body.saveVersion,
|
||||
gameData: body.gameData,
|
||||
tickCount: body.tickCount,
|
||||
era: body.era,
|
||||
})
|
||||
.returning({ id: saves.id });
|
||||
|
||||
return c.json({ id: newSave.id, created: true });
|
||||
});
|
||||
|
||||
savesRouter.delete('/:id', async (c) => {
|
||||
const userId = c.get('userId') as string;
|
||||
const saveId = c.req.param('id');
|
||||
|
||||
await db
|
||||
.delete(saves)
|
||||
.where(and(eq(saves.id, saveId), eq(saves.userId, userId)));
|
||||
|
||||
return c.json({ deleted: true });
|
||||
});
|
||||
|
||||
export { savesRouter };
|
||||
Reference in New Issue
Block a user