mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
fix: don't allow "new" or "create" as url-friendly names (#13596)
This commit is contained in:
committed by
GitHub
parent
3a1fa04590
commit
e987ad1d89
5
coderd/apidoc/docs.go
generated
5
coderd/apidoc/docs.go
generated
@ -8396,6 +8396,9 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"codersdk.CreateGroupRequest": {
|
"codersdk.CreateGroupRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatar_url": {
|
"avatar_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -10038,10 +10041,8 @@ const docTemplate = `{
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"created_at",
|
"created_at",
|
||||||
"display_name",
|
|
||||||
"id",
|
"id",
|
||||||
"is_default",
|
"is_default",
|
||||||
"name",
|
|
||||||
"updated_at"
|
"updated_at"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
10
coderd/apidoc/swagger.json
generated
10
coderd/apidoc/swagger.json
generated
@ -7472,6 +7472,7 @@
|
|||||||
},
|
},
|
||||||
"codersdk.CreateGroupRequest": {
|
"codersdk.CreateGroupRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatar_url": {
|
"avatar_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -9019,14 +9020,7 @@
|
|||||||
},
|
},
|
||||||
"codersdk.Organization": {
|
"codersdk.Organization": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["created_at", "id", "is_default", "updated_at"],
|
||||||
"created_at",
|
|
||||||
"display_name",
|
|
||||||
"id",
|
|
||||||
"is_default",
|
|
||||||
"name",
|
|
||||||
"updated_at"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -46,7 +46,7 @@ func init() {
|
|||||||
valid := NameValid(str)
|
valid := NameValid(str)
|
||||||
return valid == nil
|
return valid == nil
|
||||||
}
|
}
|
||||||
for _, tag := range []string{"username", "organization_name", "template_name", "workspace_name", "oauth2_app_name"} {
|
for _, tag := range []string{"username", "organization_name", "template_name", "group_name", "workspace_name", "oauth2_app_name"} {
|
||||||
err := Validate.RegisterValidation(tag, nameValidator)
|
err := Validate.RegisterValidation(tag, nameValidator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -62,7 +62,7 @@ func init() {
|
|||||||
valid := DisplayNameValid(str)
|
valid := DisplayNameValid(str)
|
||||||
return valid == nil
|
return valid == nil
|
||||||
}
|
}
|
||||||
for _, displayNameTag := range []string{"organization_display_name", "template_display_name"} {
|
for _, displayNameTag := range []string{"organization_display_name", "template_display_name", "group_display_name"} {
|
||||||
err := Validate.RegisterValidation(displayNameTag, displayNameValidator)
|
err := Validate.RegisterValidation(displayNameTag, displayNameValidator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -46,6 +46,10 @@ func NameValid(str string) error {
|
|||||||
if len(str) < 1 {
|
if len(str) < 1 {
|
||||||
return xerrors.New("must be >= 1 character")
|
return xerrors.New("must be >= 1 character")
|
||||||
}
|
}
|
||||||
|
// Avoid conflicts with routes like /templates/new and /groups/create.
|
||||||
|
if str == "new" || str == "create" {
|
||||||
|
return xerrors.Errorf("cannot use %q as a name", str)
|
||||||
|
}
|
||||||
matched := UsernameValidRegex.MatchString(str)
|
matched := UsernameValidRegex.MatchString(str)
|
||||||
if !matched {
|
if !matched {
|
||||||
return xerrors.New("must be alphanumeric with hyphens")
|
return xerrors.New("must be alphanumeric with hyphens")
|
||||||
|
@ -140,14 +140,14 @@ func TestPostOrganizationsByUser(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
|
||||||
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||||
Name: "new",
|
Name: "new-org",
|
||||||
DisplayName: "New",
|
DisplayName: "New organization",
|
||||||
Description: "A new organization to love and cherish forever.",
|
Description: "A new organization to love and cherish forever.",
|
||||||
Icon: "/emojis/1f48f-1f3ff.png",
|
Icon: "/emojis/1f48f-1f3ff.png",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new", o.Name)
|
require.Equal(t, "new-org", o.Name)
|
||||||
require.Equal(t, "New", o.DisplayName)
|
require.Equal(t, "New organization", o.DisplayName)
|
||||||
require.Equal(t, "A new organization to love and cherish forever.", o.Description)
|
require.Equal(t, "A new organization to love and cherish forever.", o.Description)
|
||||||
require.Equal(t, "/emojis/1f48f-1f3ff.png", o.Icon)
|
require.Equal(t, "/emojis/1f48f-1f3ff.png", o.Icon)
|
||||||
})
|
})
|
||||||
@ -159,11 +159,11 @@ func TestPostOrganizationsByUser(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
|
||||||
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||||
Name: "new",
|
Name: "new-org",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new", o.Name)
|
require.Equal(t, "new-org", o.Name)
|
||||||
require.Equal(t, "new", o.DisplayName) // should match the given `Name`
|
require.Equal(t, "new-org", o.DisplayName) // should match the given `Name`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,16 +238,16 @@ func TestPatchOrganizationsByUser(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
|
||||||
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||||
Name: "new",
|
Name: "new-org",
|
||||||
DisplayName: "New",
|
DisplayName: "New organization",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
o, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{
|
o, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{
|
||||||
Name: "new-new",
|
Name: "new-new-org",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new-new", o.Name)
|
require.Equal(t, "new-new-org", o.Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("UpdateByName", func(t *testing.T) {
|
t.Run("UpdateByName", func(t *testing.T) {
|
||||||
@ -257,17 +257,17 @@ func TestPatchOrganizationsByUser(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
|
||||||
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||||
Name: "new",
|
Name: "new-org",
|
||||||
DisplayName: "New",
|
DisplayName: "New organization",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{
|
o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{
|
||||||
Name: "new-new",
|
Name: "new-new-org",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new-new", o.Name)
|
require.Equal(t, "new-new-org", o.Name)
|
||||||
require.Equal(t, "New", o.DisplayName) // didn't change
|
require.Equal(t, "New organization", o.DisplayName) // didn't change
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("UpdateDisplayName", func(t *testing.T) {
|
t.Run("UpdateDisplayName", func(t *testing.T) {
|
||||||
@ -277,8 +277,8 @@ func TestPatchOrganizationsByUser(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
|
||||||
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||||
Name: "new",
|
Name: "new-org",
|
||||||
DisplayName: "New",
|
DisplayName: "New organization",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -286,7 +286,7 @@ func TestPatchOrganizationsByUser(t *testing.T) {
|
|||||||
DisplayName: "The Newest One",
|
DisplayName: "The Newest One",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new", o.Name) // didn't change
|
require.Equal(t, "new-org", o.Name) // didn't change
|
||||||
require.Equal(t, "The Newest One", o.DisplayName)
|
require.Equal(t, "The Newest One", o.DisplayName)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -297,8 +297,8 @@ func TestPatchOrganizationsByUser(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
|
||||||
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||||
Name: "new",
|
Name: "new-org",
|
||||||
DisplayName: "New",
|
DisplayName: "New organization",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -307,8 +307,8 @@ func TestPatchOrganizationsByUser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new", o.Name) // didn't change
|
require.Equal(t, "new-org", o.Name) // didn't change
|
||||||
require.Equal(t, "New", o.DisplayName) // didn't change
|
require.Equal(t, "New organization", o.DisplayName) // didn't change
|
||||||
require.Equal(t, "wow, this organization description is so updated!", o.Description)
|
require.Equal(t, "wow, this organization description is so updated!", o.Description)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -319,8 +319,8 @@ func TestPatchOrganizationsByUser(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
|
||||||
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||||
Name: "new",
|
Name: "new-org",
|
||||||
DisplayName: "New",
|
DisplayName: "New organization",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -329,8 +329,8 @@ func TestPatchOrganizationsByUser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new", o.Name) // didn't change
|
require.Equal(t, "new-org", o.Name) // didn't change
|
||||||
require.Equal(t, "New", o.DisplayName) // didn't change
|
require.Equal(t, "New organization", o.DisplayName) // didn't change
|
||||||
require.Equal(t, "/emojis/1f48f-1f3ff.png", o.Icon)
|
require.Equal(t, "/emojis/1f48f-1f3ff.png", o.Icon)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -37,8 +37,7 @@ func TestTemplate(t *testing.T) {
|
|||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.Template(ctx, template.ID)
|
_, err := client.Template(ctx, template.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -63,8 +62,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.Equal(t, (3 * time.Hour).Milliseconds(), expected.ActivityBumpMillis)
|
assert.Equal(t, (3 * time.Hour).Milliseconds(), expected.ActivityBumpMillis)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
got, err := user.Template(ctx, expected.ID)
|
got, err := user.Template(ctx, expected.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -86,8 +84,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
_, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||||
Name: template.Name,
|
Name: template.Name,
|
||||||
@ -98,15 +95,30 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("ReservedName", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, nil)
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
|
||||||
|
_, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||||
|
Name: "new",
|
||||||
|
VersionID: version.ID,
|
||||||
|
})
|
||||||
|
var apiErr *codersdk.Error
|
||||||
|
require.ErrorAs(t, err, &apiErr)
|
||||||
|
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("DefaultTTLTooLow", func(t *testing.T) {
|
t.Run("DefaultTTLTooLow", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
_, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||||
Name: "testing",
|
Name: "testing",
|
||||||
VersionID: version.ID,
|
VersionID: version.ID,
|
||||||
@ -124,9 +136,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||||
Name: "testing",
|
Name: "testing",
|
||||||
VersionID: version.ID,
|
VersionID: version.ID,
|
||||||
@ -143,15 +153,13 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||||||
owner := coderdtest.CreateFirstUser(t, client)
|
owner := coderdtest.CreateFirstUser(t, client)
|
||||||
user, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
user, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||||
|
|
||||||
expected := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
|
expected := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
|
||||||
request.DisableEveryoneGroupAccess = true
|
request.DisableEveryoneGroupAccess = true
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := user.Template(ctx, expected.ID)
|
_, err := user.Template(ctx, expected.ID)
|
||||||
|
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
require.ErrorAs(t, err, &apiErr)
|
||||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||||
@ -161,9 +169,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.CreateTemplate(ctx, uuid.New(), codersdk.CreateTemplateRequest{
|
_, err := client.CreateTemplate(ctx, uuid.New(), codersdk.CreateTemplateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
VersionID: uuid.New(),
|
VersionID: uuid.New(),
|
||||||
@ -241,8 +247,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
_, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
@ -398,8 +403,7 @@ func TestTemplatesByOrganization(t *testing.T) {
|
|||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
|
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -414,8 +418,7 @@ func TestTemplatesByOrganization(t *testing.T) {
|
|||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
|
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -430,8 +433,7 @@ func TestTemplatesByOrganization(t *testing.T) {
|
|||||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID)
|
coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
|
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -446,8 +448,7 @@ func TestTemplateByOrganizationAndName(t *testing.T) {
|
|||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.TemplateByName(ctx, user.OrganizationID, "something")
|
_, err := client.TemplateByName(ctx, user.OrganizationID, "something")
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
@ -462,8 +463,7 @@ func TestTemplateByOrganizationAndName(t *testing.T) {
|
|||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.TemplateByName(ctx, user.OrganizationID, template.Name)
|
_, err := client.TemplateByName(ctx, user.OrganizationID, template.Name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -497,8 +497,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
// updatedAt is too close together.
|
// updatedAt is too close together.
|
||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -542,8 +541,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
DeprecationMessage: ptr.Ref("APGL cannot deprecate"),
|
DeprecationMessage: ptr.Ref("APGL cannot deprecate"),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -566,8 +564,8 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
// updatedAt is too close together.
|
// updatedAt is too close together.
|
||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
// nolint:gocritic // Setting up unit test data
|
// nolint:gocritic // Setting up unit test data
|
||||||
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
|
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
|
||||||
ID: template.ID,
|
ID: template.ID,
|
||||||
@ -607,8 +605,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
MaxPortShareLevel: &level,
|
MaxPortShareLevel: &level,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
// AGPL cannot change max port sharing level
|
// AGPL cannot change max port sharing level
|
||||||
@ -643,8 +640,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
// We're too fast! Sleep so we can be sure that updatedAt is greater
|
// We're too fast! Sleep so we can be sure that updatedAt is greater
|
||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -675,8 +671,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
DefaultTTLMillis: -1,
|
DefaultTTLMillis: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
require.ErrorContains(t, err, "default_ttl_ms: Must be a positive integer")
|
require.ErrorContains(t, err, "default_ttl_ms: Must be a positive integer")
|
||||||
@ -886,8 +881,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: template.Name,
|
||||||
@ -921,8 +915,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
DefaultTTLMillis: -int64(time.Hour),
|
DefaultTTLMillis: -int64(time.Hour),
|
||||||
@ -956,8 +949,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
Icon: "",
|
Icon: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -1164,8 +1156,7 @@ func TestDeleteTemplate(t *testing.T) {
|
|||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := client.DeleteTemplate(ctx, template.ID)
|
err := client.DeleteTemplate(ctx, template.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -1183,8 +1174,7 @@ func TestDeleteTemplate(t *testing.T) {
|
|||||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := client.DeleteTemplate(ctx, template.ID)
|
err := client.DeleteTemplate(ctx, template.ID)
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
|
@ -18,8 +18,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateGroupRequest struct {
|
type CreateGroupRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name" validate:"required,group_name"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name" validate:"omitempty,group_display_name"`
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
QuotaAllowance int `json:"quota_allowance"`
|
QuotaAllowance int `json:"quota_allowance"`
|
||||||
}
|
}
|
||||||
@ -111,8 +111,8 @@ func (c *Client) Group(ctx context.Context, group uuid.UUID) (Group, error) {
|
|||||||
type PatchGroupRequest struct {
|
type PatchGroupRequest struct {
|
||||||
AddUsers []string `json:"add_users"`
|
AddUsers []string `json:"add_users"`
|
||||||
RemoveUsers []string `json:"remove_users"`
|
RemoveUsers []string `json:"remove_users"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" validate:"omitempty,group_name"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name" validate:"omitempty,group_display_name"`
|
||||||
AvatarURL *string `json:"avatar_url"`
|
AvatarURL *string `json:"avatar_url"`
|
||||||
QuotaAllowance *int `json:"quota_allowance"`
|
QuotaAllowance *int `json:"quota_allowance"`
|
||||||
}
|
}
|
||||||
|
@ -41,8 +41,8 @@ func ProvisionerTypeValid[T ProvisionerType | string](pt T) error {
|
|||||||
// Organization is the JSON representation of a Coder organization.
|
// Organization is the JSON representation of a Coder organization.
|
||||||
type Organization struct {
|
type Organization struct {
|
||||||
ID uuid.UUID `table:"id" json:"id" validate:"required" format:"uuid"`
|
ID uuid.UUID `table:"id" json:"id" validate:"required" format:"uuid"`
|
||||||
Name string `table:"name,default_sort" json:"name" validate:"required,username"`
|
Name string `table:"name,default_sort" json:"name"`
|
||||||
DisplayName string `table:"display_name" json:"display_name" validate:"required"`
|
DisplayName string `table:"display_name" json:"display_name"`
|
||||||
Description string `table:"description" json:"description"`
|
Description string `table:"description" json:"description"`
|
||||||
CreatedAt time.Time `table:"created_at" json:"created_at" validate:"required" format:"date-time"`
|
CreatedAt time.Time `table:"created_at" json:"created_at" validate:"required" format:"date-time"`
|
||||||
UpdatedAt time.Time `table:"updated_at" json:"updated_at" validate:"required" format:"date-time"`
|
UpdatedAt time.Time `table:"updated_at" json:"updated_at" validate:"required" format:"date-time"`
|
||||||
|
6
docs/api/schemas.md
generated
6
docs/api/schemas.md
generated
@ -1020,7 +1020,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||||||
| ----------------- | ------- | -------- | ------------ | ----------- |
|
| ----------------- | ------- | -------- | ------------ | ----------- |
|
||||||
| `avatar_url` | string | false | | |
|
| `avatar_url` | string | false | | |
|
||||||
| `display_name` | string | false | | |
|
| `display_name` | string | false | | |
|
||||||
| `name` | string | false | | |
|
| `name` | string | true | | |
|
||||||
| `quota_allowance` | integer | false | | |
|
| `quota_allowance` | integer | false | | |
|
||||||
|
|
||||||
## codersdk.CreateOrganizationRequest
|
## codersdk.CreateOrganizationRequest
|
||||||
@ -3227,11 +3227,11 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
| -------------- | ------- | -------- | ------------ | ----------- |
|
| -------------- | ------- | -------- | ------------ | ----------- |
|
||||||
| `created_at` | string | true | | |
|
| `created_at` | string | true | | |
|
||||||
| `description` | string | false | | |
|
| `description` | string | false | | |
|
||||||
| `display_name` | string | true | | |
|
| `display_name` | string | false | | |
|
||||||
| `icon` | string | false | | |
|
| `icon` | string | false | | |
|
||||||
| `id` | string | true | | |
|
| `id` | string | true | | |
|
||||||
| `is_default` | boolean | true | | |
|
| `is_default` | boolean | true | | |
|
||||||
| `name` | string | true | | |
|
| `name` | string | false | | |
|
||||||
| `updated_at` | string | true | | |
|
| `updated_at` | string | true | | |
|
||||||
|
|
||||||
## codersdk.OrganizationMember
|
## codersdk.OrganizationMember
|
||||||
|
4
docs/api/users.md
generated
4
docs/api/users.md
generated
@ -1024,11 +1024,11 @@ Status Code **200**
|
|||||||
| `[array item]` | array | false | | |
|
| `[array item]` | array | false | | |
|
||||||
| `» created_at` | string(date-time) | true | | |
|
| `» created_at` | string(date-time) | true | | |
|
||||||
| `» description` | string | false | | |
|
| `» description` | string | false | | |
|
||||||
| `» display_name` | string | true | | |
|
| `» display_name` | string | false | | |
|
||||||
| `» icon` | string | false | | |
|
| `» icon` | string | false | | |
|
||||||
| `» id` | string(uuid) | true | | |
|
| `» id` | string(uuid) | true | | |
|
||||||
| `» is_default` | boolean | true | | |
|
| `» is_default` | boolean | true | | |
|
||||||
| `» name` | string | true | | |
|
| `» name` | string | false | | |
|
||||||
| `» updated_at` | string(date-time) | true | | |
|
| `» updated_at` | string(date-time) | true | | |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
@ -41,7 +41,7 @@ func TestTemplateCreate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
inv, conf := newCLI(t, "templates",
|
inv, conf := newCLI(t, "templates",
|
||||||
"create", "new",
|
"create", "new-template",
|
||||||
"--directory", source,
|
"--directory", source,
|
||||||
"--test.provisioner", string(database.ProvisionerTypeEcho),
|
"--test.provisioner", string(database.ProvisionerTypeEcho),
|
||||||
"--require-active-version",
|
"--require-active-version",
|
||||||
@ -54,7 +54,7 @@ func TestTemplateCreate(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
template, err := templateAdmin.TemplateByName(ctx, user.OrganizationID, "new")
|
template, err := templateAdmin.TemplateByName(ctx, user.OrganizationID, "new-template")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, template.RequireActiveVersion)
|
require.True(t, template.RequireActiveVersion)
|
||||||
})
|
})
|
||||||
@ -86,7 +86,7 @@ func TestTemplateCreate(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
inv, conf := newCLI(t, "templates",
|
inv, conf := newCLI(t, "templates",
|
||||||
"create", "new",
|
"create", "new-template",
|
||||||
"--directory", source,
|
"--directory", source,
|
||||||
"--test.provisioner", string(database.ProvisionerTypeEcho),
|
"--test.provisioner", string(database.ProvisionerTypeEcho),
|
||||||
"--failure-ttl="+expectedFailureTTL.String(),
|
"--failure-ttl="+expectedFailureTTL.String(),
|
||||||
@ -102,7 +102,7 @@ func TestTemplateCreate(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
template, err := templateAdmin.TemplateByName(ctx, user.OrganizationID, "new")
|
template, err := templateAdmin.TemplateByName(ctx, user.OrganizationID, "new-template")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, expectedFailureTTL.Milliseconds(), template.FailureTTLMillis)
|
require.Equal(t, expectedFailureTTL.Milliseconds(), template.FailureTTLMillis)
|
||||||
require.Equal(t, expectedDormancyThreshold.Milliseconds(), template.TimeTilDormantMillis)
|
require.Equal(t, expectedDormancyThreshold.Milliseconds(), template.TimeTilDormantMillis)
|
||||||
@ -123,7 +123,7 @@ func TestTemplateCreate(t *testing.T) {
|
|||||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin())
|
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin())
|
||||||
|
|
||||||
inv, conf := newCLI(t, "templates",
|
inv, conf := newCLI(t, "templates",
|
||||||
"create", "new",
|
"create", "new-template",
|
||||||
"--require-active-version",
|
"--require-active-version",
|
||||||
"-y",
|
"-y",
|
||||||
)
|
)
|
||||||
|
@ -101,6 +101,26 @@ func TestCreateGroup(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusConflict, cerr.StatusCode())
|
require.Equal(t, http.StatusConflict, cerr.StatusCode())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("ReservedName", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureTemplateRBAC: 1,
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
userAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleUserAdmin())
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
_, err := userAdminClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
|
||||||
|
Name: "new",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
var apiErr *codersdk.Error
|
||||||
|
require.ErrorAs(t, err, &apiErr)
|
||||||
|
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("allUsers", func(t *testing.T) {
|
t.Run("allUsers", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user