+138
-44
service/apikey/apikey.go
+138
-44
service/apikey/apikey.go
···
4
4
"encoding/json"
5
5
"fmt"
6
6
"html/template"
7
+
"log"
7
8
"net/http"
8
9
"time"
9
10
10
11
"github.com/teal-fm/piper/db"
11
-
"github.com/teal-fm/piper/db/apikey"
12
+
db_apikey "github.com/teal-fm/piper/db/apikey" // Assuming this is the package for ApiKey struct
12
13
"github.com/teal-fm/piper/session"
13
14
)
14
15
···
24
25
}
25
26
}
26
27
28
+
// jsonResponse is a helper to send JSON responses
29
+
func jsonResponse(w http.ResponseWriter, statusCode int, data interface{}) {
30
+
w.Header().Set("Content-Type", "application/json")
31
+
w.WriteHeader(statusCode)
32
+
if data != nil {
33
+
if err := json.NewEncoder(w).Encode(data); err != nil {
34
+
log.Printf("Error encoding JSON response: %v", err)
35
+
}
36
+
}
37
+
}
38
+
39
+
// jsonError is a helper to send JSON error responses
40
+
func jsonError(w http.ResponseWriter, message string, statusCode int) {
41
+
jsonResponse(w, statusCode, map[string]string{"error": message})
42
+
}
43
+
27
44
func (s *Service) HandleAPIKeyManagement(w http.ResponseWriter, r *http.Request) {
28
45
userID, ok := session.GetUserID(r.Context())
29
46
if !ok {
30
-
http.Error(w, "Unauthorized", http.StatusUnauthorized)
47
+
// If this is an API request context, it might have already been handled by WithAPIAuth,
48
+
// but an extra check or appropriate error for the context is good.
49
+
if session.IsAPIRequest(r.Context()) {
50
+
jsonError(w, "Unauthorized", http.StatusUnauthorized)
51
+
} else {
52
+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
53
+
}
31
54
return
32
55
}
33
56
34
-
// if we have an api request return json
35
-
if session.IsAPIRequest(r.Context()) {
36
-
keys, err := s.sessions.GetAPIKeyManager().GetUserApiKeys(userID)
37
-
if err != nil {
38
-
http.Error(w, fmt.Sprintf("Error fetching API keys: %v", err), http.StatusInternalServerError)
39
-
return
40
-
}
57
+
isAPI := session.IsAPIRequest(r.Context())
58
+
59
+
if isAPI { // JSON API Handling
60
+
switch r.Method {
61
+
case http.MethodGet:
62
+
keys, err := s.sessions.GetAPIKeyManager().GetUserApiKeys(userID)
63
+
if err != nil {
64
+
jsonError(w, fmt.Sprintf("Error fetching API keys: %v", err), http.StatusInternalServerError)
65
+
return
66
+
}
67
+
// Ensure keys are safe for listing (e.g., no raw key string)
68
+
// GetUserApiKeys should return a slice of db_apikey.ApiKey or similar struct
69
+
// that includes ID, Name, KeyPrefix, CreatedAt, ExpiresAt.
70
+
jsonResponse(w, http.StatusOK, map[string]interface{}{"api_keys": keys})
71
+
72
+
case http.MethodPost:
73
+
var reqBody struct {
74
+
Name string `json:"name"`
75
+
}
76
+
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
77
+
jsonError(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
78
+
return
79
+
}
80
+
keyName := reqBody.Name
81
+
if keyName == "" {
82
+
keyName = fmt.Sprintf("API Key (via API) - %s", time.Now().Format(time.RFC3339))
83
+
}
84
+
validityDays := 30 // Default, could be made configurable via request body
85
+
86
+
// IMPORTANT: Assumes CreateAPIKeyAndReturnRawKey method exists on SessionManager
87
+
// and returns the database object and the raw key string.
88
+
// Signature: (apiKey *db_apikey.ApiKey, rawKeyString string, err error)
89
+
apiKeyObj, err := s.sessions.CreateAPIKey(userID, keyName, validityDays)
90
+
if err != nil {
91
+
jsonError(w, fmt.Sprintf("Error creating API key: %v", err), http.StatusInternalServerError)
92
+
return
93
+
}
94
+
95
+
jsonResponse(w, http.StatusCreated, map[string]any{
96
+
"id": apiKeyObj.ID,
97
+
"name": apiKeyObj.Name,
98
+
"created_at": apiKeyObj.CreatedAt,
99
+
"expires_at": apiKeyObj.ExpiresAt,
100
+
})
41
101
42
-
w.Header().Set("Content-Type", "application/json")
43
-
json.NewEncoder(w).Encode(map[string]any{
44
-
"api_keys": keys,
45
-
})
46
-
return
102
+
case http.MethodDelete:
103
+
keyID := r.URL.Query().Get("key_id")
104
+
if keyID == "" {
105
+
jsonError(w, "Query parameter 'key_id' is required", http.StatusBadRequest)
106
+
return
107
+
}
108
+
109
+
key, exists := s.sessions.GetAPIKeyManager().GetApiKey(keyID)
110
+
if !exists || key.UserID != userID {
111
+
jsonError(w, "API key not found or not owned by user", http.StatusNotFound)
112
+
return
113
+
}
114
+
115
+
if err := s.sessions.GetAPIKeyManager().DeleteApiKey(keyID); err != nil {
116
+
jsonError(w, fmt.Sprintf("Error deleting API key: %v", err), http.StatusInternalServerError)
117
+
return
118
+
}
119
+
jsonResponse(w, http.StatusOK, map[string]string{"message": "API key deleted successfully"})
120
+
121
+
default:
122
+
jsonError(w, "Method not allowed", http.StatusMethodNotAllowed)
123
+
}
124
+
return // End of JSON API handling
47
125
}
48
126
49
-
// if not return html
50
-
if r.Method == "POST" {
127
+
// HTML UI Handling (largely existing logic)
128
+
if r.Method == http.MethodPost { // Create key from HTML form
51
129
if err := r.ParseForm(); err != nil {
52
130
http.Error(w, "Invalid form data", http.StatusBadRequest)
53
131
return
···
57
135
if keyName == "" {
58
136
keyName = fmt.Sprintf("API Key - %s", time.Now().Format(time.RFC3339))
59
137
}
60
-
61
-
validityDays := 30
138
+
validityDays := 30 // Default for HTML form creation
62
139
140
+
// Uses the existing CreateAPIKey, which likely doesn't return the raw key.
141
+
// The HTML flow currently redirects and shows the key ID.
142
+
// The template message about "only time you'll see this key" is misleading if it shows ID.
143
+
// This might require a separate enhancement if the HTML view should show the raw key.
63
144
apiKey, err := s.sessions.CreateAPIKey(userID, keyName, validityDays)
64
145
if err != nil {
65
146
http.Error(w, fmt.Sprintf("Error creating API key: %v", err), http.StatusInternalServerError)
66
147
return
67
148
}
68
-
149
+
// Redirects, passing the ID of the created key.
150
+
// The template shows this ID in the ".NewKey" section.
69
151
http.Redirect(w, r, "/api-keys?created="+apiKey.ID, http.StatusSeeOther)
70
152
return
71
153
}
72
154
73
-
// if we want to delete a key
74
-
if r.Method == "DELETE" {
155
+
if r.Method == http.MethodDelete { // Delete key via AJAX from HTML page
75
156
keyID := r.URL.Query().Get("key_id")
76
157
if keyID == "" {
77
-
http.Error(w, "Key ID is required", http.StatusBadRequest)
158
+
// For AJAX, a JSON error response is more appropriate than http.Error
159
+
jsonError(w, "Key ID is required", http.StatusBadRequest)
78
160
return
79
161
}
80
162
81
163
key, exists := s.sessions.GetAPIKeyManager().GetApiKey(keyID)
82
164
if !exists || key.UserID != userID {
83
-
http.Error(w, "Invalid API key", http.StatusBadRequest)
165
+
jsonError(w, "Invalid API key or not owned by user", http.StatusBadRequest) // StatusNotFound or StatusForbidden
84
166
return
85
167
}
86
168
87
169
if err := s.sessions.GetAPIKeyManager().DeleteApiKey(keyID); err != nil {
88
-
http.Error(w, fmt.Sprintf("Error deleting API key: %v", err), http.StatusInternalServerError)
170
+
jsonError(w, fmt.Sprintf("Error deleting API key: %v", err), http.StatusInternalServerError)
89
171
return
90
172
}
91
-
92
-
w.Header().Set("Content-Type", "application/json")
93
-
w.Write([]byte(`{"success": true}`))
173
+
// AJAX client expects JSON
174
+
jsonResponse(w, http.StatusOK, map[string]interface{}{"success": true})
94
175
return
95
176
}
96
177
97
-
// show keys
178
+
// GET request: Display HTML page for API Key Management
98
179
keys, err := s.sessions.GetAPIKeyManager().GetUserApiKeys(userID)
99
180
if err != nil {
100
181
http.Error(w, fmt.Sprintf("Error fetching API keys: %v", err), http.StatusInternalServerError)
101
182
return
102
183
}
103
184
104
-
newlyCreatedKey := r.URL.Query().Get("created")
185
+
// newlyCreatedKey will be the ID from the redirect after form POST
186
+
newlyCreatedKeyID := r.URL.Query().Get("created")
187
+
var newKeyValueToShow string
188
+
189
+
if newlyCreatedKeyID != "" {
190
+
// For HTML, we only have the ID. The template message should be adjusted
191
+
// if it implies the raw key is shown.
192
+
// If you enhance CreateAPIKey for HTML to also pass the raw key (e.g. via flash message),
193
+
// this logic would change. For now, it's the ID.
194
+
newKeyValueToShow = newlyCreatedKeyID
195
+
}
105
196
106
197
tmpl := `
107
198
<!DOCTYPE html>
···
194
285
</form>
195
286
</div>
196
287
197
-
{{if .NewKey}}
288
+
{{if .NewKeyID}} <!-- Changed from .NewKey to .NewKeyID for clarity -->
198
289
<div class="new-key-alert">
199
-
<h3>Your new API key has been created</h3>
200
-
<p><strong>Important:</strong> This is the only time you'll see this key. Please copy it now and store it securely.</p>
201
-
<div class="key-value">{{.NewKey}}</div>
290
+
<h3>Your new API key (ID: {{.NewKeyID}}) has been created</h3>
291
+
<!-- The message below is misleading if only the ID is shown.
292
+
Consider changing this text or modifying the flow to show the actual key once for HTML. -->
293
+
<p><strong>Important:</strong> If this is an ID, ensure you have copied the actual key if it was displayed previously. For keys generated via the API, the key is returned in the API response.</p>
202
294
</div>
203
295
{{end}}
204
296
···
209
301
<thead>
210
302
<tr>
211
303
<th>Name</th>
304
+
<th>Prefix</th>
212
305
<th>Created</th>
213
306
<th>Expires</th>
214
307
<th>Actions</th>
···
218
311
{{range .Keys}}
219
312
<tr>
220
313
<td>{{.Name}}</td>
314
+
<td>{{.KeyPrefix}}</td> <!-- Added KeyPrefix for better identification -->
221
315
<td>{{formatTime .CreatedAt}}</td>
222
316
<td>{{formatTime .ExpiresAt}}</td>
223
317
<td>
···
236
330
<h2>API Usage</h2>
237
331
<p>To use your API key, include it in the Authorization header of your HTTP requests:</p>
238
332
<pre>Authorization: Bearer YOUR_API_KEY</pre>
239
-
<p>Or include it as a query parameter:</p>
333
+
<p>Or include it as a query parameter (less secure for the key itself):</p>
240
334
<pre>https://your-piper-instance.com/endpoint?api_key=YOUR_API_KEY</pre>
241
335
</div>
242
336
243
337
<script>
244
338
function deleteKey(keyId) {
245
339
if (confirm('Are you sure you want to delete this API key? This action cannot be undone.')) {
246
-
fetch('/api-keys?key_id=' + keyId, {
340
+
fetch('/api-keys?key_id=' + keyId, { // This endpoint is handled by HandleAPIKeyManagement
247
341
method: 'DELETE',
248
342
})
249
343
.then(response => response.json())
···
251
345
if (data.success) {
252
346
window.location.reload();
253
347
} else {
254
-
alert('Failed to delete API key');
348
+
alert('Failed to delete API key: ' + (data.error || 'Unknown error'));
255
349
}
256
350
})
257
351
.catch(error => {
258
352
console.error('Error:', error);
259
-
alert('Failed to delete API key');
353
+
alert('Failed to delete API key due to a network or processing error.');
260
354
});
261
355
}
262
356
}
···
264
358
</body>
265
359
</html>
266
360
`
267
-
268
-
// Format time function for the template
269
361
funcMap := template.FuncMap{
270
362
"formatTime": func(t time.Time) string {
363
+
if t.IsZero() {
364
+
return "N/A"
365
+
}
271
366
return t.Format("Jan 02, 2006 15:04")
272
367
},
273
368
}
274
369
275
-
// Parse the template with the function map
276
370
t, err := template.New("apikeys").Funcs(funcMap).Parse(tmpl)
277
371
if err != nil {
278
372
http.Error(w, fmt.Sprintf("Error parsing template: %v", err), http.StatusInternalServerError)
···
280
374
}
281
375
282
376
data := struct {
283
-
Keys []*apikey.ApiKey
284
-
NewKey string
377
+
Keys []*db_apikey.ApiKey // Assuming GetUserApiKeys returns this type
378
+
NewKeyID string // Changed from NewKey for clarity as it's an ID
285
379
}{
286
-
Keys: keys,
287
-
NewKey: newlyCreatedKey,
380
+
Keys: keys,
381
+
NewKeyID: newKeyValueToShow,
288
382
}
289
383
290
384
w.Header().Set("Content-Type", "text/html")