Skip to content

Commit 923631e

Browse files
author
Datanoise
committed
feat(admin): Improve IP whitelist management
- Prevents duplicate entries from being added to the IP whitelist. - Updates the IP whitelist on the admin page dynamically using JavaScript, removing the need for a page reload. - Refactors whitelist and ban logic into helper functions on the Config struct.
1 parent c0aa9b7 commit 923631e

File tree

3 files changed

+209
-22
lines changed

3 files changed

+209
-22
lines changed

config/config.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,54 @@ type Config struct {
121121
Users map[string]*User `json:"users"`
122122
}
123123

124+
func (c *Config) IsWhitelisted(ip string) bool {
125+
for _, existingIP := range c.WhitelistedIPs {
126+
if existingIP == ip {
127+
return true
128+
}
129+
}
130+
return false
131+
}
132+
133+
func (c *Config) AddWhitelistedIP(ip string) {
134+
if !c.IsWhitelisted(ip) {
135+
c.WhitelistedIPs = append(c.WhitelistedIPs, ip)
136+
}
137+
}
138+
139+
func (c *Config) RemoveWhitelistedIP(ip string) {
140+
for i, existingIP := range c.WhitelistedIPs {
141+
if existingIP == ip {
142+
c.WhitelistedIPs = append(c.WhitelistedIPs[:i], c.WhitelistedIPs[i+1:]...)
143+
return
144+
}
145+
}
146+
}
147+
148+
func (c *Config) IsBanned(ip string) bool {
149+
for _, existingIP := range c.BannedIPs {
150+
if existingIP == ip {
151+
return true
152+
}
153+
}
154+
return false
155+
}
156+
157+
func (c *Config) AddBannedIP(ip string) {
158+
if !c.IsBanned(ip) {
159+
c.BannedIPs = append(c.BannedIPs, ip)
160+
}
161+
}
162+
163+
func (c *Config) RemoveBannedIP(ip string) {
164+
for i, existingIP := range c.BannedIPs {
165+
if existingIP == ip {
166+
c.BannedIPs = append(c.BannedIPs[:i], c.BannedIPs[i+1:]...)
167+
return
168+
}
169+
}
170+
}
171+
124172
func HashPassword(p string) (string, error) {
125173
bytes, err := bcrypt.GenerateFromPassword([]byte(p), 12)
126174
return string(bytes), err

server/handlers_admin.go

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -336,35 +336,72 @@ func (s *Server) handleRemoveBannedIP(w http.ResponseWriter, r *http.Request) {
336336

337337
func (s *Server) handleAddWhitelistedIP(w http.ResponseWriter, r *http.Request) {
338338
if !s.isCSRFSafe(r) {
339+
http.Error(w, "Forbidden", http.StatusForbidden)
339340
return
340341
}
341342
user, ok := s.checkAuth(r)
342-
if ok && user.Role == config.RoleSuperAdmin {
343-
ip := r.FormValue("ip")
344-
if ip != "" {
345-
s.Config.WhitelistedIPs = append(s.Config.WhitelistedIPs, ip)
346-
s.Config.SaveConfig()
343+
if !ok || user.Role != config.RoleSuperAdmin {
344+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
345+
return
346+
}
347+
348+
ip := r.FormValue("ip")
349+
if ip == "" {
350+
http.Error(w, "IP address cannot be empty", http.StatusBadRequest)
351+
return
352+
}
353+
354+
// Check for duplicates
355+
for _, existingIP := range s.Config.WhitelistedIPs {
356+
if existingIP == ip {
357+
http.Error(w, "IP address already in the whitelist", http.StatusConflict)
358+
return
347359
}
348360
}
349-
http.Redirect(w, r, "/admin", http.StatusSeeOther)
361+
362+
s.Config.WhitelistedIPs = append(s.Config.WhitelistedIPs, ip)
363+
// For consistency, sort the list after adding
364+
sort.Strings(s.Config.WhitelistedIPs)
365+
s.Config.SaveConfig()
366+
367+
w.Header().Set("Content-Type", "application/json")
368+
json.NewEncoder(w).Encode(map[string]string{"ip": ip, "status": "added"})
350369
}
351370

352371
func (s *Server) handleRemoveWhitelistedIP(w http.ResponseWriter, r *http.Request) {
353372
if !s.isCSRFSafe(r) {
373+
http.Error(w, "Forbidden", http.StatusForbidden)
354374
return
355375
}
356376
user, ok := s.checkAuth(r)
357-
if ok && user.Role == config.RoleSuperAdmin {
358-
ip := r.FormValue("ip")
359-
for i, b := range s.Config.WhitelistedIPs {
360-
if b == ip {
361-
s.Config.WhitelistedIPs = append(s.Config.WhitelistedIPs[:i], s.Config.WhitelistedIPs[i+1:]...)
362-
s.Config.SaveConfig()
363-
break
364-
}
377+
if !ok || user.Role != config.RoleSuperAdmin {
378+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
379+
return
380+
}
381+
382+
ip := r.FormValue("ip")
383+
if ip == "" {
384+
http.Error(w, "IP address cannot be empty", http.StatusBadRequest)
385+
return
386+
}
387+
388+
found := false
389+
for i, b := range s.Config.WhitelistedIPs {
390+
if b == ip {
391+
s.Config.WhitelistedIPs = append(s.Config.WhitelistedIPs[:i], s.Config.WhitelistedIPs[i+1:]...)
392+
s.Config.SaveConfig()
393+
found = true
394+
break
365395
}
366396
}
367-
http.Redirect(w, r, "/admin", http.StatusSeeOther)
397+
398+
if !found {
399+
http.Error(w, "IP not found in whitelist", http.StatusNotFound)
400+
return
401+
}
402+
403+
w.Header().Set("Content-Type", "application/json")
404+
json.NewEncoder(w).Encode(map[string]string{"ip": ip, "status": "removed"})
368405
}
369406

370407
func (s *Server) handleClearAuthLockout(w http.ResponseWriter, r *http.Request) {

server/templates/admin.html

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,7 +1506,7 @@ <h1 class="page-title"><i data-lucide="shield-check"></i> Security & Bans</h1>
15061506
<h2>Ban Address or Range</h2>
15071507
<div class="card">
15081508
<form action="/admin/add-banned-ip" method="POST"
1509-
onsubmit="return submitForm(event, false)">
1509+
onsubmit="return submitForm(event, true)">
15101510
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
15111511
<div class="form-group">
15121512
<label>IP Address or CIDR Range</label>
@@ -1554,8 +1554,7 @@ <h2>Banned IP List</h2>
15541554
<section>
15551555
<h2>Whitelist Address or Range</h2>
15561556
<div class="card">
1557-
<form action="/admin/add-whitelisted-ip" method="POST"
1558-
onsubmit="return submitForm(event, false)">
1557+
<form action="/admin/add-whitelisted-ip" method="POST">
15591558
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
15601559
<div class="form-group">
15611560
<label>IP Address or CIDR Range</label>
@@ -1575,21 +1574,20 @@ <h2>Whitelisted IP List</h2>
15751574
<th style="width:30%">Action</th>
15761575
</tr>
15771576
</thead>
1578-
<tbody>
1577+
<tbody id="whitelist-tbody">
15791578
{{range .Config.WhitelistedIPs}}
15801579
<tr>
15811580
<td>{{.}}</td>
15821581
<td>
1583-
<form action="/admin/remove-whitelisted-ip" method="POST"
1584-
onsubmit="return submitForm(event, false)">
1582+
<form action="/admin/remove-whitelisted-ip" method="POST" class="remove-whitelist-form">
15851583
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
15861584
<input type="hidden" name="ip" value="{{.}}">
15871585
<button type="submit" class="btn btn-outline btn-sm">Remove</button>
15881586
</form>
15891587
</td>
15901588
</tr>
15911589
{{else}}
1592-
<tr>
1590+
<tr id="no-whitelist-ips-row">
15931591
<td colspan="2" style="text-align:center; padding:2rem">No IPs whitelisted.</td>
15941592
</tr>
15951593
{{end}}
@@ -2565,6 +2563,110 @@ <h2>Edit AutoDJ</h2>
25652563
if (window.location.hash) { var it = window.location.hash.substring(1); if (document.getElementById(it)) { showTab(it, false); } }
25662564
})();
25672565
</script>
2566+
<script>
2567+
document.addEventListener('DOMContentLoaded', function() {
2568+
const addWhitelistForm = document.querySelector('form[action="/admin/add-whitelisted-ip"]');
2569+
const whitelistTbody = document.getElementById('whitelist-tbody');
2570+
2571+
// --- Handle ADDING a whitelisted IP ---
2572+
if (addWhitelistForm) {
2573+
addWhitelistForm.addEventListener('submit', function(event) {
2574+
event.preventDefault();
2575+
const form = event.target;
2576+
const ipInput = form.querySelector('input[name="ip"]');
2577+
const ip = ipInput.value.trim();
2578+
2579+
if (!ip) {
2580+
alert('IP address cannot be empty.');
2581+
return;
2582+
}
2583+
2584+
// Client-side duplicate check
2585+
const existingIPs = Array.from(whitelistTbody.querySelectorAll('tr td:first-child')).map(td => td.textContent.trim());
2586+
if (existingIPs.includes(ip)) {
2587+
alert('This IP address is already in the whitelist.');
2588+
return;
2589+
}
2590+
2591+
const formData = new FormData(form);
2592+
2593+
fetch(form.action, {
2594+
method: 'POST',
2595+
body: formData
2596+
})
2597+
.then(response => {
2598+
if (response.ok) {
2599+
return response.json();
2600+
}
2601+
return response.text().then(text => { throw new Error(text || 'An unknown error occurred.') });
2602+
})
2603+
.then(data => {
2604+
if (data.status === 'added') {
2605+
const noIpRow = document.getElementById('no-whitelist-ips-row');
2606+
if (noIpRow) {
2607+
noIpRow.remove();
2608+
}
2609+
2610+
const newRow = document.createElement('tr');
2611+
newRow.innerHTML = `
2612+
<td>${data.ip}</td>
2613+
<td>
2614+
<form action="/admin/remove-whitelisted-ip" method="POST" class="remove-whitelist-form">
2615+
<input type="hidden" name="csrf" value="${csrfToken}">
2616+
<input type="hidden" name="ip" value="${data.ip}">
2617+
<button type="submit" class="btn btn-outline btn-sm">Remove</button>
2618+
</form>
2619+
</td>
2620+
`;
2621+
whitelistTbody.appendChild(newRow);
2622+
newRow.querySelector('.remove-whitelist-form').addEventListener('submit', handleRemoveSubmit);
2623+
ipInput.value = '';
2624+
}
2625+
})
2626+
.catch(error => {
2627+
alert('Failed to add IP: ' + error.message);
2628+
});
2629+
});
2630+
}
2631+
2632+
// --- Handle REMOVING a whitelisted IP ---
2633+
const handleRemoveSubmit = function(event) {
2634+
event.preventDefault();
2635+
const form = event.target;
2636+
const row = form.closest('tr');
2637+
const formData = new FormData(form);
2638+
2639+
fetch(form.action, {
2640+
method: 'POST',
2641+
body: formData
2642+
})
2643+
.then(response => {
2644+
if (response.ok) {
2645+
return response.json();
2646+
}
2647+
return response.text().then(text => { throw new Error(text || 'An unknown error occurred.') });
2648+
})
2649+
.then(data => {
2650+
if (data.status === 'removed') {
2651+
row.remove();
2652+
if (whitelistTbody.childElementCount === 0) {
2653+
const noIpRow = document.createElement('tr');
2654+
noIpRow.id = 'no-whitelist-ips-row';
2655+
noIpRow.innerHTML = '<td colspan="2" style="text-align:center; padding:2rem">No IPs whitelisted.</td>';
2656+
whitelistTbody.appendChild(noIpRow);
2657+
}
2658+
}
2659+
})
2660+
.catch(error => {
2661+
alert('Failed to remove IP: ' + error.message);
2662+
});
2663+
};
2664+
2665+
document.querySelectorAll('.remove-whitelist-form').forEach(form => {
2666+
form.addEventListener('submit', handleRemoveSubmit);
2667+
});
2668+
});
2669+
</script>
25682670
</body>
25692671

25702672
</html>

0 commit comments

Comments
 (0)