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: {