Files
zpan/server/app.ts
T
Jasper VanandClaude Sonnet 4.6 66b3ee3435 feat: public share landing page /s/:token + Workers SSR OG meta (#312)
* feat: public share landing page /s/:token + Workers SSR OG meta

- Add SPA route `/s/:token` (TanStack Router, outside _authenticated)
- Implement share components: ShareLanding, FilePreview, FolderBrowser,
  PasswordPrompt, SaveToDriveDialog, ShareError
- File preview: image/video/audio/PDF via object URL fetch; fallback for
  other types with download CTA
- Folder browser: breadcrumb navigation + children table with download
- Password gate: POST /api/share/:token/verify with error feedback
- Save to drive: workspace + folder picker, quota/password/gone error handling
- Workers SSR: inject OG meta tags for /s/:token requests (title, description,
  image, twitter:card); fetch share metadata via /api/share/:token
- Add /s/* to wrangler.toml run_worker_first for SSR routing
- Add zValidator to /:token/children endpoint for typed RPC query params
- Export ShareApiRoute type from server/app.ts; add RPC clients in rpc.ts
- Add share.* i18n keys (en + zh)
- 9 new unit tests covering error code derivation, escaping, i18n coverage

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

* test: add coverage for share public API wrappers and path traversal guard

- api.test.ts: add unit tests for getShareLanding, verifySharePassword,
  getShareChildren, saveShareToDrive (success + all error paths)
- share-public.integration.test.ts: add path traversal guard test
  (.. in path param returns 400 Invalid path)

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

* test: cover explicit page/pageSize params in children endpoint

Add integration test for GET /api/share/:token/children with explicit
page and pageSize query params to satisfy codecov/patch branch coverage.

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

* test: add error path coverage for children endpoint

Cover invalid token (404), trashed matter (410), and non-numeric
page/pageSize (NaN fallback) in GET /:token/children to satisfy
codecov/patch threshold requirements.

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

* fix: add ASSETS binding to wrangler.toml for Workers SSR

Without binding = "ASSETS", env.ASSETS is undefined at runtime
and the /s/:token SSR handler throws error code 1101.

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

* fix: resolve CF SSR OG meta by calling service layer directly instead of self-subrequest

Cloudflare Workers cannot fetch() their own origin when the path is listed
in run_worker_first — the request loops back and returns a 500 error code 1101.
Replace the HTTP subrequest in fetchShareMeta with a direct call to
resolveShareByToken(platform.db, token) from the service layer.

Add CF integration tests asserting that a valid landing share produces real
og:title metadata and an unknown token falls back gracefully.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 13:16:20 -04:00

101 lines
3.8 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 shareApi from './routes/share-api'
import shareDirect from './routes/share-direct'
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 share routes — no auth required; mount before authMiddleware.
// /api/share/* is covered by run_worker_first=["/api/*"] in wrangler.toml.
// /dl/* is listed separately in run_worker_first.
// /s/:token is intentionally left for the T8 SPA landing page.
app.route('/api/share', shareApi)
app.route('/dl', shareDirect)
// 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 ShareApiRoute = typeof shareApi
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