fix(api): update UID compatibility
Use the original UID format consistently across API, username, and store validation so UUID-based callers continue to work. Regenerate API documentation and add regression coverage for UUID memo IDs.
This commit is contained in:
@@ -3,9 +3,5 @@ package base
|
|||||||
import "regexp"
|
import "regexp"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ResourceIDMatcher matches new user-provided resource IDs per AIP-122.
|
UIDMatcher = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$`)
|
||||||
ResourceIDMatcher = regexp.MustCompile(`^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$`)
|
|
||||||
// UIDMatcher also accepts the previous 36-character format so existing
|
|
||||||
// generated and persisted UIDs remain writable by the store layer.
|
|
||||||
UIDMatcher = regexp.MustCompile(`^(?:[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?|[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?)$`)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,8 +23,8 @@ func TestUIDMatcher(t *testing.T) {
|
|||||||
{"abc123", true},
|
{"abc123", true},
|
||||||
{"abc123-", false},
|
{"abc123-", false},
|
||||||
{"123e4567-e89b-12d3-a456-426614174000", true}, // UUID v4 from IDP
|
{"123e4567-e89b-12d3-a456-426614174000", true}, // UUID v4 from IDP
|
||||||
{"a123456789012345678901234567890123456", true}, // New AIP-compatible IDs can exceed the legacy limit.
|
{"a12345678901234567890123456789012345", true}, // 36 characters.
|
||||||
{"A123456789012345678901234567890123456", false}, // Legacy uppercase IDs remain capped at 36 characters.
|
{"a123456789012345678901234567890123456", false}, // 37 characters.
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -37,27 +36,3 @@ func TestUIDMatcher(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResourceIDMatcher(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{"a", true},
|
|
||||||
{"abc-123", true},
|
|
||||||
{"a" + strings.Repeat("b", 62), true},
|
|
||||||
{"a" + strings.Repeat("b", 63), false},
|
|
||||||
{"1abc", false},
|
|
||||||
{"Abc", false},
|
|
||||||
{"abc-", false},
|
|
||||||
{"abc_def", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.input, func(t *testing.T) {
|
|
||||||
if got := ResourceIDMatcher.MatchString(test.input); got != test.expected {
|
|
||||||
t.Errorf("ResourceIDMatcher.MatchString(%q) = %v, want %v", test.input, got, test.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ message CreateAttachmentRequest {
|
|||||||
|
|
||||||
// Optional. The attachment ID to use for this attachment.
|
// Optional. The attachment ID to use for this attachment.
|
||||||
// If empty, a unique ID will be generated.
|
// If empty, a unique ID will be generated.
|
||||||
// Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
string attachment_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
string attachment_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ message CreateIdentityProviderRequest {
|
|||||||
|
|
||||||
// Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
// Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
||||||
// If not provided, the system will generate one.
|
// If not provided, the system will generate one.
|
||||||
// Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
string identity_provider_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
string identity_provider_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ message Memo {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The resource name of the memo.
|
// The resource name of the memo.
|
||||||
// Format: memos/{memo}, memo is the user defined id or uuid.
|
// Format: memos/{memo}, where memo is the user-defined UID.
|
||||||
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
|
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
|
||||||
|
|
||||||
// The state of the memo.
|
// The state of the memo.
|
||||||
@@ -296,7 +296,7 @@ message CreateMemoRequest {
|
|||||||
|
|
||||||
// Optional. The memo ID to use for this memo.
|
// Optional. The memo ID to use for this memo.
|
||||||
// If empty, a unique ID will be generated.
|
// If empty, a unique ID will be generated.
|
||||||
// Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
string memo_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
string memo_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -312,8 +312,8 @@ message CreateUserRequest {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Optional. The resource ID to use for this user. If set, it must equal
|
// Optional. The resource ID to use for this user. If set, it must equal
|
||||||
// user.username and follow the User resource ID format.
|
// user.username, follow the UID format, and not consist entirely of digits.
|
||||||
// Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
string user_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
string user_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
||||||
|
|
||||||
// Optional. If set, validate the request but don't actually create the user.
|
// Optional. If set, validate the request but don't actually create the user.
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ type CreateAttachmentRequest struct {
|
|||||||
Attachment *Attachment `protobuf:"bytes,1,opt,name=attachment,proto3" json:"attachment,omitempty"`
|
Attachment *Attachment `protobuf:"bytes,1,opt,name=attachment,proto3" json:"attachment,omitempty"`
|
||||||
// Optional. The attachment ID to use for this attachment.
|
// Optional. The attachment ID to use for this attachment.
|
||||||
// If empty, a unique ID will be generated.
|
// If empty, a unique ID will be generated.
|
||||||
// Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
AttachmentId string `protobuf:"bytes,2,opt,name=attachment_id,json=attachmentId,proto3" json:"attachment_id,omitempty"`
|
AttachmentId string `protobuf:"bytes,2,opt,name=attachment_id,json=attachmentId,proto3" json:"attachment_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
|
|||||||
@@ -512,7 +512,7 @@ type CreateIdentityProviderRequest struct {
|
|||||||
IdentityProvider *IdentityProvider `protobuf:"bytes,1,opt,name=identity_provider,json=identityProvider,proto3" json:"identity_provider,omitempty"`
|
IdentityProvider *IdentityProvider `protobuf:"bytes,1,opt,name=identity_provider,json=identityProvider,proto3" json:"identity_provider,omitempty"`
|
||||||
// Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
// Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
||||||
// If not provided, the system will generate one.
|
// If not provided, the system will generate one.
|
||||||
// Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
IdentityProviderId string `protobuf:"bytes,2,opt,name=identity_provider_id,json=identityProviderId,proto3" json:"identity_provider_id,omitempty"`
|
IdentityProviderId string `protobuf:"bytes,2,opt,name=identity_provider_id,json=identityProviderId,proto3" json:"identity_provider_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ func (x *Reaction) GetCreateTime() *timestamppb.Timestamp {
|
|||||||
type Memo struct {
|
type Memo struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// The resource name of the memo.
|
// The resource name of the memo.
|
||||||
// Format: memos/{memo}, memo is the user defined id or uuid.
|
// Format: memos/{memo}, where memo is the user-defined UID.
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
// The state of the memo.
|
// The state of the memo.
|
||||||
State State `protobuf:"varint,2,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"`
|
State State `protobuf:"varint,2,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"`
|
||||||
@@ -472,7 +472,7 @@ type CreateMemoRequest struct {
|
|||||||
Memo *Memo `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"`
|
Memo *Memo `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"`
|
||||||
// Optional. The memo ID to use for this memo.
|
// Optional. The memo ID to use for this memo.
|
||||||
// If empty, a unique ID will be generated.
|
// If empty, a unique ID will be generated.
|
||||||
// Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
MemoId string `protobuf:"bytes,2,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"`
|
MemoId string `protobuf:"bytes,2,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
|
|||||||
@@ -651,8 +651,8 @@ type CreateUserRequest struct {
|
|||||||
// Required. The user to create.
|
// Required. The user to create.
|
||||||
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||||
// Optional. The resource ID to use for this user. If set, it must equal
|
// Optional. The resource ID to use for this user. If set, it must equal
|
||||||
// user.username and follow the User resource ID format.
|
// user.username, follow the UID format, and not consist entirely of digits.
|
||||||
// Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||||
// Optional. If set, validate the request but don't actually create the user.
|
// Optional. If set, validate the request but don't actually create the user.
|
||||||
ValidateOnly bool `protobuf:"varint,3,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"`
|
ValidateOnly bool `protobuf:"varint,3,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"`
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ paths:
|
|||||||
description: |-
|
description: |-
|
||||||
Optional. The attachment ID to use for this attachment.
|
Optional. The attachment ID to use for this attachment.
|
||||||
If empty, a unique ID will be generated.
|
If empty, a unique ID will be generated.
|
||||||
Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -350,7 +350,7 @@ paths:
|
|||||||
description: |-
|
description: |-
|
||||||
Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
||||||
If not provided, the system will generate one.
|
If not provided, the system will generate one.
|
||||||
Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -705,7 +705,7 @@ paths:
|
|||||||
description: |-
|
description: |-
|
||||||
Optional. The memo ID to use for this memo.
|
Optional. The memo ID to use for this memo.
|
||||||
If empty, a unique ID will be generated.
|
If empty, a unique ID will be generated.
|
||||||
Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -1355,8 +1355,8 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
description: |-
|
description: |-
|
||||||
Optional. The resource ID to use for this user. If set, it must equal
|
Optional. The resource ID to use for this user. If set, it must equal
|
||||||
user.username and follow the User resource ID format.
|
user.username, follow the UID format, and not consist entirely of digits.
|
||||||
Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- name: validateOnly
|
- name: validateOnly
|
||||||
@@ -3230,7 +3230,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
The resource name of the memo.
|
The resource name of the memo.
|
||||||
Format: memos/{memo}, memo is the user defined id or uuid.
|
Format: memos/{memo}, where memo is the user-defined UID.
|
||||||
state:
|
state:
|
||||||
enum:
|
enum:
|
||||||
- STATE_UNSPECIFIED
|
- STATE_UNSPECIFIED
|
||||||
|
|||||||
@@ -136,14 +136,14 @@ func ExtractIdentityProviderUIDFromName(name string) (string, error) {
|
|||||||
|
|
||||||
// ValidateAndGenerateUID validates a user-provided UID or generates a new one.
|
// ValidateAndGenerateUID validates a user-provided UID or generates a new one.
|
||||||
// If provided is empty, a new shortuuid is generated.
|
// If provided is empty, a new shortuuid is generated.
|
||||||
// If provided is non-empty, it is validated as a user-provided resource ID.
|
// If provided is non-empty, it is validated against base.UIDMatcher.
|
||||||
func ValidateAndGenerateUID(provided string) (string, error) {
|
func ValidateAndGenerateUID(provided string) (string, error) {
|
||||||
uid := strings.TrimSpace(provided)
|
uid := strings.TrimSpace(provided)
|
||||||
if uid == "" {
|
if uid == "" {
|
||||||
return shortuuid.New(), nil
|
return shortuuid.New(), nil
|
||||||
}
|
}
|
||||||
if !base.ResourceIDMatcher.MatchString(uid) {
|
if !base.UIDMatcher.MatchString(uid) {
|
||||||
return "", status.Errorf(codes.InvalidArgument, "invalid resource ID: must be 1-63 characters, start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and end with a letter or digit")
|
return "", status.Errorf(codes.InvalidArgument, "invalid UID: must be 1-36 characters, contain only letters, digits, or hyphens, and start and end with a letter or digit")
|
||||||
}
|
}
|
||||||
return uid, nil
|
return uid, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ func TestValidateAndGenerateUIDValidatesUserProvidedResourceIDs(t *testing.T) {
|
|||||||
wantError bool
|
wantError bool
|
||||||
}{
|
}{
|
||||||
{name: "lowercase", provided: "memo-1"},
|
{name: "lowercase", provided: "memo-1"},
|
||||||
{name: "maximum length", provided: "a" + strings.Repeat("b", 62)},
|
{name: "UUID", provided: "21ec98aa-9a8f-458c-a2a3-c7dc69b6f591"},
|
||||||
{name: "digit first", provided: "1-memo", wantError: true},
|
{name: "maximum length", provided: "a" + strings.Repeat("b", 35)},
|
||||||
{name: "uppercase", provided: "Memo", wantError: true},
|
{name: "digit first", provided: "1-memo"},
|
||||||
{name: "too long", provided: "a" + strings.Repeat("b", 63), wantError: true},
|
{name: "uppercase", provided: "Memo"},
|
||||||
|
{name: "too long", provided: "a" + strings.Repeat("b", 36), wantError: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import (
|
|||||||
|
|
||||||
// deriveSSOUsername produces the local username for a new SSO-created user.
|
// deriveSSOUsername produces the local username for a new SSO-created user.
|
||||||
//
|
//
|
||||||
// The current policy prefixes a UUID with a letter so the generated value
|
// The current policy is to use a standard UUID string directly. This keeps the
|
||||||
// follows the same AIP-compatible format as user-selected usernames.
|
// username independent of IdP profile fields and avoids availability probes or
|
||||||
|
// retry loops around concurrent first-time logins.
|
||||||
func deriveSSOUsername() (string, error) {
|
func deriveSSOUsername() (string, error) {
|
||||||
username := "user-" + util.GenUUID()
|
username := util.GenUUID()
|
||||||
if err := validateWritableUsername(username); err != nil {
|
if err := validateWritableUsername(username); err != nil {
|
||||||
return "", errors.Wrap(err, "generated username did not satisfy username constraints")
|
return "", errors.Wrap(err, "generated UUID did not satisfy username constraints")
|
||||||
}
|
}
|
||||||
return username, nil
|
return username, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,8 +9,8 @@ func TestDeriveSSOUsername(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("deriveSSOUsername() returned error: %v", err)
|
t.Fatalf("deriveSSOUsername() returned error: %v", err)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(username, "user-") {
|
if len(username) != 36 {
|
||||||
t.Fatalf("deriveSSOUsername() = %q, want user- prefix", username)
|
t.Fatalf("deriveSSOUsername() = %q, want a 36-character UUID", username)
|
||||||
}
|
}
|
||||||
if err := validateWritableUsername(username); err != nil {
|
if err := validateWritableUsername(username); err != nil {
|
||||||
t.Fatalf("deriveSSOUsername() produced invalid username %q: %v", username, err)
|
t.Fatalf("deriveSSOUsername() produced invalid username %q: %v", username, err)
|
||||||
|
|||||||
@@ -17,6 +17,27 @@ import (
|
|||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestCreateMemoAcceptsUUID(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
ts := NewTestService(t)
|
||||||
|
defer ts.Cleanup()
|
||||||
|
|
||||||
|
user, err := ts.CreateRegularUser(ctx, "test-user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
userCtx := ts.CreateUserContext(ctx, user.ID)
|
||||||
|
|
||||||
|
const memoID = "21ec98aa-9a8f-458c-a2a3-c7dc69b6f591"
|
||||||
|
memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
|
||||||
|
Memo: &apiv1.Memo{
|
||||||
|
Content: "Created with a UUID",
|
||||||
|
Visibility: apiv1.Visibility_PRIVATE,
|
||||||
|
},
|
||||||
|
MemoId: memoID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "memos/"+memoID, memo.Name)
|
||||||
|
}
|
||||||
|
|
||||||
func TestListMemos(t *testing.T) {
|
func TestListMemos(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,24 @@ func parseUsernameFromName(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateWritableUsername(username string) error {
|
func validateWritableUsername(username string) error {
|
||||||
if !base.ResourceIDMatcher.MatchString(username) {
|
if username == "" || isNumericUsername(username) || !base.UIDMatcher.MatchString(username) {
|
||||||
return errors.New("invalid username: must be 1-63 characters, start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and end with a letter or digit")
|
return errors.Errorf("invalid username %q", username)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isNumericUsername(username string) bool {
|
||||||
|
if username == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, char := range username {
|
||||||
|
if char < '0' || char > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ResolveUserByName resolves a username-based user resource name to a store user.
|
// ResolveUserByName resolves a username-based user resource name to a store user.
|
||||||
func ResolveUserByName(ctx context.Context, stores *store.Store, name string) (*store.User, error) {
|
func ResolveUserByName(ctx context.Context, stores *store.Store, name string) (*store.User, error) {
|
||||||
username, err := parseUsernameFromName(name)
|
username, err := parseUsernameFromName(name)
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ func TestValidateWritableUsername(t *testing.T) {
|
|||||||
username: "alice",
|
username: "alice",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mixed case",
|
name: "mixed case",
|
||||||
username: "Alice",
|
username: "Alice",
|
||||||
wantError: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "hyphenated",
|
name: "hyphenated",
|
||||||
@@ -30,17 +29,16 @@ func TestValidateWritableUsername(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "maximum length",
|
name: "maximum length",
|
||||||
username: "a" + strings.Repeat("b", 62),
|
username: "a" + strings.Repeat("b", 35),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "too long",
|
name: "too long",
|
||||||
username: "a" + strings.Repeat("b", 63),
|
username: "a" + strings.Repeat("b", 36),
|
||||||
wantError: true,
|
wantError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "digit first",
|
name: "digit first",
|
||||||
username: "1alice",
|
username: "1alice",
|
||||||
wantError: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "hyphen last",
|
name: "hyphen last",
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export type CreateAttachmentRequest = Message<"memos.api.v1.CreateAttachmentRequ
|
|||||||
/**
|
/**
|
||||||
* Optional. The attachment ID to use for this attachment.
|
* Optional. The attachment ID to use for this attachment.
|
||||||
* If empty, a unique ID will be generated.
|
* If empty, a unique ID will be generated.
|
||||||
* Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
* Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
*
|
*
|
||||||
* @generated from field: string attachment_id = 2;
|
* @generated from field: string attachment_id = 2;
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export type CreateIdentityProviderRequest = Message<"memos.api.v1.CreateIdentity
|
|||||||
/**
|
/**
|
||||||
* Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
* Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
||||||
* If not provided, the system will generate one.
|
* If not provided, the system will generate one.
|
||||||
* Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
* Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
*
|
*
|
||||||
* @generated from field: string identity_provider_id = 2;
|
* @generated from field: string identity_provider_id = 2;
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ReactionSchema: GenMessage<Reaction> = /*@__PURE__*/
|
|||||||
export type Memo = Message<"memos.api.v1.Memo"> & {
|
export type Memo = Message<"memos.api.v1.Memo"> & {
|
||||||
/**
|
/**
|
||||||
* The resource name of the memo.
|
* The resource name of the memo.
|
||||||
* Format: memos/{memo}, memo is the user defined id or uuid.
|
* Format: memos/{memo}, where memo is the user-defined UID.
|
||||||
*
|
*
|
||||||
* @generated from field: string name = 1;
|
* @generated from field: string name = 1;
|
||||||
*/
|
*/
|
||||||
@@ -292,7 +292,7 @@ export type CreateMemoRequest = Message<"memos.api.v1.CreateMemoRequest"> & {
|
|||||||
/**
|
/**
|
||||||
* Optional. The memo ID to use for this memo.
|
* Optional. The memo ID to use for this memo.
|
||||||
* If empty, a unique ID will be generated.
|
* If empty, a unique ID will be generated.
|
||||||
* Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
* Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
*
|
*
|
||||||
* @generated from field: string memo_id = 2;
|
* @generated from field: string memo_id = 2;
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -293,8 +293,8 @@ export type CreateUserRequest = Message<"memos.api.v1.CreateUserRequest"> & {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional. The resource ID to use for this user. If set, it must equal
|
* Optional. The resource ID to use for this user. If set, it must equal
|
||||||
* user.username and follow the User resource ID format.
|
* user.username, follow the UID format, and not consist entirely of digits.
|
||||||
* Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$
|
* Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
||||||
*
|
*
|
||||||
* @generated from field: string user_id = 2;
|
* @generated from field: string user_id = 2;
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user