From 358a176bd9b79a8d1d37b5d9f2a9b5d26e35deb2 Mon Sep 17 00:00:00 2001 From: Igor Zalutski Date: Fri, 2 Aug 2024 11:54:23 +0100 Subject: [PATCH 1/5] Add migration --- supabase/migrations/20240802102519_public_key_in_org.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 supabase/migrations/20240802102519_public_key_in_org.sql diff --git a/supabase/migrations/20240802102519_public_key_in_org.sql b/supabase/migrations/20240802102519_public_key_in_org.sql new file mode 100644 index 00000000..f0469ce4 --- /dev/null +++ b/supabase/migrations/20240802102519_public_key_in_org.sql @@ -0,0 +1,2 @@ +ALTER TABLE organizations +ADD COLUMN public_key TEXT; \ No newline at end of file From f53f3605dc7f66d77610117f24d97127c04cba8d Mon Sep 17 00:00:00 2001 From: Igor Zalutski Date: Fri, 2 Aug 2024 13:33:28 +0100 Subject: [PATCH 2/5] Skeleton for key editing --- .../settings/SetSecretsKey.tsx | 52 +++++++++++++++++++ .../settings/page.tsx | 4 ++ 2 files changed, 56 insertions(+) create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SetSecretsKey.tsx diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SetSecretsKey.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SetSecretsKey.tsx new file mode 100644 index 00000000..57668735 --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SetSecretsKey.tsx @@ -0,0 +1,52 @@ +'use server'; + +import { Button } from '@/components/Button'; +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; + +const publicKey: string = 'asdfasdf'; //TODO state, fetch +const privateKey: string = 'asdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaa'; //TODO state + +function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + + + Secrets Key + + + Public key for encrypting sensitive variables + + + + {children} + + + ); +} + +export async function SetSecretsKey({ }: {}) { + if (publicKey) { + return ( + + {/* TODO copy icon, formatting */} + {/* TODO delete button in Danger Zone */} +

+ Public key: {publicKey} +

+ + {privateKey && ( +

+ Private key (ONLY SHOWN ONCE - SAVE IN YOUR GITHUB ACTION SECRETS (ORG LEVEL)):
{privateKey}
+

+ )} +
+ ); + } else { + return ( + + {/* TODO generate key pair, set private key state locally, send public key to the server */} + + + ); + } +} \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/page.tsx index 133d8d32..b1167d96 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/page.tsx @@ -9,6 +9,7 @@ import { Suspense } from "react"; import { DeleteOrganization } from "./DeleteOrganization"; import { EditOrganizationForm } from "./EditOrganizationForm"; import { SetDefaultOrganizationPreference } from "./SetDefaultOrganizationPreference"; +import { SetSecretsKey } from "./SetSecretsKey"; import { SettingsFormSkeleton } from "./SettingsSkeletons"; async function EditOrganization({ @@ -69,6 +70,9 @@ export default async function EditOrganizationPage({ }> + }> + + }> From d48a086a0d390d4a2ba5afa95eb9bb867f027c87 Mon Sep 17 00:00:00 2001 From: psiddharthdesign <107192927+psiddharthdesign@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:13:34 +0530 Subject: [PATCH 3/5] build ui and tentative server actions for secrets key pair --- .../settings/SecretKeyManager.tsx | 156 ++++++++++++++++++ .../settings/SetSecretsKey.tsx | 50 +++--- .../settings/page.tsx | 2 +- src/data/user/secretKey.ts | 95 +++++++++++ src/lib/database.types.ts | 10 ++ 5 files changed, 286 insertions(+), 27 deletions(-) create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx create mode 100644 src/data/user/secretKey.ts diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx new file mode 100644 index 00000000..33ef26fa --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx @@ -0,0 +1,156 @@ +// SecretsKeyManager.tsx +'use client'; + +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { motion } from 'framer-motion'; +import { Copy, Trash2 } from 'lucide-react'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +interface SecretsKeyManagerProps { + publicKey: string | null; + onCreateKeyPair: () => Promise<{ publicKey: string; privateKey: string }>; + onDeletePublicKey: () => Promise; +} + +export function SecretsKeyManager({ publicKey: initialPublicKey, onCreateKeyPair, onDeletePublicKey }: SecretsKeyManagerProps) { + const [publicKey, setPublicKey] = useState(initialPublicKey); + const [privateKey, setPrivateKey] = useState(null); + const [isCreating, setIsCreating] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const handleCreateKeyPair = async () => { + setIsCreating(true); + try { + const { publicKey: newPublicKey, privateKey: newPrivateKey } = await onCreateKeyPair(); + setPublicKey(newPublicKey); + setPrivateKey(newPrivateKey); + } catch (error) { + console.error('Failed to create key pair:', error); + toast.error('Failed to create key pair. Please try again.'); + } finally { + setIsCreating(false); + } + }; + + const handleDeletePublicKey = async () => { + setIsDeleting(true); + try { + await onDeletePublicKey(); + setPublicKey(null); + setPrivateKey(null); + toast.success('Public key has been deleted.'); + } catch (error) { + console.error('Failed to delete public key:', error); + toast.error('Failed to delete public key. Please try again.'); + } finally { + setIsDeleting(false); + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.success('The key has been copied to your clipboard.'); + }; + + return ( + + + + + Secrets Key + + + Public key for encrypting sensitive variables + + + + {publicKey ? ( +
+
+ +
+ + +
+
+ {privateKey && ( + + Private Key (ONLY SHOWN ONCE) + +

Save this in your GitHub Action Secrets (org level):

+
+ + +
+
+
+ )} +
+ ) : ( + + )} +
+ {publicKey && ( + + + + + + + + Are you absolutely sure? + + This action cannot be undone. You will lose all your secrets without the possibility to recover them. + + + + + + + + + + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SetSecretsKey.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SetSecretsKey.tsx index 57668735..e68d5cbd 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SetSecretsKey.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SetSecretsKey.tsx @@ -1,7 +1,8 @@ 'use server'; -import { Button } from '@/components/Button'; import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { createKeyPair, deletePublicKey, getPublicKey } from '@/data/user/secretKey'; +import { SecretsKeyManager } from './SecretKeyManager'; const publicKey: string = 'asdfasdf'; //TODO state, fetch const privateKey: string = 'asdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaaasdfaa'; //TODO state @@ -24,29 +25,26 @@ function Wrapper({ children }: { children: React.ReactNode }) { ); } -export async function SetSecretsKey({ }: {}) { - if (publicKey) { - return ( - - {/* TODO copy icon, formatting */} - {/* TODO delete button in Danger Zone */} -

- Public key: {publicKey} -

- - {privateKey && ( -

- Private key (ONLY SHOWN ONCE - SAVE IN YOUR GITHUB ACTION SECRETS (ORG LEVEL)):
{privateKey}
-

- )} -
- ); - } else { - return ( - - {/* TODO generate key pair, set private key state locally, send public key to the server */} - - - ); - } +export async function SetSecretsKey({ organizationId }: { organizationId: string }) { + const publicKey = await getPublicKey(organizationId); + return ( + { + 'use server'; + const result = await createKeyPair(organizationId); + if (result.status === 'error') { + throw new Error(result.message); + } + return result.data; + }} + onDeletePublicKey={async () => { + 'use server'; + const result = await deletePublicKey(organizationId); + if (result.status === 'error') { + throw new Error(result.message); + } + }} + /> + ); } \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/page.tsx index b1167d96..4c2536f5 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/page.tsx @@ -71,7 +71,7 @@ export default async function EditOrganizationPage({ }> - + }> diff --git a/src/data/user/secretKey.ts b/src/data/user/secretKey.ts new file mode 100644 index 00000000..405b6397 --- /dev/null +++ b/src/data/user/secretKey.ts @@ -0,0 +1,95 @@ +// data/user/secretsKey.ts +'use server'; + +import { createSupabaseUserServerActionClient } from '@/supabase-clients/user/createSupabaseUserServerActionClient'; +import { createSupabaseUserServerComponentClient } from '@/supabase-clients/user/createSupabaseUserServerComponentClient'; +import { SAPayload } from '@/types'; +import crypto from 'crypto'; +import { revalidatePath } from 'next/cache'; + +export async function getPublicKey( + organizationId: string, +): Promise { + const supabase = createSupabaseUserServerComponentClient(); + const { data, error } = await supabase + .from('organizations') + .select('public_key') + .eq('id', organizationId) + .single(); + + if (error) { + console.error('Error fetching public key:', error); + return null; + } + + return data?.public_key || null; +} + +export async function createKeyPair( + organizationId: string, +): Promise> { + const supabase = createSupabaseUserServerActionClient(); + + try { + // Generate RSA key pair + const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }); + + // Save public key to the database + const { error } = await supabase + .from('organizations') + .update({ public_key: publicKey }) + .eq('id', organizationId); + + if (error) throw error; + + revalidatePath(`/org/${organizationId}/settings`); + + return { + status: 'success', + data: { publicKey, privateKey }, + }; + } catch (error) { + console.error('Error creating key pair:', error); + return { + status: 'error', + message: 'Failed to create key pair', + }; + } +} + +export async function deletePublicKey( + organizationId: string, +): Promise { + const supabase = createSupabaseUserServerActionClient(); + + try { + const { error } = await supabase + .from('organizations') + .update({ public_key: null }) + .eq('id', organizationId); + + if (error) throw error; + + revalidatePath(`/org/${organizationId}/settings`); + + return { + status: 'success', + }; + } catch (error) { + console.error('Error deleting public key:', error); + return { + status: 'error', + message: 'Failed to delete public key', + }; + } +} diff --git a/src/lib/database.types.ts b/src/lib/database.types.ts index 577d0477..929211e7 100644 --- a/src/lib/database.types.ts +++ b/src/lib/database.types.ts @@ -1085,18 +1085,21 @@ export type Database = { Row: { created_at: string id: string + public_key: string | null slug: string title: string } Insert: { created_at?: string id?: string + public_key?: string | null slug?: string title?: string } Update: { created_at?: string id?: string + public_key?: string | null slug?: string title?: string } @@ -1337,6 +1340,13 @@ export type Database = { referencedRelation: "repos" referencedColumns: ["id"] }, + { + foreignKeyName: "projects_team_id_fkey" + columns: ["team_id"] + isOneToOne: false + referencedRelation: "teams" + referencedColumns: ["id"] + }, ] } repos: { From ff68cfb074b830d767d65334f89b589223aae33e Mon Sep 17 00:00:00 2001 From: psiddharthdesign <107192927+psiddharthdesign@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:16:58 +0530 Subject: [PATCH 4/5] minor ui change --- .../(specific-organization-pages)/settings/SecretKeyManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx index 33ef26fa..56246e3c 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx @@ -95,7 +95,7 @@ export function SecretsKeyManager({ publicKey: initialPublicKey, onCreateKeyPair {privateKey && ( - + Private Key (ONLY SHOWN ONCE)

Save this in your GitHub Action Secrets (org level):

From a27a334c3e42eb1423138f128cfb59c2466fd36a Mon Sep 17 00:00:00 2001 From: psiddharthdesign <107192927+psiddharthdesign@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:43:48 +0530 Subject: [PATCH 5/5] fix / minor fixes --- .../settings/SecretKeyManager.tsx | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx index 56246e3c..53c23e65 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/SecretKeyManager.tsx @@ -21,6 +21,7 @@ interface SecretsKeyManagerProps { export function SecretsKeyManager({ publicKey: initialPublicKey, onCreateKeyPair, onDeletePublicKey }: SecretsKeyManagerProps) { const [publicKey, setPublicKey] = useState(initialPublicKey); const [privateKey, setPrivateKey] = useState(null); + const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useState(false); const [isCreating, setIsCreating] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -30,6 +31,7 @@ export function SecretsKeyManager({ publicKey: initialPublicKey, onCreateKeyPair const { publicKey: newPublicKey, privateKey: newPrivateKey } = await onCreateKeyPair(); setPublicKey(newPublicKey); setPrivateKey(newPrivateKey); + setIsPrivateKeyCopied(false); } catch (error) { console.error('Failed to create key pair:', error); toast.error('Failed to create key pair. Please try again.'); @@ -44,6 +46,7 @@ export function SecretsKeyManager({ publicKey: initialPublicKey, onCreateKeyPair await onDeletePublicKey(); setPublicKey(null); setPrivateKey(null); + setIsPrivateKeyCopied(false); toast.success('Public key has been deleted.'); } catch (error) { console.error('Failed to delete public key:', error); @@ -53,9 +56,12 @@ export function SecretsKeyManager({ publicKey: initialPublicKey, onCreateKeyPair } }; - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - toast.success('The key has been copied to your clipboard.'); + const copyPrivateKeyToClipboard = () => { + if (privateKey) { + navigator.clipboard.writeText(privateKey); + toast.success('The private key has been copied to your clipboard.'); + setIsPrivateKeyCopied(true); + } }; return ( @@ -88,7 +94,10 @@ export function SecretsKeyManager({ publicKey: initialPublicKey, onCreateKeyPair variant="outline" size="icon" className="ml-2" - onClick={() => copyToClipboard(publicKey)} + onClick={() => { + navigator.clipboard.writeText(publicKey); + toast.success('Public key copied to clipboard.'); + }} > @@ -102,17 +111,19 @@ export function SecretsKeyManager({ publicKey: initialPublicKey, onCreateKeyPair
- + {!isPrivateKeyCopied && ( + + )}