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,222 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from "react";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { Label } from "@/components/ui/label";
|
|
8
|
+
import type { ProjectOAuthSettings } from "@apiDatabase/types";
|
|
9
|
+
import { ChevronRight, Mail } from "lucide-react";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
function GithubIcon({ className }: { className?: string }) {
|
|
13
|
+
return (
|
|
14
|
+
<svg
|
|
15
|
+
className={className}
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
fill="currentColor"
|
|
18
|
+
aria-hidden
|
|
19
|
+
>
|
|
20
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ProviderId = "email" | "google" | "github";
|
|
26
|
+
|
|
27
|
+
const providers: Array<{
|
|
28
|
+
id: ProviderId;
|
|
29
|
+
name: string;
|
|
30
|
+
icon: React.ReactNode;
|
|
31
|
+
}> = [
|
|
32
|
+
{ id: "email", name: "Email", icon: <Mail className="size-5" /> },
|
|
33
|
+
{
|
|
34
|
+
id: "google",
|
|
35
|
+
name: "Google",
|
|
36
|
+
icon: <span className="text-base font-semibold text-blue-600">G</span>,
|
|
37
|
+
},
|
|
38
|
+
{ id: "github", name: "GitHub", icon: <GithubIcon className="size-5" /> },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function isProviderEnabled(
|
|
42
|
+
id: ProviderId,
|
|
43
|
+
settings: ProjectOAuthSettings,
|
|
44
|
+
): boolean {
|
|
45
|
+
if (id === "email") return true;
|
|
46
|
+
if (id === "google") return Boolean(settings.googleClientId);
|
|
47
|
+
return Boolean(settings.githubClientId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function ProjectAuthProviders({
|
|
51
|
+
orgSlug,
|
|
52
|
+
projectSlug,
|
|
53
|
+
initialSettings,
|
|
54
|
+
}: {
|
|
55
|
+
orgSlug: string;
|
|
56
|
+
projectSlug: string;
|
|
57
|
+
initialSettings: ProjectOAuthSettings;
|
|
58
|
+
}) {
|
|
59
|
+
const [isPending, startTransition] = useTransition();
|
|
60
|
+
const [settings, setSettings] =
|
|
61
|
+
useState<ProjectOAuthSettings>(initialSettings);
|
|
62
|
+
const [selected, setSelected] = useState<ProviderId | null>(null);
|
|
63
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
64
|
+
|
|
65
|
+
function save() {
|
|
66
|
+
setMessage(null);
|
|
67
|
+
startTransition(async () => {
|
|
68
|
+
const res = await fetch(
|
|
69
|
+
`${process.env.NEXT_PUBLIC_API_URL}/orgs/${orgSlug}/projects/${projectSlug}/auth/settings`,
|
|
70
|
+
{
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
credentials: "include",
|
|
74
|
+
body: JSON.stringify(settings),
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
setMessage("Save failed");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setMessage("Saved");
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="space-y-6 p-6">
|
|
89
|
+
<div>
|
|
90
|
+
<h1 className="text-2xl font-medium">Sign In / Providers</h1>
|
|
91
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
92
|
+
Configure authentication providers for this project.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="rounded-lg border divide-y">
|
|
97
|
+
{providers.map((provider) => {
|
|
98
|
+
const enabled = isProviderEnabled(provider.id, settings);
|
|
99
|
+
const isOpen = selected === provider.id;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div key={provider.id}>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={() => setSelected(isOpen ? null : provider.id)}
|
|
106
|
+
className="flex w-full items-center gap-4 px-4 py-4 text-left hover:bg-muted/40"
|
|
107
|
+
>
|
|
108
|
+
<div className="flex size-9 items-center justify-center rounded-md border bg-background">
|
|
109
|
+
{provider.icon}
|
|
110
|
+
</div>
|
|
111
|
+
<div className="min-w-0 flex-1">
|
|
112
|
+
<p className="font-medium">{provider.name}</p>
|
|
113
|
+
</div>
|
|
114
|
+
<Badge
|
|
115
|
+
variant={enabled ? "default" : "secondary"}
|
|
116
|
+
className={cn(
|
|
117
|
+
enabled && "bg-emerald-600 text-white hover:bg-emerald-600",
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{enabled ? "Enabled" : "Disabled"}
|
|
121
|
+
</Badge>
|
|
122
|
+
<ChevronRight
|
|
123
|
+
className={cn(
|
|
124
|
+
"size-4 text-muted-foreground transition-transform",
|
|
125
|
+
isOpen && "rotate-90",
|
|
126
|
+
)}
|
|
127
|
+
/>
|
|
128
|
+
</button>
|
|
129
|
+
|
|
130
|
+
{isOpen && provider.id === "email" && (
|
|
131
|
+
<div className="border-t bg-muted/20 px-4 py-4 text-sm text-muted-foreground">
|
|
132
|
+
Email + password and magic link are always enabled for this
|
|
133
|
+
project. No extra configuration needed.
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{isOpen && provider.id === "google" && (
|
|
138
|
+
<div className="space-y-4 border-t bg-muted/20 px-4 py-4">
|
|
139
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
140
|
+
<div className="space-y-2">
|
|
141
|
+
<Label htmlFor="googleClientId">Client ID</Label>
|
|
142
|
+
<Input
|
|
143
|
+
id="googleClientId"
|
|
144
|
+
value={settings.googleClientId ?? ""}
|
|
145
|
+
onChange={(e) =>
|
|
146
|
+
setSettings((s) => ({
|
|
147
|
+
...s,
|
|
148
|
+
googleClientId: e.target.value,
|
|
149
|
+
}))
|
|
150
|
+
}
|
|
151
|
+
placeholder="Google OAuth client ID"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="space-y-2">
|
|
155
|
+
<Label htmlFor="googleClientSecret">Client Secret</Label>
|
|
156
|
+
<Input
|
|
157
|
+
id="googleClientSecret"
|
|
158
|
+
type="password"
|
|
159
|
+
value={settings.googleClientSecret ?? ""}
|
|
160
|
+
onChange={(e) =>
|
|
161
|
+
setSettings((s) => ({
|
|
162
|
+
...s,
|
|
163
|
+
googleClientSecret: e.target.value,
|
|
164
|
+
}))
|
|
165
|
+
}
|
|
166
|
+
placeholder="Google OAuth client secret"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<Button type="button" onClick={save} disabled={isPending}>
|
|
171
|
+
{isPending ? "Saving…" : "Save Google settings"}
|
|
172
|
+
</Button>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{isOpen && provider.id === "github" && (
|
|
177
|
+
<div className="space-y-4 border-t bg-muted/20 px-4 py-4">
|
|
178
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
179
|
+
<div className="space-y-2">
|
|
180
|
+
<Label htmlFor="githubClientId">Client ID</Label>
|
|
181
|
+
<Input
|
|
182
|
+
id="githubClientId"
|
|
183
|
+
value={settings.githubClientId ?? ""}
|
|
184
|
+
onChange={(e) =>
|
|
185
|
+
setSettings((s) => ({
|
|
186
|
+
...s,
|
|
187
|
+
githubClientId: e.target.value,
|
|
188
|
+
}))
|
|
189
|
+
}
|
|
190
|
+
placeholder="GitHub OAuth client ID"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="space-y-2">
|
|
194
|
+
<Label htmlFor="githubClientSecret">Client Secret</Label>
|
|
195
|
+
<Input
|
|
196
|
+
id="githubClientSecret"
|
|
197
|
+
type="password"
|
|
198
|
+
value={settings.githubClientSecret ?? ""}
|
|
199
|
+
onChange={(e) =>
|
|
200
|
+
setSettings((s) => ({
|
|
201
|
+
...s,
|
|
202
|
+
githubClientSecret: e.target.value,
|
|
203
|
+
}))
|
|
204
|
+
}
|
|
205
|
+
placeholder="GitHub OAuth client secret"
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<Button type="button" onClick={save} disabled={isPending}>
|
|
210
|
+
{isPending ? "Saving…" : "Save GitHub settings"}
|
|
211
|
+
</Button>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{message && <p className="text-sm">{message}</p>}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PROJECT_AUTH_INTENT } from "./project-auth-constants";
|
|
3
|
+
import {
|
|
4
|
+
githubOAuthSchema,
|
|
5
|
+
googleOAuthSchema,
|
|
6
|
+
siteUrlSchema,
|
|
7
|
+
} from "./project-auth-client.schema";
|
|
8
|
+
|
|
9
|
+
export const projectAuthServerSchema = z.discriminatedUnion("intent", [
|
|
10
|
+
z.object({
|
|
11
|
+
intent: z.literal(PROJECT_AUTH_INTENT.SAVE_GOOGLE),
|
|
12
|
+
...googleOAuthSchema.shape,
|
|
13
|
+
}),
|
|
14
|
+
z.object({
|
|
15
|
+
intent: z.literal(PROJECT_AUTH_INTENT.SAVE_GITHUB),
|
|
16
|
+
...githubOAuthSchema.shape,
|
|
17
|
+
}),
|
|
18
|
+
z.object({
|
|
19
|
+
intent: z.literal(PROJECT_AUTH_INTENT.SAVE_SITE_URL),
|
|
20
|
+
...siteUrlSchema.shape,
|
|
21
|
+
}),
|
|
22
|
+
]);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const manageItems = [{ label: "Users", segment: "users" }] as const;
|
|
8
|
+
|
|
9
|
+
const configItems = [
|
|
10
|
+
{ label: "Sign In / Providers", segment: "providers" },
|
|
11
|
+
{ label: "URL Configuration", segment: "url-configuration" },
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
function NavLink({
|
|
15
|
+
href,
|
|
16
|
+
active,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
href: string;
|
|
20
|
+
active: boolean;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<Link
|
|
25
|
+
href={href}
|
|
26
|
+
className={cn(
|
|
27
|
+
"block shrink-0 whitespace-nowrap rounded-md px-3 py-2 text-sm transition-colors",
|
|
28
|
+
active
|
|
29
|
+
? "bg-muted font-medium text-foreground"
|
|
30
|
+
: "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</Link>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ProjectAuthShell({
|
|
39
|
+
orgSlug,
|
|
40
|
+
projectSlug,
|
|
41
|
+
children,
|
|
42
|
+
}: {
|
|
43
|
+
orgSlug: string;
|
|
44
|
+
projectSlug: string;
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
}) {
|
|
47
|
+
const pathname = usePathname();
|
|
48
|
+
const base = `/organizations/${orgSlug}/${projectSlug}/auth`;
|
|
49
|
+
|
|
50
|
+
function isActive(segment: string) {
|
|
51
|
+
return (
|
|
52
|
+
pathname === `${base}/${segment}` ||
|
|
53
|
+
pathname.startsWith(`${base}/${segment}/`)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex h-full min-h-0 w-full flex-col md:flex-row">
|
|
59
|
+
<aside className="flex w-full shrink-0 flex-col border-b bg-background md:w-56 md:border-b-0 md:border-r">
|
|
60
|
+
<div className="border-b px-4 py-3 md:py-4">
|
|
61
|
+
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
62
|
+
Authentication
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<nav className="flex gap-1 overflow-x-auto p-2 md:flex-col md:gap-0 md:space-y-6 md:overflow-visible md:p-3">
|
|
67
|
+
<div className="flex shrink-0 gap-1 md:block md:space-y-1">
|
|
68
|
+
<p className="hidden px-3 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground md:block">
|
|
69
|
+
Manage
|
|
70
|
+
</p>
|
|
71
|
+
<div className="flex gap-1 md:block md:space-y-1">
|
|
72
|
+
{manageItems.map(({ label, segment }) => (
|
|
73
|
+
<NavLink
|
|
74
|
+
key={segment}
|
|
75
|
+
href={`${base}/${segment}`}
|
|
76
|
+
active={isActive(segment)}
|
|
77
|
+
>
|
|
78
|
+
{label}
|
|
79
|
+
</NavLink>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="flex shrink-0 gap-1 md:block md:space-y-1">
|
|
85
|
+
<p className="hidden px-3 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground md:block">
|
|
86
|
+
Configuration
|
|
87
|
+
</p>
|
|
88
|
+
<div className="flex gap-1 md:block md:space-y-1">
|
|
89
|
+
{configItems.map(({ label, segment }) => (
|
|
90
|
+
<NavLink
|
|
91
|
+
key={segment}
|
|
92
|
+
href={`${base}/${segment}`}
|
|
93
|
+
active={isActive(segment)}
|
|
94
|
+
>
|
|
95
|
+
{label}
|
|
96
|
+
</NavLink>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</nav>
|
|
101
|
+
</aside>
|
|
102
|
+
|
|
103
|
+
<div className="min-w-0 flex-1 overflow-y-auto">{children}</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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 { Check, Copy } from "lucide-react";
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { projectAuthAction } from "./project-auth-action";
|
|
18
|
+
import { siteUrlSchema } from "./project-auth-client.schema";
|
|
19
|
+
import { PROJECT_AUTH_INTENT } from "./project-auth-constants";
|
|
20
|
+
|
|
21
|
+
type SiteUrlValues = z.infer<typeof siteUrlSchema>;
|
|
22
|
+
|
|
23
|
+
function CopyRow({ value }: { value: string }) {
|
|
24
|
+
const [copied, setCopied] = useState(false);
|
|
25
|
+
|
|
26
|
+
async function copy() {
|
|
27
|
+
await navigator.clipboard.writeText(value);
|
|
28
|
+
setCopied(true);
|
|
29
|
+
setTimeout(() => setCopied(false), 1500);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-center">
|
|
34
|
+
<code className="min-w-0 flex-1 truncate rounded-md border bg-muted/30 px-3 py-2 text-sm">
|
|
35
|
+
{value}
|
|
36
|
+
</code>
|
|
37
|
+
<Button type="button" variant="ghost" size="sm" onClick={copy}>
|
|
38
|
+
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ProjectAuthUrlConfig({
|
|
45
|
+
orgSlug,
|
|
46
|
+
projectSlug,
|
|
47
|
+
initialSiteUrl,
|
|
48
|
+
apiBaseUrl,
|
|
49
|
+
}: {
|
|
50
|
+
orgSlug: string;
|
|
51
|
+
projectSlug: string;
|
|
52
|
+
initialSiteUrl: string;
|
|
53
|
+
apiBaseUrl: string;
|
|
54
|
+
}) {
|
|
55
|
+
const boundAction = projectAuthAction.bind(null, { orgSlug, projectSlug });
|
|
56
|
+
const [state, formAction, isPending] = useActionState(boundAction, {});
|
|
57
|
+
|
|
58
|
+
const form = useForm<SiteUrlValues>({
|
|
59
|
+
resolver: zodResolver(siteUrlSchema),
|
|
60
|
+
defaultValues: { siteUrl: initialSiteUrl },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const redirectUrls = [
|
|
64
|
+
`${apiBaseUrl}/projects/${projectSlug}/auth/google/callback`,
|
|
65
|
+
`${apiBaseUrl}/projects/${projectSlug}/auth/github/callback`,
|
|
66
|
+
`${apiBaseUrl}/projects/${projectSlug}/auth/magic-link/verify`,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="mx-auto w-full max-w-4xl space-y-8 p-4 sm:p-6">
|
|
71
|
+
<div>
|
|
72
|
+
<h1 className="text-2xl font-medium">URL Configuration</h1>
|
|
73
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
74
|
+
Configure site URL and redirect URLs used by auth providers.
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<section className="space-y-4">
|
|
79
|
+
<div>
|
|
80
|
+
<h2 className="text-base font-medium">Site URL</h2>
|
|
81
|
+
<p className="text-sm text-muted-foreground">
|
|
82
|
+
Where users land after OAuth sign-in (your app URL).
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<form
|
|
87
|
+
action={formAction}
|
|
88
|
+
noValidate
|
|
89
|
+
onSubmitCapture={async (e) => {
|
|
90
|
+
const ok = await form.trigger(undefined, { shouldFocus: true });
|
|
91
|
+
if (!ok) e.preventDefault();
|
|
92
|
+
}}
|
|
93
|
+
className="max-w-xl space-y-4"
|
|
94
|
+
>
|
|
95
|
+
{state.error && (
|
|
96
|
+
<p className="text-sm text-destructive">{state.error}</p>
|
|
97
|
+
)}
|
|
98
|
+
{state.success && (
|
|
99
|
+
<p className="text-sm text-emerald-600">{state.success}</p>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<FieldGroup>
|
|
103
|
+
<Controller
|
|
104
|
+
name="siteUrl"
|
|
105
|
+
control={form.control}
|
|
106
|
+
render={({ field, fieldState }) => (
|
|
107
|
+
<Field data-invalid={fieldState.invalid}>
|
|
108
|
+
<FieldLabel htmlFor="siteUrl">Site URL</FieldLabel>
|
|
109
|
+
<Input
|
|
110
|
+
{...field}
|
|
111
|
+
id="siteUrl"
|
|
112
|
+
placeholder="http://localhost:3001"
|
|
113
|
+
aria-invalid={fieldState.invalid}
|
|
114
|
+
/>
|
|
115
|
+
{fieldState.invalid && (
|
|
116
|
+
<FieldError errors={[fieldState.error]} />
|
|
117
|
+
)}
|
|
118
|
+
</Field>
|
|
119
|
+
)}
|
|
120
|
+
/>
|
|
121
|
+
</FieldGroup>
|
|
122
|
+
|
|
123
|
+
<Button
|
|
124
|
+
type="submit"
|
|
125
|
+
name="intent"
|
|
126
|
+
value={PROJECT_AUTH_INTENT.SAVE_SITE_URL}
|
|
127
|
+
disabled={isPending}
|
|
128
|
+
>
|
|
129
|
+
{isPending ? "Saving…" : "Save changes"}
|
|
130
|
+
</Button>
|
|
131
|
+
</form>
|
|
132
|
+
</section>
|
|
133
|
+
|
|
134
|
+
<section className="space-y-3">
|
|
135
|
+
<div>
|
|
136
|
+
<h2 className="text-base font-medium">Redirect URLs</h2>
|
|
137
|
+
<p className="text-sm text-muted-foreground">
|
|
138
|
+
Add these callback URLs in your Google and GitHub OAuth app
|
|
139
|
+
settings.
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="rounded-lg border divide-y">
|
|
144
|
+
{redirectUrls.map((url) => (
|
|
145
|
+
<CopyRow key={url} value={url} />
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
</section>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import {
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableHead,
|
|
12
|
+
TableHeader,
|
|
13
|
+
TableRow,
|
|
14
|
+
} from "@/components/ui/table";
|
|
15
|
+
import type { ProjectAuthUser } from "@apiDatabase/types";
|
|
16
|
+
import { Mail } from "lucide-react";
|
|
17
|
+
|
|
18
|
+
function GithubIcon({ className }: { className?: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<svg
|
|
21
|
+
className={className}
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
aria-hidden
|
|
25
|
+
>
|
|
26
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ProviderIcon({ provider }: { provider: ProjectAuthUser["provider"] }) {
|
|
32
|
+
if (provider === "github") return <GithubIcon className="size-4" />;
|
|
33
|
+
if (provider === "google") {
|
|
34
|
+
return <span className="text-xs font-semibold text-blue-600">G</span>;
|
|
35
|
+
}
|
|
36
|
+
return <Mail className="size-4" />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function providerType(provider: ProjectAuthUser["provider"]) {
|
|
40
|
+
return provider === "email" ? "Email" : "Social";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ProjectAuthUsers({ users }: { users: ProjectAuthUser[] }) {
|
|
44
|
+
const [query, setQuery] = useState("");
|
|
45
|
+
|
|
46
|
+
const filtered = useMemo(() => {
|
|
47
|
+
const q = query.trim().toLowerCase();
|
|
48
|
+
if (!q) return users;
|
|
49
|
+
return users.filter(
|
|
50
|
+
(u) =>
|
|
51
|
+
u.email.toLowerCase().includes(q) || u.id.toLowerCase().includes(q),
|
|
52
|
+
);
|
|
53
|
+
}, [query, users]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="space-y-6 p-6">
|
|
57
|
+
<div>
|
|
58
|
+
<h1 className="text-2xl font-medium">Users</h1>
|
|
59
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
60
|
+
End-users who signed up to apps built on this project.
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
65
|
+
<Input
|
|
66
|
+
value={query}
|
|
67
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
68
|
+
placeholder="Search email, user ID"
|
|
69
|
+
className="max-w-sm"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="rounded-lg border">
|
|
74
|
+
<Table>
|
|
75
|
+
<TableHeader>
|
|
76
|
+
<TableRow>
|
|
77
|
+
<TableHead className="w-10" />
|
|
78
|
+
<TableHead>UID</TableHead>
|
|
79
|
+
<TableHead>Email</TableHead>
|
|
80
|
+
<TableHead>Providers</TableHead>
|
|
81
|
+
<TableHead>Provider type</TableHead>
|
|
82
|
+
<TableHead>Created at</TableHead>
|
|
83
|
+
</TableRow>
|
|
84
|
+
</TableHeader>
|
|
85
|
+
<TableBody>
|
|
86
|
+
{filtered.length === 0 ? (
|
|
87
|
+
<TableRow>
|
|
88
|
+
<TableCell
|
|
89
|
+
colSpan={6}
|
|
90
|
+
className="py-10 text-center text-muted-foreground"
|
|
91
|
+
>
|
|
92
|
+
{users.length === 0
|
|
93
|
+
? "No users yet. Sign up from the SDK demo to populate this table."
|
|
94
|
+
: "No users match your search."}
|
|
95
|
+
</TableCell>
|
|
96
|
+
</TableRow>
|
|
97
|
+
) : (
|
|
98
|
+
filtered.map((user) => (
|
|
99
|
+
<TableRow key={user.id}>
|
|
100
|
+
<TableCell>
|
|
101
|
+
<Avatar className="size-8">
|
|
102
|
+
<AvatarFallback className="text-xs">
|
|
103
|
+
{user.email.charAt(0).toUpperCase()}
|
|
104
|
+
</AvatarFallback>
|
|
105
|
+
</Avatar>
|
|
106
|
+
</TableCell>
|
|
107
|
+
<TableCell className="max-w-[140px] truncate font-mono text-xs text-muted-foreground">
|
|
108
|
+
{user.id}
|
|
109
|
+
</TableCell>
|
|
110
|
+
<TableCell className="font-medium">{user.email}</TableCell>
|
|
111
|
+
<TableCell>
|
|
112
|
+
<div className="flex items-center gap-2">
|
|
113
|
+
<ProviderIcon provider={user.provider} />
|
|
114
|
+
<span className="capitalize">{user.provider}</span>
|
|
115
|
+
{user.emailVerified && (
|
|
116
|
+
<Badge variant="secondary" className="text-xs">
|
|
117
|
+
verified
|
|
118
|
+
</Badge>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</TableCell>
|
|
122
|
+
<TableCell>{providerType(user.provider)}</TableCell>
|
|
123
|
+
<TableCell className="text-muted-foreground">
|
|
124
|
+
{new Date(user.createdAt).toLocaleString()}
|
|
125
|
+
</TableCell>
|
|
126
|
+
</TableRow>
|
|
127
|
+
))
|
|
128
|
+
)}
|
|
129
|
+
</TableBody>
|
|
130
|
+
</Table>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<p className="text-sm text-muted-foreground">
|
|
134
|
+
Total: {filtered.length} user{filtered.length === 1 ? "" : "s"}
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { revalidatePath } from "next/cache";
|
|
4
|
+
import { projectsServerSchema } from "./server.schema";
|
|
5
|
+
import { PROJECTS_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 ProjectsActionState = {
|
|
11
|
+
error?: string;
|
|
12
|
+
success?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function projectsAction(
|
|
16
|
+
{ slug }: { slug: string },
|
|
17
|
+
_prev: ProjectsActionState,
|
|
18
|
+
formData: FormData,
|
|
19
|
+
): Promise<ProjectsActionState> {
|
|
20
|
+
const raw = Object.fromEntries(formData);
|
|
21
|
+
const parsed = projectsServerSchema.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 PROJECTS_INTENT.CREATE: {
|
|
33
|
+
await apiClient.post(
|
|
34
|
+
`/orgs/${slug}/projects`,
|
|
35
|
+
{ name: (data as { name: string }).name },
|
|
36
|
+
{ headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
|
|
37
|
+
);
|
|
38
|
+
revalidatePath(`/organizations/${slug}/projects`);
|
|
39
|
+
return { success: "Project created!" };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
return { error: err.response?.data?.message ?? "Something went wrong" };
|
|
45
|
+
}
|
|
46
|
+
}
|