Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions AppScript/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,19 @@ fileprivate extension Target {
fileprivate extension Package {

static var dependencies: [Dependency] {
externalDependencies + serviceDependencies + storeDependencies
[.package(name: "Common", path: "../Common")]
+ externalDependencies
+ serviceDependencies
+ storeDependencies
}

static var externalDependencies: [Dependency] {
[.package(name: "Swinject", url: "https://github.com/Swinject/Swinject.git", from: "2.7.1")]
}

static var serviceDependencies: [Dependency] {
[.package(name: "StackexchangeNetworkService", path: "../Services/StackexchangeNetworkService")]
[.package(name: "StackexchangeNetworkService", path: "../Services/StackexchangeNetworkService"),
.package(name: "AuthManager", path: "../Services/AuthManager")]
}

static var storeDependencies: [Dependency] {
Expand Down
15 changes: 15 additions & 0 deletions AppScript/Sources/AppScript/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import Swinject
import StackexchangeNetworkService
import AuthManager
import class Common.StackexchangeAuthConfigurations
import class PageStore.PageDataManager

// MARK: - Services Assembly
Expand All @@ -19,6 +21,10 @@ final class ServicesAssembly: Assembly {
StackexchangeNetworkService()
}.inObjectScope(.weak)

container.register(AuthManager.self) { resolver in
AuthManager(configurations: try! StackexchangeAuthConfigurations.load())
}.inObjectScope(.weak)

container.register(PageDataManager.self) { resolver in
PageDataManager(service: resolver.resolve(StackexchangeNetworkService.self)!)
}.inObjectScope(.weak)
Expand All @@ -39,3 +45,12 @@ public struct ServicesAssembler {
])
}()
}

// MARK: - Extensions

public extension AuthManager {

static func appearance() -> Self {
ServicesAssembler.shared.resolve(Self.self)!
}
}
13 changes: 13 additions & 0 deletions Application/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>com.ephedra-software.StackOv</string>
<key>CFBundleURLSchemes</key>
<array>
<string>stackov</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationCategoryType</key>
Expand Down
7 changes: 5 additions & 2 deletions Common/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ let package = Package(
],
dependencies: Package.dependencies,
targets: [
.target(name: "Common", dependencies: Target.dependencies),
.target(name: "Common",
dependencies: Target.dependencies,
resources: [.process("Resources/StackexchangeAuthConfigurations.json")]),
.testTarget(name: "CommonTests", dependencies: ["Common"])
]
)
Expand All @@ -27,7 +29,7 @@ fileprivate extension Package {
fileprivate extension Target {

static var dependencies: [Dependency] {
var packages: [Dependency] = ["Introspect"]
var packages: [Dependency] = ["URLBuilder", "Introspect"]
if Package.firebaseIsEnable {
packages.append(.product(name: "FirebaseCrashlytics", package: "Firebase"))
}
Expand All @@ -43,6 +45,7 @@ fileprivate extension Package {

static var externalDependencies: [Dependency] {
var packages: [Dependency] = [
.package(name: "URLBuilder", url: "https://github.com/ephedra-software/URLBuilder.git", from: "1.0.3"),
.package(name: "Introspect", url: "https://github.com/Puasonych/SwiftUI-Introspect.git", .branch("develop"))
]
if Package.firebaseIsEnable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// StackexchangeAuthConfigurations.swift
// StackOv (Common module)
//
// Created by Erik Basargin
// Copyright © 2021 Erik Basargin. All rights reserved.
//

import Foundation
import URLBuilder

public final class StackexchangeAuthConfigurations: Codable {

// MARK: - Nested types

public enum CodingKeys: String, CodingKey {
case clientId = "client_id"
case scope
case redirectUri = "redirect_uri"
}

public enum Errors: Error {
case confFileNotFound
}

// MARK: - Public properties

public let clientId: Int
public let scope: String
public let redirectUri: URL

// MARK: - Public methods

public static func load() throws -> Self {
guard let filePath = Bundle.module.url(forResource: "\(Self.self)", withExtension: "json") else {
throw Errors.confFileNotFound
}
let data = try Data(contentsOf: filePath)
let object = try JSONDecoder().decode(Self.self, from: data)
return object
}

public func getAuthUri(host: String) -> URL {
URLBuilder.scheme(.https)
.host(custom: host)
.path(custom: "/oauth/dialog")
.query(items: [
(CodingKeys.clientId.rawValue, "\(clientId)"),
(CodingKeys.scope.rawValue, scope),
(CodingKeys.redirectUri.rawValue, redirectUri.absoluteString)
])
.url!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"client_id": 18018,
"scope": "read_inbox,write_access,private_info,no_expiry",
"redirect_uri": "stackov://stackexchange.com/oauth/login_success"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AuthManager"
BuildableName = "AuthManager"
BlueprintName = "AuthManager"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AuthManagerTests"
BuildableName = "AuthManagerTests"
BlueprintName = "AuthManagerTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AuthManager"
BuildableName = "AuthManager"
BlueprintName = "AuthManager"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
20 changes: 20 additions & 0 deletions Services/AuthManager/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "AuthManager",
platforms: [.iOS(.v14), .macOS(.v10_15)],
products: [
.library(name: "AuthManager", targets: ["AuthManager"])
],
dependencies: [
.package(path: "../Common"),
.package(path: "../Network")
],
targets: [
.target(name: "AuthManager", dependencies: ["Common", "Network"]),
.testTarget(name: "AuthManagerTests", dependencies: ["AuthManager"])
]
)
3 changes: 3 additions & 0 deletions Services/AuthManager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AuthManager

A description of this package.
100 changes: 100 additions & 0 deletions Services/AuthManager/Sources/AuthManager/AuthManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// AuthManager.swift
// StackOv (AuthManager module)
//
// Created by Erik Basargin
// Copyright © 2021 Erik Basargin. All rights reserved.
//

import Foundation
import AuthenticationServices
import Common
import Combine

#if canImport(AppKit) && !targetEnvironment(macCatalyst)
public typealias ASPresentationAnchor = NSWindow
#else
public typealias ASPresentationAnchor = UIWindow
#endif

final public class AuthManager: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding {

// MARK: - Nested types

enum Constants {
static let accessTokenKey = "access_token"
}

public enum Errors: Error {
case unexpectedError
case invalidAccessTokenUrl
case accessTokenNotFound
}

// MARK: - Properties

let configurations: StackexchangeAuthConfigurations
var authProcess: AnyCancellable?

// MARK: - Initialization and deinitialization

public init(configurations: StackexchangeAuthConfigurations) {
self.configurations = configurations
}

// MARK: - Public methods

public func singIn() {
authProcess?.cancel()
authProcess = Future<URL, Error> { [unowned self] completion in
let authUrl = configurations.getAuthUri(host: "stackoverflow.com")

let authSession = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: nil) { url, error in
if let error = error {
completion(.failure(error))
} else if let url = url {
completion(.success(url))
} else {
completion(.failure(Errors.unexpectedError))
}
}
authSession.presentationContextProvider = self
authSession.prefersEphemeralWebBrowserSession = true
authSession.start()
}
.tryMap { [unowned self] url in
return try extractToken(fromUrl: url)
}
.sink { result in
if case let .failure(error) = result {
// show banner with error
print(error)
}
} receiveValue: { accessToken in
print(accessToken)
// keep access_token in the keychain
}
}

// MARK: - Internal methods

func extractToken(fromUrl url: URL) throws -> String {
guard url.absoluteString.hasPrefix(configurations.redirectUri.absoluteString),
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
throw Errors.invalidAccessTokenUrl
}
components.query = components.fragment
components.fragment = nil
if let accessToken = components.queryItems?.first(where: { $0.name == Constants.accessTokenKey })?.value {
return accessToken
}
throw Errors.accessTokenNotFound
}

// MARK: - ASWebAuthenticationPresentationContextProviding

@objc
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return ASPresentationAnchor()
}
}
24 changes: 24 additions & 0 deletions Services/AuthManager/Tests/AuthManagerTests/AuthManagerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import XCTest
import Common
@testable import AuthManager

final class AuthManagerTests: XCTestCase {

func testAccessTokenExtractor() {
do {
let config = try StackexchangeAuthConfigurations.load()
let manager = AuthManager(configurations: config)
let tokenValue = "{access_token_value}"
let url: URL = {
var components = URLComponents(url: config.redirectUri, resolvingAgainstBaseURL: true)!
components.fragment = "access_token=\(tokenValue)"
return components.url!
}()

let extractedToken = try manager.extractToken(fromUrl: url)
XCTAssertEqual(tokenValue, extractedToken)
} catch {
XCTFail("Preinstall error: \(error)")
}
}
}
Loading