Commit 0f3c9a46 authored by Johnny's avatar Johnny

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.
parent 47ebb04d
...@@ -38,7 +38,7 @@ var ( ...@@ -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 ( ...@@ -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 ( ...@@ -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
} }
} }
......
...@@ -203,13 +203,10 @@ message User { ...@@ -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;
} }
} }
......
...@@ -29,13 +29,10 @@ const ( ...@@ -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 ( ...@@ -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 ...@@ -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 = "" + ...@@ -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" +
......
...@@ -2859,7 +2859,6 @@ components: ...@@ -2859,7 +2859,6 @@ components:
role: role:
enum: enum:
- ROLE_UNSPECIFIED - ROLE_UNSPECIFIED
- HOST
- ADMIN - ADMIN
- USER - USER
type: string type: string
......
...@@ -74,7 +74,7 @@ func TestParseAccessTokenV2(t *testing.T) { ...@@ -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)
......
...@@ -64,5 +64,5 @@ func unmarshalPageToken(s string, pageToken *v1pb.PageToken) error { ...@@ -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
} }
...@@ -21,7 +21,7 @@ func (s *APIV1Service) CreateIdentityProvider(ctx context.Context, request *v1pb ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 = ""
} }
......
...@@ -64,7 +64,7 @@ func (s *APIV1Service) GetInstanceSetting(ctx context.Context, request *v1pb.Get ...@@ -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 ...@@ -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. ...@@ -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) ...@@ -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")
......
...@@ -28,7 +28,7 @@ func TestGetInstanceProfile(t *testing.T) { ...@@ -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) { ...@@ -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) { ...@@ -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)
} }
......
...@@ -29,7 +29,7 @@ func NewTestService(t *testing.T) *TestService { ...@@ -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() { ...@@ -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",
}) })
} }
......
...@@ -37,7 +37,7 @@ func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersReq ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 { ...@@ -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 { ...@@ -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:
......
UPDATE `user` SET `role` = 'ADMIN' WHERE `role` = 'HOST';
UPDATE "user" SET role = 'ADMIN' WHERE role = 'HOST';
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;
UPDATE user SET role = 'ADMIN' WHERE role = 'HOST';
...@@ -13,7 +13,7 @@ CREATE TABLE user ( ...@@ -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,
......
...@@ -244,16 +244,16 @@ func (s *Store) preMigrate(ctx context.Context) error { ...@@ -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)
} }
......
...@@ -10,7 +10,7 @@ The demo data includes **6 carefully selected memos** that showcase the key feat ...@@ -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: ...@@ -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
......
-- 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}}');
......
...@@ -41,12 +41,11 @@ func NewTestingStore(ctx context.Context, t *testing.T) *store.Store { ...@@ -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 { ...@@ -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(),
} }
} }
......
...@@ -20,7 +20,7 @@ func TestUserStore(t *testing.T) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) {
......
...@@ -8,8 +8,6 @@ import ( ...@@ -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 ( ...@@ -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:
......
...@@ -31,9 +31,7 @@ const MemberSection = () => { ...@@ -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");
......
...@@ -45,7 +45,7 @@ const Setting = () => { ...@@ -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];
......
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;
}; };
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