Files
TFSImport/TfsExcelImporter/Config.cs
T
chenshoulin 8824a81758 feat: 实现密码加密存储与异步数据预览功能
- 在 Config.cs 中添加 DPAPI 加密支持,确保密码安全存储,兼容旧版配置。
- 更新 MainForm.cs,重构数据预览逻辑为异步方法,提升用户体验,避免界面冻结。
- 修改相关方法以支持异步操作,确保在处理大文件时界面响应流畅。
- 更新项目文件以引用 System.Security,支持加密功能。
2026-11-01 21:57:53 +08:00

168 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
namespace TfsExcelImporter
{
/// <summary>
/// 程序配置。保存于 exe 同目录 config.json,界面修改后自动写回。
/// </summary>
public class AppConfig
{
public string TfsUrl { get; set; } = "http://pdtfs01.dev.goldenstand.cn:8080/tfs/DefaultCollection";
public string Project { get; set; } = "Tools_Dev";
public string WorkItemType { get; set; } = "Task";
// 目标迭代:完整迭代路径,如 Tools_Dev\2026年Sprint\2026年Sprint10
public string IterationPath { get; set; } = "";
// 可选:区域路径
public string AreaPath { get; set; } = "";
// 认证固定 NTLM:用户名可写 user,也可写 DOMAIN\user
public string Username { get; set; } = "";
// 明文密码只存在于内存(UI 回显需要);落盘一律走 DPAPI 加密的 PasswordEnc。
// [JsonIgnore] 防止明文随 SerializeObject 写进 config.json。
[JsonIgnore]
public string Password { get; set; } = "";
// 兼容旧版明文键:反序列化时承接旧 config.json 的 "Password",下次 Save 自动转为加密存储。
// getter 恒返 null,序列化不再输出明文键。
[JsonProperty("Password", NullValueHandling = NullValueHandling.Ignore)]
private string LegacyPassword { get { return null; } set { if (!string.IsNullOrEmpty(value)) Password = value; } }
// DPAPI(CurrentUser) 加密后的密码,Base64;同机同用户可解,拷到别的机器/账户需重输。
[JsonProperty("PasswordEnc", NullValueHandling = NullValueHandling.Ignore)]
private string PasswordEnc { get; set; } = "";
// ------------------------------------------------------------- DPAPI
private static string Protect(string plain)
{
var bytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(plain), null, DataProtectionScope.CurrentUser);
return Convert.ToBase64String(bytes);
}
/// <summary>解密失败返回 null(不抛异常,让调用方回退到空密码)。</summary>
private static string TryUnprotect(string cipher)
{
try
{
var bytes = ProtectedData.Unprotect(Convert.FromBase64String(cipher), null, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(bytes);
}
catch
{
return null;
}
}
// 旧版本有 BypassRules 开关,已移除:TFS 2013 对应的 Client OM 14.x 没有 SaveFlags.BypassRules
// (15.x 才加,而 15.x 不兼容 TFS 2013),勾选从未生效。旧 config.json 里的残留键由
// Newtonsoft 按默认 MissingMemberHandling.Ignore 忽略,不需要手工清理。
public bool SkipExistingTitles { get; set; } = true;
public string DefaultState { get; set; } = "To Do";
// 自定义字段引用名;留空则按 DisplayName 自动解析
public string FinancialProjectRef { get; set; } = "";
public string CompletedRef { get; set; } = "";
public string LogDateRef { get; set; } = "";
// Original Estimate 导入时取 Completed 的值;引用名留空则按显示名解析
public string OriginalEstimateRef { get; set; } = "";
public string LastExcelPath { get; set; } = "";
// 父 Product Backlog Item:所有 Task 默认挂接的 PBI Id(如 219975);Excel 每行 PBI Id 列可覆盖
public string ParentPbiId { get; set; } = "";
// 便携优先:配置写在 exe 同目录(拷走 exe 即可带走配置);
// exe 目录只读(如放在 Program Files)时回退到 %AppData%\TfsExcelImporter。
private static string PortableConfigPath
{
get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); }
}
private static string RoamingConfigPath
{
get
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"TfsExcelImporter", "config.json");
}
}
private static string ConfigPath
{
get
{
if (File.Exists(PortableConfigPath)) return PortableConfigPath;
if (File.Exists(RoamingConfigPath)) return RoamingConfigPath;
return PortableConfigPath;
}
}
/// <summary>当前实际使用的配置文件路径(界面提示用)。</summary>
public static string ConfigLocation
{
get { return ConfigPath; }
}
public static AppConfig Load()
{
try
{
if (File.Exists(ConfigPath))
{
var cfg = JsonConvert.DeserializeObject<AppConfig>(File.ReadAllText(ConfigPath));
if (cfg != null)
{
// 加密值优先于旧明文键;解密失败(换机/换账户/文件被改)留空,用户重输即可
if (!string.IsNullOrEmpty(cfg.PasswordEnc))
{
var plain = TryUnprotect(cfg.PasswordEnc);
if (plain != null) cfg.Password = plain;
}
return cfg;
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("读取 config.json 失败: " + ex.Message);
}
return new AppConfig();
}
public void Save()
{
// 落盘前把内存中的明文加密;Password 本身带 [JsonIgnore] 不会写出
PasswordEnc = string.IsNullOrEmpty(Password) ? "" : Protect(Password);
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
try
{
File.WriteAllText(ConfigPath, json);
return;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("写入 config.json 失败: " + ex.Message);
}
// exe 目录不可写(只读介质 / Program Files)时落到用户配置目录
try
{
var fallback = RoamingConfigPath;
Directory.CreateDirectory(Path.GetDirectoryName(fallback));
File.WriteAllText(fallback, json);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("写入用户配置目录失败: " + ex.Message);
}
}
}
}