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.
Files changed (219) hide show
  1. package/README.md +42 -0
  2. package/apps/api/.prettierrc +4 -0
  3. package/apps/api/README.md +98 -0
  4. package/apps/api/drizzle/0000_right_drax.sql +87 -0
  5. package/apps/api/drizzle/meta/0000_snapshot.json +588 -0
  6. package/apps/api/drizzle/meta/_journal.json +13 -0
  7. package/apps/api/drizzle.config.ts +12 -0
  8. package/apps/api/eslint.config.mjs +35 -0
  9. package/apps/api/nest-cli.json +8 -0
  10. package/apps/api/package.json +99 -0
  11. package/apps/api/src/app.controller.spec.ts +25 -0
  12. package/apps/api/src/app.controller.ts +9 -0
  13. package/apps/api/src/app.module.ts +37 -0
  14. package/apps/api/src/app.service.ts +8 -0
  15. package/apps/api/src/auth/auth.controller.ts +113 -0
  16. package/apps/api/src/auth/auth.module.ts +15 -0
  17. package/apps/api/src/auth/auth.service.ts +309 -0
  18. package/apps/api/src/auth/decorators/current-user-decorator.ts +10 -0
  19. package/apps/api/src/auth/decorators/require-org-role-decorator.ts +7 -0
  20. package/apps/api/src/auth/dto/login.dto.ts +10 -0
  21. package/apps/api/src/auth/dto/register.dto.ts +14 -0
  22. package/apps/api/src/auth/guards/jwt-auth-guard.ts +40 -0
  23. package/apps/api/src/auth/guards/org-role.guards.ts +60 -0
  24. package/apps/api/src/db/db.module.ts +9 -0
  25. package/apps/api/src/db/drizzle.service.ts +16 -0
  26. package/apps/api/src/db/schema/index.ts +6 -0
  27. package/apps/api/src/db/schema/organizations.ts +30 -0
  28. package/apps/api/src/db/schema/projects.ts +26 -0
  29. package/apps/api/src/db/schema/query-history.ts +16 -0
  30. package/apps/api/src/db/schema/storage-buckets.ts +17 -0
  31. package/apps/api/src/db/schema/storage-objects.ts +18 -0
  32. package/apps/api/src/db/schema/users.ts +14 -0
  33. package/apps/api/src/main.ts +51 -0
  34. package/apps/api/src/members/dto/invite-member-dto.ts +6 -0
  35. package/apps/api/src/members/dto/update-role.dto.ts +8 -0
  36. package/apps/api/src/members/invite.service.ts +147 -0
  37. package/apps/api/src/members/members.controller.ts +56 -0
  38. package/apps/api/src/members/members.module.ts +13 -0
  39. package/apps/api/src/members/members.service.ts +85 -0
  40. package/apps/api/src/orgs/dto/create-org.dto.ts +8 -0
  41. package/apps/api/src/orgs/orgs.controller.ts +27 -0
  42. package/apps/api/src/orgs/orgs.module.ts +12 -0
  43. package/apps/api/src/orgs/orgs.service.ts +86 -0
  44. package/apps/api/src/project-api/project-api.controller.ts +95 -0
  45. package/apps/api/src/project-api/project-api.module.ts +12 -0
  46. package/apps/api/src/project-api/project-api.service.ts +196 -0
  47. package/apps/api/src/project-api/project-key.guard.ts +45 -0
  48. package/apps/api/src/project-api/query-parser.ts +129 -0
  49. package/apps/api/src/project-auth/dto/magic-link.dto.ts +7 -0
  50. package/apps/api/src/project-auth/dto/signin.dto.ts +10 -0
  51. package/apps/api/src/project-auth/dto/signup.dto.ts +11 -0
  52. package/apps/api/src/project-auth/project-auth-dashboard.controller.ts +35 -0
  53. package/apps/api/src/project-auth/project-auth.controller.ts +111 -0
  54. package/apps/api/src/project-auth/project-auth.module.ts +13 -0
  55. package/apps/api/src/project-auth/project-auth.service.ts +440 -0
  56. package/apps/api/src/projects/dto/create-project.dto.ts +9 -0
  57. package/apps/api/src/projects/projects.controller.ts +34 -0
  58. package/apps/api/src/projects/projects.module.ts +12 -0
  59. package/apps/api/src/projects/projects.service.ts +136 -0
  60. package/apps/api/src/realtime/realtime-socket.type.ts +14 -0
  61. package/apps/api/src/realtime/realtime.controller.ts +63 -0
  62. package/apps/api/src/realtime/realtime.gateway.ts +150 -0
  63. package/apps/api/src/realtime/realtime.module.ts +14 -0
  64. package/apps/api/src/realtime/realtime.service.ts +101 -0
  65. package/apps/api/src/realtime/trigger.service.ts +95 -0
  66. package/apps/api/src/sql-editor/dto/execute-query.dto.ts +7 -0
  67. package/apps/api/src/sql-editor/sql-editor.controller.ts +28 -0
  68. package/apps/api/src/sql-editor/sql-editor.module.ts +12 -0
  69. package/apps/api/src/sql-editor/sql-editor.service.ts +154 -0
  70. package/apps/api/src/storage/project-storage.controller.ts +140 -0
  71. package/apps/api/src/storage/storage.controller.ts +118 -0
  72. package/apps/api/src/storage/storage.module.ts +15 -0
  73. package/apps/api/src/storage/storage.service.ts +191 -0
  74. package/apps/api/src/storage/uploadthing.ts +23 -0
  75. package/apps/api/src/table-editor/dto/alter-table.dto.ts +15 -0
  76. package/apps/api/src/table-editor/dto/create-table.dto.ts +54 -0
  77. package/apps/api/src/table-editor/table-editor.controller.ts +117 -0
  78. package/apps/api/src/table-editor/table-editor.module.ts +12 -0
  79. package/apps/api/src/table-editor/table-editor.service.ts +366 -0
  80. package/apps/api/test/app.e2e-spec.ts +29 -0
  81. package/apps/api/test/jest-e2e.json +9 -0
  82. package/apps/api/tsconfig.build.json +4 -0
  83. package/apps/api/tsconfig.json +25 -0
  84. package/apps/web/.env.example +12 -0
  85. package/apps/web/AGENTS.md +9 -0
  86. package/apps/web/CLAUDE.md +1 -0
  87. package/apps/web/README.md +36 -0
  88. package/apps/web/components.json +25 -0
  89. package/apps/web/eslint.config.mjs +18 -0
  90. package/apps/web/next.config.ts +11 -0
  91. package/apps/web/package.json +48 -0
  92. package/apps/web/postcss.config.mjs +7 -0
  93. package/apps/web/public/file.svg +1 -0
  94. package/apps/web/public/globe.svg +1 -0
  95. package/apps/web/public/logo.svg +4 -0
  96. package/apps/web/public/next.svg +1 -0
  97. package/apps/web/public/vercel.svg +1 -0
  98. package/apps/web/public/window.svg +1 -0
  99. package/apps/web/src/app/(dashboard)/layout.tsx +33 -0
  100. package/apps/web/src/app/(dashboard)/organizations/(list)/layout.tsx +7 -0
  101. package/apps/web/src/app/(dashboard)/organizations/(list)/new/page.tsx +26 -0
  102. package/apps/web/src/app/(dashboard)/organizations/(list)/page.tsx +44 -0
  103. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/api/page.tsx +13 -0
  104. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/layout.tsx +19 -0
  105. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/page.tsx +10 -0
  106. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/providers/page.tsx +19 -0
  107. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/url-configuration/page.tsx +24 -0
  108. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/users/page.tsx +13 -0
  109. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/database/page.tsx +29 -0
  110. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/realtime/page.tsx +28 -0
  111. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/sql/page.tsx +21 -0
  112. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/storage/page.tsx +28 -0
  113. package/apps/web/src/app/(dashboard)/organizations/[slug]/layout.tsx +20 -0
  114. package/apps/web/src/app/(dashboard)/organizations/[slug]/projects/page.tsx +62 -0
  115. package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/members/page.tsx +28 -0
  116. package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/page.tsx +10 -0
  117. package/apps/web/src/app/(dashboard)/settings/page.tsx +12 -0
  118. package/apps/web/src/app/dashboard/page.tsx +14 -0
  119. package/apps/web/src/app/favicon.ico +0 -0
  120. package/apps/web/src/app/globals.css +115 -0
  121. package/apps/web/src/app/layout.tsx +36 -0
  122. package/apps/web/src/app/login/page.tsx +199 -0
  123. package/apps/web/src/app/page.tsx +100 -0
  124. package/apps/web/src/app/register/page.tsx +218 -0
  125. package/apps/web/src/components/app-sidebar.tsx +301 -0
  126. package/apps/web/src/components/navbar.tsx +63 -0
  127. package/apps/web/src/components/topNav.tsx +68 -0
  128. package/apps/web/src/components/ui/avatar.tsx +112 -0
  129. package/apps/web/src/components/ui/badge.tsx +48 -0
  130. package/apps/web/src/components/ui/button.tsx +66 -0
  131. package/apps/web/src/components/ui/card.tsx +102 -0
  132. package/apps/web/src/components/ui/checkbox.tsx +32 -0
  133. package/apps/web/src/components/ui/dialog.tsx +168 -0
  134. package/apps/web/src/components/ui/dropdown-menu.tsx +269 -0
  135. package/apps/web/src/components/ui/field.tsx +238 -0
  136. package/apps/web/src/components/ui/input.tsx +19 -0
  137. package/apps/web/src/components/ui/label.tsx +24 -0
  138. package/apps/web/src/components/ui/resizable.tsx +50 -0
  139. package/apps/web/src/components/ui/scroll-area.tsx +55 -0
  140. package/apps/web/src/components/ui/select.tsx +192 -0
  141. package/apps/web/src/components/ui/separator.tsx +28 -0
  142. package/apps/web/src/components/ui/sheet.tsx +147 -0
  143. package/apps/web/src/components/ui/sidebar.tsx +703 -0
  144. package/apps/web/src/components/ui/skeleton.tsx +13 -0
  145. package/apps/web/src/components/ui/switch.tsx +33 -0
  146. package/apps/web/src/components/ui/table.tsx +116 -0
  147. package/apps/web/src/components/ui/tooltip.tsx +57 -0
  148. package/apps/web/src/features/api-docs/api-docs-client.tsx +227 -0
  149. package/apps/web/src/features/api-docs/api-docs-helpers.server.ts +86 -0
  150. package/apps/web/src/features/auth/action.ts +67 -0
  151. package/apps/web/src/features/auth/client.schema.ts +13 -0
  152. package/apps/web/src/features/auth/constants.ts +6 -0
  153. package/apps/web/src/features/auth/cookies.ts +86 -0
  154. package/apps/web/src/features/auth/server.schema.ts +11 -0
  155. package/apps/web/src/features/auth/sign-out.ts +17 -0
  156. package/apps/web/src/features/members/action.ts +66 -0
  157. package/apps/web/src/features/members/client.schema.ts +11 -0
  158. package/apps/web/src/features/members/constants.ts +8 -0
  159. package/apps/web/src/features/members/members-client.tsx +198 -0
  160. package/apps/web/src/features/members/members-helpers.server.ts +21 -0
  161. package/apps/web/src/features/members/server.schema.ts +19 -0
  162. package/apps/web/src/features/organization/action.ts +45 -0
  163. package/apps/web/src/features/organization/client.schema.ts +5 -0
  164. package/apps/web/src/features/organization/constants.tsx +3 -0
  165. package/apps/web/src/features/organization/create-org-form.tsx +66 -0
  166. package/apps/web/src/features/organization/organization-helpers.server.ts +35 -0
  167. package/apps/web/src/features/organization/server.schema.ts +10 -0
  168. package/apps/web/src/features/project-auth/project-auth-action.ts +78 -0
  169. package/apps/web/src/features/project-auth/project-auth-client.schema.ts +18 -0
  170. package/apps/web/src/features/project-auth/project-auth-constants.ts +8 -0
  171. package/apps/web/src/features/project-auth/project-auth-helpers.server.ts +39 -0
  172. package/apps/web/src/features/project-auth/project-auth-providers.tsx +222 -0
  173. package/apps/web/src/features/project-auth/project-auth-server.schema.ts +22 -0
  174. package/apps/web/src/features/project-auth/project-auth-shell.tsx +106 -0
  175. package/apps/web/src/features/project-auth/project-auth-url-config.tsx +151 -0
  176. package/apps/web/src/features/project-auth/project-auth-users.tsx +138 -0
  177. package/apps/web/src/features/projects/action.ts +46 -0
  178. package/apps/web/src/features/projects/client.schema.ts +9 -0
  179. package/apps/web/src/features/projects/constants.ts +6 -0
  180. package/apps/web/src/features/projects/create-project-modal.tsx +117 -0
  181. package/apps/web/src/features/projects/project-helpers.server.ts +38 -0
  182. package/apps/web/src/features/projects/server.schema.ts +10 -0
  183. package/apps/web/src/features/realtime/realtime-client.tsx +294 -0
  184. package/apps/web/src/features/realtime/realtime-helpers.server.ts +40 -0
  185. package/apps/web/src/features/sql-editor/sql-editor-client.tsx +324 -0
  186. package/apps/web/src/features/sql-editor/sql-editor-helpers.server.ts +22 -0
  187. package/apps/web/src/features/storage/storage-client.tsx +500 -0
  188. package/apps/web/src/features/storage/storage-helpers.server.ts +36 -0
  189. package/apps/web/src/features/table-editor/action.ts +97 -0
  190. package/apps/web/src/features/table-editor/add-column-dialog.tsx +184 -0
  191. package/apps/web/src/features/table-editor/client.schema.ts +36 -0
  192. package/apps/web/src/features/table-editor/create-table-drawer.tsx +542 -0
  193. package/apps/web/src/features/table-editor/server.schema.ts +41 -0
  194. package/apps/web/src/features/table-editor/table-editor-client.tsx +737 -0
  195. package/apps/web/src/features/table-editor/table-editor-helpers.server.ts +39 -0
  196. package/apps/web/src/hooks/use-mobile.ts +19 -0
  197. package/apps/web/src/lib/axios.ts +8 -0
  198. package/apps/web/src/lib/utils.ts +6 -0
  199. package/apps/web/src/proxy.ts +72 -0
  200. package/apps/web/src/server-utils/utils.ts +10 -0
  201. package/apps/web/tsconfig.json +34 -0
  202. package/package.json +22 -0
  203. package/packages/apiDatabase-js/package.json +39 -0
  204. package/packages/apiDatabase-js/src/auth/index.ts +121 -0
  205. package/packages/apiDatabase-js/src/client.ts +25 -0
  206. package/packages/apiDatabase-js/src/db/index.ts +15 -0
  207. package/packages/apiDatabase-js/src/db/query-builder.ts +194 -0
  208. package/packages/apiDatabase-js/src/index.ts +16 -0
  209. package/packages/apiDatabase-js/src/realtime/index.ts +84 -0
  210. package/packages/apiDatabase-js/src/storage/index.ts +130 -0
  211. package/packages/apiDatabase-js/tsconfig.json +13 -0
  212. package/packages/apiDatabase-js/tsdown.config.ts +20 -0
  213. package/packages/constants/package.json +6 -0
  214. package/packages/constants/src/index.ts +76 -0
  215. package/packages/tsconfig.json +13 -0
  216. package/packages/tsdown.config.ts +20 -0
  217. package/packages/types/package.json +6 -0
  218. package/packages/types/src/index.ts +254 -0
  219. package/pnpm-workspace.yaml +10 -0
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "api",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "author": "",
6
+ "private": true,
7
+ "license": "UNLICENSED",
8
+ "scripts": {
9
+ "build": "nest build",
10
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11
+ "start": "nest start",
12
+ "start:dev": "nest start --watch",
13
+ "start:debug": "nest start --debug --watch",
14
+ "start:prod": "node dist/main",
15
+ "db:generate": "drizzle-kit generate",
16
+ "db:migrate": "drizzle-kit migrate",
17
+ "db:push": "drizzle-kit push",
18
+ "db:studio": "drizzle-kit studio",
19
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
20
+ "test": "jest",
21
+ "test:watch": "jest --watch",
22
+ "test:cov": "jest --coverage",
23
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
24
+ "test:e2e": "jest --config ./test/jest-e2e.json"
25
+ },
26
+ "dependencies": {
27
+ "@neondatabase/serverless": "^1.1.0",
28
+ "@nestjs/common": "^11.0.1",
29
+ "@nestjs/config": "^4.0.4",
30
+ "@nestjs/core": "^11.0.1",
31
+ "@nestjs/jwt": "^11.0.2",
32
+ "@nestjs/platform-express": "^11.0.1",
33
+ "@nestjs/platform-socket.io": "^11.1.27",
34
+ "@nestjs/websockets": "^11.1.27",
35
+ "@apiDatabase/types": "workspace:*",
36
+ "@apiDatabase/constants": "workspace:*",
37
+ "bcrypt": "^6.0.0",
38
+ "class-transformer": "^0.5.1",
39
+ "class-validator": "^0.15.1",
40
+ "cookie-parser": "^1.4.7",
41
+ "dotenv": "^17.4.2",
42
+ "drizzle-orm": "^0.45.2",
43
+ "pg": "^8.22.0",
44
+ "reflect-metadata": "^0.2.2",
45
+ "resend": "^6.16.0",
46
+ "rxjs": "^7.8.1",
47
+ "slugify": "^1.6.9",
48
+ "socket.io": "^4.8.3",
49
+ "uploadthing": "^7.7.4",
50
+ "ws": "^8.21.0"
51
+ },
52
+ "devDependencies": {
53
+ "@eslint/eslintrc": "^3.2.0",
54
+ "@eslint/js": "^9.18.0",
55
+ "@nestjs/cli": "^11.0.0",
56
+ "@nestjs/schematics": "^11.0.0",
57
+ "@nestjs/testing": "^11.0.1",
58
+ "@types/bcrypt": "^6.0.0",
59
+ "@types/cookie-parser": "^1.4.10",
60
+ "@types/express": "^5.0.0",
61
+ "@types/jest": "^30.0.0",
62
+ "@types/node": "^24.0.0",
63
+ "@types/pg": "^8.20.0",
64
+ "@types/supertest": "^7.0.0",
65
+ "@types/ws": "^8.18.1",
66
+ "drizzle-kit": "^0.31.10",
67
+ "eslint": "^9.18.0",
68
+ "eslint-config-prettier": "^10.0.1",
69
+ "eslint-plugin-prettier": "^5.2.2",
70
+ "globals": "^17.0.0",
71
+ "jest": "^30.0.0",
72
+ "prettier": "^3.4.2",
73
+ "source-map-support": "^0.5.21",
74
+ "supertest": "^7.0.0",
75
+ "ts-jest": "^29.2.5",
76
+ "ts-loader": "^9.5.2",
77
+ "ts-node": "^10.9.2",
78
+ "tsconfig-paths": "^4.2.0",
79
+ "typescript": "^5.7.3",
80
+ "typescript-eslint": "^8.20.0"
81
+ },
82
+ "jest": {
83
+ "moduleFileExtensions": [
84
+ "js",
85
+ "json",
86
+ "ts"
87
+ ],
88
+ "rootDir": "src",
89
+ "testRegex": ".*\\.spec\\.ts$",
90
+ "transform": {
91
+ "^.+\\.(t|j)s$": "ts-jest"
92
+ },
93
+ "collectCoverageFrom": [
94
+ "**/*.(t|j)s"
95
+ ],
96
+ "coverageDirectory": "../coverage",
97
+ "testEnvironment": "node"
98
+ }
99
+ }
@@ -0,0 +1,25 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { AppController } from './app.controller';
3
+ import { AppService } from './app.service';
4
+
5
+ describe('AppController', () => {
6
+ let appController: AppController;
7
+
8
+ beforeEach(async () => {
9
+ const app: TestingModule = await Test.createTestingModule({
10
+ controllers: [AppController],
11
+ providers: [AppService],
12
+ }).compile();
13
+
14
+ appController = app.get<AppController>(AppController);
15
+ });
16
+
17
+ describe('root', () => {
18
+ it('should return a healthy status', () => {
19
+ expect(appController.health()).toEqual({
20
+ status: 'ok',
21
+ timestamp: expect.any(String),
22
+ });
23
+ });
24
+ });
25
+ });
@@ -0,0 +1,9 @@
1
+ import { Controller, Get } from '@nestjs/common';
2
+
3
+ @Controller()
4
+ export class AppController {
5
+ @Get('health')
6
+ health() {
7
+ return { status: 'ok', timestamp: new Date().toISOString() };
8
+ }
9
+ }
@@ -0,0 +1,37 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { AppController } from './app.controller';
3
+ import { AppService } from './app.service';
4
+ import { ConfigModule } from '@nestjs/config';
5
+ import { DbModule } from './db/db.module';
6
+ import { AuthModule } from './auth/auth.module';
7
+ import { OrgsModule } from './orgs/orgs.module';
8
+ import { MembersModule } from './members/members.module';
9
+ import { ProjectsModule } from './projects/projects.module';
10
+ import { TableEditorModule } from './table-editor/table-editor.module';
11
+ import { ProjectApiModule } from './project-api/project-api.module';
12
+ import { SqlEditorModule } from './sql-editor/sql-editor.module';
13
+ import { RealtimeModule } from './realtime/realtime.module';
14
+ import { StorageModule } from './storage/storage.module';
15
+ import { ProjectAuthModule } from './project-auth/project-auth.module';
16
+
17
+ @Module({
18
+ imports: [
19
+ ConfigModule.forRoot({
20
+ isGlobal: true,
21
+ }),
22
+ DbModule,
23
+ AuthModule,
24
+ OrgsModule,
25
+ MembersModule,
26
+ ProjectsModule,
27
+ TableEditorModule,
28
+ ProjectApiModule,
29
+ SqlEditorModule,
30
+ RealtimeModule,
31
+ StorageModule,
32
+ ProjectAuthModule,
33
+ ],
34
+ controllers: [AppController],
35
+ providers: [AppService],
36
+ })
37
+ export class AppModule {}
@@ -0,0 +1,8 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ @Injectable()
4
+ export class AppService {
5
+ getHello(): string {
6
+ return 'Hello World!';
7
+ }
8
+ }
@@ -0,0 +1,113 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Get,
5
+ HttpCode,
6
+ Post,
7
+ Query,
8
+ Redirect,
9
+ Req,
10
+ Res,
11
+ UseGuards,
12
+ } from '@nestjs/common';
13
+ import type { Request, Response } from 'express';
14
+ import { ConfigService } from '@nestjs/config';
15
+ import { COOKIE_KEYS } from '@apiDatabase/constants';
16
+ import type { JwtPayload } from '@apiDatabase/types';
17
+ import { AuthService } from './auth.service';
18
+ import { RegisterDto } from './dto/register.dto';
19
+ import { LoginDto } from './dto/login.dto';
20
+ import { JwtAuthGuard } from './guards/jwt-auth-guard';
21
+ import { CurrentUser } from './decorators/current-user-decorator';
22
+ import { InviteService } from 'src/members/invite.service';
23
+
24
+ @Controller('auth')
25
+ export class AuthController {
26
+ constructor(
27
+ private authService: AuthService,
28
+ private configService: ConfigService,
29
+ private inviteService: InviteService,
30
+ ) {}
31
+
32
+ @Post('register')
33
+ async register(
34
+ @Body() dto: RegisterDto,
35
+ @Res({ passthrough: true }) res: Response,
36
+ ) {
37
+ const tokens = await this.authService.register(dto);
38
+ this.authService.setTokenCookies(res, tokens);
39
+ return { message: 'Registered successfully' };
40
+ }
41
+
42
+ @Post('login')
43
+ @HttpCode(200)
44
+ async login(
45
+ @Body() dto: LoginDto,
46
+ @Res({ passthrough: true }) res: Response,
47
+ ) {
48
+ const tokens = await this.authService.login(dto);
49
+ this.authService.setTokenCookies(res, tokens);
50
+ return { message: 'Logged in successfully' };
51
+ }
52
+
53
+ @Post('logout')
54
+ @HttpCode(200)
55
+ logout(@Res({ passthrough: true }) res: Response) {
56
+ this.authService.clearTokenCookies(res);
57
+ return { message: 'Logged out successfully' };
58
+ }
59
+
60
+ @Post('refresh')
61
+ @HttpCode(200)
62
+ async refresh(
63
+ @Req() req: Request,
64
+ @Res({ passthrough: true }) res: Response,
65
+ ) {
66
+ const refreshToken = req.cookies[COOKIE_KEYS.REFRESH_TOKEN] as string;
67
+ const tokens = await this.authService.refreshTokens(refreshToken);
68
+ this.authService.setTokenCookies(res, tokens);
69
+ return { message: 'Tokens refreshed' };
70
+ }
71
+
72
+ @UseGuards(JwtAuthGuard)
73
+ @Get('me')
74
+ me(@CurrentUser() user: JwtPayload) {
75
+ return user;
76
+ }
77
+
78
+ @Get('google')
79
+ @Redirect()
80
+ googleLogin() {
81
+ return { url: this.authService.getGoogleAuthUrl() };
82
+ }
83
+
84
+ @Get('google/callback')
85
+ async googleCallback(@Query('code') code: string, @Res() res: Response) {
86
+ const tokens = await this.authService.handleGoogleCallback(code);
87
+ this.authService.setTokenCookies(res, tokens);
88
+ return res.redirect(
89
+ `${this.configService.get<string>('WEB_URL')}/dashboard`,
90
+ );
91
+ }
92
+
93
+ @Get('github')
94
+ @Redirect()
95
+ githubLogin() {
96
+ return { url: this.authService.getGithubAuthUrl() };
97
+ }
98
+
99
+ @Get('github/callback')
100
+ async githubCallback(@Query('code') code: string, @Res() res: Response) {
101
+ const tokens = await this.authService.handleGithubCallback(code);
102
+ this.authService.setTokenCookies(res, tokens);
103
+ return res.redirect(
104
+ `${this.configService.get<string>('WEB_URL')}/dashboard`,
105
+ );
106
+ }
107
+
108
+ @Get('invite/accept')
109
+ async acceptInvite(@Query('token') token: string, @Res() res: Response) {
110
+ await this.inviteService.acceptInvite(token);
111
+ return res.redirect(`${this.configService.get('WEB_URL')}/dashboard`);
112
+ }
113
+ }
@@ -0,0 +1,15 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { JwtModule } from '@nestjs/jwt';
3
+ import { AuthService } from './auth.service';
4
+ import { AuthController } from './auth.controller';
5
+ import { JwtAuthGuard } from './guards/jwt-auth-guard';
6
+ import { MembersModule } from 'src/members/members.module';
7
+
8
+ @Module({
9
+ imports: [JwtModule.register({}), MembersModule],
10
+
11
+ providers: [AuthService, JwtAuthGuard],
12
+ controllers: [AuthController],
13
+ exports: [JwtModule, JwtAuthGuard],
14
+ })
15
+ export class AuthModule {}
@@ -0,0 +1,309 @@
1
+ import {
2
+ ConflictException,
3
+ Injectable,
4
+ UnauthorizedException,
5
+ } from '@nestjs/common';
6
+ import { JwtService } from '@nestjs/jwt';
7
+ import { ConfigService } from '@nestjs/config';
8
+ import { Response } from 'express';
9
+ import * as bcrypt from 'bcrypt';
10
+ import { randomBytes } from 'crypto';
11
+ import slugify from 'slugify';
12
+ import { eq } from 'drizzle-orm';
13
+ import { COOKIE_KEYS } from '@apiDatabase/constants';
14
+ import { DrizzleService } from '../db/drizzle.service';
15
+ import { users, organizations, orgMembers } from '../db/schema';
16
+ import { RegisterDto } from './dto/register.dto';
17
+ import { LoginDto } from './dto/login.dto';
18
+ import { JwtPayload } from '@apiDatabase/types';
19
+
20
+ @Injectable()
21
+ export class AuthService {
22
+ constructor(
23
+ private drizzle: DrizzleService,
24
+ private jwtService: JwtService,
25
+ private configService: ConfigService,
26
+ ) {}
27
+
28
+ // Cookie helpers
29
+ setTokenCookies(
30
+ res: Response,
31
+ tokens: { accessToken: string; refreshToken: string },
32
+ ) {
33
+ const isProduction = this.configService.get('NODE_ENV') === 'production';
34
+
35
+ res.cookie(COOKIE_KEYS.ACCESS_TOKEN, tokens.accessToken, {
36
+ httpOnly: true,
37
+ secure: isProduction,
38
+ sameSite: 'lax',
39
+ maxAge: 15 * 60 * 1000,
40
+ });
41
+
42
+ res.cookie(COOKIE_KEYS.REFRESH_TOKEN, tokens.refreshToken, {
43
+ httpOnly: true,
44
+ secure: isProduction,
45
+ sameSite: 'lax',
46
+ maxAge: 7 * 24 * 60 * 60 * 1000,
47
+ });
48
+ }
49
+
50
+ clearTokenCookies(res: Response) {
51
+ res.clearCookie(COOKIE_KEYS.ACCESS_TOKEN);
52
+ res.clearCookie(COOKIE_KEYS.REFRESH_TOKEN);
53
+ }
54
+
55
+ // Slug helper
56
+ private generateOrgSlug(name: string): string {
57
+ const base = slugify(`${name}-org`, { lower: true, strict: true });
58
+ const suffix = randomBytes(3).toString('hex');
59
+ return `${base}-${suffix}`;
60
+ }
61
+
62
+ // JWT signing
63
+ private signTokens(userId: string, email: string) {
64
+ const payload = { sub: userId, email };
65
+
66
+ const accessToken = this.jwtService.sign(payload, {
67
+ secret: this.configService.get('JWT_ACCESS_SECRET'),
68
+ expiresIn: this.configService.get('JWT_ACCESS_EXPIRES_IN'),
69
+ });
70
+
71
+ const refreshToken = this.jwtService.sign(payload, {
72
+ secret: this.configService.get('JWT_REFRESH_SECRET'),
73
+ expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN'),
74
+ });
75
+
76
+ return { accessToken, refreshToken };
77
+ }
78
+
79
+ // Email + Password
80
+ async register(dto: RegisterDto) {
81
+ const existing = await this.drizzle.db
82
+ .select()
83
+ .from(users)
84
+ .where(eq(users.email, dto.email))
85
+ .limit(1);
86
+
87
+ if (existing.length > 0) {
88
+ throw new ConflictException('Email already in use');
89
+ }
90
+
91
+ const passwordHash = await bcrypt.hash(dto.password, 12);
92
+
93
+ const [user] = await this.drizzle.db
94
+ .insert(users)
95
+ .values({ email: dto.email, name: dto.name, passwordHash })
96
+ .returning();
97
+
98
+ const [org] = await this.drizzle.db
99
+ .insert(organizations)
100
+ .values({
101
+ name: `${dto.name}'s Org`,
102
+ slug: this.generateOrgSlug(dto.name),
103
+ })
104
+ .returning();
105
+
106
+ await this.drizzle.db.insert(orgMembers).values({
107
+ orgId: org.id,
108
+ userId: user.id,
109
+ role: 'admin',
110
+ });
111
+
112
+ return this.signTokens(user.id, user.email);
113
+ }
114
+
115
+ async login(dto: LoginDto) {
116
+ const [user] = await this.drizzle.db
117
+ .select()
118
+ .from(users)
119
+ .where(eq(users.email, dto.email))
120
+ .limit(1);
121
+
122
+ if (!user || !user.passwordHash) {
123
+ throw new UnauthorizedException('Invalid credentials');
124
+ }
125
+
126
+ const passwordMatch = await bcrypt.compare(dto.password, user.passwordHash);
127
+ if (!passwordMatch) {
128
+ throw new UnauthorizedException('Invalid credentials');
129
+ }
130
+
131
+ return this.signTokens(user.id, user.email);
132
+ }
133
+
134
+ async refreshTokens(refreshToken: string) {
135
+ try {
136
+ const payload = this.jwtService.verify<JwtPayload>(refreshToken, {
137
+ secret: this.configService.get('JWT_REFRESH_SECRET'),
138
+ });
139
+
140
+ const [user] = await this.drizzle.db
141
+ .select()
142
+ .from(users)
143
+ .where(eq(users.id, payload.sub))
144
+ .limit(1);
145
+
146
+ if (!user) throw new UnauthorizedException();
147
+
148
+ return this.signTokens(user.id, user.email);
149
+ } catch {
150
+ throw new UnauthorizedException('Invalid refresh token');
151
+ }
152
+ }
153
+
154
+ // OAuth upsert helpers
155
+ async handleOAuthUser(profile: {
156
+ email: string;
157
+ name: string;
158
+ avatarUrl: string | null;
159
+ }) {
160
+ let [user] = await this.drizzle.db
161
+ .select()
162
+ .from(users)
163
+ .where(eq(users.email, profile.email))
164
+ .limit(1);
165
+
166
+ if (!user) {
167
+ const [newUser] = await this.drizzle.db
168
+ .insert(users)
169
+ .values({
170
+ email: profile.email,
171
+ name: profile.name,
172
+ avatarUrl: profile.avatarUrl,
173
+ })
174
+ .returning();
175
+
176
+ user = newUser;
177
+
178
+ const [org] = await this.drizzle.db
179
+ .insert(organizations)
180
+ .values({
181
+ name: `${profile.name}'s Org`,
182
+ slug: this.generateOrgSlug(profile.name),
183
+ })
184
+ .returning();
185
+
186
+ await this.drizzle.db.insert(orgMembers).values({
187
+ orgId: org.id,
188
+ userId: user.id,
189
+ role: 'admin',
190
+ });
191
+ }
192
+
193
+ return this.signTokens(user.id, user.email);
194
+ }
195
+
196
+ getGoogleAuthUrl() {
197
+ const params = new URLSearchParams({
198
+ client_id: this.configService.get<string>('GOOGLE_CLIENT_ID')!,
199
+ redirect_uri: this.configService.get<string>('GOOGLE_CALLBACK_URL')!,
200
+ response_type: 'code',
201
+ scope: 'openid email profile',
202
+ access_type: 'offline',
203
+ });
204
+
205
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
206
+ }
207
+
208
+ async handleGoogleCallback(code: string) {
209
+ const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
212
+ body: new URLSearchParams({
213
+ code,
214
+ client_id: this.configService.get<string>('GOOGLE_CLIENT_ID')!,
215
+ client_secret: this.configService.get<string>('GOOGLE_CLIENT_SECRET')!,
216
+ redirect_uri: this.configService.get<string>('GOOGLE_CALLBACK_URL')!,
217
+ grant_type: 'authorization_code',
218
+ }),
219
+ });
220
+
221
+ const tokenData = (await tokenRes.json()) as { access_token: string };
222
+
223
+ const profileRes = await fetch(
224
+ 'https://www.googleapis.com/oauth2/v3/userinfo',
225
+ {
226
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
227
+ },
228
+ );
229
+
230
+ const profile = (await profileRes.json()) as {
231
+ email: string;
232
+ name: string;
233
+ picture: string | null;
234
+ };
235
+
236
+ return this.handleOAuthUser({
237
+ email: profile.email,
238
+ name: profile.name,
239
+ avatarUrl: profile.picture ?? null,
240
+ });
241
+ }
242
+
243
+ getGithubAuthUrl() {
244
+ const params = new URLSearchParams({
245
+ client_id: this.configService.get<string>('GITHUB_CLIENT_ID')!,
246
+ redirect_uri: this.configService.get<string>('GITHUB_CALLBACK_URL')!,
247
+ scope: 'read:user user:email',
248
+ });
249
+
250
+ return `https://github.com/login/oauth/authorize?${params.toString()}`;
251
+ }
252
+
253
+ async handleGithubCallback(code: string) {
254
+ const tokenRes = await fetch(
255
+ 'https://github.com/login/oauth/access_token',
256
+ {
257
+ method: 'POST',
258
+ headers: {
259
+ 'Content-Type': 'application/json',
260
+ Accept: 'application/json',
261
+ },
262
+ body: JSON.stringify({
263
+ client_id: this.configService.get<string>('GITHUB_CLIENT_ID'),
264
+ client_secret: this.configService.get<string>('GITHUB_CLIENT_SECRET'),
265
+ code,
266
+ redirect_uri: this.configService.get<string>('GITHUB_CALLBACK_URL'),
267
+ }),
268
+ },
269
+ );
270
+
271
+ const tokenData = (await tokenRes.json()) as { access_token: string };
272
+
273
+ const profileRes = await fetch('https://api.github.com/user', {
274
+ headers: {
275
+ Authorization: `Bearer ${tokenData.access_token}`,
276
+ Accept: 'application/vnd.github+json',
277
+ },
278
+ });
279
+
280
+ const profile = (await profileRes.json()) as {
281
+ email: string | null;
282
+ name: string | null;
283
+ login: string;
284
+ avatar_url: string | null;
285
+ };
286
+
287
+ let email = profile.email;
288
+ if (!email) {
289
+ const emailRes = await fetch('https://api.github.com/user/emails', {
290
+ headers: {
291
+ Authorization: `Bearer ${tokenData.access_token}`,
292
+ Accept: 'application/vnd.github+json',
293
+ },
294
+ });
295
+ const emails = (await emailRes.json()) as {
296
+ email: string;
297
+ primary: boolean;
298
+ verified: boolean;
299
+ }[];
300
+ email = emails.find((e) => e.primary && e.verified)?.email ?? null;
301
+ }
302
+
303
+ return this.handleOAuthUser({
304
+ email: email!,
305
+ name: profile.name ?? profile.login,
306
+ avatarUrl: profile.avatar_url ?? null,
307
+ });
308
+ }
309
+ }
@@ -0,0 +1,10 @@
1
+ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2
+ import { Request } from 'express';
3
+ import type { JwtPayload } from '@apiDatabase/types';
4
+
5
+ export const CurrentUser = createParamDecorator(
6
+ (_data: unknown, ctx: ExecutionContext): JwtPayload => {
7
+ const request = ctx.switchToHttp().getRequest<Request>();
8
+ return request['user'] as JwtPayload;
9
+ },
10
+ );
@@ -0,0 +1,7 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+ import type { OrgRole } from '@apiDatabase/types';
3
+
4
+ export const ORG_ROLE_KEY = 'orgRole';
5
+
6
+ export const RequireOrgRole = (role: OrgRole) =>
7
+ SetMetadata(ORG_ROLE_KEY, role);
@@ -0,0 +1,10 @@
1
+ import { IsEmail, IsString } from 'class-validator';
2
+ import type { LoginInput } from '@apiDatabase/types';
3
+
4
+ export class LoginDto implements LoginInput {
5
+ @IsEmail()
6
+ email!: string;
7
+
8
+ @IsString()
9
+ password!: string;
10
+ }
@@ -0,0 +1,14 @@
1
+ import { IsEmail, IsString, MinLength } from 'class-validator';
2
+ import type { RegisterInput } from '@apiDatabase/types';
3
+
4
+ export class RegisterDto implements RegisterInput {
5
+ @IsEmail()
6
+ email!: string;
7
+
8
+ @IsString()
9
+ @MinLength(8)
10
+ password!: string;
11
+
12
+ @IsString()
13
+ name!: string;
14
+ }