Skip to content

Commit e8fb08a

Browse files
feat: add trial client service (#104)
* feat: trial client service * refactor: suggestions, clarity * refactor: simplify and use hincrby * chore: tighter typing, simplify key
1 parent 3caddf0 commit e8fb08a

4 files changed

Lines changed: 238 additions & 0 deletions

File tree

package-lock.json

Lines changed: 147 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"ajv-formats": "^2.0.2",
7676
"commander": "^7.2.0",
7777
"dotenv": "^16.3.1",
78+
"ioredis": "^5.8.1",
7879
"jsonwebtoken": "^9.0.0",
7980
"koa": "^2.11.0",
8081
"koa-body": "^4.2.0",

src/main/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './openapi.js';
1111
export * from './router.js';
1212
export * from './schema.js';
1313
export * from './services/index.js';
14+
export * from './trial.js';
1415
export * from './util.js';
1516

1617
export * from '@nodescript/logger';

src/main/trial.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Logger } from '@nodescript/logger';
2+
import { Redis } from 'ioredis';
3+
import { config } from 'mesh-config';
4+
import { dep } from 'mesh-ioc';
5+
6+
import { AccessForbidden } from './ac-auth.js';
7+
8+
export interface TokenServiceRestriction {
9+
serviceName: string;
10+
requestLimit: number;
11+
}
12+
13+
export interface TrialToken {
14+
serviceRestrictions: Array<TokenServiceRestriction>;
15+
[key: string]: any;
16+
}
17+
18+
export class TrialClient {
19+
20+
@config() private REDIS_URL!: string;
21+
@dep() private logger!: Logger;
22+
23+
private isRunning = false;
24+
private trialKeyPrefix = 'cache:trialClient';
25+
26+
redisClient: Redis;
27+
28+
constructor() {
29+
this.redisClient = this.createRedisClient();
30+
}
31+
32+
async start() {
33+
if (this.isRunning) {
34+
return;
35+
}
36+
this.isRunning = true;
37+
await this.redisClient.connect();
38+
this.logger.info('TrialClient Redis connected');
39+
}
40+
41+
async stop() {
42+
try {
43+
this.redisClient.disconnect();
44+
this.logger.info('TrialClient Redis disconnected');
45+
} finally {
46+
this.isRunning = false;
47+
}
48+
}
49+
50+
isTrialToken(token: Record<string, any>): token is TrialToken {
51+
return !!token.serviceRestrictions;
52+
}
53+
54+
async requireValidServiceRestriction(token: TrialToken, serviceName: string) {
55+
const serviceRestriction = token.serviceRestrictions.find((s: TokenServiceRestriction) => s.serviceName === serviceName);
56+
if (!serviceRestriction) {
57+
throw new AccessForbidden('Service access not configured on token');
58+
}
59+
const requestCount = await this.getRequestCount(token.clientId, serviceName);
60+
if (requestCount >= serviceRestriction.requestLimit) {
61+
throw new AccessForbidden('Trial token has exceeded request limit for service');
62+
}
63+
}
64+
65+
async incrementRequests(token: Record<string, any>, serviceName: string) {
66+
const redisKey = this.getServiceKey(token.clientId, serviceName);
67+
await this.redisClient.hincrby(redisKey, 'requestCount', 1);
68+
}
69+
70+
async getRequestCount(clientId: string, serviceName: string) {
71+
const redisKey = this.getServiceKey(clientId, serviceName);
72+
const requestCountStr = await this.redisClient.hget(redisKey, 'requestCount');
73+
if (requestCountStr == null) {
74+
throw new AccessForbidden('Service access for token not configured');
75+
}
76+
return Number(requestCountStr);
77+
}
78+
79+
private createRedisClient() {
80+
return new Redis(this.REDIS_URL, {
81+
lazyConnect: true,
82+
disconnectTimeout: 10,
83+
});
84+
}
85+
86+
private getServiceKey(clientId: string, serviceName: string) {
87+
return `${this.trialKeyPrefix}:${clientId}:${serviceName}`;
88+
}
89+
}

0 commit comments

Comments
 (0)