Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

more 2.0.4 items after testing #38

Merged
merged 11 commits into from
Aug 24, 2023
Merged
1 change: 1 addition & 0 deletions api/Sources/Api/Configure/migrations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ extension Configure {
app.migrations.add(DropWaitlistedAdmins())
app.migrations.add(DeviceRefactor())
app.migrations.add(AddReleaseNotes())
app.migrations.add(DeviceIdForeignKey())
}
}
18 changes: 18 additions & 0 deletions api/Sources/Api/Database/Migrations/013_DeviceIdForeignKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import FluentSQL

struct DeviceIdForeignKey: GertieMigration {
let fk = Constraint.foreignKey(
from: UserDevice.M11.self,
to: Device.M3.self,
thru: UserDevice.M11.deviceId,
onDelete: .cascade
)

func up(sql: SQLDatabase) async throws {
try await sql.add(constraint: fk)
}

func down(sql: SQLDatabase) async throws {
try await sql.drop(constraint: fk)
}
}
11 changes: 10 additions & 1 deletion api/Sources/Api/PairQL/Dashboard/Pairs/DeleteEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,19 @@ extension DeleteEntity: Resolver {
.delete()

case .user:
try await Current.db.query(User.self)
let deviceIds = try await context.userDevices().map(\.deviceId)
try await User.query()
.where(.id == input.id)
.where(.adminId == context.admin.id)
.delete(force: true)
let devices = try await Device.query()
.where(.id |=| deviceIds)
.all()
for device in devices {
if try await device.userDevices().isEmpty {
try await Current.db.delete(device.id)
}
}
try await Current.connectedApps.notify(.userDeleted(.init(input.id)))
}

Expand Down
5 changes: 4 additions & 1 deletion api/Sources/Api/PairQL/MacApp/Resolvers/CheckIn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import MacAppRoute

extension CheckIn: Resolver {
static func resolve(with input: Input, in context: UserContext) async throws -> Output {
async let v1 = RefreshRules.resolve(with: input, in: context)
async let v1 = RefreshRules.resolve(with: .init(appVersion: input.appVersion), in: context)
async let admin = context.user.admin()
async let userDevice = context.userDevice()
let channel = try await userDevice.adminDevice().appReleaseChannel
Expand All @@ -12,6 +12,9 @@ extension CheckIn: Resolver {
in: .init(requestId: context.requestId, dashboardUrl: context.dashboardUrl)
)

// TODO: use `input.filterVersion`
// https://github.com/gertrude-app/project/issues/185

return Output(
adminAccountStatus: try await admin.accountStatus,
appManifest: try await v1.appManifest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,16 @@ final class AuthedAdminResolverTests: ApiTestCase {
expect(retrieved).toBeNil()
}

func testDeletingUserDeletesOrphanedDevice() async throws {
let user = try await Entities.user().withDevice()
_ = try await DeleteEntity.resolve(
with: .init(id: user.id.rawValue, type: .user),
in: context(user.admin)
)
let retrieved = try? await Device.find(user.adminDevice.id)
expect(retrieved).toBeNil()
}

func testUpdateAdminNotification() async throws {
let admin = try await Entities.admin()
let email = try await Current.db.create(AdminVerifiedNotificationMethod(
Expand Down
25 changes: 20 additions & 5 deletions api/Tests/ApiTests/MacappPairResolvers/CheckInResolverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ final class CheckInResolverTests: ApiTestCase {
$0.screenshotsResolution = 1081
}).withDevice()

let output = try await CheckIn.resolve(with: .init(appVersion: "1.0.0"), in: user.context)
let output = try await CheckIn.resolve(
with: .init(appVersion: "1.0.0", filterVersion: nil),
in: user.context
)
expect(output.userData.name).toBe(user.name)
expect(output.userData.keyloggingEnabled).toBeFalse()
expect(output.userData.screenshotsEnabled).toBeTrue()
Expand All @@ -36,7 +39,10 @@ final class CheckInResolverTests: ApiTestCase {
$0.appReleaseChannel = .beta
})

let output = try await CheckIn.resolve(with: .init(appVersion: "1.0.0"), in: user.context)
let output = try await CheckIn.resolve(
with: .init(appVersion: "1.0.0", filterVersion: nil),
in: user.context
)

expect(output.adminAccountStatus).toEqual(.needsAttention)
expect(output.updateReleaseChannel).toEqual(.beta)
Expand All @@ -55,15 +61,21 @@ final class CheckInResolverTests: ApiTestCase {
try await Current.db.create(id)

let user = try await Entities.user().withDevice()
let output = try await CheckIn.resolve(with: .init(appVersion: "1.0.0"), in: user.context)
let output = try await CheckIn.resolve(
with: .init(appVersion: "1.0.0", filterVersion: nil),
in: user.context
)
expect(output.appManifest.apps).toEqual([app.slug: [id.bundleId]])
}

func testUserWithNoKeychainsDoesNotGetAutoIncluded() async throws {
let user = try await Entities.user().withDevice()
try await createAutoIncludeKeychain()

let output = try await CheckIn.resolve(with: .init(appVersion: "1.0.0"), in: user.context)
let output = try await CheckIn.resolve(
with: .init(appVersion: "1.0.0", filterVersion: nil),
in: user.context
)
expect(output.keys).toHaveCount(0)
}

Expand All @@ -73,7 +85,10 @@ final class CheckInResolverTests: ApiTestCase {
try await Current.db.create(UserKeychain(userId: user.id, keychainId: admin.keychain.id))
let (_, autoKey) = try await createAutoIncludeKeychain()

let output = try await CheckIn.resolve(with: .init(appVersion: "1.0.0"), in: user.context)
let output = try await CheckIn.resolve(
with: .init(appVersion: "1.0.0", filterVersion: nil),
in: user.context
)
expect(output.keys.contains(.init(id: autoKey.id.rawValue, key: autoKey.key))).toBeTrue()
}
}
Expand Down
5 changes: 3 additions & 2 deletions macapp/App/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@
{
"identity" : "swift-composable-architecture",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jaredh159/swift-composable-architecture.git",
"location" : "https://github.com/pointfreeco/swift-composable-architecture.git",
"state" : {
"revision" : "639f2d1994c997c44680c6599c4f9a58b88f7960"
"revision" : "a7c1f799b55ecb418f85094b142565834f7ee7c7",
"version" : "1.2.0"
}
},
{
Expand Down
7 changes: 1 addition & 6 deletions macapp/App/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@ let package = Package(
.library(name: "TestSupport", targets: ["TestSupport"]),
],
dependencies: [
// pin to fork to prevent swiftui catalina crash, until some resolution to:
// https://github.com/pointfreeco/swift-composable-architecture/discussions/2351
.package(
url: "https://github.com/jaredh159/swift-composable-architecture.git",
revision: "639f2d1994c997c44680c6599c4f9a58b88f7960"
),
.github("pointfreeco/swift-composable-architecture", from: "1.2.0"),
.github("pointfreeco/swift-dependencies", from: "1.0.0"),
.github("pointfreeco/combine-schedulers", from: "1.0.0"),
.github("jaredh159/swift-tagged", from: "0.8.2"),
Expand Down
21 changes: 12 additions & 9 deletions macapp/App/Sources/App/AdminWindow/AdminWindowFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ struct AdminWindowFeature: Feature {
case gotoScreenClicked(screen: Screen)
case confirmStopFilterClicked
case confirmQuitAppClicked
case reconnectUserClicked
case disconnectUserClicked
case administrateOSUserAccountsClicked
case checkForAppUpdatesClicked
case updateAppNowClicked
case setUserExemption(userId: uid_t, enabled: Bool)
case inactiveAccountRecheckClicked
case inactiveAccountDisconnectAppClicked
Expand Down Expand Up @@ -303,7 +303,7 @@ extension AdminWindowFeature.RootReducer {
case .webview(.healthCheck(.upgradeAppClicked)):
return adminAuthenticated(action)

case .webview(.checkForAppUpdatesClicked):
case .webview(.updateAppNowClicked):
return .none // handled by AppUpdatesFeature

case .webview(.confirmQuitAppClicked):
Expand All @@ -312,7 +312,7 @@ extension AdminWindowFeature.RootReducer {
case .webview(.confirmStopFilterClicked):
return adminAuthenticated(action)

case .webview(.reconnectUserClicked):
case .webview(.disconnectUserClicked):
return adminAuthenticated(action)

case .webview(.setUserExemption):
Expand Down Expand Up @@ -394,7 +394,7 @@ extension AdminWindowFeature.RootReducer {
await app.quit()
}

case .webview(.reconnectUserClicked):
case .webview(.disconnectUserClicked):
// handled by UserConnectionFeature
return .none

Expand Down Expand Up @@ -450,6 +450,7 @@ extension AdminWindowFeature.RootReducer {

func checkHealth(state: inout State, action: Action) -> Effect<Action> {
state.adminWindow.healthCheck = .init() // put all checks into checking state
let filterVersion = state.filter.version
let keyloggingEnabled = state.user.data?.keyloggingEnabled == true
let screenRecordingEnabled = state.user.data?.screenshotsEnabled == true

Expand All @@ -458,7 +459,7 @@ extension AdminWindowFeature.RootReducer {

await send(.checkIn(
result: network.isConnected()
? TaskResult { try await api.appCheckIn() }
? TaskResult { try await api.appCheckIn(filterVersion) }
: .failure(NetworkClient.NotConnected()),
reason: .healthCheck
))
Expand Down Expand Up @@ -575,6 +576,7 @@ extension AdminWindowFeature.State.View {
init(rootState: AppReducer.State) {
@Dependency(\.app) var app
let featureState = rootState.adminWindow
let installedVersion = app.installedVersion() ?? "0.0.0"

windowOpen = featureState.windowOpen
screen = featureState.screen
Expand All @@ -585,11 +587,12 @@ extension AdminWindowFeature.State.View {
screenshotMonitoringEnabled: user.screenshotsEnabled,
keystrokeMonitoringEnabled: user.keyloggingEnabled
) }
installedAppVersion = app.installedVersion() ?? "0.0.0"
installedAppVersion = installedVersion
releaseChannel = rootState.appUpdates.releaseChannel
quitting = featureState.quitting
availableAppUpdate = rootState.appUpdates.latestVersion.map { latest in
.init(

if let latest = rootState.appUpdates.latestVersion, latest.semver > installedVersion {
availableAppUpdate = .init(
semver: latest.semver,
required: latest.pace.map { pace in
@Dependency(\.date.now) var now
Expand Down
13 changes: 12 additions & 1 deletion macapp/App/Sources/App/App+PersistantState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@ import Core
import Gertie

enum Persistent {
typealias State = V1
typealias State = V2

// v2.0.4 - *
struct V2: PersistentState {
static let version = 2
var appVersion: String
var appUpdateReleaseChannel: ReleaseChannel
var filterVersion: String
var user: UserData?
}

// v2.0.0 - v2.0.3
struct V1: PersistentState {
static let version = 1
var appVersion: String
Expand All @@ -17,6 +27,7 @@ extension AppReducer.State {
.init(
appVersion: appUpdates.installedVersion,
appUpdateReleaseChannel: appUpdates.releaseChannel,
filterVersion: filter.version,
user: user.data
)
}
Expand Down
26 changes: 21 additions & 5 deletions macapp/App/Sources/App/AppMigrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,28 @@ struct AppMigrator: Migrator {
var context = "App"

func migrateLastVersion() async -> Persistent.State? {
await migrateV1()
await migrateV2()
}

func migrateV2() async -> Persistent.V2? {
var v1 = try? userDefaults.getString(Persistent.V1.storageKey).flatMap { json in
try JSON.decode(json, as: Persistent.V1.self)
}
if v1 == nil {
v1 = await migrateV1()
}
guard let v1 else { return nil }
return .init(
appVersion: v1.appVersion,
appUpdateReleaseChannel: v1.appUpdateReleaseChannel,
filterVersion: v1.appVersion,
user: v1.user
)
}

// v1 below refers to legacy 1.x version of the app
// before ComposableArchitecture rewrite
func migrateV1() async -> Persistent.State? {
func migrateV1() async -> Persistent.V1? {
typealias V1 = Legacy.V1

guard let token = userDefaults
Expand All @@ -25,12 +41,12 @@ struct AppMigrator: Migrator {

log("found v1 token `\(token)`")
await api.setUserToken(token)
let user = (try? await api.appCheckIn())?.userData
let user = (try? await api.appCheckIn(nil))?.userData
let v1Version = userDefaults.v1(.installedAppVersion) ?? "unknown"

if let user {
log("migrated v1 state from successful user api call")
return Persistent.State(
return Persistent.V1(
appVersion: v1Version,
appUpdateReleaseChannel: .stable,
user: user
Expand All @@ -53,7 +69,7 @@ struct AppMigrator: Migrator {
let screenshotsSize = userDefaults.v1(.screenshotSize).flatMap(Int.init)

log("migrated v1 state from fallback storage")
return Persistent.State(
return Persistent.V1(
appVersion: v1Version,
appUpdateReleaseChannel: .stable,
user: UserData(
Expand Down
5 changes: 3 additions & 2 deletions macapp/App/Sources/App/AppReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,14 @@ struct AppReducer: Reducer, Sendable {
switch action {
case .loadedPersistentState(.some(let persistent)):
state.appUpdates.releaseChannel = persistent.appUpdateReleaseChannel
state.filter.version = persistent.filterVersion
guard let user = persistent.user else { return .none }
state.user = .init(data: user)
return .exec { send in
return .exec { [filterVersion = state.filter.version] send in
await api.setUserToken(user.token)
try await bgQueue.sleep(for: .milliseconds(10)) // <- unit test determinism
return await send(.checkIn(
result: TaskResult { try await api.appCheckIn() },
result: TaskResult { try await api.appCheckIn(filterVersion) },
reason: .appLaunched
))
}
Expand Down
7 changes: 4 additions & 3 deletions macapp/App/Sources/App/AppUpdatesFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ extension AppUpdatesFeature.RootReducer: FilterControlling {
.exec { [updated = state.persistent] _ in
try await storage.savePersistentState(updated)
},
.exec { send in
.exec { [version = state.appUpdates.installedVersion] send in
switch await filter.state() {
case .notInstalled:
await send(.appUpdates(.delegate(.postUpdateFilterNotInstalled)))
Expand All @@ -79,10 +79,11 @@ extension AppUpdatesFeature.RootReducer: FilterControlling {
await send(.appUpdates(.delegate(.postUpdateFilterReplaceFailed)))
unexpectedError(id: "cde231a0", detail: "state: \(await filter.state())")
} else {
await send(.filter(.replacedFilterVersion(version)))

// refresh the rules post-update, or else health check will complain
await send(.checkIn(
result: TaskResult { try await api.appCheckIn() },
result: TaskResult { try await api.appCheckIn(version) },
reason: .appLaunched
))

Expand All @@ -97,7 +98,7 @@ extension AppUpdatesFeature.RootReducer: FilterControlling {

// don't need admin challenge, because sparkle can't update w/out admin auth
case .adminWindow(.delegate(.triggerAppUpdate)),
.adminWindow(.webview(.checkForAppUpdatesClicked)),
.adminWindow(.webview(.updateAppNowClicked)),
.menuBar(.updateNagUpdateClicked),
.menuBar(.updateRequiredUpdateClicked):
state.adminWindow.windowOpen = false // so they can see sparkle update
Expand Down
Loading