saveEvents used .iso8601 encoding but loadEvents used the default decoder (.deferredToDate). ISO8601 date strings could not be decoded back, causing loadEvents() to return [] after the first log — breaking event rotation and losing all previous audit entries.
83 lines
2.8 KiB
Swift
83 lines
2.8 KiB
Swift
// ── Audit Logger ────────────────────────────────────────────
|
|
// Generic local audit logger that tracks user actions for debugging.
|
|
// Stores events in a rotating JSON file (configurable max entries).
|
|
// Product apps configure with a product-specific file name.
|
|
|
|
import Foundation
|
|
|
|
/// Audit event stored locally.
|
|
public struct BLAuditEvent: Codable, Sendable {
|
|
public let id: String
|
|
public let action: String
|
|
public let details: String?
|
|
public let timestamp: Date
|
|
|
|
public init(action: String, details: String? = nil) {
|
|
self.id = UUID().uuidString
|
|
self.action = action
|
|
self.details = details
|
|
self.timestamp = Date()
|
|
}
|
|
}
|
|
|
|
/// Generic local audit logger for all ByteLyst iOS apps.
|
|
/// Stores events in a rotating JSON file in the Documents directory.
|
|
public enum BLAuditLogger {
|
|
|
|
private static var maxEvents = 1000
|
|
private static var fileName = "audit_log.json"
|
|
|
|
/// Configure the logger with a product-specific file name and max events.
|
|
public static func configure(fileName: String = "audit_log.json", maxEvents: Int = 1000) {
|
|
self.fileName = fileName
|
|
self.maxEvents = maxEvents
|
|
}
|
|
|
|
private static var fileURL: URL {
|
|
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
return docs.appendingPathComponent(fileName)
|
|
}
|
|
|
|
/// Log a user action.
|
|
public static func log(_ action: String, details: String? = nil) {
|
|
let event = BLAuditEvent(action: action, details: details)
|
|
|
|
var events = loadEvents()
|
|
events.append(event)
|
|
|
|
// Rotate: keep only the most recent maxEvents
|
|
if events.count > maxEvents {
|
|
events = Array(events.suffix(maxEvents))
|
|
}
|
|
|
|
saveEvents(events)
|
|
}
|
|
|
|
/// Get all logged events (newest first).
|
|
public static func getEvents(limit: Int = 100) -> [BLAuditEvent] {
|
|
let events = loadEvents()
|
|
return Array(events.suffix(limit).reversed())
|
|
}
|
|
|
|
/// Clear all audit logs.
|
|
public static func clear() {
|
|
try? FileManager.default.removeItem(at: fileURL)
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
private static func loadEvents() -> [BLAuditEvent] {
|
|
guard let data = try? Data(contentsOf: fileURL) else { return [] }
|
|
let decoder = JSONDecoder()
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
return (try? decoder.decode([BLAuditEvent].self, from: data)) ?? []
|
|
}
|
|
|
|
private static func saveEvents(_ events: [BLAuditEvent]) {
|
|
let encoder = JSONEncoder()
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
guard let data = try? encoder.encode(events) else { return }
|
|
try? data.write(to: fileURL, options: .atomic)
|
|
}
|
|
}
|