Skip to content

Commit 25ee84e

Browse files
authored
feat: add user info, collaborator and access token cache (#1859)
* feat: add user info, collaborator and access token cache * fix: remove debug code * chore: methods name * fix: missing dependencies module * fix: null value not hit cache * fix: include Date value cache * fix: access token service unit test * fix: collaborator unit test * fix: collaborator cache clear in createBaseCollaborator * fix: cache ttl is required * feat: delete performance cache endpoints * test: add accessToken expired e2e * fix: more safe typescript
1 parent d8d7cf6 commit 25ee84e

26 files changed

Lines changed: 363 additions & 114 deletions

‎apps/nestjs-backend/src/features/access-token/access-token.service.spec.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@ import { Test } from '@nestjs/testing';
55
import { PrismaService } from '@teable/db-main-prisma';
66
import { mockDeep, mockReset } from 'vitest-mock-extended';
77
import { GlobalModule } from '../../global/global.module';
8+
import { AccessTokenModel } from '../model/access-token';
89
import { AccessTokenModule } from './access-token.module';
910
import { AccessTokenService } from './access-token.service';
1011

1112
describe('AccessTokenService', () => {
1213
let accessTokenService: AccessTokenService;
1314
const prismaService = mockDeep<PrismaService>();
15+
const accessTokenModel = mockDeep<AccessTokenModel>();
1416

1517
beforeEach(async () => {
1618
const module: TestingModule = await Test.createTestingModule({
1719
imports: [GlobalModule, AccessTokenModule],
1820
})
1921
.overrideProvider(PrismaService)
2022
.useValue(prismaService)
23+
.overrideProvider(AccessTokenModel)
24+
.useValue(accessTokenModel)
2125
.compile();
2226

2327
accessTokenService = module.get<AccessTokenService>(AccessTokenService);
@@ -47,7 +51,7 @@ describe('AccessTokenService', () => {
4751
const sign = 'SIGN';
4852
const expiredTime = new Date(Date.now() + 2000); // Expires in 2 seconds
4953
// Mock PrismaService response
50-
prismaService.accessToken.findUniqueOrThrow.mockResolvedValue({
54+
accessTokenModel.getAccessTokenRawById.mockResolvedValue({
5155
userId: 'user123',
5256
id: accessTokenId,
5357
sign,
@@ -74,7 +78,7 @@ describe('AccessTokenService', () => {
7478
const sign = 'INVALID_SIGN';
7579

7680
// Mock PrismaService response
77-
prismaService.accessToken.findUniqueOrThrow.mockResolvedValue({
81+
accessTokenModel.getAccessTokenRawById.mockResolvedValue({
7882
userId: 'user123',
7983
id: accessTokenId,
8084
sign: 'VALID_SIGN',
@@ -97,7 +101,7 @@ describe('AccessTokenService', () => {
97101
const expiredTime = new Date(Date.now() - 1500); // Expired 1 second ago
98102

99103
// Mock PrismaService response
100-
prismaService.accessToken.findUniqueOrThrow.mockResolvedValue({
104+
accessTokenModel.getAccessTokenRawById.mockResolvedValue({
101105
userId: 'user123',
102106
id: accessTokenId,
103107
sign,

‎apps/nestjs-backend/src/features/access-token/access-token.service.ts‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ import type {
88
UpdateAccessTokenRo,
99
} from '@teable/openapi';
1010
import { ClsService } from 'nestjs-cls';
11+
import { PerformanceCacheService } from '../../performance-cache';
12+
import { generateAccessTokenCacheKey } from '../../performance-cache/generate-keys';
1113
import type { IClsStore } from '../../types/cls';
14+
import { AccessTokenModel } from '../model/access-token';
1215
import { getAccessToken } from './access-token.encryptor';
1316

1417
@Injectable()
1518
export class AccessTokenService {
1619
constructor(
1720
private readonly prismaService: PrismaService,
18-
private readonly cls: ClsService<IClsStore>
21+
private readonly cls: ClsService<IClsStore>,
22+
private readonly accessTokenModel: AccessTokenModel,
23+
private readonly performanceCacheService: PerformanceCacheService
1924
) {}
2025

2126
private transformAccessTokenEntity<
@@ -55,24 +60,18 @@ export class AccessTokenService {
5560

5661
async validate(splitAccessTokenObj: { accessTokenId: string; sign: string }) {
5762
const { accessTokenId, sign } = splitAccessTokenObj;
58-
const accessTokenEntity = await this.prismaService.accessToken
59-
.findUniqueOrThrow({
60-
where: { id: accessTokenId },
61-
select: {
62-
userId: true,
63-
id: true,
64-
sign: true,
65-
expiredTime: true,
66-
},
67-
})
68-
.catch(() => {
69-
throw new UnauthorizedException('token not found');
70-
});
63+
const accessTokenEntity = await this.accessTokenModel.getAccessTokenRawById(accessTokenId);
64+
if (!accessTokenEntity) {
65+
throw new UnauthorizedException('token not found');
66+
}
7167
if (sign !== accessTokenEntity.sign) {
7268
throw new UnauthorizedException('sign error');
7369
}
7470
// expiredTime 1ms tolerance
75-
if (accessTokenEntity.expiredTime.getTime() < Date.now() + 1000) {
71+
if (
72+
accessTokenEntity.expiredTime &&
73+
new Date(accessTokenEntity.expiredTime).getTime() < Date.now() + 1000
74+
) {
7675
throw new UnauthorizedException('token expired');
7776
}
7877
await this.prismaService.accessToken.update({
@@ -177,6 +176,7 @@ export class AccessTokenService {
177176
lastUsedTime: true,
178177
},
179178
});
179+
await this.performanceCacheService.del(generateAccessTokenCacheKey(id));
180180
return {
181181
...this.transformAccessTokenEntity(accessTokenEntity),
182182
token: getAccessToken(id, sign),
@@ -206,6 +206,7 @@ export class AccessTokenService {
206206
hasFullAccess: true,
207207
},
208208
});
209+
await this.performanceCacheService.del(generateAccessTokenCacheKey(id));
209210
return this.transformAccessTokenEntity(accessTokenEntity);
210211
}
211212

‎apps/nestjs-backend/src/features/aggregation/open-api/aggregation-open-api.module.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { Module } from '@nestjs/common';
2-
import { PerformanceCacheModule } from '../../../performance-cache';
32
import { AggregationModule } from '../aggregation.module';
43
import { AggregationOpenApiController } from './aggregation-open-api.controller';
54
import { AggregationOpenApiService } from './aggregation-open-api.service';
65

76
@Module({
87
controllers: [AggregationOpenApiController],
9-
imports: [AggregationModule, PerformanceCacheModule],
8+
imports: [AggregationModule],
109
providers: [AggregationOpenApiService],
1110
exports: [AggregationOpenApiService],
1211
})

‎apps/nestjs-backend/src/features/auth/permission.service.spec.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { noop } from 'lodash';
1010
import { ClsService } from 'nestjs-cls';
1111
import type { DeepMockProxy } from 'vitest-mock-extended';
1212
import { mockDeep, mockReset } from 'vitest-mock-extended';
13+
import { GlobalModule } from '../../global/global.module';
1314
import type { IClsStore } from '../../types/cls';
1415
import { PermissionService } from './permission.service';
1516

@@ -23,6 +24,7 @@ describe('PermissionService', () => {
2324
clsServiceMock = mockDeep<ClsService<IClsStore>>();
2425

2526
const module: TestingModule = await Test.createTestingModule({
27+
imports: [GlobalModule],
2628
providers: [PermissionService, PrismaService, ClsService],
2729
})
2830
.overrideProvider(PrismaService)

‎apps/nestjs-backend/src/features/auth/permission.service.ts‎

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,35 @@ import { intersection, union } from 'lodash';
77
import { ClsService } from 'nestjs-cls';
88
import type { IClsStore } from '../../types/cls';
99
import { getMaxLevelRole } from '../../utils/get-max-level-role';
10+
import { CollaboratorModel } from '../model/collaborator';
1011

1112
@Injectable()
1213
export class PermissionService {
1314
constructor(
1415
private readonly prismaService: PrismaService,
15-
private readonly cls: ClsService<IClsStore>
16+
private readonly cls: ClsService<IClsStore>,
17+
private readonly collaboratorModel: CollaboratorModel
1618
) {}
1719

1820
private getDepartmentIds() {
1921
const departments = this.cls.get('organization.departments');
2022
return departments?.map((department) => department.id) || [];
2123
}
2224

25+
async getSpaceCollaborators(spaceId: string, principalId: string[]) {
26+
const collaborators = await this.collaboratorModel.getCollaboratorRawByResourceId(spaceId);
27+
return collaborators.filter((collaborator) => principalId.includes(collaborator.principalId));
28+
}
29+
30+
async getBaseCollaborators(baseId: string, principalId: string[]) {
31+
const collaborators = await this.collaboratorModel.getCollaboratorRawByResourceId(baseId);
32+
return collaborators.filter((collaborator) => principalId.includes(collaborator.principalId));
33+
}
34+
2335
async getRoleBySpaceId(spaceId: string, includeInactiveResource?: boolean) {
2436
const userId = this.cls.get('user.id');
2537
const departmentIds = this.getDepartmentIds();
26-
const collaborators = await this.prismaService.collaborator.findMany({
27-
where: {
28-
principalId: { in: [...departmentIds, userId] },
29-
resourceId: spaceId,
30-
resourceType: CollaboratorType.Space,
31-
},
32-
select: { roleName: true },
33-
});
38+
const collaborators = await this.getSpaceCollaborators(spaceId, [...departmentIds, userId]);
3439
const space = await this.prismaService.space.findFirst({
3540
where: {
3641
id: spaceId,
@@ -52,14 +57,7 @@ export class PermissionService {
5257
const departmentIds = this.getDepartmentIds();
5358
const userId = this.cls.get('user.id');
5459

55-
const collaborators = await this.prismaService.collaborator.findMany({
56-
where: {
57-
principalId: { in: [...departmentIds, userId] },
58-
resourceId: baseId,
59-
resourceType: CollaboratorType.Base,
60-
},
61-
select: { roleName: true },
62-
});
60+
const collaborators = await this.getBaseCollaborators(baseId, [...departmentIds, userId]);
6361
if (!collaborators.length) {
6462
return null;
6563
}

‎apps/nestjs-backend/src/features/auth/strategies/access-token.strategy.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export class AccessTokenStrategy extends PassportStrategy(PassportAccessTokenStr
2727

2828
async validate(payload: { accessTokenId: string; sign: string }) {
2929
const { userId, accessTokenId } = await this.accessTokenService.validate(payload);
30-
3130
const user = await this.userService.getUserById(userId);
3231
if (!user) {
3332
throw new UnauthorizedException('User not found');

‎apps/nestjs-backend/src/features/auth/utils.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { type IUserMeVo } from '@teable/openapi';
33
import { pick } from 'lodash';
44
import { getPublicFullStorageUrl } from '../attachments/plugins/utils';
55

6-
export const pickUserMe = (
7-
user: Pick<
8-
Prisma.UserGetPayload<null>,
9-
'id' | 'name' | 'avatar' | 'phone' | 'email' | 'password' | 'notifyMeta' | 'isAdmin'
10-
>
11-
): IUserMeVo => {
6+
export type IPickUserMe = Pick<
7+
Prisma.UserGetPayload<null>,
8+
'id' | 'name' | 'avatar' | 'phone' | 'email' | 'password' | 'notifyMeta' | 'isAdmin'
9+
>;
10+
11+
export const pickUserMe = (user: IPickUserMe): IUserMeVo => {
1212
return {
1313
...pick(user, 'id', 'name', 'phone', 'email', 'isAdmin'),
1414
notifyMeta: typeof user.notifyMeta === 'object' ? user.notifyMeta : JSON.parse(user.notifyMeta),

‎apps/nestjs-backend/src/features/collaborator/collaborator.service.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('CollaboratorService', () => {
8282
resourceType: CollaboratorType.Base,
8383
},
8484
});
85-
expect(prismaService.$executeRawUnsafe).toBeCalled();
85+
expect(prismaService.collaborator.createMany).toBeCalled();
8686
});
8787

8888
it('should throw error if exists', async () => {

‎apps/nestjs-backend/src/features/collaborator/collaborator.service.ts‎

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -84,22 +84,17 @@ export class CollaboratorService {
8484
},
8585
});
8686

87-
const query = this.knex
88-
.insert(
89-
collaborators.map((collaborator) => ({
90-
id: getRandomString(16),
91-
resource_id: spaceId,
92-
resource_type: CollaboratorType.Space,
93-
role_name: role,
94-
principal_id: collaborator.principalId,
95-
principal_type: collaborator.principalType,
96-
created_by: currentUserId!,
97-
}))
98-
)
99-
.into('collaborator')
100-
.toQuery();
101-
102-
await this.prismaService.txClient().$executeRawUnsafe(query);
87+
await this.prismaService.txClient().collaborator.createMany({
88+
data: collaborators.map((collaborator) => ({
89+
id: getRandomString(16),
90+
resourceId: spaceId,
91+
resourceType: CollaboratorType.Space,
92+
roleName: role,
93+
principalId: collaborator.principalId,
94+
principalType: collaborator.principalType,
95+
createdBy: currentUserId!,
96+
})),
97+
});
10398
this.eventEmitterService.emitAsync(
10499
Events.COLLABORATOR_CREATE,
105100
new CollaboratorCreateEvent(spaceId)
@@ -722,22 +717,17 @@ export class CollaboratorService {
722717
throw new BadRequestException('has already existed in base');
723718
}
724719

725-
const query = this.knex
726-
.insert(
727-
collaborators.map((collaborator) => ({
728-
id: getRandomString(16),
729-
resource_id: baseId,
730-
resource_type: CollaboratorType.Base,
731-
role_name: role,
732-
principal_id: collaborator.principalId,
733-
principal_type: collaborator.principalType,
734-
created_by: currentUserId!,
735-
}))
736-
)
737-
.into('collaborator')
738-
.toQuery();
739-
740-
const res = await this.prismaService.txClient().$executeRawUnsafe(query);
720+
const res = await this.prismaService.txClient().collaborator.createMany({
721+
data: collaborators.map((collaborator) => ({
722+
id: getRandomString(16),
723+
resourceId: baseId,
724+
resourceType: CollaboratorType.Base,
725+
roleName: role,
726+
principalId: collaborator.principalId,
727+
principalType: collaborator.principalType,
728+
createdBy: currentUserId!,
729+
})),
730+
});
741731
this.eventEmitterService.emitAsync(
742732
Events.COLLABORATOR_CREATE,
743733
new CollaboratorCreateEvent(base.spaceId)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { PrismaService } from '@teable/db-main-prisma';
3+
import { PerformanceCache, PerformanceCacheService } from '../../performance-cache';
4+
import { generateAccessTokenCacheKey } from '../../performance-cache/generate-keys';
5+
import { dateToIso } from '../../utils/date-to-iso';
6+
7+
@Injectable()
8+
export class AccessTokenModel {
9+
constructor(
10+
private readonly prismaService: PrismaService,
11+
protected readonly performanceCacheService: PerformanceCacheService
12+
) {}
13+
14+
@PerformanceCache({
15+
ttl: 60 * 5,
16+
keyGenerator: generateAccessTokenCacheKey,
17+
statsType: 'access-token',
18+
})
19+
async getAccessTokenRawById(id: string) {
20+
const res = await this.prismaService.txClient().accessToken.findUnique({
21+
where: { id },
22+
});
23+
if (!res) {
24+
return null;
25+
}
26+
return dateToIso(res);
27+
}
28+
}

0 commit comments

Comments
 (0)