157 lines
5.4 KiB
Plaintext
157 lines
5.4 KiB
Plaintext
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)
|
|
}
|