mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add template display name (backend) (#4966)
* Rename to nameValidator * Refactor: NameValid * Fix: comment * Define new migration * Include display_name * Update typesGenerated.ts * Update meta * Adjust tests * CLI tests * Fix: audit * Fix: omitempty * site: display_name is optional * unit: TestUsernameValid * entities.ts: add display_name * site: TemplateSettingsPage.test.tsx * Fix: TemplateSettingsForm.tsx * Adjust tests * Add comment to display_name column * Fix: rename * Fix: make * Loosen regexp * Fix: err check * Fix: template name length * Allow for whitespaces * Update migration number
This commit is contained in:
@@ -33,13 +33,14 @@ func init() {
|
||||
}
|
||||
return name
|
||||
})
|
||||
|
||||
nameValidator := func(fl validator.FieldLevel) bool {
|
||||
f := fl.Field().Interface()
|
||||
str, ok := f.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
valid := UsernameValid(str)
|
||||
valid := NameValid(str)
|
||||
return valid == nil
|
||||
}
|
||||
for _, tag := range []string{"username", "template_name", "workspace_name"} {
|
||||
@@ -48,6 +49,20 @@ func init() {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
templateDisplayNameValidator := func(fl validator.FieldLevel) bool {
|
||||
f := fl.Field().Interface()
|
||||
str, ok := f.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
valid := TemplateDisplayNameValid(str)
|
||||
return valid == nil
|
||||
}
|
||||
err := validate.RegisterValidation("template_display_name", templateDisplayNameValidator)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience error functions don't take contexts since their responses are
|
||||
|
||||
@@ -11,10 +11,34 @@ import (
|
||||
var (
|
||||
UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$")
|
||||
usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*")
|
||||
|
||||
templateDisplayName = regexp.MustCompile(`^[^\s](.*[^\s])?$`)
|
||||
)
|
||||
|
||||
// UsernameValid returns whether the input string is a valid username.
|
||||
func UsernameValid(str string) error {
|
||||
// UsernameFrom returns a best-effort username from the provided string.
|
||||
//
|
||||
// It first attempts to validate the incoming string, which will
|
||||
// be returned if it is valid. It then will attempt to extract
|
||||
// the username from an email address. If no success happens during
|
||||
// these steps, a random username will be returned.
|
||||
func UsernameFrom(str string) string {
|
||||
if valid := NameValid(str); valid == nil {
|
||||
return str
|
||||
}
|
||||
emailAt := strings.LastIndex(str, "@")
|
||||
if emailAt >= 0 {
|
||||
str = str[:emailAt]
|
||||
}
|
||||
str = usernameReplace.ReplaceAllString(str, "")
|
||||
if valid := NameValid(str); valid == nil {
|
||||
return str
|
||||
}
|
||||
return strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-")
|
||||
}
|
||||
|
||||
// NameValid returns whether the input string is a valid name.
|
||||
// It is a generic validator for any name (user, workspace, template, etc.).
|
||||
func NameValid(str string) error {
|
||||
if len(str) > 32 {
|
||||
return xerrors.New("must be <= 32 characters")
|
||||
}
|
||||
@@ -28,23 +52,17 @@ func UsernameValid(str string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UsernameFrom returns a best-effort username from the provided string.
|
||||
//
|
||||
// It first attempts to validate the incoming string, which will
|
||||
// be returned if it is valid. It then will attempt to extract
|
||||
// the username from an email address. If no success happens during
|
||||
// these steps, a random username will be returned.
|
||||
func UsernameFrom(str string) string {
|
||||
if valid := UsernameValid(str); valid == nil {
|
||||
return str
|
||||
// TemplateDisplayNameValid returns whether the input string is a valid template display name.
|
||||
func TemplateDisplayNameValid(str string) error {
|
||||
if len(str) == 0 {
|
||||
return nil // empty display_name is correct
|
||||
}
|
||||
emailAt := strings.LastIndex(str, "@")
|
||||
if emailAt >= 0 {
|
||||
str = str[:emailAt]
|
||||
if len(str) > 64 {
|
||||
return xerrors.New("must be <= 64 characters")
|
||||
}
|
||||
str = usernameReplace.ReplaceAllString(str, "")
|
||||
if valid := UsernameValid(str); valid == nil {
|
||||
return str
|
||||
matched := templateDisplayName.MatchString(str)
|
||||
if !matched {
|
||||
return xerrors.New("must be alphanumeric with spaces")
|
||||
}
|
||||
return strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-")
|
||||
return nil
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
)
|
||||
|
||||
func TestValid(t *testing.T) {
|
||||
func TestUsernameValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Tests whether usernames are valid or not.
|
||||
testCases := []struct {
|
||||
@@ -59,7 +59,62 @@ func TestValid(t *testing.T) {
|
||||
testCase := testCase
|
||||
t.Run(testCase.Username, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid := httpapi.UsernameValid(testCase.Username)
|
||||
valid := httpapi.NameValid(testCase.Username)
|
||||
require.Equal(t, testCase.Valid, valid == nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateDisplayNameValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Tests whether display names are valid.
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Valid bool
|
||||
}{
|
||||
{"", true},
|
||||
{"1", true},
|
||||
{"12", true},
|
||||
{"1 2", true},
|
||||
{"123 456", true},
|
||||
{"1234 678901234567890", true},
|
||||
{"<b> </b>", true},
|
||||
{"S", true},
|
||||
{"a1", true},
|
||||
{"a1K2", true},
|
||||
{"!!!!1 ?????", true},
|
||||
{"k\r\rm", true},
|
||||
{"abcdefghijklmnopqrst", true},
|
||||
{"Wow Test", true},
|
||||
{"abcdefghijklmnopqrstu-", true},
|
||||
{"a1b2c3d4e5f6g7h8i9j0k-", true},
|
||||
{"BANANAS_wow", true},
|
||||
{"test--now", true},
|
||||
{"123456789012345678901234567890123", true},
|
||||
{"1234567890123456789012345678901234567890123456789012345678901234", true},
|
||||
{"-a1b2c3d4e5f6g7h8i9j0k", true},
|
||||
|
||||
{" ", false},
|
||||
{"\t", false},
|
||||
{"\r\r", false},
|
||||
{"\t1 ", false},
|
||||
{" a", false},
|
||||
{"\ra ", false},
|
||||
{" 1", false},
|
||||
{"1 ", false},
|
||||
{" aa", false},
|
||||
{"aa\r", false},
|
||||
{" 12", false},
|
||||
{"12 ", false},
|
||||
{"\fa1", false},
|
||||
{"a1\t", false},
|
||||
{"12345678901234567890123456789012345678901234567890123456789012345", false},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid := httpapi.TemplateDisplayNameValid(testCase.Name)
|
||||
require.Equal(t, testCase.Valid, valid == nil)
|
||||
})
|
||||
}
|
||||
@@ -92,7 +147,7 @@ func TestFrom(t *testing.T) {
|
||||
t.Parallel()
|
||||
converted := httpapi.UsernameFrom(testCase.From)
|
||||
t.Log(converted)
|
||||
valid := httpapi.UsernameValid(converted)
|
||||
valid := httpapi.NameValid(converted)
|
||||
require.True(t, valid == nil)
|
||||
if testCase.Match == "" {
|
||||
require.NotEqual(t, testCase.From, converted)
|
||||
Reference in New Issue
Block a user