refactor: migrate HOST roles to ADMIN

- Updated the isSuperUser function to only check for ADMIN role.
- Added SQL migration scripts for MySQL, PostgreSQL, and SQLite to change user roles from HOST to ADMIN.
- Created a new SQLite migration to alter the user table structure and ensure data integrity during the migration process.
This commit is contained in:
Johnny 2026-01-20 23:38:30 +08:00
parent 47ebb04dc3
commit 0f3c9a467d
26 changed files with 97 additions and 109 deletions

View file

@ -38,7 +38,7 @@ var (
if err := instanceProfile.Validate(); err != nil { if err := instanceProfile.Validate(); err != nil {
slog.Error("failed to validate profile", "error", err) slog.Error("failed to validate profile", "error", err)
os.Exit(1) return
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -46,21 +46,21 @@ var (
if err != nil { if err != nil {
cancel() cancel()
slog.Error("failed to create db driver", "error", err) slog.Error("failed to create db driver", "error", err)
os.Exit(1) return
} }
storeInstance := store.New(dbDriver, instanceProfile) storeInstance := store.New(dbDriver, instanceProfile)
if err := storeInstance.Migrate(ctx); err != nil { if err := storeInstance.Migrate(ctx); err != nil {
cancel() cancel()
slog.Error("failed to migrate", "error", err) slog.Error("failed to migrate", "error", err)
os.Exit(1) return
} }
s, err := server.NewServer(ctx, instanceProfile, storeInstance) s, err := server.NewServer(ctx, instanceProfile, storeInstance)
if err != nil { if err != nil {
cancel() cancel()
slog.Error("failed to create server", "error", err) slog.Error("failed to create server", "error", err)
os.Exit(1) return
} }
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
@ -73,7 +73,7 @@ var (
if err != http.ErrServerClosed { if err != http.ErrServerClosed {
slog.Error("failed to start server", "error", err) slog.Error("failed to start server", "error", err)
cancel() cancel()
os.Exit(1) return
} }
} }

View file

@ -203,13 +203,10 @@ message User {
// User role enumeration. // User role enumeration.
enum Role { enum Role {
// Unspecified role.
ROLE_UNSPECIFIED = 0; ROLE_UNSPECIFIED = 0;
// Host role with full system access. // Admin role with system access.
HOST = 1;
// Admin role with administrative privileges.
ADMIN = 2; ADMIN = 2;
// Regular user role. // User role with limited access.
USER = 3; USER = 3;
} }
} }

View file

@ -29,13 +29,10 @@ const (
type User_Role int32 type User_Role int32
const ( const (
// Unspecified role.
User_ROLE_UNSPECIFIED User_Role = 0 User_ROLE_UNSPECIFIED User_Role = 0
// Host role with full system access. // Admin role with system access.
User_HOST User_Role = 1
// Admin role with administrative privileges.
User_ADMIN User_Role = 2 User_ADMIN User_Role = 2
// Regular user role. // User role with limited access.
User_USER User_Role = 3 User_USER User_Role = 3
) )
@ -43,13 +40,11 @@ const (
var ( var (
User_Role_name = map[int32]string{ User_Role_name = map[int32]string{
0: "ROLE_UNSPECIFIED", 0: "ROLE_UNSPECIFIED",
1: "HOST",
2: "ADMIN", 2: "ADMIN",
3: "USER", 3: "USER",
} }
User_Role_value = map[string]int32{ User_Role_value = map[string]int32{
"ROLE_UNSPECIFIED": 0, "ROLE_UNSPECIFIED": 0,
"HOST": 1,
"ADMIN": 2, "ADMIN": 2,
"USER": 3, "USER": 3,
} }
@ -2509,7 +2504,7 @@ var File_api_v1_user_service_proto protoreflect.FileDescriptor
const file_api_v1_user_service_proto_rawDesc = "" + const file_api_v1_user_service_proto_rawDesc = "" +
"\n" + "\n" +
"\x19api/v1/user_service.proto\x12\fmemos.api.v1\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcb\x04\n" + "\x19api/v1/user_service.proto\x12\fmemos.api.v1\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc1\x04\n" +
"\x04User\x12\x17\n" + "\x04User\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x120\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x120\n" +
"\x04role\x18\x02 \x01(\x0e2\x17.memos.api.v1.User.RoleB\x03\xe0A\x02R\x04role\x12\x1f\n" + "\x04role\x18\x02 \x01(\x0e2\x17.memos.api.v1.User.RoleB\x03\xe0A\x02R\x04role\x12\x1f\n" +
@ -2525,10 +2520,9 @@ const file_api_v1_user_service_proto_rawDesc = "" +
" \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + " \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" +
"createTime\x12@\n" + "createTime\x12@\n" +
"\vupdate_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + "\vupdate_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" +
"updateTime\";\n" + "updateTime\"1\n" +
"\x04Role\x12\x14\n" + "\x04Role\x12\x14\n" +
"\x10ROLE_UNSPECIFIED\x10\x00\x12\b\n" + "\x10ROLE_UNSPECIFIED\x10\x00\x12\t\n" +
"\x04HOST\x10\x01\x12\t\n" +
"\x05ADMIN\x10\x02\x12\b\n" + "\x05ADMIN\x10\x02\x12\b\n" +
"\x04USER\x10\x03:7\xeaA4\n" + "\x04USER\x10\x03:7\xeaA4\n" +
"\x11memos.api.v1/User\x12\fusers/{user}\x1a\x04name*\x05users2\x04user\"\x9d\x01\n" + "\x11memos.api.v1/User\x12\fusers/{user}\x1a\x04name*\x05users2\x04user\"\x9d\x01\n" +

View file

@ -2859,7 +2859,6 @@ components:
role: role:
enum: enum:
- ROLE_UNSPECIFIED - ROLE_UNSPECIFIED
- HOST
- ADMIN - ADMIN
- USER - USER
type: string type: string

View file

@ -74,7 +74,7 @@ func TestParseAccessTokenV2(t *testing.T) {
}) })
t.Run("parses token with different roles", func(t *testing.T) { t.Run("parses token with different roles", func(t *testing.T) {
roles := []string{"USER", "ADMIN", "HOST"} roles := []string{"USER", "ADMIN"}
for _, role := range roles { for _, role := range roles {
token, _, err := GenerateAccessTokenV2(1, "testuser", role, "ACTIVE", secret) token, _, err := GenerateAccessTokenV2(1, "testuser", role, "ACTIVE", secret)
require.NoError(t, err) require.NoError(t, err)

View file

@ -64,5 +64,5 @@ func unmarshalPageToken(s string, pageToken *v1pb.PageToken) error {
} }
func isSuperUser(user *store.User) bool { func isSuperUser(user *store.User) bool {
return user.Role == store.RoleAdmin || user.Role == store.RoleHost return user.Role == store.RoleAdmin
} }

View file

@ -21,7 +21,7 @@ func (s *APIV1Service) CreateIdentityProvider(ctx context.Context, request *v1pb
if currentUser == nil { if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if currentUser.Role != store.RoleHost { if currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -90,7 +90,7 @@ func (s *APIV1Service) UpdateIdentityProvider(ctx context.Context, request *v1pb
if currentUser == nil { if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if currentUser.Role != store.RoleHost { if currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -134,7 +134,7 @@ func (s *APIV1Service) DeleteIdentityProvider(ctx context.Context, request *v1pb
if currentUser == nil { if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if currentUser.Role != store.RoleHost { if currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -228,7 +228,7 @@ func convertIdentityProviderConfigToStore(identityProviderType v1pb.IdentityProv
} }
func redactIdentityProviderResponse(identityProvider *v1pb.IdentityProvider, userRole store.Role) *v1pb.IdentityProvider { func redactIdentityProviderResponse(identityProvider *v1pb.IdentityProvider, userRole store.Role) *v1pb.IdentityProvider {
if userRole != store.RoleHost { if userRole != store.RoleAdmin {
if identityProvider.Type == v1pb.IdentityProvider_OAUTH2 { if identityProvider.Type == v1pb.IdentityProvider_OAUTH2 {
identityProvider.Config.GetOauth2Config().ClientSecret = "" identityProvider.Config.GetOauth2Config().ClientSecret = ""
} }

View file

@ -64,7 +64,7 @@ func (s *APIV1Service) GetInstanceSetting(ctx context.Context, request *v1pb.Get
return nil, status.Errorf(codes.NotFound, "instance setting not found") return nil, status.Errorf(codes.NotFound, "instance setting not found")
} }
// For storage setting, only host can get it. // For storage setting, only admin can get it.
if instanceSetting.Key == storepb.InstanceSettingKey_STORAGE { if instanceSetting.Key == storepb.InstanceSettingKey_STORAGE {
user, err := s.fetchCurrentUser(ctx) user, err := s.fetchCurrentUser(ctx)
if err != nil { if err != nil {
@ -73,7 +73,7 @@ func (s *APIV1Service) GetInstanceSetting(ctx context.Context, request *v1pb.Get
if user == nil { if user == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if user.Role != store.RoleHost { if user.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
} }
@ -89,7 +89,7 @@ func (s *APIV1Service) UpdateInstanceSetting(ctx context.Context, request *v1pb.
if user == nil { if user == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if user.Role != store.RoleHost { if user.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -277,9 +277,9 @@ func (s *APIV1Service) GetInstanceOwner(ctx context.Context) (*v1pb.User, error)
return ownerCache, nil return ownerCache, nil
} }
hostUserType := store.RoleHost adminUserType := store.RoleAdmin
user, err := s.Store.GetUser(ctx, &store.FindUser{ user, err := s.Store.GetUser(ctx, &store.FindUser{
Role: &hostUserType, 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 owner")

View file

@ -28,7 +28,7 @@ func TestGetInstanceProfile(t *testing.T) {
// Verify the response contains expected data // Verify the response contains expected data
require.Equal(t, "test-1.0.0", resp.Version) require.Equal(t, "test-1.0.0", resp.Version)
require.Equal(t, "dev", resp.Mode) require.True(t, resp.Demo)
require.Equal(t, "http://localhost:8080", resp.InstanceUrl) require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
// Owner should be empty since no users are created // Owner should be empty since no users are created
@ -55,7 +55,7 @@ func TestGetInstanceProfile(t *testing.T) {
// Verify the response contains expected data including owner // Verify the response contains expected data including owner
require.Equal(t, "test-1.0.0", resp.Version) require.Equal(t, "test-1.0.0", resp.Version)
require.Equal(t, "dev", resp.Mode) require.True(t, resp.Demo)
require.Equal(t, "http://localhost:8080", resp.InstanceUrl) require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
// User name should be "users/{id}" format where id is the user's ID // User name should be "users/{id}" format where id is the user's ID
@ -102,7 +102,7 @@ func TestGetInstanceProfile_Concurrency(t *testing.T) {
case resp := <-results: case resp := <-results:
require.NotNil(t, resp) require.NotNil(t, resp)
require.Equal(t, "test-1.0.0", resp.Version) require.Equal(t, "test-1.0.0", resp.Version)
require.Equal(t, "dev", resp.Mode) require.True(t, resp.Demo)
require.Equal(t, "http://localhost:8080", resp.InstanceUrl) require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
require.Equal(t, expectedOwnerName, resp.Owner) require.Equal(t, expectedOwnerName, resp.Owner)
} }

View file

@ -29,7 +29,7 @@ func NewTestService(t *testing.T) *TestService {
// Create a test profile // Create a test profile
testProfile := &profile.Profile{ testProfile := &profile.Profile{
Mode: "dev", Demo: true,
Version: "test-1.0.0", Version: "test-1.0.0",
InstanceURL: "http://localhost:8080", InstanceURL: "http://localhost:8080",
Driver: "sqlite", Driver: "sqlite",
@ -62,11 +62,11 @@ func (ts *TestService) Cleanup() {
// Note: Owner cache is package-level in parent package, cannot clear from test package // Note: Owner cache is package-level in parent package, cannot clear from test package
} }
// CreateHostUser creates a host user for testing. // CreateHostUser creates an admin user for testing.
func (ts *TestService) CreateHostUser(ctx context.Context, username string) (*store.User, error) { func (ts *TestService) CreateHostUser(ctx context.Context, username string) (*store.User, error) {
return ts.Store.CreateUser(ctx, &store.User{ return ts.Store.CreateUser(ctx, &store.User{
Username: username, Username: username,
Role: store.RoleHost, Role: store.RoleAdmin,
Email: username + "@example.com", Email: username + "@example.com",
}) })
} }

View file

@ -37,7 +37,7 @@ func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersReq
if currentUser == nil { if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { if currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -132,17 +132,17 @@ func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserR
// Determine the role to assign // Determine the role to assign
var roleToAssign store.Role var roleToAssign store.Role
if isFirstUser { if isFirstUser {
// First-time setup: create the first user as HOST (no authentication required) // First-time setup: create the first user as ADMIN (no authentication required)
roleToAssign = store.RoleHost roleToAssign = store.RoleAdmin
} else if currentUser != nil && currentUser.Role == store.RoleHost { } else if currentUser != nil && currentUser.Role == store.RoleAdmin {
// Authenticated HOST user can create users with any role specified in request // Authenticated ADMIN user can create users with any role specified in request
if request.User.Role != v1pb.User_ROLE_UNSPECIFIED { if request.User.Role != v1pb.User_ROLE_UNSPECIFIED {
roleToAssign = convertUserRoleToStore(request.User.Role) roleToAssign = convertUserRoleToStore(request.User.Role)
} else { } else {
roleToAssign = store.RoleUser roleToAssign = store.RoleUser
} }
} else { } else {
// Unauthenticated or non-HOST users can only create normal users // Unauthenticated or non-ADMIN users can only create normal users
roleToAssign = store.RoleUser roleToAssign = store.RoleUser
} }
@ -197,7 +197,7 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
} }
// Check permission. // Check permission.
// Only allow admin or self to update user. // Only allow admin or self to update user.
if currentUser.ID != userID && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost { if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -264,7 +264,7 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
update.Description = &request.User.Description update.Description = &request.User.Description
case "role": case "role":
// Only allow admin to update role. // Only allow admin to update role.
if currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost { if currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
role := convertUserRoleToStore(request.User.Role) role := convertUserRoleToStore(request.User.Role)
@ -301,7 +301,7 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
} }
if currentUser.ID != userID && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost { if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -542,7 +542,7 @@ func (s *APIV1Service) ListPersonalAccessTokens(ctx context.Context, request *v1
claims := auth.GetUserClaims(ctx) claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID { if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx) currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin) { if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
} }
@ -689,7 +689,7 @@ func (s *APIV1Service) ListUserWebhooks(ctx context.Context, request *v1pb.ListU
if currentUser == nil { if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -721,7 +721,7 @@ func (s *APIV1Service) CreateUserWebhook(ctx context.Context, request *v1pb.Crea
if currentUser == nil { if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -761,7 +761,7 @@ func (s *APIV1Service) UpdateUserWebhook(ctx context.Context, request *v1pb.Upda
if currentUser == nil { if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -833,7 +833,7 @@ func (s *APIV1Service) DeleteUserWebhook(ctx context.Context, request *v1pb.Dele
if currentUser == nil { if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
} }
if currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
@ -928,8 +928,6 @@ func convertUserFromStore(user *store.User) *v1pb.User {
func convertUserRoleFromStore(role store.Role) v1pb.User_Role { func convertUserRoleFromStore(role store.Role) v1pb.User_Role {
switch role { switch role {
case store.RoleHost:
return v1pb.User_HOST
case store.RoleAdmin: case store.RoleAdmin:
return v1pb.User_ADMIN return v1pb.User_ADMIN
case store.RoleUser: case store.RoleUser:
@ -941,8 +939,6 @@ func convertUserRoleFromStore(role store.Role) v1pb.User_Role {
func convertUserRoleToStore(role v1pb.User_Role) store.Role { func convertUserRoleToStore(role v1pb.User_Role) store.Role {
switch role { switch role {
case v1pb.User_HOST:
return store.RoleHost
case v1pb.User_ADMIN: case v1pb.User_ADMIN:
return store.RoleAdmin return store.RoleAdmin
default: default:

View file

@ -0,0 +1 @@
UPDATE `user` SET `role` = 'ADMIN' WHERE `role` = 'HOST';

View file

@ -0,0 +1 @@
UPDATE "user" SET role = 'ADMIN' WHERE role = 'HOST';

View file

@ -0,0 +1,24 @@
ALTER TABLE user RENAME TO user_old;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
avatar_url TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT ''
);
INSERT INTO user (
id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url, description
)
SELECT
id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url, description
FROM user_old;
DROP TABLE user_old;

View file

@ -0,0 +1 @@
UPDATE user SET role = 'ADMIN' WHERE role = 'HOST';

View file

@ -13,7 +13,7 @@ CREATE TABLE user (
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER', role TEXT NOT NULL DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '', nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,

View file

@ -244,16 +244,16 @@ func (s *Store) preMigrate(ctx context.Context) error {
return errors.Wrap(err, "failed to get current schema version") return errors.Wrap(err, "failed to get current schema version")
} }
slog.Info("database initialized successfully", slog.String("schemaVersion", schemaVersion)) slog.Info("database initialized successfully", slog.String("schemaVersion", schemaVersion))
if err := s.updateCurrentSchemaVersion(ctx, schemaVersion); err != nil { if err := s.updateCurrentSchemaVersion(ctx, schemaVersion); err != nil {
return errors.Wrap(err, "failed to update current schema version") return errors.Wrap(err, "failed to update current schema version")
}
}
if err := s.checkMinimumUpgradeVersion(ctx); err != nil {
return err // Error message is already descriptive, don't wrap it
}
return nil
} }
}
if err := s.checkMinimumUpgradeVersion(ctx); err != nil {
return err // Error message is already descriptive, don't wrap it
}
return nil
}
func (s *Store) getMigrationBasePath() string { func (s *Store) getMigrationBasePath() string {
return fmt.Sprintf("migration/%s/", s.profile.Driver) return fmt.Sprintf("migration/%s/", s.profile.Driver)
} }

View file

@ -10,7 +10,7 @@ The demo data includes **6 carefully selected memos** that showcase the key feat
- **Username**: `demo` - **Username**: `demo`
- **Password**: `secret` (default password) - **Password**: `secret` (default password)
- **Role**: HOST - **Role**: ADMIN
- **Nickname**: Demo User - **Nickname**: Demo User
## Demo Memos (6 total) ## Demo Memos (6 total)
@ -198,7 +198,7 @@ Login with:
- All memos are set to PUBLIC visibility - All memos are set to PUBLIC visibility
- **Two memos are pinned**: Welcome (#1) and Sponsor (#6) - **Two memos are pinned**: Welcome (#1) and Sponsor (#6)
- User has HOST role to showcase all features - User has ADMIN role to showcase all features
- Reactions are distributed across memos - Reactions are distributed across memos
- One memo relation demonstrates linking - One memo relation demonstrates linking
- Content is optimized for the compact markdown styles - Content is optimized for the compact markdown styles

View file

@ -1,5 +1,5 @@
-- Demo User -- Demo User
INSERT INTO user (id,username,role,nickname,password_hash) VALUES(1,'demo','HOST','Demo User','$2a$10$c.slEVgf5b/3BnAWlLb/vOu7VVSOKJ4ljwMe9xzlx9IhKnvAsJYM6'); INSERT INTO user (id,username,role,nickname,password_hash) VALUES(1,'demo','ADMIN','Demo User','$2a$10$c.slEVgf5b/3BnAWlLb/vOu7VVSOKJ4ljwMe9xzlx9IhKnvAsJYM6');
-- Welcome Memo (Pinned) -- Welcome Memo (Pinned)
INSERT INTO memo (id,uid,creator_id,content,visibility,pinned,payload) VALUES(1,'welcome2memos001',1,replace('# Welcome to Memos!\\n\\nA privacy-first, lightweight note-taking service. Easily capture and share your great thoughts.\\n\\n## Key Features\\n\\n- **Privacy First**: Your data stays with you\\n- **Markdown Support**: Full CommonMark + GFM syntax\\n- **Quick Capture**: Jot down thoughts instantly\\n- **Organize with Tags**: Use #tags to categorize\\n- **Open Source**: Free and open source software\\n\\n---\\n\\nStart exploring the demo memos below to see what you can do! #welcome #getting-started','\\n',char(10)),'PUBLIC',1,'{"tags":["welcome","getting-started"],"property":{"hasLink":false}}'); INSERT INTO memo (id,uid,creator_id,content,visibility,pinned,payload) VALUES(1,'welcome2memos001',1,replace('# Welcome to Memos!\\n\\nA privacy-first, lightweight note-taking service. Easily capture and share your great thoughts.\\n\\n## Key Features\\n\\n- **Privacy First**: Your data stays with you\\n- **Markdown Support**: Full CommonMark + GFM syntax\\n- **Quick Capture**: Jot down thoughts instantly\\n- **Organize with Tags**: Use #tags to categorize\\n- **Open Source**: Free and open source software\\n\\n---\\n\\nStart exploring the demo memos below to see what you can do! #welcome #getting-started','\\n',char(10)),'PUBLIC',1,'{"tags":["welcome","getting-started"],"property":{"hasLink":false}}');

View file

@ -41,12 +41,11 @@ func NewTestingStore(ctx context.Context, t *testing.T) *store.Store {
// This is useful for testing migrations on existing data. // This is useful for testing migrations on existing data.
func NewTestingStoreWithDSN(_ context.Context, t *testing.T, driver, dsn string) *store.Store { func NewTestingStoreWithDSN(_ context.Context, t *testing.T, driver, dsn string) *store.Store {
profile := &profile.Profile{ profile := &profile.Profile{
Mode: "prod",
Port: getUnusedPort(), Port: getUnusedPort(),
Data: t.TempDir(), // Dummy dir, DSN matters Data: t.TempDir(), // Dummy dir, DSN matters
DSN: dsn, DSN: dsn,
Driver: driver, Driver: driver,
Version: version.GetCurrentVersion("prod"), Version: version.GetCurrentVersion(),
} }
dbDriver, err := db.NewDBDriver(profile) dbDriver, err := db.NewDBDriver(profile)
if err != nil { if err != nil {
@ -95,12 +94,11 @@ func getTestingProfileForDriver(t *testing.T, driver string) *profile.Profile {
} }
return &profile.Profile{ return &profile.Profile{
Mode: mode,
Port: port, Port: port,
Data: dir, Data: dir,
DSN: dsn, DSN: dsn,
Driver: driver, Driver: driver,
Version: version.GetCurrentVersion(mode), Version: version.GetCurrentVersion(),
} }
} }

View file

@ -20,7 +20,7 @@ func TestUserStore(t *testing.T) {
users, err := ts.ListUsers(ctx, &store.FindUser{}) users, err := ts.ListUsers(ctx, &store.FindUser{})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(users)) require.Equal(t, 1, len(users))
require.Equal(t, store.RoleHost, users[0].Role) require.Equal(t, store.RoleAdmin, users[0].Role)
require.Equal(t, user, users[0]) require.Equal(t, user, users[0])
userPatchNickname := "test_nickname_2" userPatchNickname := "test_nickname_2"
userPatch := &store.UpdateUser{ userPatch := &store.UpdateUser{
@ -104,7 +104,7 @@ func TestUserListByRole(t *testing.T) {
_, err := createTestingHostUser(ctx, ts) _, err := createTestingHostUser(ctx, ts)
require.NoError(t, err) require.NoError(t, err)
adminUser, err := createTestingUserWithRole(ctx, ts, "admin_user", store.RoleAdmin) _, err = createTestingUserWithRole(ctx, ts, "admin_user", store.RoleAdmin)
require.NoError(t, err) require.NoError(t, err)
regularUser, err := createTestingUserWithRole(ctx, ts, "regular_user", store.RoleUser) regularUser, err := createTestingUserWithRole(ctx, ts, "regular_user", store.RoleUser)
@ -115,19 +115,11 @@ func TestUserListByRole(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 3, len(allUsers)) require.Equal(t, 3, len(allUsers))
// List only HOST users
hostRole := store.RoleHost
hostUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &hostRole})
require.NoError(t, err)
require.Equal(t, 1, len(hostUsers))
require.Equal(t, store.RoleHost, hostUsers[0].Role)
// List only ADMIN users // List only ADMIN users
adminRole := store.RoleAdmin adminRole := store.RoleAdmin
adminUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &adminRole}) adminOnlyUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &adminRole})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(adminUsers)) require.Equal(t, 2, len(adminOnlyUsers))
require.Equal(t, adminUser.ID, adminUsers[0].ID)
// List only USER role users // List only USER role users
userRole := store.RoleUser userRole := store.RoleUser
@ -227,7 +219,7 @@ func TestUserListWithLimit(t *testing.T) {
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
role := store.RoleUser role := store.RoleUser
if i == 0 { if i == 0 {
role = store.RoleHost role = store.RoleAdmin
} }
_, err := createTestingUserWithRole(ctx, ts, fmt.Sprintf("user%d", i), role) _, err := createTestingUserWithRole(ctx, ts, fmt.Sprintf("user%d", i), role)
require.NoError(t, err) require.NoError(t, err)
@ -243,7 +235,7 @@ func TestUserListWithLimit(t *testing.T) {
} }
func createTestingHostUser(ctx context.Context, ts *store.Store) (*store.User, error) { func createTestingHostUser(ctx context.Context, ts *store.Store) (*store.User, error) {
return createTestingUserWithRole(ctx, ts, "test", store.RoleHost) return createTestingUserWithRole(ctx, ts, "test", store.RoleAdmin)
} }
func createTestingUserWithRole(ctx context.Context, ts *store.Store, username string, role store.Role) (*store.User, error) { func createTestingUserWithRole(ctx context.Context, ts *store.Store, username string, role store.Role) (*store.User, error) {

View file

@ -8,8 +8,6 @@ import (
type Role string type Role string
const ( const (
// RoleHost is the HOST role.
RoleHost Role = "HOST"
// RoleAdmin is the ADMIN role. // RoleAdmin is the ADMIN role.
RoleAdmin Role = "ADMIN" RoleAdmin Role = "ADMIN"
// RoleUser is the USER role. // RoleUser is the USER role.
@ -18,8 +16,6 @@ const (
func (e Role) String() string { func (e Role) String() string {
switch e { switch e {
case RoleHost:
return "HOST"
case RoleAdmin: case RoleAdmin:
return "ADMIN" return "ADMIN"
default: default:

View file

@ -31,9 +31,7 @@ const MemberSection = () => {
const [deleteTarget, setDeleteTarget] = useState<User | undefined>(undefined); const [deleteTarget, setDeleteTarget] = useState<User | undefined>(undefined);
const stringifyUserRole = (role: User_Role) => { const stringifyUserRole = (role: User_Role) => {
if (role === User_Role.HOST) { if (role === User_Role.ADMIN) {
return "Host";
} else if (role === User_Role.ADMIN) {
return t("setting.member-section.admin"); return t("setting.member-section.admin");
} else { } else {
return t("setting.member-section.user"); return t("setting.member-section.user");

View file

@ -45,7 +45,7 @@ const Setting = () => {
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
selectedSection: "my-account", selectedSection: "my-account",
}); });
const isHost = user?.role === User_Role.HOST; const isHost = user?.role === User_Role.ADMIN;
const settingsSectionList = useMemo(() => { const settingsSectionList = useMemo(() => {
let settingList = [...BASIC_SECTIONS]; let settingList = [...BASIC_SECTIONS];

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
import { User, User_Role } from "@/types/proto/api/v1/user_service_pb"; import { User, User_Role } from "@/types/proto/api/v1/user_service_pb";
export const isSuperUser = (user: User | undefined) => { export const isSuperUser = (user: User | undefined) => {
return user && (user.role === User_Role.ADMIN || user.role === User_Role.HOST); return user && user.role === User_Role.ADMIN;
}; };