dankinsoid / swift-api-client

Comprehensive and modular Swift library for API design.
MIT License
10 stars 0 forks source link

How to paste Bearer token from request? #2

Open WiRight opened 1 day ago

WiRight commented 1 day ago

Hello! Thx for your library!

I'm new in Swift

I've created ApiClient via macros and add two methods - login and refresh. How to pass acessToken from login request into other requests?

ApiClient.swift

import Foundation
import SwiftAPIClient

@API
public struct ApiClient {
    init(baseUrl: BaseURL) {
        client = APIClient(baseURL: baseUrl.url)
            .path("v1")
            .bodyDecoder(ApiDecoder())
            .bodyEncoder(.json)
            .errorDecoder(.decodable(ApiResponse<String>.self))
            .queryEncoder(.urlQuery(arrayEncodingStrategy: .commaSeparator))
            .tokenRefresher { refreshToken, client, _ in
                guard let refreshToken else {
                    throw ApiResponse<String>(
                        status: "error",
                        message: "No refresh token",
                        error: "No refresh token",
                        data: nil
                    )
                }

                let refreshInfo: AuthResponse = try await client("chats", "refresh")
                    .body(["refresh_token": refreshToken])
                    .put()

                return (refreshInfo.token.accessToken, refreshInfo.token.refreshToken, nil)
            }
    }
}

let apiClient = ApiClient(baseUrl: .dev)

public extension ApiClient {
    @Path
    struct Auth {
        init(client: APIClient) {
            self.client = client.auth(enabled: false)
        }

        @POST("/login")
        func login(_ body: LoginRequest) async throws -> AuthResponse {}

        @PUT("/refresh")
        func refresh() async throws -> AuthResponse {
            client.auth(enabled: true)
        }
    }

    @Path
    struct Chats {
        init (client: APIClient) {
            self.client = client.auth(enabled: true)
        }

        // Note: this request required access_token!
        @GET("/")
        func list() async throws -> [Chat] {}
    }
}

ApiResponse.swift

struct ApiResponse<T: Decodable>: Decodable, Error {
    let status: String
    let message: String
    let error: String?
    let data: T?
}
WiRight commented 1 day ago

Update

For now i use next solution

    @Path
    struct Chats {
        init (client: APIClient) {
            self.client = client.auth(enabled: true)
        }

        @GET("/")
        func list() async throws -> [Chat] {
            client.auth(.bearer(token: AuthStore().accessToken)) // <-- here
        }
    }

AuthStore - is a ObservableObject wich stores accessToken via @AppStorage

Is it clear?

WiRight commented 1 day ago

My bad...

Find solution


@API
public struct ApiClient {
    init(baseUrl: BaseURL) {
        client = APIClient(baseURL: baseUrl.url)
            .path("v1")
            .bodyDecoder(ApiDecoder())
            .bodyEncoder(.json)
            .errorDecoder(.decodable(ApiResponse<String>.self))
            .auth(.bearer(token: AuthStore().accessToken)) // <-- Paste auth here
            .queryEncoder(.urlQuery(arrayEncodingStrategy: .commaSeparator))
            // TODO: new error is here
            .tokenRefresher { refreshToken, client, _ in
                guard let refreshToken else {
                    throw ApiResponse<String>(
                        status: "error",
                        message: "No refresh token",
                        error: "No refresh token",
                        data: nil
                    )
                }

                let refreshInfo: AuthResponse = try await client("chats", "refresh")
                    .body(["refresh_token": refreshToken])
                    .put()

                return (refreshInfo.token.accessToken, refreshInfo.token.refreshToken, nil)
            }
    }
}

But i have new trouble - with tokenRefresher getting error

image

If comment - all be ok

image
WiRight commented 1 day ago

Finally done

Creating custom SecureCacheService was solution for me

Leave code here

ApiClient.swift


public struct MKeychainCacheService: SecureCacheService {
    public static var `default` = MKeychainCacheService()

    public func load(for key: SecureCacheServiceKey) throws -> String? {
        UserDefaults.standard.string(forKey: key.value)
    }

    public func load(for key: SecureCacheServiceKey) async throws -> String? {
        UserDefaults.standard.string(forKey: key.value)
    }

    public func save(_ value: String?, for key: SecureCacheServiceKey) async throws {
        UserDefaults.standard.setValue(value, forKey: key.value)
    }

    public func clear() async throws {
        UserDefaults.standard.setValue(nil, forKey: SecureCacheServiceKey.accessToken.value)
        UserDefaults.standard.setValue(nil, forKey: SecureCacheServiceKey.refreshToken.value)
        UserDefaults.standard.setValue(nil, forKey: SecureCacheServiceKey.expiryDate.value)
    }
}

@API
public struct ApiClient {
    init(baseUrl: BaseURL) {
        client = APIClient(baseURL: baseUrl.url)
            .path("v1")
            .bodyDecoder(ApiDecoder())
            .bodyEncoder(.json)
            .errorDecoder(.decodable(ApiResponse<String>.self))
            .queryEncoder(.urlQuery(arrayEncodingStrategy: .commaSeparator))
            .tokenRefresher(
                cacheService: MKeychainCacheService(),
                refresh: { refreshToken, client, _ in
                    guard let refreshToken else {
                        throw ApiResponse<String>(
                            status: "error",
                            message: "No refresh token",
                            error: "No refresh token",
                            data: nil
                        )
                    }

                    let refreshInfo: AuthResponse = try await client("chats", "refresh")
                        .body(["refresh_token": refreshToken])
                        .put()

                    return (refreshInfo.token.accessToken, refreshInfo.token.refreshToken, nil)
                }
            )
    }
}

// Other api @Path here...

May be this issue can be closed, but i'm not sure that this way is clear