170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
import { Hono } from 'hono'
|
|
import { jwt, sign, verify } from 'hono/jwt'
|
|
import { drizzle } from 'drizzle-orm/d1'
|
|
import { eq, and, inArray, desc } from 'drizzle-orm'
|
|
import { usersTable, postsTable, followsTable, likesTable } from './db/schema'
|
|
|
|
type Bindings = {
|
|
DB: D1Database
|
|
JWT_SECRET: string
|
|
}
|
|
|
|
const app = new Hono<{ Bindings: Bindings }>()
|
|
|
|
// Middleware for JWT protection on some routes
|
|
app.use('/api/protected/*', async (c, next) => {
|
|
const jwtMiddleware = jwt({
|
|
secret: c.env.JWT_SECRET || 'supersecret',
|
|
alg: 'HS256'
|
|
})
|
|
return jwtMiddleware(c, next)
|
|
})
|
|
|
|
// --- AUTH ---
|
|
app.post('/api/signup', async (c) => {
|
|
const { username, password } = await c.req.json()
|
|
const db = drizzle(c.env.DB)
|
|
|
|
try {
|
|
const result = await db.insert(usersTable).values({
|
|
username,
|
|
passwordHash: password, // In a real app, hash this!
|
|
}).returning()
|
|
|
|
return c.json({ success: true, user: result[0] })
|
|
} catch (e) {
|
|
console.error('Signup error:', e)
|
|
return c.json({ error: 'Username taken or database error', details: String(e) }, 400)
|
|
}
|
|
})
|
|
|
|
app.post('/api/login', async (c) => {
|
|
const { username, password } = await c.req.json()
|
|
const db = drizzle(c.env.DB)
|
|
|
|
const user = await db.select().from(usersTable)
|
|
.where(and(eq(usersTable.username, username), eq(usersTable.passwordHash, password)))
|
|
.get()
|
|
|
|
if (!user) return c.json({ error: 'Invalid credentials' }, 401)
|
|
|
|
const token = await sign({ id: user.id, username: user.username }, c.env.JWT_SECRET || 'supersecret', 'HS256')
|
|
return c.json({ token, user })
|
|
})
|
|
|
|
// --- POSTS & FEED ---
|
|
app.get('/api/protected/feed', async (c) => {
|
|
const payload = c.get('jwtPayload')
|
|
const db = drizzle(c.env.DB)
|
|
|
|
try {
|
|
// Get followed users
|
|
const followed = await db.select({ id: followsTable.followingId })
|
|
.from(followsTable)
|
|
.where(eq(followsTable.followerId, payload.id))
|
|
.all()
|
|
|
|
const followedIds = [payload.id, ...followed.map(f => f.id)]
|
|
|
|
const feed = await db.select({
|
|
id: postsTable.id,
|
|
content: postsTable.content,
|
|
createdAt: postsTable.createdAt,
|
|
username: usersTable.username,
|
|
userId: usersTable.id
|
|
})
|
|
.from(postsTable)
|
|
.leftJoin(usersTable, eq(postsTable.userId, usersTable.id))
|
|
.where(inArray(postsTable.userId, followedIds))
|
|
.orderBy(desc(postsTable.createdAt))
|
|
.all()
|
|
|
|
return c.json(feed)
|
|
} catch (e) {
|
|
console.error('Feed error:', e)
|
|
return c.json({ error: 'Failed to fetch feed', details: String(e) }, 500)
|
|
}
|
|
})
|
|
|
|
app.post('/api/protected/posts', async (c) => {
|
|
const payload = c.get('jwtPayload')
|
|
const { content } = await c.req.json()
|
|
const db = drizzle(c.env.DB)
|
|
|
|
try {
|
|
const result = await db.insert(postsTable).values({
|
|
userId: payload.id,
|
|
content,
|
|
}).returning()
|
|
|
|
return c.json(result[0])
|
|
} catch (e) {
|
|
console.error('Post creation error:', e)
|
|
return c.json({ error: 'Failed to create post', details: String(e) }, 500)
|
|
}
|
|
})
|
|
|
|
// --- FOLLOW & LIKE ---
|
|
app.post('/api/protected/follow/:id', async (c) => {
|
|
const payload = c.get('jwtPayload')
|
|
const targetId = parseInt(c.req.param('id'))
|
|
const db = drizzle(c.env.DB)
|
|
|
|
await db.insert(followsTable).values({
|
|
followerId: payload.id,
|
|
followingId: targetId,
|
|
}).run()
|
|
|
|
return c.json({ success: true })
|
|
})
|
|
|
|
app.post('/api/protected/like/:id', async (c) => {
|
|
const payload = c.get('jwtPayload')
|
|
const postId = parseInt(c.req.param('id'))
|
|
const db = drizzle(c.env.DB)
|
|
|
|
await db.insert(likesTable).values({
|
|
userId: payload.id,
|
|
postId,
|
|
}).run()
|
|
|
|
return c.json({ success: true })
|
|
})
|
|
|
|
// Serve React App
|
|
app.get('*', async (c) => {
|
|
return c.html(`
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Social Media App</title>
|
|
${import.meta.env?.PROD ? '' : `
|
|
<script type="module">
|
|
import RefreshRuntime from "/@react-refresh"
|
|
RefreshRuntime.injectIntoGlobalHook(window)
|
|
window.$RefreshReg$ = () => {}
|
|
window.$RefreshSig$ = () => (type) => type
|
|
window.__reactRefreshLog = () => {}
|
|
</script>
|
|
<script type="module" src="/@vite/client"></script>
|
|
`}
|
|
<style>
|
|
body { font-family: sans-serif; background: #f0f2f5; margin: 0; padding: 20px; }
|
|
.container { max-width: 600px; margin: auto; }
|
|
.card { background: white; padding: 15px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
|
.btn { background: #007bff; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; }
|
|
.nav { margin-bottom: 20px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="module" src="/src/client.tsx"></script>
|
|
</body>
|
|
</html>
|
|
`)
|
|
})
|
|
|
|
export default app
|