From 8fbe30d7acd7d199db2864907ea1ec80bfbab4ae Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 17 Apr 2026 00:11:10 +0800 Subject: [PATCH] feat(auth): password change dialog + workspace member provisioning improvements --- .../mate/auth/controller/AuthController.java | 13 +- .../vip/mate/auth/model/LoginResponse.java | 1 + .../vip/mate/auth/service/AuthService.java | 23 +- .../core/controller/WorkspaceController.java | 30 +- mateclaw-ui/src/api/index.ts | 4 +- .../src/components/ChangePasswordDialog.vue | 305 ++++++++++++++++++ mateclaw-ui/src/i18n/locales/en-US.ts | 23 ++ mateclaw-ui/src/i18n/locales/zh-CN.ts | 23 ++ mateclaw-ui/src/router/index.ts | 5 +- mateclaw-ui/src/views/Login.vue | 1 + .../src/views/Security/Members/index.vue | 50 ++- .../src/views/Settings/Models/index.vue | 24 +- mateclaw-ui/src/views/layout/MainLayout.vue | 16 +- 13 files changed, 492 insertions(+), 26 deletions(-) create mode 100644 mateclaw-ui/src/components/ChangePasswordDialog.vue diff --git a/mateclaw-server/src/main/java/vip/mate/auth/controller/AuthController.java b/mateclaw-server/src/main/java/vip/mate/auth/controller/AuthController.java index 14e53552..b82521fa 100644 --- a/mateclaw-server/src/main/java/vip/mate/auth/controller/AuthController.java +++ b/mateclaw-server/src/main/java/vip/mate/auth/controller/AuthController.java @@ -3,12 +3,14 @@ package vip.mate.auth.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import vip.mate.auth.model.LoginRequest; import vip.mate.auth.model.LoginResponse; import vip.mate.auth.model.UserEntity; import vip.mate.auth.service.AuthService; import vip.mate.common.result.R; +import vip.mate.exception.MateClawException; import java.util.List; @@ -48,8 +50,15 @@ public class AuthController { public R changePassword( @PathVariable Long id, @RequestParam String oldPassword, - @RequestParam String newPassword) { - authService.changePassword(id, oldPassword, newPassword); + @RequestParam String newPassword, + Authentication auth) { + // Resolve user from the JWT principal — the {id} path segment is + // informational. A user may only change their own password. + UserEntity me = authService.findByUsername(auth.getName()); + if (me == null) { + throw new MateClawException("err.auth.user_not_found", "用户不存在"); + } + authService.changePassword(me.getId(), oldPassword, newPassword); return R.ok(); } } diff --git a/mateclaw-server/src/main/java/vip/mate/auth/model/LoginResponse.java b/mateclaw-server/src/main/java/vip/mate/auth/model/LoginResponse.java index ddbb9401..2960f411 100644 --- a/mateclaw-server/src/main/java/vip/mate/auth/model/LoginResponse.java +++ b/mateclaw-server/src/main/java/vip/mate/auth/model/LoginResponse.java @@ -11,6 +11,7 @@ import lombok.Data; @Data @AllArgsConstructor public class LoginResponse { + private Long id; private String token; private String username; private String nickname; diff --git a/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java b/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java index 4a61d315..25f9642c 100644 --- a/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java +++ b/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java @@ -55,7 +55,7 @@ public class AuthService { } String token = generateToken(user); - return new LoginResponse(token, user.getUsername(), user.getNickname(), user.getRole()); + return new LoginResponse(user.getId(), token, user.getUsername(), user.getNickname(), user.getRole()); } /** @@ -76,7 +76,10 @@ public class AuthService { if (count > 0) { throw new MateClawException("err.auth.username_exists", "用户名已存在: " + user.getUsername()); } - user.setPassword(passwordEncoder.encode(user.getPassword())); + if (user.getPassword() == null || user.getPassword().isBlank()) { + throw new MateClawException("err.auth.password_required", "Password is required"); + } + user.setPassword(passwordEncoder.encode(user.getPassword().trim())); user.setEnabled(true); if (user.getRole() == null) { user.setRole("user"); @@ -86,6 +89,22 @@ public class AuthService { return user; } + /** + * Reset password (admin operation — no old password required). + * Used when an admin wants to set/reset a member's password. + */ + public void resetPassword(Long userId, String newPassword) { + if (newPassword == null || newPassword.isBlank()) { + throw new MateClawException("err.auth.password_required", "Password is required"); + } + UserEntity user = userMapper.selectById(userId); + if (user == null) { + throw new MateClawException("err.auth.user_not_found", "用户不存在"); + } + user.setPassword(passwordEncoder.encode(newPassword.trim())); + userMapper.updateById(user); + } + /** * 修改密码 */ diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/core/controller/WorkspaceController.java b/mateclaw-server/src/main/java/vip/mate/workspace/core/controller/WorkspaceController.java index 6a1c2ae8..1f1a26c8 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/core/controller/WorkspaceController.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/core/controller/WorkspaceController.java @@ -94,7 +94,35 @@ public class WorkspaceController { Authentication auth) { Long userId = resolveUserId(auth); workspaceService.requirePermission(id, userId, "admin"); - Long targetUserId = Long.valueOf(body.get("userId").toString()); + + Long targetUserId; + if (body.containsKey("username")) { + String username = body.get("username").toString().trim(); + String password = body.containsKey("password") && body.get("password") != null + ? body.get("password").toString().trim() : null; + UserEntity target = authService.findByUsername(username); + if (target == null) { + // User does not exist — create account (password required) + if (password == null || password.isBlank()) { + throw new MateClawException("err.workspace.user_not_found", + "User not found: " + username + ". Provide a password to create the account."); + } + UserEntity newUser = new UserEntity(); + newUser.setUsername(username); + newUser.setPassword(password); + newUser.setNickname(body.containsKey("nickname") + ? body.get("nickname").toString() : username); + target = authService.createUser(newUser); + } else if (password != null && !password.isBlank()) { + // User exists AND admin provided a password — reset it. + // This fixes the case where an admin removes a member, re-adds + // them with a new password, but the stale password blocks login. + authService.resetPassword(target.getId(), password); + } + targetUserId = target.getId(); + } else { + targetUserId = Long.valueOf(body.get("userId").toString()); + } String role = body.containsKey("role") ? body.get("role").toString() : "member"; return R.ok(workspaceService.addMember(id, targetUserId, role)); } diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index c48f40bf..f2f83169 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -68,6 +68,8 @@ export const authApi = { http.post('/auth/login', data), listUsers: () => http.get('/auth/users'), createUser: (data: any) => http.post('/auth/users', data), + changePassword: (id: number, oldPassword: string, newPassword: string) => + http.put(`/auth/users/${id}/password`, null, { params: { oldPassword, newPassword } }), } // ==================== Agent ==================== @@ -415,7 +417,7 @@ export const workspaceTeamApi = { update: (id: string | number, data: any) => http.put(`/workspaces/${id}`, data), delete: (id: string | number) => http.delete(`/workspaces/${id}`), listMembers: (id: string | number) => http.get(`/workspaces/${id}/members`), - addMember: (id: string | number, data: { userId: number; role?: string }) => + addMember: (id: string | number, data: { username: string; password?: string; nickname?: string; role?: string }) => http.post(`/workspaces/${id}/members`, data), updateMemberRole: (id: string | number, memberId: string | number, role: string) => http.put(`/workspaces/${id}/members/${memberId}`, { role }), diff --git a/mateclaw-ui/src/components/ChangePasswordDialog.vue b/mateclaw-ui/src/components/ChangePasswordDialog.vue new file mode 100644 index 00000000..7895e95c --- /dev/null +++ b/mateclaw-ui/src/components/ChangePasswordDialog.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 847f5479..cba50482 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -33,6 +33,16 @@ export default { enable: 'Enable', disable: 'Disable', }, + auth: { + changePassword: 'Change Password', + oldPassword: 'Current Password', + newPassword: 'New Password', + confirmPassword: 'Confirm New Password', + fieldsRequired: 'Please enter current and new password', + passwordMismatch: 'New passwords do not match', + passwordChanged: 'Password changed successfully', + passwordChangeFailed: 'Failed to change password', + }, chat: { status: { idle: 'Ready', @@ -301,6 +311,9 @@ export default { providerDeleted: 'Provider deleted', modelAdded: 'Model added', modelRemoved: 'Model removed', + modelAddFailed: 'Failed to add model', + modelRemoveFailed: 'Failed to remove model', + activeChangeFailed: 'Failed to change active model', deleteConfirm: 'Delete provider "{name}"?', removeConfirm: 'Remove model "{name}"?', generateConfigInvalidJson: 'Generate kwargs is not valid JSON', @@ -715,6 +728,16 @@ export default { title: 'Add Member', userId: 'User ID', userIdPlaceholder: 'Enter user ID', + user: 'Select User', + userPlaceholder: 'Search username…', + username: 'Username', + usernamePlaceholder: 'Enter username', + usernameHint: 'Account will be created automatically if user does not exist', + password: 'Password', + passwordPlaceholder: 'Required for new users', + passwordHint: 'Leave empty for existing users, required for new users', + nickname: 'Nickname', + nicknamePlaceholder: 'Optional display name', role: 'Role', }, actions: { diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 96b9adc6..b553c15a 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -33,6 +33,16 @@ export default { enable: '启用', disable: '停用', }, + auth: { + changePassword: '修改密码', + oldPassword: '当前密码', + newPassword: '新密码', + confirmPassword: '确认新密码', + fieldsRequired: '请填写当前密码和新密码', + passwordMismatch: '两次输入的新密码不一致', + passwordChanged: '密码修改成功', + passwordChangeFailed: '密码修改失败', + }, chat: { status: { idle: '就绪', @@ -291,6 +301,9 @@ export default { providerDeleted: 'Provider 已删除', modelAdded: '模型已添加', modelRemoved: '模型已移除', + modelAddFailed: '模型添加失败', + modelRemoveFailed: '模型删除失败', + activeChangeFailed: '激活模型切换失败', deleteConfirm: '确认删除 Provider “{name}”?', removeConfirm: '确认移除模型 “{name}”?', generateConfigInvalidJson: 'Generate Kwargs 不是合法 JSON', @@ -715,6 +728,16 @@ export default { title: '添加成员', userId: '用户 ID', userIdPlaceholder: '输入用户 ID', + user: '选择用户', + userPlaceholder: '搜索用户名…', + username: '用户名', + usernamePlaceholder: '输入用户名', + usernameHint: '如果用户不存在,将自动创建账号', + password: '密码', + passwordPlaceholder: '新用户需填写密码', + passwordHint: '已有用户无需填写,新用户必填', + nickname: '昵称', + nicknamePlaceholder: '可选,用户显示名称', role: '角色', }, actions: { diff --git a/mateclaw-ui/src/router/index.ts b/mateclaw-ui/src/router/index.ts index f5b93e4d..322b0d4b 100644 --- a/mateclaw-ui/src/router/index.ts +++ b/mateclaw-ui/src/router/index.ts @@ -221,7 +221,10 @@ router.beforeEach((to, _from, next) => { return } const token = localStorage.getItem('token') - if (to.name !== 'Login' && !token) { + if (to.name === 'Login' && token) { + // Already logged in — skip login page + next({ path: '/' }) + } else if (to.name !== 'Login' && !token) { next({ name: 'Login' }) } else { next() diff --git a/mateclaw-ui/src/views/Login.vue b/mateclaw-ui/src/views/Login.vue index e5e2e547..bb147402 100644 --- a/mateclaw-ui/src/views/Login.vue +++ b/mateclaw-ui/src/views/Login.vue @@ -77,6 +77,7 @@ async function handleLogin() { const res: any = await authApi.login(form) const data = res.data || res localStorage.setItem('token', data.token) + localStorage.setItem('userId', String(data.id || '1')) localStorage.setItem('username', data.username || form.username) localStorage.setItem('role', data.role || 'user') router.push('/') diff --git a/mateclaw-ui/src/views/Security/Members/index.vue b/mateclaw-ui/src/views/Security/Members/index.vue index d312494a..6752051c 100644 --- a/mateclaw-ui/src/views/Security/Members/index.vue +++ b/mateclaw-ui/src/views/Security/Members/index.vue @@ -83,12 +83,22 @@ @@ -107,7 +117,7 @@