Add refresh tokens.

This commit is contained in:
Marcin Czachursk 2023-02-06 14:50:28 +01:00
parent f9ffe542a5
commit f780d78a48
7 changed files with 160 additions and 7 deletions

View File

@ -43,7 +43,7 @@ public extension MastodonClient {
self?.oAuthContinuation = continuation
oauthClient?.renewAccessToken(
withRefreshToken: "refrestoken",
withRefreshToken: refreshToken,
completionHandler: { result in
switch result {
case let .success((credentials, _, _)):

View File

@ -234,6 +234,7 @@
F89D6C4329718092001DA3D4 /* AccentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentsSection.swift; sourceTree = "<group>"; };
F89D6C4529718193001DA3D4 /* ThemeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSection.swift; sourceTree = "<group>"; };
F89D6C49297196FF001DA3D4 /* ImagesViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesViewer.swift; sourceTree = "<group>"; };
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-002.xcdatamodel"; sourceTree = "<group>"; };
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
F8B1E6502973FB7E00EE0D10 /* ToastrService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastrService.swift; sourceTree = "<group>"; };
F8B9B344298D1FCB009CC69C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
@ -1071,10 +1072,11 @@
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */,
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */,
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */,
);
currentVersion = F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */;
currentVersion = F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */;
path = Vernissage.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@ -17,7 +17,7 @@ extension ApplicationSettings {
@NSManaged public var theme: Int32
@NSManaged public var tintColor: Int32
@NSManaged public var avatarShape: Int32
@NSManaged public var lastRefreshTokens: Date
}
extension ApplicationSettings : Identifiable {

View File

@ -56,7 +56,7 @@ public class AuthorizationService {
// Verify address.
_ = try await client.readInstanceInformation()
// Create application (we will get clientId amd clientSecret).
// Create application (we will get clientId and clientSecret).
let oAuthApp = try await client.createApp(
named: AppConstants.oauthApplicationName,
redirectUri: AppConstants.oauthRedirectUri,
@ -76,8 +76,8 @@ public class AuthorizationService {
// Get account information from server.
let account = try await authenticatedClient.verifyCredentials()
// Create account object in database.
let accountData = AccountDataHandler.shared.createAccountDataEntity()
// Get/create account object in database.
let accountData = self.getAccountData(account: account)
accountData.id = account.id
accountData.username = account.username
@ -125,6 +125,44 @@ public class AuthorizationService {
result(accountData)
}
public func refreshAccessTokens() async {
let accounts = AccountDataHandler.shared.getAccountsData()
await withTaskGroup(of: Void.self) { group in
for account in accounts {
group.addTask {
do {
try await self.refreshAccessToken(accountData: account)
} catch {
ErrorService.shared.handle(error, message: "Error during refreshing access token for account '\(account.acct)'.")
}
}
}
}
}
private func refreshAccessToken(accountData: AccountData) async throws {
let client = MastodonClient(baseURL: accountData.serverUrl)
guard let refreshToken = accountData.refreshToken else {
return
}
let oAuthSwiftCredential = try await client.refreshToken(clientId: accountData.clientId,
clientSecret: accountData.clientSecret,
refreshToken: refreshToken)
// Get authenticated client.
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
// Get account information from server.
let account = try await authenticatedClient.verifyCredentials()
try await self.update(account: accountData,
basedOn: account,
accessToken: oAuthSwiftCredential.oauthToken,
refreshToken: oAuthSwiftCredential.oauthRefreshToken)
}
private func refreshCredentials(for accountData: AccountData,
presentationContextProvider: ASWebAuthenticationPresentationContextProviding
) async throws {
@ -191,4 +229,12 @@ public class AuthorizationService {
// Save account data in database and in application state.
CoreDataHandler.shared.save()
}
private func getAccountData(account: Account) -> AccountData {
if let accountFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id) {
return accountFromDb
}
return AccountDataHandler.shared.createAccountDataEntity()
}
}

View File

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Vernissage-001.xcdatamodel</string>
<string>Vernissage-002.xcdatamodel</string>
</dict>
</plist>

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22C65" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AccountData" representedClassName="AccountData" syncable="YES">
<attribute name="accessToken" optional="YES" attributeType="String"/>
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" optional="YES" attributeType="URI"/>
<attribute name="avatarData" optional="YES" attributeType="Binary"/>
<attribute name="clientId" attributeType="String"/>
<attribute name="clientSecret" attributeType="String"/>
<attribute name="clientVapidKey" attributeType="String"/>
<attribute name="createdAt" attributeType="String"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="followersCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="header" optional="YES" attributeType="URI"/>
<attribute name="id" attributeType="String"/>
<attribute name="lastSeenStatusId" optional="YES" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="refreshToken" optional="YES" attributeType="String"/>
<attribute name="serverUrl" attributeType="URI"/>
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="username" attributeType="String"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="StatusData" inverseName="pixelfedAccount" inverseEntity="StatusData"/>
</entity>
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
<attribute name="avatarShape" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
<attribute name="currentAccount" optional="YES" attributeType="String"/>
<attribute name="lastRefreshTokens" attributeType="Date" defaultDateTimeInterval="694256400" usesScalarValueType="NO"/>
<attribute name="theme" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
</entity>
<entity name="AttachmentData" representedClassName="AttachmentData" syncable="YES">
<attribute name="blurhash" optional="YES" attributeType="String"/>
<attribute name="data" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
<attribute name="exifCamera" optional="YES" attributeType="String"/>
<attribute name="exifCreatedDate" optional="YES" attributeType="String"/>
<attribute name="exifExposure" optional="YES" attributeType="String"/>
<attribute name="exifLens" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="metaImageHeight" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="metaImageWidth" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="previewUrl" optional="YES" attributeType="URI"/>
<attribute name="remoteUrl" optional="YES" attributeType="URI"/>
<attribute name="statusId" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="type" attributeType="String"/>
<attribute name="url" attributeType="URI"/>
<relationship name="statusRelation" maxCount="1" deletionRule="Nullify" destinationEntity="StatusData" inverseName="attachmentsRelation" inverseEntity="StatusData"/>
</entity>
<entity name="StatusData" representedClassName="StatusData" syncable="YES">
<attribute name="accountAvatar" optional="YES" attributeType="URI"/>
<attribute name="accountDisplayName" optional="YES" attributeType="String"/>
<attribute name="accountId" attributeType="String"/>
<attribute name="accountUsername" optional="YES" attributeType="String"/>
<attribute name="applicationName" optional="YES" attributeType="String"/>
<attribute name="applicationWebsite" optional="YES" attributeType="URI"/>
<attribute name="bookmarked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="String"/>
<attribute name="favourited" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="favouritesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="inReplyToAccount" optional="YES" attributeType="String"/>
<attribute name="inReplyToId" optional="YES" attributeType="String"/>
<attribute name="muted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="pinned" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="reblogged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="rebloggedAccountAvatar" optional="YES" attributeType="URI"/>
<attribute name="rebloggedAccountDisplayName" optional="YES" attributeType="String"/>
<attribute name="rebloggedAccountId" optional="YES" attributeType="String"/>
<attribute name="rebloggedAccountUsername" optional="YES" attributeType="String"/>
<attribute name="rebloggedStatusId" optional="YES" attributeType="String"/>
<attribute name="reblogsCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="repliesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String"/>
<attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="visibility" attributeType="String"/>
<relationship name="attachmentsRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
<relationship name="pixelfedAccount" maxCount="1" deletionRule="Nullify" destinationEntity="AccountData" inverseName="statuses" inverseEntity="AccountData"/>
</entity>
</model>

View File

@ -58,6 +58,9 @@ struct VernissageApp: App {
// Load user preferences from database.
self.loadUserPreferences()
// Refresh other access tokens.
await self.refreshAccessTokens()
// Verify access token correctness.
let authorizationSession = AuthorizationSession()
await AuthorizationService.shared.verifyAccount(session: authorizationSession) { accountData in
@ -124,6 +127,23 @@ struct VernissageApp: App {
ImagePipeline.shared = pipeline
}
private func refreshAccessTokens() async {
let defaultSettings = ApplicationSettingsHandler.shared.getDefaultSettings()
print(defaultSettings.lastRefreshTokens)
// Run refreshing access tokens once per day.
guard let refreshTokenDate = Calendar.current.date(byAdding: .day, value: 1, to: defaultSettings.lastRefreshTokens), refreshTokenDate < Date.now else {
return
}
// Refresh access tokens.
await AuthorizationService.shared.refreshAccessTokens()
// Update time when refresh tokens has been updated.
defaultSettings.lastRefreshTokens = Date.now
CoreDataHandler.shared.save()
}
}
class AppDelegate: NSObject, UIApplicationDelegate {