feat: update InstanceProfile to include initialization status

- Removed the owner field from InstanceProfile as it is no longer needed.
- Added an initialized field to InstanceProfile to indicate if the instance has completed first-time setup.
- Updated GetInstanceProfile method to set initialized based on the existence of an admin user.
- Modified tests to reflect changes in InstanceProfile and ensure correct behavior regarding instance initialization.
- Adjusted frontend logic to redirect users based on the initialized status instead of the owner field.
This commit is contained in:
Steven 2026-01-22 20:59:40 +08:00
parent c240b70591
commit ba099b72ed
25 changed files with 151 additions and 73 deletions

View File

@ -34,10 +34,6 @@ service InstanceService {
// Instance profile message containing basic instance information.
message InstanceProfile {
// The name of instance owner.
// Format: users/{user}
string owner = 1;
// Version is the current version of instance.
string version = 2;
@ -46,6 +42,11 @@ 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;
}
// Request for instance profile.

View File

@ -138,15 +138,16 @@ func (InstanceSetting_StorageSetting_StorageType) EnumDescriptor() ([]byte, []in
// Instance profile message containing basic instance information.
type InstanceProfile struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The name of instance owner.
// Format: users/{user}
Owner string `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty"`
// Version is the current version of instance.
Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"`
// Demo indicates if the instance is in demo mode.
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"`
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"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -181,13 +182,6 @@ func (*InstanceProfile) Descriptor() ([]byte, []int) {
return file_api_v1_instance_service_proto_rawDescGZIP(), []int{0}
}
func (x *InstanceProfile) GetOwner() string {
if x != nil {
return x.Owner
}
return ""
}
func (x *InstanceProfile) GetVersion() string {
if x != nil {
return x.Version
@ -209,6 +203,13 @@ func (x *InstanceProfile) GetInstanceUrl() string {
return ""
}
func (x *InstanceProfile) GetInitialized() bool {
if x != nil {
return x.Initialized
}
return false
}
// Request for instance profile.
type GetInstanceProfileRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -875,12 +876,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\"x\n" +
"\x0fInstanceProfile\x12\x14\n" +
"\x05owner\x18\x01 \x01(\tR\x05owner\x12\x18\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" +
"\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\"\x1b\n" +
"\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12 \n" +
"\vinitialized\x18\a \x01(\bR\vinitialized\"\x1b\n" +
"\x19GetInstanceProfileRequest\"\x99\x0f\n" +
"\x0fInstanceSetting\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" +

View File

@ -2132,11 +2132,6 @@ components:
InstanceProfile:
type: object
properties:
owner:
type: string
description: |-
The name of instance owner.
Format: users/{user}
version:
type: string
description: Version is the current version of instance.
@ -2146,6 +2141,12 @@ components:
instanceUrl:
type: string
description: Instance URL is the URL of the instance.
initialized:
type: boolean
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.
description: Instance profile message containing basic instance information.
InstanceSetting:
type: object

View File

@ -15,17 +15,16 @@ import (
// GetInstanceProfile returns the instance profile.
func (s *APIV1Service) GetInstanceProfile(ctx context.Context, _ *v1pb.GetInstanceProfileRequest) (*v1pb.InstanceProfile, error) {
instanceProfile := &v1pb.InstanceProfile{
Version: s.Profile.Version,
Demo: s.Profile.Demo,
InstanceUrl: s.Profile.InstanceURL,
}
owner, err := s.GetInstanceOwner(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance owner: %v", err)
}
if owner != nil {
instanceProfile.Owner = owner.Name
instanceProfile := &v1pb.InstanceProfile{
Version: s.Profile.Version,
Demo: s.Profile.Demo,
InstanceUrl: s.Profile.InstanceURL,
Initialized: owner != nil,
}
return instanceProfile, nil
}
@ -291,3 +290,9 @@ func (s *APIV1Service) GetInstanceOwner(ctx context.Context) (*v1pb.User, error)
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 (s *APIV1Service) ClearInstanceOwnerCache() {
ownerCache = nil
}

View File

@ -0,0 +1,58 @@
package test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
)
func TestInstanceOwnerCache(t *testing.T) {
ctx := context.Background()
t.Run("Instance becomes initialized after first admin user is created", func(t *testing.T) {
// Create test service
ts := NewTestService(t)
defer ts.Cleanup()
// 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")
// Create the first admin user
user, err := ts.CreateHostUser(ctx, "admin")
require.NoError(t, err)
require.NotNil(t, user)
// 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")
})
t.Run("ClearInstanceOwnerCache works correctly", func(t *testing.T) {
// Create test service
ts := NewTestService(t)
defer ts.Cleanup()
// Create 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)
})
}

View File

@ -2,7 +2,6 @@ package test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
@ -31,11 +30,11 @@ func TestGetInstanceProfile(t *testing.T) {
require.True(t, resp.Demo)
require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
// Owner should be empty since no users are created
require.Empty(t, resp.Owner)
// Instance should not be initialized since no admin users are created
require.False(t, resp.Initialized)
})
t.Run("GetInstanceProfile with owner", func(t *testing.T) {
t.Run("GetInstanceProfile with initialized instance", func(t *testing.T) {
// Create test service for this specific test
ts := NewTestService(t)
defer ts.Cleanup()
@ -53,14 +52,13 @@ func TestGetInstanceProfile(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, resp)
// Verify the response contains expected data including owner
// Verify the response contains expected data with initialized flag
require.Equal(t, "test-1.0.0", resp.Version)
require.True(t, resp.Demo)
require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
// User name should be "users/{id}" format where id is the user's ID
expectedOwnerName := fmt.Sprintf("users/%d", hostUser.ID)
require.Equal(t, expectedOwnerName, resp.Owner)
// Instance should be initialized since an admin user exists
require.True(t, resp.Initialized)
})
}
@ -73,9 +71,8 @@ func TestGetInstanceProfile_Concurrency(t *testing.T) {
defer ts.Cleanup()
// Create a host user
hostUser, err := ts.CreateHostUser(ctx, "admin")
_, err := ts.CreateHostUser(ctx, "admin")
require.NoError(t, err)
expectedOwnerName := fmt.Sprintf("users/%d", hostUser.ID)
// Make concurrent requests
numGoroutines := 10
@ -104,7 +101,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.Equal(t, expectedOwnerName, resp.Owner)
require.True(t, resp.Initialized)
}
}
})

View File

@ -177,6 +177,12 @@ 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
}

View File

@ -20,12 +20,12 @@ const App = () => {
cleanupExpiredOAuthState();
}, []);
// Redirect to sign up page if no instance owner
// Redirect to sign up page if instance not initialized (no admin account exists yet)
useEffect(() => {
if (!instanceProfile.owner) {
if (!instanceProfile.initialized) {
navigateTo("/auth/signup");
}
}, [instanceProfile.owner, navigateTo]);
}, [instanceProfile.initialized, navigateTo]);
useEffect(() => {
if (instanceGeneralSetting.additionalStyle) {

View File

@ -1,9 +1,9 @@
import { MemoRenderContext } from "@/components/MasonryView";
import MemoView from "@/components/MemoView";
import PagedMemoList from "@/components/PagedMemoList";
import { useInstance } from "@/contexts/InstanceContext";
import { useMemoFilters, useMemoSorting } from "@/hooks";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useInstance } from "@/contexts/InstanceContext";
import { State } from "@/types/proto/api/v1/common_pb";
import { Memo } from "@/types/proto/api/v1/memo_service_pb";

View File

@ -9,18 +9,22 @@ import AuthFooter from "@/components/AuthFooter";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { authServiceClient, userServiceClient } from "@/connect";
import { useAuth } from "@/contexts/AuthContext";
import { useInstance } from "@/contexts/InstanceContext";
import useLoading from "@/hooks/useLoading";
import useNavigateTo from "@/hooks/useNavigateTo";
import { handleError } from "@/lib/error";
import { User_Role, UserSchema } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
const SignUp = () => {
const t = useTranslate();
const navigateTo = useNavigateTo();
const actionBtnLoadingState = useLoading(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const { generalSetting: instanceGeneralSetting, profile } = useInstance();
const { initialize: initAuth } = useAuth();
const { generalSetting: instanceGeneralSetting, profile, initialize: initInstance } = useInstance();
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string;
@ -64,7 +68,11 @@ const SignUp = () => {
if (response.accessToken) {
setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);
}
window.location.href = "/";
// Refresh auth context to load the current user
await initAuth();
// Refetch instance profile to update the initialized status
await initInstance();
navigateTo("/");
} catch (error: unknown) {
handleError(error, toast.error, {
fallbackMessage: "Sign up failed",
@ -127,7 +135,7 @@ const SignUp = () => {
) : (
<p className="w-full text-2xl mt-2 text-muted-foreground">Sign up is not allowed.</p>
)}
{!profile.owner ? (
{!profile.initialized ? (
<p className="w-full mt-4 text-sm font-medium text-muted-foreground">{t("auth.host-tip")}</p>
) : (
<p className="w-full mt-4 text-sm">

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/activity_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/attachment_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/auth_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/common.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/idp_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/instance_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
@ -16,7 +16,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("Ch1hcGkvdjEvaW5zdGFuY2Vfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxIlUKD0luc3RhbmNlUHJvZmlsZRINCgVvd25lchgBIAEoCRIPCgd2ZXJzaW9uGAIgASgJEgwKBGRlbW8YAyABKAgSFAoMaW5zdGFuY2VfdXJsGAYgASgJIhsKGUdldEluc3RhbmNlUHJvZmlsZVJlcXVlc3QiswsKD0luc3RhbmNlU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSRwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMiwubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5HZW5lcmFsU2V0dGluZ0gAEkcKD3N0b3JhZ2Vfc2V0dGluZxgDIAEoCzIsLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmdIABJQChRtZW1vX3JlbGF0ZWRfc2V0dGluZxgEIAEoCzIwLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuTWVtb1JlbGF0ZWRTZXR0aW5nSAAahwMKDkdlbmVyYWxTZXR0aW5nEiIKGmRpc2FsbG93X3VzZXJfcmVnaXN0cmF0aW9uGAIgASgIEh4KFmRpc2FsbG93X3Bhc3N3b3JkX2F1dGgYAyABKAgSGQoRYWRkaXRpb25hbF9zY3JpcHQYBCABKAkSGAoQYWRkaXRpb25hbF9zdHlsZRgFIAEoCRJSCg5jdXN0b21fcHJvZmlsZRgGIAEoCzI6Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuR2VuZXJhbFNldHRpbmcuQ3VzdG9tUHJvZmlsZRIdChV3ZWVrX3N0YXJ0X2RheV9vZmZzZXQYByABKAUSIAoYZGlzYWxsb3dfY2hhbmdlX3VzZXJuYW1lGAggASgIEiAKGGRpc2FsbG93X2NoYW5nZV9uaWNrbmFtZRgJIAEoCBpFCg1DdXN0b21Qcm9maWxlEg0KBXRpdGxlGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhAKCGxvZ29fdXJsGAMgASgJGroDCg5TdG9yYWdlU2V0dGluZxJOCgxzdG9yYWdlX3R5cGUYASABKA4yOC5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLlN0b3JhZ2VTZXR0aW5nLlN0b3JhZ2VUeXBlEhkKEWZpbGVwYXRoX3RlbXBsYXRlGAIgASgJEhwKFHVwbG9hZF9zaXplX2xpbWl0X21iGAMgASgDEkgKCXMzX2NvbmZpZxgEIAEoCzI1Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmcuUzNDb25maWcahgEKCFMzQ29uZmlnEhUKDWFjY2Vzc19rZXlfaWQYASABKAkSGQoRYWNjZXNzX2tleV9zZWNyZXQYAiABKAkSEAoIZW5kcG9pbnQYAyABKAkSDgoGcmVnaW9uGAQgASgJEg4KBmJ1Y2tldBgFIAEoCRIWCg51c2VfcGF0aF9zdHlsZRgGIAEoCCJMCgtTdG9yYWdlVHlwZRIcChhTVE9SQUdFX1RZUEVfVU5TUEVDSUZJRUQQABIMCghEQVRBQkFTRRABEgkKBUxPQ0FMEAISBgoCUzMQAxqtAQoSTWVtb1JlbGF0ZWRTZXR0aW5nEiIKGmRpc2FsbG93X3B1YmxpY192aXNpYmlsaXR5GAEgASgIEiAKGGRpc3BsYXlfd2l0aF91cGRhdGVfdGltZRgCIAEoCBIcChRjb250ZW50X2xlbmd0aF9saW1pdBgDIAEoBRIgChhlbmFibGVfZG91YmxlX2NsaWNrX2VkaXQYBCABKAgSEQoJcmVhY3Rpb25zGAcgAygJIkYKA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESCwoHU1RPUkFHRRACEhAKDE1FTU9fUkVMQVRFRBADOmHqQV4KHG1lbW9zLmFwaS52MS9JbnN0YW5jZVNldHRpbmcSG2luc3RhbmNlL3NldHRpbmdzL3tzZXR0aW5nfSoQaW5zdGFuY2VTZXR0aW5nczIPaW5zdGFuY2VTZXR0aW5nQgcKBXZhbHVlIk8KGUdldEluc3RhbmNlU2V0dGluZ1JlcXVlc3QSMgoEbmFtZRgBIAEoCUIk4EEC+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("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]);
/**
* Instance profile message containing basic instance information.
@ -24,14 +24,6 @@ export const file_api_v1_instance_service: GenFile = /*@__PURE__*/
* @generated from message memos.api.v1.InstanceProfile
*/
export type InstanceProfile = Message<"memos.api.v1.InstanceProfile"> & {
/**
* The name of instance owner.
* Format: users/{user}
*
* @generated from field: string owner = 1;
*/
owner: string;
/**
* Version is the current version of instance.
*
@ -52,6 +44,15 @@ export type InstanceProfile = Message<"memos.api.v1.InstanceProfile"> & {
* @generated from field: string instance_url = 6;
*/
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.
*
* @generated from field: bool initialized = 7;
*/
initialized: boolean;
};
/**

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/memo_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/shortcut_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file api/v1/user_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file google/api/annotations.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file google/api/client.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file google/api/field_behavior.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file google/api/http.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file google/api/launch_stage.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file google/api/resource.proto (package google.api, syntax proto3)
/* eslint-disable */