Skip to content
Merged
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
15 changes: 7 additions & 8 deletions .github/workflows/main-safety.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
# This workflow will build a Swift project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift

name: Main safety

name: Main safety
on:
pull_request:
branches: [ "main" ]

jobs:
build_and_test:

runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- name: Setup Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: "5.10.0"
swift-version: "6.2.0"

- name: Get swift version
run: swift --version

- name: Build
run: swift build -v

Expand Down
19 changes: 5 additions & 14 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 69 additions & 17 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,48 @@ let package = Package(
.macOS(.v14),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "Coffee-Kit",
targets: ["Coffee-Kit"]
name: "FoundationKit",
targets: ["FoundationKit"]
),
.library(
name: "Authentication-Kit",
targets: ["Authentication-Kit"]
name: "AuthenticationKit",
targets: ["AuthenticationKit"]
),
.library(
name: "ImageKit",
targets: ["ImageKit"]
),
.library(
name: "OrderKit",
targets: ["OrderKit"]
),
.library(
name: "ProductKit",
targets: ["ProductKit"]
)
],
dependencies: [
.package(
url: "https://github.com/perrystreetsoftware/Harmonize.git",
from: "0.1.0"
from: "0.9.0"
)
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "Coffee-Kit",
name: "FoundationKit",
dependencies: [],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.defaultIsolation(MainActor.self)
]
),
.target(
name: "AuthenticationKit",
dependencies: [
"Authentication-Kit"
"FoundationKit"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
Expand All @@ -41,24 +60,57 @@ let package = Package(
.defaultIsolation(MainActor.self)
]
),
.testTarget(
name: "Coffee-KitTests",
.target(
name: "ProductKit",
dependencies: [
"Coffee-Kit",
.product(name: "Harmonize", package: "Harmonize"),
"FoundationKit",
"AuthenticationKit"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.defaultIsolation(MainActor.self)
]
),

.target(
name: "Authentication-Kit",
name: "OrderKit",
dependencies: [
"FoundationKit",
"AuthenticationKit",
"ProductKit"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.defaultIsolation(MainActor.self)
]
)
),
.target(
name: "ImageKit",
dependencies: [
"FoundationKit",
"AuthenticationKit",
"ProductKit"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.defaultIsolation(MainActor.self)
]
),
.testTarget(
name: "Coffee-KitTests",
dependencies: [
"FoundationKit",
"AuthenticationKit",
"ProductKit",
"OrderKit",
"ImageKit",
.product(name: "Harmonize", package: "Harmonize"),
]
),
]
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import Foundation
import FoundationKit


public actor AutenticationManager {
Expand All @@ -9,15 +9,12 @@ public actor AutenticationManager {
private let accessTokenService = "CoffeeLover.AccessToken"
private let refreshTokenService = "CoffeeLover.RefreshToken"
private let userService = "CoffeeLover.User"

private let baseURL: URL

// ARCHITECTURE TRICK: Hier speichern wir den aktuell laufenden Refresh-Call
private var refreshTask: Task<String, Error>?

public init(keychain: KeychainService, baseURL: URL) {
public init(keychain: KeychainService, databaseAPI: DatabaseAPI) {
self.keychain = keychain
self.baseURL = baseURL
self.baseURL = databaseAPI.baseURL / "authentication"
}

public func getValidAccessToken() async throws -> String {
Expand All @@ -26,14 +23,13 @@ public actor AutenticationManager {
return try await refreshSession()
}

// JWT Validierung ohne try!
do {
let expired = try await JWTValidator.isExpired(token: currentToken)
if !expired {
return currentToken
}
} catch {
print("⚠️ Token validation failed: \(error). Attempting refresh...")
print("Token validation failed: \(error). Attempting refresh...")
}

return try await refreshSession()
Expand Down Expand Up @@ -72,7 +68,7 @@ public actor AutenticationManager {
let userDecoder = JSONDecoder()
return try userDecoder.decode(User.self, from: data)
} catch {
print("ℹ️ No user data found in keychain.")
print("No user data found in keychain.")
return nil
}
}
Expand All @@ -83,9 +79,9 @@ public actor AutenticationManager {
try await keychain.delete(account: account, service: accessTokenService)
try await keychain.delete(account: account, service: refreshTokenService)
try await keychain.delete(account: account, service: userService)
print("👤 User logged out and tokens/data cleared.")
print("User logged out and tokens/data cleared.")
} catch {
print("⚠️ Error during logout: \(error)")
print("Error during logout: \(error)")
}
}

Expand All @@ -105,24 +101,24 @@ public actor AutenticationManager {
do {
_ = try await keychain.read(account: account, service: refreshTokenService)
} catch {
print("ℹ️ No session active (Keychain error: \(error)). Skipping authentication header for \(request.url?.absoluteString ?? "unknown URL").")
print("No session active (Keychain error: \(error)). Skipping authentication header for \(request.url?.absoluteString ?? "unknown URL").")
return
}

do {
let token = try await getValidAccessToken()
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
print("Added Authorization header to \(request.url?.absoluteString ?? "unknown URL").")
print("Added Authorization header to \(request.url?.absoluteString ?? "unknown URL").")
} catch {
print("⚠️ Authentication failed for \(request.url?.absoluteString ?? "unknown URL"): \(error)")
print("Authentication failed for \(request.url?.absoluteString ?? "unknown URL"): \(error)")
}
}

/// Holt einen neuen AccessToken, schützt aber vor parallelen Aufrufen!
private func refreshSession() async throws -> String {

if let existingTask = refreshTask {
print("Refresh already in progress. Attaching to the existing task...")
print("Refresh already in progress. Attaching to the existing task...")
return try await existingTask.value
}

Expand All @@ -143,7 +139,7 @@ public actor AutenticationManager {
request.httpBody = try? JSONEncoder().encode(["refreshToken": refreshToken])

do {
print("🚀 Sending refresh call to backend...")
print("Sending refresh call to backend...")
let (data, response) = try await URLSession.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
Expand All @@ -158,7 +154,7 @@ public actor AutenticationManager {

try await storeTokens(accessToken: tokenResponse.accessToken, refreshToken: tokenResponse.refreshToken)

print("Refresh successful! New Access Token stored.")
print("Refresh successful! New Access Token stored.")
return tokenResponse.accessToken

} catch let error as URLError {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

import Foundation
import FoundationKit


public enum CredentialDenyReason {
Expand Down Expand Up @@ -52,11 +53,10 @@ public final class AuthenticationBuilder {
@ObservationIgnored private let session = URLSession.shared

public init(
authManager: AutenticationManager,
baseURL: URL
webserviceProvider: WebserviceProvider
) {
self.authManager = authManager
self.baseURL = baseURL
self.authManager = webserviceProvider.autheticationManager
self.baseURL = webserviceProvider.databaseAPI.baseURL / "authentication"
}

public var isValidEmail: CredentialState {
Expand Down
21 changes: 21 additions & 0 deletions Sources/AuthenticationKit/WebserviceProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Webservice.swift
// Coffee Lover
//
// Created by Christoph Rohde on 20.10.24.
//

import Foundation

public struct WebserviceProvider {
public let databaseAPI: DatabaseAPI
public let autheticationManager: AutenticationManager

public init(
inMode databaseAPI: consuming DatabaseAPI,
autheticationManager: AutenticationManager
) {
self.databaseAPI = databaseAPI
self.autheticationManager = autheticationManager
}
}
2 changes: 0 additions & 2 deletions Sources/Coffee-Kit/Coffee_Kit.swift

This file was deleted.

Loading
Loading