Skip to content

Commit 2879bc6

Browse files
authored
Merge pull request #452 from Miablo/mirror-name-input-validation
feat: add input validation for mirror names
2 parents fdcb497 + b11a360 commit 2879bc6

4 files changed

Lines changed: 76 additions & 29 deletions

File tree

src/app/components/dialog/CreateMirrorDialog.tsx

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import {
88
TextInput,
99
} from '@primer/react'
1010
import { Dialog } from '@primer/react/drafts'
11+
import { mirrorNameSchema } from 'server/repos/schema'
1112

1213
import { useState } from 'react'
1314

15+
const DEFAULT_REPO_NAME = 'repository-name'
16+
1417
interface CreateMirrorDialogProps {
1518
orgLogin: string
1619
forkParentOwnerLogin: string
@@ -29,12 +32,19 @@ export const CreateMirrorDialog = ({
2932
createMirror,
3033
}: CreateMirrorDialogProps) => {
3134
// set to default value of 'repository-name' for display purposes
32-
const [repoName, setRepoName] = useState('repository-name')
35+
const [repoName, setRepoName] = useState(DEFAULT_REPO_NAME)
3336

3437
if (!isOpen) {
3538
return null
3639
}
3740

41+
const hasUserInput = repoName !== DEFAULT_REPO_NAME && repoName !== ''
42+
const validation = mirrorNameSchema.safeParse(repoName)
43+
const validationError =
44+
hasUserInput && !validation.success
45+
? validation.error.issues[0].message
46+
: null
47+
3848
return (
3949
<Dialog
4050
title="Create a new mirror"
@@ -44,22 +54,22 @@ export const CreateMirrorDialog = ({
4454
content: 'Cancel',
4555
onClick: () => {
4656
closeDialog()
47-
setRepoName('repository-name')
57+
setRepoName(DEFAULT_REPO_NAME)
4858
},
4959
},
5060
{
5161
content: 'Confirm',
5262
variant: 'primary',
5363
onClick: () => {
5464
createMirror({ repoName, branchName: repoName })
55-
setRepoName('repository-name')
65+
setRepoName(DEFAULT_REPO_NAME)
5666
},
57-
disabled: repoName === 'repository-name' || repoName === '',
67+
disabled: !hasUserInput || !validation.success,
5868
},
5969
]}
6070
onClose={() => {
6171
closeDialog()
62-
setRepoName('repository-name')
72+
setRepoName(DEFAULT_REPO_NAME)
6373
}}
6474
width="large"
6575
>
@@ -71,17 +81,24 @@ export const CreateMirrorDialog = ({
7181
block
7282
placeholder="e.g. repository-name"
7383
maxLength={100}
84+
validationStatus={validationError ? 'error' : undefined}
7485
/>
75-
<FormControl.Caption>
76-
This is a private mirror of{' '}
77-
<Link
78-
href={`https://github.com/${forkParentOwnerLogin}/${forkParentName}`}
79-
target="_blank"
80-
rel="noreferrer noopener"
81-
>
82-
{forkParentOwnerLogin}/{forkParentName}
83-
</Link>
84-
</FormControl.Caption>
86+
{validationError ? (
87+
<FormControl.Validation variant="error">
88+
{validationError}
89+
</FormControl.Validation>
90+
) : (
91+
<FormControl.Caption>
92+
This is a private mirror of{' '}
93+
<Link
94+
href={`https://github.com/${forkParentOwnerLogin}/${forkParentName}`}
95+
target="_blank"
96+
rel="noreferrer noopener"
97+
>
98+
{forkParentOwnerLogin}/{forkParentName}
99+
</Link>
100+
</FormControl.Caption>
101+
)}
85102
</FormControl>
86103
<FormControl>
87104
<FormControl.Label>Mirror location</FormControl.Label>

src/app/components/dialog/EditMirrorDialog.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
TextInput,
99
} from '@primer/react'
1010
import { Dialog } from '@primer/react/drafts'
11+
import { mirrorNameSchema } from 'server/repos/schema'
1112

1213
import { useEffect, useState } from 'react'
1314

@@ -47,6 +48,13 @@ export const EditMirrorDialog = ({
4748
return null
4849
}
4950

51+
const hasUserInput = newMirrorName !== mirrorName && newMirrorName !== ''
52+
const validation = mirrorNameSchema.safeParse(newMirrorName)
53+
const validationError =
54+
hasUserInput && !validation.success
55+
? validation.error.issues[0].message
56+
: null
57+
5058
return (
5159
<Dialog
5260
title="Edit mirror"
@@ -70,7 +78,7 @@ export const EditMirrorDialog = ({
7078
})
7179
setNewMirrorName(mirrorName)
7280
},
73-
disabled: newMirrorName === mirrorName || newMirrorName === '',
81+
disabled: !hasUserInput || !validation.success,
7482
},
7583
]}
7684
onClose={() => {
@@ -87,17 +95,24 @@ export const EditMirrorDialog = ({
8795
block
8896
placeholder={mirrorName}
8997
maxLength={100}
98+
validationStatus={validationError ? 'error' : undefined}
9099
/>
91-
<FormControl.Caption>
92-
This is a private mirror of{' '}
93-
<Link
94-
href={`https://github.com/${forkParentOwnerLogin}/${forkParentName}`}
95-
target="_blank"
96-
rel="noreferrer noopener"
97-
>
98-
{forkParentOwnerLogin}/{forkParentName}
99-
</Link>
100-
</FormControl.Caption>
100+
{validationError ? (
101+
<FormControl.Validation variant="error">
102+
{validationError}
103+
</FormControl.Validation>
104+
) : (
105+
<FormControl.Caption>
106+
This is a private mirror of{' '}
107+
<Link
108+
href={`https://github.com/${forkParentOwnerLogin}/${forkParentName}`}
109+
target="_blank"
110+
rel="noreferrer noopener"
111+
>
112+
{forkParentOwnerLogin}/{forkParentName}
113+
</Link>
114+
</FormControl.Caption>
115+
)}
101116
</FormControl>
102117
<FormControl>
103118
<FormControl.Label>Mirror location</FormControl.Label>

src/server/repos/schema.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import { z } from 'zod'
22

3+
export const mirrorNameSchema = z
4+
.string()
5+
.min(1, 'Mirror name is required')
6+
.max(100, 'Mirror name cannot exceed 100 characters')
7+
.regex(
8+
/^[A-Za-z0-9._-]+$/,
9+
'Only letters, numbers, hyphens, underscores, and periods are allowed',
10+
)
11+
.refine((name) => name !== '.' && name !== '..', {
12+
message: 'Mirror name cannot be "." or ".."',
13+
})
14+
.refine((name) => !name.toLowerCase().endsWith('.git'), {
15+
message: 'Mirror name cannot end with ".git"',
16+
})
17+
318
export const CreateMirrorSchema = z.object({
419
orgId: z.string(),
520
forkRepoOwner: z.string(),
621
forkRepoName: z.string(),
722
forkId: z.string(),
8-
newRepoName: z.string().max(100),
23+
newRepoName: mirrorNameSchema,
924
newBranchName: z.string(),
1025
})
1126

@@ -17,7 +32,7 @@ export const ListMirrorsSchema = z.object({
1732
export const EditMirrorSchema = z.object({
1833
orgId: z.string(),
1934
mirrorName: z.string(),
20-
newMirrorName: z.string().max(100),
35+
newMirrorName: mirrorNameSchema,
2136
})
2237

2338
export const DeleteMirrorSchema = z.object({

test/server/repos.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ describe('Repos router', () => {
354354
})
355355
.catch((error) => {
356356
expect(error.message).toMatch(
357-
/String must contain at most 100 character\(s\)/,
357+
/Mirror name cannot exceed 100 characters/,
358358
)
359359
})
360360
})

0 commit comments

Comments
 (0)