Files
zpan/server/app.ts
T
Jasper Van 2c8e2cc837 feat: v2.3.0 T1 — 站内信系统 (in-app notifications) (#307)
* feat: add in-app notification system (站内信) — schema, service, API, Bell UI

- Add `notifications` table to DB schema with userId/type/title/body/refType/refId/metadata/readAt/createdAt fields; two indexes for list & unread queries
- Migration `0010_notifications.sql` created manually (drizzle-kit requires TTY)
- Service layer: createNotification, listNotifications (paginated + unreadOnly filter), markAsRead (idempotent, owner-only), markAllAsRead, unreadCount
- REST API at `/api/notifications`: list + unreadCount, GET unread-count, POST :id/read (204), POST read-all
- Shared `Notification` type, `listNotificationsQuerySchema`, RPC client export
- NotificationBell (badge, 30s polling), NotificationDropdown, NotificationItem components injected into AppSidebar footer
- Bell badge capped at "9+"; unread items bold; click marks read + navigates via refType/refId
- i18n: en + zh translations for all notification keys
- 26 Node integration tests + 5 CF smoke tests; all 1884 + 26 tests pass

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

* test: add notification API wrapper tests and component logic tests; fix dead condition

- Add tests for listNotifications, getUnreadCount, markNotificationRead, markAllNotificationsRead in api.test.ts
- Add notification-bell.test.ts: badge label logic (0/5/"9+" cap) and polling interval
- Add notification-dropdown.test.ts: mark-all-read visibility, empty state, query key
- Add notification-item.test.ts: resolveHref (share token nav, malformed JSON), diffMinutes, isUnread, title style
- Fix dead condition in markNotificationRead: simplify `!res.ok && res.status !== 204` → `!res.ok`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: trigger CI check run for test coverage fixes

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 01:23:54 -04:00

91 lines
3.3 KiB
TypeScript

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import type { Auth } from './auth'
import { authMiddleware } from './middleware/auth'
import { accessLog } from './middleware/logger'
import type { Env } from './middleware/platform'
import { platformMiddleware } from './middleware/platform'
import type { Platform } from './platform/interface'
import authProviders from './routes/auth-providers'
import emailConfig from './routes/email-config'
import { adminInviteCodes, publicInviteCodes } from './routes/invite-codes'
import { notifications } from './routes/notifications'
import objects from './routes/objects'
import profile from './routes/profile'
import { adminQuotas, userQuotas } from './routes/quotas'
import shares from './routes/shares'
import storages from './routes/storages'
import system from './routes/system'
import { publicTeams, teams } from './routes/teams'
import trash from './routes/trash'
import users from './routes/users'
export function createApp(platform: Platform, auth: Auth) {
const app = new Hono<Env>()
app.use('/*', platformMiddleware(platform, auth))
app.use('/api/*', accessLog)
app.use(
'/api/*',
cors({
origin: (origin) => origin || '*',
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
}),
)
app.on(['POST', 'GET'], '/api/auth/*', async (c) => {
const a = c.get('auth')
return a.handler(c.req.raw)
})
// Public routes — no auth required; mount before authMiddleware
app.route('/api/profiles', profile)
app.route('/api/teams', publicTeams)
app.use('/api/*', authMiddleware)
// Mount routes separately to avoid deep type chain accumulation.
// Each .route() call is independent — TypeScript doesn't stack types.
app.route('/api/objects', objects)
app.route('/api/shares', shares)
app.route('/api/recycle-bin', trash)
app.route('/api/teams', teams)
app.route('/api/admin/storages', storages)
app.route('/api/admin/users', users)
app.route('/api/admin/email-config', emailConfig)
app.route('/api/admin/invite-codes', adminInviteCodes)
app.route('/api/invite-codes', publicInviteCodes)
app.route('/api/admin/quotas', adminQuotas)
app.route('/api/quotas', userQuotas)
app.route('/api/system', system)
app.route('/api/auth-providers', authProviders)
app.route('/api/notifications', notifications)
app.get('/api/health', (c) => c.json({ status: 'ok' }))
return app
}
export type AppType = ReturnType<typeof createApp>
// Sub-router types for RPC clients — avoids combined AppType OOM
export type ObjectsRoute = typeof objects
export type SharesRoute = typeof shares
export type TrashRoute = typeof trash
export type StoragesRoute = typeof storages
export type UsersRoute = typeof users
export type AdminQuotasRoute = typeof adminQuotas
export type UserQuotasRoute = typeof userQuotas
export type SystemRoute = typeof system
export type EmailConfigRoute = typeof emailConfig
export type AdminInviteCodesRoute = typeof adminInviteCodes
export type PublicInviteCodesRoute = typeof publicInviteCodes
export type AuthProvidersRoute = typeof authProviders
export type ProfileRoute = typeof profile
export type TeamsRoute = typeof teams
export type PublicTeamsRoute = typeof publicTeams
export type NotificationsRoute = typeof notifications