mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-30 17:50:07 +08:00
2c8e2cc837
* 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>
91 lines
3.3 KiB
TypeScript
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
|