ইউজার অথেনটিকেশন – ল্যারভেল + সুইফট

মোবাইল অ্যাপ ডেভেলপমেন্টের সময় সাধারণত আমরা ব্যাকেন্ড থেকে প্রাপ্ত রেস্ট এপিআই এর মাধ্যমে বিভিন্ন কাজ করে থাকি। এর মধ্যে গুরুত্বপূর্ণ একটা অংশ হচ্ছে ইউজার অথেনটিকেশন।

ইউজার অথেনটিকেশনের জন্য ব্যাকেন্ডে ইউজার ম্যানেজমেন্ট সিস্টেম থাকতে হবে। যেখানে ইউজার রেজিট্রেশন, লগিন, লগআউট ইত্যাদি করতে পারবে। ল্যারাভেলে এই ইউজার অথেনটিকেশন এবং সিকিউরিটি খুব সহজে ম্যানেজ করা যায়। ল্যারাভেলে REST API তৈরি এবং অথেনটিকেশন বিস্তারিত দেখেছি কিভাবে রেস্ট এপিআই তৈরি, ইউজার অথেনটিকেশন করা যায়। এই লেখাতে শুধু মাত্র ইউজার অথেনটিকেশন পার্ট দেখব।

ব্যাকেন্ড পার্ট – ইউজার এপিআই তৈরি

ল্যারাভেল হার্ড ব্যবহার করে একটা ল্যারাভেল প্রজেক্ট তৈরি করে নিব। যেমন আমি নাম দিয়েছি UserAuth। হার্ড ব্যবহার করলে বাড়তি কিছু সুবিধা পাওয়া যায়। এর মধ্যে একটা হচ্ছে কাস্টম টেস্ট ডোমেইন। যেমন UserAuth নামে যদি প্রজেক্ট তৈরি করি, তাহলে http://userauth.test এড্রেসে প্রজেক্টটি দেখা যাবে। এটি যদিও গুরুত্বপূর্ণ নয়।

আমরা যেহেতু API নিয়ে কাজ করব, তাই API ইন্সটল করে নিবঃ

 php artisan install:api

যেহেতু ডিফল্ট ভাবেই User মডেল তৈরি থাকে তাই আমাদের আলাদা করে মডেল তৈরি করতে হবে না। তবে এপিআই নিয়ে কাজ করার জন্য HasApiTokens প্রপার্টি যুক্ত করতে হবে। । তার জন্য use HasFactory, Notifiable; পরিবর্তন করে লিখবে হবে use HasApiTokens, HasFactory, Notifiable;। এবং use Laravel\Sanctum\HasApiTokens; ইনক্লুড করে নিতে হবে।

এছাড়া একটা কন্ট্রোলার তৈরি করে নিবঃ

php artisan make:controller ApiAuthController  

এই ফাইলে register, login এবং logout ফাংশন যোগ করবঃ

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class ApiAuthController extends Controller
{
    public function register(Request $request){
        $data = $request->validate([
          'name'=> 'required|max:255',
          'email'=> 'required|email|unique:users',
          'password' => 'required'
        ]);
      $user =  User::create($data);
      $token = $user->createToken($request->name);
      return [
          'user' => $user,
          'token' => $token->plainTextToken
      ];
      }
      public function login(Request $request){
        $request->validate([
            'email' => 'required|email',
            'password' => 'required|string',
        ]);
          $user = User::where('email', $request->email) -> first();

          if (!$user || !Hash::check($request->password, $user->password)) {
            return response()->json([
                'message' => 'The provided credentials are incorrect.'
            ], 401);
          }
          $token = $user->createToken($user->name);
          return [
              'user' => $user,
              'token' => $token->plainTextToken
          ];
    }

    public function logout(Request $request){
        $request->user()->tokens()->delete();
         return ['message' => 'User logged out successfully'];
    }
}

এপিআই রুটস

Route::post('/register', [ApiAuthController::class, 'register']);
Route::post('/login', [ApiAuthController::class, 'login']);
Route::post('/logout', [ApiAuthController::class, 'logout'])->middleware('auth:sanctum');;

পোস্টম্যান ব্যবহার করে এখন চাইলে ইউজার রেজিট্রেশন, লগিন এবং লগআউট করা যাবে। এগুলো সব কিছু নিয়ে বিস্তারিত ল্যারাভেলে REST API তৈরি এবং অথেনটিকেশন লেখায় লিখেছি। তাই এখানে আর বিস্তারিত লিখিনি।

SwiftUI অ্যাপ

একটা iOS প্রজেক্ট তৈরি করব। ইন্টারফেস হিসেবে SwiftUI সিলেক্ট করব।

আইওএস অ্যাপে ননসিকিউর ওয়েব সাইটে পোস্ট রিকোয়েস্ট করা যায় না। লোকাল ওয়েব সাইটে যেহেতু SSL নেই, তাই এক্সেসপশন যোগ করতে হবে। info.plist এই এক্সপেশন যোগ করা যাবে। বুঝার সুবিধার্থে সম্পূর্ন info.plist এখানে দিয়ে দিলাম।

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <false/>
  <key>NSExceptionDomains</key>
  <dict>
    <key>userauth.test</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>

</dict>
</plist>

ইজার রেজিস্ট্রেশনঃ

ইউজার রেজিস্ট্রেশনের জন্য আমাদের তিনটা ফিল্ড লাগবে, name, email, password।

TextField("Name", text: $name)
TextField("Email", text: $email)
TextField("Password", text: $password )

আমাদের তিনটা ভ্যারিয়েবলও লাগবেঃ

    @State var name = "Ali"
    @State var email = "[email protected]"
    @State var password = "12345"

আমরা যখন ইউজার রেজিস্ট্রেশন বা লগিন করি, তখন রেসপন্স এমন পাইঃ

{
    "user": {
        "id": 1,
        "name": "ali",
        "email": "[email protected]",
        "email_verified_at": null,
        "created_at": "2024-09-05T13:32:08.000000Z",
        "updated_at": "2024-09-05T13:32:08.000000Z"
    },
    "token": "27|sq2YRcp20NSvJLT5NOKSdsAwbAY0lAMErqLccyNa29f460aa"
}

এই রেসপন্সের উপর ভিত্তি করে স্ট্রাকচার তৈরি করে নিব:

struct UserResponse: Codable {
    let user: User
    let token: String
}

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

আমাদের সব গুলো ডেটার দরকার নেই, তাই সব গুলো ডেটা ডিকোড করব না। শুধু id, name , token এবং ইমেইল ডিকোড করব।

এছাড়া যখন কোন এরর আসবে, তখন আমরা এমন রেসপন্স পাবোঃ

{
    "message": "The email has already been taken.",
    "errors": {
        "email": [
            "The email has already been taken."
        ]
    }
}

এই ক্ষেত্রেও আমরা সব গুলো এরর কোড ডিকোড না করে শুধু মূল message টা ডিকোড করব। তাই আরেকটা স্ট্যরাকচার তৈরি করব এমনঃ

struct ErrorResponse: Codable {
    let message: String
}

রেজিস্ট্রেশন ফাংশনঃ

    func register() async  {
        guard let url = URL(string: "http://userauth.test/api/register") else { return }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        let loginDetails = [ "name": name, "email": email, "password": password]
        
        request.httpBody = try? JSONSerialization.data(withJSONObject: loginDetails, options: [])
        
        do {
            // Perform the request
            let (responseData, response) = try await URLSession.shared.data(for: request)
            // Check the response status
            guard let httpResponse = response as? HTTPURLResponse else {
                throw URLError(.unknown)
            }
            
            switch httpResponse.statusCode {
            case 200:
                // Decode the response data
                userResponse = try JSONDecoder().decode(UserResponse.self, from: responseData)
            default:
                // decode error response
                let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: responseData)
                errorMessage = errorResponse.message
            }
        } catch{
            print("Error:  \(error.localizedDescription)")
        }
    }

সুইফটে সিম্পল GET রিকোয়েস্ট অনেক সহজ। কিন্তু POST রিকোয়েস্ট একটু কমপ্লিকেটেড। কারণ এই ক্ষেত্রে হেডার ভ্যালু, রিকোয়েস্ট বডি ইত্যাদি যোগ করতে হয়। যখন আমরা সাকসেসফুলি ইউজার রেজিস্ট্রেশন করতে পারবো, তার ডেটা ডিকোড করছি। অ্যাপে ইউজার নেম এবং ইমেইল দেখিয়েছি।

import SwiftUI

struct ContentView: View {
    
    @State var name = "Ali"
    @State var email = "[email protected]"
    @State var password = "12345"
    
    @State private var errorMessage: String?
    
    @State private var userResponse: UserResponse?
    
    
    var body: some View {
        VStack(spacing: 20){
            
            if let userResponse = userResponse {
                Text("User Name: \(userResponse.user.name)")
                Text("User Email: \(userResponse.user.email)")
                
            } else {
                TextField("Name", text: $name)
                TextField("Email", text: $email)
                TextField("Password", text: $password )
                
                Button("Register") {
                    Task{
                        await register()
                    }
                }
            }

            if let errorMessage = errorMessage {
                Text("Error: \(errorMessage)")
            }
        }
        .textFieldStyle(.roundedBorder)
        .padding()
    }
    
    func register() async  {
        guard let url = URL(string: "http://userauth.test/api/register") else { return }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        let loginDetails = [ "name": name, "email": email, "password": password]
        
        request.httpBody = try? JSONSerialization.data(withJSONObject: loginDetails, options: [])
        
        do {
            // Perform the request
            let (responseData, response) = try await URLSession.shared.data(for: request)
            // Check the response status
            guard let httpResponse = response as? HTTPURLResponse else {
                throw URLError(.unknown)
            }
            
            switch httpResponse.statusCode {
            case 200:
                // Decode the response data
                userResponse = try JSONDecoder().decode(UserResponse.self, from: responseData)
            default:
                // decode error response
                let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: responseData)
                errorMessage = errorResponse.message
            }
        } catch{
            print("Error:  \(error.localizedDescription)")
        }
    }
}
#Preview {
    ContentView()
}


struct UserResponse: Codable {
    let user: User
    let token: String
}

struct User: Codable {
    let id: Int
    let name: String
    let email: String

}

struct ErrorResponse: Codable {
    let message: String
}

অ্যাপ রান করলে ইউজার রেজিস্ট্রেশন ফরম দেখতে পাবো। বার বার জেনো টাইপ করতে না হয়, তাই ভ্যারিয়েবল তৈরির সময় এই ডেটা গুলো ইনপুট দিয়ে রেখেছি। এছাড়া পাসওয়ার্ড দেখার সুবিধার্থে TextField এ রেখেছি। প্রোডাকশন অ্যাপে SecureField ব্যবহার করতে পারেন।

এখন যদি রেজিস্টারে ক্লিক করি তাহলে ইউজার রেজিস্টার হবে। এবং আমাদের ইউজারের ডেটা দেখাবে।

একই ভাবে আমরা লগিন এবং লগআউট ফাংশনালিটি যোগ করতে পারি। সব গুলো ফাংশনালিটি সহ সম্পূর্ন কোড এক সাথেঃ

import SwiftUI

struct ContentView: View {
    
    @State var name = "Ali"
    @State var email = "[email protected]"
    @State var password = "12345"
    @State private var errorMessage: String?
    @State private var userResponse: UserResponse?
    @State var shouldPresentSheet = false
    
    var body: some View {
        VStack(spacing: 20){
            if let userResponse = userResponse {
                Text("User Name: \(userResponse.user.name)")
                Text("User Email: \(userResponse.user.email)")
                Button("Logout") {
                    Task{
                        await logout()
                    }
                }
            } else {
                TextField("Email", text: $email)
                TextField("Password", text: $password )
                Button("Login") {
                    Task{
                        await login()
                    }
                }
                
                // User Registration Button and Form
                Button("Create an account") {
                    shouldPresentSheet.toggle()
                }
                .sheet(isPresented: $shouldPresentSheet) {
                    print("Sheet dismissed!")
                } content: {
                    
                    VStack{
                        TextField("Name", text: $name)
                        TextField("Email", text: $email)
                        TextField("Password", text: $password )
                        Button("Register") {
                            Task{
                                await register()
                            }
                        }
                        
                        if let errorMessage = errorMessage {
                            Text("Error: \(errorMessage)")
                        }
                        
                    }.padding()
                        .textFieldStyle(.roundedBorder)
                    
                }
            }
            if let errorMessage = errorMessage {
                Text("Error: \(errorMessage)")
            }
        }
        .textFieldStyle(.roundedBorder)
        .padding()
    }
    
    
    func register() async  {
        guard let url = URL(string: "http://userauth.test/api/register") else { return }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        let loginDetails = [ "name": name, "email": email, "password": password]
        
        request.httpBody = try? JSONSerialization.data(withJSONObject: loginDetails, options: [])
        
        do {
            // Perform the request
            let (responseData, response) = try await URLSession.shared.data(for: request)
            // Check the response status
            guard let httpResponse = response as? HTTPURLResponse else {
                throw URLError(.unknown)
            }
            switch httpResponse.statusCode {
            case 200:
                // Decode the response data
                userResponse = try JSONDecoder().decode(UserResponse.self, from: responseData)
            default:
                // decode error response
                let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: responseData)
                errorMessage = errorResponse.message
            }
            
        } catch{
            print("Error:  \(error.localizedDescription)")
        }
    }
    
    
    func login() async  {
        guard let url = URL(string: "http://userauth.test/api/login") else { return }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        let loginDetails = ["email": email, "password": password]
        
        request.httpBody = try? JSONEncoder().encode(loginDetails)
        
        
        do {
            // Perform the request
            let (responseData, response) = try await URLSession.shared.data(for: request)
            
            
            // Check the response status
            guard let httpResponse = response as? HTTPURLResponse else {
                throw URLError(.unknown)
            }
            
            switch httpResponse.statusCode {
            case 200:
                self.shouldPresentSheet = false
                // Decode the response data
                userResponse = try JSONDecoder().decode(UserResponse.self, from: responseData)
                
            default:
                // decode error response
                let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: responseData)
                errorMessage = errorResponse.message
            }
        } catch{
            print("Error:  \(error.localizedDescription)")
        }
    }
    
    func logout() async  {
        guard let url = URL(string: "http://userauth.test/api/logout") else { return }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        // Set the Authorization header with the Bearer token
        request.setValue("Bearer \(userResponse?.token ?? "")", forHTTPHeaderField: "Authorization")
        
        
        do {
            // Perform the request
            let (responseData, response) = try await URLSession.shared.data(for: request)
            // Check the response status
            guard let httpResponse = response as? HTTPURLResponse else {
                throw URLError(.unknown)
            }
            
            switch httpResponse.statusCode {
            case 200:
                // clearing the local user data
                self.userResponse = nil
                if let responseString = String(data: responseData, encoding: .utf8) {
                    print("Response as String: \(responseString)")
                }
                
            default:
                // decode error response
                let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: responseData)
                errorMessage = errorResponse.message
            }
            
        } catch{
            print("Error:  \(error.localizedDescription)")
        }
    }
    
}

#Preview {
    ContentView()
}


struct UserResponse: Codable {
    let user: User
    let token: String
}

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

struct ErrorResponse: Codable {
    let message: String
}

যদিও এভাবে লেখা গুড প্র্যাকটিস না। আলাদা আলাদা ফাইলে লিখলে ভালো হতো। এছাড়া আমরা চাইলে নেটওয়ার্ক কলকে জেনালাইজ করতে পারি। যেন কোড রিপিট কম হয়। মূল কনসেফট যেন বুঝা যায়, তাই এভাবে লিখেছি। কোড গুলো পাওয়া যাবে গিটহাবে। ব্যাকেন্ড এবং আইওএস, দুইটা এক সাথে পাওয়া যাবে। এই সম্পর্কিত আরেকটা লেখাঃ

Leave a Reply