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,11 @@
1
+ import { z } from "zod";
2
+ import { AUTH_INTENT } from "./constants";
3
+ import { loginSchema, registerSchema } from "./client.schema";
4
+
5
+ export const authServerSchema = z.discriminatedUnion("intent", [
6
+ z.object({ intent: z.literal(AUTH_INTENT.LOGIN), ...loginSchema.shape }),
7
+ z.object({
8
+ intent: z.literal(AUTH_INTENT.REGISTER),
9
+ ...registerSchema.shape,
10
+ }),
11
+ ]);
@@ -0,0 +1,17 @@
1
+ "use server";
2
+
3
+ import { redirect } from "next/navigation";
4
+ import { cookies } from "next/headers";
5
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
6
+
7
+ export async function signOut() {
8
+ await fetch(`${process.env.API_URL}/auth/logout`, {
9
+ method: "POST",
10
+ });
11
+
12
+ const cookieStore = await cookies();
13
+ cookieStore.delete(COOKIE_KEYS.ACCESS_TOKEN);
14
+ cookieStore.delete(COOKIE_KEYS.REFRESH_TOKEN);
15
+
16
+ redirect("/login");
17
+ }
@@ -0,0 +1,66 @@
1
+ "use server";
2
+
3
+ import { revalidatePath } from "next/cache";
4
+ import { membersServerSchema } from "./server.schema";
5
+ import { MEMBERS_INTENT } from "./constants";
6
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
7
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
8
+ import apiClient from "@/lib/axios";
9
+
10
+ export type MembersActionState = {
11
+ error?: string;
12
+ success?: string;
13
+ };
14
+
15
+ export async function membersAction(
16
+ { slug }: { slug: string },
17
+ _prev: MembersActionState,
18
+ formData: FormData,
19
+ ): Promise<MembersActionState> {
20
+ const raw = Object.fromEntries(formData);
21
+ const parsed = membersServerSchema.safeParse(raw);
22
+
23
+ if (!parsed.success) {
24
+ return { error: parsed.error.flatten().formErrors[0] ?? "Invalid input" };
25
+ }
26
+
27
+ const token = await retrieveTokenFromCookie();
28
+ const { intent, ...data } = parsed.data;
29
+
30
+ try {
31
+ switch (intent) {
32
+ case MEMBERS_INTENT.INVITE: {
33
+ await apiClient.post(
34
+ `/orgs/${slug}/members/invite`,
35
+ { email: (data as { email: string }).email },
36
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
37
+ );
38
+ revalidatePath(`/organizations/${slug}/settings/members`);
39
+ return { success: "Invite sent!" };
40
+ }
41
+
42
+ case MEMBERS_INTENT.UPDATE_ROLE: {
43
+ const { memberId, role } = data as { memberId: string; role: string };
44
+ await apiClient.patch(
45
+ `/orgs/${slug}/members/${memberId}/role`,
46
+ { role },
47
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
48
+ );
49
+ revalidatePath(`/organizations/${slug}/settings/members`);
50
+ return { success: "Role updated" };
51
+ }
52
+
53
+ case MEMBERS_INTENT.REMOVE: {
54
+ const { memberId } = data as { memberId: string };
55
+ await apiClient.delete(`/orgs/${slug}/members/${memberId}`, {
56
+ headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` },
57
+ });
58
+ revalidatePath(`/organizations/${slug}/settings/members`);
59
+ return { success: "Member removed" };
60
+ }
61
+ }
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ } catch (err: any) {
64
+ return { error: err.response?.data?.message ?? "Something went wrong" };
65
+ }
66
+ }
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import { ORG_ROLES } from "@apiDatabase/constants";
3
+ import type { InviteMemberInput, UpdateRoleInput } from "@apiDatabase/types";
4
+
5
+ export const inviteSchema = z.object({
6
+ email: z.string().email("Invalid email"),
7
+ }) satisfies z.ZodType<InviteMemberInput>;
8
+
9
+ export const updateRoleSchema = z.object({
10
+ role: z.enum([ORG_ROLES.ADMIN, ORG_ROLES.DEVELOPER]),
11
+ }) satisfies z.ZodType<UpdateRoleInput>;
@@ -0,0 +1,8 @@
1
+ export const MEMBERS_INTENT = {
2
+ INVITE: "INVITE",
3
+ UPDATE_ROLE: "UPDATE_ROLE",
4
+ REMOVE: "REMOVE",
5
+ } as const;
6
+
7
+ export type MembersIntent =
8
+ (typeof MEMBERS_INTENT)[keyof typeof MEMBERS_INTENT];
@@ -0,0 +1,198 @@
1
+ "use client";
2
+
3
+ import { useActionState } from "react";
4
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Field, FieldError, FieldGroup } from "@/components/ui/field";
8
+ import { Input } from "@/components/ui/input";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@/components/ui/select";
16
+ import { ORG_ROLES } from "@apiDatabase/constants";
17
+ import { OrgMemberWithUser, OrgRole } from "@apiDatabase/types";
18
+ import { membersAction } from "./action";
19
+ import { Controller, useForm } from "react-hook-form";
20
+ import z from "zod";
21
+ import { inviteSchema } from "./client.schema";
22
+ import { zodResolver } from "@hookform/resolvers/zod";
23
+ import { MEMBERS_INTENT } from "./constants";
24
+ import { Badge } from "@/components/ui/badge";
25
+
26
+ interface MembersClientProps {
27
+ slug: string;
28
+ members: OrgMemberWithUser[];
29
+ currentUserRole: OrgRole;
30
+ }
31
+
32
+ export function MembersClient({
33
+ currentUserRole,
34
+ members,
35
+ slug,
36
+ }: MembersClientProps) {
37
+ const isAdmin = currentUserRole === ORG_ROLES.ADMIN;
38
+ const boundAction = membersAction.bind(null, { slug });
39
+ const [state, formAction, isPending] = useActionState(boundAction, {});
40
+
41
+ const form = useForm<z.infer<typeof inviteSchema>>({
42
+ resolver: zodResolver(inviteSchema),
43
+ defaultValues: { email: "" },
44
+ });
45
+
46
+ return (
47
+ <div className="space-y-6">
48
+ {/* ── Invite form — admin only ─────────────────────────────── */}
49
+ {isAdmin && (
50
+ <Card>
51
+ <CardHeader>
52
+ <CardTitle className="text-base">Invite a member</CardTitle>
53
+ </CardHeader>
54
+ <CardContent>
55
+ {state.error && (
56
+ <p className="text-sm text-destructive mb-3">{state.error}</p>
57
+ )}
58
+ {state.success && (
59
+ <p className="text-sm text-green-600 mb-3">{state.success}</p>
60
+ )}
61
+ <form
62
+ action={formAction}
63
+ noValidate
64
+ onSubmitCapture={async (e) => {
65
+ const ok = await form.trigger(undefined, { shouldFocus: true });
66
+ if (!ok) e.preventDefault();
67
+ }}
68
+ className="flex gap-2"
69
+ >
70
+ <FieldGroup className="flex-1">
71
+ <Controller
72
+ name="email"
73
+ control={form.control}
74
+ render={({ field, fieldState }) => (
75
+ <Field data-invalid={fieldState.invalid}>
76
+ <Input
77
+ {...field}
78
+ type="email"
79
+ placeholder="colleague@example.com"
80
+ aria-invalid={fieldState.invalid}
81
+ />
82
+ {fieldState.invalid && (
83
+ <FieldError errors={[fieldState.error]} />
84
+ )}
85
+ </Field>
86
+ )}
87
+ />
88
+ </FieldGroup>
89
+ <Button
90
+ type="submit"
91
+ name="intent"
92
+ value={MEMBERS_INTENT.INVITE}
93
+ disabled={isPending}
94
+ >
95
+ Send invite
96
+ </Button>
97
+ </form>
98
+ </CardContent>
99
+ </Card>
100
+ )}
101
+
102
+ {/* ── Members list ─────────────────────────────────────────── */}
103
+ <Card>
104
+ <CardHeader>
105
+ <CardTitle className="text-base">
106
+ {members.length} {members.length === 1 ? "member" : "members"}
107
+ </CardTitle>
108
+ </CardHeader>
109
+ <CardContent className="divide-y divide-border">
110
+ {members.map((member) => (
111
+ <div
112
+ key={member.id}
113
+ className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
114
+ >
115
+ <div className="flex items-center gap-3">
116
+ <Avatar className="size-8">
117
+ <AvatarFallback>
118
+ {(member.user.name ?? member.user.email)
119
+ .charAt(0)
120
+ .toUpperCase()}
121
+ </AvatarFallback>
122
+ </Avatar>
123
+ <div>
124
+ <p className="text-sm font-medium">
125
+ {member.user.name ?? "—"}
126
+ </p>
127
+ <p className="text-xs text-muted-foreground">
128
+ {member.user.email}
129
+ </p>
130
+ </div>
131
+ </div>
132
+
133
+ <div className="flex items-center gap-2">
134
+ {/* role badge / selector */}
135
+ {isAdmin ? (
136
+ <form action={formAction}>
137
+ <input type="hidden" name="memberId" value={member.id} />
138
+ <Select
139
+ name="role"
140
+ defaultValue={member.role}
141
+ onValueChange={(value) => {
142
+ const form = document.createElement("form");
143
+ // handled by the select's own form submission below
144
+ }}
145
+ >
146
+ <SelectTrigger className="h-7 text-xs w-28">
147
+ <SelectValue />
148
+ </SelectTrigger>
149
+ <SelectContent>
150
+ <SelectItem value={ORG_ROLES.ADMIN}>Admin</SelectItem>
151
+ <SelectItem value={ORG_ROLES.DEVELOPER}>
152
+ Developer
153
+ </SelectItem>
154
+ </SelectContent>
155
+ </Select>
156
+ <Button
157
+ type="submit"
158
+ name="intent"
159
+ value={MEMBERS_INTENT.UPDATE_ROLE}
160
+ size="sm"
161
+ variant="ghost"
162
+ className="h-7 text-xs"
163
+ disabled={isPending}
164
+ >
165
+ Save
166
+ </Button>
167
+ </form>
168
+ ) : (
169
+ <Badge variant="secondary" className="capitalize">
170
+ {member.role}
171
+ </Badge>
172
+ )}
173
+
174
+ {/* remove button — admin only, can't remove self */}
175
+ {isAdmin && (
176
+ <form action={formAction}>
177
+ <input type="hidden" name="memberId" value={member.id} />
178
+ <Button
179
+ type="submit"
180
+ name="intent"
181
+ value={MEMBERS_INTENT.REMOVE}
182
+ size="sm"
183
+ variant="ghost"
184
+ className="h-7 text-xs text-destructive hover:text-destructive"
185
+ disabled={isPending}
186
+ >
187
+ Remove
188
+ </Button>
189
+ </form>
190
+ )}
191
+ </div>
192
+ </div>
193
+ ))}
194
+ </CardContent>
195
+ </Card>
196
+ </div>
197
+ );
198
+ }
@@ -0,0 +1,21 @@
1
+ import { redirect } from "next/navigation";
2
+ import apiClient from "@/lib/axios";
3
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
4
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
5
+ import type { OrgMemberWithUser } from "@apiDatabase/types";
6
+
7
+ export async function retrieveMembersFromApi(
8
+ slug: string,
9
+ ): Promise<OrgMemberWithUser[]> {
10
+ const token = await retrieveTokenFromCookie();
11
+
12
+ try {
13
+ const { data } = await apiClient.get<OrgMemberWithUser[]>(
14
+ `/orgs/${slug}/members`,
15
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
16
+ );
17
+ return data;
18
+ } catch {
19
+ redirect("/organizations");
20
+ }
21
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { MEMBERS_INTENT } from "./constants";
3
+ import { inviteSchema, updateRoleSchema } from "./client.schema";
4
+
5
+ export const membersServerSchema = z.discriminatedUnion("intent", [
6
+ z.object({
7
+ intent: z.literal(MEMBERS_INTENT.INVITE),
8
+ ...inviteSchema.shape,
9
+ }),
10
+ z.object({
11
+ intent: z.literal(MEMBERS_INTENT.UPDATE_ROLE),
12
+ memberId: z.string(),
13
+ ...updateRoleSchema.shape,
14
+ }),
15
+ z.object({
16
+ intent: z.literal(MEMBERS_INTENT.REMOVE),
17
+ memberId: z.string(),
18
+ }),
19
+ ]);
@@ -0,0 +1,45 @@
1
+ "use server";
2
+
3
+ import { redirect } from "next/navigation";
4
+ import { revalidatePath } from "next/cache";
5
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
6
+ import apiClient from "@/lib/axios";
7
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
8
+ import { ORGANIZATION_INTENT } from "./constants";
9
+ import { organizationServerSchema } from "./server.schema";
10
+
11
+ export type OrganizationActionState = {
12
+ error?: string;
13
+ };
14
+
15
+ export async function organizationAction(
16
+ _prev: OrganizationActionState,
17
+ formData: FormData,
18
+ ): Promise<OrganizationActionState> {
19
+ const raw = Object.fromEntries(formData);
20
+ const parsed = organizationServerSchema.safeParse(raw);
21
+
22
+ if (!parsed.success) {
23
+ return { error: parsed.error.flatten().formErrors[0] ?? "Invalid input" };
24
+ }
25
+
26
+ const token = await retrieveTokenFromCookie();
27
+
28
+ let slug: string;
29
+
30
+ try {
31
+ const { data } = await apiClient.post<{ slug: string }>(
32
+ "/orgs",
33
+ { name: parsed.data.name },
34
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
35
+ );
36
+
37
+ slug = data.slug;
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ } catch (err: any) {
40
+ return { error: err.response?.data?.message ?? "Something went wrong" };
41
+ }
42
+
43
+ revalidatePath("/organizations");
44
+ redirect(`/organizations/${slug}/projects`);
45
+ }
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+
3
+ export const createOrgSchema = z.object({
4
+ name: z.string().min(2, "Name must be at least 2 characters"),
5
+ });
@@ -0,0 +1,3 @@
1
+ export const ORGANIZATION_INTENT = {
2
+ CREATE: "create",
3
+ } as const;
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { useActionState } from "react";
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import { Controller, useForm } from "react-hook-form";
6
+ import type { z } from "zod";
7
+ import { Button } from "@/components/ui/button";
8
+ import {
9
+ Field,
10
+ FieldError,
11
+ FieldGroup,
12
+ FieldLabel,
13
+ } from "@/components/ui/field";
14
+ import { Input } from "@/components/ui/input";
15
+ import { organizationAction } from "./action";
16
+ import { createOrgSchema } from "./client.schema";
17
+ import { ORGANIZATION_INTENT } from "./constants";
18
+
19
+ type CreateOrgValues = z.infer<typeof createOrgSchema>;
20
+
21
+ export function CreateOrgForm() {
22
+ const [state, formAction, isPending] = useActionState(organizationAction, {});
23
+ const form = useForm<CreateOrgValues>({
24
+ resolver: zodResolver(createOrgSchema),
25
+ defaultValues: { name: "" },
26
+ });
27
+
28
+ return (
29
+ <form
30
+ action={formAction}
31
+ noValidate
32
+ onSubmitCapture={async (e) => {
33
+ const ok = await form.trigger(undefined, { shouldFocus: true });
34
+ if (!ok) e.preventDefault();
35
+ }}
36
+ className="max-w-md space-y-4"
37
+ >
38
+ <input type="hidden" name="intent" value={ORGANIZATION_INTENT.CREATE} />
39
+
40
+ {state.error && <p className="text-sm text-destructive">{state.error}</p>}
41
+
42
+ <FieldGroup>
43
+ <Controller
44
+ name="name"
45
+ control={form.control}
46
+ render={({ field, fieldState }) => (
47
+ <Field data-invalid={fieldState.invalid}>
48
+ <FieldLabel htmlFor="name">Organization name</FieldLabel>
49
+ <Input
50
+ id="name"
51
+ placeholder="My Company"
52
+ {...field}
53
+ name="name"
54
+ />
55
+ {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
56
+ </Field>
57
+ )}
58
+ />
59
+ </FieldGroup>
60
+
61
+ <Button type="submit" disabled={isPending}>
62
+ {isPending ? "Creating…" : "Create organization"}
63
+ </Button>
64
+ </form>
65
+ );
66
+ }
@@ -0,0 +1,35 @@
1
+ import { redirect } from "next/navigation";
2
+ import apiClient from "@/lib/axios";
3
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
4
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
5
+ import type { OrganizationWithMeta } from "@apiDatabase/types";
6
+
7
+ export async function retrieveMyOrganizationsFromApi(): Promise<
8
+ OrganizationWithMeta[]
9
+ > {
10
+ const token = await retrieveTokenFromCookie();
11
+
12
+ try {
13
+ const { data } = await apiClient.get<OrganizationWithMeta[]>("/orgs", {
14
+ headers: {
15
+ Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}`,
16
+ },
17
+ });
18
+ return data;
19
+ } catch {
20
+ redirect("/login");
21
+ }
22
+ }
23
+
24
+ export async function retrieveOrganizationBySlugFromApi(slug: string) {
25
+ const token = await retrieveTokenFromCookie();
26
+
27
+ try {
28
+ const { data } = await apiClient.get(`/orgs/${slug}`, {
29
+ headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` },
30
+ });
31
+ return data;
32
+ } catch {
33
+ redirect("/organizations");
34
+ }
35
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import { ORGANIZATION_INTENT } from "./constants";
3
+ import { createOrgSchema } from "./client.schema";
4
+
5
+ export const organizationServerSchema = z.discriminatedUnion("intent", [
6
+ z.object({
7
+ intent: z.literal(ORGANIZATION_INTENT.CREATE),
8
+ ...createOrgSchema.shape,
9
+ }),
10
+ ]);
@@ -0,0 +1,78 @@
1
+ "use server";
2
+
3
+ import { revalidatePath } from "next/cache";
4
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
5
+ import apiClient from "@/lib/axios";
6
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
7
+ import { PROJECT_AUTH_INTENT } from "./project-auth-constants";
8
+ import { projectAuthServerSchema } from "./project-auth-server.schema";
9
+
10
+ export type ProjectAuthActionState = {
11
+ error?: string;
12
+ success?: string;
13
+ };
14
+
15
+ export async function projectAuthAction(
16
+ {
17
+ orgSlug,
18
+ projectSlug,
19
+ }: {
20
+ orgSlug: string;
21
+ projectSlug: string;
22
+ },
23
+ _prev: ProjectAuthActionState,
24
+ formData: FormData,
25
+ ): Promise<ProjectAuthActionState> {
26
+ const raw = Object.fromEntries(formData);
27
+ const parsed = projectAuthServerSchema.safeParse(raw);
28
+
29
+ if (!parsed.success) {
30
+ return { error: parsed.error.flatten().formErrors[0] ?? "Invalid input" };
31
+ }
32
+
33
+ const token = await retrieveTokenFromCookie();
34
+ const { intent, ...data } = parsed.data;
35
+
36
+ try {
37
+ switch (intent) {
38
+ case PROJECT_AUTH_INTENT.SAVE_GOOGLE: {
39
+ await apiClient.post(
40
+ `/orgs/${orgSlug}/projects/${projectSlug}/auth/settings`,
41
+ data,
42
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
43
+ );
44
+ revalidatePath(
45
+ `/organizations/${orgSlug}/${projectSlug}/auth/providers`,
46
+ );
47
+ return { success: "Google settings saved" };
48
+ }
49
+
50
+ case PROJECT_AUTH_INTENT.SAVE_GITHUB: {
51
+ await apiClient.post(
52
+ `/orgs/${orgSlug}/projects/${projectSlug}/auth/settings`,
53
+ data,
54
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
55
+ );
56
+ revalidatePath(
57
+ `/organizations/${orgSlug}/${projectSlug}/auth/providers`,
58
+ );
59
+ return { success: "GitHub settings saved" };
60
+ }
61
+
62
+ case PROJECT_AUTH_INTENT.SAVE_SITE_URL: {
63
+ await apiClient.post(
64
+ `/orgs/${orgSlug}/projects/${projectSlug}/auth/settings`,
65
+ data,
66
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
67
+ );
68
+ revalidatePath(
69
+ `/organizations/${orgSlug}/${projectSlug}/auth/url-configuration`,
70
+ );
71
+ return { success: "Site URL saved" };
72
+ }
73
+ }
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ } catch (err: any) {
76
+ return { error: err.response?.data?.message ?? "Something went wrong" };
77
+ }
78
+ }
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+
3
+ export const googleOAuthSchema = z.object({
4
+ googleClientId: z.string().min(1, "Client ID is required"),
5
+ googleClientSecret: z.string().min(1, "Client secret is required"),
6
+ });
7
+
8
+ export const githubOAuthSchema = z.object({
9
+ githubClientId: z.string().min(1, "Client ID is required"),
10
+ githubClientSecret: z.string().min(1, "Client secret is required"),
11
+ });
12
+
13
+ export const siteUrlSchema = z.object({
14
+ siteUrl: z
15
+ .string()
16
+ .min(1, "Site URL is required")
17
+ .url("Must be a valid URL (include https://)"),
18
+ });
@@ -0,0 +1,8 @@
1
+ export const PROJECT_AUTH_INTENT = {
2
+ SAVE_GOOGLE: "SAVE_GOOGLE",
3
+ SAVE_GITHUB: "SAVE_GITHUB",
4
+ SAVE_SITE_URL: "SAVE_SITE_URL",
5
+ } as const;
6
+
7
+ export type ProjectAuthIntent =
8
+ (typeof PROJECT_AUTH_INTENT)[keyof typeof PROJECT_AUTH_INTENT];
@@ -0,0 +1,39 @@
1
+ import { redirect } from "next/navigation";
2
+ import apiClient from "@/lib/axios";
3
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
4
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
5
+ import type { ProjectAuthUser, ProjectOAuthSettings } from "@apiDatabase/types";
6
+
7
+ export async function retrieveProjectAuthUsersFromApi(
8
+ orgSlug: string,
9
+ projectSlug: string,
10
+ ): Promise<ProjectAuthUser[]> {
11
+ const token = await retrieveTokenFromCookie();
12
+
13
+ try {
14
+ const { data } = await apiClient.get<ProjectAuthUser[]>(
15
+ `/orgs/${orgSlug}/projects/${projectSlug}/auth/users`,
16
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
17
+ );
18
+ return data;
19
+ } catch {
20
+ redirect(`/organizations/${orgSlug}/projects`);
21
+ }
22
+ }
23
+
24
+ export async function retrieveOAuthSettingsFromApi(
25
+ orgSlug: string,
26
+ projectSlug: string,
27
+ ): Promise<ProjectOAuthSettings> {
28
+ const token = await retrieveTokenFromCookie();
29
+
30
+ try {
31
+ const { data } = await apiClient.get<ProjectOAuthSettings>(
32
+ `/orgs/${orgSlug}/projects/${projectSlug}/auth/settings`,
33
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
34
+ );
35
+ return data;
36
+ } catch {
37
+ redirect(`/organizations/${orgSlug}/projects`);
38
+ }
39
+ }