Skip to content

Commit b055882

Browse files
committed
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
1 parent 8102212 commit b055882

File tree

11 files changed

+75
-117
lines changed

11 files changed

+75
-117
lines changed

proto/api/v1/instance_service.proto

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ syntax = "proto3";
22

33
package memos.api.v1;
44

5+
import "api/v1/user_service.proto";
56
import "google/api/annotations.proto";
67
import "google/api/client.proto";
78
import "google/api/field_behavior.proto";
@@ -43,10 +44,9 @@ message InstanceProfile {
4344
// Instance URL is the URL of the instance.
4445
string instance_url = 6;
4546

46-
// Indicates if the instance has completed first-time setup.
47-
// When false, the instance requires initialization (creating the first admin account).
48-
// This follows the pattern used by other self-hosted platforms for setup workflows.
49-
bool initialized = 7;
47+
// The first administrator who set up this instance.
48+
// When null, instance requires initial setup (creating the first admin account).
49+
User admin = 7;
5050
}
5151

5252
// Request for instance profile.

proto/gen/api/v1/instance_service.pb.go

Lines changed: 32 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/gen/openapi.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2141,12 +2141,12 @@ components:
21412141
instanceUrl:
21422142
type: string
21432143
description: Instance URL is the URL of the instance.
2144-
initialized:
2145-
type: boolean
2144+
admin:
2145+
allOf:
2146+
- $ref: '#/components/schemas/User'
21462147
description: |-
2147-
Indicates if the instance has completed first-time setup.
2148-
When false, the instance requires initialization (creating the first admin account).
2149-
This follows the pattern used by other self-hosted platforms for setup workflows.
2148+
The first administrator who set up this instance.
2149+
When null, instance requires initial setup (creating the first admin account).
21502150
description: Instance profile message containing basic instance information.
21512151
InstanceSetting:
21522152
type: object

server/router/api/v1/instance_service.go

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package v1
33
import (
44
"context"
55
"fmt"
6-
"sync"
76

87
"github.com/pkg/errors"
98
"google.golang.org/grpc/codes"
@@ -16,16 +15,16 @@ import (
1615

1716
// GetInstanceProfile returns the instance profile.
1817
func (s *APIV1Service) GetInstanceProfile(ctx context.Context, _ *v1pb.GetInstanceProfileRequest) (*v1pb.InstanceProfile, error) {
19-
owner, err := s.GetInstanceOwner(ctx)
18+
admin, err := s.GetInstanceAdmin(ctx)
2019
if err != nil {
21-
return nil, status.Errorf(codes.Internal, "failed to get instance owner: %v", err)
20+
return nil, status.Errorf(codes.Internal, "failed to get instance admin: %v", err)
2221
}
2322

2423
instanceProfile := &v1pb.InstanceProfile{
2524
Version: s.Profile.Version,
2625
Demo: s.Profile.Demo,
2726
InstanceUrl: s.Profile.InstanceURL,
28-
Initialized: owner != nil,
27+
Admin: admin, // nil when not initialized
2928
}
3029
return instanceProfile, nil
3130
}
@@ -270,48 +269,17 @@ func convertInstanceMemoRelatedSettingToStore(setting *v1pb.InstanceSetting_Memo
270269
}
271270
}
272271

273-
var (
274-
ownerCache *v1pb.User
275-
ownerCacheMutex sync.RWMutex
276-
)
277-
278-
func (s *APIV1Service) GetInstanceOwner(ctx context.Context) (*v1pb.User, error) {
279-
// Try read lock first for cache hit
280-
ownerCacheMutex.RLock()
281-
if ownerCache != nil {
282-
defer ownerCacheMutex.RUnlock()
283-
return ownerCache, nil
284-
}
285-
ownerCacheMutex.RUnlock()
286-
287-
// Upgrade to write lock to populate cache
288-
ownerCacheMutex.Lock()
289-
defer ownerCacheMutex.Unlock()
290-
291-
// Double-check after acquiring write lock
292-
if ownerCache != nil {
293-
return ownerCache, nil
294-
}
295-
272+
func (s *APIV1Service) GetInstanceAdmin(ctx context.Context) (*v1pb.User, error) {
296273
adminUserType := store.RoleAdmin
297274
user, err := s.Store.GetUser(ctx, &store.FindUser{
298275
Role: &adminUserType,
299276
})
300277
if err != nil {
301-
return nil, errors.Wrapf(err, "failed to find owner")
278+
return nil, errors.Wrapf(err, "failed to find admin")
302279
}
303280
if user == nil {
304281
return nil, nil
305282
}
306283

307-
ownerCache = convertUserFromStore(user)
308-
return ownerCache, nil
309-
}
310-
311-
// ClearInstanceOwnerCache clears the cached instance owner.
312-
// This should be called when an admin user is created or when the owner changes.
313-
func (*APIV1Service) ClearInstanceOwnerCache() {
314-
ownerCacheMutex.Lock()
315-
defer ownerCacheMutex.Unlock()
316-
ownerCache = nil
284+
return convertUserFromStore(user), nil
317285
}

server/router/api/v1/test/instance_owner_cache_test.go renamed to server/router/api/v1/test/instance_admin_cache_test.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
v1pb "github.com/usememos/memos/proto/gen/api/v1"
1010
)
1111

12-
func TestInstanceOwnerCache(t *testing.T) {
12+
func TestInstanceAdminRetrieval(t *testing.T) {
1313
ctx := context.Background()
1414

1515
t.Run("Instance becomes initialized after first admin user is created", func(t *testing.T) {
@@ -20,7 +20,7 @@ func TestInstanceOwnerCache(t *testing.T) {
2020
// Verify instance is not initialized initially
2121
profile1, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})
2222
require.NoError(t, err)
23-
require.False(t, profile1.Initialized, "Instance should not be initialized before first admin user")
23+
require.Nil(t, profile1.Admin, "Instance should not be initialized before first admin user")
2424

2525
// Create the first admin user
2626
user, err := ts.CreateHostUser(ctx, "admin")
@@ -30,29 +30,25 @@ func TestInstanceOwnerCache(t *testing.T) {
3030
// Verify instance is now initialized
3131
profile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})
3232
require.NoError(t, err)
33-
require.True(t, profile2.Initialized, "Instance should be initialized after first admin user is created")
33+
require.NotNil(t, profile2.Admin, "Instance should be initialized after first admin user is created")
34+
require.Equal(t, user.Username, profile2.Admin.Username)
3435
})
3536

36-
t.Run("ClearInstanceOwnerCache works correctly", func(t *testing.T) {
37+
t.Run("Admin retrieval is cached by Store layer", func(t *testing.T) {
3738
// Create test service
3839
ts := NewTestService(t)
3940
defer ts.Cleanup()
4041

4142
// Create admin user
42-
_, err := ts.CreateHostUser(ctx, "admin")
43-
require.NoError(t, err)
44-
45-
// Verify initialized
46-
profile1, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})
43+
user, err := ts.CreateHostUser(ctx, "admin")
4744
require.NoError(t, err)
48-
require.True(t, profile1.Initialized)
4945

50-
// Clear cache
51-
ts.Service.ClearInstanceOwnerCache()
52-
53-
// Should still be initialized (cache is refilled from DB)
54-
profile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})
55-
require.NoError(t, err)
56-
require.True(t, profile2.Initialized)
46+
// Multiple calls should return consistent admin user (from cache)
47+
for i := 0; i < 5; i++ {
48+
profile, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})
49+
require.NoError(t, err)
50+
require.NotNil(t, profile.Admin)
51+
require.Equal(t, user.Username, profile.Admin.Username)
52+
}
5753
})
5854
}

server/router/api/v1/test/instance_service_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestGetInstanceProfile(t *testing.T) {
3131
require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
3232

3333
// Instance should not be initialized since no admin users are created
34-
require.False(t, resp.Initialized)
34+
require.Nil(t, resp.Admin)
3535
})
3636

3737
t.Run("GetInstanceProfile with initialized instance", func(t *testing.T) {
@@ -58,7 +58,8 @@ func TestGetInstanceProfile(t *testing.T) {
5858
require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
5959

6060
// Instance should be initialized since an admin user exists
61-
require.True(t, resp.Initialized)
61+
require.NotNil(t, resp.Admin)
62+
require.Equal(t, hostUser.Username, resp.Admin.Username)
6263
})
6364
}
6465

@@ -101,7 +102,7 @@ func TestGetInstanceProfile_Concurrency(t *testing.T) {
101102
require.Equal(t, "test-1.0.0", resp.Version)
102103
require.True(t, resp.Demo)
103104
require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
104-
require.True(t, resp.Initialized)
105+
require.NotNil(t, resp.Admin)
105106
}
106107
}
107108
})

server/router/api/v1/test/test_helper.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ func NewTestService(t *testing.T) *TestService {
4848
MarkdownService: markdownService,
4949
}
5050

51-
// Clear any cached state from previous tests
52-
service.ClearInstanceOwnerCache()
53-
5451
return &TestService{
5552
Service: service,
5653
Store: testStore,
@@ -59,9 +56,8 @@ func NewTestService(t *testing.T) *TestService {
5956
}
6057
}
6158

62-
// Cleanup clears caches and closes resources after test.
59+
// Cleanup closes resources after test.
6360
func (ts *TestService) Cleanup() {
64-
ts.Service.ClearInstanceOwnerCache()
6561
ts.Store.Close()
6662
}
6763

server/router/api/v1/user_service.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,6 @@ func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserR
177177
return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
178178
}
179179

180-
// If this is the first admin user being created, clear the owner cache
181-
// so that GetInstanceProfile will return initialized=true
182-
if roleToAssign == store.RoleAdmin {
183-
s.ClearInstanceOwnerCache()
184-
}
185-
186180
return convertUserFromStore(user), nil
187181
}
188182

web/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ const App = () => {
2222

2323
// Redirect to sign up page if instance not initialized (no admin account exists yet)
2424
useEffect(() => {
25-
if (!instanceProfile.initialized) {
25+
if (!instanceProfile.admin) {
2626
navigateTo("/auth/signup");
2727
}
28-
}, [instanceProfile.initialized, navigateTo]);
28+
}, [instanceProfile.admin, navigateTo]);
2929

3030
useEffect(() => {
3131
if (instanceGeneralSetting.additionalStyle) {

web/src/pages/SignUp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const SignUp = () => {
135135
) : (
136136
<p className="w-full text-2xl mt-2 text-muted-foreground">Sign up is not allowed.</p>
137137
)}
138-
{!profile.initialized ? (
138+
{!profile.admin ? (
139139
<p className="w-full mt-4 text-sm font-medium text-muted-foreground">{t("auth.host-tip")}</p>
140140
) : (
141141
<p className="w-full mt-4 text-sm">

0 commit comments

Comments
 (0)