diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/annotation/AdminOnly.java b/data-agent-backend/src/main/java/io/github/malonetalk/annotation/AdminOnly.java new file mode 100644 index 0000000..2af3280 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/annotation/AdminOnly.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 标记需要管理员权限的 Controller 方法或类。 + * + *

可用于方法级覆盖类级行为。拦截器检查当前用户 role_id == 1 时放行,否则返回 403。 + * 与 {@link io.github.malonetalk.common.ErrorCode#FORBIDDEN} 联动。 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AdminOnly {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/common/UserContext.java b/data-agent-backend/src/main/java/io/github/malonetalk/common/UserContext.java index 4065854..e749481 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/common/UserContext.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/common/UserContext.java @@ -22,8 +22,10 @@ * *

仅承载鉴权必要字段(不含 password_hash),供管理/会话等同步 API 取用。Agent 异步链路 * (Reactor 弹性线程)拿不到此 ThreadLocal——权限轮次会改为通过 ToolCallContext 显式传 userId。 + * + *

{@code roleId} 用于 @AdminOnly 权限判定:1=管理员,其他值=普通用户。 */ -public record UserContext(Integer userId, String username, String displayName) { +public record UserContext(Integer userId, String username, String displayName, Integer roleId) { private static final ThreadLocal HOLDER = new ThreadLocal<>(); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/config/AdminBootstrapRunner.java b/data-agent-backend/src/main/java/io/github/malonetalk/config/AdminBootstrapRunner.java index 2d06e36..a6d1a6c 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/config/AdminBootstrapRunner.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/config/AdminBootstrapRunner.java @@ -62,7 +62,7 @@ public void run(String... args) { admin.setUsername("admin"); admin.setPasswordHash(PasswordUtil.hash(adminInitPassword)); admin.setDisplayName("管理员"); - admin.setRoleId(0); + admin.setRoleId(1); // 管理员角色,对应 @AdminOnly 权限判定 admin.setIdpType("LOCAL"); admin.setIdpUserId(null); admin.setStatus(1); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/DatasourceController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DatasourceController.java index 280bb1f..da9b162 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/DatasourceController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DatasourceController.java @@ -18,6 +18,7 @@ package io.github.malonetalk.controller; import io.github.malonetalk.agent.datasource.DataSourceType; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.common.Result; import io.github.malonetalk.convertor.DatasourceConverter; @@ -59,6 +60,7 @@ public Result findById(@PathVariable Integer id) { return Result.success(datasourceConverter.toResponse(requireDatasource(id))); } + @AdminOnly @PostMapping public Result save(@Valid @RequestBody DatasourceRequest request) { DataSourceType type = requireDatasourceType(request.type()); @@ -70,6 +72,7 @@ public Result save(@Valid @RequestBody DatasourceRequest request) { return Result.success(); } + @AdminOnly @PutMapping("/{id}") public Result update( @PathVariable Integer id, @Valid @RequestBody DatasourceRequest request) { @@ -91,6 +94,7 @@ public Result update( return Result.success(true); } + @AdminOnly @DeleteMapping("/{id}") public Result deleteById(@PathVariable Integer id) { requireDatasource(id); @@ -117,6 +121,7 @@ public Result> findByType(@PathVariable String type) { return Result.success(list); } + @AdminOnly @PutMapping("/{id}/activate") public Result activate(@PathVariable Integer id) { requireDatasource(id); @@ -126,6 +131,7 @@ public Result activate(@PathVariable Integer id) { return Result.success(true); } + @AdminOnly @PutMapping("/{id}/deactivate") public Result deactivate(@PathVariable Integer id) { requireDatasource(id); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/DomainController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DomainController.java index 851f1d6..a57d728 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/DomainController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DomainController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.DomainCreateRequest; @@ -39,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@AdminOnly @RestController @RequestMapping("/api/domains") @RequiredArgsConstructor diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/McpServerController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/McpServerController.java index c609024..bf75edf 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/McpServerController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/McpServerController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.common.Result; import io.github.malonetalk.convertor.McpServerConverter; @@ -38,6 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@AdminOnly @RestController @AllArgsConstructor @RequestMapping("/api/mcp-server") diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/MetricController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/MetricController.java index efa2008..a6fbd77 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/MetricController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/MetricController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.Result; import io.github.malonetalk.convertor.MetricConverter; import io.github.malonetalk.dto.MetricRequest; @@ -38,6 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@AdminOnly @Slf4j @RestController @AllArgsConstructor diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ReportController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ReportController.java index b023ae6..9f64ddf 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ReportController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ReportController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.ReportPageQuery; import io.github.malonetalk.dto.ReportResponse; @@ -43,6 +44,7 @@ public Result> findReports(@Valid ReportPageQuery q return Result.success(reportService.getReportPage(query)); } + @AdminOnly @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id) { RequestAssert.requireNonNegative(id, "id must be non-negative."); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/SysUserController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/SysUserController.java new file mode 100644 index 0000000..d61f8e6 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/SysUserController.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.controller; + +import io.github.malonetalk.annotation.AdminOnly; +import io.github.malonetalk.common.Result; +import io.github.malonetalk.dto.ResetPasswordRequest; +import io.github.malonetalk.dto.UserCreateRequest; +import io.github.malonetalk.dto.UserResponse; +import io.github.malonetalk.dto.UserUpdateRequest; +import io.github.malonetalk.service.SysUserService; +import jakarta.validation.Valid; +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 用户管理 CRUD(权限轮次再加 @AdminOnly;本轮登录后即可用)。 + * + *

username 唯一性由 Service 层保证;password 不允许通过 update 接口修改(需调用重置密码)。 + */ +@AdminOnly +@RestController +@AllArgsConstructor +@RequestMapping("/api/sys/user") +@Validated +public class SysUserController { + + private final SysUserService sysUserService; + + @GetMapping + public Result> listAll() { + return Result.success(sysUserService.listAll()); + } + + @PostMapping + public Result create(@Valid @RequestBody UserCreateRequest request) { + return Result.success(sysUserService.create(request)); + } + + @PutMapping("/{id}") + public Result update( + @PathVariable Integer id, @Valid @RequestBody UserUpdateRequest request) { + return Result.success(sysUserService.update(id, request)); + } + + /** 管理员重置用户密码(不需旧密码)。 */ + @PutMapping("/{id}/password") + public Result resetPassword( + @PathVariable Integer id, @Valid @RequestBody ResetPasswordRequest request) { + sysUserService.resetPassword(id, request.newPassword()); + return Result.success(true); + } + + /** 启 / 停用户。 */ + @PutMapping("/{id}/status") + public Result updateStatus( + @PathVariable Integer id, + @RequestParam + @jakarta.validation.constraints.Min(0) + @jakarta.validation.constraints.Max(1) + Integer status) { + sysUserService.updateStatus(id, status); + return Result.success(true); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableColumnSemanticController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableColumnSemanticController.java index 8de1af8..23be576 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableColumnSemanticController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableColumnSemanticController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.pagination.PageResponse; import io.github.malonetalk.dto.semantic.BatchResetColumnSemanticRequest; @@ -39,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@AdminOnly @RestController @Validated @RequestMapping("/api/semantic/tables/columns/{tableName}") diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationSemanticController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationSemanticController.java index f86ff42..b34f5fd 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationSemanticController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationSemanticController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.pagination.PageResponse; import io.github.malonetalk.dto.semantic.BatchDeleteLogicalTableRelationRequest; @@ -42,6 +43,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@AdminOnly @RestController @Validated @RequestMapping("/api/semantic/tables/relations/{tableName}") diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationWorkspaceController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationWorkspaceController.java index c2827a7..4d1ef04 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationWorkspaceController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationWorkspaceController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.semantic.RelationWorkspacePageQuery; import io.github.malonetalk.dto.semantic.RelationWorkspaceResponse; @@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@AdminOnly @RestController @RequestMapping("/api/semantic/tables/relations/workspace") @RequiredArgsConstructor diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticController.java index 0a524e3..19cfc7d 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.pagination.PageResponse; import io.github.malonetalk.dto.semantic.BatchResetTableSemanticRequest; @@ -39,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@AdminOnly @RestController @Validated @RequestMapping("/api/semantic/tables") diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticSyncController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticSyncController.java index e155adb..ded0dca 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticSyncController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticSyncController.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.pagination.PageResponse; import io.github.malonetalk.dto.semantic.PhysicalTableCandidatePageQuery; @@ -33,6 +34,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@AdminOnly @RestController @RequestMapping("/api/semantic/tables/sync") @RequiredArgsConstructor diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ResetPasswordRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ResetPasswordRequest.java new file mode 100644 index 0000000..3af0a82 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ResetPasswordRequest.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** 管理员重置用户密码(不需旧密码);区别于 {@link ChangePasswordRequest}(用户自己改,需验旧密码)。 */ +public record ResetPasswordRequest( + @NotBlank(message = "newPassword 不能为空") + @Size(min = 6, max = 64, message = "newPassword 长度需在 6-64 之间") + String newPassword) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserCreateRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserCreateRequest.java new file mode 100644 index 0000000..a9f08bb --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserCreateRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record UserCreateRequest( + @NotBlank(message = "username 不能为空") String username, + @NotBlank(message = "password 不能为空") + @Size(min = 6, max = 64, message = "password 长度需在 6-64 之间") + String password, + @NotBlank(message = "displayName 不能为空") String displayName, + @NotNull @Min(0) @Max(1) Integer roleId) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserResponse.java new file mode 100644 index 0000000..300bdd7 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import java.time.LocalDateTime; + +public record UserResponse( + Integer id, + String username, + String displayName, + Integer roleId, + Integer status, + LocalDateTime createTime) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserUpdateRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserUpdateRequest.java new file mode 100644 index 0000000..4bb3412 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/UserUpdateRequest.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +/** 更新用户。roleId 为 null 表示不修改角色。 */ +public record UserUpdateRequest( + @NotBlank(message = "displayName 不能为空") String displayName, + @Min(0) @Max(1) Integer roleId) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/interceptor/AuthInterceptor.java b/data-agent-backend/src/main/java/io/github/malonetalk/interceptor/AuthInterceptor.java index 926cd22..b86327a 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/interceptor/AuthInterceptor.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/interceptor/AuthInterceptor.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.interceptor; +import io.github.malonetalk.annotation.AdminOnly; import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.common.UserContext; import io.github.malonetalk.exception.BusinessException; @@ -26,6 +27,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; /** @@ -35,12 +37,15 @@ * → 放入 UserContext。token 缺失/过期/非法 或 用户被禁用 一律抛 {@link ErrorCode#UNAUTHORIZED}, * 由 GlobalExceptionHandler 统一输出 401。每次查库保证禁用即时生效。 * - *

权限轮次再加 @AdminOnly 与表/列拦截;本轮登录后所有接口行为与现状一致。 + *

方法/类上有 {@link AdminOnly} 且当前用户 role_id != 1 时返回 + * {@link ErrorCode#FORBIDDEN} (403)。表/列拦截、会话隔离 = 后续轮次。 */ @Component @AllArgsConstructor public class AuthInterceptor implements HandlerInterceptor { + private static final int ADMIN_ROLE_ID = 1; + private final JwtUtil jwtUtil; private final SysUserMapper sysUserMapper; @@ -58,6 +63,12 @@ public boolean preHandle( ErrorCode.UNAUTHORIZED, "Account is disabled or does not exist."); } UserContext.set(context); + + if (handler instanceof HandlerMethod handlerMethod && isAdminRequired(handlerMethod)) { + if (context.roleId() == null || context.roleId() != ADMIN_ROLE_ID) { + throw BusinessException.of(ErrorCode.FORBIDDEN, "需要管理员权限"); + } + } return true; } @@ -70,6 +81,15 @@ public void afterCompletion( UserContext.clear(); } + /** 方法或所在类上有 @AdminOnly 注解时要求管理员权限;方法级注解覆盖类级。 */ + private boolean isAdminRequired(HandlerMethod handlerMethod) { + AdminOnly methodAnnotation = handlerMethod.getMethodAnnotation(AdminOnly.class); + if (methodAnnotation != null) { + return true; + } + return handlerMethod.getBeanType().isAnnotationPresent(AdminOnly.class); + } + private String extractBearer(HttpServletRequest request) { String header = request.getHeader("Authorization"); if (header == null || header.isBlank()) { diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SysUserMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SysUserMapper.java index ceefded..77f03f8 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SysUserMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SysUserMapper.java @@ -19,6 +19,7 @@ import io.github.malonetalk.common.UserContext; import io.github.malonetalk.entity.SysUser; +import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -41,4 +42,10 @@ int updatePassword( @Param("updateTime") java.time.LocalDateTime updateTime); int countAll(); + + List selectAll(); + + int update(SysUser user); + + int updateStatus(@Param("id") Integer id, @Param("status") Integer status); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/SysUserService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/SysUserService.java new file mode 100644 index 0000000..4b2fe4e --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/SysUserService.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.dto.UserCreateRequest; +import io.github.malonetalk.dto.UserResponse; +import io.github.malonetalk.dto.UserUpdateRequest; +import java.util.List; + +public interface SysUserService { + + List listAll(); + + UserResponse create(UserCreateRequest request); + + UserResponse update(Integer id, UserUpdateRequest request); + + void resetPassword(Integer id, String newPassword); + + void updateStatus(Integer id, Integer status); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/SysUserServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/SysUserServiceImpl.java new file mode 100644 index 0000000..c2aff40 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/SysUserServiceImpl.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.dto.UserCreateRequest; +import io.github.malonetalk.dto.UserResponse; +import io.github.malonetalk.dto.UserUpdateRequest; +import io.github.malonetalk.entity.SysUser; +import io.github.malonetalk.exception.BusinessException; +import io.github.malonetalk.mapper.SysUserMapper; +import io.github.malonetalk.util.PasswordUtil; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class SysUserServiceImpl implements SysUserService { + + private final SysUserMapper sysUserMapper; + + @Override + public List listAll() { + return sysUserMapper.selectAll().stream().map(this::toResponse).toList(); + } + + @Override + public UserResponse create(UserCreateRequest request) { + SysUser existing = sysUserMapper.selectByUsername(request.username()); + if (existing != null) { + throw BusinessException.of( + ErrorCode.DATA_CONFLICT, "用户名 '" + request.username() + "' 已存在"); + } + LocalDateTime now = LocalDateTime.now(); + SysUser user = new SysUser(); + user.setUsername(request.username()); + user.setPasswordHash(PasswordUtil.hash(request.password())); + user.setDisplayName(request.displayName()); + user.setRoleId(request.roleId()); + user.setIdpType("LOCAL"); + user.setStatus(1); + user.setCreateTime(now); + user.setUpdateTime(now); + sysUserMapper.insert(user); + return toResponse(user); + } + + @Override + public UserResponse update(Integer id, UserUpdateRequest request) { + SysUser user = requireUser(id); + user.setDisplayName(request.displayName()); + if (request.roleId() != null) { + user.setRoleId(request.roleId()); + } + user.setUpdateTime(LocalDateTime.now()); + sysUserMapper.update(user); + return toResponse(user); + } + + @Override + public void resetPassword(Integer id, String newPassword) { + SysUser user = requireUser(id); + if (user.getPasswordHash() == null) { + throw BusinessException.of(ErrorCode.BAD_REQUEST, "外部身份源用户无法重置密码"); + } + sysUserMapper.updatePassword(id, PasswordUtil.hash(newPassword), LocalDateTime.now()); + } + + @Override + public void updateStatus(Integer id, Integer status) { + requireUser(id); + sysUserMapper.updateStatus(id, status); + } + + private SysUser requireUser(Integer id) { + SysUser user = sysUserMapper.selectById(id); + if (user == null) { + throw BusinessException.of(ErrorCode.RESOURCE_NOT_FOUND, "用户不存在"); + } + return user; + } + + private UserResponse toResponse(SysUser user) { + return new UserResponse( + user.getId(), + user.getUsername(), + user.getDisplayName(), + user.getRoleId(), + user.getStatus(), + user.getCreateTime()); + } +} diff --git a/data-agent-backend/src/main/resources/application.properties b/data-agent-backend/src/main/resources/application.properties index dccb8e1..084e531 100644 --- a/data-agent-backend/src/main/resources/application.properties +++ b/data-agent-backend/src/main/resources/application.properties @@ -27,3 +27,10 @@ io.github.malonetalk.model.base-url= io.github.malonetalk.model.api-key=${IO_GITHUB_MALONETALK_MODEL_API_KEY:} spring.config.import=classpath:skill.properties + +# Auth Configuration (login round): all env-driven, no secrets committed. +# JWT secret must be >= 32 bytes in production; blank => in-memory random key (dev only, tokens invalidated on restart). +jwt.secret=${JWT_SECRET:} +jwt.expiration-hours=${JWT_EXPIRATION_HOURS:24} +# Initial admin password for first startup when sys_user is empty; fail-closed if unset. +admin.init-password=${ADMIN_INIT_PASSWORD:} diff --git a/data-agent-backend/src/main/resources/mapper/SysUserMapper.xml b/data-agent-backend/src/main/resources/mapper/SysUserMapper.xml index f43caa3..58c25d1 100644 --- a/data-agent-backend/src/main/resources/mapper/SysUserMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/SysUserMapper.xml @@ -20,6 +20,7 @@ + @@ -33,7 +34,7 @@ @@ -57,4 +58,22 @@ SELECT COUNT(*) FROM sys_user + + + + UPDATE sys_user + + display_name = #{displayName}, + role_id = #{roleId}, + update_time = #{updateTime}, + + WHERE id = #{id} + + + + UPDATE sys_user SET status = #{status}, update_time = NOW() WHERE id = #{id} + + diff --git a/data-agent-frontend/src/api/sysUser.ts b/data-agent-frontend/src/api/sysUser.ts new file mode 100644 index 0000000..17e9b5f --- /dev/null +++ b/data-agent-frontend/src/api/sysUser.ts @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ + +import request from './request'; + +export interface UserResponse { + id: number; + username: string; + displayName: string; + roleId: number; // 1=管理员 0=普通用户 + status: number; // 1=启用 0=禁用 + createTime: string; +} + +export interface UserCreateRequest { + username: string; + password: string; + displayName: string; + roleId: number; +} + +export interface UserUpdateRequest { + displayName: string; + roleId: number | null; +} + +type ApiResult = { code: number; message: string; data: T }; + +export function listUsers() { + return request.get>('/sys/user').then(res => res.data.data); +} + +export function createUser(payload: UserCreateRequest) { + return request.post>('/sys/user', payload).then(res => res.data.data); +} + +export function updateUser(id: number, payload: UserUpdateRequest) { + return request + .put>(`/sys/user/${id}`, payload) + .then(res => res.data.data); +} + +export function resetPassword(id: number, newPassword: string) { + return request + .put>(`/sys/user/${id}/password`, { newPassword }) + .then(res => res.data.data); +} + +export function updateStatus(id: number, status: number) { + return request + .put>(`/sys/user/${id}/status`, null, { + params: { status }, + }) + .then(res => res.data.data); +} diff --git a/data-agent-frontend/src/components/layout/AppSidebar.vue b/data-agent-frontend/src/components/layout/AppSidebar.vue index 5bf2dec..8f5d6b2 100644 --- a/data-agent-frontend/src/components/layout/AppSidebar.vue +++ b/data-agent-frontend/src/components/layout/AppSidebar.vue @@ -30,6 +30,7 @@ { path: '/semantic', title: '语义管理', icon: 'Collection' }, { path: '/report', title: '报告管理', icon: 'Document' }, { path: '/metric', title: '指标口径管理', icon: 'DataLine' }, + { path: '/sys-user', title: '用户管理', icon: 'User' }, ]; const activeMenu = computed(() => route.path); diff --git a/data-agent-frontend/src/router/index.ts b/data-agent-frontend/src/router/index.ts index fe2b92d..7264f69 100644 --- a/data-agent-frontend/src/router/index.ts +++ b/data-agent-frontend/src/router/index.ts @@ -67,6 +67,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/metric/MetricManage.vue'), meta: { title: '指标口径管理' }, }, + { + path: '/sys-user', + name: 'UserManage', + component: () => import('@/views/sys-user/UserManage.vue'), + meta: { title: '用户管理' }, + }, ]; const router = createRouter({ diff --git a/data-agent-frontend/src/views/sys-user/UserManage.vue b/data-agent-frontend/src/views/sys-user/UserManage.vue new file mode 100644 index 0000000..f44979b --- /dev/null +++ b/data-agent-frontend/src/views/sys-user/UserManage.vue @@ -0,0 +1,291 @@ + + + + + + +