-
-
Notifications
You must be signed in to change notification settings - Fork 677
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
82 changed files
with
4,199 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
apps/nestjs-backend/src/features/next/plugin/plugin-proxy.middleware.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// proxy.middleware.ts | ||
import type { NestMiddleware } from '@nestjs/common'; | ||
import type { Request, Response } from 'express'; | ||
import type { RequestHandler } from 'http-proxy-middleware'; | ||
import { createProxyMiddleware } from 'http-proxy-middleware'; | ||
import { BaseConfig, IBaseConfig } from '../../../configs/base.config'; | ||
|
||
export class PluginProxyMiddleware implements NestMiddleware { | ||
private proxy: RequestHandler; | ||
|
||
constructor(@BaseConfig() private readonly baseConfig: IBaseConfig) { | ||
this.proxy = createProxyMiddleware({ | ||
target: `http://127.0.0.1:${baseConfig.pluginServerPort}`, | ||
}); | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
async use(req: Request, res: Response, next: () => void): Promise<any> { | ||
this.proxy(req, res, next); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
apps/nestjs-backend/src/features/next/plugin/plugin-proxy.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { MiddlewareConsumer, NestModule } from '@nestjs/common'; | ||
import { Module, RequestMethod } from '@nestjs/common'; | ||
import { PluginProxyMiddleware } from './plugin-proxy.middleware'; | ||
@Module({ | ||
providers: [], | ||
imports: [], | ||
}) | ||
export class PluginProxyModule implements NestModule { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
configure(consumer: MiddlewareConsumer): any { | ||
consumer.apply(PluginProxyMiddleware).forRoutes({ | ||
method: RequestMethod.ALL, | ||
path: 'plugin/?*', | ||
}); | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
apps/nestjs-backend/src/features/next/plugin/plugin.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { PluginProxyModule } from './plugin-proxy.module'; | ||
@Module({ | ||
imports: [PluginProxyModule], | ||
providers: [], | ||
controllers: [], | ||
}) | ||
export class NextPluginModule {} |
191 changes: 191 additions & 0 deletions
191
apps/nestjs-backend/src/features/plugin/plugin-auth.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import { | ||
BadRequestException, | ||
Injectable, | ||
InternalServerErrorException, | ||
NotFoundException, | ||
} from '@nestjs/common'; | ||
import { JwtService } from '@nestjs/jwt'; | ||
import { getRandomString } from '@teable/core'; | ||
import { PrismaService } from '@teable/db-main-prisma'; | ||
import { | ||
PluginStatus, | ||
type IPluginGetTokenRo, | ||
type IPluginGetTokenVo, | ||
type IPluginRefreshTokenRo, | ||
type IPluginRefreshTokenVo, | ||
} from '@teable/openapi'; | ||
import { CacheService } from '../../cache/cache.service'; | ||
import { second } from '../../utils/second'; | ||
import { AccessTokenService } from '../access-token/access-token.service'; | ||
import { validateSecret } from './utils'; | ||
|
||
interface IRefreshPayload { | ||
pluginId: string; | ||
secret: string; | ||
accessTokenId: string; | ||
} | ||
|
||
@Injectable() | ||
export class PluginAuthService { | ||
accessTokenExpireIn = second('10m'); | ||
refreshTokenExpireIn = second('30d'); | ||
|
||
constructor( | ||
private readonly prismaService: PrismaService, | ||
private readonly cacheService: CacheService, | ||
private readonly accessTokenService: AccessTokenService, | ||
private readonly jwtService: JwtService | ||
) {} | ||
|
||
private generateAccessToken({ | ||
userId, | ||
scopes, | ||
clientId, | ||
name, | ||
baseId, | ||
}: { | ||
userId: string; | ||
scopes: string[]; | ||
clientId: string; | ||
name: string; | ||
baseId: string; | ||
}) { | ||
return this.accessTokenService.createAccessToken({ | ||
clientId, | ||
name: `plugin:${name}`, | ||
scopes, | ||
userId, | ||
baseIds: [baseId], | ||
// 10 minutes | ||
expiredTime: new Date(Date.now() + this.accessTokenExpireIn * 1000).toISOString(), | ||
}); | ||
} | ||
|
||
private async generateRefreshToken({ pluginId, secret, accessTokenId }: IRefreshPayload) { | ||
return this.jwtService.signAsync( | ||
{ | ||
secret, | ||
accessTokenId, | ||
pluginId, | ||
}, | ||
{ expiresIn: this.refreshTokenExpireIn } | ||
); | ||
} | ||
|
||
private async validateSecret(secret: string, pluginId: string) { | ||
const plugin = await this.prismaService.plugin | ||
.findFirstOrThrow({ | ||
where: { id: pluginId, status: PluginStatus.Published }, | ||
}) | ||
.catch(() => { | ||
throw new NotFoundException('Plugin not found'); | ||
}); | ||
if (!plugin.pluginUser) { | ||
throw new BadRequestException('Plugin user not found'); | ||
} | ||
const checkSecret = await validateSecret(secret, plugin.secret); | ||
if (!checkSecret) { | ||
throw new BadRequestException('Invalid secret'); | ||
} | ||
return { | ||
...plugin, | ||
pluginUser: plugin.pluginUser, | ||
}; | ||
} | ||
|
||
async token(pluginId: string, ro: IPluginGetTokenRo): Promise<IPluginGetTokenVo> { | ||
const { secret, scopes, baseId } = ro; | ||
const plugin = await this.validateSecret(secret, pluginId); | ||
|
||
const accessToken = await this.generateAccessToken({ | ||
userId: plugin.pluginUser, | ||
scopes, | ||
baseId, | ||
clientId: pluginId, | ||
name: plugin.name, | ||
}); | ||
|
||
const refreshToken = await this.generateRefreshToken({ | ||
pluginId, | ||
secret, | ||
accessTokenId: accessToken.id, | ||
}); | ||
|
||
return { | ||
accessToken: accessToken.token, | ||
refreshToken, | ||
scopes, | ||
expiresIn: this.accessTokenExpireIn, | ||
refreshExpiresIn: this.refreshTokenExpireIn, | ||
}; | ||
} | ||
|
||
async refreshToken(pluginId: string, ro: IPluginRefreshTokenRo): Promise<IPluginRefreshTokenVo> { | ||
const { secret, refreshToken } = ro; | ||
const plugin = await this.validateSecret(secret, pluginId); | ||
const payload = await this.jwtService.verifyAsync<IRefreshPayload>(refreshToken).catch(() => { | ||
// eslint-disable-next-line sonarjs/no-duplicate-string | ||
throw new BadRequestException('Invalid refresh token'); | ||
}); | ||
|
||
if ( | ||
payload.pluginId !== pluginId || | ||
payload.secret !== secret || | ||
payload.accessTokenId === undefined | ||
) { | ||
throw new BadRequestException('Invalid refresh token'); | ||
} | ||
return this.prismaService.$tx(async (prisma) => { | ||
const oldAccessToken = await prisma.accessToken | ||
.findFirstOrThrow({ | ||
where: { id: payload.accessTokenId }, | ||
}) | ||
.catch(() => { | ||
throw new BadRequestException('Invalid refresh token'); | ||
}); | ||
|
||
await prisma.accessToken.delete({ | ||
where: { id: payload.accessTokenId, userId: plugin.pluginUser }, | ||
}); | ||
|
||
const baseId = oldAccessToken.baseIds ? JSON.parse(oldAccessToken.baseIds)[0] : ''; | ||
const scopes = oldAccessToken.scopes ? JSON.parse(oldAccessToken.scopes) : []; | ||
if (!baseId) { | ||
throw new InternalServerErrorException('Anomalous token with no baseId'); | ||
} | ||
|
||
const accessToken = await this.generateAccessToken({ | ||
userId: plugin.pluginUser, | ||
scopes, | ||
baseId, | ||
clientId: pluginId, | ||
name: plugin.name, | ||
}); | ||
|
||
const refreshToken = await this.generateRefreshToken({ | ||
pluginId, | ||
secret, | ||
accessTokenId: accessToken.id, | ||
}); | ||
return { | ||
accessToken: accessToken.token, | ||
refreshToken, | ||
scopes, | ||
expiresIn: this.accessTokenExpireIn, | ||
refreshExpiresIn: this.refreshTokenExpireIn, | ||
}; | ||
}); | ||
} | ||
|
||
async authCode(pluginId: string, baseId: string) { | ||
const count = await this.prismaService.pluginInstall.count({ | ||
where: { pluginId, baseId }, | ||
}); | ||
if (count === 0) { | ||
throw new NotFoundException('Plugin not installed'); | ||
} | ||
const authCode = getRandomString(16); | ||
await this.cacheService.set(`plugin:auth-code:${authCode}`, { baseId, pluginId }, second('5m')); | ||
return authCode; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,27 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { JwtModule } from '@nestjs/jwt'; | ||
import { authConfig, type IAuthConfig } from '../../configs/auth.config'; | ||
import { AccessTokenModule } from '../access-token/access-token.module'; | ||
import { UserModule } from '../user/user.module'; | ||
import { PluginAuthService } from './plugin-auth.service'; | ||
import { PluginController } from './plugin.controller'; | ||
import { PluginService } from './plugin.service'; | ||
|
||
@Module({ | ||
imports: [UserModule], | ||
providers: [PluginService], | ||
imports: [ | ||
UserModule, | ||
AccessTokenModule, | ||
JwtModule.registerAsync({ | ||
useFactory: (config: IAuthConfig) => ({ | ||
secret: config.jwt.secret, | ||
signOptions: { | ||
expiresIn: config.jwt.expiresIn, | ||
}, | ||
}), | ||
inject: [authConfig.KEY], | ||
}), | ||
], | ||
providers: [PluginService, PluginAuthService], | ||
controllers: [PluginController], | ||
}) | ||
export class PluginModule {} |
Oops, something went wrong.