Skip to content

Commit bd495e1

Browse files
committed
fix
1 parent 00bc53e commit bd495e1

27 files changed

Lines changed: 1244 additions & 32 deletions

examples/app-crm/dist/objectstack.json

Lines changed: 201 additions & 10 deletions
Large diffs are not rendered by default.

examples/app-crm/src/objects/account.object.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ export const Account = ObjectSchema.create({
138138
group: 'branding',
139139
}),
140140

141+
// Company logo (uploaded image)
142+
logo: Field.image({
143+
label: 'Company Logo',
144+
group: 'branding',
145+
crop: true,
146+
}),
147+
141148
// Date field
142149
last_activity_date: Field.date({
143150
label: 'Last Activity Date',

examples/app-crm/src/objects/case.hook.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ const caseValidation: Hook = {
2727
description: 'Apply SLA defaults for critical cases.',
2828
handler: async (ctx: HookContext) => {
2929
const { input } = ctx;
30+
31+
// Web-to-Case: stamp safe defaults for anonymous submissions and strip
32+
// any client-supplied fields that should be controlled server-side.
33+
// Guests are unauthenticated, identified by the absence of ctx.user.id.
34+
// Only applies on INSERT (no `ctx.previous`).
35+
const isGuestSubmission = !ctx.previous && !ctx.user?.id;
36+
if (isGuestSubmission) {
37+
if (!input.origin) input.origin = 'web';
38+
if (!input.status) input.status = 'new';
39+
if (!input.priority) input.priority = 'medium';
40+
delete (input as Record<string, unknown>).owner;
41+
delete (input as Record<string, unknown>).is_escalated;
42+
delete (input as Record<string, unknown>).is_closed;
43+
delete (input as Record<string, unknown>).internal_notes;
44+
delete (input as Record<string, unknown>).resolution;
45+
}
46+
3047
const priority =
3148
(typeof input.priority === 'string' && input.priority) ||
3249
(typeof ctx.previous?.priority === 'string' && (ctx.previous.priority as string)) ||

examples/app-crm/src/objects/lead.hook.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,25 @@ const leadHook: Hook = {
7676
}
7777

7878
if (event === 'beforeInsert') {
79+
// Web-to-Lead: anonymous submissions from the public form get
80+
// sensible defaults stamped server-side so they cannot be spoofed
81+
// by the client. Guests are unauthenticated, so we identify them
82+
// by the absence of `ctx.user?.id`. (The `guest_portal` profile
83+
// already restricts them to INSERT-only on `lead`.)
84+
const isGuestSubmission = !ctx.user?.id;
85+
if (isGuestSubmission) {
86+
if (!input.lead_source) input.lead_source = 'web';
87+
if (!input.status) input.status = 'new';
88+
// Never trust client-supplied conversion / ownership fields on
89+
// a public form — strip them defensively.
90+
delete (input as Record<string, unknown>).is_converted;
91+
delete (input as Record<string, unknown>).converted_account;
92+
delete (input as Record<string, unknown>).converted_contact;
93+
delete (input as Record<string, unknown>).converted_opportunity;
94+
delete (input as Record<string, unknown>).converted_date;
95+
delete (input as Record<string, unknown>).owner;
96+
}
97+
7998
if (typeof input.rating !== 'number') {
8099
input.rating = computeRating(input);
81100
}

examples/app-crm/src/objects/product.object.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,14 @@ export const Product = ObjectSchema.create({
112112
}),
113113

114114
// Images and Assets
115-
image_url: Field.url({
115+
image: Field.image({
116116
label: 'Product Image',
117+
crop: true,
117118
}),
118-
119-
datasheet_url: Field.url({
120-
label: 'Datasheet URL',
119+
120+
datasheet: Field.file({
121+
label: 'Datasheet',
122+
accept: ['application/pdf', '.pdf'],
121123
}),
122124

123125
// Tax & billing
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Guest Portal Profile
5+
*
6+
* Permission set applied to anonymous (unauthenticated) visitors who submit
7+
* the public Web-to-Lead and Web-to-Case forms.
8+
*
9+
* IRON-CLAD RULE: guests must NEVER be able to read existing CRM data.
10+
* The only thing they can do is INSERT a new `lead` or `case` row through
11+
* the form views whose `sharing.allowAnonymous = true`.
12+
*
13+
* Any new object added to the CRM stack is implicitly DENIED for guests —
14+
* profile permissions are explicit-allow only.
15+
*/
16+
export const GuestPortalProfile = {
17+
name: 'guest_portal',
18+
label: 'Guest (Public Forms)',
19+
isProfile: true,
20+
description:
21+
'Anonymous visitors submitting public Web-to-Lead / Web-to-Case forms. ' +
22+
'INSERT-only on lead and case; no read/edit/delete on any object.',
23+
objects: {
24+
lead: {
25+
allowCreate: true,
26+
allowRead: false,
27+
allowEdit: false,
28+
allowDelete: false,
29+
viewAllRecords: false,
30+
modifyAllRecords: false,
31+
},
32+
case: {
33+
allowCreate: true,
34+
allowRead: false,
35+
allowEdit: false,
36+
allowDelete: false,
37+
viewAllRecords: false,
38+
modifyAllRecords: false,
39+
},
40+
},
41+
};

examples/app-crm/src/profiles/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/**
44
* Profile Definitions Barrel
55
*/
6+
export { GuestPortalProfile } from './guest-portal.profile';
67
export { MarketingUserProfile } from './marketing-user.profile';
78
export { SalesManagerProfile } from './sales-manager.profile';
89
export { SalesRepProfile } from './sales-rep.profile';

examples/app-crm/src/translations/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const en: TranslationData = {
3939
is_active: { label: 'Active' },
4040
last_activity_date: { label: 'Last Activity Date' },
4141
brand_color: { label: 'Brand Color' },
42+
logo: { label: 'Company Logo' },
4243
tier: {
4344
label: 'Customer Tier',
4445
options: { strategic: 'Strategic', enterprise: 'Enterprise', mid_market: 'Mid-Market', smb: 'SMB' },
@@ -380,6 +381,8 @@ export const en: TranslationData = {
380381
product_manager: { label: 'Product Manager' },
381382
image_url: { label: 'Product Image' },
382383
datasheet_url: { label: 'Datasheet URL' },
384+
image: { label: 'Product Image' },
385+
datasheet: { label: 'Datasheet' },
383386
},
384387
_views: {
385388
all_products: { label: 'All Products' },

examples/app-crm/src/translations/es-ES.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const esES: TranslationData = {
3838
is_active: { label: 'Activo' },
3939
last_activity_date: { label: 'Fecha de Última Actividad' },
4040
brand_color: { label: 'Color de Marca' },
41+
logo: { label: 'Logo de la Empresa' },
4142
},
4243
_views: {
4344
all_accounts: { label: 'Todas las Cuentas', description: 'Lista maestra de cuentas con ingresos e industria' },
@@ -344,6 +345,8 @@ export const esES: TranslationData = {
344345
product_manager: { label: 'Gerente de Producto' },
345346
image_url: { label: 'Imagen de Producto' },
346347
datasheet_url: { label: 'URL de Ficha Técnica' },
348+
image: { label: 'Imagen de Producto' },
349+
datasheet: { label: 'Ficha Técnica' },
347350
},
348351
_views: {
349352
all_products: { label: 'Todos los Productos' },

examples/app-crm/src/translations/ja-JP.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const jaJP: TranslationData = {
3838
is_active: { label: '有効' },
3939
last_activity_date: { label: '最終活動日' },
4040
brand_color: { label: 'ブランドカラー' },
41+
logo: { label: '会社ロゴ' },
4142
},
4243
_views: {
4344
all_accounts: { label: '全取引先', description: '売上と業種を含む取引先の一覧' },
@@ -344,6 +345,8 @@ export const jaJP: TranslationData = {
344345
product_manager: { label: 'プロダクトマネージャー' },
345346
image_url: { label: '製品画像' },
346347
datasheet_url: { label: 'データシートURL' },
348+
image: { label: '製品画像' },
349+
datasheet: { label: 'データシート' },
347350
},
348351
_views: {
349352
all_products: { label: '全製品' },

0 commit comments

Comments
 (0)