From b0558824c43b7faebe6c8dce408bf1f7a86d879b Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 28 Jan 2026 23:27:53 +0800 Subject: [PATCH] feat: update instance profile to use admin user instead of initialized flag - Changed InstanceProfile to include admin user field - Updated GetInstanceProfile method to retrieve admin user - Modified related tests to reflect changes in admin user retrieval - Removed owner cache logic and tests, introducing new admin cache tests --- proto/api/v1/instance_service.proto | 8 +-- proto/gen/api/v1/instance_service.pb.go | 62 ++++++++++--------- proto/gen/openapi.yaml | 10 +-- server/router/api/v1/instance_service.go | 44 ++----------- ...e_test.go => instance_admin_cache_test.go} | 30 ++++----- .../api/v1/test/instance_service_test.go | 7 ++- server/router/api/v1/test/test_helper.go | 6 +- server/router/api/v1/user_service.go | 6 -- web/src/App.tsx | 4 +- web/src/pages/SignUp.tsx | 2 +- .../types/proto/api/v1/instance_service_pb.ts | 13 ++-- 11 files changed, 75 insertions(+), 117 deletions(-) rename server/router/api/v1/test/{instance_owner_cache_test.go => instance_admin_cache_test.go} (54%) diff --git a/proto/api/v1/instance_service.proto b/proto/api/v1/instance_service.proto index baeea0bd9..f4ce3d501 100644 --- a/proto/api/v1/instance_service.proto +++ b/proto/api/v1/instance_service.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package memos.api.v1; +import "api/v1/user_service.proto"; import "google/api/annotations.proto"; import "google/api/client.proto"; import "google/api/field_behavior.proto"; @@ -43,10 +44,9 @@ message InstanceProfile { // Instance URL is the URL of the instance. string instance_url = 6; - // Indicates if the instance has completed first-time setup. - // When false, the instance requires initialization (creating the first admin account). - // This follows the pattern used by other self-hosted platforms for setup workflows. - bool initialized = 7; + // The first administrator who set up this instance. + // When null, instance requires initial setup (creating the first admin account). + User admin = 7; } // Request for instance profile. diff --git a/proto/gen/api/v1/instance_service.pb.go b/proto/gen/api/v1/instance_service.pb.go index edef32e91..5be2dd4bd 100644 --- a/proto/gen/api/v1/instance_service.pb.go +++ b/proto/gen/api/v1/instance_service.pb.go @@ -144,10 +144,9 @@ type InstanceProfile struct { Demo bool `protobuf:"varint,3,opt,name=demo,proto3" json:"demo,omitempty"` // Instance URL is the URL of the instance. InstanceUrl string `protobuf:"bytes,6,opt,name=instance_url,json=instanceUrl,proto3" json:"instance_url,omitempty"` - // Indicates if the instance has completed first-time setup. - // When false, the instance requires initialization (creating the first admin account). - // This follows the pattern used by other self-hosted platforms for setup workflows. - Initialized bool `protobuf:"varint,7,opt,name=initialized,proto3" json:"initialized,omitempty"` + // The first administrator who set up this instance. + // When null, instance requires initial setup (creating the first admin account). + Admin *User `protobuf:"bytes,7,opt,name=admin,proto3" json:"admin,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -203,11 +202,11 @@ func (x *InstanceProfile) GetInstanceUrl() string { return "" } -func (x *InstanceProfile) GetInitialized() bool { +func (x *InstanceProfile) GetAdmin() *User { if x != nil { - return x.Initialized + return x.Admin } - return false + return nil } // Request for instance profile. @@ -876,12 +875,12 @@ var File_api_v1_instance_service_proto protoreflect.FileDescriptor const file_api_v1_instance_service_proto_rawDesc = "" + "\n" + - "\x1dapi/v1/instance_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a google/protobuf/field_mask.proto\"\x84\x01\n" + + "\x1dapi/v1/instance_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a google/protobuf/field_mask.proto\"\x8c\x01\n" + "\x0fInstanceProfile\x12\x18\n" + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + "\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" + - "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12 \n" + - "\vinitialized\x18\a \x01(\bR\vinitialized\"\x1b\n" + + "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12(\n" + + "\x05admin\x18\a \x01(\v2\x12.memos.api.v1.UserR\x05admin\"\x1b\n" + "\x19GetInstanceProfileRequest\"\x99\x0f\n" + "\x0fInstanceSetting\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" + @@ -971,28 +970,30 @@ var file_api_v1_instance_service_proto_goTypes = []any{ (*InstanceSetting_MemoRelatedSetting)(nil), // 9: memos.api.v1.InstanceSetting.MemoRelatedSetting (*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 10: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile (*InstanceSetting_StorageSetting_S3Config)(nil), // 11: memos.api.v1.InstanceSetting.StorageSetting.S3Config - (*fieldmaskpb.FieldMask)(nil), // 12: google.protobuf.FieldMask + (*User)(nil), // 12: memos.api.v1.User + (*fieldmaskpb.FieldMask)(nil), // 13: google.protobuf.FieldMask } var file_api_v1_instance_service_proto_depIdxs = []int32{ - 7, // 0: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting - 8, // 1: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting - 9, // 2: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting - 4, // 3: memos.api.v1.UpdateInstanceSettingRequest.setting:type_name -> memos.api.v1.InstanceSetting - 12, // 4: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask - 10, // 5: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile - 1, // 6: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType - 11, // 7: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config - 3, // 8: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest - 5, // 9: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest - 6, // 10: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest - 2, // 11: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile - 4, // 12: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting - 4, // 13: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting - 11, // [11:14] is the sub-list for method output_type - 8, // [8:11] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 12, // 0: memos.api.v1.InstanceProfile.admin:type_name -> memos.api.v1.User + 7, // 1: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting + 8, // 2: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting + 9, // 3: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting + 4, // 4: memos.api.v1.UpdateInstanceSettingRequest.setting:type_name -> memos.api.v1.InstanceSetting + 13, // 5: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask + 10, // 6: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile + 1, // 7: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType + 11, // 8: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config + 3, // 9: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest + 5, // 10: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest + 6, // 11: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest + 2, // 12: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile + 4, // 13: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting + 4, // 14: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting + 12, // [12:15] is the sub-list for method output_type + 9, // [9:12] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_api_v1_instance_service_proto_init() } @@ -1000,6 +1001,7 @@ func file_api_v1_instance_service_proto_init() { if File_api_v1_instance_service_proto != nil { return } + file_api_v1_user_service_proto_init() file_api_v1_instance_service_proto_msgTypes[2].OneofWrappers = []any{ (*InstanceSetting_GeneralSetting_)(nil), (*InstanceSetting_StorageSetting_)(nil), diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index 3cbc36ebf..d8ed9eed1 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -2141,12 +2141,12 @@ components: instanceUrl: type: string description: Instance URL is the URL of the instance. - initialized: - type: boolean + admin: + allOf: + - $ref: '#/components/schemas/User' description: |- - Indicates if the instance has completed first-time setup. - When false, the instance requires initialization (creating the first admin account). - This follows the pattern used by other self-hosted platforms for setup workflows. + The first administrator who set up this instance. + When null, instance requires initial setup (creating the first admin account). description: Instance profile message containing basic instance information. InstanceSetting: type: object diff --git a/server/router/api/v1/instance_service.go b/server/router/api/v1/instance_service.go index 0765706e5..520862771 100644 --- a/server/router/api/v1/instance_service.go +++ b/server/router/api/v1/instance_service.go @@ -3,7 +3,6 @@ package v1 import ( "context" "fmt" - "sync" "github.com/pkg/errors" "google.golang.org/grpc/codes" @@ -16,16 +15,16 @@ import ( // GetInstanceProfile returns the instance profile. func (s *APIV1Service) GetInstanceProfile(ctx context.Context, _ *v1pb.GetInstanceProfileRequest) (*v1pb.InstanceProfile, error) { - owner, err := s.GetInstanceOwner(ctx) + admin, err := s.GetInstanceAdmin(ctx) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get instance owner: %v", err) + return nil, status.Errorf(codes.Internal, "failed to get instance admin: %v", err) } instanceProfile := &v1pb.InstanceProfile{ Version: s.Profile.Version, Demo: s.Profile.Demo, InstanceUrl: s.Profile.InstanceURL, - Initialized: owner != nil, + Admin: admin, // nil when not initialized } return instanceProfile, nil } @@ -270,48 +269,17 @@ func convertInstanceMemoRelatedSettingToStore(setting *v1pb.InstanceSetting_Memo } } -var ( - ownerCache *v1pb.User - ownerCacheMutex sync.RWMutex -) - -func (s *APIV1Service) GetInstanceOwner(ctx context.Context) (*v1pb.User, error) { - // Try read lock first for cache hit - ownerCacheMutex.RLock() - if ownerCache != nil { - defer ownerCacheMutex.RUnlock() - return ownerCache, nil - } - ownerCacheMutex.RUnlock() - - // Upgrade to write lock to populate cache - ownerCacheMutex.Lock() - defer ownerCacheMutex.Unlock() - - // Double-check after acquiring write lock - if ownerCache != nil { - return ownerCache, nil - } - +func (s *APIV1Service) GetInstanceAdmin(ctx context.Context) (*v1pb.User, error) { adminUserType := store.RoleAdmin user, err := s.Store.GetUser(ctx, &store.FindUser{ Role: &adminUserType, }) if err != nil { - return nil, errors.Wrapf(err, "failed to find owner") + return nil, errors.Wrapf(err, "failed to find admin") } if user == nil { return nil, nil } - ownerCache = convertUserFromStore(user) - return ownerCache, nil -} - -// ClearInstanceOwnerCache clears the cached instance owner. -// This should be called when an admin user is created or when the owner changes. -func (*APIV1Service) ClearInstanceOwnerCache() { - ownerCacheMutex.Lock() - defer ownerCacheMutex.Unlock() - ownerCache = nil + return convertUserFromStore(user), nil } diff --git a/server/router/api/v1/test/instance_owner_cache_test.go b/server/router/api/v1/test/instance_admin_cache_test.go similarity index 54% rename from server/router/api/v1/test/instance_owner_cache_test.go rename to server/router/api/v1/test/instance_admin_cache_test.go index 032bd3435..5fa217160 100644 --- a/server/router/api/v1/test/instance_owner_cache_test.go +++ b/server/router/api/v1/test/instance_admin_cache_test.go @@ -9,7 +9,7 @@ import ( v1pb "github.com/usememos/memos/proto/gen/api/v1" ) -func TestInstanceOwnerCache(t *testing.T) { +func TestInstanceAdminRetrieval(t *testing.T) { ctx := context.Background() t.Run("Instance becomes initialized after first admin user is created", func(t *testing.T) { @@ -20,7 +20,7 @@ func TestInstanceOwnerCache(t *testing.T) { // Verify instance is not initialized initially profile1, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) require.NoError(t, err) - require.False(t, profile1.Initialized, "Instance should not be initialized before first admin user") + require.Nil(t, profile1.Admin, "Instance should not be initialized before first admin user") // Create the first admin user user, err := ts.CreateHostUser(ctx, "admin") @@ -30,29 +30,25 @@ func TestInstanceOwnerCache(t *testing.T) { // Verify instance is now initialized profile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) require.NoError(t, err) - require.True(t, profile2.Initialized, "Instance should be initialized after first admin user is created") + require.NotNil(t, profile2.Admin, "Instance should be initialized after first admin user is created") + require.Equal(t, user.Username, profile2.Admin.Username) }) - t.Run("ClearInstanceOwnerCache works correctly", func(t *testing.T) { + t.Run("Admin retrieval is cached by Store layer", func(t *testing.T) { // Create test service ts := NewTestService(t) defer ts.Cleanup() // Create admin user - _, err := ts.CreateHostUser(ctx, "admin") + user, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) - // Verify initialized - profile1, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) - require.NoError(t, err) - require.True(t, profile1.Initialized) - - // Clear cache - ts.Service.ClearInstanceOwnerCache() - - // Should still be initialized (cache is refilled from DB) - profile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) - require.NoError(t, err) - require.True(t, profile2.Initialized) + // Multiple calls should return consistent admin user (from cache) + for i := 0; i < 5; i++ { + profile, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) + require.NoError(t, err) + require.NotNil(t, profile.Admin) + require.Equal(t, user.Username, profile.Admin.Username) + } }) } diff --git a/server/router/api/v1/test/instance_service_test.go b/server/router/api/v1/test/instance_service_test.go index 383c25319..2043cf8b6 100644 --- a/server/router/api/v1/test/instance_service_test.go +++ b/server/router/api/v1/test/instance_service_test.go @@ -31,7 +31,7 @@ func TestGetInstanceProfile(t *testing.T) { require.Equal(t, "http://localhost:8080", resp.InstanceUrl) // Instance should not be initialized since no admin users are created - require.False(t, resp.Initialized) + require.Nil(t, resp.Admin) }) t.Run("GetInstanceProfile with initialized instance", func(t *testing.T) { @@ -58,7 +58,8 @@ func TestGetInstanceProfile(t *testing.T) { require.Equal(t, "http://localhost:8080", resp.InstanceUrl) // Instance should be initialized since an admin user exists - require.True(t, resp.Initialized) + require.NotNil(t, resp.Admin) + require.Equal(t, hostUser.Username, resp.Admin.Username) }) } @@ -101,7 +102,7 @@ func TestGetInstanceProfile_Concurrency(t *testing.T) { require.Equal(t, "test-1.0.0", resp.Version) require.True(t, resp.Demo) require.Equal(t, "http://localhost:8080", resp.InstanceUrl) - require.True(t, resp.Initialized) + require.NotNil(t, resp.Admin) } } }) diff --git a/server/router/api/v1/test/test_helper.go b/server/router/api/v1/test/test_helper.go index fabfaf0db..779ad2eea 100644 --- a/server/router/api/v1/test/test_helper.go +++ b/server/router/api/v1/test/test_helper.go @@ -48,9 +48,6 @@ func NewTestService(t *testing.T) *TestService { MarkdownService: markdownService, } - // Clear any cached state from previous tests - service.ClearInstanceOwnerCache() - return &TestService{ Service: service, Store: testStore, @@ -59,9 +56,8 @@ func NewTestService(t *testing.T) *TestService { } } -// Cleanup clears caches and closes resources after test. +// Cleanup closes resources after test. func (ts *TestService) Cleanup() { - ts.Service.ClearInstanceOwnerCache() ts.Store.Close() } diff --git a/server/router/api/v1/user_service.go b/server/router/api/v1/user_service.go index da0b85cb2..64e0e42e9 100644 --- a/server/router/api/v1/user_service.go +++ b/server/router/api/v1/user_service.go @@ -177,12 +177,6 @@ func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserR return nil, status.Errorf(codes.Internal, "failed to create user: %v", err) } - // If this is the first admin user being created, clear the owner cache - // so that GetInstanceProfile will return initialized=true - if roleToAssign == store.RoleAdmin { - s.ClearInstanceOwnerCache() - } - return convertUserFromStore(user), nil } diff --git a/web/src/App.tsx b/web/src/App.tsx index c94b77986..0acba201d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -22,10 +22,10 @@ const App = () => { // Redirect to sign up page if instance not initialized (no admin account exists yet) useEffect(() => { - if (!instanceProfile.initialized) { + if (!instanceProfile.admin) { navigateTo("/auth/signup"); } - }, [instanceProfile.initialized, navigateTo]); + }, [instanceProfile.admin, navigateTo]); useEffect(() => { if (instanceGeneralSetting.additionalStyle) { diff --git a/web/src/pages/SignUp.tsx b/web/src/pages/SignUp.tsx index c291cab23..b051da24e 100644 --- a/web/src/pages/SignUp.tsx +++ b/web/src/pages/SignUp.tsx @@ -135,7 +135,7 @@ const SignUp = () => { ) : (

Sign up is not allowed.

)} - {!profile.initialized ? ( + {!profile.admin ? (

{t("auth.host-tip")}

) : (

diff --git a/web/src/types/proto/api/v1/instance_service_pb.ts b/web/src/types/proto/api/v1/instance_service_pb.ts index 5d4163bf1..cee832955 100644 --- a/web/src/types/proto/api/v1/instance_service_pb.ts +++ b/web/src/types/proto/api/v1/instance_service_pb.ts @@ -4,6 +4,8 @@ import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { User } from "./user_service_pb"; +import { file_api_v1_user_service } from "./user_service_pb"; import { file_google_api_annotations } from "../../google/api/annotations_pb"; import { file_google_api_client } from "../../google/api/client_pb"; import { file_google_api_field_behavior } from "../../google/api/field_behavior_pb"; @@ -16,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file api/v1/instance_service.proto. */ export const file_api_v1_instance_service: GenFile = /*@__PURE__*/ - fileDesc("Ch1hcGkvdjEvaW5zdGFuY2Vfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxIlsKD0luc3RhbmNlUHJvZmlsZRIPCgd2ZXJzaW9uGAIgASgJEgwKBGRlbW8YAyABKAgSFAoMaW5zdGFuY2VfdXJsGAYgASgJEhMKC2luaXRpYWxpemVkGAcgASgIIhsKGUdldEluc3RhbmNlUHJvZmlsZVJlcXVlc3QiswsKD0luc3RhbmNlU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSRwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMiwubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5HZW5lcmFsU2V0dGluZ0gAEkcKD3N0b3JhZ2Vfc2V0dGluZxgDIAEoCzIsLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmdIABJQChRtZW1vX3JlbGF0ZWRfc2V0dGluZxgEIAEoCzIwLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuTWVtb1JlbGF0ZWRTZXR0aW5nSAAahwMKDkdlbmVyYWxTZXR0aW5nEiIKGmRpc2FsbG93X3VzZXJfcmVnaXN0cmF0aW9uGAIgASgIEh4KFmRpc2FsbG93X3Bhc3N3b3JkX2F1dGgYAyABKAgSGQoRYWRkaXRpb25hbF9zY3JpcHQYBCABKAkSGAoQYWRkaXRpb25hbF9zdHlsZRgFIAEoCRJSCg5jdXN0b21fcHJvZmlsZRgGIAEoCzI6Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuR2VuZXJhbFNldHRpbmcuQ3VzdG9tUHJvZmlsZRIdChV3ZWVrX3N0YXJ0X2RheV9vZmZzZXQYByABKAUSIAoYZGlzYWxsb3dfY2hhbmdlX3VzZXJuYW1lGAggASgIEiAKGGRpc2FsbG93X2NoYW5nZV9uaWNrbmFtZRgJIAEoCBpFCg1DdXN0b21Qcm9maWxlEg0KBXRpdGxlGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhAKCGxvZ29fdXJsGAMgASgJGroDCg5TdG9yYWdlU2V0dGluZxJOCgxzdG9yYWdlX3R5cGUYASABKA4yOC5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLlN0b3JhZ2VTZXR0aW5nLlN0b3JhZ2VUeXBlEhkKEWZpbGVwYXRoX3RlbXBsYXRlGAIgASgJEhwKFHVwbG9hZF9zaXplX2xpbWl0X21iGAMgASgDEkgKCXMzX2NvbmZpZxgEIAEoCzI1Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmcuUzNDb25maWcahgEKCFMzQ29uZmlnEhUKDWFjY2Vzc19rZXlfaWQYASABKAkSGQoRYWNjZXNzX2tleV9zZWNyZXQYAiABKAkSEAoIZW5kcG9pbnQYAyABKAkSDgoGcmVnaW9uGAQgASgJEg4KBmJ1Y2tldBgFIAEoCRIWCg51c2VfcGF0aF9zdHlsZRgGIAEoCCJMCgtTdG9yYWdlVHlwZRIcChhTVE9SQUdFX1RZUEVfVU5TUEVDSUZJRUQQABIMCghEQVRBQkFTRRABEgkKBUxPQ0FMEAISBgoCUzMQAxqtAQoSTWVtb1JlbGF0ZWRTZXR0aW5nEiIKGmRpc2FsbG93X3B1YmxpY192aXNpYmlsaXR5GAEgASgIEiAKGGRpc3BsYXlfd2l0aF91cGRhdGVfdGltZRgCIAEoCBIcChRjb250ZW50X2xlbmd0aF9saW1pdBgDIAEoBRIgChhlbmFibGVfZG91YmxlX2NsaWNrX2VkaXQYBCABKAgSEQoJcmVhY3Rpb25zGAcgAygJIkYKA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESCwoHU1RPUkFHRRACEhAKDE1FTU9fUkVMQVRFRBADOmHqQV4KHG1lbW9zLmFwaS52MS9JbnN0YW5jZVNldHRpbmcSG2luc3RhbmNlL3NldHRpbmdzL3tzZXR0aW5nfSoQaW5zdGFuY2VTZXR0aW5nczIPaW5zdGFuY2VTZXR0aW5nQgcKBXZhbHVlIk8KGUdldEluc3RhbmNlU2V0dGluZ1JlcXVlc3QSMgoEbmFtZRgBIAEoCUIk4EEC+kEeChxtZW1vcy5hcGkudjEvSW5zdGFuY2VTZXR0aW5nIokBChxVcGRhdGVJbnN0YW5jZVNldHRpbmdSZXF1ZXN0EjMKB3NldHRpbmcYASABKAsyHS5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQEy2wMKD0luc3RhbmNlU2VydmljZRJ+ChJHZXRJbnN0YW5jZVByb2ZpbGUSJy5tZW1vcy5hcGkudjEuR2V0SW5zdGFuY2VQcm9maWxlUmVxdWVzdBodLm1lbW9zLmFwaS52MS5JbnN0YW5jZVByb2ZpbGUiIILT5JMCGhIYL2FwaS92MS9pbnN0YW5jZS9wcm9maWxlEo8BChJHZXRJbnN0YW5jZVNldHRpbmcSJy5tZW1vcy5hcGkudjEuR2V0SW5zdGFuY2VTZXR0aW5nUmVxdWVzdBodLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmciMdpBBG5hbWWC0+STAiQSIi9hcGkvdjEve25hbWU9aW5zdGFuY2Uvc2V0dGluZ3MvKn0StQEKFVVwZGF0ZUluc3RhbmNlU2V0dGluZxIqLm1lbW9zLmFwaS52MS5VcGRhdGVJbnN0YW5jZVNldHRpbmdSZXF1ZXN0Gh0ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZyJR2kETc2V0dGluZyx1cGRhdGVfbWFza4LT5JMCNToHc2V0dGluZzIqL2FwaS92MS97c2V0dGluZy5uYW1lPWluc3RhbmNlL3NldHRpbmdzLyp9QqwBChBjb20ubWVtb3MuYXBpLnYxQhRJbnN0YW5jZVNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z", [file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_field_mask]); + fileDesc("Ch1hcGkvdjEvaW5zdGFuY2Vfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxImkKD0luc3RhbmNlUHJvZmlsZRIPCgd2ZXJzaW9uGAIgASgJEgwKBGRlbW8YAyABKAgSFAoMaW5zdGFuY2VfdXJsGAYgASgJEiEKBWFkbWluGAcgASgLMhIubWVtb3MuYXBpLnYxLlVzZXIiGwoZR2V0SW5zdGFuY2VQcm9maWxlUmVxdWVzdCKzCwoPSW5zdGFuY2VTZXR0aW5nEhEKBG5hbWUYASABKAlCA+BBCBJHCg9nZW5lcmFsX3NldHRpbmcYAiABKAsyLC5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLkdlbmVyYWxTZXR0aW5nSAASRwoPc3RvcmFnZV9zZXR0aW5nGAMgASgLMiwubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5TdG9yYWdlU2V0dGluZ0gAElAKFG1lbW9fcmVsYXRlZF9zZXR0aW5nGAQgASgLMjAubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5NZW1vUmVsYXRlZFNldHRpbmdIABqHAwoOR2VuZXJhbFNldHRpbmcSIgoaZGlzYWxsb3dfdXNlcl9yZWdpc3RyYXRpb24YAiABKAgSHgoWZGlzYWxsb3dfcGFzc3dvcmRfYXV0aBgDIAEoCBIZChFhZGRpdGlvbmFsX3NjcmlwdBgEIAEoCRIYChBhZGRpdGlvbmFsX3N0eWxlGAUgASgJElIKDmN1c3RvbV9wcm9maWxlGAYgASgLMjoubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5HZW5lcmFsU2V0dGluZy5DdXN0b21Qcm9maWxlEh0KFXdlZWtfc3RhcnRfZGF5X29mZnNldBgHIAEoBRIgChhkaXNhbGxvd19jaGFuZ2VfdXNlcm5hbWUYCCABKAgSIAoYZGlzYWxsb3dfY2hhbmdlX25pY2tuYW1lGAkgASgIGkUKDUN1c3RvbVByb2ZpbGUSDQoFdGl0bGUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSEAoIbG9nb191cmwYAyABKAkaugMKDlN0b3JhZ2VTZXR0aW5nEk4KDHN0b3JhZ2VfdHlwZRgBIAEoDjI4Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmcuU3RvcmFnZVR5cGUSGQoRZmlsZXBhdGhfdGVtcGxhdGUYAiABKAkSHAoUdXBsb2FkX3NpemVfbGltaXRfbWIYAyABKAMSSAoJczNfY29uZmlnGAQgASgLMjUubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5TdG9yYWdlU2V0dGluZy5TM0NvbmZpZxqGAQoIUzNDb25maWcSFQoNYWNjZXNzX2tleV9pZBgBIAEoCRIZChFhY2Nlc3Nfa2V5X3NlY3JldBgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIOCgZyZWdpb24YBCABKAkSDgoGYnVja2V0GAUgASgJEhYKDnVzZV9wYXRoX3N0eWxlGAYgASgIIkwKC1N0b3JhZ2VUeXBlEhwKGFNUT1JBR0VfVFlQRV9VTlNQRUNJRklFRBAAEgwKCERBVEFCQVNFEAESCQoFTE9DQUwQAhIGCgJTMxADGq0BChJNZW1vUmVsYXRlZFNldHRpbmcSIgoaZGlzYWxsb3dfcHVibGljX3Zpc2liaWxpdHkYASABKAgSIAoYZGlzcGxheV93aXRoX3VwZGF0ZV90aW1lGAIgASgIEhwKFGNvbnRlbnRfbGVuZ3RoX2xpbWl0GAMgASgFEiAKGGVuYWJsZV9kb3VibGVfY2xpY2tfZWRpdBgEIAEoCBIRCglyZWFjdGlvbnMYByADKAkiRgoDS2V5EhMKD0tFWV9VTlNQRUNJRklFRBAAEgsKB0dFTkVSQUwQARILCgdTVE9SQUdFEAISEAoMTUVNT19SRUxBVEVEEAM6YepBXgocbWVtb3MuYXBpLnYxL0luc3RhbmNlU2V0dGluZxIbaW5zdGFuY2Uvc2V0dGluZ3Mve3NldHRpbmd9KhBpbnN0YW5jZVNldHRpbmdzMg9pbnN0YW5jZVNldHRpbmdCBwoFdmFsdWUiTwoZR2V0SW5zdGFuY2VTZXR0aW5nUmVxdWVzdBIyCgRuYW1lGAEgASgJQiTgQQL6QR4KHG1lbW9zLmFwaS52MS9JbnN0YW5jZVNldHRpbmciiQEKHFVwZGF0ZUluc3RhbmNlU2V0dGluZ1JlcXVlc3QSMwoHc2V0dGluZxgBIAEoCzIdLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmdCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBATLbAwoPSW5zdGFuY2VTZXJ2aWNlEn4KEkdldEluc3RhbmNlUHJvZmlsZRInLm1lbW9zLmFwaS52MS5HZXRJbnN0YW5jZVByb2ZpbGVSZXF1ZXN0Gh0ubWVtb3MuYXBpLnYxLkluc3RhbmNlUHJvZmlsZSIggtPkkwIaEhgvYXBpL3YxL2luc3RhbmNlL3Byb2ZpbGUSjwEKEkdldEluc3RhbmNlU2V0dGluZxInLm1lbW9zLmFwaS52MS5HZXRJbnN0YW5jZVNldHRpbmdSZXF1ZXN0Gh0ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZyIx2kEEbmFtZYLT5JMCJBIiL2FwaS92MS97bmFtZT1pbnN0YW5jZS9zZXR0aW5ncy8qfRK1AQoVVXBkYXRlSW5zdGFuY2VTZXR0aW5nEioubWVtb3MuYXBpLnYxLlVwZGF0ZUluc3RhbmNlU2V0dGluZ1JlcXVlc3QaHS5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nIlHaQRNzZXR0aW5nLHVwZGF0ZV9tYXNrgtPkkwI1OgdzZXR0aW5nMiovYXBpL3YxL3tzZXR0aW5nLm5hbWU9aW5zdGFuY2Uvc2V0dGluZ3MvKn1CrAEKEGNvbS5tZW1vcy5hcGkudjFCFEluc3RhbmNlU2VydmljZVByb3RvUAFaMGdpdGh1Yi5jb20vdXNlbWVtb3MvbWVtb3MvcHJvdG8vZ2VuL2FwaS92MTthcGl2MaICA01BWKoCDE1lbW9zLkFwaS5WMcoCDE1lbW9zXEFwaVxWMeICGE1lbW9zXEFwaVxWMVxHUEJNZXRhZGF0YeoCDk1lbW9zOjpBcGk6OlYxYgZwcm90bzM", [file_api_v1_user_service, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_field_mask]); /** * Instance profile message containing basic instance information. @@ -46,13 +48,12 @@ export type InstanceProfile = Message<"memos.api.v1.InstanceProfile"> & { instanceUrl: string; /** - * Indicates if the instance has completed first-time setup. - * When false, the instance requires initialization (creating the first admin account). - * This follows the pattern used by other self-hosted platforms for setup workflows. + * The first administrator who set up this instance. + * When null, instance requires initial setup (creating the first admin account). * - * @generated from field: bool initialized = 7; + * @generated from field: memos.api.v1.User admin = 7; */ - initialized: boolean; + admin?: User; }; /**