Commit b0558824 authored by Steven's avatar Steven

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
parent 81022123
...@@ -2,6 +2,7 @@ syntax = "proto3"; ...@@ -2,6 +2,7 @@ syntax = "proto3";
package memos.api.v1; package memos.api.v1;
import "api/v1/user_service.proto";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/api/client.proto"; import "google/api/client.proto";
import "google/api/field_behavior.proto"; import "google/api/field_behavior.proto";
...@@ -43,10 +44,9 @@ message InstanceProfile { ...@@ -43,10 +44,9 @@ message InstanceProfile {
// Instance URL is the URL of the instance. // Instance URL is the URL of the instance.
string instance_url = 6; string instance_url = 6;
// Indicates if the instance has completed first-time setup. // The first administrator who set up this instance.
// When false, the instance requires initialization (creating the first admin account). // When null, instance requires initial setup (creating the first admin account).
// This follows the pattern used by other self-hosted platforms for setup workflows. User admin = 7;
bool initialized = 7;
} }
// Request for instance profile. // Request for instance profile.
......
...@@ -144,10 +144,9 @@ type InstanceProfile struct { ...@@ -144,10 +144,9 @@ type InstanceProfile struct {
Demo bool `protobuf:"varint,3,opt,name=demo,proto3" json:"demo,omitempty"` Demo bool `protobuf:"varint,3,opt,name=demo,proto3" json:"demo,omitempty"`
// Instance URL is the URL of the instance. // 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. // The first administrator who set up this instance.
// When false, the instance requires initialization (creating the first admin account). // When null, instance requires initial setup (creating the first admin account).
// This follows the pattern used by other self-hosted platforms for setup workflows. Admin *User `protobuf:"bytes,7,opt,name=admin,proto3" json:"admin,omitempty"`
Initialized bool `protobuf:"varint,7,opt,name=initialized,proto3" json:"initialized,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
...@@ -203,11 +202,11 @@ func (x *InstanceProfile) GetInstanceUrl() string { ...@@ -203,11 +202,11 @@ func (x *InstanceProfile) GetInstanceUrl() string {
return "" return ""
} }
func (x *InstanceProfile) GetInitialized() bool { func (x *InstanceProfile) GetAdmin() *User {
if x != nil { if x != nil {
return x.Initialized return x.Admin
} }
return false return nil
} }
// Request for instance profile. // Request for instance profile.
...@@ -876,12 +875,12 @@ var File_api_v1_instance_service_proto protoreflect.FileDescriptor ...@@ -876,12 +875,12 @@ var File_api_v1_instance_service_proto protoreflect.FileDescriptor
const file_api_v1_instance_service_proto_rawDesc = "" + const file_api_v1_instance_service_proto_rawDesc = "" +
"\n" + "\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" + "\x0fInstanceProfile\x12\x18\n" +
"\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" +
"\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" + "\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" +
"\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12 \n" + "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12(\n" +
"\vinitialized\x18\a \x01(\bR\vinitialized\"\x1b\n" + "\x05admin\x18\a \x01(\v2\x12.memos.api.v1.UserR\x05admin\"\x1b\n" +
"\x19GetInstanceProfileRequest\"\x99\x0f\n" + "\x19GetInstanceProfileRequest\"\x99\x0f\n" +
"\x0fInstanceSetting\x12\x17\n" + "\x0fInstanceSetting\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\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{ ...@@ -971,28 +970,30 @@ var file_api_v1_instance_service_proto_goTypes = []any{
(*InstanceSetting_MemoRelatedSetting)(nil), // 9: memos.api.v1.InstanceSetting.MemoRelatedSetting (*InstanceSetting_MemoRelatedSetting)(nil), // 9: memos.api.v1.InstanceSetting.MemoRelatedSetting
(*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 10: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile (*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 10: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile
(*InstanceSetting_StorageSetting_S3Config)(nil), // 11: memos.api.v1.InstanceSetting.StorageSetting.S3Config (*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{ var file_api_v1_instance_service_proto_depIdxs = []int32{
7, // 0: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting 12, // 0: memos.api.v1.InstanceProfile.admin:type_name -> memos.api.v1.User
8, // 1: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting 7, // 1: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting
9, // 2: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting 8, // 2: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting
4, // 3: memos.api.v1.UpdateInstanceSettingRequest.setting:type_name -> memos.api.v1.InstanceSetting 9, // 3: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting
12, // 4: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask 4, // 4: memos.api.v1.UpdateInstanceSettingRequest.setting:type_name -> memos.api.v1.InstanceSetting
10, // 5: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile 13, // 5: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
1, // 6: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType 10, // 6: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile
11, // 7: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config 1, // 7: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType
3, // 8: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest 11, // 8: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config
5, // 9: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest 3, // 9: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest
6, // 10: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest 5, // 10: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest
2, // 11: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile 6, // 11: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest
4, // 12: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting 2, // 12: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile
4, // 13: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting 4, // 13: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting
11, // [11:14] is the sub-list for method output_type 4, // 14: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting
8, // [8:11] is the sub-list for method input_type 12, // [12:15] is the sub-list for method output_type
8, // [8:8] is the sub-list for extension type_name 9, // [9:12] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension extendee 9, // [9:9] is the sub-list for extension type_name
0, // [0:8] is the sub-list for field 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() } func init() { file_api_v1_instance_service_proto_init() }
...@@ -1000,6 +1001,7 @@ func 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 { if File_api_v1_instance_service_proto != nil {
return return
} }
file_api_v1_user_service_proto_init()
file_api_v1_instance_service_proto_msgTypes[2].OneofWrappers = []any{ file_api_v1_instance_service_proto_msgTypes[2].OneofWrappers = []any{
(*InstanceSetting_GeneralSetting_)(nil), (*InstanceSetting_GeneralSetting_)(nil),
(*InstanceSetting_StorageSetting_)(nil), (*InstanceSetting_StorageSetting_)(nil),
......
...@@ -2141,12 +2141,12 @@ components: ...@@ -2141,12 +2141,12 @@ components:
instanceUrl: instanceUrl:
type: string type: string
description: Instance URL is the URL of the instance. description: Instance URL is the URL of the instance.
initialized: admin:
type: boolean allOf:
- $ref: '#/components/schemas/User'
description: |- description: |-
Indicates if the instance has completed first-time setup. The first administrator who set up this instance.
When false, the instance requires initialization (creating the first admin account). When null, instance requires initial setup (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. description: Instance profile message containing basic instance information.
InstanceSetting: InstanceSetting:
type: object type: object
......
...@@ -3,7 +3,6 @@ package v1 ...@@ -3,7 +3,6 @@ package v1
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
...@@ -16,16 +15,16 @@ import ( ...@@ -16,16 +15,16 @@ import (
// GetInstanceProfile returns the instance profile. // GetInstanceProfile returns the instance profile.
func (s *APIV1Service) GetInstanceProfile(ctx context.Context, _ *v1pb.GetInstanceProfileRequest) (*v1pb.InstanceProfile, error) { 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 { 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{ instanceProfile := &v1pb.InstanceProfile{
Version: s.Profile.Version, Version: s.Profile.Version,
Demo: s.Profile.Demo, Demo: s.Profile.Demo,
InstanceUrl: s.Profile.InstanceURL, InstanceUrl: s.Profile.InstanceURL,
Initialized: owner != nil, Admin: admin, // nil when not initialized
} }
return instanceProfile, nil return instanceProfile, nil
} }
...@@ -270,48 +269,17 @@ func convertInstanceMemoRelatedSettingToStore(setting *v1pb.InstanceSetting_Memo ...@@ -270,48 +269,17 @@ func convertInstanceMemoRelatedSettingToStore(setting *v1pb.InstanceSetting_Memo
} }
} }
var ( func (s *APIV1Service) GetInstanceAdmin(ctx context.Context) (*v1pb.User, error) {
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
}
adminUserType := store.RoleAdmin adminUserType := store.RoleAdmin
user, err := s.Store.GetUser(ctx, &store.FindUser{ user, err := s.Store.GetUser(ctx, &store.FindUser{
Role: &adminUserType, Role: &adminUserType,
}) })
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to find owner") return nil, errors.Wrapf(err, "failed to find admin")
} }
if user == nil { if user == nil {
return nil, nil return nil, nil
} }
ownerCache = convertUserFromStore(user) return convertUserFromStore(user), nil
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
} }
...@@ -9,7 +9,7 @@ import ( ...@@ -9,7 +9,7 @@ import (
v1pb "github.com/usememos/memos/proto/gen/api/v1" v1pb "github.com/usememos/memos/proto/gen/api/v1"
) )
func TestInstanceOwnerCache(t *testing.T) { func TestInstanceAdminRetrieval(t *testing.T) {
ctx := context.Background() ctx := context.Background()
t.Run("Instance becomes initialized after first admin user is created", func(t *testing.T) { t.Run("Instance becomes initialized after first admin user is created", func(t *testing.T) {
...@@ -20,7 +20,7 @@ func TestInstanceOwnerCache(t *testing.T) { ...@@ -20,7 +20,7 @@ func TestInstanceOwnerCache(t *testing.T) {
// Verify instance is not initialized initially // Verify instance is not initialized initially
profile1, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) profile1, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})
require.NoError(t, err) 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 // Create the first admin user
user, err := ts.CreateHostUser(ctx, "admin") user, err := ts.CreateHostUser(ctx, "admin")
...@@ -30,29 +30,25 @@ func TestInstanceOwnerCache(t *testing.T) { ...@@ -30,29 +30,25 @@ func TestInstanceOwnerCache(t *testing.T) {
// Verify instance is now initialized // Verify instance is now initialized
profile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) profile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})
require.NoError(t, err) 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 // Create test service
ts := NewTestService(t) ts := NewTestService(t)
defer ts.Cleanup() defer ts.Cleanup()
// Create admin user // 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.NoError(t, err)
require.True(t, profile1.Initialized)
// Clear cache // Multiple calls should return consistent admin user (from cache)
ts.Service.ClearInstanceOwnerCache() for i := 0; i < 5; i++ {
profile, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})
// Should still be initialized (cache is refilled from DB) require.NoError(t, err)
profile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) require.NotNil(t, profile.Admin)
require.NoError(t, err) require.Equal(t, user.Username, profile.Admin.Username)
require.True(t, profile2.Initialized) }
}) })
} }
...@@ -31,7 +31,7 @@ func TestGetInstanceProfile(t *testing.T) { ...@@ -31,7 +31,7 @@ func TestGetInstanceProfile(t *testing.T) {
require.Equal(t, "http://localhost:8080", resp.InstanceUrl) require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
// Instance should not be initialized since no admin users are created // 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) { t.Run("GetInstanceProfile with initialized instance", func(t *testing.T) {
...@@ -58,7 +58,8 @@ func TestGetInstanceProfile(t *testing.T) { ...@@ -58,7 +58,8 @@ func TestGetInstanceProfile(t *testing.T) {
require.Equal(t, "http://localhost:8080", resp.InstanceUrl) require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
// Instance should be initialized since an admin user exists // 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) { ...@@ -101,7 +102,7 @@ func TestGetInstanceProfile_Concurrency(t *testing.T) {
require.Equal(t, "test-1.0.0", resp.Version) require.Equal(t, "test-1.0.0", resp.Version)
require.True(t, resp.Demo) require.True(t, resp.Demo)
require.Equal(t, "http://localhost:8080", resp.InstanceUrl) require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
require.True(t, resp.Initialized) require.NotNil(t, resp.Admin)
} }
} }
}) })
......
...@@ -48,9 +48,6 @@ func NewTestService(t *testing.T) *TestService { ...@@ -48,9 +48,6 @@ func NewTestService(t *testing.T) *TestService {
MarkdownService: markdownService, MarkdownService: markdownService,
} }
// Clear any cached state from previous tests
service.ClearInstanceOwnerCache()
return &TestService{ return &TestService{
Service: service, Service: service,
Store: testStore, Store: testStore,
...@@ -59,9 +56,8 @@ func NewTestService(t *testing.T) *TestService { ...@@ -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() { func (ts *TestService) Cleanup() {
ts.Service.ClearInstanceOwnerCache()
ts.Store.Close() ts.Store.Close()
} }
......
...@@ -177,12 +177,6 @@ func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserR ...@@ -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) 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 return convertUserFromStore(user), nil
} }
......
...@@ -22,10 +22,10 @@ const App = () => { ...@@ -22,10 +22,10 @@ const App = () => {
// Redirect to sign up page if instance not initialized (no admin account exists yet) // Redirect to sign up page if instance not initialized (no admin account exists yet)
useEffect(() => { useEffect(() => {
if (!instanceProfile.initialized) { if (!instanceProfile.admin) {
navigateTo("/auth/signup"); navigateTo("/auth/signup");
} }
}, [instanceProfile.initialized, navigateTo]); }, [instanceProfile.admin, navigateTo]);
useEffect(() => { useEffect(() => {
if (instanceGeneralSetting.additionalStyle) { if (instanceGeneralSetting.additionalStyle) {
......
...@@ -135,7 +135,7 @@ const SignUp = () => { ...@@ -135,7 +135,7 @@ const SignUp = () => {
) : ( ) : (
<p className="w-full text-2xl mt-2 text-muted-foreground">Sign up is not allowed.</p> <p className="w-full text-2xl mt-2 text-muted-foreground">Sign up is not allowed.</p>
)} )}
{!profile.initialized ? ( {!profile.admin ? (
<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 font-medium text-muted-foreground">{t("auth.host-tip")}</p>
) : ( ) : (
<p className="w-full mt-4 text-sm"> <p className="w-full mt-4 text-sm">
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } 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_annotations } from "../../google/api/annotations_pb";
import { file_google_api_client } from "../../google/api/client_pb"; import { file_google_api_client } from "../../google/api/client_pb";
import { file_google_api_field_behavior } from "../../google/api/field_behavior_pb"; import { file_google_api_field_behavior } from "../../google/api/field_behavior_pb";
...@@ -16,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; ...@@ -16,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file api/v1/instance_service.proto. * Describes the file api/v1/instance_service.proto.
*/ */
export const file_api_v1_instance_service: GenFile = /*@__PURE__*/ 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. * Instance profile message containing basic instance information.
...@@ -46,13 +48,12 @@ export type InstanceProfile = Message<"memos.api.v1.InstanceProfile"> & { ...@@ -46,13 +48,12 @@ export type InstanceProfile = Message<"memos.api.v1.InstanceProfile"> & {
instanceUrl: string; instanceUrl: string;
/** /**
* Indicates if the instance has completed first-time setup. * The first administrator who set up this instance.
* When false, the instance requires initialization (creating the first admin account). * When null, instance requires initial setup (creating the first admin account).
* This follows the pattern used by other self-hosted platforms for setup workflows.
* *
* @generated from field: bool initialized = 7; * @generated from field: memos.api.v1.User admin = 7;
*/ */
initialized: boolean; admin?: User;
}; };
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment