1
+ // SecretsKeyManager.tsx
2
+ 'use client' ;
3
+
4
+ import { Alert , AlertDescription , AlertTitle } from '@/components/ui/alert' ;
5
+ import { Button } from '@/components/ui/button' ;
6
+ import { Card , CardContent , CardDescription , CardFooter , CardHeader , CardTitle } from '@/components/ui/card' ;
7
+ import { Dialog , DialogContent , DialogDescription , DialogFooter , DialogHeader , DialogTitle , DialogTrigger } from '@/components/ui/dialog' ;
8
+ import { Input } from '@/components/ui/input' ;
9
+ import { Label } from '@/components/ui/label' ;
10
+ import { motion } from 'framer-motion' ;
11
+ import { Copy , Trash2 } from 'lucide-react' ;
12
+ import { useState } from 'react' ;
13
+ import { toast } from 'sonner' ;
14
+
15
+ interface SecretsKeyManagerProps {
16
+ publicKey : string | null ;
17
+ onCreateKeyPair : ( ) => Promise < { publicKey : string ; privateKey : string } > ;
18
+ onDeletePublicKey : ( ) => Promise < void > ;
19
+ }
20
+
21
+ export function SecretsKeyManager ( { publicKey : initialPublicKey , onCreateKeyPair, onDeletePublicKey } : SecretsKeyManagerProps ) {
22
+ const [ publicKey , setPublicKey ] = useState < string | null > ( initialPublicKey ) ;
23
+ const [ privateKey , setPrivateKey ] = useState < string | null > ( null ) ;
24
+ const [ isPrivateKeyCopied , setIsPrivateKeyCopied ] = useState ( false ) ;
25
+ const [ isCreating , setIsCreating ] = useState ( false ) ;
26
+ const [ isDeleting , setIsDeleting ] = useState ( false ) ;
27
+
28
+ const handleCreateKeyPair = async ( ) => {
29
+ setIsCreating ( true ) ;
30
+ try {
31
+ const { publicKey : newPublicKey , privateKey : newPrivateKey } = await onCreateKeyPair ( ) ;
32
+ setPublicKey ( newPublicKey ) ;
33
+ setPrivateKey ( newPrivateKey ) ;
34
+ setIsPrivateKeyCopied ( false ) ;
35
+ } catch ( error ) {
36
+ console . error ( 'Failed to create key pair:' , error ) ;
37
+ toast . error ( 'Failed to create key pair. Please try again.' ) ;
38
+ } finally {
39
+ setIsCreating ( false ) ;
40
+ }
41
+ } ;
42
+
43
+ const handleDeletePublicKey = async ( ) => {
44
+ setIsDeleting ( true ) ;
45
+ try {
46
+ await onDeletePublicKey ( ) ;
47
+ setPublicKey ( null ) ;
48
+ setPrivateKey ( null ) ;
49
+ setIsPrivateKeyCopied ( false ) ;
50
+ toast . success ( 'Public key has been deleted.' ) ;
51
+ } catch ( error ) {
52
+ console . error ( 'Failed to delete public key:' , error ) ;
53
+ toast . error ( 'Failed to delete public key. Please try again.' ) ;
54
+ } finally {
55
+ setIsDeleting ( false ) ;
56
+ }
57
+ } ;
58
+
59
+ const copyPrivateKeyToClipboard = ( ) => {
60
+ if ( privateKey ) {
61
+ navigator . clipboard . writeText ( privateKey ) ;
62
+ toast . success ( 'The private key has been copied to your clipboard.' ) ;
63
+ setIsPrivateKeyCopied ( true ) ;
64
+ }
65
+ } ;
66
+
67
+ return (
68
+ < motion . div
69
+ initial = { { opacity : 0 , y : 20 } }
70
+ animate = { { opacity : 1 , y : 0 } }
71
+ transition = { { duration : 0.5 } }
72
+ >
73
+ < Card >
74
+ < CardHeader >
75
+ < CardTitle className = "flex items-center space-x-2" >
76
+ < span > Secrets Key</ span >
77
+ </ CardTitle >
78
+ < CardDescription >
79
+ Public key for encrypting sensitive variables
80
+ </ CardDescription >
81
+ </ CardHeader >
82
+ < CardContent >
83
+ { publicKey ? (
84
+ < div className = "space-y-4" >
85
+ < div >
86
+ < Label > Public Key</ Label >
87
+ < div className = "flex items-center mt-1" >
88
+ < Input
89
+ readOnly
90
+ value = { publicKey }
91
+ className = "font-mono text-sm"
92
+ />
93
+ < Button
94
+ variant = "outline"
95
+ size = "icon"
96
+ className = "ml-2"
97
+ onClick = { ( ) => {
98
+ navigator . clipboard . writeText ( publicKey ) ;
99
+ toast . success ( 'Public key copied to clipboard.' ) ;
100
+ } }
101
+ >
102
+ < Copy className = "h-4 w-4" />
103
+ </ Button >
104
+ </ div >
105
+ </ div >
106
+ { privateKey && (
107
+ < Alert className = 'bg-muted/50' >
108
+ < AlertTitle > Private Key (ONLY SHOWN ONCE)</ AlertTitle >
109
+ < AlertDescription >
110
+ < p className = "mb-2" > Save this in your GitHub Action Secrets (org level):</ p >
111
+ < div className = "flex items-center" >
112
+ < Input
113
+ readOnly
114
+ value = { isPrivateKeyCopied ? '•' . repeat ( 100 ) : privateKey }
115
+ className = "font-mono text-sm"
116
+ />
117
+ { ! isPrivateKeyCopied && (
118
+ < Button
119
+ variant = "outline"
120
+ size = "icon"
121
+ className = "ml-2"
122
+ onClick = { copyPrivateKeyToClipboard }
123
+ >
124
+ < Copy className = "h-4 w-4" />
125
+ </ Button >
126
+ ) }
127
+ </ div >
128
+ </ AlertDescription >
129
+ </ Alert >
130
+ ) }
131
+ </ div >
132
+ ) : (
133
+ < Button onClick = { handleCreateKeyPair } disabled = { isCreating } >
134
+ { isCreating ? 'Creating...' : 'Create Secrets Key' }
135
+ </ Button >
136
+ ) }
137
+ </ CardContent >
138
+ { publicKey && (
139
+ < CardFooter >
140
+ < Dialog >
141
+ < DialogTrigger asChild >
142
+ < Button variant = "destructive" >
143
+ < Trash2 className = "mr-2 h-4 w-4" />
144
+ Delete Secrets Key
145
+ </ Button >
146
+ </ DialogTrigger >
147
+ < DialogContent >
148
+ < DialogHeader >
149
+ < DialogTitle > Are you absolutely sure?</ DialogTitle >
150
+ < DialogDescription >
151
+ This action cannot be undone. You will lose all your secrets without the possibility to recover them.
152
+ </ DialogDescription >
153
+ </ DialogHeader >
154
+ < DialogFooter >
155
+ < Button variant = "outline" onClick = { ( ) => { } } > Cancel</ Button >
156
+ < Button variant = "destructive" onClick = { handleDeletePublicKey } disabled = { isDeleting } >
157
+ { isDeleting ? 'Deleting...' : 'Delete' }
158
+ </ Button >
159
+ </ DialogFooter >
160
+ </ DialogContent >
161
+ </ Dialog >
162
+ </ CardFooter >
163
+ ) }
164
+ </ Card >
165
+ </ motion . div >
166
+ ) ;
167
+ }
0 commit comments