Skip to content

Commit 8f95f27

Browse files
committed
feat: add API key, documentType alias, and error handling
1 parent 8fbb682 commit 8f95f27

7 files changed

Lines changed: 161 additions & 14 deletions

File tree

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace EasyInk.Engine.Models;
1+
using Newtonsoft.Json;
2+
3+
namespace EasyInk.Engine.Models;
24

35
/// <summary>
46
/// 用户数据参数
@@ -8,10 +10,25 @@ public class UserDataParams
810
/// <summary>
911
/// 用户ID
1012
/// </summary>
11-
public string UserId { get; set; } = default!;
13+
public string? UserId { get; set; }
1214

1315
/// <summary>
1416
/// 标签类型
1517
/// </summary>
16-
public string LabelType { get; set; } = default!;
18+
public string? LabelType { get; set; }
19+
20+
/// <summary>
21+
/// 文档类型。兼容前端 SDK 使用的 documentType 字段,内部仍映射到审计日志的 LabelType。
22+
/// </summary>
23+
[JsonProperty("documentType")]
24+
public string? DocumentType
25+
{
26+
get => LabelType;
27+
set => LabelType = value;
28+
}
29+
30+
public bool ShouldSerializeDocumentType()
31+
{
32+
return false;
33+
}
1734
}

lib/EasyInk.Net/EasyInk.Engine/tests/ModelTests.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using EasyInk.Engine.Models;
1+
using EasyInk.Engine;
2+
using EasyInk.Engine.Models;
3+
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
25
using Xunit;
36

47
namespace EasyInk.Engine.Tests;
@@ -96,3 +99,44 @@ public void Error_SetsErrorInfo()
9699
Assert.Equal("details", result.ErrorInfo!.Details);
97100
}
98101
}
102+
103+
public class UserDataParamsTests
104+
{
105+
[Fact]
106+
public void Deserialize_DocumentType_MapsToLabelType()
107+
{
108+
var userData = JsonConvert.DeserializeObject<UserDataParams>(
109+
@"{""userId"":""demo-user"",""documentType"":""receipt""}",
110+
JsonConfig.CamelCase);
111+
112+
Assert.Equal("demo-user", userData!.UserId);
113+
Assert.Equal("receipt", userData.LabelType);
114+
Assert.Equal("receipt", userData.DocumentType);
115+
}
116+
117+
[Fact]
118+
public void Deserialize_LabelType_RemainsSupported()
119+
{
120+
var userData = JsonConvert.DeserializeObject<UserDataParams>(
121+
@"{""userId"":""demo-user"",""labelType"":""shipping""}",
122+
JsonConfig.CamelCase);
123+
124+
Assert.Equal("shipping", userData!.LabelType);
125+
Assert.Equal("shipping", userData.DocumentType);
126+
}
127+
128+
[Fact]
129+
public void Serialize_OmitsDocumentTypeAlias()
130+
{
131+
var json = JsonConvert.SerializeObject(new UserDataParams
132+
{
133+
UserId = "demo-user",
134+
LabelType = "receipt"
135+
}, JsonConfig.CamelCase);
136+
var token = JObject.Parse(json);
137+
138+
Assert.Equal("demo-user", token["userId"]!.ToString());
139+
Assert.Equal("receipt", token["labelType"]!.ToString());
140+
Assert.Null(token["documentType"]);
141+
}
142+
}

lib/EasyInk.Net/EasyInk.Printer/src/Server/WebSocketHandler.cs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class WebSocketHandler : IDisposable
2323
private readonly SemaphoreSlim _broadcastLock = new SemaphoreSlim(1, 1);
2424
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
2525
private readonly int _maxConnections;
26+
private readonly string? _apiKey;
2627
private readonly Task _pingTask;
2728
private bool _disposed;
2829
private WebSocketCommandHandler? _commandHandler;
@@ -31,9 +32,10 @@ public class WebSocketHandler : IDisposable
3132

3233
public event Action? ConnectionCountChanged;
3334

34-
public WebSocketHandler(int maxConnections = 100)
35+
public WebSocketHandler(int maxConnections = 100, string? apiKey = null)
3536
{
3637
_maxConnections = maxConnections < 10 ? 10 : maxConnections;
38+
_apiKey = apiKey;
3739
_pingTask = PingLoop();
3840
}
3941

@@ -103,14 +105,15 @@ public async Task HandleConnection(HttpListenerContext context)
103105
return;
104106
}
105107

108+
if (!ValidateApiKey(context.Request))
109+
{
110+
await WriteJsonError(context.Response, 401, ErrorCode.Unauthorized, LangManager.Get("Api_InvalidApiKey"));
111+
return;
112+
}
113+
106114
if (_connections.Count >= _maxConnections)
107115
{
108-
context.Response.StatusCode = 429;
109-
var bytes = Encoding.UTF8.GetBytes("{\"success\":false,\"errorInfo\":{\"code\":\"TooManyConnections\",\"message\":\"" + LangManager.Get("Ws_ConnectionLimit") + "\"}}");
110-
context.Response.ContentType = "application/json";
111-
context.Response.ContentLength64 = bytes.Length;
112-
await context.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
113-
context.Response.Close();
116+
await WriteJsonError(context.Response, 429, "TooManyConnections", LangManager.Get("Ws_ConnectionLimit"));
114117
return;
115118
}
116119

@@ -215,6 +218,32 @@ private static async Task SendErrorQuietly(WebSocket ws, string code, string err
215218
}
216219
}
217220

221+
private bool ValidateApiKey(HttpListenerRequest request)
222+
{
223+
return ValidateApiKeyCore(_apiKey, request.QueryString["apiKey"], request.Headers["X-API-Key"]);
224+
}
225+
226+
internal static bool ValidateApiKeyCore(string? configuredKey, string? queryApiKey, string? headerApiKey)
227+
{
228+
return Router.ValidateApiKeyCore(configuredKey, queryApiKey)
229+
|| Router.ValidateApiKeyCore(configuredKey, headerApiKey);
230+
}
231+
232+
private static async Task WriteJsonError(HttpListenerResponse response, int statusCode, string code, string message)
233+
{
234+
var json = JsonConvert.SerializeObject(new
235+
{
236+
success = false,
237+
errorInfo = new { code, message }
238+
});
239+
var bytes = Encoding.UTF8.GetBytes(json);
240+
response.StatusCode = statusCode;
241+
response.ContentType = "application/json";
242+
response.ContentLength64 = bytes.Length;
243+
await response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
244+
response.Close();
245+
}
246+
218247
public async Task Broadcast(string message)
219248
{
220249
var bytes = Encoding.UTF8.GetBytes(message);

lib/EasyInk.Net/EasyInk.Printer/src/ServiceConfig.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static ServiceProvider Configure(HostConfig config)
5555
services.AddSingleton<HttpServer>(sp =>
5656
new HttpServer(config.HttpPort, config.MaxConcurrentRequests));
5757
services.AddSingleton<WebSocketHandler>(sp =>
58-
new WebSocketHandler(config.MaxWebSocketConnections));
58+
new WebSocketHandler(config.MaxWebSocketConnections, config.ApiKey));
5959
services.AddSingleton<WebSocketCommandHandler>();
6060
services.AddSingleton<Router>();
6161

lib/EasyInk.Net/EasyInk.Printer/tests/WebSocketHandlerTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ public void ConnectionCountChanged_EventCanBeSubscribed()
2626
Assert.False(fired);
2727
}
2828

29+
[Theory]
30+
[InlineData(null, null, null, true)]
31+
[InlineData("secret", "secret", null, true)]
32+
[InlineData("secret", null, "secret", true)]
33+
[InlineData("secret", "wrong", "secret", true)]
34+
[InlineData("secret", "wrong", null, false)]
35+
[InlineData("secret", null, null, false)]
36+
public void ValidateApiKeyCore_AcceptsQueryOrHeaderKey(string? configuredKey, string? queryKey, string? headerKey, bool expected)
37+
{
38+
Assert.Equal(expected, WebSocketHandler.ValidateApiKeyCore(configuredKey, queryKey, headerKey));
39+
}
40+
2941
[Theory]
3042
[InlineData(64)]
3143
[InlineData(995)]

packages/print/integration-easyink-printer/src/client.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ describe('easy ink printer client', () => {
112112
await assertion
113113
})
114114

115+
it('rejects printer list service errors instead of treating them as an empty list', async () => {
116+
Object.defineProperty(globalThis, 'fetch', {
117+
configurable: true,
118+
value: vi.fn(async () => new Response(JSON.stringify({
119+
success: false,
120+
errorInfo: {
121+
code: 'INTERNAL_ERROR',
122+
message: 'printer backend failed',
123+
},
124+
}), {
125+
status: 200,
126+
headers: { 'Content-Type': 'application/json' },
127+
})),
128+
})
129+
const client = new EasyInkPrinterClient()
130+
131+
await expect(client.refreshPrinters()).rejects.toMatchObject({
132+
code: 'INTERNAL_ERROR',
133+
message: 'printer backend failed',
134+
})
135+
expect(client.devices).toEqual([])
136+
})
137+
115138
it('rejects immediately when WebSocket send fails', async () => {
116139
const client = new EasyInkPrinterClient({ responseTimeoutMs: 1000 })
117140
const socket = await connectClient(client)

packages/print/integration-easyink-printer/src/client.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ interface PrinterResultMessage {
216216
errorInfo?: { code?: string, message?: string, details?: string }
217217
}
218218

219+
interface PrinterHttpResult {
220+
success?: boolean
221+
data?: unknown
222+
errorInfo?: { code?: string, message?: string, details?: string }
223+
}
224+
219225
export class EasyInkPrinterClient {
220226
serviceUrl: string
221227
apiKey?: string
@@ -378,8 +384,8 @@ export class EasyInkPrinterClient {
378384
if (!response.ok)
379385
throw new EasyInkPrintError(`获取打印机列表失败: HTTP ${response.status}`, 'PRINTER_LIST_FAILED')
380386

381-
const payload = await response.json() as { data?: unknown }
382-
const devices = normalizePrinterDevices(payload.data ?? payload)
387+
const payload = await response.json() as PrinterHttpResult
388+
const devices = normalizePrinterDevices(unwrapPrinterResultData(payload))
383389
this.devices = devices
384390
this.ensureSelectedPrinter(devices)
385391
return devices
@@ -883,6 +889,22 @@ function normalizePrinterDevices(data: unknown): EasyInkPrinterDevice[] {
883889
.filter(device => device.name.length > 0) as EasyInkPrinterDevice[]
884890
}
885891

892+
function unwrapPrinterResultData(payload: unknown): unknown {
893+
if (!isRecord(payload))
894+
return payload
895+
896+
if (payload.success === false) {
897+
const errorInfo = isRecord(payload.errorInfo) ? payload.errorInfo : undefined
898+
throw new EasyInkPrintError(
899+
toOptionalString(errorInfo?.message) ?? '打印服务请求失败',
900+
toOptionalString(errorInfo?.code) ?? 'PRINTER_REQUEST_FAILED',
901+
errorInfo,
902+
)
903+
}
904+
905+
return 'data' in payload ? payload.data : payload
906+
}
907+
886908
function isRecord(value: unknown): value is Record<string, unknown> {
887909
return typeof value === 'object' && value !== null
888910
}

0 commit comments

Comments
 (0)