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

ApiKeys view

Changed files
+281 -319
cmd
pages
service
apikey
+7 -6
cmd/handlers.go
··· 16 16 ) 17 17 18 18 type HomeParams struct { 19 - IsLoggedIn bool 20 - LastFMUsername *string 19 + NavBar pages.NavBar 21 20 } 22 21 23 - func home(database *db.DB, pages *pages.Pages) http.HandlerFunc { 22 + func home(database *db.DB, pg *pages.Pages) http.HandlerFunc { 24 23 return func(w http.ResponseWriter, r *http.Request) { 25 24 26 25 w.Header().Set("Content-Type", "text/html") ··· 39 38 } 40 39 } 41 40 params := HomeParams{ 42 - IsLoggedIn: isLoggedIn, 43 - LastFMUsername: &lastfmUsername, 41 + NavBar: pages.NavBar{ 42 + IsLoggedIn: isLoggedIn, 43 + LastFMUsername: lastfmUsername, 44 + }, 44 45 } 45 - err := pages.Execute("home", w, params) 46 + err := pg.Execute("home", w, params) 46 47 if err != nil { 47 48 log.Printf("Error executing template: %v", err) 48 49 }
+1 -1
cmd/routes.go
··· 22 22 // Authenticated Web Routes 23 23 mux.HandleFunc("/current-track", session.WithAuth(app.spotifyService.HandleCurrentTrack, app.sessionManager)) 24 24 mux.HandleFunc("/history", session.WithAuth(app.spotifyService.HandleTrackHistory, app.sessionManager)) 25 - mux.HandleFunc("/api-keys", session.WithAuth(app.apiKeyService.HandleAPIKeyManagement, app.sessionManager)) 25 + mux.HandleFunc("/api-keys", session.WithAuth(app.apiKeyService.HandleAPIKeyManagement(app.database, app.pages), app.sessionManager)) 26 26 mux.HandleFunc("/link-lastfm", session.WithAuth(handleLinkLastfmForm(app.database), app.sessionManager)) // GET form 27 27 mux.HandleFunc("/link-lastfm/submit", session.WithAuth(handleLinkLastfmSubmit(app.database), app.sessionManager)) // POST submit - Changed route slightly 28 28 mux.HandleFunc("/logout", app.sessionManager.HandleLogout)
+8 -6
pages/pages.go
··· 1 1 package pages 2 2 3 - // inspired from tangled's implementation 3 + // forked and inspired from tangled's implementation 4 4 //https://tangled.org/@tangled.org/core/blob/master/appview/pages/pages.go 5 5 6 6 import ( ··· 30 30 31 31 func (p *Pages) fragmentPaths() ([]string, error) { 32 32 var fragmentPaths []string 33 - // When using os.DirFS("templates"), the FS root is already the templates directory. 34 - // Walk from "." and use relative paths (no "templates/" prefix). 35 33 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 36 34 if err != nil { 37 35 return err ··· 42 40 if !strings.HasSuffix(path, ".gohtml") { 43 41 return nil 44 42 } 45 - //if !strings.Contains(path, "fragments/") { 46 - // return nil 47 - //} 48 43 fragmentPaths = append(fragmentPaths, path) 49 44 return nil 50 45 }) ··· 138 133 139 134 return tpl.ExecuteTemplate(w, "layouts/base", params) 140 135 } 136 + 137 + // Shared view/template params 138 + 139 + type NavBar struct { 140 + IsLoggedIn bool 141 + LastFMUsername string 142 + }
+92
pages/templates/apiKeys.gohtml
··· 1 + 2 + {{ define "content" }} 3 + 4 + {{ template "components/navBar" .NavBar }} 5 + 6 + 7 + <h1>API Key Management</h1> 8 + 9 + <div class="card"> 10 + <h2>Create New API Key</h2> 11 + <p>API keys allow programmatic access to your Piper account data.</p> 12 + <form method="POST" action="/api-keys"> 13 + <div style="margin-bottom: 15px;"> 14 + <label for="name">Key Name (for your reference):</label> 15 + <input type="text" id="name" name="name" placeholder="My Application" style="width: 100%; padding: 8px; margin-top: 5px;"> 16 + </div> 17 + <button type="submit" class="btn">Generate New API Key</button> 18 + </form> 19 + </div> 20 + 21 + {{if .NewKeyID}} <!-- Changed from .NewKey to .NewKeyID for clarity --> 22 + <div class="new-key-alert"> 23 + <h3>Your new API key (ID: {{.NewKeyID}}) has been created</h3> 24 + <!-- The message below is misleading if only the ID is shown. 25 + Consider changing this text or modifying the flow to show the actual key once for HTML. --> 26 + <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> 27 + </div> 28 + {{end}} 29 + 30 + <div class="card"> 31 + <h2>Your API Keys</h2> 32 + {{if .Keys}} 33 + <table> 34 + <thead> 35 + <tr> 36 + <th>Name</th> 37 + <th>Prefix</th> 38 + <th>Created</th> 39 + <th>Expires</th> 40 + <th>Actions</th> 41 + </tr> 42 + </thead> 43 + <tbody> 44 + {{range .Keys}} 45 + <tr> 46 + <td>{{.Name}}</td> 47 + <td>{{.KeyPrefix}}</td> <!-- Added KeyPrefix for better identification --> 48 + <td>{{formatTime .CreatedAt}}</td> 49 + <td>{{formatTime .ExpiresAt}}</td> 50 + <td> 51 + <button class="btn btn-danger" onclick="deleteKey('{{.ID}}')">Delete</button> 52 + </td> 53 + </tr> 54 + {{end}} 55 + </tbody> 56 + </table> 57 + {{else}} 58 + <p>You don't have any API keys yet.</p> 59 + {{end}} 60 + </div> 61 + 62 + <div class="card"> 63 + <h2>API Usage</h2> 64 + <p>To use your API key, include it in the Authorization header of your HTTP requests:</p> 65 + <pre>Authorization: Bearer YOUR_API_KEY</pre> 66 + <p>Or include it as a query parameter (less secure for the key itself):</p> 67 + <pre>https://your-piper-instance.com/endpoint?api_key=YOUR_API_KEY</pre> 68 + </div> 69 + 70 + <script> 71 + function deleteKey(keyId) { 72 + if (confirm('Are you sure you want to delete this API key? This action cannot be undone.')) { 73 + fetch('/api-keys?key_id=' + keyId, { // This endpoint is handled by HandleAPIKeyManagement 74 + method: 'DELETE', 75 + }) 76 + .then(response => response.json()) 77 + .then(data => { 78 + if (data.success) { 79 + window.location.reload(); 80 + } else { 81 + alert('Failed to delete API key: ' + (data.error || 'Unknown error')); 82 + } 83 + }) 84 + .catch(error => { 85 + console.error('Error:', error); 86 + alert('Failed to delete API key due to a network or processing error.'); 87 + }); 88 + } 89 + } 90 + </script> 91 + 92 + {{ end }}
+20
pages/templates/components/navBar.gohtml
··· 1 + {{ define "components/navBar" }} 2 + 3 + <div class="nav"> 4 + <a href="/">Home</a> 5 + 6 + {{if .IsLoggedIn}} 7 + <a href="/current-track">Spotify Current</a> 8 + <a href="/history">Spotify History</a> 9 + <a href="/link-lastfm">Link Last.fm</a> 10 + {{ if .LastFMUsername }} 11 + <a href="/lastfm/recent">Last.fm Recent</a> 12 + {{ end }} 13 + <a href="/api-keys">API Keys</a> 14 + <a href="/login/spotify">Connect Spotify Account</a> 15 + <a href="/logout">Logout</a> 16 + {{ else }} 17 + <a href="/login/atproto">Login with ATProto</a> 18 + {{ end }} 19 + </div> 20 + {{ end }}
+6 -6
pages/templates/home.gohtml
··· 5 5 <div class="nav"> 6 6 <a href="/">Home</a> 7 7 8 - {{if .IsLoggedIn}} 8 + {{if .NavBar.IsLoggedIn}} 9 9 <a href="/current-track">Spotify Current</a> 10 10 <a href="/history">Spotify History</a> 11 11 <a href="/link-lastfm">Link Last.fm</a> 12 - {{ if .LastFMUsername }} 12 + {{ if .NavBar.LastFMUsername }} 13 13 <a href="/lastfm/recent">Last.fm Recent</a> 14 14 {{ end }} 15 15 <a href="/api-keys">API Keys</a> ··· 24 24 <h2>Welcome to Piper</h2> 25 25 <p>Piper is a multi-user application that records what you're listening to on Spotify and Last.fm, saving your listening history.</p> 26 26 27 - {{if .IsLoggedIn}} 27 + {{if .NavBar.IsLoggedIn}} 28 28 <p>You're logged in!</p> 29 29 <ul> 30 30 <li><a href="/login/spotify">Connect your Spotify account</a> to start tracking.</li> ··· 33 33 <p>Once connected, you can check out your:</p> 34 34 <ul> 35 35 <li><a href="/current-track">Spotify current track</a> or <a href="/history">listening history</a>.</li> 36 - {{ if .LastFMUsername }} 36 + {{ if .NavBar.LastFMUsername }} 37 37 <li><a href="/lastfm/recent">Last.fm recent tracks</a>.</li> 38 38 {{ end }} 39 39 40 40 </ul> 41 41 <p>You can also manage your <a href="/api-keys">API keys</a> for programmatic access.</p> 42 42 43 - {{ if .LastFMUsername }} 44 - <p class='service-status'>Last.fm Username: {{ .LastFMUsername }}</p> 43 + {{ if .NavBar.LastFMUsername }} 44 + <p class='service-status'>Last.fm Username: {{ .NavBar.LastFMUsername }}</p> 45 45 {{else }} 46 46 <p class='service-status'>Last.fm account not linked.</p> 47 47 {{end}}
+147 -300
service/apikey/apikey.go
··· 3 3 import ( 4 4 "encoding/json" 5 5 "fmt" 6 - "html/template" 7 6 "log" 8 7 "net/http" 9 8 "time" 10 9 11 10 "github.com/teal-fm/piper/db" 12 11 db_apikey "github.com/teal-fm/piper/db/apikey" // Assuming this is the package for ApiKey struct 12 + "github.com/teal-fm/piper/pages" 13 13 "github.com/teal-fm/piper/session" 14 14 ) 15 15 ··· 41 41 jsonResponse(w, statusCode, map[string]string{"error": message}) 42 42 } 43 43 44 - func (s *Service) HandleAPIKeyManagement(w http.ResponseWriter, r *http.Request) { 45 - userID, ok := session.GetUserID(r.Context()) 46 - if !ok { 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) 44 + func (s *Service) HandleAPIKeyManagement(database *db.DB, pg *pages.Pages) http.HandlerFunc { 45 + return func(w http.ResponseWriter, r *http.Request) { 46 + 47 + userID, ok := session.GetUserID(r.Context()) 48 + if !ok { 49 + // If this is an API request context, it might have already been handled by WithAPIAuth, 50 + // but an extra check or appropriate error for the context is good. 51 + if session.IsAPIRequest(r.Context()) { 52 + jsonError(w, "Unauthorized", http.StatusUnauthorized) 53 + } else { 54 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 55 + } 56 + return 53 57 } 54 - return 55 - } 56 58 57 - isAPI := session.IsAPIRequest(r.Context()) 59 + lastfmUsername := "" 60 + user, err := database.GetUserByID(userID) 61 + if err == nil && user != nil && user.LastFMUsername != nil { 62 + lastfmUsername = *user.LastFMUsername 63 + } else if err != nil { 64 + log.Printf("Error fetching user %d details for home page: %v", userID, err) 65 + } 66 + isAPI := session.IsAPIRequest(r.Context()) 58 67 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]any{"api_keys": keys}) 68 + if isAPI { // JSON API Handling 69 + switch r.Method { 70 + case http.MethodGet: 71 + keys, err := s.sessions.GetAPIKeyManager().GetUserApiKeys(userID) 72 + if err != nil { 73 + jsonError(w, fmt.Sprintf("Error fetching API keys: %v", err), http.StatusInternalServerError) 74 + return 75 + } 76 + // Ensure keys are safe for listing (e.g., no raw key string) 77 + // GetUserApiKeys should return a slice of db_apikey.ApiKey or similar struct 78 + // that includes ID, Name, KeyPrefix, CreatedAt, ExpiresAt. 79 + jsonResponse(w, http.StatusOK, map[string]any{"api_keys": keys}) 71 80 72 - case http.MethodPost: 73 - var reqBody struct { 74 - Name string `json:"name"` 81 + case http.MethodPost: 82 + var reqBody struct { 83 + Name string `json:"name"` 84 + } 85 + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { 86 + jsonError(w, "Invalid request body: "+err.Error(), http.StatusBadRequest) 87 + return 88 + } 89 + keyName := reqBody.Name 90 + if keyName == "" { 91 + keyName = fmt.Sprintf("API Key (via API) - %s", time.Now().UTC().Format(time.RFC3339)) 92 + } 93 + validityDays := 30 // Default, could be made configurable via request body 94 + 95 + // IMPORTANT: Assumes CreateAPIKeyAndReturnRawKey method exists on SessionManager 96 + // and returns the database object and the raw key string. 97 + // Signature: (apiKey *db_apikey.ApiKey, rawKeyString string, err error) 98 + apiKeyObj, err := s.sessions.CreateAPIKey(userID, keyName, validityDays) 99 + if err != nil { 100 + jsonError(w, fmt.Sprintf("Error creating API key: %v", err), http.StatusInternalServerError) 101 + return 102 + } 103 + 104 + jsonResponse(w, http.StatusCreated, map[string]any{ 105 + "id": apiKeyObj.ID, 106 + "name": apiKeyObj.Name, 107 + "created_at": apiKeyObj.CreatedAt, 108 + "expires_at": apiKeyObj.ExpiresAt, 109 + }) 110 + 111 + case http.MethodDelete: 112 + keyID := r.URL.Query().Get("key_id") 113 + if keyID == "" { 114 + jsonError(w, "Query parameter 'key_id' is required", http.StatusBadRequest) 115 + return 116 + } 117 + 118 + key, exists := s.sessions.GetAPIKeyManager().GetApiKey(keyID) 119 + if !exists || key.UserID != userID { 120 + jsonError(w, "API key not found or not owned by user", http.StatusNotFound) 121 + return 122 + } 123 + 124 + if err := s.sessions.GetAPIKeyManager().DeleteApiKey(keyID); err != nil { 125 + jsonError(w, fmt.Sprintf("Error deleting API key: %v", err), http.StatusInternalServerError) 126 + return 127 + } 128 + jsonResponse(w, http.StatusOK, map[string]string{"message": "API key deleted successfully"}) 129 + 130 + default: 131 + jsonError(w, "Method not allowed", http.StatusMethodNotAllowed) 75 132 } 76 - if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { 77 - jsonError(w, "Invalid request body: "+err.Error(), http.StatusBadRequest) 133 + return // End of JSON API handling 134 + } 135 + 136 + // HTML UI Handling (largely existing logic) 137 + if r.Method == http.MethodPost { // Create key from HTML form 138 + if err := r.ParseForm(); err != nil { 139 + http.Error(w, "Invalid form data", http.StatusBadRequest) 78 140 return 79 141 } 80 - keyName := reqBody.Name 142 + 143 + keyName := r.FormValue("name") 81 144 if keyName == "" { 82 - keyName = fmt.Sprintf("API Key (via API) - %s", time.Now().UTC().Format(time.RFC3339)) 145 + keyName = fmt.Sprintf("API Key - %s", time.Now().UTC().Format(time.RFC3339)) 83 146 } 84 - validityDays := 30 // Default, could be made configurable via request body 147 + validityDays := 1024 85 148 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) 149 + // Uses the existing CreateAPIKey, which likely doesn't return the raw key. 150 + // The HTML flow currently redirects and shows the key ID. 151 + // The template message about "only time you'll see this key" is misleading if it shows ID. 152 + // This might require a separate enhancement if the HTML view should show the raw key. 153 + apiKey, err := s.sessions.CreateAPIKey(userID, keyName, validityDays) 90 154 if err != nil { 91 - jsonError(w, fmt.Sprintf("Error creating API key: %v", err), http.StatusInternalServerError) 155 + http.Error(w, fmt.Sprintf("Error creating API key: %v", err), http.StatusInternalServerError) 92 156 return 93 157 } 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 - }) 158 + // Redirects, passing the ID of the created key. 159 + // The template shows this ID in the ".NewKey" section. 160 + http.Redirect(w, r, "/api-keys?created="+apiKey.ID, http.StatusSeeOther) 161 + return 162 + } 101 163 102 - case http.MethodDelete: 164 + if r.Method == http.MethodDelete { // Delete key via AJAX from HTML page 103 165 keyID := r.URL.Query().Get("key_id") 104 166 if keyID == "" { 105 - jsonError(w, "Query parameter 'key_id' is required", http.StatusBadRequest) 167 + // For AJAX, a JSON error response is more appropriate than http.Error 168 + jsonError(w, "Key ID is required", http.StatusBadRequest) 106 169 return 107 170 } 108 171 109 172 key, exists := s.sessions.GetAPIKeyManager().GetApiKey(keyID) 110 173 if !exists || key.UserID != userID { 111 - jsonError(w, "API key not found or not owned by user", http.StatusNotFound) 174 + jsonError(w, "Invalid API key or not owned by user", http.StatusBadRequest) // StatusNotFound or StatusForbidden 112 175 return 113 176 } 114 177 ··· 116 179 jsonError(w, fmt.Sprintf("Error deleting API key: %v", err), http.StatusInternalServerError) 117 180 return 118 181 } 119 - jsonResponse(w, http.StatusOK, map[string]string{"message": "API key deleted successfully"}) 120 - 121 - default: 122 - jsonError(w, "Method not allowed", http.StatusMethodNotAllowed) 182 + // AJAX client expects JSON 183 + jsonResponse(w, http.StatusOK, map[string]any{"success": true}) 184 + return 123 185 } 124 - return // End of JSON API handling 125 - } 126 186 127 - // HTML UI Handling (largely existing logic) 128 - if r.Method == http.MethodPost { // Create key from HTML form 129 - if err := r.ParseForm(); err != nil { 130 - http.Error(w, "Invalid form data", http.StatusBadRequest) 187 + // GET request: Display HTML page for API Key Management 188 + keys, err := s.sessions.GetAPIKeyManager().GetUserApiKeys(userID) 189 + if err != nil { 190 + http.Error(w, fmt.Sprintf("Error fetching API keys: %v", err), http.StatusInternalServerError) 131 191 return 132 192 } 133 193 134 - keyName := r.FormValue("name") 135 - if keyName == "" { 136 - keyName = fmt.Sprintf("API Key - %s", time.Now().UTC().Format(time.RFC3339)) 194 + // newlyCreatedKey will be the ID from the redirect after form POST 195 + newlyCreatedKeyID := r.URL.Query().Get("created") 196 + var newKeyValueToShow string 197 + 198 + if newlyCreatedKeyID != "" { 199 + // For HTML, we only have the ID. The template message should be adjusted 200 + // if it implies the raw key is shown. 201 + // If you enhance CreateAPIKey for HTML to also pass the raw key (e.g. via flash message), 202 + // this logic would change. For now, it's the ID. 203 + newKeyValueToShow = newlyCreatedKeyID 137 204 } 138 - validityDays := 1024 139 205 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. 144 - apiKey, err := s.sessions.CreateAPIKey(userID, keyName, validityDays) 206 + //t, err := template.New("apikeys").Funcs(funcMap).Parse(tmpl) 145 207 if err != nil { 146 - http.Error(w, fmt.Sprintf("Error creating API key: %v", err), http.StatusInternalServerError) 208 + http.Error(w, fmt.Sprintf("Error parsing template: %v", err), http.StatusInternalServerError) 147 209 return 148 210 } 149 - // Redirects, passing the ID of the created key. 150 - // The template shows this ID in the ".NewKey" section. 151 - http.Redirect(w, r, "/api-keys?created="+apiKey.ID, http.StatusSeeOther) 152 - return 153 - } 154 211 155 - if r.Method == http.MethodDelete { // Delete key via AJAX from HTML page 156 - keyID := r.URL.Query().Get("key_id") 157 - if keyID == "" { 158 - // For AJAX, a JSON error response is more appropriate than http.Error 159 - jsonError(w, "Key ID is required", http.StatusBadRequest) 160 - return 212 + data := struct { 213 + Keys []*db_apikey.ApiKey // Assuming GetUserApiKeys returns this type 214 + NewKeyID string // Changed from NewKey for clarity as it's an ID 215 + NavBar pages.NavBar 216 + }{ 217 + Keys: keys, 218 + NewKeyID: newKeyValueToShow, 219 + NavBar: pages.NavBar{ 220 + IsLoggedIn: ok, 221 + //Just leaving empty so we don't have to pull in the db here, may change 222 + LastFMUsername: lastfmUsername, 223 + }, 161 224 } 162 225 163 - key, exists := s.sessions.GetAPIKeyManager().GetApiKey(keyID) 164 - if !exists || key.UserID != userID { 165 - jsonError(w, "Invalid API key or not owned by user", http.StatusBadRequest) // StatusNotFound or StatusForbidden 166 - return 167 - } 168 - 169 - if err := s.sessions.GetAPIKeyManager().DeleteApiKey(keyID); err != nil { 170 - jsonError(w, fmt.Sprintf("Error deleting API key: %v", err), http.StatusInternalServerError) 171 - return 226 + w.Header().Set("Content-Type", "text/html") 227 + err = pg.Execute("apiKeys", w, data) 228 + if err != nil { 229 + log.Printf("Error executing template: %v", err) 172 230 } 173 - // AJAX client expects JSON 174 - jsonResponse(w, http.StatusOK, map[string]any{"success": true}) 175 - return 176 - } 177 - 178 - // GET request: Display HTML page for API Key Management 179 - keys, err := s.sessions.GetAPIKeyManager().GetUserApiKeys(userID) 180 - if err != nil { 181 - http.Error(w, fmt.Sprintf("Error fetching API keys: %v", err), http.StatusInternalServerError) 182 - return 183 - } 184 - 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 231 + //t.Execute(w, data) 195 232 } 196 - 197 - tmpl := ` 198 - <!DOCTYPE html> 199 - <html> 200 - <head> 201 - <title>API Key Management - Piper</title> 202 - <style> 203 - body { 204 - font-family: Arial, sans-serif; 205 - max-width: 800px; 206 - margin: 0 auto; 207 - padding: 20px; 208 - line-height: 1.6; 209 - } 210 - h1, h2 { 211 - color: #1DB954; /* Spotify green */ 212 - } 213 - .nav { 214 - display: flex; 215 - margin-bottom: 20px; 216 - } 217 - .nav a { 218 - margin-right: 15px; 219 - text-decoration: none; 220 - color: #1DB954; 221 - font-weight: bold; 222 - } 223 - .card { 224 - border: 1px solid #ddd; 225 - border-radius: 8px; 226 - padding: 20px; 227 - margin-bottom: 20px; 228 - } 229 - table { 230 - width: 100%; 231 - border-collapse: collapse; 232 - } 233 - table th, table td { 234 - padding: 8px; 235 - text-align: left; 236 - border-bottom: 1px solid #ddd; 237 - } 238 - .key-value { 239 - font-family: monospace; 240 - padding: 10px; 241 - background-color: #f5f5f5; 242 - border: 1px solid #ddd; 243 - border-radius: 4px; 244 - word-break: break-all; 245 - } 246 - .new-key-alert { 247 - background-color: #f8f9fa; 248 - border-left: 4px solid #1DB954; 249 - padding: 15px; 250 - margin-bottom: 20px; 251 - } 252 - .btn { 253 - padding: 8px 16px; 254 - background-color: #1DB954; 255 - color: white; 256 - border: none; 257 - border-radius: 4px; 258 - cursor: pointer; 259 - } 260 - .btn-danger { 261 - background-color: #dc3545; 262 - } 263 - </style> 264 - </head> 265 - <body> 266 - <div class="nav"> 267 - <a href="/">Home</a> 268 - <a href="/current-track">Current Track</a> 269 - <a href="/history">Track History</a> 270 - <a href="/api-keys" class="active">API Keys</a> 271 - <a href="/logout">Logout</a> 272 - </div> 273 - 274 - <h1>API Key Management</h1> 275 - 276 - <div class="card"> 277 - <h2>Create New API Key</h2> 278 - <p>API keys allow programmatic access to your Piper account data.</p> 279 - <form method="POST" action="/api-keys"> 280 - <div style="margin-bottom: 15px;"> 281 - <label for="name">Key Name (for your reference):</label> 282 - <input type="text" id="name" name="name" placeholder="My Application" style="width: 100%; padding: 8px; margin-top: 5px;"> 283 - </div> 284 - <button type="submit" class="btn">Generate New API Key</button> 285 - </form> 286 - </div> 287 - 288 - {{if .NewKeyID}} <!-- Changed from .NewKey to .NewKeyID for clarity --> 289 - <div class="new-key-alert"> 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> 294 - </div> 295 - {{end}} 296 - 297 - <div class="card"> 298 - <h2>Your API Keys</h2> 299 - {{if .Keys}} 300 - <table> 301 - <thead> 302 - <tr> 303 - <th>Name</th> 304 - <th>Prefix</th> 305 - <th>Created</th> 306 - <th>Expires</th> 307 - <th>Actions</th> 308 - </tr> 309 - </thead> 310 - <tbody> 311 - {{range .Keys}} 312 - <tr> 313 - <td>{{.Name}}</td> 314 - <td>{{.KeyPrefix}}</td> <!-- Added KeyPrefix for better identification --> 315 - <td>{{formatTime .CreatedAt}}</td> 316 - <td>{{formatTime .ExpiresAt}}</td> 317 - <td> 318 - <button class="btn btn-danger" onclick="deleteKey('{{.ID}}')">Delete</button> 319 - </td> 320 - </tr> 321 - {{end}} 322 - </tbody> 323 - </table> 324 - {{else}} 325 - <p>You don't have any API keys yet.</p> 326 - {{end}} 327 - </div> 328 - 329 - <div class="card"> 330 - <h2>API Usage</h2> 331 - <p>To use your API key, include it in the Authorization header of your HTTP requests:</p> 332 - <pre>Authorization: Bearer YOUR_API_KEY</pre> 333 - <p>Or include it as a query parameter (less secure for the key itself):</p> 334 - <pre>https://your-piper-instance.com/endpoint?api_key=YOUR_API_KEY</pre> 335 - </div> 336 - 337 - <script> 338 - function deleteKey(keyId) { 339 - if (confirm('Are you sure you want to delete this API key? This action cannot be undone.')) { 340 - fetch('/api-keys?key_id=' + keyId, { // This endpoint is handled by HandleAPIKeyManagement 341 - method: 'DELETE', 342 - }) 343 - .then(response => response.json()) 344 - .then(data => { 345 - if (data.success) { 346 - window.location.reload(); 347 - } else { 348 - alert('Failed to delete API key: ' + (data.error || 'Unknown error')); 349 - } 350 - }) 351 - .catch(error => { 352 - console.error('Error:', error); 353 - alert('Failed to delete API key due to a network or processing error.'); 354 - }); 355 - } 356 - } 357 - </script> 358 - </body> 359 - </html> 360 - ` 361 - funcMap := template.FuncMap{ 362 - "formatTime": func(t time.Time) string { 363 - if t.IsZero() { 364 - return "N/A" 365 - } 366 - return t.Format("Jan 02, 2006 15:04") 367 - }, 368 - } 369 - 370 - t, err := template.New("apikeys").Funcs(funcMap).Parse(tmpl) 371 - if err != nil { 372 - http.Error(w, fmt.Sprintf("Error parsing template: %v", err), http.StatusInternalServerError) 373 - return 374 - } 375 - 376 - data := struct { 377 - Keys []*db_apikey.ApiKey // Assuming GetUserApiKeys returns this type 378 - NewKeyID string // Changed from NewKey for clarity as it's an ID 379 - }{ 380 - Keys: keys, 381 - NewKeyID: newKeyValueToShow, 382 - } 383 - 384 - w.Header().Set("Content-Type", "text/html") 385 - t.Execute(w, data) 386 233 }