[WIP] music platform user data scraper
teal-fm atproto

apikeyify stuff

Natalie B f7d7ca3a 03e35635

Changed files
+138 -44
service
apikey
+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")