Skip to content

Commit cfcfa9c

Browse files
authored
Improve validation (#48)
* add stricter username and slug handling * add better validation patterns * add color and icon validation * refactor validation files for sidebar items * refactor more validation * move more validation to shared utility * address comments * fixes * fix outdated mock * address comments * fix fallow CI * fix * fix font preload * address changes, fixes * fixes
1 parent 721950e commit cfcfa9c

157 files changed

Lines changed: 3701 additions & 1779 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ concurrency:
1111

1212
env:
1313
NODE_VERSION: 22
14+
VITE_SITE_URL: 'http://localhost:3000'
1415

1516
jobs:
1617
setup:
@@ -39,26 +40,18 @@ jobs:
3940
- run: vp run lint:convex # TODO: remove this once plugins exist for oxlint
4041

4142
fallow:
43+
if: github.event_name == 'pull_request'
4244
name: Fallow
4345
runs-on: ubuntu-latest
44-
needs: [setup]
4546
steps:
4647
- uses: actions/checkout@v4
4748
with:
4849
fetch-depth: 0
49-
- uses: voidzero-dev/setup-vp@v1
50+
- uses: fallow-rs/fallow@v2
5051
with:
51-
node-version: ${{ env.NODE_VERSION }}
52-
cache: true
53-
- run: vp install --frozen-lockfile
54-
- name: Run Fallow
55-
continue-on-error: true
56-
run: |
57-
if [ "${{ github.event_name }}" = "pull_request" ]; then
58-
vp run fallow audit --base origin/main --format annotations --format json > fallow-report.json
59-
else
60-
vp run fallow --format json > fallow-report.json
61-
fi
52+
annotations: true
53+
fail-on-issues: false
54+
format: sarif
6255

6356
react-compiler:
6457
name: React Compiler Health
@@ -90,7 +83,6 @@ jobs:
9083
if: github.event_name == 'pull_request'
9184
run: npx -y react-doctor@latest . --verbose --fail-on error --diff=${{ github.event.pull_request.base.ref }}
9285
- name: Run React Doctor (main)
93-
id: doctor
9486
if: github.ref == 'refs/heads/main'
9587
run: npx -y react-doctor@latest . --verbose --fail-on error
9688

.vscode/settings.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"editor.defaultFormatter": "oxc.oxc-vscode",
33
"oxc.fmt.configPath": "./vite.config.ts",
4-
"editor.formatOnSave": true,
4+
"editor.formatOnSave": false,
55
"editor.formatOnSaveMode": "file",
66
"editor.codeActionsOnSave": {
7-
"source.fixAll.oxc": "explicit",
8-
"source.fixAll.eslint": "explicit"
7+
"source.fixAll.oxc": "never",
8+
"source.fixAll.eslint": "never"
99
},
1010

1111
"eslint.enable": true,

convex/_generated/api.d.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,12 @@ import type * as canvases_functions_updateCanvas from "../canvases/functions/upd
6666
import type * as canvases_mutations from "../canvases/mutations.js";
6767
import type * as canvases_triggers from "../canvases/triggers.js";
6868
import type * as canvases_types from "../canvases/types.js";
69+
import type * as common_async from "../common/async.js";
6970
import type * as common_constants from "../common/constants.js";
7071
import type * as common_logger from "../common/logger.js";
7172
import type * as common_slug from "../common/slug.js";
7273
import type * as common_types from "../common/types.js";
74+
import type * as common_zod from "../common/zod.js";
7375
import type * as crons from "../crons.js";
7476
import type * as documentSnapshots_functions_createSnapshot from "../documentSnapshots/functions/createSnapshot.js";
7577
import type * as documentSnapshots_functions_getSnapshot from "../documentSnapshots/functions/getSnapshot.js";
@@ -155,7 +157,6 @@ import type * as sessions_functions_updateSession from "../sessions/functions/up
155157
import type * as sessions_mutations from "../sessions/mutations.js";
156158
import type * as sessions_queries from "../sessions/queries.js";
157159
import type * as sessions_types from "../sessions/types.js";
158-
import type * as sidebarItems_createParentTarget from "../sidebarItems/createParentTarget.js";
159160
import type * as sidebarItems_functions_claimPreviewGeneration from "../sidebarItems/functions/claimPreviewGeneration.js";
160161
import type * as sidebarItems_functions_collectDescendants from "../sidebarItems/functions/collectDescendants.js";
161162
import type * as sidebarItems_functions_defaultItemName from "../sidebarItems/functions/defaultItemName.js";
@@ -179,12 +180,18 @@ import type * as sidebarItems_schema_anySidebarItemValidator from "../sidebarIte
179180
import type * as sidebarItems_schema_anySidebarItemWithContentValidator from "../sidebarItems/schema/anySidebarItemWithContentValidator.js";
180181
import type * as sidebarItems_schema_sidebarItemsTable from "../sidebarItems/schema/sidebarItemsTable.js";
181182
import type * as sidebarItems_schema_validators from "../sidebarItems/schema/validators.js";
182-
import type * as sidebarItems_sharedValidation from "../sidebarItems/sharedValidation.js";
183183
import type * as sidebarItems_triggerTypes from "../sidebarItems/triggerTypes.js";
184184
import type * as sidebarItems_triggers from "../sidebarItems/triggers.js";
185185
import type * as sidebarItems_types_baseTypes from "../sidebarItems/types/baseTypes.js";
186186
import type * as sidebarItems_types_types from "../sidebarItems/types/types.js";
187-
import type * as sidebarItems_validation from "../sidebarItems/validation.js";
187+
import type * as sidebarItems_validation_access from "../sidebarItems/validation/access.js";
188+
import type * as sidebarItems_validation_color from "../sidebarItems/validation/color.js";
189+
import type * as sidebarItems_validation_icon from "../sidebarItems/validation/icon.js";
190+
import type * as sidebarItems_validation_move from "../sidebarItems/validation/move.js";
191+
import type * as sidebarItems_validation_name from "../sidebarItems/validation/name.js";
192+
import type * as sidebarItems_validation_orchestration from "../sidebarItems/validation/orchestration.js";
193+
import type * as sidebarItems_validation_parent from "../sidebarItems/validation/parent.js";
194+
import type * as sidebarItems_validation_slug from "../sidebarItems/validation/slug.js";
188195
import type * as sidebarShares_functions_getSidebarItemShares from "../sidebarShares/functions/getSidebarItemShares.js";
189196
import type * as sidebarShares_functions_getSidebarItemSharesForItem from "../sidebarShares/functions/getSidebarItemSharesForItem.js";
190197
import type * as sidebarShares_functions_getSidebarItemWithShares from "../sidebarShares/functions/getSidebarItemWithShares.js";
@@ -295,10 +302,12 @@ declare const fullApi: ApiFromModules<{
295302
"canvases/mutations": typeof canvases_mutations;
296303
"canvases/triggers": typeof canvases_triggers;
297304
"canvases/types": typeof canvases_types;
305+
"common/async": typeof common_async;
298306
"common/constants": typeof common_constants;
299307
"common/logger": typeof common_logger;
300308
"common/slug": typeof common_slug;
301309
"common/types": typeof common_types;
310+
"common/zod": typeof common_zod;
302311
crons: typeof crons;
303312
"documentSnapshots/functions/createSnapshot": typeof documentSnapshots_functions_createSnapshot;
304313
"documentSnapshots/functions/getSnapshot": typeof documentSnapshots_functions_getSnapshot;
@@ -384,7 +393,6 @@ declare const fullApi: ApiFromModules<{
384393
"sessions/mutations": typeof sessions_mutations;
385394
"sessions/queries": typeof sessions_queries;
386395
"sessions/types": typeof sessions_types;
387-
"sidebarItems/createParentTarget": typeof sidebarItems_createParentTarget;
388396
"sidebarItems/functions/claimPreviewGeneration": typeof sidebarItems_functions_claimPreviewGeneration;
389397
"sidebarItems/functions/collectDescendants": typeof sidebarItems_functions_collectDescendants;
390398
"sidebarItems/functions/defaultItemName": typeof sidebarItems_functions_defaultItemName;
@@ -408,12 +416,18 @@ declare const fullApi: ApiFromModules<{
408416
"sidebarItems/schema/anySidebarItemWithContentValidator": typeof sidebarItems_schema_anySidebarItemWithContentValidator;
409417
"sidebarItems/schema/sidebarItemsTable": typeof sidebarItems_schema_sidebarItemsTable;
410418
"sidebarItems/schema/validators": typeof sidebarItems_schema_validators;
411-
"sidebarItems/sharedValidation": typeof sidebarItems_sharedValidation;
412419
"sidebarItems/triggerTypes": typeof sidebarItems_triggerTypes;
413420
"sidebarItems/triggers": typeof sidebarItems_triggers;
414421
"sidebarItems/types/baseTypes": typeof sidebarItems_types_baseTypes;
415422
"sidebarItems/types/types": typeof sidebarItems_types_types;
416-
"sidebarItems/validation": typeof sidebarItems_validation;
423+
"sidebarItems/validation/access": typeof sidebarItems_validation_access;
424+
"sidebarItems/validation/color": typeof sidebarItems_validation_color;
425+
"sidebarItems/validation/icon": typeof sidebarItems_validation_icon;
426+
"sidebarItems/validation/move": typeof sidebarItems_validation_move;
427+
"sidebarItems/validation/name": typeof sidebarItems_validation_name;
428+
"sidebarItems/validation/orchestration": typeof sidebarItems_validation_orchestration;
429+
"sidebarItems/validation/parent": typeof sidebarItems_validation_parent;
430+
"sidebarItems/validation/slug": typeof sidebarItems_validation_slug;
417431
"sidebarShares/functions/getSidebarItemShares": typeof sidebarShares_functions_getSidebarItemShares;
418432
"sidebarShares/functions/getSidebarItemSharesForItem": typeof sidebarShares_functions_getSidebarItemSharesForItem;
419433
"sidebarShares/functions/getSidebarItemWithShares": typeof sidebarShares_functions_getSidebarItemWithShares;

convex/_test/factories.helper.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { CAMPAIGN_MEMBER_ROLE, CAMPAIGN_MEMBER_STATUS } from '../campaigns/types'
2+
import type { SidebarItemColor } from '../sidebarItems/validation/color'
3+
import type { SidebarItemIconName } from '../sidebarItems/validation/icon'
24
import { SIDEBAR_ITEM_LOCATION, SIDEBAR_ITEM_TYPES } from '../sidebarItems/types/baseTypes'
35
import { SHARE_STATUS } from '../blockShares/types'
46
import { slugify } from '../common/slug'
7+
import { assertCampaignSlug } from '../campaigns/validation'
8+
import { assertSidebarItemName } from '../sidebarItems/validation/name'
9+
import { assertSidebarItemSlug } from '../sidebarItems/validation/slug'
10+
import { assertUsername } from '../users/validation'
511
import { makeYjsUpdateWithBlocks } from '../yjsSync/__tests__/makeYjsUpdate.helper'
612
import type { TestBlock } from '../yjsSync/__tests__/makeYjsUpdate.helper'
713
import type { TestConvex } from 'convex-test'
@@ -12,6 +18,7 @@ import type { PermissionLevel } from '../permissions/types'
1218
import type { ShareStatus } from '../blockShares/types'
1319
import type { BlockNoteId, BlockProps, BlockType, InlineContent } from '../blocks/types'
1420
import type { CustomBlock } from '../notes/editorSpecs'
21+
import type { SidebarItemName } from '../sidebarItems/validation/name'
1522
import { createHash } from 'crypto'
1623

1724
type T = TestConvex<typeof schema>
@@ -65,16 +72,21 @@ export async function createUserProfile(
6572
}>,
6673
) {
6774
const n = nextId()
75+
const { username, ...rest } = overrides ?? {}
6876
const defaults = {
6977
authUserId: `auth-user-${n}`,
70-
username: `user-${n}`,
78+
username: assertUsername(`user-${n}`),
7179
email: `user-${n}@test.com`,
7280
emailVerified: null,
7381
name: `Test User ${n}`,
7482
profileImage: null,
7583
twoFactorEnabled: null,
7684
}
77-
const data = { ...defaults, ...overrides }
85+
const data = {
86+
...defaults,
87+
...rest,
88+
...(username !== undefined ? { username: assertUsername(username) } : {}),
89+
}
7890
const id = await t.run(async (ctx) => {
7991
return await ctx.db.insert('userProfiles', data)
8092
})
@@ -93,15 +105,21 @@ export async function createCampaignWithDm(
93105
}>,
94106
) {
95107
const n = nextId()
108+
const { slug, ...rest } = overrides ?? {}
96109
const defaults = {
97110
name: `Campaign ${n}`,
98111
description: '',
99112
dmUserId: dmProfile._id,
100-
slug: `campaign-${n}`,
113+
slug: assertCampaignSlug(`campaign-${n}`),
101114
status: 'Active' as const,
102115
currentSessionId: null,
103116
}
104-
const campaignData = { ...defaults, ...overrides, dmUserId: dmProfile._id }
117+
const campaignData = {
118+
...defaults,
119+
...rest,
120+
dmUserId: dmProfile._id,
121+
...(slug !== undefined ? { slug: assertCampaignSlug(slug) } : {}),
122+
}
105123
const campaignId = await t.run(async (ctx) => {
106124
return await ctx.db.insert('campaigns', campaignData)
107125
})
@@ -142,9 +160,9 @@ export async function addPlayerToCampaign(
142160
const sidebarItemBase = (
143161
campaignId: Id<'campaigns'>,
144162
creatorProfileId: Id<'userProfiles'>,
145-
name: string,
163+
name: SidebarItemName,
146164
): {
147-
name: string
165+
name: SidebarItemName
148166
slug: string
149167
campaignId: Id<'campaigns'>
150168
iconName: null
@@ -158,7 +176,7 @@ const sidebarItemBase = (
158176
previewUpdatedAt: null
159177
} & ReturnType<typeof commonFields> => ({
160178
name,
161-
slug: slugify(name),
179+
slug: assertSidebarItemSlug(slugify(name)),
162180
campaignId,
163181
iconName: null,
164182
color: null,
@@ -178,8 +196,8 @@ type CommonSidebarItemOverrides = Partial<{
178196
parentId: Id<'sidebarItems'> | null
179197
allPermissionLevel: PermissionLevel | null
180198
location: SidebarItemLocation
181-
iconName: string | null
182-
color: string | null
199+
iconName: SidebarItemIconName | null
200+
color: SidebarItemColor | null
183201
deletionTime: number | null
184202
deletedBy: Id<'userProfiles'> | null
185203
previewStorageId: Id<'_storage'> | null
@@ -201,15 +219,24 @@ async function insertSidebarItem(
201219
overrides?: CommonSidebarItemOverrides & Record<string, unknown>,
202220
) {
203221
const n = nextId()
204-
const name = overrides?.name ?? `${label} ${n}`
205-
206-
const { inheritShares, imageStorageId, storageId, ...sidebarOverrides } = overrides ?? {}
222+
const name = assertSidebarItemName(overrides?.name ?? `${label} ${n}`)
223+
224+
const {
225+
inheritShares,
226+
imageStorageId,
227+
storageId,
228+
slug,
229+
name: _name,
230+
...sidebarOverrides
231+
} = overrides ?? {}
232+
const validatedSlug =
233+
slug !== undefined ? assertSidebarItemSlug(slug) : assertSidebarItemSlug(slugify(name))
207234

208235
const sharedData = {
209236
...sidebarItemBase(campaignId, creatorProfileId, name),
210237
type,
211238
...sidebarOverrides,
212-
slug: overrides?.slug ?? slugify(name),
239+
slug: validatedSlug,
213240
}
214241

215242
const extensionFields: Record<string, unknown> = {}

convex/auth/functions/onCreateUser.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { findUniqueSlug } from '../../common/slug'
2+
import { parseUsername } from '../../users/validation'
3+
import { USERNAME_MAX_LENGTH } from '../../users/constants'
24
import type { MutationCtx } from '../../_generated/server'
35

46
type AuthUserDoc = {
@@ -17,13 +19,20 @@ export async function onCreateUser(ctx: MutationCtx, user: AuthUserDoc): Promise
1719
user.name?.toLowerCase().replace(/\s+/g, '') ||
1820
`user${String(user._id).slice(-8)}`
1921

20-
const username = await findUniqueSlug(baseUsername, async (slug) => {
21-
const conflict = await ctx.db
22-
.query('userProfiles')
23-
.withIndex('by_username', (q) => q.eq('username', slug))
24-
.unique()
25-
return conflict !== null
26-
})
22+
const username = await findUniqueSlug(
23+
baseUsername,
24+
async (slug) => {
25+
const conflict = await ctx.db
26+
.query('userProfiles')
27+
.withIndex('by_username', (q) => q.eq('username', slug))
28+
.unique()
29+
return conflict !== null
30+
},
31+
{
32+
maxLength: USERNAME_MAX_LENGTH,
33+
isValidCandidate: (slug) => parseUsername(slug) !== null,
34+
},
35+
)
2736

2837
await ctx.db.insert('userProfiles', {
2938
authUserId: String(user._id),

convex/blockShares/functions/setBlocksShareStatus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { asyncMap } from 'convex-helpers'
2-
import { requireItemAccess } from '../../sidebarItems/validation'
2+
import { requireItemAccess } from '../../sidebarItems/validation/access'
33
import { PERMISSION_LEVEL } from '../../permissions/types'
44
import { logEditHistory } from '../../editHistory/log'
55
import { EDIT_HISTORY_ACTION } from '../../editHistory/types'

convex/blockShares/functions/shareBlocks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { asyncMap } from 'convex-helpers'
2-
import { requireItemAccess } from '../../sidebarItems/validation'
2+
import { requireItemAccess } from '../../sidebarItems/validation/access'
33
import { PERMISSION_LEVEL } from '../../permissions/types'
44
import { logEditHistory } from '../../editHistory/log'
55
import { EDIT_HISTORY_ACTION } from '../../editHistory/types'

convex/blockShares/functions/unshareBlocks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { asyncMap } from 'convex-helpers'
2-
import { requireItemAccess } from '../../sidebarItems/validation'
2+
import { requireItemAccess } from '../../sidebarItems/validation/access'
33
import { PERMISSION_LEVEL } from '../../permissions/types'
44
import { logEditHistory } from '../../editHistory/log'
55
import { EDIT_HISTORY_ACTION } from '../../editHistory/types'

convex/blocks/functions/getBlockWithShares.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getCampaignMembers } from '../../campaigns/functions/getCampaignMembers
44
import { getBlockSharesByBlock } from '../../blockShares/functions/getBlockSharesForBlock'
55
import { PERMISSION_LEVEL } from '../../permissions/types'
66
import { SHARE_STATUS } from '../../blockShares/types'
7-
import { checkItemAccess } from '../../sidebarItems/validation'
7+
import { checkItemAccess } from '../../sidebarItems/validation/access'
88
import { findBlockByBlockNoteId } from './findBlockByBlockNoteId'
99
import { getSidebarItem } from '../../sidebarItems/functions/getSidebarItem'
1010
import type { DmQueryCtx } from '../../functions'

convex/blocks/functions/getBlocksWithShares.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CAMPAIGN_MEMBER_ROLE } from '../../campaigns/types'
44
import { getCampaignMembers } from '../../campaigns/functions/getCampaignMembers'
55
import { PERMISSION_LEVEL } from '../../permissions/types'
66
import { SHARE_STATUS } from '../../blockShares/types'
7-
import { checkItemAccess } from '../../sidebarItems/validation'
7+
import { checkItemAccess } from '../../sidebarItems/validation/access'
88
import { findBlockByBlockNoteId } from './findBlockByBlockNoteId'
99
import { getSidebarItem } from '../../sidebarItems/functions/getSidebarItem'
1010
import type { ShareStatus } from '../../blockShares/types'

0 commit comments

Comments
 (0)