Skip to content

Commit 268df92

Browse files
authored
Merge pull request #222 from import-ai/fix/mem
refactor: replace KVStore with CacheService
2 parents 214258a + 7706f1d commit 268df92

22 files changed

+150
-160
lines changed

src/api-key/api-key.service.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { Test, TestingModule } from '@nestjs/testing';
33
import { getRepositoryToken } from '@nestjs/typeorm';
44
import { Repository } from 'typeorm';
5-
import { ForbiddenException } from '@nestjs/common';
65
import { APIKeyService } from './api-key.service';
76
import {
87
APIKey,
@@ -74,8 +73,10 @@ describe('APIKeyService', () => {
7473
t: jest.fn((key: string, options?: any) => {
7574
// Return mock translations for test purposes
7675
const translations: Record<string, string> = {
77-
'apikey.errors.noPermissionForNamespace': 'User {{userId}} does not have permission to namespace {{namespaceId}}',
78-
'apikey.errors.noWritePermission': 'User {{userId}} does not have write permission to resource {{resourceId}} in namespace {{namespaceId}}',
76+
'apikey.errors.noPermissionForNamespace':
77+
'User {{userId}} does not have permission to namespace {{namespaceId}}',
78+
'apikey.errors.noWritePermission':
79+
'User {{userId}} does not have write permission to resource {{resourceId}} in namespace {{namespaceId}}',
7980
};
8081
let translation = translations[key] || key;
8182
if (options?.args) {

src/auth/api-key/api-key-auth.guard.spec.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import { Test, TestingModule } from '@nestjs/testing';
2-
import {
3-
ExecutionContext,
4-
UnauthorizedException,
5-
ForbiddenException,
6-
} from '@nestjs/common';
2+
import { ExecutionContext } from '@nestjs/common';
73
import { Reflector } from '@nestjs/core';
84
import { APIKeyAuthGuard } from 'omniboxd/auth';
95
import { APIKeyService } from 'omniboxd/api-key/api-key.service';
@@ -20,7 +16,6 @@ describe('APIKeyAuthGuard', () => {
2016
let guard: APIKeyAuthGuard;
2117
let apiKeyService: jest.Mocked<APIKeyService>;
2218
let reflector: jest.Mocked<Reflector>;
23-
let i18nService: jest.Mocked<I18nService>;
2419

2520
beforeEach(async () => {
2621
const module: TestingModule = await Test.createTestingModule({
@@ -44,11 +39,14 @@ describe('APIKeyAuthGuard', () => {
4439
t: jest.fn((key: string) => {
4540
// Return mock translations for test purposes
4641
const translations: Record<string, string> = {
47-
'apikey.errors.authorizationHeaderRequired': 'Authorization header is required',
42+
'apikey.errors.authorizationHeaderRequired':
43+
'Authorization header is required',
4844
'apikey.errors.invalidApiKeyFormat': 'Invalid API key format',
4945
'apikey.errors.invalidApiKey': 'Invalid API key',
50-
'apikey.errors.noPermissionForTarget': 'No permission for target {{target}}',
51-
'apikey.errors.noSpecificPermission': 'No {{permission}} permission for target {{target}}',
46+
'apikey.errors.noPermissionForTarget':
47+
'No permission for target {{target}}',
48+
'apikey.errors.noSpecificPermission':
49+
'No {{permission}} permission for target {{target}}',
5250
};
5351
return translations[key] || key;
5452
}),
@@ -60,7 +58,6 @@ describe('APIKeyAuthGuard', () => {
6058
guard = module.get<APIKeyAuthGuard>(APIKeyAuthGuard);
6159
apiKeyService = module.get(APIKeyService);
6260
reflector = module.get(Reflector);
63-
i18nService = module.get(I18nService);
6461
});
6562

6663
const createMockExecutionContext = (
@@ -111,9 +108,7 @@ describe('APIKeyAuthGuard', () => {
111108
.mockReturnValueOnce({ enabled: true }); // apiKeyAuthOptions = { enabled: true }
112109
const context = createMockExecutionContext();
113110

114-
await expect(guard.canActivate(context)).rejects.toThrow(
115-
AppException,
116-
);
111+
await expect(guard.canActivate(context)).rejects.toThrow(AppException);
117112
});
118113

119114
it('should throw AppException when API key does not start with sk-', async () => {
@@ -122,9 +117,7 @@ describe('APIKeyAuthGuard', () => {
122117
.mockReturnValueOnce({ enabled: true }); // apiKeyAuthOptions = { enabled: true }
123118
const context = createMockExecutionContext('Bearer invalid-key');
124119

125-
await expect(guard.canActivate(context)).rejects.toThrow(
126-
AppException,
127-
);
120+
await expect(guard.canActivate(context)).rejects.toThrow(AppException);
128121
});
129122

130123
it('should throw AppException when API key is not found', async () => {
@@ -134,9 +127,7 @@ describe('APIKeyAuthGuard', () => {
134127
const context = createMockExecutionContext('Bearer sk-validformat');
135128
apiKeyService.findByValue.mockResolvedValue(null);
136129

137-
await expect(guard.canActivate(context)).rejects.toThrow(
138-
AppException,
139-
);
130+
await expect(guard.canActivate(context)).rejects.toThrow(AppException);
140131
});
141132

142133
it('should allow valid API key and set apiKey and user on request', async () => {
@@ -245,9 +236,7 @@ describe('APIKeyAuthGuard', () => {
245236
const context = createMockExecutionContext('Bearer sk-validkey');
246237
apiKeyService.findByValue.mockResolvedValue(mockApiKey);
247238

248-
await expect(guard.canActivate(context)).rejects.toThrow(
249-
AppException,
250-
);
239+
await expect(guard.canActivate(context)).rejects.toThrow(AppException);
251240
});
252241

253242
it('should throw AppException when API key lacks specific permission type', async () => {
@@ -283,9 +272,7 @@ describe('APIKeyAuthGuard', () => {
283272
const context = createMockExecutionContext('Bearer sk-validkey');
284273
apiKeyService.findByValue.mockResolvedValue(mockApiKey);
285274

286-
await expect(guard.canActivate(context)).rejects.toThrow(
287-
AppException,
288-
);
275+
await expect(guard.canActivate(context)).rejects.toThrow(AppException);
289276
});
290277

291278
it('should allow API key with multiple matching permissions', async () => {

src/auth/api-key/api-key-auth.guard.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class APIKeyAuthGuard implements CanActivate {
7777
apiKeyAuthOptions.permissions &&
7878
apiKeyAuthOptions.permissions.length > 0
7979
) {
80-
await this.validatePermissions(
80+
this.validatePermissions(
8181
apiKey.attrs.permissions || [],
8282
apiKeyAuthOptions.permissions,
8383
);
@@ -90,10 +90,10 @@ export class APIKeyAuthGuard implements CanActivate {
9090
return true;
9191
}
9292

93-
private async validatePermissions(
93+
private validatePermissions(
9494
apiKeyPermissions: APIKeyPermission[],
9595
requiredPermissions: APIKeyPermission[],
96-
): Promise<void> {
96+
): void {
9797
for (const required of requiredPermissions) {
9898
const apiKeyPermission = apiKeyPermissions.find(
9999
(p) => p.target === required.target,

src/auth/auth.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { SocialService } from 'omniboxd/auth/social.service';
2323
import { APIKeyModule } from 'omniboxd/api-key/api-key.module';
2424
import { APIKeyAuthGuard } from 'omniboxd/auth/api-key/api-key-auth.guard';
2525
import { CookieAuthGuard } from 'omniboxd/auth/cookie/cookie-auth.guard';
26+
import { CacheService } from 'omniboxd/common/cache.service';
2627

2728
@Module({
2829
exports: [AuthService, WechatService, GoogleService, SocialService],
@@ -39,6 +40,7 @@ import { CookieAuthGuard } from 'omniboxd/auth/cookie/cookie-auth.guard';
3940
GoogleService,
4041
JwtStrategy,
4142
LocalStrategy,
43+
CacheService,
4244
{
4345
provide: APP_GUARD,
4446
useClass: JwtAuthGuard,

src/auth/auth.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ export class AuthService {
288288
}
289289
}
290290

291-
async jwtVerify(token: string) {
291+
jwtVerify(token: string) {
292292
if (!token) {
293293
const message = this.i18n.t('auth.errors.noToken');
294294
throw new AppException(message, 'NO_TOKEN', HttpStatus.UNAUTHORIZED);

src/auth/cookie/cookie-auth.guard.spec.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Test, TestingModule } from '@nestjs/testing';
2-
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
2+
import { ExecutionContext } from '@nestjs/common';
33
import { Reflector } from '@nestjs/core';
44
import { CookieAuthGuard } from 'omniboxd/auth/cookie/cookie-auth.guard';
55
import { AuthService } from 'omniboxd/auth/auth.service';
@@ -10,7 +10,6 @@ describe('CookieAuthGuard', () => {
1010
let guard: CookieAuthGuard;
1111
let reflector: jest.Mocked<Reflector>;
1212
let authService: jest.Mocked<AuthService>;
13-
let i18nService: jest.Mocked<I18nService>;
1413

1514
beforeEach(async () => {
1615
const module: TestingModule = await Test.createTestingModule({
@@ -34,7 +33,8 @@ describe('CookieAuthGuard', () => {
3433
t: jest.fn((key: string) => {
3534
// Return mock translations for test purposes
3635
const translations: Record<string, string> = {
37-
'auth.errors.tokenCookieRequired': 'Authentication token cookie is required',
36+
'auth.errors.tokenCookieRequired':
37+
'Authentication token cookie is required',
3838
'auth.errors.tokenInvalid': 'Invalid or expired token',
3939
'auth.errors.invalidTokenPayload': 'Invalid token payload',
4040
};
@@ -48,7 +48,6 @@ describe('CookieAuthGuard', () => {
4848
guard = module.get<CookieAuthGuard>(CookieAuthGuard);
4949
reflector = module.get(Reflector);
5050
authService = module.get(AuthService);
51-
i18nService = module.get(I18nService);
5251
});
5352

5453
const createMockExecutionContext = (cookies: any = {}): ExecutionContext => {
@@ -94,9 +93,7 @@ describe('CookieAuthGuard', () => {
9493
.mockReturnValueOnce({ enabled: true }); // cookieAuthOptions with default onAuthFail = 'reject'
9594
const context = createMockExecutionContext();
9695

97-
await expect(guard.canActivate(context)).rejects.toThrow(
98-
AppException,
99-
);
96+
await expect(guard.canActivate(context)).rejects.toThrow(AppException);
10097
});
10198

10299
it('should throw AppException when token is invalid', async () => {
@@ -106,9 +103,7 @@ describe('CookieAuthGuard', () => {
106103
const context = createMockExecutionContext({ token: 'invalid-token' });
107104
authService.jwtVerify.mockRejectedValue(new Error('Invalid token'));
108105

109-
await expect(guard.canActivate(context)).rejects.toThrow(
110-
AppException,
111-
);
106+
await expect(guard.canActivate(context)).rejects.toThrow(AppException);
112107
});
113108

114109
it('should throw AppException when token payload is invalid', async () => {
@@ -118,9 +113,7 @@ describe('CookieAuthGuard', () => {
118113
const context = createMockExecutionContext({ token: 'valid-token' });
119114
authService.jwtVerify.mockResolvedValue({}); // No sub field
120115

121-
await expect(guard.canActivate(context)).rejects.toThrow(
122-
AppException,
123-
);
116+
await expect(guard.canActivate(context)).rejects.toThrow(AppException);
124117
});
125118

126119
it('should successfully authenticate with valid token and set cookie auth data', async () => {

src/auth/social.service.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ import { EntityManager } from 'typeorm';
22
import generateId from 'omniboxd/utils/generate-id';
33
import { UserService } from 'omniboxd/user/user.service';
44
import { WechatCheckResponseDto } from 'omniboxd/auth/dto/wechat-login.dto';
5-
import { HttpStatus, Inject, Injectable } from '@nestjs/common';
5+
import { HttpStatus, Injectable } from '@nestjs/common';
66
import { AppException } from 'omniboxd/common/exceptions/app.exception';
77
import { I18nService } from 'nestjs-i18n';
8-
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
9-
import { KVStore } from 'omniboxd/common/kv-store';
10-
import { ConfigService } from '@nestjs/config';
8+
import { CacheService } from 'omniboxd/common/cache.service';
119

1210
export interface UserSocialState {
1311
type: string;
@@ -18,22 +16,15 @@ export interface UserSocialState {
1816

1917
@Injectable()
2018
export class SocialService {
19+
private readonly namespace = '/social/states';
2120
private readonly minUsernameLength = 2;
2221
private readonly maxUsernameLength = 32;
23-
private readonly kvStore: KVStore<UserSocialState>;
2422

2523
constructor(
2624
private readonly userService: UserService,
2725
private readonly i18n: I18nService,
28-
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
29-
private readonly configService: ConfigService,
30-
) {
31-
this.kvStore = new KVStore(
32-
this.cacheManager,
33-
'/social/states',
34-
this.configService,
35-
);
36-
}
26+
private readonly cacheService: CacheService,
27+
) {}
3728

3829
/**
3930
* Generate a kv state, return a kv key
@@ -44,7 +35,8 @@ export class SocialService {
4435
async generateState(type: string, prefix: string = ''): Promise<string> {
4536
const key = `${prefix ? prefix + '_' : ''}${generateId()}`;
4637
const expiresIn = 5 * 60 * 1000; // Expires in 5 minutes
47-
await this.kvStore.set(
38+
await this.cacheService.set<UserSocialState>(
39+
this.namespace,
4840
key,
4941
{
5042
type,
@@ -57,13 +49,18 @@ export class SocialService {
5749
}
5850

5951
async getState(state: string) {
60-
return await this.kvStore.get(state);
52+
return await this.cacheService.get<UserSocialState>(this.namespace, state);
6153
}
6254

6355
async updateState(state: string, data: UserSocialState) {
6456
const ttl = data.expiresIn - (Date.now() - data.createdAt);
6557
if (ttl > 0) {
66-
await this.kvStore.set(state, data, ttl);
58+
await this.cacheService.set<UserSocialState>(
59+
this.namespace,
60+
state,
61+
data,
62+
ttl,
63+
);
6764
}
6865
}
6966

src/common/cache.service.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
3+
import { ConfigService } from '@nestjs/config';
4+
5+
@Injectable()
6+
export class CacheService {
7+
private readonly env: string;
8+
9+
constructor(
10+
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
11+
private readonly configService: ConfigService,
12+
) {
13+
this.env = this.configService.get<string>('ENV', 'unknown');
14+
}
15+
16+
private getKey(namespace: string, key: string): string {
17+
return `/${this.env}${namespace}/${key}`;
18+
}
19+
20+
async get<T>(namespace: string, key: string): Promise<T | null> {
21+
return await this.cacheManager.get<T>(this.getKey(namespace, key));
22+
}
23+
24+
async set<T>(
25+
namespace: string,
26+
key: string,
27+
value: T,
28+
ttl?: number,
29+
): Promise<void> {
30+
await this.cacheManager.set(this.getKey(namespace, key), value, ttl);
31+
}
32+
33+
async delete(namespace: string, key: string): Promise<void> {
34+
await this.cacheManager.del(this.getKey(namespace, key));
35+
}
36+
}

src/common/kv-store.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/decorators/user-id.decorator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ export const UserId = createParamDecorator(
1818

1919
if (!userId && !optional) {
2020
// Note: Decorators cannot easily inject I18nService, using static message
21-
throw new AppException('Not authorized', 'NOT_AUTHORIZED', HttpStatus.UNAUTHORIZED);
21+
throw new AppException(
22+
'Not authorized',
23+
'NOT_AUTHORIZED',
24+
HttpStatus.UNAUTHORIZED,
25+
);
2226
}
2327

2428
return userId;

0 commit comments

Comments
 (0)