+7
-6
cmd/handlers.go
+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
+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
+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
+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 }}
+6
-6
pages/templates/home.gohtml
+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
+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
}