added admin page and audit log viewing

This commit is contained in:
2026-02-11 19:07:40 +11:00
parent 2a3f4e4861
commit 4c80165f01
22 changed files with 1298 additions and 155 deletions

View File

@@ -0,0 +1,156 @@
package adminview
import "git.haelnorr.com/h/oslstats/internal/db"
import "fmt"
import "time"
import "encoding/json"
templ AuditLogDetail(log *db.AuditLog) {
<!-- Modal overlay -->
<div
class="fixed inset-0 bg-crust/80 flex items-center justify-center z-50 p-4"
x-data="{ show: true }"
x-show="show"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click.self="show = false; setTimeout(() => document.getElementById('modal-container').innerHTML = '', 200)"
>
<!-- Modal content -->
<div
class="bg-base border border-surface1 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
>
<!-- Header -->
<div class="flex justify-between items-center p-6 border-b border-surface1">
<h2 class="text-xl font-bold text-text">Audit Log Details</h2>
<button
@click="show = false; setTimeout(() => document.getElementById('modal-container').innerHTML = '', 200)"
class="text-subtext0 hover:text-text transition"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Body -->
<div class="p-6 space-y-4">
<!-- ID -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">ID</label>
<p class="text-text">{ fmt.Sprintf("%d", log.ID) }</p>
</div>
<!-- User -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">User</label>
<p class="text-text">
if log.User != nil {
{ log.User.Username } <span class="text-subtext1 text-sm">(ID: { fmt.Sprintf("%d", log.UserID) })</span>
} else {
<span class="text-subtext1 italic">Unknown User (ID: { fmt.Sprintf("%d", log.UserID) })</span>
}
</p>
</div>
<!-- Timestamp -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">Timestamp</label>
<p class="text-text">{ formatDetailTimestamp(log.CreatedAt) }</p>
</div>
<!-- Action -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">Action</label>
<p class="text-text font-mono">{ log.Action }</p>
</div>
<!-- Resource Type -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">Resource Type</label>
<p class="text-text">{ log.ResourceType }</p>
</div>
<!-- Resource ID -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">Resource ID</label>
<p class="text-text font-mono">
if log.ResourceID != nil {
{ *log.ResourceID }
} else {
<span class="text-subtext1 italic">N/A</span>
}
</p>
</div>
<!-- Result -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">Result</label>
<div>
@resultBadge(log.Result)
</div>
</div>
<!-- Error Message (if applicable) -->
if log.ErrorMessage != nil && *log.ErrorMessage != "" {
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">Error Message</label>
<div class="bg-red/10 border border-red/30 rounded p-3">
<p class="text-red font-mono text-sm">{ *log.ErrorMessage }</p>
</div>
</div>
}
<!-- IP Address -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">IP Address</label>
<p class="text-text font-mono">{ log.IPAddress }</p>
</div>
<!-- User Agent -->
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">User Agent</label>
<p class="text-text text-sm break-all">{ log.UserAgent }</p>
</div>
<!-- Details JSON -->
if log.Details != nil && len(log.Details) > 0 && string(log.Details) != "null" {
<div>
<label class="block text-sm font-semibold text-subtext0 mb-1">Details</label>
<div class="bg-mantle border border-surface1 rounded p-3 overflow-x-auto">
<pre class="text-text text-xs font-mono whitespace-pre-wrap">{ formatJSON(log.Details) }</pre>
</div>
</div>
}
</div>
<!-- Footer -->
<div class="flex justify-end gap-2 p-6 border-t border-surface1">
<button
@click="show = false; setTimeout(() => document.getElementById('modal-container').innerHTML = '', 200)"
class="px-4 py-2 bg-surface1 hover:bg-surface2 text-text rounded font-medium transition hover:cursor-pointer"
>
Close
</button>
</div>
</div>
</div>
}
func formatDetailTimestamp(unixTime int64) string {
t := time.Unix(unixTime, 0)
return t.Format("Monday, January 2, 2006 at 3:04:05 PM MST")
}
func formatJSON(raw []byte) string {
if len(raw) == 0 || string(raw) == "null" {
return "No details available"
}
// Pretty print the JSON
var obj interface{}
if err := json.Unmarshal(raw, &obj); err != nil {
return string(raw)
}
pretty, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return string(raw)
}
return string(pretty)
}