Gestion du token pour l'authentification Directus

Bonjour à tous, @mbritto, @Mrt1,

Suite au coatching, j’ai tenté de bosser la gestion des tokens pour l’authentification des requêtes Directus. J’ai choisi de stocker l’accessToken et le refreshToken et de les réinitialiser à vide après un certain temps. Mais j’ai l’impression que mon timer ne se met. jamais en route pour remettre à vide mes tokens. Est-ce que vous voyez un problème sur mon code ?

func login() async throws {
        var loginPath: String
        var json: [String : String]
        
        if refreshToken.isEmpty {
            loginPath = "https://api.pickfungi.com/auth/login"
            json = ["email": "[email protected]", "password": "XXXXX"]
        } else {
            loginPath = "https://api.pickfungi.com/auth/refresh"
            json = ["refresh_token": refreshToken]
        }
        
        guard let loginUrl = URL(string: loginPath) else {
            throw ShowError.URLError
        }
        var urlRequest = URLRequest(url: loginUrl)
        urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        urlRequest.httpMethod = "POST"
        
        let jsonData = try? JSONSerialization.data(withJSONObject: json, options: [])
        urlRequest.httpBody = jsonData
        
        let (data, response) = try await URLSession.shared.data(for: urlRequest)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else { throw ShowError.httpStatusCodeError }
        
        guard let downloadedLoginResponse = try? JSONDecoder().decode(LoginResponse.self, from: data) else {
            throw ShowError.dataError
        }
        
        accessToken = downloadedLoginResponse.data.access_token
        refreshToken = downloadedLoginResponse.data.refresh_token
        
        Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { timer in
            self.accessToken = ""
        }

        Timer.scheduledTimer(withTimeInterval: 20.0, repeats: false) { timer in
            self.refreshToken = ""
        }
    }

En plus de ce problème là, je viens de percuter que je créé un RemoteDataProvider par viewModel.
En gros, View1 appelle ViewModel1 qui utilise une instance d’un objet Manager qui appelle une instance d’un RemoteDataProvider. Et View2 appelle ViewModel2 qui utilise une autre instance de l’objet Manager qui appelle une autre instance d’un RemoteDataProvider. Comme je stocke mon accessToken et mon refreshToken dans mon RemoteDataProvider, je relance une connexion login/mot de passe pour chacune des vues …
Est-ce normal de créer une instance pour chaque vue ? Doit-on remonter le stockage des tokens ailleurs pour que tous les appels puissent en profiter ?

Merci de votre aide !

Pour le token, je te conseille d’éviter les timers. Il vaut mieux que tu stockes ton token, ton token refresh et la date d’expiration dans 3 variables.
Puis, à chaque fois que tu t’apprêtes à lancer une requête tu commences par vérifier la date d’expiration :

  • si elle est dans le futur alors tu peux faire ta requête avec ton token
  • si elle est dans le passé alors tu utilises ton token refresh pour regenerer un nouveau token avant de faire ta requete

Non, il faudrait que tu n’aies qu’une seule instance de ton RemoteDataProvider sinon tu va recréer sans arrêt des tokens alors que les précédents sont encore valides

Hello @mbritto,

J’ai corrigé mon code en définissant une date d’expiration à laquelle j’ajoute 14m et 50sec à chaque login/refresh :

expireDate = Date().addingTimeInterval(890)

Et je teste avant chaque requête nécessitant une authentification si ma date d’expiration est plus vieille que la date système :

if expireDate < Date() {
      try await login()
}

Il me reste plus qu’à régler mes problèmes d’instance :slight_smile:

Quand je lance mon appli et ma première vue, je crée un ViewModel :

@StateObject var viewModel = ViewModel()

Et dans mon ViewModel, je crée un manager :

private var manager = Manager()

Dans mon manager, je crée également un remoteDataProvider :

var request = RemoteDataProvider()

Si je navigue vers un autre écran, comment puis-je accéder à mon manager crée dans ma première vue ? Aujourd’hui, j’en recrée un …

Merci pour ton aide !!

1 « J'aime »

Dans l’idéal, il te faudrait un objet qui chapeaute ta navigation, une sorte de routeur qui crée chacun des écrans et leur fait passer ce dont ils ont besoin. C’est assez complexe à mettre en place et c’est justement l’objet du cours avancé d’architecture et de navigation que je viens de sortir en Dart/Flutter.
Il faudra faire une version iOS/Swift de ce cours, mais avec la WWDC qui a lieu dans 1 mois et demi ça fait un peu tard pour sortir un cours Swift/SwiftUI avec un risque de changements importants.

La solution la plus rapide et la plus simple pour toi serait le singleton pour ton RemoteDataProvider

Je vais laisser en l’état pour l’instant. Mon appli n’est pas prête de sortir, il me reste beaucoup de boulot. Et je verrai par la suite si tu proposes un cours dans ce sens :slight_smile:

1 « J'aime »

Salut @Tazooou,

Tu peux declarer ton RemoteDataProvider() dans ton App et la passer a une vue contenant toutes les autres : « MainScreen » par example (ou tout autre vue contenant toutes les vues où tu en a besoin, comme a expliqué @mbritto).

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            MainScreen(dataProvider: RemoteDataProvider())
        }
    }
}
struct MainScreen<T>: View where T: RemoteDataProviderProtocol {
    
    @ObservedObject private var dataProvider: T
    
    init(dataProvider: T) {
        self.dataProvider = dataProvider
    }
    
    var body: some View {
        VStack {
            if dataProvider.isLoggedIn {
                HomeScreen(dataProvider: dataProvider)
            } else {
                LoginScreen(dataProvider: dataProvider)
            }
        }
    }
}

Puis dans tes écrans descendants, récupérer le DataProvider, en le passant par le onAppear, et ajouter son propre ViewModel :

struct HomeScreen<T>: View where T: RemoteDataProviderProtocol {
    
    @ObservedObject var dataProvider: T
    @StateObject private var viewModel = HomeViewModel()
    
    var body: some View {
        Text("Home Screen")
            .onAppear {
                self.viewModel.dataProvider = self.dataProvider as? RemoteDataProviderProtocol
            }
    }
}

Le protocole doit conformer à ObservableObject :

protocol RemoteDataProviderProtocol: ObservableObject {
    var isLoggedIn: String { get }
    func login() -> Void
}

Happy Coding ! :wink:

1 « J'aime »

Merci @cedric06nice. Je vais regarder ça de près :slight_smile:

1 « J'aime »