mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 15:44:32 +00:00
Something in the way
This commit is contained in:
parent
018050081d
commit
9bd445986d
19 changed files with 2395 additions and 1 deletions
|
|
@ -0,0 +1,202 @@
|
|||
import Foundation
|
||||
import Defaults
|
||||
import AtollExtensionKit
|
||||
|
||||
@MainActor
|
||||
final class ExtensionAuthorizationManager: ObservableObject {
|
||||
static let shared = ExtensionAuthorizationManager()
|
||||
|
||||
@Published private(set) var entries: [ExtensionAuthorizationEntry]
|
||||
@Published private(set) var rateLimitRecords: [ExtensionRateLimitRecord]
|
||||
|
||||
private let persistenceQueue = DispatchQueue(label: "com.atoll.extensions.authorization", qos: .utility)
|
||||
|
||||
private init() {
|
||||
self.entries = Defaults[.extensionAuthorizationEntries]
|
||||
self.rateLimitRecords = Defaults[.extensionRateLimitRecords]
|
||||
normalizeState()
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
var isExtensionsFeatureEnabled: Bool { Defaults[.enableThirdPartyExtensions] }
|
||||
var areLiveActivitiesEnabled: Bool { Defaults[.enableExtensionLiveActivities] }
|
||||
var areLockScreenWidgetsEnabled: Bool { Defaults[.enableExtensionLockScreenWidgets] }
|
||||
|
||||
func updateFeatureToggles(extensionsEnabled: Bool? = nil,
|
||||
liveActivitiesEnabled: Bool? = nil,
|
||||
lockScreenWidgetsEnabled: Bool? = nil) {
|
||||
if let extensionsEnabled { Defaults[.enableThirdPartyExtensions] = extensionsEnabled }
|
||||
if let liveActivitiesEnabled { Defaults[.enableExtensionLiveActivities] = liveActivitiesEnabled }
|
||||
if let lockScreenWidgetsEnabled { Defaults[.enableExtensionLockScreenWidgets] = lockScreenWidgetsEnabled }
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
func authorizationEntry(for bundleIdentifier: String) -> ExtensionAuthorizationEntry? {
|
||||
entries.first { $0.bundleIdentifier == bundleIdentifier }
|
||||
}
|
||||
|
||||
func ensureEntryExists(bundleIdentifier: String, appName: String?) -> ExtensionAuthorizationEntry {
|
||||
if let existing = authorizationEntry(for: bundleIdentifier) {
|
||||
return existing
|
||||
}
|
||||
let resolvedName = appName ?? bundleIdentifier
|
||||
let entry = ExtensionAuthorizationEntry(
|
||||
bundleIdentifier: bundleIdentifier,
|
||||
appName: resolvedName,
|
||||
status: .pending,
|
||||
allowedScopes: defaultScopes()
|
||||
)
|
||||
entries.append(entry)
|
||||
persistEntries()
|
||||
return entry
|
||||
}
|
||||
|
||||
func authorize(bundleIdentifier: String, appName: String?, scopes: Set<ExtensionPermissionScope>? = nil) {
|
||||
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
|
||||
entry.appName = appName ?? entry.appName
|
||||
entry.status = .authorized
|
||||
entry.allowedScopes = scopes ?? entry.allowedScopes
|
||||
entry.grantedAt = .now
|
||||
entry.lastDeniedReason = nil
|
||||
}
|
||||
}
|
||||
|
||||
func deny(bundleIdentifier: String, reason: String?) {
|
||||
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
|
||||
entry.status = .denied
|
||||
entry.lastDeniedReason = reason
|
||||
}
|
||||
}
|
||||
|
||||
func revoke(bundleIdentifier: String, reason: String?) {
|
||||
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
|
||||
entry.status = .revoked
|
||||
entry.lastDeniedReason = reason
|
||||
}
|
||||
}
|
||||
|
||||
func recordActivity(for bundleIdentifier: String, scope: ExtensionPermissionScope) {
|
||||
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
|
||||
entry.lastActivityAt = .now
|
||||
}
|
||||
|
||||
updateRateLimitRecord(bundleIdentifier: bundleIdentifier) { record in
|
||||
let now = Date()
|
||||
switch scope {
|
||||
case .liveActivities:
|
||||
record.activityTimestamps.append(now)
|
||||
case .lockScreenWidgets:
|
||||
record.widgetTimestamps.append(now)
|
||||
}
|
||||
record.activityTimestamps = flushOldTimestamps(record.activityTimestamps)
|
||||
record.widgetTimestamps = flushOldTimestamps(record.widgetTimestamps)
|
||||
}
|
||||
}
|
||||
|
||||
func updateAllowedScopes(bundleIdentifier: String, allowedScopes: Set<ExtensionPermissionScope>) {
|
||||
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
|
||||
entry.allowedScopes = allowedScopes
|
||||
}
|
||||
}
|
||||
|
||||
func resetRateLimits(for bundleIdentifier: String) {
|
||||
rateLimitRecords.removeAll { $0.bundleIdentifier == bundleIdentifier }
|
||||
persistRateLimitRecords()
|
||||
}
|
||||
|
||||
func removeEntry(bundleIdentifier: String) {
|
||||
entries.removeAll { $0.bundleIdentifier == bundleIdentifier }
|
||||
rateLimitRecords.removeAll { $0.bundleIdentifier == bundleIdentifier }
|
||||
persistEntries()
|
||||
persistRateLimitRecords()
|
||||
}
|
||||
|
||||
// MARK: - Validation Helpers
|
||||
|
||||
func canProcessLiveActivityRequest(from bundleIdentifier: String) -> Bool {
|
||||
guard preflight(bundleIdentifier: bundleIdentifier, scope: .liveActivities) else { return false }
|
||||
guard areLiveActivitiesEnabled else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
func canProcessLockScreenRequest(from bundleIdentifier: String) -> Bool {
|
||||
guard preflight(bundleIdentifier: bundleIdentifier, scope: .lockScreenWidgets) else { return false }
|
||||
guard areLockScreenWidgetsEnabled else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
func recordDeniedRequest(bundleIdentifier: String, reason: String) {
|
||||
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
|
||||
entry.lastDeniedReason = reason
|
||||
entry.status = entry.status == .authorized ? .revoked : entry.status
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Internal State Updates
|
||||
|
||||
private func updateEntry(bundleIdentifier: String, mutate: (inout ExtensionAuthorizationEntry) -> Void) {
|
||||
guard let index = entries.firstIndex(where: { $0.bundleIdentifier == bundleIdentifier }) else {
|
||||
var entry = ensureEntryExists(bundleIdentifier: bundleIdentifier, appName: bundleIdentifier)
|
||||
mutate(&entry)
|
||||
entries.removeAll { $0.bundleIdentifier == bundleIdentifier }
|
||||
entries.append(entry)
|
||||
persistEntries()
|
||||
return
|
||||
}
|
||||
var entry = entries[index]
|
||||
mutate(&entry)
|
||||
entries[index] = entry
|
||||
persistEntries()
|
||||
}
|
||||
|
||||
private func updateRateLimitRecord(bundleIdentifier: String, mutate: (inout ExtensionRateLimitRecord) -> Void) {
|
||||
if let index = rateLimitRecords.firstIndex(where: { $0.bundleIdentifier == bundleIdentifier }) {
|
||||
var record = rateLimitRecords[index]
|
||||
mutate(&record)
|
||||
rateLimitRecords[index] = record
|
||||
persistRateLimitRecords()
|
||||
} else {
|
||||
var newRecord = ExtensionRateLimitRecord(bundleIdentifier: bundleIdentifier)
|
||||
mutate(&newRecord)
|
||||
rateLimitRecords.append(newRecord)
|
||||
persistRateLimitRecords()
|
||||
}
|
||||
}
|
||||
|
||||
private func preflight(bundleIdentifier: String, scope: ExtensionPermissionScope) -> Bool {
|
||||
guard isExtensionsFeatureEnabled else { return false }
|
||||
guard let entry = authorizationEntry(for: bundleIdentifier) else { return false }
|
||||
guard entry.isAuthorized else { return false }
|
||||
guard entry.allowedScopes.contains(scope) else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
private func flushOldTimestamps(_ timestamps: [Date]) -> [Date] {
|
||||
let threshold = Date().addingTimeInterval(-60 * 5)
|
||||
return timestamps.filter { $0 >= threshold }
|
||||
}
|
||||
|
||||
private func defaultScopes() -> Set<ExtensionPermissionScope> {
|
||||
Set(ExtensionPermissionScope.allCases)
|
||||
}
|
||||
|
||||
private func persistEntries() {
|
||||
let entriesSnapshot = entries
|
||||
persistenceQueue.async {
|
||||
Defaults[.extensionAuthorizationEntries] = entriesSnapshot
|
||||
}
|
||||
}
|
||||
|
||||
private func persistRateLimitRecords() {
|
||||
let recordsSnapshot = rateLimitRecords
|
||||
persistenceQueue.async {
|
||||
Defaults[.extensionRateLimitRecords] = recordsSnapshot
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizeState() {
|
||||
entries = entries.filter { !$0.bundleIdentifier.isEmpty }
|
||||
rateLimitRecords = rateLimitRecords.filter { !$0.bundleIdentifier.isEmpty }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue