Commit Graph
622 Commits
Author SHA1 Message Date
saltbo 4537bfac51 feat(preview): add microsoft office viewer 2026-04-30 09:32:49 -04:00
saltbo 57cc834047 feat(share): redesign access page preview 2026-04-30 08:58:31 -04:00
saltbo 90e2bab024 test(cf): resolve shared aliases in vitest config 2026-04-29 21:13:21 -04:00
saltbo a9cf593e82 test(licensing): authorize localhost in Pro seed 2026-04-29 21:06:55 -04:00
saltbo 41dc5099b0 copy(billing): refine Pro license wording 2026-04-29 20:53:54 -04:00
saltbo f0e7af2798 feat(licensing): redesign Pro license binding 2026-04-29 20:20:18 -04:00
saltbo d28dbe764a fix(email): require explicit provider selection 2026-04-27 22:39:56 -04:00
saltbo 69bc49272b fix(cf): remove email sender allowlist 2026-04-27 22:29:15 -04:00
Jasper Van 660e9fd191 Merge pull request #353 from saltbo/feat/cf-email-send
feat: add Cloudflare Worker mail service toggle
2026-04-27 22:07:00 -04:00
saltbo 5164c89a6f test(email): close patch coverage gaps 2026-04-27 21:57:56 -04:00
saltbo 42de0f8315 test(email): cover cloudflare fallback branches 2026-04-27 21:49:26 -04:00
saltbo 7b9206f7e9 fix(test): stabilize cloudflare storages coverage 2026-04-27 21:41:09 -04:00
saltbo 115aa4a33d fix(test): correct teams upgrade hint scenario 2026-04-27 21:34:16 -04:00
saltbo 2657724448 fix(e2e): stabilize admin and site invitation flows 2026-04-27 21:28:07 -04:00
saltbo 2272a87616 test(email): enable site invitation mail fixtures 2026-04-27 21:28:07 -04:00
saltbo d75d7e5461 feat(email): add cloudflare worker mail service toggle 2026-04-27 21:28:07 -04:00
saltbo 5b743ccc07 chore(admin): polish licensing layout and labels 2026-04-27 21:24:44 -04:00
saltbo ec9cc0b5e0 feat(admin): add announcement placeholder 2026-04-27 21:15:41 -04:00
saltbo bf7c3389ea refactor(site): centralize default site metadata 2026-04-27 21:00:40 -04:00
saltbo 54c6104967 chore(admin): reorder sidebar settings link 2026-04-27 20:56:12 -04:00
saltbo bf83be3634 feat(admin): add audit logs placeholder 2026-04-27 20:55:14 -04:00
saltbo d7f1ceb6bc feat(licensing): adjust free plan limits 2026-04-27 20:41:46 -04:00
saltbo 8fcd677099 fix(admin): reorder users page actions 2026-04-27 20:12:56 -04:00
saltbo 1a1426896a test(e2e): cover site invitation signup flow 2026-04-27 20:11:07 -04:00
saltbo c4fc8c9f84 feat(admin): add site invitation signup flow 2026-04-27 19:49:19 -04:00
saltbo 736d7c8d10 feat(admin): refine pro-gated settings UI 2026-04-27 19:33:04 -04:00
saltbo fb3b15a38c feat(admin): redesign settings and rename auth to oauth 2026-04-27 19:08:17 -04:00
saltbo 4872f97d9f fix: align licensing pairing with cloud certificates 2026-04-27 08:46:05 -04:00
saltbo 45b623e362 fix: reject invalid cloud refresh certificates 2026-04-27 08:39:30 -04:00
saltbo faed8cbece refactor(admin): streamline admin navigation and licensing management
- Add Overview dashboard as the admin entry point
- Merge Branding configuration into System Settings and unify brand name as siteName
- Independent Email settings from Auth configuration
- Rename Billing to Licensing with more professional iconography
- Reorder navigation items by priority: Overview, Users, Storages, Auth, Email, Settings, Licensing
- Streamline feature registry by simplifying categories and descriptions
2026-04-26 08:34:49 -04:00
saltboandCopilot 9af31ad105 style: make Powered by ZPan a subtle watermark with hover link
- Reduce font to 10px with 30% opacity for watermark effect
- Remove default underline, show only on hover
- Hover slightly increases opacity to 50%

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 09:06:30 -04:00
saltboandCopilot b8ef531dfd style: move Powered by ZPan above storage quota and add repo link
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 08:59:58 -04:00
saltboandCopilot b179db9efd feat: centralize Pro feature definitions into a single registry
- Create shared/feature-registry.ts as the single source of truth
- Define 16 features across 6 categories with typed CellValue per plan
- Derive ProFeature union type from as-const registry (type-safe)
- Add 4 Coming Soon Pro features (audit log, webhooks, SSO, analytics)
- Rewrite ComparisonTable to render from registry with category grouping
- Merge 'Up to 3 Teams' + 'Teams Unlimited' into one quota-diff row
- Add i18n keys for all features and categories (en + zh)
- Remove orphaned ProFeatures const from shared/constants.ts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 08:46:05 -04:00
dependabot[bot] b40b2bf46b build(deps-dev): bump postcss from 8.5.9 to 8.5.10 (#351)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.9 to 8.5.10.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.9...8.5.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 07:39:16 -04:00
saltboandCopilot 7cb85606a5 fix: remove QR code, keep countdown timer in pairing modal
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 23:20:53 -04:00
saltboandCopilot 79adf45c91 feat: add QR code and countdown timer to pairing modal
- Add qrcode.react dependency for QR code generation
- Display QR code in PairingModal encoding the pairing URL
- Add countdown timer showing time remaining until code expires
- Auto-expire and stop polling when countdown reaches 0
- All 2809 tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 23:16:43 -04:00
saltboandCopilot fa943ca8b3 feat: replace license_binding table with system_options keys
- Add license-state.ts helper for reading/writing license state as
  system_options key-value pairs instead of a dedicated singleton table
- Rewrite refresh.ts, has-feature.ts, entitlement.ts, licensing-admin.ts,
  licensing-refresh-runner.ts to use license-state helpers
- Generate migration 0014 to drop license_binding table
- Update all 10 test files to use setLicenseOptions instead of
  db.insert(licenseBinding)
- All 2809 tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 22:58:13 -04:00
saltboandCopilot 784b54a631 fix: move billing page from user settings to admin panel
- Move billing.tsx and billing.test.tsx to admin/ route
- Add billing nav item to admin sidebar with CreditCard icon
- Update UpgradeHint CTA link from /settings/billing to /admin/billing
- Remove billing tab from user settings layout
- Add i18n keys for admin.nav.billing (en: Billing, zh: 订阅管理)
- Regenerate TanStack Router route tree
- Apply migration 0013_licensing to production D1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 21:07:08 -04:00
saltboandCopilot 17d70d605e test: add E2E licensing integration tests for zpan ↔ zpan-cloud
Comprehensive end-to-end test covering:
- Live cloud API contract (pairing create/poll, entitlement 401)
- Feature gates: community (unbound) → Pro features blocked
- Feature gates: Pro binding → all 4 features enabled
- Feature gates: expired cert → features revoked
- Feature gates: partial features (subset of Pro)
- Full lifecycle: pair → approve → activate → open_registration → unbind → blocked
- PASETO verification: reject unknown keys, verify PUBLIC_KEYS configured
- System options: 402 without Pro, 201 with Pro for open_registration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 20:37:25 -04:00
saltboandCopilot 134d325f56 fix: update cloud public key for production keypair
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 20:08:43 -04:00
saltboandCopilot 76645d47b3 fix: align licensing client with zpan-cloud API contract
- Send refresh token as Authorization Bearer header (not JSON body)
- Read 'certificate' field from refresh response (was 'entitlement')
- Handle both PASETO tokens and legacy JSON in cached certificates
- Verify PASETO tokens in getPlanFromCert() for admin display
- Extract expires_at from snake_case entitlement in pairing poll
- Update all related tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 20:01:08 -04:00
Jasper VanandBob 04f9d93ddf feat: v2.6 Z11 — production public key, Docker cron docs, release notes (#350)
* feat: v2.6 Z11 — prod public key, Docker cron docs, release notes

- Replace DEV placeholder in public-keys.ts with cloud.zpan.space
  production Ed25519 key (k4.public.sphdaogcyIh2_6_yZnO4_xQsi2m52HH9j2CPHcKlGGw)
  from cloud C5 cross-repo PR
- Add external cron section to docs/deploy/docker.md for the
  POST /api/licensing/refresh-cron endpoint (Z6)
- Create docs/v2.6-release-notes.md with what's new, retroactive gate
  notice (open_registration, teams_unlimited, team_quotas), upgrade guide

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

* test(licensing): decouple verify/entitlement tests from DEV secret key

Tests were hardcoded to the old DEV placeholder key. Now they generate
a fresh throwaway keypair per suite (beforeAll/afterAll), inject the
public key into PUBLIC_KEYS, and restore the original on teardown.

This keeps the tests independent of whichever production key is in
PUBLIC_KEYS, so rotating the key never breaks the test suite.

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

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 09:20:58 -04:00
Jasper VanandBob 3f2890c6ea feat: retroactive gate — per-team storage quota Pro-only (Z10) (#349)
- server/routes/quotas.ts: gate PUT /:orgId with requireFeature('team_quotas') → 402 on Community
- server/services/matter.ts: add teamQuotaEnabled param to incrementUsageIfAllowed and confirmUpload; when false, skip per-team quota check but still track storage usage
- server/services/save-to-drive.ts: thread teamQuotaEnabled through saveShareToDrive → saveFile/saveFolderRecursive
- server/routes/objects.ts: check hasFeature('team_quotas') before confirmUpload
- server/routes/shares.ts: skip isQuotaSufficient pre-check and pass teamQuotaEnabled to saveShareToDriveService when not Pro
- src/routes/_authenticated/admin/users/index.tsx: hide quota column/button behind useEntitlement('team_quotas'); show UpgradeHint when not Pro
- server/test/setup.ts: add seedProLicense helper for integration tests
- Update affected integration tests to seed Pro license where quota enforcement is expected

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 09:03:15 -04:00
Jasper VanandBob de5ff92f03 feat: gate team creation at 3 orgs for community plan (teams_unlimited required for 4th) (#348)
- Add COMMUNITY_TEAM_LIMIT=3 to shared/constants so both server and frontend share one source of truth
- Add server/services/team-count-guard.ts: countUserOrgs + checkTeamLimit (consults licensing state)
- Hook into better-auth organization.beforeCreateOrganization to throw 402 when limit is reached
- Update /teams UI: isAtLimit guard (with loading protection) shows ProBadge on "New Team" button and opens UpgradeHint dialog instead of create dialog when user is at limit

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 08:59:01 -04:00
Jasper VanandBob c86c7731c1 feat: v2.6 Z7 white-label branding (logo, favicon, wordmark, hide footer) (#346)
* feat: v2.6 Z7 white-label branding — logo, favicon, wordmark, hide footer

Implements the white-label branding feature gated by `white_label` Pro entitlement:

Backend:
- GET /api/branding (public) — returns BrandingConfig from systemOptions
- PUT /api/admin/branding (admin + requireFeature) — multipart upload for
  logo/favicon files + wordmark_text/hide_powered_by fields
- DELETE /api/admin/branding/:field (admin + requireFeature) — resets one field
- server/services/branding.ts — S3 upload to _system/branding/, atomic
  upsertOption via onConflictDoUpdate

Frontend:
- BrandingProvider wraps the app; fetches branding on boot, applies favicon via
  <link>, sets --site-wordmark CSS var
- AppSidebar uses branding context: custom logo src, wordmark text, and
  "Powered by ZPan" footer (hidden when hide_powered_by is set)
- Admin /branding page: shows UpgradeHint for non-Pro, upload form with live
  preview panel for Pro users
- BrandingConfig and BrandingField types moved to shared/types/ per convention

Tests: integration tests for all auth guards (401/403/402), validation (415/422),
and happy paths; frontend api.test.ts covers getBranding, saveBranding,
resetBrandingField.

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

* test: add S3 upload and validation coverage for branding routes

Add integration tests for logo/favicon file upload, MIME validation,
size limits, missing storage, and seeded branding values to bring
patch coverage above codecov threshold.

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

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 08:50:53 -04:00
Jasper VanandBob 1cbe92640c feat: gate open registration behind Pro feature (Z8) (#347)
- Add server/services/signup-mode-guard.ts: getEffectiveSignupMode()
  applies Pro check when stored mode is 'open'; non-Pro falls back to
  invite-only so downgraded instances stay secure
- Update server/auth.ts: replace internal getSignupMode() with
  getEffectiveSignupMode() from new service
- Update server/routes/system.ts: PUT auth_signup_mode=open returns 402
  feature_not_available when open_registration feature is absent
- Update RegistrationModeSection: 'open' radio disabled with ProBadge
  for non-Pro; clicking it opens UpgradeHint dialog instead of saving
- Add integration tests: 6-combination matrix (3 modes × 2 plans) plus
  admin API guard tests
- Update auth.integration.test.ts: split open-mode tests for Pro/non-Pro

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 08:40:28 -04:00
Jasper VanandBob 29102e623d feat: v2.6 Z6 — 6h background entitlement refresh (#345)
- Add server/services/licensing-refresh-runner.ts: shared runner with
  5-min dedup guard, structured INFO logs, and no-op for unbound state
- Add workers/scheduled.ts + export scheduled() in workers/bootstrap.ts
  for CF Workers cron (every 6 hours)
- Add [triggers] crons = ["0 */6 * * *"] to wrangler.toml
- Add setInterval refresh on boot in server/entry-node.ts with
  "licensing.refresh.scheduler.started interval=6h" log
- Add POST /api/licensing/refresh-cron?secret=... public endpoint
  (timing-safe secret comparison) for non-CF platforms
- Extract ZPAN_CLOUD_URL_DEFAULT to shared/constants.ts, replacing
  four duplicated literals
- Document REFRESH_CRON_SECRET + scheduler setup in all 5 non-CF
  deploy guides (vercel, netlify, aws-lambda, azure-functions, cloud-run)

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 08:22:10 -04:00
Jasper Van d24c388150 feat: add Settings → Billing page with pairing modal and bound/unbound states (#344)
- Add Billing tab to settings route layout
- Create BillingPage with unbound (comparison table + Connect CTA) and bound states
- Create PairingModal with device-code flow: calls POST /api/licensing/pair,
  shows code + pairing_url, polls every 5s, auto-closes on approval
- Create ComparisonTable showing Community vs Pro feature list from v2.6 spec
- Create BoundStatusCard showing email, plan, features, expiry, refresh controls,
  and disconnect dialog
- Register billing route in routeTree.gen.ts
- Add i18n keys for en and zh locales
- Add billing.test.tsx with pure logic tests for all components

Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
2026-04-24 08:10:52 -04:00
Jasper VanandBob c05bfc7c20 feat: v2.6 Z3 — ProBadge, UpgradeHint, useEntitlement frontend primitives (#342)
Backend (has-feature, requireFeature, /api/licensing/status) already shipped in Z2/Z4.
This PR contributes the frontend feature-gate primitives only:

- src/components/ProBadge.tsx: "Pro" pill badge with brand color #1A73E8
- src/components/UpgradeHint.tsx: block upsell card; CTA "Connect to Cloud" (unbound)
  or "Manage on Cloud" (bound but feature missing); links to /settings/billing
- src/hooks/useEntitlement.ts: React Query hook wrapping getLicensingStatus;
  query key ['licensing','status'], 60s stale/refetch, exposes hasFeature(name)
- src/components/ProBadge.test.tsx: 4 render tests (text, brand color, slot, className)
- src/components/UpgradeHint.test.tsx: 7 render tests (headline, CTA per bound state,
  link target, feature label, slot attribute)

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 07:49:17 -04:00
Jasper VanandBob 86fab7716b feat(licensing): cloud client + binding API (pair, poll, refresh, disconnect) (#343)
* feat(licensing): cloud client + binding API (pair, poll, refresh, disconnect)

- server/licensing/public-keys.ts — DEV PASERK placeholder (production key lands via C5 cross-repo PR)
- server/licensing/verify.ts — verifyCertificate() using paseto-ts/v4, returns LicenseEntitlement | null
- server/licensing/entitlement.ts — loadEntitlement() with 60s in-process memoization + invalidateEntitlementCache()
- server/licensing/has-feature.ts — loadBindingState() + hasFeature() pure sync check
- server/licensing/instance-id.ts — getOrCreateInstanceId() lazily persisted in systemOptions under 'instance_id'
- server/licensing/refresh.ts — performRefresh(): calls cloud, verifies cert, rotates DB row; handles CloudUnboundError (clear binding) and CloudNetworkError (update error log, keep cached cert)
- server/services/licensing-cloud.ts — createPairing(), pollPairing(), refreshEntitlement() with 10s timeout; CloudUnboundError + CloudNetworkError for typed error handling
- server/routes/licensing.ts — public GET /api/licensing/status (no auth required)
- server/routes/licensing-admin.ts — admin-only: POST /pair, GET /pair/:code/poll, POST /refresh, DELETE /binding
- server/middleware/require-feature.ts — requireFeature(name) middleware, returns 402 when feature missing
- server/app.ts — mount /api/licensing (public) + /api/licensing (admin) + export route types
- src/lib/rpc.ts — licensingApi + licensingAdminApi RPC clients
- src/lib/api.ts — getLicensingStatus(), connectCloud(), pollPairing(), refreshLicense(), disconnectCloud()
- src/lib/api.test.ts — 17 new tests covering all 5 new api.ts wrappers
- shared/types/licensing.ts — update LicenseEntitlement.issued_at/expires_at to string (ISO-8601)
- paseto-ts dependency added for PASETO v4 public verification

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

* fix(licensing): fix biome lint issues — remove unused imports, format test file

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

* fix(licensing): apply biome format fixes to refresh, require-feature, licensing-cloud

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

* test(licensing): add unit and integration tests for all new licensing modules

- server/licensing/public-keys.test.ts — PUBLIC_KEYS format validation
- server/licensing/verify.test.ts — verifyCertificate: valid cert, invalid sig, expired, instance mismatch, key rotation
- server/licensing/has-feature.test.ts — hasFeature: null/unbound/empty/expired/future states
- server/services/licensing-cloud.test.ts — createPairing, pollPairing, refreshEntitlement: success, 401 Unbound, network error
- server/routes/licensing.integration.test.ts — GET /api/licensing/status: unbound, bound+cert, bound+no-cert, public access
- server/routes/licensing-admin.integration.test.ts — auth guards (401/403) + POST /pair, GET /pair/:code/poll, POST /refresh, DELETE /binding
- server/test/setup.ts — add license_binding table to in-memory schema

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

* test(licensing): add entitlement cache and refresh orchestration unit tests

- entitlement.test.ts — loadEntitlement: no row, no cert, valid PASETO cert, expired cert; invalidateEntitlementCache: re-reads from DB after invalidation
- refresh.test.ts — performRefresh: no-op when unbound, rotates token (pre-C5 object), rotates token (PASETO string), clears binding on 401 Unbound, updates error log on network failure

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

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 07:37:30 -04:00