openxiangda 2.0.0-alpha.48 → 2.0.0-alpha.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ # OpenXiangda 2.0 Anonymous Public Access
2
+
3
+ Read this reference whenever the requirement includes an external person without
4
+ a platform account, anonymous or guest access, a public form/page, resumable
5
+ submission, duplicate checks, public attachment upload, or reading the visitor's
6
+ own submitted records.
7
+
8
+ ## Decide the boundary first
9
+
10
+ Record these choices before editing:
11
+
12
+ - the one static public route and one Native resource it serves;
13
+ - the exact fields the visitor may write and receive;
14
+ - whether drafts are needed and their bounded inactivity lifetime;
15
+ - whether the page needs `own.list`, `own.read`, or both;
16
+ - every named duplicate validation and its exact field tuple;
17
+ - file fields and their normal resource-level type, count, size and MIME limits;
18
+ - same-browser-only acceptance and the negative browser/device cases.
19
+
20
+ This contract identifies possession of one browser profile, not a natural person.
21
+ Clearing the platform HttpOnly cookie, private browsing, another browser or another
22
+ device creates a new anonymous visitor. Do not promise recovery, merge or
23
+ cross-device continuity. WeChat, DingTalk, SMS/email verification and automatic
24
+ platform-account creation require later, separate identity contracts.
25
+
26
+ ## Declare the one public policy
27
+
28
+ Declare the resource normally, then add one static `surface: 'user'` route and one
29
+ `frontend.publicAccess` policy in `openxiangda.config.ts`:
30
+
31
+ ```ts
32
+ export default defineOpenXiangdaApp({
33
+ // ...app, authz and data declarations...
34
+ frontend: {
35
+ root: 'apps/web',
36
+ routes: [
37
+ {
38
+ code: 'visitor-apply',
39
+ path: '/visitor/apply',
40
+ label: '访客预约',
41
+ surface: 'user',
42
+ },
43
+ ],
44
+ publicAccess: {
45
+ policies: [
46
+ {
47
+ code: 'visitor-apply-public',
48
+ routeCode: 'visitor-apply',
49
+ mode: 'anonymous',
50
+ resourceCode: 'visitor-requests',
51
+ operations: [
52
+ 'draft.read',
53
+ 'draft.update',
54
+ 'validate',
55
+ 'create',
56
+ 'own.list',
57
+ 'own.read',
58
+ ],
59
+ fields: ['visitorName', 'phone', 'visitDate', 'photo'],
60
+ requiredFields: ['visitorName', 'phone', 'visitDate'],
61
+ ownRecordFields: ['visitorName', 'phone', 'visitDate', 'photo'],
62
+ draft: {
63
+ enabled: true,
64
+ inactivityTtlSeconds: 2_592_000,
65
+ maxBytes: 262_144,
66
+ },
67
+ validations: [
68
+ {
69
+ code: 'phone-unused',
70
+ kind: 'duplicate',
71
+ fields: ['phone'],
72
+ result: 'availability',
73
+ },
74
+ ],
75
+ },
76
+ ],
77
+ },
78
+ },
79
+ });
80
+ ```
81
+
82
+ The route must be static and must not also declare `capability` or `access`.
83
+ `fields`, `requiredFields`, `ownRecordFields` and validation fields must reference
84
+ declared fields on the same resource. Public create must cover every writable
85
+ required business field on that Native resource; `check` rejects an incomplete or
86
+ widened declaration.
87
+
88
+ Request only the operations the page uses:
89
+
90
+ - `draft.read` and `draft.update` resume the current browser's active draft;
91
+ - `validate` runs only named availability checks and never returns matching rows;
92
+ - `create` performs the final idempotent submission;
93
+ - `own.list` returns a bounded, server-ordered page of this browser's submitted
94
+ records;
95
+ - `own.read` returns one submitted record only after the server repeats the owner
96
+ and submitted-draft receipt checks.
97
+
98
+ ## Use the generated browser client
99
+
100
+ The template already passes generated `anonymousPublicAccess` to
101
+ `OpenXiangdaApplication`. Bind the route through the normal generated `appRoutes`
102
+ contribution, then use only the dedicated client in that page:
103
+
104
+ ```tsx
105
+ import { createAnonymousPublicClient } from 'openxiangda/react';
106
+
107
+ const publicClient = createAnonymousPublicClient({ routeCode: 'visitor-apply' });
108
+
109
+ const session = await publicClient.bootstrap();
110
+ const draft = session.draft ?? await publicClient.currentDraft();
111
+ const saved = await publicClient.saveDraft(draft.revision, {
112
+ visitorName,
113
+ phone,
114
+ visitDate,
115
+ });
116
+
117
+ const availability = await publicClient.validate('phone-unused', { phone });
118
+ const photo = await publicClient.upload('photo', file);
119
+ const withPhoto = await publicClient.saveDraft(saved.revision, { photo });
120
+ const receipt = await publicClient.submit(
121
+ withPhoto.revision,
122
+ crypto.randomUUID(),
123
+ );
124
+
125
+ const page = await publicClient.listOwn({ pageSize: 20 });
126
+ const detail = await publicClient.getOwn(receipt.recordId);
127
+ ```
128
+
129
+ Call `bootstrap()` before other methods and reuse the same idempotency key when
130
+ retrying one uncertain submission. Draft writes use the returned revision as CAS;
131
+ on conflict, reload the current draft instead of overwriting it. Treat duplicate
132
+ preflight as UI feedback only: the platform repeats validation atomically during
133
+ submission.
134
+
135
+ Never call the general Native Data API from this page, construct `created_by`,
136
+ accept a caller-selected draft id, query arbitrary filters/order/count, expose an
137
+ object-store credential, or use a NestJS action as anonymous CRUD. Never store an
138
+ identity in local storage or derive ownership from IP, user-agent or a browser
139
+ fingerprint. IP may participate only in platform rate limiting.
140
+
141
+ ## Acceptance
142
+
143
+ After `pnpm openxiangda check --json`, verify the exact preproduction route in real
144
+ browsers:
145
+
146
+ 1. a fresh browser can bootstrap, save, reload and resume one draft;
147
+ 2. declared file upload/preview and final submission succeed;
148
+ 3. named validation returns only `available` or `duplicate`, and concurrent final
149
+ duplicate submissions cannot both succeed;
150
+ 4. the same browser can list and open its submitted records;
151
+ 5. another browser gets an empty list and the same record id returns 404;
152
+ 6. clearing the browser credential loses draft/history access as documented;
153
+ 7. undeclared routes, fields, operations, validations and ordinary Data API calls
154
+ remain denied;
155
+ 8. production uses HTTPS and no public object-storage bucket or anonymous upload
156
+ whitelist was introduced.
157
+
158
+ Read [Data and authorization](data-authz.md), [Frontend](frontend.md) and
159
+ [Testing](testing.md) for the surrounding resource, page and delivery checks.
@@ -22,6 +22,14 @@ For each changed resource, verify the real chain:
22
22
  5. stored values and audit records match the declared shape;
23
23
  6. PostgreSQL/RLS remains authoritative, including current-user and multi-role-union cases.
24
24
 
25
+ For `frontend.publicAccess`, add a separate anonymous-browser matrix. Verify draft
26
+ resume, declared file upload, named availability validation, idempotent submission,
27
+ and same-browser `own.list`/`own.read`. In a second browser verify an empty list and
28
+ 404 for the first browser's record id. Clear the first browser credential and
29
+ confirm access is lost as documented. Also prove undeclared routes, fields,
30
+ operations, validations and ordinary Data API calls remain denied. A logged-in
31
+ admin path or a mocked client does not close anonymous public acceptance.
32
+
25
33
  For platform generator changes, add a representative multi-resource fixture
26
34
  (the compatibility corpus fixes 43 resources, complete list/form/detail/mobile
27
35
  surfaces, Perspective/AuthZ/Workflow/Event and exactly 161 producers), assert
@@ -5,6 +5,7 @@
5
5
  - Use Vite, React Router, Refine Core and Ant Design. Do not add Umi, ProComponents or another admin shell.
6
6
  - Bind every generated `appRoutes` entry to its local page with `defineApplicationContributions`; desktop `admin` routes stay inside the platform Shell, while `user` routes render without an admin Shell for independent mobile/user experiences. Generated resource CRUD routes are compiler-owned under `/admin/resources/<resourceCode>...` and `/m/admin/resources/<resourceCode>...`; never recreate root resource paths, aliases or redirects. Explicit routes that have the same canonical shape as another explicit or generated route fail compilation, even when dynamic parameter names differ. Use only the typed `toolbar`, `row` and `detail` resource slots for generated resource actions. Declare the complete editable admin menu with `defineAdminNavigation` and its page/group helpers; the Shell renders only generated `adminNavigation` references and permissions only filter them. Do not create another router, menu store, layout, identity provider, permission store or copied CRUD page; route/action access uses capability or `allOf`/`anyOf`, while Data/App API and Workflow authorization remain server-owned.
7
7
  - Application login is optional `frontend.authentication`: existing platform users only, registration rejected, exact desktop `/login` and mobile `/m/login`. Bind generated `authenticationSurfaces` to separate PC/mobile renderers through `defineApplicationContributions`; renderers own only brand visuals and call `ApplicationLoginSurfaceProps`. The platform alone owns passwords, providers, OAuth state/callbacks, secure cookies, current identity and authorization. Never put login in protected `appRoutes`, call a v1 auth route, store tokens, create users/roles, or add another Router/identity provider. Keep QA in generated `platformAuthManifest`, outside the protected route denominator.
8
+ - External users without platform accounts use only an exact static `surface: 'user'` route declared through `frontend.publicAccess`. Declare the one resource, bounded field sets, required operations, draft limits and named duplicate validations; consume only generated `anonymousPublicAccess` and `createAnonymousPublicClient`. `own.list`/`own.read` mean records submitted by the same platform-issued HttpOnly browser credential, not a verified natural person, and another browser or cleared cookie intentionally loses access. Never create a guest role/user, call the general Native Data API, expose an anonymous upload path, store identity locally or derive ownership from IP, user-agent or fingerprint.
8
9
  - Declare each resource once in `openxiangda.config.ts` with only `code`, `name`, `fields` and optional `mutationOwner`, generated/list/layout/data-policy settings. Each field owns type, label, required state, Surface flags, reference/file metadata and access. Resource codes are lower kebab-case. Use `native` for direct Data API mutations, `action`, `readonly` or `workflow` for non-Native ownership; never grant or generate Native mutation for a non-Native owner.
9
10
  - Never write resource-level `schemaVersion`, `appCode`, `schema`, `surface`, `capabilities`, `fieldPolicies` or `platform/data` modules. The compiler derives the strict DataResource, CRUD capabilities, Surface and AI Schema. The application manifest still starts with its one top-level `schemaVersion: 3`.
10
11
  - Ordinary list/get/create/update/delete, filters, export and batch operations use the platform Native Data API. Do not create Function CRUD or NestJS wrappers.