ta_data_mas 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/apps/api/.prettierrc +4 -0
- package/apps/api/README.md +98 -0
- package/apps/api/drizzle/0000_right_drax.sql +87 -0
- package/apps/api/drizzle/meta/0000_snapshot.json +588 -0
- package/apps/api/drizzle/meta/_journal.json +13 -0
- package/apps/api/drizzle.config.ts +12 -0
- package/apps/api/eslint.config.mjs +35 -0
- package/apps/api/nest-cli.json +8 -0
- package/apps/api/package.json +99 -0
- package/apps/api/src/app.controller.spec.ts +25 -0
- package/apps/api/src/app.controller.ts +9 -0
- package/apps/api/src/app.module.ts +37 -0
- package/apps/api/src/app.service.ts +8 -0
- package/apps/api/src/auth/auth.controller.ts +113 -0
- package/apps/api/src/auth/auth.module.ts +15 -0
- package/apps/api/src/auth/auth.service.ts +309 -0
- package/apps/api/src/auth/decorators/current-user-decorator.ts +10 -0
- package/apps/api/src/auth/decorators/require-org-role-decorator.ts +7 -0
- package/apps/api/src/auth/dto/login.dto.ts +10 -0
- package/apps/api/src/auth/dto/register.dto.ts +14 -0
- package/apps/api/src/auth/guards/jwt-auth-guard.ts +40 -0
- package/apps/api/src/auth/guards/org-role.guards.ts +60 -0
- package/apps/api/src/db/db.module.ts +9 -0
- package/apps/api/src/db/drizzle.service.ts +16 -0
- package/apps/api/src/db/schema/index.ts +6 -0
- package/apps/api/src/db/schema/organizations.ts +30 -0
- package/apps/api/src/db/schema/projects.ts +26 -0
- package/apps/api/src/db/schema/query-history.ts +16 -0
- package/apps/api/src/db/schema/storage-buckets.ts +17 -0
- package/apps/api/src/db/schema/storage-objects.ts +18 -0
- package/apps/api/src/db/schema/users.ts +14 -0
- package/apps/api/src/main.ts +51 -0
- package/apps/api/src/members/dto/invite-member-dto.ts +6 -0
- package/apps/api/src/members/dto/update-role.dto.ts +8 -0
- package/apps/api/src/members/invite.service.ts +147 -0
- package/apps/api/src/members/members.controller.ts +56 -0
- package/apps/api/src/members/members.module.ts +13 -0
- package/apps/api/src/members/members.service.ts +85 -0
- package/apps/api/src/orgs/dto/create-org.dto.ts +8 -0
- package/apps/api/src/orgs/orgs.controller.ts +27 -0
- package/apps/api/src/orgs/orgs.module.ts +12 -0
- package/apps/api/src/orgs/orgs.service.ts +86 -0
- package/apps/api/src/project-api/project-api.controller.ts +95 -0
- package/apps/api/src/project-api/project-api.module.ts +12 -0
- package/apps/api/src/project-api/project-api.service.ts +196 -0
- package/apps/api/src/project-api/project-key.guard.ts +45 -0
- package/apps/api/src/project-api/query-parser.ts +129 -0
- package/apps/api/src/project-auth/dto/magic-link.dto.ts +7 -0
- package/apps/api/src/project-auth/dto/signin.dto.ts +10 -0
- package/apps/api/src/project-auth/dto/signup.dto.ts +11 -0
- package/apps/api/src/project-auth/project-auth-dashboard.controller.ts +35 -0
- package/apps/api/src/project-auth/project-auth.controller.ts +111 -0
- package/apps/api/src/project-auth/project-auth.module.ts +13 -0
- package/apps/api/src/project-auth/project-auth.service.ts +440 -0
- package/apps/api/src/projects/dto/create-project.dto.ts +9 -0
- package/apps/api/src/projects/projects.controller.ts +34 -0
- package/apps/api/src/projects/projects.module.ts +12 -0
- package/apps/api/src/projects/projects.service.ts +136 -0
- package/apps/api/src/realtime/realtime-socket.type.ts +14 -0
- package/apps/api/src/realtime/realtime.controller.ts +63 -0
- package/apps/api/src/realtime/realtime.gateway.ts +150 -0
- package/apps/api/src/realtime/realtime.module.ts +14 -0
- package/apps/api/src/realtime/realtime.service.ts +101 -0
- package/apps/api/src/realtime/trigger.service.ts +95 -0
- package/apps/api/src/sql-editor/dto/execute-query.dto.ts +7 -0
- package/apps/api/src/sql-editor/sql-editor.controller.ts +28 -0
- package/apps/api/src/sql-editor/sql-editor.module.ts +12 -0
- package/apps/api/src/sql-editor/sql-editor.service.ts +154 -0
- package/apps/api/src/storage/project-storage.controller.ts +140 -0
- package/apps/api/src/storage/storage.controller.ts +118 -0
- package/apps/api/src/storage/storage.module.ts +15 -0
- package/apps/api/src/storage/storage.service.ts +191 -0
- package/apps/api/src/storage/uploadthing.ts +23 -0
- package/apps/api/src/table-editor/dto/alter-table.dto.ts +15 -0
- package/apps/api/src/table-editor/dto/create-table.dto.ts +54 -0
- package/apps/api/src/table-editor/table-editor.controller.ts +117 -0
- package/apps/api/src/table-editor/table-editor.module.ts +12 -0
- package/apps/api/src/table-editor/table-editor.service.ts +366 -0
- package/apps/api/test/app.e2e-spec.ts +29 -0
- package/apps/api/test/jest-e2e.json +9 -0
- package/apps/api/tsconfig.build.json +4 -0
- package/apps/api/tsconfig.json +25 -0
- package/apps/web/.env.example +12 -0
- package/apps/web/AGENTS.md +9 -0
- package/apps/web/CLAUDE.md +1 -0
- package/apps/web/README.md +36 -0
- package/apps/web/components.json +25 -0
- package/apps/web/eslint.config.mjs +18 -0
- package/apps/web/next.config.ts +11 -0
- package/apps/web/package.json +48 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/logo.svg +4 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/(dashboard)/layout.tsx +33 -0
- package/apps/web/src/app/(dashboard)/organizations/(list)/layout.tsx +7 -0
- package/apps/web/src/app/(dashboard)/organizations/(list)/new/page.tsx +26 -0
- package/apps/web/src/app/(dashboard)/organizations/(list)/page.tsx +44 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/api/page.tsx +13 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/layout.tsx +19 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/page.tsx +10 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/providers/page.tsx +19 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/url-configuration/page.tsx +24 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/users/page.tsx +13 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/database/page.tsx +29 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/realtime/page.tsx +28 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/sql/page.tsx +21 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/storage/page.tsx +28 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/layout.tsx +20 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/projects/page.tsx +62 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/members/page.tsx +28 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/page.tsx +10 -0
- package/apps/web/src/app/(dashboard)/settings/page.tsx +12 -0
- package/apps/web/src/app/dashboard/page.tsx +14 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/globals.css +115 -0
- package/apps/web/src/app/layout.tsx +36 -0
- package/apps/web/src/app/login/page.tsx +199 -0
- package/apps/web/src/app/page.tsx +100 -0
- package/apps/web/src/app/register/page.tsx +218 -0
- package/apps/web/src/components/app-sidebar.tsx +301 -0
- package/apps/web/src/components/navbar.tsx +63 -0
- package/apps/web/src/components/topNav.tsx +68 -0
- package/apps/web/src/components/ui/avatar.tsx +112 -0
- package/apps/web/src/components/ui/badge.tsx +48 -0
- package/apps/web/src/components/ui/button.tsx +66 -0
- package/apps/web/src/components/ui/card.tsx +102 -0
- package/apps/web/src/components/ui/checkbox.tsx +32 -0
- package/apps/web/src/components/ui/dialog.tsx +168 -0
- package/apps/web/src/components/ui/dropdown-menu.tsx +269 -0
- package/apps/web/src/components/ui/field.tsx +238 -0
- package/apps/web/src/components/ui/input.tsx +19 -0
- package/apps/web/src/components/ui/label.tsx +24 -0
- package/apps/web/src/components/ui/resizable.tsx +50 -0
- package/apps/web/src/components/ui/scroll-area.tsx +55 -0
- package/apps/web/src/components/ui/select.tsx +192 -0
- package/apps/web/src/components/ui/separator.tsx +28 -0
- package/apps/web/src/components/ui/sheet.tsx +147 -0
- package/apps/web/src/components/ui/sidebar.tsx +703 -0
- package/apps/web/src/components/ui/skeleton.tsx +13 -0
- package/apps/web/src/components/ui/switch.tsx +33 -0
- package/apps/web/src/components/ui/table.tsx +116 -0
- package/apps/web/src/components/ui/tooltip.tsx +57 -0
- package/apps/web/src/features/api-docs/api-docs-client.tsx +227 -0
- package/apps/web/src/features/api-docs/api-docs-helpers.server.ts +86 -0
- package/apps/web/src/features/auth/action.ts +67 -0
- package/apps/web/src/features/auth/client.schema.ts +13 -0
- package/apps/web/src/features/auth/constants.ts +6 -0
- package/apps/web/src/features/auth/cookies.ts +86 -0
- package/apps/web/src/features/auth/server.schema.ts +11 -0
- package/apps/web/src/features/auth/sign-out.ts +17 -0
- package/apps/web/src/features/members/action.ts +66 -0
- package/apps/web/src/features/members/client.schema.ts +11 -0
- package/apps/web/src/features/members/constants.ts +8 -0
- package/apps/web/src/features/members/members-client.tsx +198 -0
- package/apps/web/src/features/members/members-helpers.server.ts +21 -0
- package/apps/web/src/features/members/server.schema.ts +19 -0
- package/apps/web/src/features/organization/action.ts +45 -0
- package/apps/web/src/features/organization/client.schema.ts +5 -0
- package/apps/web/src/features/organization/constants.tsx +3 -0
- package/apps/web/src/features/organization/create-org-form.tsx +66 -0
- package/apps/web/src/features/organization/organization-helpers.server.ts +35 -0
- package/apps/web/src/features/organization/server.schema.ts +10 -0
- package/apps/web/src/features/project-auth/project-auth-action.ts +78 -0
- package/apps/web/src/features/project-auth/project-auth-client.schema.ts +18 -0
- package/apps/web/src/features/project-auth/project-auth-constants.ts +8 -0
- package/apps/web/src/features/project-auth/project-auth-helpers.server.ts +39 -0
- package/apps/web/src/features/project-auth/project-auth-providers.tsx +222 -0
- package/apps/web/src/features/project-auth/project-auth-server.schema.ts +22 -0
- package/apps/web/src/features/project-auth/project-auth-shell.tsx +106 -0
- package/apps/web/src/features/project-auth/project-auth-url-config.tsx +151 -0
- package/apps/web/src/features/project-auth/project-auth-users.tsx +138 -0
- package/apps/web/src/features/projects/action.ts +46 -0
- package/apps/web/src/features/projects/client.schema.ts +9 -0
- package/apps/web/src/features/projects/constants.ts +6 -0
- package/apps/web/src/features/projects/create-project-modal.tsx +117 -0
- package/apps/web/src/features/projects/project-helpers.server.ts +38 -0
- package/apps/web/src/features/projects/server.schema.ts +10 -0
- package/apps/web/src/features/realtime/realtime-client.tsx +294 -0
- package/apps/web/src/features/realtime/realtime-helpers.server.ts +40 -0
- package/apps/web/src/features/sql-editor/sql-editor-client.tsx +324 -0
- package/apps/web/src/features/sql-editor/sql-editor-helpers.server.ts +22 -0
- package/apps/web/src/features/storage/storage-client.tsx +500 -0
- package/apps/web/src/features/storage/storage-helpers.server.ts +36 -0
- package/apps/web/src/features/table-editor/action.ts +97 -0
- package/apps/web/src/features/table-editor/add-column-dialog.tsx +184 -0
- package/apps/web/src/features/table-editor/client.schema.ts +36 -0
- package/apps/web/src/features/table-editor/create-table-drawer.tsx +542 -0
- package/apps/web/src/features/table-editor/server.schema.ts +41 -0
- package/apps/web/src/features/table-editor/table-editor-client.tsx +737 -0
- package/apps/web/src/features/table-editor/table-editor-helpers.server.ts +39 -0
- package/apps/web/src/hooks/use-mobile.ts +19 -0
- package/apps/web/src/lib/axios.ts +8 -0
- package/apps/web/src/lib/utils.ts +6 -0
- package/apps/web/src/proxy.ts +72 -0
- package/apps/web/src/server-utils/utils.ts +10 -0
- package/apps/web/tsconfig.json +34 -0
- package/package.json +22 -0
- package/packages/apiDatabase-js/package.json +39 -0
- package/packages/apiDatabase-js/src/auth/index.ts +121 -0
- package/packages/apiDatabase-js/src/client.ts +25 -0
- package/packages/apiDatabase-js/src/db/index.ts +15 -0
- package/packages/apiDatabase-js/src/db/query-builder.ts +194 -0
- package/packages/apiDatabase-js/src/index.ts +16 -0
- package/packages/apiDatabase-js/src/realtime/index.ts +84 -0
- package/packages/apiDatabase-js/src/storage/index.ts +130 -0
- package/packages/apiDatabase-js/tsconfig.json +13 -0
- package/packages/apiDatabase-js/tsdown.config.ts +20 -0
- package/packages/constants/package.json +6 -0
- package/packages/constants/src/index.ts +76 -0
- package/packages/tsconfig.json +13 -0
- package/packages/tsdown.config.ts +20 -0
- package/packages/types/package.json +6 -0
- package/packages/types/src/index.ts +254 -0
- package/pnpm-workspace.yaml +10 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { OrgsService } from './orgs.service';
|
|
3
|
+
import { OrgsController } from './orgs.controller';
|
|
4
|
+
import { AuthModule } from '../auth/auth.module';
|
|
5
|
+
|
|
6
|
+
@Module({
|
|
7
|
+
imports: [AuthModule],
|
|
8
|
+
providers: [OrgsService],
|
|
9
|
+
controllers: [OrgsController],
|
|
10
|
+
exports: [OrgsService],
|
|
11
|
+
})
|
|
12
|
+
export class OrgsModule {}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import { and, eq, isNull, sql } from 'drizzle-orm';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import slugify from 'slugify';
|
|
5
|
+
import { DrizzleService } from '../db/drizzle.service';
|
|
6
|
+
import { organizations, orgMembers, projects } from '../db/schema';
|
|
7
|
+
import { CreateOrgDto } from './dto/create-org.dto';
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class OrgsService {
|
|
11
|
+
constructor(private drizzle: DrizzleService) {}
|
|
12
|
+
|
|
13
|
+
private generateOrgSlug(name: string): string {
|
|
14
|
+
const base = slugify(`${name}-org`, { lower: true, strict: true });
|
|
15
|
+
const suffix = randomBytes(3).toString('hex');
|
|
16
|
+
return `${base}-${suffix}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getMyOrgs(userId: string) {
|
|
20
|
+
return this.drizzle.db
|
|
21
|
+
.select({
|
|
22
|
+
id: organizations.id,
|
|
23
|
+
name: organizations.name,
|
|
24
|
+
slug: organizations.slug,
|
|
25
|
+
createdAt: organizations.createdAt,
|
|
26
|
+
updatedAt: organizations.updatedAt,
|
|
27
|
+
role: orgMembers.role,
|
|
28
|
+
projectCount: sql<number>`cast(count(distinct ${projects.id}) as int)`,
|
|
29
|
+
memberCount: sql<number>`(
|
|
30
|
+
select cast(count(*) as int)
|
|
31
|
+
from org_members om
|
|
32
|
+
where om.org_id = ${organizations.id}
|
|
33
|
+
and om.removed_at is null
|
|
34
|
+
)`,
|
|
35
|
+
})
|
|
36
|
+
.from(orgMembers)
|
|
37
|
+
.innerJoin(organizations, eq(orgMembers.orgId, organizations.id))
|
|
38
|
+
.leftJoin(projects, eq(projects.orgId, organizations.id))
|
|
39
|
+
.where(and(eq(orgMembers.userId, userId), isNull(orgMembers.removedAt)))
|
|
40
|
+
.groupBy(organizations.id, orgMembers.role);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getOrgBySlug(slug: string, userId: string) {
|
|
44
|
+
const [row] = await this.drizzle.db
|
|
45
|
+
.select({
|
|
46
|
+
id: organizations.id,
|
|
47
|
+
name: organizations.name,
|
|
48
|
+
slug: organizations.slug,
|
|
49
|
+
createdAt: organizations.createdAt,
|
|
50
|
+
updatedAt: organizations.updatedAt,
|
|
51
|
+
role: orgMembers.role,
|
|
52
|
+
})
|
|
53
|
+
.from(organizations)
|
|
54
|
+
.innerJoin(
|
|
55
|
+
orgMembers,
|
|
56
|
+
and(
|
|
57
|
+
eq(orgMembers.orgId, organizations.id),
|
|
58
|
+
eq(orgMembers.userId, userId),
|
|
59
|
+
isNull(orgMembers.removedAt),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
.where(eq(organizations.slug, slug))
|
|
63
|
+
.limit(1);
|
|
64
|
+
|
|
65
|
+
if (!row) throw new NotFoundException('Organization not found');
|
|
66
|
+
return row;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async createOrg(dto: CreateOrgDto, userId: string) {
|
|
70
|
+
const [org] = await this.drizzle.db
|
|
71
|
+
.insert(organizations)
|
|
72
|
+
.values({
|
|
73
|
+
name: dto.name,
|
|
74
|
+
slug: this.generateOrgSlug(dto.name),
|
|
75
|
+
})
|
|
76
|
+
.returning();
|
|
77
|
+
|
|
78
|
+
await this.drizzle.db.insert(orgMembers).values({
|
|
79
|
+
orgId: org.id,
|
|
80
|
+
userId,
|
|
81
|
+
role: 'admin',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return org;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Delete,
|
|
5
|
+
ForbiddenException,
|
|
6
|
+
Get,
|
|
7
|
+
Param,
|
|
8
|
+
Patch,
|
|
9
|
+
Post,
|
|
10
|
+
Query,
|
|
11
|
+
Req,
|
|
12
|
+
UseGuards,
|
|
13
|
+
} from '@nestjs/common';
|
|
14
|
+
import type { Request as ExpressRequest } from 'express';
|
|
15
|
+
import { PROJECT_KEY_ROLES } from '@apiDatabase/constants';
|
|
16
|
+
import { ProjectApiService } from './project-api.service';
|
|
17
|
+
import { ProjectKeyGuard, ProjectKeyPayload } from './project-key.guard';
|
|
18
|
+
|
|
19
|
+
@Controller('projects/:projectSlug/rest')
|
|
20
|
+
@UseGuards(ProjectKeyGuard)
|
|
21
|
+
export class ProjectApiController {
|
|
22
|
+
constructor(private projectApiService: ProjectApiService) {}
|
|
23
|
+
|
|
24
|
+
private getProjectKey(req: ExpressRequest): ProjectKeyPayload {
|
|
25
|
+
return req['projectKey'] as ProjectKeyPayload;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private assertWriteAccess(req: ExpressRequest): void {
|
|
29
|
+
const { role } = this.getProjectKey(req);
|
|
30
|
+
if (role !== PROJECT_KEY_ROLES.SERVICE_ROLE) {
|
|
31
|
+
throw new ForbiddenException(
|
|
32
|
+
'Write operations require the service role key',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Get(':table')
|
|
38
|
+
getRows(
|
|
39
|
+
@Req() req: ExpressRequest,
|
|
40
|
+
@Param('projectSlug') projectSlug: string,
|
|
41
|
+
@Param('table') table: string,
|
|
42
|
+
@Query() query: Record<string, string>,
|
|
43
|
+
) {
|
|
44
|
+
const { projectId } = this.getProjectKey(req);
|
|
45
|
+
return this.projectApiService.getRows(projectId, projectSlug, table, query);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Post(':table')
|
|
49
|
+
insertRow(
|
|
50
|
+
@Req() req: ExpressRequest,
|
|
51
|
+
@Param('projectSlug') projectSlug: string,
|
|
52
|
+
@Param('table') table: string,
|
|
53
|
+
@Body() body: Record<string, unknown>,
|
|
54
|
+
) {
|
|
55
|
+
this.assertWriteAccess(req);
|
|
56
|
+
const { projectId } = this.getProjectKey(req);
|
|
57
|
+
return this.projectApiService.insertRow(
|
|
58
|
+
projectId,
|
|
59
|
+
projectSlug,
|
|
60
|
+
table,
|
|
61
|
+
body,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Patch(':table/:id')
|
|
66
|
+
updateRow(
|
|
67
|
+
@Req() req: ExpressRequest,
|
|
68
|
+
@Param('projectSlug') projectSlug: string,
|
|
69
|
+
@Param('table') table: string,
|
|
70
|
+
@Param('id') id: string,
|
|
71
|
+
@Body() body: Record<string, unknown>,
|
|
72
|
+
) {
|
|
73
|
+
this.assertWriteAccess(req);
|
|
74
|
+
const { projectId } = this.getProjectKey(req);
|
|
75
|
+
return this.projectApiService.updateRow(
|
|
76
|
+
projectId,
|
|
77
|
+
projectSlug,
|
|
78
|
+
table,
|
|
79
|
+
id,
|
|
80
|
+
body,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Delete(':table/:id')
|
|
85
|
+
deleteRow(
|
|
86
|
+
@Req() req: ExpressRequest,
|
|
87
|
+
@Param('projectSlug') projectSlug: string,
|
|
88
|
+
@Param('table') table: string,
|
|
89
|
+
@Param('id') id: string,
|
|
90
|
+
) {
|
|
91
|
+
this.assertWriteAccess(req);
|
|
92
|
+
const { projectId } = this.getProjectKey(req);
|
|
93
|
+
return this.projectApiService.deleteRow(projectId, projectSlug, table, id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
3
|
+
import { ProjectApiService } from './project-api.service';
|
|
4
|
+
import { ProjectApiController } from './project-api.controller';
|
|
5
|
+
import { ProjectKeyGuard } from './project-key.guard';
|
|
6
|
+
|
|
7
|
+
@Module({
|
|
8
|
+
imports: [JwtModule.register({})],
|
|
9
|
+
providers: [ProjectApiService, ProjectKeyGuard],
|
|
10
|
+
controllers: [ProjectApiController],
|
|
11
|
+
})
|
|
12
|
+
export class ProjectApiModule {}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
ForbiddenException,
|
|
4
|
+
Injectable,
|
|
5
|
+
NotFoundException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { eq } from 'drizzle-orm';
|
|
8
|
+
import { DrizzleService } from '../db/drizzle.service';
|
|
9
|
+
import { projects } from '../db/schema';
|
|
10
|
+
import { parseQueryParams, buildWhereClause } from './query-parser';
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class ProjectApiService {
|
|
14
|
+
constructor(private drizzle: DrizzleService) {}
|
|
15
|
+
|
|
16
|
+
private assertSafeIdentifier(name: string, label: string): void {
|
|
17
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
18
|
+
throw new BadRequestException(`Invalid ${label}: ${name}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private formatLiteral(value: unknown): string {
|
|
23
|
+
if (value === null || value === undefined) return 'NULL';
|
|
24
|
+
if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';
|
|
25
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
26
|
+
return String(value);
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'object') {
|
|
32
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'::jsonb`;
|
|
33
|
+
}
|
|
34
|
+
throw new BadRequestException('Unsupported column value');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async resolveProjectSchema(
|
|
38
|
+
projectId: string,
|
|
39
|
+
projectSlug: string,
|
|
40
|
+
): Promise<string> {
|
|
41
|
+
const [project] = await this.drizzle.db
|
|
42
|
+
.select({ dbSchema: projects.dbSchema, slug: projects.slug })
|
|
43
|
+
.from(projects)
|
|
44
|
+
.where(eq(projects.id, projectId))
|
|
45
|
+
.limit(1);
|
|
46
|
+
|
|
47
|
+
if (!project) throw new NotFoundException('Project not found');
|
|
48
|
+
|
|
49
|
+
if (project.slug !== projectSlug) {
|
|
50
|
+
throw new ForbiddenException('API key does not match this project URL');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return project.dbSchema;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async getPrimaryKeyColumn(
|
|
57
|
+
schema: string,
|
|
58
|
+
tableName: string,
|
|
59
|
+
): Promise<string> {
|
|
60
|
+
const result = await this.drizzle.db.execute<{ column_name: string }>(
|
|
61
|
+
`SELECT kcu.column_name
|
|
62
|
+
FROM information_schema.table_constraints tc
|
|
63
|
+
JOIN information_schema.key_column_usage kcu
|
|
64
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
65
|
+
AND tc.table_schema = kcu.table_schema
|
|
66
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
67
|
+
AND tc.table_schema = '${schema}'
|
|
68
|
+
AND tc.table_name = '${tableName}'
|
|
69
|
+
ORDER BY kcu.ordinal_position ASC
|
|
70
|
+
LIMIT 1`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const pk = result.rows[0]?.column_name;
|
|
74
|
+
if (!pk) {
|
|
75
|
+
throw new BadRequestException(`Table "${tableName}" has no primary key`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return pk;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getRows(
|
|
82
|
+
projectId: string,
|
|
83
|
+
projectSlug: string,
|
|
84
|
+
tableName: string,
|
|
85
|
+
rawParams: Record<string, string>,
|
|
86
|
+
) {
|
|
87
|
+
this.assertSafeIdentifier(tableName, 'table name');
|
|
88
|
+
const schema = await this.resolveProjectSchema(projectId, projectSlug);
|
|
89
|
+
const { select, filters, orderBy, limit, offset } =
|
|
90
|
+
parseQueryParams(rawParams);
|
|
91
|
+
|
|
92
|
+
const columns =
|
|
93
|
+
select.length > 0 ? select.map((c) => `"${c}"`).join(', ') : '*';
|
|
94
|
+
|
|
95
|
+
const where = buildWhereClause(filters);
|
|
96
|
+
const order = orderBy
|
|
97
|
+
? `ORDER BY "${orderBy.column}" ${orderBy.direction}`
|
|
98
|
+
: '';
|
|
99
|
+
|
|
100
|
+
const sql = `
|
|
101
|
+
SELECT ${columns}
|
|
102
|
+
FROM "${schema}"."${tableName}"
|
|
103
|
+
${where}
|
|
104
|
+
${order}
|
|
105
|
+
LIMIT ${limit}
|
|
106
|
+
OFFSET ${offset}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const result = await this.drizzle.db.execute<Record<string, unknown>>(sql);
|
|
110
|
+
return result.rows;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async insertRow(
|
|
114
|
+
projectId: string,
|
|
115
|
+
projectSlug: string,
|
|
116
|
+
tableName: string,
|
|
117
|
+
body: Record<string, unknown>,
|
|
118
|
+
) {
|
|
119
|
+
this.assertSafeIdentifier(tableName, 'table name');
|
|
120
|
+
const schema = await this.resolveProjectSchema(projectId, projectSlug);
|
|
121
|
+
|
|
122
|
+
const entries = Object.entries(body);
|
|
123
|
+
if (entries.length === 0) {
|
|
124
|
+
throw new BadRequestException('Request body cannot be empty');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
entries.forEach(([col]) => this.assertSafeIdentifier(col, 'column name'));
|
|
128
|
+
|
|
129
|
+
const columns = entries.map(([c]) => `"${c}"`).join(', ');
|
|
130
|
+
const values = entries.map(([, v]) => this.formatLiteral(v)).join(', ');
|
|
131
|
+
|
|
132
|
+
const sql = `
|
|
133
|
+
INSERT INTO "${schema}"."${tableName}" (${columns})
|
|
134
|
+
VALUES (${values})
|
|
135
|
+
RETURNING *
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const result = await this.drizzle.db.execute<Record<string, unknown>>(sql);
|
|
139
|
+
return result.rows[0];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async updateRow(
|
|
143
|
+
projectId: string,
|
|
144
|
+
projectSlug: string,
|
|
145
|
+
tableName: string,
|
|
146
|
+
rowId: string,
|
|
147
|
+
body: Record<string, unknown>,
|
|
148
|
+
) {
|
|
149
|
+
this.assertSafeIdentifier(tableName, 'table name');
|
|
150
|
+
const schema = await this.resolveProjectSchema(projectId, projectSlug);
|
|
151
|
+
const pkColumn = await this.getPrimaryKeyColumn(schema, tableName);
|
|
152
|
+
|
|
153
|
+
const entries = Object.entries(body);
|
|
154
|
+
if (entries.length === 0) {
|
|
155
|
+
throw new BadRequestException('Request body cannot be empty');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
entries.forEach(([col]) => this.assertSafeIdentifier(col, 'column name'));
|
|
159
|
+
|
|
160
|
+
const setClauses = entries
|
|
161
|
+
.map(([col, val]) => `"${col}" = ${this.formatLiteral(val)}`)
|
|
162
|
+
.join(', ');
|
|
163
|
+
|
|
164
|
+
const sql = `
|
|
165
|
+
UPDATE "${schema}"."${tableName}"
|
|
166
|
+
SET ${setClauses}
|
|
167
|
+
WHERE "${pkColumn}" = ${this.formatLiteral(rowId)}
|
|
168
|
+
RETURNING *
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
const result = await this.drizzle.db.execute<Record<string, unknown>>(sql);
|
|
172
|
+
if (!result.rows[0]) throw new NotFoundException('Row not found');
|
|
173
|
+
return result.rows[0];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async deleteRow(
|
|
177
|
+
projectId: string,
|
|
178
|
+
projectSlug: string,
|
|
179
|
+
tableName: string,
|
|
180
|
+
rowId: string,
|
|
181
|
+
) {
|
|
182
|
+
this.assertSafeIdentifier(tableName, 'table name');
|
|
183
|
+
const schema = await this.resolveProjectSchema(projectId, projectSlug);
|
|
184
|
+
const pkColumn = await this.getPrimaryKeyColumn(schema, tableName);
|
|
185
|
+
|
|
186
|
+
const sql = `
|
|
187
|
+
DELETE FROM "${schema}"."${tableName}"
|
|
188
|
+
WHERE "${pkColumn}" = ${this.formatLiteral(rowId)}
|
|
189
|
+
RETURNING *
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const result = await this.drizzle.db.execute<Record<string, unknown>>(sql);
|
|
193
|
+
if (!result.rows[0]) throw new NotFoundException('Row not found');
|
|
194
|
+
return result.rows[0];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
Injectable,
|
|
5
|
+
UnauthorizedException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { JwtService } from '@nestjs/jwt';
|
|
8
|
+
import { ConfigService } from '@nestjs/config';
|
|
9
|
+
import { Request } from 'express';
|
|
10
|
+
import { PROJECT_KEY_ROLES } from '@apiDatabase/constants';
|
|
11
|
+
|
|
12
|
+
export interface ProjectKeyPayload {
|
|
13
|
+
projectId: string;
|
|
14
|
+
role: typeof PROJECT_KEY_ROLES.ANON | typeof PROJECT_KEY_ROLES.SERVICE_ROLE;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class ProjectKeyGuard implements CanActivate {
|
|
19
|
+
constructor(
|
|
20
|
+
private jwtService: JwtService,
|
|
21
|
+
private configService: ConfigService,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
canActivate(context: ExecutionContext): boolean {
|
|
25
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
26
|
+
const authHeader = request.headers.authorization;
|
|
27
|
+
|
|
28
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
29
|
+
throw new UnauthorizedException('Missing API key');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const token = authHeader.slice(7);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const payload = this.jwtService.verify<ProjectKeyPayload>(token, {
|
|
36
|
+
secret: this.configService.get<string>('PROJECT_JWT_SECRET'),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
request['projectKey'] = payload;
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
throw new UnauthorizedException('Invalid API key');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { BadRequestException } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
RESERVED_QUERY_PARAMS,
|
|
4
|
+
FILTER_OPERATORS,
|
|
5
|
+
type FilterOperator,
|
|
6
|
+
} from '@apiDatabase/constants';
|
|
7
|
+
|
|
8
|
+
export interface ParsedQuery {
|
|
9
|
+
select: string[];
|
|
10
|
+
filters: FilterClause[];
|
|
11
|
+
orderBy: OrderClause | null;
|
|
12
|
+
limit: number;
|
|
13
|
+
offset: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface FilterClause {
|
|
17
|
+
column: string;
|
|
18
|
+
operator: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OrderClause {
|
|
23
|
+
column: string;
|
|
24
|
+
direction: 'ASC' | 'DESC';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertSafeIdentifier(name: string, label: string): void {
|
|
28
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
29
|
+
throw new BadRequestException(`Invalid ${label}: ${name}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatSqlValue(operator: string, rawValue: string): string {
|
|
34
|
+
const value = rawValue.replace(/;/g, '');
|
|
35
|
+
|
|
36
|
+
if (operator === 'IS') {
|
|
37
|
+
return value.toUpperCase() === 'NULL' ? 'NULL' : 'NOT NULL';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (operator === 'LIKE' || operator === 'ILIKE') {
|
|
41
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
|
|
45
|
+
return value.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseQueryParams(params: Record<string, string>): ParsedQuery {
|
|
56
|
+
const filters: FilterClause[] = [];
|
|
57
|
+
let select: string[] = [];
|
|
58
|
+
let orderBy: OrderClause | null = null;
|
|
59
|
+
let limit = 100;
|
|
60
|
+
let offset = 0;
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(params)) {
|
|
63
|
+
if (RESERVED_QUERY_PARAMS.has(key)) {
|
|
64
|
+
switch (key) {
|
|
65
|
+
case 'select':
|
|
66
|
+
select = value
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((c) => c.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
select.forEach((col) => assertSafeIdentifier(col, 'select column'));
|
|
71
|
+
break;
|
|
72
|
+
case 'order': {
|
|
73
|
+
const [col, dir] = value.split('.');
|
|
74
|
+
assertSafeIdentifier(col, 'order column');
|
|
75
|
+
orderBy = {
|
|
76
|
+
column: col,
|
|
77
|
+
direction: dir?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC',
|
|
78
|
+
};
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'limit':
|
|
82
|
+
limit = Math.min(parseInt(value, 10) || 100, 1000);
|
|
83
|
+
break;
|
|
84
|
+
case 'offset':
|
|
85
|
+
offset = Math.max(parseInt(value, 10) || 0, 0);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
assertSafeIdentifier(key, 'filter column');
|
|
92
|
+
|
|
93
|
+
const dotIndex = value.indexOf('.');
|
|
94
|
+
if (dotIndex === -1) continue;
|
|
95
|
+
|
|
96
|
+
const operator = value.slice(0, dotIndex) as FilterOperator;
|
|
97
|
+
const filterValue = value.slice(dotIndex + 1);
|
|
98
|
+
|
|
99
|
+
if (!(operator in FILTER_OPERATORS)) continue;
|
|
100
|
+
|
|
101
|
+
filters.push({
|
|
102
|
+
column: key,
|
|
103
|
+
operator: FILTER_OPERATORS[operator],
|
|
104
|
+
value: filterValue,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { select, filters, orderBy, limit, offset };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildWhereClause(filters: FilterClause[]): string {
|
|
112
|
+
if (filters.length === 0) return '';
|
|
113
|
+
|
|
114
|
+
const clauses = filters.map(({ column, operator, value }) => {
|
|
115
|
+
const sqlValue = formatSqlValue(operator, value);
|
|
116
|
+
|
|
117
|
+
if (operator === 'IS') {
|
|
118
|
+
return `"${column}" IS ${sqlValue}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (operator === 'LIKE' || operator === 'ILIKE') {
|
|
122
|
+
return `"${column}" ${operator} ${sqlValue}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return `"${column}" ${operator} ${sqlValue}`;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return `WHERE ${clauses.join(' AND ')}`;
|
|
129
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IsEmail, IsString, MinLength } from 'class-validator';
|
|
2
|
+
import type { SignUpInput } from '@apiDatabase/types';
|
|
3
|
+
|
|
4
|
+
export class SignUpDto implements SignUpInput {
|
|
5
|
+
@IsEmail()
|
|
6
|
+
email!: string;
|
|
7
|
+
|
|
8
|
+
@IsString()
|
|
9
|
+
@MinLength(8)
|
|
10
|
+
password!: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
|
2
|
+
import { ProjectAuthService } from './project-auth.service';
|
|
3
|
+
import { JwtAuthGuard } from '../auth/guards/jwt-auth-guard';
|
|
4
|
+
import { OrgRoleGuard } from '../auth/guards/org-role.guards';
|
|
5
|
+
|
|
6
|
+
@Controller('orgs/:slug/projects/:projectSlug/auth')
|
|
7
|
+
@UseGuards(JwtAuthGuard, OrgRoleGuard)
|
|
8
|
+
export class ProjectAuthDashboardController {
|
|
9
|
+
constructor(private projectAuthService: ProjectAuthService) {}
|
|
10
|
+
|
|
11
|
+
@Get('users')
|
|
12
|
+
getUsers(@Param('projectSlug') projectSlug: string) {
|
|
13
|
+
return this.projectAuthService.getUsers(projectSlug);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Get('settings')
|
|
17
|
+
getSettings(@Param('projectSlug') projectSlug: string) {
|
|
18
|
+
return this.projectAuthService.getOAuthSettings(projectSlug);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Post('settings')
|
|
22
|
+
updateSettings(
|
|
23
|
+
@Param('projectSlug') projectSlug: string,
|
|
24
|
+
@Body()
|
|
25
|
+
settings: {
|
|
26
|
+
siteUrl?: string;
|
|
27
|
+
googleClientId?: string;
|
|
28
|
+
googleClientSecret?: string;
|
|
29
|
+
githubClientId?: string;
|
|
30
|
+
githubClientSecret?: string;
|
|
31
|
+
},
|
|
32
|
+
) {
|
|
33
|
+
return this.projectAuthService.updateOAuthSettings(projectSlug, settings);
|
|
34
|
+
}
|
|
35
|
+
}
|