Skip to content

Commit 6365bd8

Browse files
committed
[client] add JWT management methods
1 parent b56c45d commit 6365bd8

File tree

3 files changed

+188
-10
lines changed

3 files changed

+188
-10
lines changed

src/client.test.ts

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
MessageState,
1212
ProcessState,
1313
RegisterWebHookRequest,
14+
TokenRequest,
15+
TokenResponse,
1416
WebHook,
1517
WebHookEventType,
1618
} from './domain';
@@ -345,7 +347,7 @@ describe('Client', () => {
345347
describe('Client with JWT Authentication', () => {
346348
let client: Client;
347349
let mockHttpClient: HttpClient;
348-
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ';
350+
const jwtToken = 'fake-token-123';
349351

350352
beforeEach(() => {
351353
mockHttpClient = {
@@ -389,7 +391,7 @@ describe('Client', () => {
389391
{
390392
"Content-Type": "application/json",
391393
"User-Agent": "android-sms-gateway/3.0 (client; js)",
392-
Authorization: `Bearer ${jwtToken}`,
394+
Authorization: `Bearer fake-token-123`,
393395
},
394396
);
395397
expect(result).toBe(expectedState);
@@ -416,7 +418,7 @@ describe('Client', () => {
416418
`${BASE_URL}/message/${messageId}`,
417419
{
418420
"User-Agent": "android-sms-gateway/3.0 (client; js)",
419-
Authorization: `Bearer ${jwtToken}`,
421+
Authorization: `Bearer fake-token-123`,
420422
},
421423
);
422424
expect(result).toBe(expectedState);
@@ -508,4 +510,93 @@ describe('Client', () => {
508510
);
509511
expect(result).toBe(undefined);
510512
});
513+
514+
// JWT Token Management Tests
515+
describe('JWT Token Management', () => {
516+
let client: Client;
517+
let mockHttpClient: HttpClient;
518+
519+
beforeEach(() => {
520+
mockHttpClient = {
521+
get: jest.fn(),
522+
post: jest.fn(),
523+
put: jest.fn(),
524+
patch: jest.fn(),
525+
delete: jest.fn(),
526+
} as unknown as HttpClient;
527+
client = new Client('login', 'password', mockHttpClient);
528+
});
529+
530+
it('generates a new token', async () => {
531+
const tokenRequest: TokenRequest = {
532+
scopes: ['read', 'write'],
533+
ttl: 3600,
534+
};
535+
const expectedResponse: TokenResponse = {
536+
access_token: 'fake-token-123',
537+
token_type: 'Bearer',
538+
id: 'token-id-123',
539+
expires_at: '2024-12-31T23:59:59Z',
540+
};
541+
542+
(mockHttpClient.post as jest.Mock).mockResolvedValue(expectedResponse);
543+
544+
const result = await client.generateToken(tokenRequest);
545+
546+
expect(mockHttpClient.post).toHaveBeenCalledWith(
547+
`${BASE_URL}/auth/token`,
548+
tokenRequest,
549+
{
550+
"Content-Type": "application/json",
551+
"User-Agent": "android-sms-gateway/3.0 (client; js)",
552+
Authorization: expect.any(String),
553+
},
554+
);
555+
expect(result).toBe(expectedResponse);
556+
});
557+
558+
it('generates a new token without TTL', async () => {
559+
const tokenRequest: TokenRequest = {
560+
scopes: ['read'],
561+
};
562+
const expectedResponse: TokenResponse = {
563+
access_token: 'fake-token-123',
564+
token_type: 'Bearer',
565+
id: 'token-id-456',
566+
expires_at: '2024-12-31T23:59:59Z',
567+
};
568+
569+
(mockHttpClient.post as jest.Mock).mockResolvedValue(expectedResponse);
570+
571+
const result = await client.generateToken(tokenRequest);
572+
573+
expect(mockHttpClient.post).toHaveBeenCalledWith(
574+
`${BASE_URL}/auth/token`,
575+
tokenRequest,
576+
{
577+
"Content-Type": "application/json",
578+
"User-Agent": "android-sms-gateway/3.0 (client; js)",
579+
Authorization: expect.any(String),
580+
},
581+
);
582+
expect(result).toBe(expectedResponse);
583+
});
584+
585+
it('revokes a token', async () => {
586+
const jti = 'token-id-123';
587+
588+
(mockHttpClient.delete as jest.Mock).mockResolvedValue(undefined);
589+
590+
const result = await client.revokeToken(jti);
591+
592+
expect(mockHttpClient.delete).toHaveBeenCalledWith(
593+
`${BASE_URL}/auth/token/${jti}`,
594+
{
595+
"User-Agent": "android-sms-gateway/3.0 (client; js)",
596+
Authorization: expect.any(String),
597+
},
598+
);
599+
expect(result).toBe(undefined);
600+
});
601+
});
511602
});

src/client.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
DeviceSettings,
88
HealthResponse,
99
LogEntry,
10-
MessagesExportRequest
10+
MessagesExportRequest,
11+
TokenRequest,
12+
TokenResponse
1113
} from "./domain";
1214
import { HttpClient } from "./http";
1315

@@ -53,39 +55,56 @@ export class Client {
5355
* Gets the default HTTP client implementation
5456
*/
5557
private getDefaultHttpClient(): HttpClient {
56-
// This would typically be implemented elsewhere, but we'll provide a basic implementation
58+
const handleResponse = async (response: Response): Promise<any> => {
59+
if (response.status === 204) {
60+
return null;
61+
}
62+
63+
if (!response.ok) {
64+
const text = await response.text();
65+
throw new Error(`HTTP error ${response.status}: ${text}`);
66+
}
67+
68+
const contentType = response.headers.get("Content-Type");
69+
if (contentType && contentType.includes("application/json")) {
70+
return await response.json();
71+
} else {
72+
return await response.text();
73+
}
74+
};
75+
5776
return {
5877
get: async <T>(url: string, headers?: Record<string, string>): Promise<T> => {
5978
const response = await fetch(url, { method: 'GET', headers });
60-
return response.json();
79+
return handleResponse(response);
6180
},
6281
post: async <T>(url: string, body: any, headers?: Record<string, string>): Promise<T> => {
6382
const response = await fetch(url, {
6483
method: 'POST',
6584
headers,
6685
body: JSON.stringify(body)
6786
});
68-
return response.json();
87+
return handleResponse(response);
6988
},
7089
put: async <T>(url: string, body: any, headers?: Record<string, string>): Promise<T> => {
7190
const response = await fetch(url, {
7291
method: 'PUT',
7392
headers,
7493
body: JSON.stringify(body)
7594
});
76-
return response.json();
95+
return handleResponse(response);
7796
},
7897
patch: async <T>(url: string, body: any, headers?: Record<string, string>): Promise<T> => {
7998
const response = await fetch(url, {
8099
method: 'PATCH',
81100
headers,
82101
body: JSON.stringify(body)
83102
});
84-
return response.json();
103+
return handleResponse(response);
85104
},
86105
delete: async <T>(url: string, headers?: Record<string, string>): Promise<T> => {
87106
const response = await fetch(url, { method: 'DELETE', headers });
88-
return response.json();
107+
return handleResponse(response);
89108
},
90109
};
91110
}
@@ -287,4 +306,32 @@ export class Client {
287306

288307
return this.httpClient.patch<void>(url, settings, headers);
289308
}
309+
310+
/**
311+
* Generate a new JWT token with specified scopes and TTL
312+
* @param request - The token request parameters
313+
* @returns The generated token response
314+
*/
315+
async generateToken(request: TokenRequest): Promise<TokenResponse> {
316+
const url = `${this.baseUrl}/auth/token`;
317+
const headers = {
318+
"Content-Type": "application/json",
319+
...this.defaultHeaders,
320+
};
321+
322+
return this.httpClient.post<TokenResponse>(url, request, headers);
323+
}
324+
325+
/**
326+
* Revoke a JWT token by its ID
327+
* @param jti - The JWT token ID to revoke
328+
*/
329+
async revokeToken(jti: string): Promise<void> {
330+
const url = `${this.baseUrl}/auth/token/${jti}`;
331+
const headers = {
332+
...this.defaultHeaders,
333+
};
334+
335+
return this.httpClient.delete<void>(url, headers);
336+
}
290337
}

src/domain.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,46 @@ export interface MessagesExportRequest {
471471
until: Date;
472472
}
473473

474+
/**
475+
* Represents a request to generate a new JWT token.
476+
*/
477+
export interface TokenRequest {
478+
/**
479+
* The scopes to include in the token.
480+
*/
481+
scopes: string[];
482+
483+
/**
484+
* The time-to-live (TTL) of the token in seconds.
485+
*/
486+
ttl?: number;
487+
}
488+
489+
/**
490+
* Represents a response containing a new JWT token.
491+
*/
492+
export interface TokenResponse {
493+
/**
494+
* The JWT access token.
495+
*/
496+
access_token: string;
497+
498+
/**
499+
* The type of the token.
500+
*/
501+
token_type: string;
502+
503+
/**
504+
* The unique identifier of the token.
505+
*/
506+
id: string;
507+
508+
/**
509+
* The expiration time of the token.
510+
*/
511+
expires_at: string;
512+
}
513+
474514
/**
475515
* Represents the payload of a webhook event.
476516
*/

0 commit comments

Comments
 (0)