mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-05 09:24:49 +01:00
Updater (#43)
This commit is contained in:
@@ -13,10 +13,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
list.add(store: SmartCard.Store())
|
||||
return list
|
||||
}()
|
||||
let updater = PreviewUpdater()
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
|
||||
let contentView = ContentView(storeList: storeList)
|
||||
let contentView = ContentView(storeList: storeList, updater: updater)
|
||||
// Create the window and set the content view.
|
||||
window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
|
||||
struct ContentView: View {
|
||||
struct ContentView<UpdaterType: UpdaterProtocol>: View {
|
||||
|
||||
@ObservedObject var storeList: SecretStoreList
|
||||
@ObservedObject var updater: UpdaterType
|
||||
|
||||
@State fileprivate var active: AnySecret.ID?
|
||||
@State fileprivate var showingDeletion = false
|
||||
@State fileprivate var deletingSecret: AnySecret?
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List(selection: $active) {
|
||||
ForEach(storeList.stores) { store in
|
||||
if store.isAvailable {
|
||||
Section(header: Text(store.name)) {
|
||||
if store.secrets.isEmpty {
|
||||
if store is AnySecretStoreModifiable {
|
||||
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) {
|
||||
Text("No Secrets")
|
||||
VStack {
|
||||
if updater.update != nil {
|
||||
updateNotice()
|
||||
}
|
||||
NavigationView {
|
||||
List(selection: $active) {
|
||||
ForEach(storeList.stores) { store in
|
||||
if store.isAvailable {
|
||||
Section(header: Text(store.name)) {
|
||||
if store.secrets.isEmpty {
|
||||
if store is AnySecretStoreModifiable {
|
||||
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) {
|
||||
Text("No Secrets")
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) {
|
||||
Text("No Secrets")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) {
|
||||
Text("No Secrets")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ForEach(store.secrets) { secret in
|
||||
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
|
||||
Text(secret.name)
|
||||
}.contextMenu {
|
||||
if store is AnySecretStoreModifiable {
|
||||
Button(action: { self.delete(secret: secret) }) {
|
||||
Text("Delete")
|
||||
ForEach(store.secrets) { secret in
|
||||
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
|
||||
Text(secret.name)
|
||||
}.contextMenu {
|
||||
if store is AnySecretStoreModifiable {
|
||||
Button(action: { self.delete(secret: secret) }) {
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,31 +46,45 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onAppear {
|
||||
let fallback: AnyHashable
|
||||
if self.storeList.modifiableStore?.isAvailable ?? false {
|
||||
fallback = Constants.emptyStoreModifiableTag
|
||||
} else {
|
||||
fallback = Constants.emptyStoreTag
|
||||
}
|
||||
self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
|
||||
}
|
||||
}.onAppear {
|
||||
let fallback: AnyHashable
|
||||
if self.storeList.modifiableStore?.isAvailable ?? false {
|
||||
fallback = Constants.emptyStoreModifiableTag
|
||||
} else {
|
||||
fallback = Constants.emptyStoreTag
|
||||
}
|
||||
self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
|
||||
.listStyle(SidebarListStyle())
|
||||
.frame(minWidth: 100, idealWidth: 240)
|
||||
}
|
||||
.listStyle(SidebarListStyle())
|
||||
.frame(minWidth: 100, idealWidth: 240)
|
||||
}
|
||||
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
||||
.sheet(isPresented: $showingDeletion) {
|
||||
if self.storeList.modifiableStore != nil {
|
||||
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
|
||||
self.showingDeletion = false
|
||||
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
||||
.sheet(isPresented: $showingDeletion) {
|
||||
if self.storeList.modifiableStore != nil {
|
||||
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
|
||||
self.showingDeletion = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
func updateNotice() -> some View {
|
||||
guard let update = updater.update else { return AnyView(Spacer()) }
|
||||
let severity: NoticeView.Severity
|
||||
let text: String
|
||||
if update.critical {
|
||||
severity = .critical
|
||||
text = "Critical Security Update Required"
|
||||
} else {
|
||||
severity = .advisory
|
||||
text = "Update Available"
|
||||
}
|
||||
return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update") {
|
||||
NSWorkspace.shared.open(update.html_url)
|
||||
})
|
||||
}
|
||||
|
||||
func delete<SecretType: Secret>(secret: SecretType) {
|
||||
deletingSecret = AnySecret(secret)
|
||||
self.showingDeletion = true
|
||||
@@ -72,24 +92,23 @@ struct ContentView: View {
|
||||
|
||||
}
|
||||
|
||||
extension ContentView {
|
||||
|
||||
enum Constants {
|
||||
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
|
||||
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
|
||||
}
|
||||
|
||||
fileprivate enum Constants {
|
||||
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
|
||||
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
|
||||
ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
|
||||
ContentView(storeList: Preview.storeList(stores: [Preview.Store()]))
|
||||
ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]))
|
||||
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater())
|
||||
ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater())
|
||||
ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater())
|
||||
ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater())
|
||||
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .advisory))
|
||||
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>$(CI_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CI_VERSION)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
||||
57
Secretive/NoticeView.swift
Normal file
57
Secretive/NoticeView.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct NoticeView: View {
|
||||
|
||||
let text: String
|
||||
let severity: Severity
|
||||
let actionTitle: String?
|
||||
let action: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(text).bold()
|
||||
Spacer()
|
||||
if action != nil {
|
||||
Button(action: action!) {
|
||||
Text(actionTitle!)
|
||||
}
|
||||
}
|
||||
}.padding().background(color)
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch severity {
|
||||
case .advisory:
|
||||
return Color.orange
|
||||
case .critical:
|
||||
return Color.red
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NoticeView {
|
||||
|
||||
enum Severity {
|
||||
case advisory, critical
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct NoticeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
NoticeView(text: "Agent Not Running", severity: .advisory, actionTitle: "Run Setup") {
|
||||
print("OK")
|
||||
}
|
||||
NoticeView(text: "Critical Security Update Required", severity: .critical, actionTitle: "Update") {
|
||||
print("OK")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
27
Secretive/Preview Content/PreviewUpdater.swift
Normal file
27
Secretive/Preview Content/PreviewUpdater.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class PreviewUpdater: UpdaterProtocol {
|
||||
|
||||
let update: Release?
|
||||
|
||||
init(update: Update = .none) {
|
||||
switch update {
|
||||
case .none:
|
||||
self.update = nil
|
||||
case .advisory:
|
||||
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Some regular update")
|
||||
case .critical:
|
||||
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PreviewUpdater {
|
||||
|
||||
enum Update {
|
||||
case none, advisory, critical
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.smartcard</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>
|
||||
|
||||
82
Secretive/Updater.swift
Normal file
82
Secretive/Updater.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
protocol UpdaterProtocol: ObservableObject {
|
||||
|
||||
var update: Release? { get }
|
||||
|
||||
}
|
||||
|
||||
class Updater: ObservableObject, UpdaterProtocol {
|
||||
|
||||
@Published var update: Release?
|
||||
|
||||
init() {
|
||||
checkForUpdates()
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
|
||||
self.checkForUpdates()
|
||||
}
|
||||
timer.tolerance = 60*60
|
||||
}
|
||||
|
||||
func checkForUpdates() {
|
||||
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
|
||||
guard let data = data else { return }
|
||||
guard let release = try? JSONDecoder().decode(Release.self, from: data) else { return }
|
||||
self.evaluate(release: release)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func evaluate(release: Release) {
|
||||
let latestVersion = semVer(from: release.name)
|
||||
let currentVersion = semVer(from: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
|
||||
for (latest, current) in zip(latestVersion, currentVersion) {
|
||||
if latest > current {
|
||||
DispatchQueue.main.async {
|
||||
self.update = release
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func semVer(from stringVersion: String) -> [Int] {
|
||||
var split = stringVersion.split(separator: ".").compactMap { Int($0) }
|
||||
while split.count < 3 {
|
||||
split.append(0)
|
||||
}
|
||||
return split
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Updater {
|
||||
|
||||
enum Constants {
|
||||
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases/latest")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct Release: Codable {
|
||||
let name: String
|
||||
let html_url: URL
|
||||
let body: String
|
||||
}
|
||||
|
||||
|
||||
extension Release {
|
||||
|
||||
var critical: Bool {
|
||||
return body.contains(Constants.securityContent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Release {
|
||||
|
||||
enum Constants {
|
||||
static let securityContent = "Critical Security Update"
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user