diff --git a/adminSystem/src/api/amt.ts b/adminSystem/src/api/amt.ts index f618627..934fb69 100644 --- a/adminSystem/src/api/amt.ts +++ b/adminSystem/src/api/amt.ts @@ -42,6 +42,24 @@ export const deviceApi = { }) }, + // 手动添加设备 + addDevice(data: { ipAddress: string; hostname?: string; description?: string; windowsUsername?: string; windowsPassword?: string }) { + return request.post({ + url: '/api/devices', + params: data, + showSuccessMessage: true + }) + }, + + // 更新设备 + updateDevice(id: number, data: { hostname?: string; description?: string }) { + return request.put({ + url: `/api/devices/${id}`, + params: data, + showSuccessMessage: true + }) + }, + // 删除设备 deleteDevice(id: number) { return request.del({ @@ -62,6 +80,22 @@ export const deviceApi = { return request.get({ url: `/api/devices/${id}/status` }) + }, + + // 设置设备 Windows 凭据 + setDeviceCredentials(id: number, data: { username?: string; password?: string }) { + return request.put({ + url: `/api/devices/${id}/credentials`, + params: data, + showSuccessMessage: true + }) + }, + + // 获取设备 Windows 凭据 + getDeviceCredentials(id: number) { + return request.get({ + url: `/api/devices/${id}/credentials` + }) } } @@ -221,7 +255,7 @@ export const remoteDesktopApi = { // 直接连接(需要凭据) connect(deviceId: number, credentials: { username: string; password: string; domain?: string }) { return request.post({ - url: `/api/remotedesktop/connect/${deviceId}`, + url: `/api/remote-desktop/connect/${deviceId}`, params: credentials }) }, @@ -229,7 +263,7 @@ export const remoteDesktopApi = { // 生成访问 Token generateToken(deviceId: number, options: { credentialId?: number; expiresInMinutes?: number; maxUseCount?: number; note?: string } = {}) { return request.post({ - url: `/api/remotedesktop/generate-token/${deviceId}`, + url: `/api/remote-desktop/generate-token/${deviceId}`, params: options }) }, @@ -237,28 +271,28 @@ export const remoteDesktopApi = { // 通过 Token 连接 connectByToken(token: string) { return request.get({ - url: `/api/remotedesktop/connect-by-token/${token}` + url: `/api/remote-desktop/connect-by-token/${token}` }) }, // 验证 Token validateToken(token: string) { return request.get({ - url: `/api/remotedesktop/validate-token/${token}` + url: `/api/remote-desktop/validate-token/${token}` }) }, // 获取设备的所有 Token getDeviceTokens(deviceId: number) { return request.get({ - url: `/api/remotedesktop/list-tokens/${deviceId}` + url: `/api/remote-desktop/list-tokens/${deviceId}` }) }, // 撤销 Token revokeToken(tokenId: number) { return request.del({ - url: `/api/remotedesktop/revoke-token/${tokenId}`, + url: `/api/remote-desktop/revoke-token/${tokenId}`, showSuccessMessage: true }) }, @@ -266,14 +300,14 @@ export const remoteDesktopApi = { // 清理过期 Token cleanupTokens() { return request.post({ - url: '/api/remotedesktop/cleanup-tokens' + url: '/api/remote-desktop/cleanup-tokens' }) }, // 测试 Guacamole 连接 test() { return request.get({ - url: '/api/remotedesktop/test' + url: '/api/remote-desktop/test' }) } } diff --git a/adminSystem/src/views/amt/devices.vue b/adminSystem/src/views/amt/devices.vue index dfb0126..f165c16 100644 --- a/adminSystem/src/views/amt/devices.vue +++ b/adminSystem/src/views/amt/devices.vue @@ -3,7 +3,7 @@ + + + - + @@ -114,8 +142,11 @@ const isCheckingStatus = ref(false) const searchKeyword = ref('') const showHardwareModal = ref(false) const showRemoteDesktopModal = ref(false) +const showCredentialsDialog = ref(false) const selectedDeviceId = ref(0) const selectedDevice = ref(null) +const credentialsForm = ref({ username: '', password: '' }) +const savingCredentials = ref(false) let statusCheckInterval: number | null = null @@ -223,10 +254,43 @@ const handleRemoteDesktop = (device: any) => { ElMessage.warning('设备操作系统未运行,无法连接远程桌面') return } + if (!device.windowsUsername) { + ElMessage.warning('请先配置该设备的 Windows 登录账号') + return + } selectedDevice.value = device showRemoteDesktopModal.value = true } +const handleSetCredentials = async (device: any) => { + selectedDevice.value = device + credentialsForm.value = { username: device.windowsUsername || '', password: '' } + showCredentialsDialog.value = true +} + +const saveCredentials = async () => { + if (!credentialsForm.value.username) { + ElMessage.warning('请输入用户名') + return + } + + savingCredentials.value = true + try { + await deviceApi.setDeviceCredentials(selectedDevice.value.id, credentialsForm.value) + // 更新本地数据 + const device = devices.value.find(d => d.id === selectedDevice.value.id) + if (device) { + device.windowsUsername = credentialsForm.value.username + } + showCredentialsDialog.value = false + ElMessage.success('账号配置成功') + } catch (error) { + ElMessage.error('保存失败') + } finally { + savingCredentials.value = false + } +} + const handlePowerCommand = async (command: string, device: any) => { const actionMap: Record = { 'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' }, diff --git a/adminSystem/src/views/amt/modules/remote-desktop-modal.vue b/adminSystem/src/views/amt/modules/remote-desktop-modal.vue index 848b7df..d66374b 100644 --- a/adminSystem/src/views/amt/modules/remote-desktop-modal.vue +++ b/adminSystem/src/views/amt/modules/remote-desktop-modal.vue @@ -29,8 +29,11 @@
- 使用默认 Windows 凭据快速连接,无需输入密码 + 使用设备配置的 Windows 账号快速连接 +
+ 当前账号: {{ device.windowsUsername }} +
一键连接 @@ -40,13 +43,6 @@ - -
- - - - - - - - - 连接 - - -
-
-
@@ -124,7 +104,7 @@ + + diff --git a/backend-csharp/AmtScanner.Api/Controllers/CredentialsController.cs b/backend-csharp/AmtScanner.Api/Controllers/CredentialsController.cs index 1001e3c..1822aa9 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/CredentialsController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/CredentialsController.cs @@ -16,10 +16,10 @@ public class CredentialsController : ControllerBase } [HttpGet] - public async Task>> GetAllCredentials() + public async Task>>> GetAllCredentials() { var credentials = await _credentialService.GetAllCredentialsAsync(); - return credentials.Select(c => new CredentialResponse + var result = credentials.Select(c => new CredentialResponse { Id = c.Id, Name = c.Name, @@ -28,10 +28,11 @@ public class CredentialsController : ControllerBase Description = c.Description, HasPassword = !string.IsNullOrEmpty(c.Password) }).ToList(); + return Ok(ApiResponse>.Success(result)); } [HttpPost] - public async Task> CreateCredential([FromBody] CredentialRequest request) + public async Task>> CreateCredential([FromBody] CredentialRequest request) { var credential = new AmtCredential { @@ -44,7 +45,7 @@ public class CredentialsController : ControllerBase var created = await _credentialService.CreateCredentialAsync(credential); - return CreatedAtAction(nameof(GetAllCredentials), new { id = created.Id }, new CredentialResponse + var response = new CredentialResponse { Id = created.Id, Name = created.Name, @@ -52,11 +53,12 @@ public class CredentialsController : ControllerBase IsDefault = created.IsDefault, Description = created.Description, HasPassword = true - }); + }; + return Ok(ApiResponse.Success(response, "创建成功")); } [HttpPut("{id}")] - public async Task> UpdateCredential(long id, [FromBody] CredentialRequest request) + public async Task>> UpdateCredential(long id, [FromBody] CredentialRequest request) { try { @@ -71,7 +73,7 @@ public class CredentialsController : ControllerBase var updated = await _credentialService.UpdateCredentialAsync(id, credential); - return new CredentialResponse + var response = new CredentialResponse { Id = updated.Id, Name = updated.Name, @@ -80,18 +82,19 @@ public class CredentialsController : ControllerBase Description = updated.Description, HasPassword = !string.IsNullOrEmpty(updated.Password) }; + return Ok(ApiResponse.Success(response, "更新成功")); } catch (KeyNotFoundException) { - return NotFound(); + return Ok(ApiResponse.Fail(404, "凭据不存在")); } } [HttpDelete("{id}")] - public async Task DeleteCredential(long id) + public async Task>> DeleteCredential(long id) { await _credentialService.DeleteCredentialAsync(id); - return NoContent(); + return Ok(ApiResponse.Success(null, "删除成功")); } } diff --git a/backend-csharp/AmtScanner.Api/Controllers/DevicesController.cs b/backend-csharp/AmtScanner.Api/Controllers/DevicesController.cs index 5099f2b..030b098 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/DevicesController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/DevicesController.cs @@ -29,45 +29,166 @@ public class DevicesController : ControllerBase } [HttpGet] - public async Task>> GetAllDevices() + public async Task>>> GetAllDevices() { - return await _context.AmtDevices.ToListAsync(); + var devices = await _context.AmtDevices.ToListAsync(); + return Ok(ApiResponse>.Success(devices)); } [HttpGet("{id}")] - public async Task> GetDevice(long id) + public async Task>> GetDevice(long id) { var device = await _context.AmtDevices.FindAsync(id); if (device == null) { - return NotFound(); + return Ok(ApiResponse.Fail(404, "设备不存在")); } - return device; + return Ok(ApiResponse.Success(device)); } [HttpDelete("{id}")] - public async Task DeleteDevice(long id) + public async Task>> DeleteDevice(long id) { var device = await _context.AmtDevices.FindAsync(id); if (device == null) { - return NotFound(); + return Ok(ApiResponse.Fail(404, "设备不存在")); } _context.AmtDevices.Remove(device); await _context.SaveChangesAsync(); - return NoContent(); + return Ok(ApiResponse.Success(null, "删除成功")); + } + + /// + /// 手动添加设备 + /// + [HttpPost] + public async Task>> AddDevice([FromBody] AddDeviceRequest request) + { + // 验证 IP 地址格式 + if (string.IsNullOrWhiteSpace(request.IpAddress)) + { + return Ok(ApiResponse.Fail(400, "IP 地址不能为空")); + } + + // 检查设备是否已存在 + var existingDevice = await _context.AmtDevices.FirstOrDefaultAsync(d => d.IpAddress == request.IpAddress); + if (existingDevice != null) + { + return Ok(ApiResponse.Fail(400, $"设备 {request.IpAddress} 已存在")); + } + + var device = new AmtDevice + { + IpAddress = request.IpAddress, + Hostname = request.Hostname, + Description = request.Description, + MajorVersion = 0, + MinorVersion = 0, + ProvisioningState = ProvisioningState.UNKNOWN, + AmtOnline = false, + OsOnline = false, + DiscoveredAt = DateTime.UtcNow, + LastSeenAt = DateTime.UtcNow + }; + + // 如果提供了 Windows 凭据,一并保存 + if (!string.IsNullOrEmpty(request.WindowsUsername)) + { + device.WindowsUsername = request.WindowsUsername; + if (!string.IsNullOrEmpty(request.WindowsPassword)) + { + device.WindowsPassword = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.WindowsPassword)); + } + } + + _context.AmtDevices.Add(device); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Manually added device {Ip}", request.IpAddress); + + return Ok(ApiResponse.Success(device, "设备添加成功")); + } + + /// + /// 更新设备信息 + /// + [HttpPut("{id}")] + public async Task>> UpdateDevice(long id, [FromBody] UpdateDeviceRequest request) + { + var device = await _context.AmtDevices.FindAsync(id); + + if (device == null) + { + return Ok(ApiResponse.Fail(404, "设备不存在")); + } + + if (!string.IsNullOrEmpty(request.Hostname)) + device.Hostname = request.Hostname; + if (!string.IsNullOrEmpty(request.Description)) + device.Description = request.Description; + + await _context.SaveChangesAsync(); + + return Ok(ApiResponse.Success(device, "更新成功")); + } + + /// + /// 设置设备的 Windows 登录凭据 + /// + [HttpPut("{id}/credentials")] + public async Task>> SetDeviceCredentials(long id, [FromBody] SetDeviceCredentialsRequest request) + { + var device = await _context.AmtDevices.FindAsync(id); + + if (device == null) + { + return Ok(ApiResponse.Fail(404, "设备不存在")); + } + + device.WindowsUsername = request.Username; + // 简单加密存储密码(生产环境应使用更安全的加密方式) + device.WindowsPassword = string.IsNullOrEmpty(request.Password) ? null : + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.Password)); + + await _context.SaveChangesAsync(); + + _logger.LogInformation("Updated Windows credentials for device {Id} ({Ip})", id, device.IpAddress); + + return Ok(ApiResponse.Success(null, "凭据设置成功")); + } + + /// + /// 获取设备的 Windows 凭据(仅返回用户名,不返回密码) + /// + [HttpGet("{id}/credentials")] + public async Task>> GetDeviceCredentials(long id) + { + var device = await _context.AmtDevices.FindAsync(id); + + if (device == null) + { + return Ok(ApiResponse.Fail(404, "设备不存在")); + } + + return Ok(ApiResponse.Success(new DeviceCredentialsDto + { + DeviceId = device.Id, + Username = device.WindowsUsername, + HasPassword = !string.IsNullOrEmpty(device.WindowsPassword) + })); } /// /// 检测所有设备的在线状态 /// [HttpGet("status")] - public async Task>> CheckAllDevicesStatus() + public async Task>>> CheckAllDevicesStatus() { var devices = await _context.AmtDevices.ToListAsync(); var credentials = await _context.AmtCredentials.ToListAsync(); @@ -107,20 +228,20 @@ public class DevicesController : ControllerBase // 保存更新 await _context.SaveChangesAsync(); - return statusList; + return Ok(ApiResponse>.Success(statusList)); } /// /// 检测单个设备的在线状态 /// [HttpGet("{id}/status")] - public async Task> CheckDeviceStatus(long id) + public async Task>> CheckDeviceStatus(long id) { var device = await _context.AmtDevices.FindAsync(id); if (device == null) { - return NotFound(); + return Ok(ApiResponse.Fail(404, "设备不存在")); } var credentials = await _context.AmtCredentials.ToListAsync(); @@ -142,13 +263,13 @@ public class DevicesController : ControllerBase } await _context.SaveChangesAsync(); - return new DeviceStatusDto + return Ok(ApiResponse.Success(new DeviceStatusDto { Id = device.Id, IpAddress = device.IpAddress, AmtOnline = amtOnline, OsOnline = osOnline - }; + })); } /// @@ -246,3 +367,43 @@ public class DeviceStatusDto public bool AmtOnline { get; set; } public bool OsOnline { get; set; } } + +/// +/// 更新设备请求 +/// +public class UpdateDeviceRequest +{ + public string? Hostname { get; set; } + public string? Description { get; set; } +} + +/// +/// 设置设备 Windows 凭据请求 +/// +public class SetDeviceCredentialsRequest +{ + public string? Username { get; set; } + public string? Password { get; set; } +} + +/// +/// 设备凭据DTO +/// +public class DeviceCredentialsDto +{ + public long DeviceId { get; set; } + public string? Username { get; set; } + public bool HasPassword { get; set; } +} + +/// +/// 添加设备请求 +/// +public class AddDeviceRequest +{ + public string IpAddress { get; set; } = string.Empty; + public string? Hostname { get; set; } + public string? Description { get; set; } + public string? WindowsUsername { get; set; } + public string? WindowsPassword { get; set; } +} diff --git a/backend-csharp/AmtScanner.Api/Controllers/HardwareInfoController.cs b/backend-csharp/AmtScanner.Api/Controllers/HardwareInfoController.cs index f77a133..2c1f986 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/HardwareInfoController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/HardwareInfoController.cs @@ -20,24 +20,24 @@ public class HardwareInfoController : ControllerBase } [HttpGet("{deviceId}")] - public async Task> GetHardwareInfo( + public async Task>> GetHardwareInfo( long deviceId, [FromQuery] bool refresh = false) { try { var result = await _hardwareInfoService.GetHardwareInfoAsync(deviceId, refresh); - return Ok(result); + return Ok(ApiResponse.Success(result)); } catch (Exception ex) { _logger.LogError(ex, "Error getting hardware info for device {DeviceId}", deviceId); - return StatusCode(500, new { error = ex.Message }); + return Ok(ApiResponse.Fail(500, ex.Message)); } } [HttpPost("batch")] - public async Task>> GetBatchHardwareInfo( + public async Task>> GetBatchHardwareInfo( [FromBody] BatchHardwareInfoRequest request) { try @@ -45,12 +45,17 @@ public class HardwareInfoController : ControllerBase var results = await _hardwareInfoService.GetBatchHardwareInfoAsync( request.DeviceIds, request.Refresh); - return Ok(new { results }); + return Ok(ApiResponse.Success(new BatchHardwareInfoResponse { Results = results })); } catch (Exception ex) { _logger.LogError(ex, "Error getting batch hardware info"); - return StatusCode(500, new { error = ex.Message }); + return Ok(ApiResponse.Fail(500, ex.Message)); } } } + +public class BatchHardwareInfoResponse +{ + public List Results { get; set; } = new(); +} diff --git a/backend-csharp/AmtScanner.Api/Controllers/PowerController.cs b/backend-csharp/AmtScanner.Api/Controllers/PowerController.cs index 7a06fec..a98e906 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/PowerController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/PowerController.cs @@ -1,4 +1,5 @@ using AmtScanner.Api.Data; +using AmtScanner.Api.Models; using AmtScanner.Api.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -30,18 +31,18 @@ public class PowerController : ControllerBase /// 获取设备电源状态 /// [HttpGet("{deviceId}/state")] - public async Task> GetPowerState(long deviceId) + public async Task>> GetPowerState(long deviceId) { var device = await _context.AmtDevices.FindAsync(deviceId); if (device == null) { - return NotFound(new { error = "设备不存在" }); + return Ok(ApiResponse.Fail(404, "设备不存在")); } var credential = await _credentialService.GetDefaultCredentialAsync(); if (credential == null) { - return BadRequest(new { error = "没有配置默认凭据" }); + return Ok(ApiResponse.Fail(400, "没有配置默认凭据")); } var openPorts = new List(); @@ -53,13 +54,13 @@ public class PowerController : ControllerBase if (openPorts.Count == 0) { - return Ok(new PowerStateResponse + return Ok(ApiResponse.Success(new PowerStateResponse { DeviceId = deviceId, IpAddress = device.IpAddress, Success = false, Error = "设备离线或AMT端口不可用" - }); + })); } // 解密密码 @@ -71,7 +72,7 @@ public class PowerController : ControllerBase decryptedPassword, openPorts); - return Ok(new PowerStateResponse + return Ok(ApiResponse.Success(new PowerStateResponse { DeviceId = deviceId, IpAddress = device.IpAddress, @@ -79,14 +80,14 @@ public class PowerController : ControllerBase PowerState = result.PowerState, PowerStateText = result.PowerStateText, Error = result.Error - }); + })); } /// /// 开机 /// [HttpPost("{deviceId}/power-on")] - public async Task> PowerOn(long deviceId) + public async Task>> PowerOn(long deviceId) { return await ExecutePowerAction(deviceId, PowerAction.PowerOn); } @@ -95,7 +96,7 @@ public class PowerController : ControllerBase /// 关机(优雅关机) /// [HttpPost("{deviceId}/power-off")] - public async Task> PowerOff(long deviceId) + public async Task>> PowerOff(long deviceId) { return await ExecutePowerAction(deviceId, PowerAction.GracefulOff); } @@ -104,7 +105,7 @@ public class PowerController : ControllerBase /// 强制关机 /// [HttpPost("{deviceId}/force-off")] - public async Task> ForceOff(long deviceId) + public async Task>> ForceOff(long deviceId) { return await ExecutePowerAction(deviceId, PowerAction.PowerOff); } @@ -113,7 +114,7 @@ public class PowerController : ControllerBase /// 重启(优雅重启) /// [HttpPost("{deviceId}/restart")] - public async Task> Restart(long deviceId) + public async Task>> Restart(long deviceId) { return await ExecutePowerAction(deviceId, PowerAction.GracefulReset); } @@ -122,7 +123,7 @@ public class PowerController : ControllerBase /// 强制重启 /// [HttpPost("{deviceId}/force-restart")] - public async Task> ForceRestart(long deviceId) + public async Task>> ForceRestart(long deviceId) { return await ExecutePowerAction(deviceId, PowerAction.Reset); } @@ -131,23 +132,23 @@ public class PowerController : ControllerBase /// 电源循环(硬重启) /// [HttpPost("{deviceId}/power-cycle")] - public async Task> PowerCycle(long deviceId) + public async Task>> PowerCycle(long deviceId) { return await ExecutePowerAction(deviceId, PowerAction.PowerCycle); } - private async Task> ExecutePowerAction(long deviceId, PowerAction action) + private async Task>> ExecutePowerAction(long deviceId, PowerAction action) { var device = await _context.AmtDevices.FindAsync(deviceId); if (device == null) { - return NotFound(new { error = "设备不存在" }); + return Ok(ApiResponse.Fail(404, "设备不存在")); } var credential = await _credentialService.GetDefaultCredentialAsync(); if (credential == null) { - return BadRequest(new { error = "没有配置默认凭据" }); + return Ok(ApiResponse.Fail(400, "没有配置默认凭据")); } // 检测可用端口 @@ -155,14 +156,14 @@ public class PowerController : ControllerBase if (openPorts.Count == 0) { - return Ok(new PowerActionResponse + return Ok(ApiResponse.Success(new PowerActionResponse { DeviceId = deviceId, IpAddress = device.IpAddress, Action = action.ToString(), Success = false, Error = "AMT端口不可用" - }); + })); } // 解密密码 @@ -175,7 +176,7 @@ public class PowerController : ControllerBase openPorts, action); - return Ok(new PowerActionResponse + return Ok(ApiResponse.Success(new PowerActionResponse { DeviceId = deviceId, IpAddress = device.IpAddress, @@ -183,7 +184,7 @@ public class PowerController : ControllerBase Success = result.Success, Message = result.Message, Error = result.Error - }); + })); } private async Task> DetectOpenPortsAsync(string ipAddress) diff --git a/backend-csharp/AmtScanner.Api/Controllers/RemoteDesktopController.cs b/backend-csharp/AmtScanner.Api/Controllers/RemoteDesktopController.cs index f22c082..5c6e6de 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/RemoteDesktopController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/RemoteDesktopController.cs @@ -1,4 +1,4 @@ -using AmtScanner.Api.Data; +using AmtScanner.Api.Data; using AmtScanner.Api.Models; using AmtScanner.Api.Services; using Microsoft.AspNetCore.Mvc; @@ -8,7 +8,7 @@ using System.Security.Cryptography; namespace AmtScanner.Api.Controllers; [ApiController] -[Route("api/[controller]")] +[Route("api/remote-desktop")] public class RemoteDesktopController : ControllerBase { private readonly IGuacamoleService _guacamoleService; @@ -25,6 +25,15 @@ public class RemoteDesktopController : ControllerBase _logger = logger; } + /// + /// 健康检查 + /// + [HttpGet("health")] + public IActionResult Health() + { + return Ok(ApiResponse.Success(new { status = "ok" }, "远程桌面服务正常")); + } + /// /// 生成远程访问 Token(管理员使用) /// @@ -37,18 +46,9 @@ public class RemoteDesktopController : ControllerBase if (device == null) return Ok(ApiResponse.Fail(404, "设备不存在")); - WindowsCredential? credential = null; - if (request.CredentialId.HasValue) - { - credential = await _context.WindowsCredentials.FindAsync(request.CredentialId.Value); - } - else - { - credential = await _context.WindowsCredentials.FirstOrDefaultAsync(c => c.IsDefault); - } - - if (credential == null) - return Ok(ApiResponse.Fail(400, "请先配置 Windows 凭据")); + // 检查设备是否配置了 Windows 凭据 + if (string.IsNullOrEmpty(device.WindowsUsername) || string.IsNullOrEmpty(device.WindowsPassword)) + return Ok(ApiResponse.Fail(400, "请先为该设备配置 Windows 登录凭据")); var token = GenerateRandomToken(); var expiresAt = DateTime.UtcNow.AddMinutes(request.ExpiresInMinutes ?? 30); @@ -57,7 +57,6 @@ public class RemoteDesktopController : ControllerBase { Token = token, DeviceId = deviceId, - WindowsCredentialId = credential.Id, ExpiresAt = expiresAt, MaxUseCount = request.MaxUseCount ?? 1, Note = request.Note @@ -67,7 +66,7 @@ public class RemoteDesktopController : ControllerBase await _context.SaveChangesAsync(); var baseUrl = $"{Request.Scheme}://{Request.Host}"; - var accessUrl = $"{baseUrl}/remote/{token}"; + var accessUrl = $"{baseUrl}/#/remote/{token}"; _logger.LogInformation("Generated remote access token for device {Ip}, expires at {ExpiresAt}", device.IpAddress, expiresAt); @@ -91,7 +90,6 @@ public class RemoteDesktopController : ControllerBase { var accessToken = await _context.RemoteAccessTokens .Include(t => t.Device) - .Include(t => t.WindowsCredential) .FirstOrDefaultAsync(t => t.Token == token); if (accessToken == null) @@ -100,8 +98,13 @@ public class RemoteDesktopController : ControllerBase if (!accessToken.IsValid()) return Ok(ApiResponse.Fail(400, "访问链接已过期或已达到使用次数上限")); - if (accessToken.Device == null || accessToken.WindowsCredential == null) - return Ok(ApiResponse.Fail(400, "设备或凭据信息不完整")); + if (accessToken.Device == null) + return Ok(ApiResponse.Fail(400, "设备信息不完整")); + + if (string.IsNullOrEmpty(accessToken.Device.WindowsUsername) || string.IsNullOrEmpty(accessToken.Device.WindowsPassword)) + return Ok(ApiResponse.Fail(400, "设备未配置 Windows 登录凭据")); + + var password = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(accessToken.Device.WindowsPassword)); accessToken.UseCount++; accessToken.UsedAt = DateTime.UtcNow; @@ -114,7 +117,7 @@ public class RemoteDesktopController : ControllerBase var connectionName = $"AMT-{accessToken.Device.IpAddress}"; var connectionId = await _guacamoleService.CreateOrGetConnectionAsync( guacToken, connectionName, accessToken.Device.IpAddress, - accessToken.WindowsCredential.Username, accessToken.WindowsCredential.Password); + accessToken.Device.WindowsUsername!, password); if (string.IsNullOrEmpty(connectionId)) return Ok(ApiResponse.Fail(500, "创建远程连接失败")); @@ -131,6 +134,7 @@ public class RemoteDesktopController : ControllerBase })); } + /// /// 验证 Token 是否有效 /// @@ -204,9 +208,10 @@ public class RemoteDesktopController : ControllerBase var count = await _context.RemoteAccessTokens .Where(t => t.ExpiresAt < DateTime.UtcNow) .ExecuteDeleteAsync(); - return Ok(ApiResponse.Success(new CleanupTokensResponse { DeletedCount = count }, "已清理 " + count + " 个过期 Token")); + return Ok(ApiResponse.Success(new CleanupTokensResponse { DeletedCount = count }, $"已清理 {count} 个过期 Token")); } + /// /// 直接连接(需要输入凭据) /// @@ -264,7 +269,6 @@ public class RemoteDesktopController : ControllerBase public class GenerateTokenRequest { - public long? CredentialId { get; set; } public int? ExpiresInMinutes { get; set; } = 30; public int? MaxUseCount { get; set; } = 1; public string? Note { get; set; } diff --git a/backend-csharp/AmtScanner.Api/Controllers/ScanController.cs b/backend-csharp/AmtScanner.Api/Controllers/ScanController.cs index 9f75570..04be814 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/ScanController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/ScanController.cs @@ -1,6 +1,8 @@ +using AmtScanner.Api.Models; using AmtScanner.Api.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using System.Collections.Concurrent; namespace AmtScanner.Api.Controllers; @@ -12,6 +14,9 @@ public class ScanController : ControllerBase private readonly IHubContext _hubContext; private readonly ILogger _logger; + // 存储扫描进度状态 + private static readonly ConcurrentDictionary _scanStatuses = new(); + public ScanController( IAmtScannerService scannerService, IHubContext hubContext, @@ -23,17 +28,36 @@ public class ScanController : ControllerBase } [HttpPost("start")] - public async Task StartScan([FromBody] ScanRequest request) + public async Task>> StartScan([FromBody] ScanRequest request) { var taskId = Guid.NewGuid().ToString(); _logger.LogInformation("Starting scan task: {TaskId}", taskId); + // 初始化扫描状态 + _scanStatuses[taskId] = new ScanStatusInfo + { + TaskId = taskId, + Status = "running", + ScannedCount = 0, + TotalCount = 0, + FoundDevices = 0 + }; + // Start scan in background _ = Task.Run(async () => { var progress = new Progress(async p => { + // 更新状态存储 + if (_scanStatuses.TryGetValue(taskId, out var status)) + { + status.ScannedCount = p.ScannedCount; + status.TotalCount = p.TotalCount; + status.FoundDevices = p.FoundDevices; + status.CurrentIp = p.CurrentIp; + } + await _hubContext.Clients.All.SendAsync("ReceiveScanProgress", p); }); @@ -46,28 +70,75 @@ public class ScanController : ControllerBase progress ); + // 更新状态为完成 + if (_scanStatuses.TryGetValue(taskId, out var status)) + { + status.Status = "completed"; + } + // Send completion notification _logger.LogInformation("Scan task {TaskId} completed", taskId); await _hubContext.Clients.All.SendAsync("ScanCompleted", new { taskId }); } catch (Exception ex) { + // 更新状态为错误 + if (_scanStatuses.TryGetValue(taskId, out var status)) + { + status.Status = "error"; + status.Error = ex.Message; + } + _logger.LogError(ex, "Error in scan task {TaskId}", taskId); await _hubContext.Clients.All.SendAsync("ScanError", new { taskId, error = ex.Message }); } }); - return Ok(new { taskId }); + return Ok(ApiResponse.Success(new ScanStartResponse { TaskId = taskId }, "扫描任务已启动")); + } + + [HttpGet("status/{taskId}")] + public ActionResult> GetScanStatus(string taskId) + { + if (_scanStatuses.TryGetValue(taskId, out var status)) + { + return Ok(ApiResponse.Success(status)); + } + + return Ok(ApiResponse.Fail(404, "扫描任务不存在")); } [HttpPost("cancel/{taskId}")] - public IActionResult CancelScan(string taskId) + public ActionResult> CancelScan(string taskId) { _scannerService.CancelScan(taskId); - return Ok(); + + // 更新状态为已取消 + if (_scanStatuses.TryGetValue(taskId, out var status)) + { + status.Status = "cancelled"; + } + + return Ok(ApiResponse.Success(null, "扫描任务已取消")); } } +public class ScanStartResponse +{ + public string TaskId { get; set; } = string.Empty; +} + +public class ScanStatusInfo +{ + public string TaskId { get; set; } = string.Empty; + public string Status { get; set; } = "idle"; // idle, running, completed, cancelled, error + public int ScannedCount { get; set; } + public int TotalCount { get; set; } + public int FoundDevices { get; set; } + public string? CurrentIp { get; set; } + public string? Error { get; set; } +} + public class ScanRequest { public string NetworkSegment { get; set; } = string.Empty; diff --git a/backend-csharp/AmtScanner.Api/Controllers/WindowsCredentialsController.cs b/backend-csharp/AmtScanner.Api/Controllers/WindowsCredentialsController.cs index e60d265..98fbf0e 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/WindowsCredentialsController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/WindowsCredentialsController.cs @@ -22,7 +22,7 @@ public class WindowsCredentialsController : ControllerBase /// 获取所有 Windows 凭据 /// [HttpGet] - public async Task>> GetAll() + public async Task>>> GetAll() { var credentials = await _context.WindowsCredentials .OrderByDescending(c => c.IsDefault) @@ -39,14 +39,14 @@ public class WindowsCredentialsController : ControllerBase }) .ToListAsync(); - return Ok(credentials); + return Ok(ApiResponse>.Success(credentials)); } /// /// 创建 Windows 凭据 /// [HttpPost] - public async Task> Create([FromBody] CreateWindowsCredentialRequest request) + public async Task>> Create([FromBody] CreateWindowsCredentialRequest request) { // 如果设为默认,取消其他默认 if (request.IsDefault) @@ -71,7 +71,7 @@ public class WindowsCredentialsController : ControllerBase _logger.LogInformation("Created Windows credential: {Name}", credential.Name); - return Ok(new WindowsCredentialDto + var dto = new WindowsCredentialDto { Id = credential.Id, Name = credential.Name, @@ -80,19 +80,21 @@ public class WindowsCredentialsController : ControllerBase IsDefault = credential.IsDefault, Note = credential.Note, CreatedAt = credential.CreatedAt - }); + }; + + return Ok(ApiResponse.Success(dto, "创建成功")); } /// /// 更新 Windows 凭据 /// [HttpPut("{id}")] - public async Task Update(long id, [FromBody] UpdateWindowsCredentialRequest request) + public async Task>> Update(long id, [FromBody] UpdateWindowsCredentialRequest request) { var credential = await _context.WindowsCredentials.FindAsync(id); if (credential == null) { - return NotFound(new { error = "凭据不存在" }); + return Ok(ApiResponse.Fail(404, "凭据不存在")); } // 如果设为默认,取消其他默认 @@ -116,19 +118,19 @@ public class WindowsCredentialsController : ControllerBase await _context.SaveChangesAsync(); - return Ok(new { success = true }); + return Ok(ApiResponse.Success(null, "更新成功")); } /// /// 删除 Windows 凭据 /// [HttpDelete("{id}")] - public async Task Delete(long id) + public async Task>> Delete(long id) { var credential = await _context.WindowsCredentials.FindAsync(id); if (credential == null) { - return NotFound(new { error = "凭据不存在" }); + return Ok(ApiResponse.Fail(404, "凭据不存在")); } _context.WindowsCredentials.Remove(credential); @@ -136,19 +138,19 @@ public class WindowsCredentialsController : ControllerBase _logger.LogInformation("Deleted Windows credential: {Name}", credential.Name); - return Ok(new { success = true }); + return Ok(ApiResponse.Success(null, "删除成功")); } /// /// 设置默认凭据 /// [HttpPost("{id}/set-default")] - public async Task SetDefault(long id) + public async Task>> SetDefault(long id) { var credential = await _context.WindowsCredentials.FindAsync(id); if (credential == null) { - return NotFound(new { error = "凭据不存在" }); + return Ok(ApiResponse.Fail(404, "凭据不存在")); } // 取消其他默认 @@ -159,7 +161,7 @@ public class WindowsCredentialsController : ControllerBase credential.IsDefault = true; await _context.SaveChangesAsync(); - return Ok(new { success = true }); + return Ok(ApiResponse.Success(null, "设置成功")); } } diff --git a/backend-csharp/AmtScanner.Api/Data/AppDbContext.cs b/backend-csharp/AmtScanner.Api/Data/AppDbContext.cs index 99e40a9..e2a89c2 100644 --- a/backend-csharp/AmtScanner.Api/Data/AppDbContext.cs +++ b/backend-csharp/AmtScanner.Api/Data/AppDbContext.cs @@ -95,12 +95,6 @@ public class AppDbContext : DbContext .HasForeignKey(t => t.DeviceId) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(t => t.WindowsCredential) - .WithMany() - .HasForeignKey(t => t.WindowsCredentialId) - .OnDelete(DeleteBehavior.SetNull); - // User 配置 modelBuilder.Entity() .Property(u => u.UserName) diff --git a/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs b/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs index 17acb71..92e6d28 100644 --- a/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs +++ b/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs @@ -101,18 +101,21 @@ public static class DbSeeder var menus = new List { - // 仪表盘菜单 - 与前端 dashboard.ts 匹配(系统内置) + // 仪表盘菜单(系统内置) new() { Id = 1, Name = "Dashboard", Path = "/dashboard", Component = "/index/index", Title = "menus.dashboard.title", Icon = "ri:pie-chart-line", Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true }, new() { Id = 2, ParentId = 1, Name = "Console", Path = "console", Component = "/dashboard/console", Title = "menus.dashboard.console", KeepAlive = false, Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true }, // AMT 设备管理菜单(系统内置) - new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "设备管理", Icon = "ri:computer-line", Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, - new() { Id = 6, ParentId = 5, Name = "AmtScan", Path = "scan", Component = "/amt/scan", Title = "网络扫描", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, - new() { Id = 7, ParentId = 5, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备列表", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "AMT设备管理", Icon = "ri:computer-line", Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + new() { Id = 6, ParentId = 5, Name = "AmtScan", Path = "scan", Component = "/amt/scan", Title = "设备扫描", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + new() { Id = 7, ParentId = 5, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备管理", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 8, ParentId = 5, Name = "AmtCredentials", Path = "credentials", Component = "/amt/credentials", Title = "AMT凭据", KeepAlive = true, Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, - new() { Id = 9, ParentId = 5, Name = "WindowsCredentials", Path = "windows-credentials", Component = "/amt/windows-credentials", Title = "Windows凭据", KeepAlive = true, Sort = 4, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, - // 系统管理菜单 - 与前端 system.ts 匹配(系统内置) + // 桌面管理菜单(系统内置) + new() { Id = 20, Name = "DesktopManage", Path = "/desktop-manage", Component = "/index/index", Title = "桌面管理", Icon = "ri:remote-control-line", Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + new() { Id = 21, ParentId = 20, Name = "DesktopDevices", Path = "devices", Component = "/desktop-manage/devices", Title = "远程桌面", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + + // 系统管理菜单(系统内置) new() { Id = 10, Name = "System", Path = "/system", Component = "/index/index", Title = "menus.system.title", Icon = "ri:user-3-line", Sort = 99, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 11, ParentId = 10, Name = "User", Path = "user", Component = "/system/user", Title = "menus.system.user", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 12, ParentId = 10, Name = "Role", Path = "role", Component = "/system/role", Title = "menus.system.role", KeepAlive = true, Sort = 2, Roles = "R_SUPER", IsSystem = true }, diff --git a/backend-csharp/AmtScanner.Api/Migrations/20260120122638_AddDeviceWindowsCredentials.Designer.cs b/backend-csharp/AmtScanner.Api/Migrations/20260120122638_AddDeviceWindowsCredentials.Designer.cs new file mode 100644 index 0000000..f9e6e03 --- /dev/null +++ b/backend-csharp/AmtScanner.Api/Migrations/20260120122638_AddDeviceWindowsCredentials.Designer.cs @@ -0,0 +1,656 @@ +// +using System; +using AmtScanner.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AmtScanner.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260120122638_AddDeviceWindowsCredentials")] + partial class AddDeviceWindowsCredentials + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("AmtScanner.Api.Models.AmtCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("AmtCredentials"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.AmtDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AmtOnline") + .HasColumnType("tinyint(1)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DiscoveredAt") + .HasColumnType("datetime(6)"); + + b.Property("Hostname") + .HasColumnType("longtext"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastSeenAt") + .HasColumnType("datetime(6)"); + + b.Property("MajorVersion") + .HasColumnType("int"); + + b.Property("MinorVersion") + .HasColumnType("int"); + + b.Property("OsOnline") + .HasColumnType("tinyint(1)"); + + b.Property("ProvisioningState") + .HasColumnType("int"); + + b.Property("WindowsPassword") + .HasColumnType("longtext"); + + b.Property("WindowsUsername") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("IpAddress") + .IsUnique(); + + b.ToTable("AmtDevices"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetime(6)"); + + b.Property("ProcessorCores") + .HasColumnType("int"); + + b.Property("ProcessorCurrentClockSpeed") + .HasColumnType("int"); + + b.Property("ProcessorMaxClockSpeed") + .HasColumnType("int"); + + b.Property("ProcessorModel") + .HasColumnType("longtext"); + + b.Property("ProcessorThreads") + .HasColumnType("int"); + + b.Property("SystemManufacturer") + .HasColumnType("longtext"); + + b.Property("SystemModel") + .HasColumnType("longtext"); + + b.Property("SystemSerialNumber") + .HasColumnType("longtext"); + + b.Property("TotalMemoryBytes") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("LastUpdated"); + + b.ToTable("HardwareInfos"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.MemoryModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CapacityBytes") + .HasColumnType("bigint"); + + b.Property("HardwareInfoId") + .HasColumnType("bigint"); + + b.Property("Manufacturer") + .HasColumnType("longtext"); + + b.Property("MemoryType") + .HasColumnType("longtext"); + + b.Property("PartNumber") + .HasColumnType("longtext"); + + b.Property("SerialNumber") + .HasColumnType("longtext"); + + b.Property("SlotLocation") + .HasColumnType("longtext"); + + b.Property("SpeedMHz") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("HardwareInfoId"); + + b.ToTable("MemoryModules"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Menu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AuthList") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Component") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Icon") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("IsHide") + .HasColumnType("tinyint(1)"); + + b.Property("IsHideTab") + .HasColumnType("tinyint(1)"); + + b.Property("IsIframe") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("KeepAlive") + .HasColumnType("tinyint(1)"); + + b.Property("Link") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ParentId") + .HasColumnType("int"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Roles") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Sort") + .HasColumnType("int"); + + b.Property("Title") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("ParentId"); + + b.ToTable("Menus"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("MaxUseCount") + .HasColumnType("int"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("UseCount") + .HasColumnType("int"); + + b.Property("UsedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Token") + .IsUnique(); + + b.ToTable("RemoteAccessTokens"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("RoleCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("RoleCode") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b => + { + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("MenuId") + .HasColumnType("int"); + + b.HasKey("RoleId", "MenuId"); + + b.HasIndex("MenuId"); + + b.ToTable("RoleMenus"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CapacityBytes") + .HasColumnType("bigint"); + + b.Property("DeviceId") + .HasColumnType("longtext"); + + b.Property("HardwareInfoId") + .HasColumnType("bigint"); + + b.Property("InterfaceType") + .HasColumnType("longtext"); + + b.Property("Model") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("HardwareInfoId"); + + b.ToTable("StorageDevices"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Avatar") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("varchar(1)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("NickName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("RefreshToken") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RefreshTokenExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("varchar(1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.WindowsCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Domain") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("WindowsCredentials"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b => + { + b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.MemoryModule", b => + { + b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo") + .WithMany("MemoryModules") + .HasForeignKey("HardwareInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HardwareInfo"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Menu", b => + { + b.HasOne("AmtScanner.Api.Models.Menu", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b => + { + b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b => + { + b.HasOne("AmtScanner.Api.Models.Menu", "Menu") + .WithMany("RoleMenus") + .HasForeignKey("MenuId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AmtScanner.Api.Models.Role", "Role") + .WithMany("RoleMenus") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Menu"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b => + { + b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo") + .WithMany("StorageDevices") + .HasForeignKey("HardwareInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HardwareInfo"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b => + { + b.HasOne("AmtScanner.Api.Models.Role", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AmtScanner.Api.Models.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b => + { + b.Navigation("MemoryModules"); + + b.Navigation("StorageDevices"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Menu", b => + { + b.Navigation("Children"); + + b.Navigation("RoleMenus"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Role", b => + { + b.Navigation("RoleMenus"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.User", b => + { + b.Navigation("UserRoles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend-csharp/AmtScanner.Api/Migrations/20260120122638_AddDeviceWindowsCredentials.cs b/backend-csharp/AmtScanner.Api/Migrations/20260120122638_AddDeviceWindowsCredentials.cs new file mode 100644 index 0000000..7f4ac90 --- /dev/null +++ b/backend-csharp/AmtScanner.Api/Migrations/20260120122638_AddDeviceWindowsCredentials.cs @@ -0,0 +1,99 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AmtScanner.Api.Migrations +{ + /// + public partial class AddDeviceWindowsCredentials : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // 使用原生 SQL 安全删除外键和索引(如果存在) + migrationBuilder.Sql(@" + SET @fk_exists = (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = DATABASE() + AND TABLE_NAME = 'RemoteAccessTokens' + AND CONSTRAINT_NAME = 'FK_RemoteAccessTokens_WindowsCredentials_WindowsCredentialId'); + SET @sql = IF(@fk_exists > 0, + 'ALTER TABLE RemoteAccessTokens DROP FOREIGN KEY FK_RemoteAccessTokens_WindowsCredentials_WindowsCredentialId', + 'SELECT 1'); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + "); + + migrationBuilder.Sql(@" + SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'RemoteAccessTokens' + AND INDEX_NAME = 'IX_RemoteAccessTokens_WindowsCredentialId'); + SET @sql = IF(@idx_exists > 0, + 'ALTER TABLE RemoteAccessTokens DROP INDEX IX_RemoteAccessTokens_WindowsCredentialId', + 'SELECT 1'); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + "); + + migrationBuilder.Sql(@" + SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'RemoteAccessTokens' + AND COLUMN_NAME = 'WindowsCredentialId'); + SET @sql = IF(@col_exists > 0, + 'ALTER TABLE RemoteAccessTokens DROP COLUMN WindowsCredentialId', + 'SELECT 1'); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + "); + + migrationBuilder.AddColumn( + name: "WindowsPassword", + table: "AmtDevices", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "WindowsUsername", + table: "AmtDevices", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "WindowsPassword", + table: "AmtDevices"); + + migrationBuilder.DropColumn( + name: "WindowsUsername", + table: "AmtDevices"); + + migrationBuilder.AddColumn( + name: "WindowsCredentialId", + table: "RemoteAccessTokens", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_RemoteAccessTokens_WindowsCredentialId", + table: "RemoteAccessTokens", + column: "WindowsCredentialId"); + + migrationBuilder.AddForeignKey( + name: "FK_RemoteAccessTokens_WindowsCredentials_WindowsCredentialId", + table: "RemoteAccessTokens", + column: "WindowsCredentialId", + principalTable: "WindowsCredentials", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/backend-csharp/AmtScanner.Api/Migrations/AppDbContextModelSnapshot.cs b/backend-csharp/AmtScanner.Api/Migrations/AppDbContextModelSnapshot.cs index c787c9c..7edf43c 100644 --- a/backend-csharp/AmtScanner.Api/Migrations/AppDbContextModelSnapshot.cs +++ b/backend-csharp/AmtScanner.Api/Migrations/AppDbContextModelSnapshot.cs @@ -95,6 +95,12 @@ namespace AmtScanner.Api.Migrations b.Property("ProvisioningState") .HasColumnType("int"); + b.Property("WindowsPassword") + .HasColumnType("longtext"); + + b.Property("WindowsUsername") + .HasColumnType("longtext"); + b.HasKey("Id"); b.HasIndex("IpAddress") @@ -303,9 +309,6 @@ namespace AmtScanner.Api.Migrations b.Property("UsedAt") .HasColumnType("datetime(6)"); - b.Property("WindowsCredentialId") - .HasColumnType("bigint"); - b.HasKey("Id"); b.HasIndex("DeviceId"); @@ -313,8 +316,6 @@ namespace AmtScanner.Api.Migrations b.HasIndex("Token") .IsUnique(); - b.HasIndex("WindowsCredentialId"); - b.ToTable("RemoteAccessTokens"); }); @@ -569,14 +570,7 @@ namespace AmtScanner.Api.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("AmtScanner.Api.Models.WindowsCredential", "WindowsCredential") - .WithMany() - .HasForeignKey("WindowsCredentialId") - .OnDelete(DeleteBehavior.SetNull); - b.Navigation("Device"); - - b.Navigation("WindowsCredential"); }); modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b => diff --git a/backend-csharp/AmtScanner.Api/Models/AmtDevice.cs b/backend-csharp/AmtScanner.Api/Models/AmtDevice.cs index 3450ff1..e782ef4 100644 --- a/backend-csharp/AmtScanner.Api/Models/AmtDevice.cs +++ b/backend-csharp/AmtScanner.Api/Models/AmtDevice.cs @@ -30,6 +30,16 @@ public class AmtDevice /// public bool OsOnline { get; set; } + /// + /// Windows 登录用户名 + /// + public string? WindowsUsername { get; set; } + + /// + /// Windows 登录密码(加密存储) + /// + public string? WindowsPassword { get; set; } + public DateTime DiscoveredAt { get; set; } public DateTime LastSeenAt { get; set; } diff --git a/backend-csharp/AmtScanner.Api/Models/RemoteAccessToken.cs b/backend-csharp/AmtScanner.Api/Models/RemoteAccessToken.cs index 7a7b1e9..147ed15 100644 --- a/backend-csharp/AmtScanner.Api/Models/RemoteAccessToken.cs +++ b/backend-csharp/AmtScanner.Api/Models/RemoteAccessToken.cs @@ -27,16 +27,6 @@ public class RemoteAccessToken /// public AmtDevice? Device { get; set; } - /// - /// 关联的 Windows 凭据 ID - /// - public long? WindowsCredentialId { get; set; } - - /// - /// 关联的 Windows 凭据 - /// - public WindowsCredential? WindowsCredential { get; set; } - /// /// 创建时间 /// diff --git a/backend-csharp/AmtScanner.Api/add_desktop_manage_menu.sql b/backend-csharp/AmtScanner.Api/add_desktop_manage_menu.sql new file mode 100644 index 0000000..bf06029 --- /dev/null +++ b/backend-csharp/AmtScanner.Api/add_desktop_manage_menu.sql @@ -0,0 +1,25 @@ +-- 添加桌面管理菜单 +-- 数据库: amtscanner +-- 已执行: 2026-01-20 + +-- 添加一级菜单:桌面管理 +INSERT INTO `Menus` (`Id`, `ParentId`, `Name`, `Path`, `Component`, `Title`, `Icon`, `Sort`, `Roles`, `IsSystem`, `IsHide`, `KeepAlive`, `IsIframe`, `IsHideTab`, `CreatedAt`) +VALUES (20, NULL, 'DesktopManage', '/desktop-manage', '/index/index', '桌面管理', 'ri:remote-control-line', 3, 'R_SUPER,R_ADMIN', 1, 0, 0, 0, 0, NOW()); + +-- 添加二级菜单:远程桌面 +INSERT INTO `Menus` (`Id`, `ParentId`, `Name`, `Path`, `Component`, `Title`, `Icon`, `Sort`, `Roles`, `IsSystem`, `IsHide`, `KeepAlive`, `IsIframe`, `IsHideTab`, `CreatedAt`) +VALUES (21, 20, 'DesktopDevices', 'devices', '/desktop-manage/devices', '远程桌面', NULL, 1, 'R_SUPER,R_ADMIN', 1, 0, 1, 0, 0, NOW()); + +-- 为超级管理员角色分配新菜单权限 +INSERT INTO `RoleMenus` (`RoleId`, `MenuId`) +SELECT r.Id, 20 FROM `Roles` r WHERE r.RoleCode = 'R_SUPER'; + +INSERT INTO `RoleMenus` (`RoleId`, `MenuId`) +SELECT r.Id, 21 FROM `Roles` r WHERE r.RoleCode = 'R_SUPER'; + +-- 为管理员角色分配新菜单权限 +INSERT INTO `RoleMenus` (`RoleId`, `MenuId`) +SELECT r.Id, 20 FROM `Roles` r WHERE r.RoleCode = 'R_ADMIN'; + +INSERT INTO `RoleMenus` (`RoleId`, `MenuId`) +SELECT r.Id, 21 FROM `Roles` r WHERE r.RoleCode = 'R_ADMIN';