Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
f9e07a22
Commit
f9e07a22
authored
May 15, 2025
by
johnnyjoy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support update user's role
parent
ead2d700
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
158 additions
and
15 deletions
+158
-15
user_service.go
server/router/api/v1/user_service.go
+12
-7
user.go
store/db/mysql/user.go
+3
-0
user.go
store/db/postgres/user.go
+3
-0
user.go
store/db/sqlite/user.go
+3
-0
CreateUserDialog.tsx
web/src/components/CreateUserDialog.tsx
+134
-0
MemberSection.tsx
web/src/components/Settings/MemberSection.tsx
+2
-8
en.json
web/src/locales/en.json
+1
-0
No files found.
server/router/api/v1/user_service.go
View file @
f9e07a22
...
@@ -145,11 +145,9 @@ func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserR
...
@@ -145,11 +145,9 @@ func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserR
}
}
func
(
s
*
APIV1Service
)
UpdateUser
(
ctx
context
.
Context
,
request
*
v1pb
.
UpdateUserRequest
)
(
*
v1pb
.
User
,
error
)
{
func
(
s
*
APIV1Service
)
UpdateUser
(
ctx
context
.
Context
,
request
*
v1pb
.
UpdateUserRequest
)
(
*
v1pb
.
User
,
error
)
{
workspaceGeneralSetting
,
err
:=
s
.
Store
.
GetWorkspaceGeneralSetting
(
ctx
)
if
request
.
UpdateMask
==
nil
||
len
(
request
.
UpdateMask
.
Paths
)
==
0
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"update mask is empty"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get workspace general setting: %v"
,
err
)
}
}
userID
,
err
:=
ExtractUserIDFromName
(
request
.
User
.
Name
)
userID
,
err
:=
ExtractUserIDFromName
(
request
.
User
.
Name
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid user name: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid user name: %v"
,
err
)
...
@@ -158,12 +156,11 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
...
@@ -158,12 +156,11 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
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
)
}
}
// Check permission.
// 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
&&
currentUser
.
Role
!=
store
.
RoleHost
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
}
if
request
.
UpdateMask
==
nil
||
len
(
request
.
UpdateMask
.
Paths
)
==
0
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"update mask is empty"
)
}
user
,
err
:=
s
.
Store
.
GetUser
(
ctx
,
&
store
.
FindUser
{
ID
:
&
userID
})
user
,
err
:=
s
.
Store
.
GetUser
(
ctx
,
&
store
.
FindUser
{
ID
:
&
userID
})
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -178,6 +175,10 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
...
@@ -178,6 +175,10 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
ID
:
user
.
ID
,
ID
:
user
.
ID
,
UpdatedTs
:
&
currentTs
,
UpdatedTs
:
&
currentTs
,
}
}
workspaceGeneralSetting
,
err
:=
s
.
Store
.
GetWorkspaceGeneralSetting
(
ctx
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get workspace general setting: %v"
,
err
)
}
for
_
,
field
:=
range
request
.
UpdateMask
.
Paths
{
for
_
,
field
:=
range
request
.
UpdateMask
.
Paths
{
if
field
==
"username"
{
if
field
==
"username"
{
if
workspaceGeneralSetting
.
DisallowChangeUsername
{
if
workspaceGeneralSetting
.
DisallowChangeUsername
{
...
@@ -199,6 +200,10 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
...
@@ -199,6 +200,10 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
}
else
if
field
==
"description"
{
}
else
if
field
==
"description"
{
update
.
Description
=
&
request
.
User
.
Description
update
.
Description
=
&
request
.
User
.
Description
}
else
if
field
==
"role"
{
}
else
if
field
==
"role"
{
// Only allow admin to update role.
if
currentUser
.
Role
!=
store
.
RoleAdmin
&&
currentUser
.
Role
!=
store
.
RoleHost
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
role
:=
convertUserRoleToStore
(
request
.
User
.
Role
)
role
:=
convertUserRoleToStore
(
request
.
User
.
Role
)
update
.
Role
=
&
role
update
.
Role
=
&
role
}
else
if
field
==
"password"
{
}
else
if
field
==
"password"
{
...
...
store/db/mysql/user.go
View file @
f9e07a22
...
@@ -64,6 +64,9 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
...
@@ -64,6 +64,9 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
if
v
:=
update
.
Description
;
v
!=
nil
{
if
v
:=
update
.
Description
;
v
!=
nil
{
set
,
args
=
append
(
set
,
"`description` = ?"
),
append
(
args
,
*
v
)
set
,
args
=
append
(
set
,
"`description` = ?"
),
append
(
args
,
*
v
)
}
}
if
v
:=
update
.
Role
;
v
!=
nil
{
set
,
args
=
append
(
set
,
"`role` = ?"
),
append
(
args
,
*
v
)
}
args
=
append
(
args
,
update
.
ID
)
args
=
append
(
args
,
update
.
ID
)
query
:=
"UPDATE `user` SET "
+
strings
.
Join
(
set
,
", "
)
+
" WHERE `id` = ?"
query
:=
"UPDATE `user` SET "
+
strings
.
Join
(
set
,
", "
)
+
" WHERE `id` = ?"
...
...
store/db/postgres/user.go
View file @
f9e07a22
...
@@ -51,6 +51,9 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
...
@@ -51,6 +51,9 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
if
v
:=
update
.
Description
;
v
!=
nil
{
if
v
:=
update
.
Description
;
v
!=
nil
{
set
,
args
=
append
(
set
,
"description = "
+
placeholder
(
len
(
args
)
+
1
)),
append
(
args
,
*
v
)
set
,
args
=
append
(
set
,
"description = "
+
placeholder
(
len
(
args
)
+
1
)),
append
(
args
,
*
v
)
}
}
if
v
:=
update
.
Role
;
v
!=
nil
{
set
,
args
=
append
(
set
,
"role = "
+
placeholder
(
len
(
args
)
+
1
)),
append
(
args
,
*
v
)
}
query
:=
`
query
:=
`
UPDATE "user"
UPDATE "user"
...
...
store/db/sqlite/user.go
View file @
f9e07a22
...
@@ -52,6 +52,9 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
...
@@ -52,6 +52,9 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
if
v
:=
update
.
Description
;
v
!=
nil
{
if
v
:=
update
.
Description
;
v
!=
nil
{
set
,
args
=
append
(
set
,
"description = ?"
),
append
(
args
,
*
v
)
set
,
args
=
append
(
set
,
"description = ?"
),
append
(
args
,
*
v
)
}
}
if
v
:=
update
.
Role
;
v
!=
nil
{
set
,
args
=
append
(
set
,
"role = ?"
),
append
(
args
,
*
v
)
}
args
=
append
(
args
,
update
.
ID
)
args
=
append
(
args
,
update
.
ID
)
query
:=
`
query
:=
`
...
...
web/src/components/CreateUserDialog.tsx
0 → 100644
View file @
f9e07a22
import
{
Radio
,
RadioGroup
}
from
"@mui/joy"
;
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
User
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
user
?:
User
;
confirmCallback
?:
()
=>
void
;
}
const
CreateUserDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
confirmCallback
,
destroy
}
=
props
;
const
t
=
useTranslate
();
const
[
user
,
setUser
]
=
useState
(
User
.
fromPartial
({
...
props
.
user
}));
const
requestState
=
useLoading
(
false
);
const
isCreating
=
!
props
.
user
;
const
setPartialUser
=
(
state
:
Partial
<
User
>
)
=>
{
setUser
({
...
user
,
...
state
,
});
};
const
handleConfirm
=
async
()
=>
{
if
(
isCreating
&&
(
!
user
.
username
||
!
user
.
password
))
{
toast
.
error
(
"Username and password cannot be empty"
);
return
;
}
try
{
if
(
isCreating
)
{
await
userServiceClient
.
createUser
({
user
});
toast
.
success
(
"Create user successfully"
);
}
else
{
const
updateMask
=
[];
if
(
user
.
username
!==
props
.
user
?.
username
)
{
updateMask
.
push
(
"username"
);
}
if
(
user
.
password
)
{
updateMask
.
push
(
"password"
);
}
if
(
user
.
role
!==
props
.
user
?.
role
)
{
updateMask
.
push
(
"role"
);
}
await
userServiceClient
.
updateUser
({
user
,
updateMask
});
toast
.
success
(
"Update user successfully"
);
}
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
}
if
(
confirmCallback
)
{
confirmCallback
();
}
destroy
();
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
`${isCreating ? t("common.create") : t("common.edit")} ${t("common.user")}`
}
</
p
>
<
Button
size=
"sm"
variant=
"plain"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start max-w-md min-w-72"
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"text-sm whitespace-nowrap mb-1"
>
{
t
(
"common.username"
)
}
</
span
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
{
t
(
"common.username"
)
}
value=
{
user
.
username
}
onChange=
{
(
e
)
=>
setPartialUser
({
username
:
e
.
target
.
value
,
})
}
/>
<
span
className=
"text-sm whitespace-nowrap mt-3 mb-1"
>
{
t
(
"common.password"
)
}
</
span
>
<
Input
className=
"w-full"
type=
"password"
placeholder=
{
t
(
"common.password"
)
}
autoComplete=
"off"
value=
{
user
.
password
}
onChange=
{
(
e
)
=>
setPartialUser
({
password
:
e
.
target
.
value
,
})
}
/>
<
span
className=
"text-sm whitespace-nowrap mt-3 mb-1"
>
{
t
(
"common.role"
)
}
</
span
>
<
RadioGroup
orientation=
"horizontal"
defaultValue=
{
user
.
role
}
onChange=
{
(
e
)
=>
setPartialUser
({
role
:
e
.
target
.
value
as
User_Role
})
}
>
<
Radio
value=
{
User_Role
.
USER
}
label=
{
t
(
"setting.member-section.user"
)
}
/>
<
Radio
value=
{
User_Role
.
ADMIN
}
label=
{
t
(
"setting.member-section.admin"
)
}
/>
</
RadioGroup
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2 mt-2"
>
<
Button
variant=
"plain"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
{
t
(
"common.confirm"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showCreateUserDialog
(
user
?:
User
,
confirmCallback
?:
()
=>
void
)
{
generateDialog
(
{
className
:
"create-user-dialog"
,
dialogName
:
"create-user-dialog"
,
},
CreateUserDialog
,
{
user
,
confirmCallback
},
);
}
export
default
showCreateUserDialog
;
web/src/components/Settings/MemberSection.tsx
View file @
f9e07a22
...
@@ -10,7 +10,7 @@ import { userStore } from "@/store/v2";
...
@@ -10,7 +10,7 @@ import { userStore } from "@/store/v2";
import
{
State
}
from
"@/types/proto/api/v1/common"
;
import
{
State
}
from
"@/types/proto/api/v1/common"
;
import
{
User
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
import
{
User
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showC
hangeMemberPasswordDialog
from
"../ChangeMemberPassword
Dialog"
;
import
showC
reateUserDialog
from
"../CreateUser
Dialog"
;
interface
LocalState
{
interface
LocalState
{
creatingUser
:
User
;
creatingUser
:
User
;
...
@@ -106,10 +106,6 @@ const MemberSection = () => {
...
@@ -106,10 +106,6 @@ const MemberSection = () => {
});
});
};
};
const
handleChangePasswordClick
=
(
user
:
User
)
=>
{
showChangeMemberPasswordDialog
(
user
);
};
const
handleArchiveUserClick
=
async
(
user
:
User
)
=>
{
const
handleArchiveUserClick
=
async
(
user
:
User
)
=>
{
const
confirmed
=
window
.
confirm
(
t
(
"setting.member-section.archive-warning"
,
{
username
:
user
.
nickname
}));
const
confirmed
=
window
.
confirm
(
t
(
"setting.member-section.archive-warning"
,
{
username
:
user
.
nickname
}));
if
(
confirmed
)
{
if
(
confirmed
)
{
...
@@ -222,9 +218,7 @@ const MemberSection = () => {
...
@@ -222,9 +218,7 @@ const MemberSection = () => {
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
</
MenuButton
>
</
MenuButton
>
<
Menu
placement=
"bottom-end"
size=
"sm"
>
<
Menu
placement=
"bottom-end"
size=
"sm"
>
<
MenuItem
onClick=
{
()
=>
handleChangePasswordClick
(
user
)
}
>
<
MenuItem
onClick=
{
()
=>
showCreateUserDialog
(
user
,
()
=>
fetchUsers
())
}
>
{
t
(
"common.update"
)
}
</
MenuItem
>
{
t
(
"setting.account-section.change-password"
)
}
</
MenuItem
>
{
user
.
state
===
State
.
NORMAL
?
(
{
user
.
state
===
State
.
NORMAL
?
(
<
MenuItem
onClick=
{
()
=>
handleArchiveUserClick
(
user
)
}
>
{
t
(
"setting.member-section.archive-member"
)
}
</
MenuItem
>
<
MenuItem
onClick=
{
()
=>
handleArchiveUserClick
(
user
)
}
>
{
t
(
"setting.member-section.archive-member"
)
}
</
MenuItem
>
)
:
(
)
:
(
...
...
web/src/locales/en.json
View file @
f9e07a22
...
@@ -94,6 +94,7 @@
...
@@ -94,6 +94,7 @@
"unpin"
:
"Unpin"
,
"unpin"
:
"Unpin"
,
"update"
:
"Update"
,
"update"
:
"Update"
,
"upload"
:
"Upload"
,
"upload"
:
"Upload"
,
"user"
:
"User"
,
"username"
:
"Username"
,
"username"
:
"Username"
,
"version"
:
"Version"
,
"version"
:
"Version"
,
"visibility"
:
"Visibility"
,
"visibility"
:
"Visibility"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment