Skip to content

Commit 95595f5

Browse files
authored
Merge pull request #1849 from rocket-admin/backend-frontend_agent
Backend frontend agent
2 parents 8d22503 + ac93e20 commit 95595f5

20 files changed

Lines changed: 1337 additions & 1 deletion

backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { TableWidgetModule } from './entities/widget/table-widget.module.js';
4646
import { AgentsModule } from './microservices/agents-microservice/agents.module.js';
4747
import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas-gateway.module.js';
4848
import { SaasModule } from './microservices/saas-microservice/saas.module.js';
49+
import { SitenovaModule } from './microservices/sitenova-microservice/sitenova.module.js';
4950
import { AppLoggerMiddleware } from './middlewares/logging-middleware/app-logger-middlewate.js';
5051
import { SelfHostedOperationsModule } from './selfhosted-operations/selhosted-operations.module.js';
5152
import { ConfigModule } from './shared/config/config.module.js';
@@ -86,6 +87,7 @@ import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js';
8687
TableActionModule,
8788
SaasModule,
8889
AgentsModule,
90+
SitenovaModule,
8991
CompanyInfoModule,
9092
SaaSGatewayModule,
9193
TableTriggersModule,

backend/src/common/data-injection.tokens.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ export enum UseCaseType {
188188
AGENTS_SCAN_AND_CREATE_SETTINGS = 'AGENTS_SCAN_AND_CREATE_SETTINGS',
189189
AGENTS_GET_COMPANY_SUBSCRIPTION_INFO = 'AGENTS_GET_COMPANY_SUBSCRIPTION_INFO',
190190

191+
SITENOVA_EXECUTE_RAW_QUERY = 'SITENOVA_EXECUTE_RAW_QUERY',
192+
SITENOVA_REGISTER_ENDUSER = 'SITENOVA_REGISTER_ENDUSER',
193+
SITENOVA_LOGIN_ENDUSER = 'SITENOVA_LOGIN_ENDUSER',
194+
191195
CREATE_TABLE_FILTERS = 'CREATE_TABLE_FILTERS',
192196
FIND_TABLE_FILTERS = 'FIND_TABLE_FILTERS',
193197
DELETE_TABLE_FILTERS = 'DELETE_TABLE_FILTERS',
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class SitenovaRawQueryResultRO {
4+
@ApiProperty({
5+
description: 'Raw result returned by the connected database for the executed statement.',
6+
})
7+
result: unknown;
8+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Audience claim that marks a token as a SiteNova generated-site end-user token (a site visitor),
2+
// keeping it cryptographically and logically separate from RocketAdmin platform user tokens.
3+
export const SITENOVA_ENDUSER_AUDIENCE = 'sitenova:enduser';
4+
5+
// Token lifetime for generated-site visitors.
6+
export const SITENOVA_ENDUSER_TOKEN_TTL = '7d';
7+
8+
// JWT payload issued to a generated-site end-user on register/login.
9+
export interface SitenovaEndUserTokenPayload {
10+
sub: string; // the user's identifier in the connection's users table (email by default)
11+
cid: string; // connectionId the token is bound to
12+
aud: string; // SITENOVA_ENDUSER_AUDIENCE
13+
}
14+
15+
// Input DS for the register/login use cases.
16+
export class SitenovaRegisterEndUserDs {
17+
connectionId: string;
18+
tableName: string;
19+
email: string;
20+
password: string;
21+
emailField: string;
22+
passwordField: string;
23+
extra: Record<string, unknown>;
24+
}
25+
26+
export class SitenovaLoginEndUserDs {
27+
connectionId: string;
28+
tableName: string;
29+
email: string;
30+
password: string;
31+
emailField: string;
32+
passwordField: string;
33+
}
34+
35+
// Result of register/login: an end-user token plus the public (password-stripped) user row.
36+
export class SitenovaEndUserAuthResultDs {
37+
token: string;
38+
user: Record<string, unknown>;
39+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class SitenovaExecuteRawQueryDs {
2+
connectionId: string;
3+
userId: string;
4+
masterPassword: string | null;
5+
query: string;
6+
tableName: string | null;
7+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import { IsInt, IsNotEmpty, IsObject, IsOptional, IsString, MinLength } from 'class-validator';
3+
4+
class SitenovaAuthBaseDto {
5+
@ApiProperty({ description: 'Name of the users/auth table in the connected database.' })
6+
@IsString()
7+
@IsNotEmpty()
8+
tableName: string;
9+
10+
@ApiProperty({ description: 'End-user identifier (matched against the email column).' })
11+
@IsString()
12+
@IsNotEmpty()
13+
email: string;
14+
15+
@ApiPropertyOptional({ description: 'Override the email column name (default: "email").' })
16+
@IsOptional()
17+
@IsString()
18+
emailField?: string;
19+
20+
@ApiPropertyOptional({ description: 'Override the password column name (default: "password").' })
21+
@IsOptional()
22+
@IsString()
23+
passwordField?: string;
24+
}
25+
26+
export class SitenovaRegisterDto extends SitenovaAuthBaseDto {
27+
@ApiProperty({ description: 'End-user password (stored hashed; min 6 chars).' })
28+
@IsString()
29+
@MinLength(6)
30+
password: string;
31+
32+
@ApiPropertyOptional({ type: Object, description: 'Additional columns to set on the new user row.' })
33+
@IsOptional()
34+
@IsObject()
35+
extra?: Record<string, unknown>;
36+
}
37+
38+
export class SitenovaLoginDto extends SitenovaAuthBaseDto {
39+
@ApiProperty({ description: 'End-user password.' })
40+
@IsString()
41+
@IsNotEmpty()
42+
password: string;
43+
}
44+
45+
export class SitenovaSiteCreateRowDto {
46+
@ApiProperty()
47+
@IsString()
48+
@IsNotEmpty()
49+
tableName: string;
50+
51+
@ApiProperty({ type: Object })
52+
@IsObject()
53+
row: Record<string, unknown>;
54+
}
55+
56+
export class SitenovaSiteGetRowsDto {
57+
@ApiProperty()
58+
@IsString()
59+
@IsNotEmpty()
60+
tableName: string;
61+
62+
@ApiPropertyOptional()
63+
@IsOptional()
64+
@IsInt()
65+
page?: number;
66+
67+
@ApiPropertyOptional()
68+
@IsOptional()
69+
@IsInt()
70+
perPage?: number;
71+
72+
@ApiPropertyOptional()
73+
@IsOptional()
74+
@IsString()
75+
search?: string;
76+
77+
@ApiPropertyOptional({ type: Object })
78+
@IsOptional()
79+
@IsObject()
80+
filters?: Record<string, unknown>;
81+
}
82+
83+
export class SitenovaSiteRowByPrimaryKeyDto {
84+
@ApiProperty()
85+
@IsString()
86+
@IsNotEmpty()
87+
tableName: string;
88+
89+
@ApiProperty({ type: Object })
90+
@IsObject()
91+
primaryKey: Record<string, unknown>;
92+
}
93+
94+
export class SitenovaSiteUpdateRowDto extends SitenovaSiteRowByPrimaryKeyDto {
95+
@ApiProperty({ type: Object })
96+
@IsObject()
97+
row: Record<string, unknown>;
98+
}
99+
100+
export class SitenovaEndUserAuthResponseDto {
101+
@ApiProperty({ description: 'End-user JWT to send as Bearer on write requests.' })
102+
token: string;
103+
104+
@ApiProperty({ type: Object, description: 'The authenticated user row (password field stripped).' })
105+
user: Record<string, unknown>;
106+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
3+
4+
export class SitenovaBaseDto {
5+
@ApiProperty({ description: 'Id of the RocketAdmin user the operation is performed on behalf of.' })
6+
@IsString()
7+
@IsNotEmpty()
8+
@IsUUID()
9+
userId: string;
10+
11+
@ApiPropertyOptional({ description: 'Master password for connections stored with encryption.' })
12+
@IsOptional()
13+
@IsString()
14+
masterPassword?: string | null;
15+
}
16+
17+
export class SitenovaExecuteRawQueryDto extends SitenovaBaseDto {
18+
@ApiProperty({ description: 'Raw SQL statement to execute (DDL/DML allowed).' })
19+
@IsString()
20+
@IsNotEmpty()
21+
query: string;
22+
23+
@ApiPropertyOptional({
24+
description: 'Optional table/collection name. Required only by engines whose raw-query API is table-scoped.',
25+
})
26+
@IsOptional()
27+
@IsString()
28+
tableName?: string | null;
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
2+
import { Request } from 'express';
3+
import { extractTokenFromHeader } from '../../../authorization/utils/extract-token-from-header.js';
4+
import { Messages } from '../../../exceptions/text/messages.js';
5+
import { SitenovaEndUserTokenPayload } from '../data-structures/sitenova-site.ds.js';
6+
import { SitenovaEndUserAuthService } from '../services/sitenova-enduser-auth.service.js';
7+
8+
export interface RequestWithSitenovaEndUser extends Request {
9+
sitenovaEndUser?: SitenovaEndUserTokenPayload;
10+
}
11+
12+
// Gates write operations on the generated-site data API. The site visitor must present a valid
13+
// end-user JWT (issued by login/register) bound to the connection in the route. The connected DB
14+
// user's privileges remain the outer bound; the trusted `tableName` allow-listing is deferred.
15+
@Injectable()
16+
export class SitenovaEndUserAuthGuard implements CanActivate {
17+
constructor(private readonly endUserAuthService: SitenovaEndUserAuthService) {}
18+
19+
async canActivate(context: ExecutionContext): Promise<boolean> {
20+
const request = context.switchToHttp().getRequest<RequestWithSitenovaEndUser>();
21+
const connectionId = request.params?.connectionId as string | undefined;
22+
const token = extractTokenFromHeader(request);
23+
if (!connectionId || !token) {
24+
throw new UnauthorizedException(Messages.AUTHORIZATION_REJECTED);
25+
}
26+
const decoded = await this.endUserAuthService.verifyEndUserToken(connectionId, token);
27+
if (!decoded) {
28+
throw new UnauthorizedException(Messages.AUTHORIZATION_REJECTED);
29+
}
30+
request.sitenovaEndUser = decoded;
31+
return true;
32+
}
33+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { BadRequestException, CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
2+
import { Request } from 'express';
3+
import { CedarAction, PUBLIC_USER_ID } from '../../../entities/cedar-authorization/cedar-action-map.js';
4+
import { CedarAuthorizationService } from '../../../entities/cedar-authorization/cedar-authorization.service.js';
5+
import { Messages } from '../../../exceptions/text/messages.js';
6+
7+
// Gates read operations on the generated-site data API. Reads are allowed only when the connection
8+
// has a public policy that grants table:query on the requested table — the same public-permissions
9+
// mechanism the existing /table/crud routes use (see QueryTableGuard). The visitor is anonymous
10+
// here; column visibility follows the connection's public-read policy downstream.
11+
@Injectable()
12+
export class SitenovaPublicReadGuard implements CanActivate {
13+
constructor(private readonly cedarAuthService: CedarAuthorizationService) {}
14+
15+
async canActivate(context: ExecutionContext): Promise<boolean> {
16+
const request = context.switchToHttp().getRequest<Request>();
17+
const connectionId = request.params?.connectionId as string | undefined;
18+
const tableName = request.body?.tableName as string | undefined;
19+
if (!connectionId) {
20+
throw new BadRequestException(Messages.CONNECTION_ID_MISSING);
21+
}
22+
if (!tableName) {
23+
throw new BadRequestException(Messages.TABLE_NAME_MISSING);
24+
}
25+
26+
const publicEnabled = await this.cedarAuthService.isPublicAccessEnabled(connectionId);
27+
if (!publicEnabled) {
28+
throw new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS);
29+
}
30+
const allowed = await this.cedarAuthService.validate({
31+
userId: PUBLIC_USER_ID,
32+
action: CedarAction.TableQuery,
33+
connectionId,
34+
tableName,
35+
publicAccess: true,
36+
});
37+
if (!allowed) {
38+
throw new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS);
39+
}
40+
return true;
41+
}
42+
}

0 commit comments

Comments
 (0)