Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/app-crm/objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
SalesManagerRole,
FinanceApproverRole,
SalesUserPermissionSet,
GuestPortalProfile,
HighValueOpportunitySharingRule,
RepLeadSharingRule,
WonDealActivitySharingRule,
Expand Down Expand Up @@ -116,7 +117,7 @@ export default defineStack({

// Security
roles: [SalesRepRole, SalesManagerRole, FinanceApproverRole],
permissions: [SalesUserPermissionSet],
permissions: [SalesUserPermissionSet, GuestPortalProfile],
sharingRules: [
HighValueOpportunitySharingRule,
RepLeadSharingRule,
Expand Down
4 changes: 4 additions & 0 deletions examples/app-crm/src/objects/lead.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const Lead = ObjectSchema.create({
status: Field.select({
label: 'Status',
required: true,
// Write-path default so minimal creates (e.g. the public Web-to-Lead form,
// which doesn't expose status) satisfy `required` — the option `default`
// below is only a UI preselect.
defaultValue: 'new',
options: [
{ label: 'New', value: 'new', default: true, color: '#94A3B8' },
{ label: 'Contacted', value: 'contacted', color: '#3B82F6' },
Expand Down
17 changes: 5 additions & 12 deletions examples/app-crm/src/portals/customer.portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,11 @@ export const CustomerPortal: Portal = {
target: '_blank',
},
],
anonymousEntry: {
routes: [
{
path: '/contact',
actionRef: 'crm_lead.create',
rateLimit: { rule: '5/hour/ip', scope: 'ip' },
captcha: true,
bindIdentityFromField: 'email',
},
],
defaultRateLimit: { rule: '100/day/ip', scope: 'ip' },
},
// NOTE: `anonymousEntry` is a spec property with no runtime consumer yet — the
// routes here would never mount (verified: 404). For a WORKING public capture
// form, see the `web_to_lead` form view in `views/lead.view.ts`
// (`sharing.allowAnonymous` → live `/api/v1/forms/contact-us` endpoint). Re-add
// `anonymousEntry` here only once the runtime mounts it.
defaultRoute: {
viewRef: 'crm_activity.activity_grid',
},
Expand Down
1 change: 1 addition & 0 deletions examples/app-crm/src/security/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
SalesManagerRole,
FinanceApproverRole,
SalesUserPermissionSet,
GuestPortalProfile,
} from './sales-roles.js';

export {
Expand Down
18 changes: 18 additions & 0 deletions examples/app-crm/src/security/sales-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,21 @@ export const SalesUserPermissionSet: Security.PermissionSet = {
crm_activity: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
},
};

/**
* Guest profile for the public Web-to-Lead form (lead.view.ts `web_to_lead`).
*
* Applied to anonymous (unauthenticated) visitors who POST the public form. The
* anonymous permission path checks the FULL object name, so this MUST key
* `crm_lead` (not a short `lead`). INSERT-only — guests can never read, edit, or
* delete any record.
*/
export const GuestPortalProfile: Security.PermissionSet = {
name: 'guest_portal',
label: 'Guest (Public Forms)',
description: 'Anonymous Web-to-Lead submitters — INSERT-only on crm_lead.',
isProfile: true,
objects: {
crm_lead: { allowRead: false, allowCreate: true, allowEdit: false, allowDelete: false },
},
};
32 changes: 32 additions & 0 deletions examples/app-crm/src/views/lead.view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ export const LeadViews = defineView({
},
},
formViews: {
/**
* PUBLIC / ANONYMOUS — Web-to-Lead.
*
* Hosted at GET/POST `/api/v1/forms/contact-us` (+ `/submit`). An
* unauthenticated visitor — or an `EmbeddableForm` iframe on a marketing
* site — submits it to create a `crm_lead`. `sharing.allowAnonymous` opens
* the public endpoint; the `guest_portal` profile (INSERT-only on crm_lead)
* authorizes the write. The lead object's own defaults/hooks stamp internal
* fields (status, owner, score).
*/
web_to_lead: {
type: 'simple',
data: { provider: 'object', object: 'crm_lead' },
sections: [
{
label: 'Contact us',
columns: 1,
fields: [
{ field: 'name', required: true },
{ field: 'company' },
{ field: 'email', required: true },
{ field: 'phone' },
{ field: 'title' },
],
},
],
sharing: {
enabled: true,
allowAnonymous: true,
publicLink: '/forms/contact-us',
},
},
default: {
type: 'simple',
sections: [
Expand Down
Loading