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,111 @@
|
|
|
1
|
+
import { Body, Controller, Get, Param, Post, Query, Res } from '@nestjs/common';
|
|
2
|
+
import type { Response } from 'express';
|
|
3
|
+
import { ConfigService } from '@nestjs/config';
|
|
4
|
+
import { ProjectAuthService } from './project-auth.service';
|
|
5
|
+
import { SignUpDto } from './dto/signup.dto';
|
|
6
|
+
import { SignInDto } from './dto/signin.dto';
|
|
7
|
+
import { MagicLinkDto } from './dto/magic-link.dto';
|
|
8
|
+
|
|
9
|
+
@Controller('projects/:projectSlug/auth')
|
|
10
|
+
export class ProjectAuthController {
|
|
11
|
+
constructor(
|
|
12
|
+
private projectAuthService: ProjectAuthService,
|
|
13
|
+
private configService: ConfigService,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
@Post('signup')
|
|
17
|
+
signUp(@Param('projectSlug') projectSlug: string, @Body() dto: SignUpDto) {
|
|
18
|
+
return this.projectAuthService.signUp(projectSlug, dto);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Post('signin')
|
|
22
|
+
signIn(@Param('projectSlug') projectSlug: string, @Body() dto: SignInDto) {
|
|
23
|
+
return this.projectAuthService.signIn(projectSlug, dto);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Post('magic-link')
|
|
27
|
+
sendMagicLink(
|
|
28
|
+
@Param('projectSlug') projectSlug: string,
|
|
29
|
+
@Body() dto: MagicLinkDto,
|
|
30
|
+
) {
|
|
31
|
+
return this.projectAuthService.sendMagicLink(projectSlug, dto.email);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Get('magic-link/verify')
|
|
35
|
+
verifyMagicLink(
|
|
36
|
+
@Param('projectSlug') projectSlug: string,
|
|
37
|
+
@Query('token') token: string,
|
|
38
|
+
) {
|
|
39
|
+
return this.projectAuthService.verifyMagicLink(projectSlug, token);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Get('google')
|
|
43
|
+
async googleLogin(
|
|
44
|
+
@Param('projectSlug') projectSlug: string,
|
|
45
|
+
@Res() res: Response,
|
|
46
|
+
) {
|
|
47
|
+
const settings =
|
|
48
|
+
await this.projectAuthService.getOAuthSettings(projectSlug);
|
|
49
|
+
if (!settings.googleClientId) {
|
|
50
|
+
return res.status(400).json({ message: 'Google OAuth not configured' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const apiUrl = this.configService.get<string>('API_URL');
|
|
54
|
+
const redirectUri = `${apiUrl}/projects/${projectSlug}/auth/google/callback`;
|
|
55
|
+
const url = this.projectAuthService.buildGoogleAuthUrl(
|
|
56
|
+
settings.googleClientId,
|
|
57
|
+
redirectUri,
|
|
58
|
+
);
|
|
59
|
+
return res.redirect(url);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Get('google/callback')
|
|
63
|
+
async googleCallback(
|
|
64
|
+
@Param('projectSlug') projectSlug: string,
|
|
65
|
+
@Query('code') code: string,
|
|
66
|
+
@Res() res: Response,
|
|
67
|
+
) {
|
|
68
|
+
const result = await this.projectAuthService.handleGoogleCallback(
|
|
69
|
+
projectSlug,
|
|
70
|
+
code,
|
|
71
|
+
);
|
|
72
|
+
const webUrl =
|
|
73
|
+
await this.projectAuthService.getSiteUrlForProject(projectSlug);
|
|
74
|
+
return res.redirect(`${webUrl}/?access_token=${result.accessToken}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Get('github')
|
|
78
|
+
async githubLogin(
|
|
79
|
+
@Param('projectSlug') projectSlug: string,
|
|
80
|
+
@Res() res: Response,
|
|
81
|
+
) {
|
|
82
|
+
const settings =
|
|
83
|
+
await this.projectAuthService.getOAuthSettings(projectSlug);
|
|
84
|
+
if (!settings.githubClientId) {
|
|
85
|
+
return res.status(400).json({ message: 'GitHub OAuth not configured' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const apiUrl = this.configService.get<string>('API_URL');
|
|
89
|
+
const redirectUri = `${apiUrl}/projects/${projectSlug}/auth/github/callback`;
|
|
90
|
+
const url = this.projectAuthService.buildGithubAuthUrl(
|
|
91
|
+
settings.githubClientId,
|
|
92
|
+
redirectUri,
|
|
93
|
+
);
|
|
94
|
+
return res.redirect(url);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Get('github/callback')
|
|
98
|
+
async githubCallback(
|
|
99
|
+
@Param('projectSlug') projectSlug: string,
|
|
100
|
+
@Query('code') code: string,
|
|
101
|
+
@Res() res: Response,
|
|
102
|
+
) {
|
|
103
|
+
const result = await this.projectAuthService.handleGithubCallback(
|
|
104
|
+
projectSlug,
|
|
105
|
+
code,
|
|
106
|
+
);
|
|
107
|
+
const webUrl =
|
|
108
|
+
await this.projectAuthService.getSiteUrlForProject(projectSlug);
|
|
109
|
+
return res.redirect(`${webUrl}/?access_token=${result.accessToken}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
3
|
+
import { ProjectAuthService } from './project-auth.service';
|
|
4
|
+
import { ProjectAuthController } from './project-auth.controller';
|
|
5
|
+
import { ProjectAuthDashboardController } from './project-auth-dashboard.controller';
|
|
6
|
+
import { AuthModule } from '../auth/auth.module';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [JwtModule.register({}), AuthModule],
|
|
10
|
+
providers: [ProjectAuthService],
|
|
11
|
+
controllers: [ProjectAuthController, ProjectAuthDashboardController],
|
|
12
|
+
})
|
|
13
|
+
export class ProjectAuthModule {}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
ConflictException,
|
|
4
|
+
Injectable,
|
|
5
|
+
NotFoundException,
|
|
6
|
+
UnauthorizedException,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { JwtService } from '@nestjs/jwt';
|
|
9
|
+
import { ConfigService } from '@nestjs/config';
|
|
10
|
+
import { Resend } from 'resend';
|
|
11
|
+
import * as bcrypt from 'bcrypt';
|
|
12
|
+
import { eq, sql } from 'drizzle-orm';
|
|
13
|
+
import { DrizzleService } from '../db/drizzle.service';
|
|
14
|
+
import { projects } from '../db/schema';
|
|
15
|
+
import { AUTH_PROVIDERS, MAGIC_LINK_EXPIRES_IN } from '@apiDatabase/constants';
|
|
16
|
+
import type { SignUpInput, SignInInput } from '@apiDatabase/types';
|
|
17
|
+
|
|
18
|
+
interface ProjectAuthTokenPayload {
|
|
19
|
+
sub: string;
|
|
20
|
+
email: string;
|
|
21
|
+
projectId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Injectable()
|
|
25
|
+
export class ProjectAuthService {
|
|
26
|
+
private resend: Resend;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private drizzle: DrizzleService,
|
|
30
|
+
private jwtService: JwtService,
|
|
31
|
+
private configService: ConfigService,
|
|
32
|
+
) {
|
|
33
|
+
this.resend = new Resend(this.configService.get<string>('RESEND_API_KEY'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async getProject(projectSlug: string) {
|
|
37
|
+
const [project] = await this.drizzle.db
|
|
38
|
+
.select()
|
|
39
|
+
.from(projects)
|
|
40
|
+
.where(eq(projects.slug, projectSlug))
|
|
41
|
+
.limit(1);
|
|
42
|
+
|
|
43
|
+
if (!project) throw new NotFoundException('Project not found');
|
|
44
|
+
return project;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// not every project needs Auth
|
|
48
|
+
private async ensureAuthUsersTable(dbSchema: string): Promise<void> {
|
|
49
|
+
await this.drizzle.db.execute(
|
|
50
|
+
sql.raw(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS "${dbSchema}"."auth_users" (
|
|
52
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
53
|
+
email TEXT NOT NULL UNIQUE,
|
|
54
|
+
password_hash TEXT,
|
|
55
|
+
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
56
|
+
provider TEXT NOT NULL DEFAULT 'email',
|
|
57
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
58
|
+
)
|
|
59
|
+
`),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private signToken(
|
|
64
|
+
userId: string,
|
|
65
|
+
email: string,
|
|
66
|
+
projectId: string,
|
|
67
|
+
secret: string,
|
|
68
|
+
): string {
|
|
69
|
+
return this.jwtService.sign(
|
|
70
|
+
{ sub: userId, email, projectId } satisfies ProjectAuthTokenPayload,
|
|
71
|
+
{ secret, expiresIn: '7d' },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async signUp(projectSlug: string, dto: SignUpInput) {
|
|
76
|
+
const project = await this.getProject(projectSlug);
|
|
77
|
+
await this.ensureAuthUsersTable(project.dbSchema);
|
|
78
|
+
|
|
79
|
+
const existing = await this.drizzle.db.execute<{ id: string }>(sql`
|
|
80
|
+
SELECT id FROM ${sql.identifier(project.dbSchema)}.auth_users
|
|
81
|
+
WHERE email = ${dto.email}
|
|
82
|
+
`);
|
|
83
|
+
|
|
84
|
+
if (existing.rows.length > 0) {
|
|
85
|
+
throw new ConflictException('Email already registered');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const passwordHash = await bcrypt.hash(dto.password, 12);
|
|
89
|
+
|
|
90
|
+
const result = await this.drizzle.db.execute<{
|
|
91
|
+
id: string;
|
|
92
|
+
email: string;
|
|
93
|
+
}>(sql`
|
|
94
|
+
INSERT INTO ${sql.identifier(project.dbSchema)}.auth_users
|
|
95
|
+
(email, password_hash, provider)
|
|
96
|
+
VALUES (${dto.email}, ${passwordHash}, ${AUTH_PROVIDERS.EMAIL})
|
|
97
|
+
RETURNING id, email
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
const user = result.rows[0];
|
|
101
|
+
const accessToken = this.signToken(
|
|
102
|
+
user.id,
|
|
103
|
+
user.email,
|
|
104
|
+
project.id,
|
|
105
|
+
project.authJwtSecret,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return { user: { id: user.id, email: user.email }, accessToken };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async signIn(projectSlug: string, dto: SignInInput) {
|
|
112
|
+
const project = await this.getProject(projectSlug);
|
|
113
|
+
await this.ensureAuthUsersTable(project.dbSchema);
|
|
114
|
+
|
|
115
|
+
const result = await this.drizzle.db.execute<{
|
|
116
|
+
id: string;
|
|
117
|
+
email: string;
|
|
118
|
+
password_hash: string | null;
|
|
119
|
+
}>(sql`
|
|
120
|
+
SELECT id, email, password_hash
|
|
121
|
+
FROM ${sql.identifier(project.dbSchema)}.auth_users
|
|
122
|
+
WHERE email = ${dto.email}
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
const user = result.rows[0];
|
|
126
|
+
|
|
127
|
+
if (!user?.password_hash) {
|
|
128
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const match = await bcrypt.compare(dto.password, user.password_hash);
|
|
132
|
+
if (!match) throw new UnauthorizedException('Invalid credentials');
|
|
133
|
+
|
|
134
|
+
const accessToken = this.signToken(
|
|
135
|
+
user.id,
|
|
136
|
+
user.email,
|
|
137
|
+
project.id,
|
|
138
|
+
project.authJwtSecret,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return { user: { id: user.id, email: user.email }, accessToken };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async sendMagicLink(projectSlug: string, email: string) {
|
|
145
|
+
const project = await this.getProject(projectSlug);
|
|
146
|
+
await this.ensureAuthUsersTable(project.dbSchema);
|
|
147
|
+
|
|
148
|
+
const existing = await this.drizzle.db.execute<{ id: string }>(sql`
|
|
149
|
+
SELECT id FROM ${sql.identifier(project.dbSchema)}.auth_users
|
|
150
|
+
WHERE email = ${email}
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
if (existing.rows.length === 0) {
|
|
154
|
+
await this.drizzle.db.execute(sql`
|
|
155
|
+
INSERT INTO ${sql.identifier(project.dbSchema)}.auth_users
|
|
156
|
+
(email, provider, email_verified)
|
|
157
|
+
VALUES (${email}, ${AUTH_PROVIDERS.EMAIL}, true)
|
|
158
|
+
`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const linkToken = this.jwtService.sign(
|
|
162
|
+
{ email, projectId: project.id },
|
|
163
|
+
{ secret: project.authJwtSecret, expiresIn: MAGIC_LINK_EXPIRES_IN },
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const apiUrl = this.configService.get<string>('API_URL');
|
|
167
|
+
const magicLinkUrl = `${apiUrl}/projects/${projectSlug}/auth/magic-link/verify?token=${linkToken}`;
|
|
168
|
+
|
|
169
|
+
await this.resend.emails.send({
|
|
170
|
+
from: 'Supavolt <onboarding@resend.dev>',
|
|
171
|
+
to: email,
|
|
172
|
+
subject: 'Your magic link',
|
|
173
|
+
html: `
|
|
174
|
+
<p>Click to sign in (expires in 15 minutes):</p>
|
|
175
|
+
<a href="${magicLinkUrl}">Sign in</a>
|
|
176
|
+
`,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return { message: 'Magic link sent' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async verifyMagicLink(projectSlug: string, token: string) {
|
|
183
|
+
const project = await this.getProject(projectSlug);
|
|
184
|
+
|
|
185
|
+
let payload: { email: string; projectId: string };
|
|
186
|
+
try {
|
|
187
|
+
payload = this.jwtService.verify(token, {
|
|
188
|
+
secret: project.authJwtSecret,
|
|
189
|
+
});
|
|
190
|
+
} catch {
|
|
191
|
+
throw new BadRequestException('Invalid or expired magic link');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result = await this.drizzle.db.execute<{ id: string; email: string }>(
|
|
195
|
+
sql`
|
|
196
|
+
SELECT id, email FROM ${sql.identifier(project.dbSchema)}.auth_users
|
|
197
|
+
WHERE email = ${payload.email}
|
|
198
|
+
`,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const user = result.rows[0];
|
|
202
|
+
if (!user) throw new NotFoundException('User not found');
|
|
203
|
+
|
|
204
|
+
const accessToken = this.signToken(
|
|
205
|
+
user.id,
|
|
206
|
+
user.email,
|
|
207
|
+
project.id,
|
|
208
|
+
project.authJwtSecret,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return { user: { id: user.id, email: user.email }, accessToken };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
buildGoogleAuthUrl(clientId: string, redirectUri: string): string {
|
|
215
|
+
const params = new URLSearchParams({
|
|
216
|
+
client_id: clientId,
|
|
217
|
+
redirect_uri: redirectUri,
|
|
218
|
+
response_type: 'code',
|
|
219
|
+
scope: 'openid email profile',
|
|
220
|
+
});
|
|
221
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
buildGithubAuthUrl(clientId: string, redirectUri: string): string {
|
|
225
|
+
const params = new URLSearchParams({
|
|
226
|
+
client_id: clientId,
|
|
227
|
+
redirect_uri: redirectUri,
|
|
228
|
+
scope: 'user:email',
|
|
229
|
+
});
|
|
230
|
+
return `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async handleGoogleCallback(projectSlug: string, code: string) {
|
|
234
|
+
const project = await this.getProject(projectSlug);
|
|
235
|
+
await this.ensureAuthUsersTable(project.dbSchema);
|
|
236
|
+
|
|
237
|
+
if (!project.googleClientId || !project.googleClientSecret) {
|
|
238
|
+
throw new BadRequestException(
|
|
239
|
+
'Google OAuth not configured for this project',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const apiUrl = this.configService.get<string>('API_URL');
|
|
244
|
+
const redirectUri = `${apiUrl}/projects/${projectSlug}/auth/google/callback`;
|
|
245
|
+
|
|
246
|
+
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
249
|
+
body: new URLSearchParams({
|
|
250
|
+
code,
|
|
251
|
+
client_id: project.googleClientId,
|
|
252
|
+
client_secret: project.googleClientSecret,
|
|
253
|
+
redirect_uri: redirectUri,
|
|
254
|
+
grant_type: 'authorization_code',
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const tokenData = (await tokenRes.json()) as { access_token: string };
|
|
259
|
+
|
|
260
|
+
const profileRes = await fetch(
|
|
261
|
+
'https://www.googleapis.com/oauth2/v3/userinfo',
|
|
262
|
+
{ headers: { Authorization: `Bearer ${tokenData.access_token}` } },
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const profile = (await profileRes.json()) as { email: string };
|
|
266
|
+
return this.findOrCreateOAuthUser(
|
|
267
|
+
project,
|
|
268
|
+
profile.email,
|
|
269
|
+
AUTH_PROVIDERS.GOOGLE,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async handleGithubCallback(projectSlug: string, code: string) {
|
|
274
|
+
const project = await this.getProject(projectSlug);
|
|
275
|
+
await this.ensureAuthUsersTable(project.dbSchema);
|
|
276
|
+
|
|
277
|
+
if (!project.githubClientId || !project.githubClientSecret) {
|
|
278
|
+
throw new BadRequestException(
|
|
279
|
+
'GitHub OAuth not configured for this project',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const apiUrl = this.configService.get<string>('API_URL');
|
|
284
|
+
const redirectUri = `${apiUrl}/projects/${projectSlug}/auth/github/callback`;
|
|
285
|
+
|
|
286
|
+
const tokenRes = await fetch(
|
|
287
|
+
'https://github.com/login/oauth/access_token',
|
|
288
|
+
{
|
|
289
|
+
method: 'POST',
|
|
290
|
+
headers: {
|
|
291
|
+
'Content-Type': 'application/json',
|
|
292
|
+
Accept: 'application/json',
|
|
293
|
+
},
|
|
294
|
+
body: JSON.stringify({
|
|
295
|
+
client_id: project.githubClientId,
|
|
296
|
+
client_secret: project.githubClientSecret,
|
|
297
|
+
code,
|
|
298
|
+
redirect_uri: redirectUri,
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const tokenData = (await tokenRes.json()) as { access_token: string };
|
|
304
|
+
|
|
305
|
+
const profileRes = await fetch('https://api.github.com/user/emails', {
|
|
306
|
+
headers: {
|
|
307
|
+
Authorization: `Bearer ${tokenData.access_token}`,
|
|
308
|
+
Accept: 'application/vnd.github+json',
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const emails = (await profileRes.json()) as Array<{
|
|
313
|
+
email: string;
|
|
314
|
+
primary: boolean;
|
|
315
|
+
}>;
|
|
316
|
+
|
|
317
|
+
const primary = emails.find((e) => e.primary) ?? emails[0];
|
|
318
|
+
if (!primary?.email) {
|
|
319
|
+
throw new BadRequestException('Could not read email from GitHub');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return this.findOrCreateOAuthUser(
|
|
323
|
+
project,
|
|
324
|
+
primary.email,
|
|
325
|
+
AUTH_PROVIDERS.GITHUB,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async findOrCreateOAuthUser(
|
|
330
|
+
project: typeof projects.$inferSelect,
|
|
331
|
+
email: string,
|
|
332
|
+
provider: string,
|
|
333
|
+
) {
|
|
334
|
+
const existing = await this.drizzle.db.execute<{
|
|
335
|
+
id: string;
|
|
336
|
+
email: string;
|
|
337
|
+
}>(
|
|
338
|
+
sql`
|
|
339
|
+
SELECT id, email FROM ${sql.identifier(project.dbSchema)}.auth_users
|
|
340
|
+
WHERE email = ${email}
|
|
341
|
+
`,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
let user = existing.rows[0];
|
|
345
|
+
|
|
346
|
+
if (!user) {
|
|
347
|
+
const result = await this.drizzle.db.execute<{
|
|
348
|
+
id: string;
|
|
349
|
+
email: string;
|
|
350
|
+
}>(
|
|
351
|
+
sql`
|
|
352
|
+
INSERT INTO ${sql.identifier(project.dbSchema)}.auth_users
|
|
353
|
+
(email, provider, email_verified)
|
|
354
|
+
VALUES (${email}, ${provider}, true)
|
|
355
|
+
RETURNING id, email
|
|
356
|
+
`,
|
|
357
|
+
);
|
|
358
|
+
user = result.rows[0];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const accessToken = this.signToken(
|
|
362
|
+
user.id,
|
|
363
|
+
user.email,
|
|
364
|
+
project.id,
|
|
365
|
+
project.authJwtSecret,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
return { user: { id: user.id, email: user.email }, accessToken };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async getUsers(projectSlug: string) {
|
|
372
|
+
const project = await this.getProject(projectSlug);
|
|
373
|
+
await this.ensureAuthUsersTable(project.dbSchema);
|
|
374
|
+
|
|
375
|
+
const result = await this.drizzle.db.execute<{
|
|
376
|
+
id: string;
|
|
377
|
+
email: string;
|
|
378
|
+
email_verified: boolean;
|
|
379
|
+
provider: string;
|
|
380
|
+
created_at: string;
|
|
381
|
+
}>(sql`
|
|
382
|
+
SELECT id, email, email_verified, provider, created_at
|
|
383
|
+
FROM ${sql.identifier(project.dbSchema)}.auth_users
|
|
384
|
+
ORDER BY created_at DESC
|
|
385
|
+
`);
|
|
386
|
+
|
|
387
|
+
return result.rows.map((row) => ({
|
|
388
|
+
id: row.id,
|
|
389
|
+
email: row.email,
|
|
390
|
+
emailVerified: row.email_verified,
|
|
391
|
+
provider: row.provider,
|
|
392
|
+
createdAt: row.created_at,
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async getOAuthSettings(projectSlug: string) {
|
|
397
|
+
const project = await this.getProject(projectSlug);
|
|
398
|
+
return {
|
|
399
|
+
siteUrl: project.siteUrl,
|
|
400
|
+
googleClientId: project.googleClientId,
|
|
401
|
+
googleClientSecret: project.googleClientSecret,
|
|
402
|
+
githubClientId: project.githubClientId,
|
|
403
|
+
githubClientSecret: project.githubClientSecret,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
resolveSiteUrl(project: typeof projects.$inferSelect): string {
|
|
408
|
+
return (
|
|
409
|
+
project.siteUrl ??
|
|
410
|
+
this.configService.get<string>('WEB_URL') ??
|
|
411
|
+
'http://localhost:3001'
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async getSiteUrlForProject(projectSlug: string): Promise<string> {
|
|
416
|
+
const project = await this.getProject(projectSlug);
|
|
417
|
+
return this.resolveSiteUrl(project);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async updateOAuthSettings(
|
|
421
|
+
projectSlug: string,
|
|
422
|
+
settings: {
|
|
423
|
+
siteUrl?: string;
|
|
424
|
+
googleClientId?: string;
|
|
425
|
+
googleClientSecret?: string;
|
|
426
|
+
githubClientId?: string;
|
|
427
|
+
githubClientSecret?: string;
|
|
428
|
+
},
|
|
429
|
+
) {
|
|
430
|
+
const project = await this.getProject(projectSlug);
|
|
431
|
+
|
|
432
|
+
const [updated] = await this.drizzle.db
|
|
433
|
+
.update(projects)
|
|
434
|
+
.set(settings)
|
|
435
|
+
.where(eq(projects.id, project.id))
|
|
436
|
+
.returning();
|
|
437
|
+
|
|
438
|
+
return updated;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
|
2
|
+
import { ProjectsService } from './projects.service';
|
|
3
|
+
import { CreateProjectDto } from './dto/create-project.dto';
|
|
4
|
+
import { JwtAuthGuard } from '../auth/guards/jwt-auth-guard';
|
|
5
|
+
import { OrgRoleGuard } from '../auth/guards/org-role.guards';
|
|
6
|
+
import { RequireOrgRole } from '../auth/decorators/require-org-role-decorator';
|
|
7
|
+
import { CurrentUser } from '../auth/decorators/current-user-decorator';
|
|
8
|
+
import type { JwtPayload } from '@apiDatabase/types';
|
|
9
|
+
|
|
10
|
+
@Controller('orgs/:slug/projects')
|
|
11
|
+
@UseGuards(JwtAuthGuard, OrgRoleGuard)
|
|
12
|
+
export class ProjectsController {
|
|
13
|
+
constructor(private projectsService: ProjectsService) {}
|
|
14
|
+
|
|
15
|
+
@Get()
|
|
16
|
+
getProjects(@Param('slug') slug: string, @CurrentUser() user: JwtPayload) {
|
|
17
|
+
return this.projectsService.getProjectsForOrg(slug, user.sub);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Get(':projectSlug')
|
|
21
|
+
getProject(
|
|
22
|
+
@Param('slug') slug: string,
|
|
23
|
+
@Param('projectSlug') projectSlug: string,
|
|
24
|
+
@CurrentUser() user: JwtPayload,
|
|
25
|
+
) {
|
|
26
|
+
return this.projectsService.getProjectBySlug(slug, projectSlug, user.sub);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Post()
|
|
30
|
+
@RequireOrgRole('admin')
|
|
31
|
+
createProject(@Param('slug') slug: string, @Body() dto: CreateProjectDto) {
|
|
32
|
+
return this.projectsService.createProject(slug, dto);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
3
|
+
import { ProjectsService } from './projects.service';
|
|
4
|
+
import { ProjectsController } from './projects.controller';
|
|
5
|
+
|
|
6
|
+
@Module({
|
|
7
|
+
imports: [JwtModule.register({})],
|
|
8
|
+
providers: [ProjectsService],
|
|
9
|
+
controllers: [ProjectsController],
|
|
10
|
+
exports: [ProjectsService],
|
|
11
|
+
})
|
|
12
|
+
export class ProjectsModule {}
|