diff --git a/Cargo.toml b/Cargo.toml index 94f1ccd..b239074 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "tauri-plugin-auth" version = "0.1.0" authors = [ "Rizky Denianto" ] -description = "" +description = "Program sederhana untuk interaksi dengan autentikasi aplikasi tauri di berbagai sistem operasi" edition = "2024" rust-version = "1.98.0" exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"] @@ -18,8 +18,8 @@ strip = "symbols" debug = false [package.metadata.platforms.support] -windows = { level = "full" } -linux = { level = "full" } +windows = { level = "none" } +linux = { level = "none" } macos = { level = "none" } android = { level = "full" } ios = { level = "none" } diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..e42aa84 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.plugin.auth" + compileSdk = 36 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) + + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") + implementation("com.google.android.libraries.identity.googleid:googleid:") +} diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..d7782a4 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,31 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + google() + } + resolutionStrategy { + eachPlugin { + switch (requested.id.id) { + case "com.android.library": + useVersion("8.0.2") + break + case "org.jetbrains.kotlin.android": + useVersion("1.8.20") + break + } + } + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + google() + + } +} + +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/src/main/java/AuthPlugin.kt b/android/src/main/java/AuthPlugin.kt new file mode 100644 index 0000000..3a988f5 --- /dev/null +++ b/android/src/main/java/AuthPlugin.kt @@ -0,0 +1,157 @@ +package com.plugin.auth + +import android.app.Activity +import android.content.Context +import android.util.Base64 +import androidx.credentials.ClearCredentialStateRequest +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.exceptions.ClearCredentialException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException +import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.security.SecureRandom + + +@InvokeArg +class KirimArgs { + var idKlien: String? = null +} + +@TauriPlugin +class BluclasPlugin(private val activity: Activity) : Plugin(activity) { + private val credentialManager by lazy { CredentialManager.create(activity) } + private val scope = CoroutineScope(Dispatchers.Main) + private val idKlien: String? = null + + @Command + fun masuk(invoke: Invoke) { + val args = invoke.parseArgs(KirimArgs::class.java) + idKlien = args.idKlien + if (idKlien.isNullOrEmpty()) { + invoke.reject("ID Klien belum disediakan") + return + } + + val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(true) + .setServerClientId(idKlien) + .setNonce(generateSecureRandomNonce()) + .build() + + val request = GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + + scope.launch { + try { + val result = credentialManager.getCredential( + request = request, + context = activity + ) + handleSignInResult(result, invoke) + } catch (e: NoCredentialException) { + if (args.filterByAuthorizedAccounts) { + signInWithAllAccounts(idKlien, invoke) + } else { + invoke.reject("Tidak ada kredensial Google yang tersedia: ${e.message}") + } + } catch (e: GetCredentialException) { + invoke.reject("Gagal masuk: ${e.message}") + } + } + } + + @Command + fun keluar(invoke: Invoke) { + scope.launch { + try { + credentialManager.clearCredentialState(ClearCredentialStateRequest()) + invoke.resolve() + } catch (e: ClearCredentialException) { + invoke.reject("Sign-out failed: ${e.message}") + } + } + } + + private suspend fun signInWithAllAccounts(serverClientId: String, invoke: Invoke) { + val googleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(false) + .setServerClientId(serverClientId) + .setNonce(generateSecureRandomNonce()) + .build() + + val request = GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + + try { + val result = credentialManager.getCredential( + request = request, + context = activity + ) + handleSignInResult(result, invoke) + } catch (e: GetCredentialException) { + invoke.reject("Sign-in failed: ${e.message}") + } + } + + private fun generateSecureRandomNonce(byteLength: Int = 32): String { + val randomBytes = ByteArray(byteLength) + SecureRandom().nextBytes(randomBytes) + return Base64.encodeToString(randomBytes, Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING) + } + + private fun handleSignInResult(result: GetCredentialResponse, invoke: Invoke) { + val credential = result.credential + + when (credential) { + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + val googleIdTokenCredential = + GoogleIdTokenCredential.createFrom(credential.data) + + // NOTE: do not treat the user as signed in on the client + // alone — send idToken to your backend and verify it there + // before trusting it. + val ret = JSObject() + ret.put("idToken", googleIdTokenCredential.idToken) + ret.put("id", googleIdTokenCredential.id) + ret.put("displayName", googleIdTokenCredential.displayName) + ret.put("givenName", googleIdTokenCredential.givenName) + ret.put("familyName", googleIdTokenCredential.familyName) + ret.put( + "profilePictureUri", + googleIdTokenCredential.profilePictureUri?.toString() + ) + ret.put("phoneNumber", googleIdTokenCredential.phoneNumber) + + invoke.resolve(ret) + } catch (e: GoogleIdTokenParsingException) { + invoke.reject("Invalid Google ID token response: ${e.message}") + } + } else { + invoke.reject("Unexpected credential type: ${credential.type}") + } + } + + else -> { + invoke.reject("Unexpected credential type") + } + } + } +} diff --git a/guest-js/index.ts b/guest-js/index.ts index bf9ebba..4cbf4d0 100644 --- a/guest-js/index.ts +++ b/guest-js/index.ts @@ -1,5 +1,13 @@ -import { invoke } from '@tauri-apps/api/core' +import { invoke } from "@tauri-apps/api/core"; -export async function autentikasi(): Promise<[null]> { - return await invoke<[null]>(""); +export type SAuthMuatanMasuk = { + "id-klien": string; +}; + +export async function masuk(muatan: SAuthMuatanMasuk): Promise { + return await invoke("plugin:auth|masuk", { payload: muatan }); +} + +export async function keluar(): Promise { + return await invoke("plugin:auth|masuk"); } diff --git a/package.json b/package.json index 4bb2cfc..bb618a9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "tauri-plugin-auth-api", "version": "0.1.0", "author": "Rizky Denianto", - "description": "", + "description": "Program sederhana untuk interaksi dengan autentikasi aplikasi tauri di berbagai sistem operasi", "type": "module", "types": "./dist-js/index.d.ts", "main": "./dist-js/index.cjs", diff --git a/src/commands.rs b/src/commands.rs index 54c0590..2143653 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -2,8 +2,14 @@ use tauri::{AppHandle, Runtime, command}; use crate::AuthExt; use crate::Result; +use crate::models::SAuthMuatanMasuk; #[command] -pub(crate) async fn autentikasi(app: AppHandle) -> Result> { - return app.auth().autentikasi(); +pub(crate) async fn masuk(app: AppHandle, payload: SAuthMuatanMasuk) -> Result<()> { + return app.auth().masuk(payload); +} + +#[command] +pub(crate) async fn keluar(app: AppHandle) -> Result<()> { + return app.auth().keluar(); } diff --git a/src/desktop.rs b/src/desktop.rs index 8f71e7d..5af21db 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -1,6 +1,8 @@ use serde::de::DeserializeOwned; use tauri::{AppHandle, Runtime, plugin::PluginApi}; +use crate::models::SAuthMuatanMasuk; + pub fn init( app: &AppHandle, _api: PluginApi, @@ -12,7 +14,11 @@ pub fn init( pub struct Auth(AppHandle); impl Auth { - pub fn autentikasi(&self) -> crate::Result> { - return Ok(vec![]); + pub fn masuk(&self, muatan: SAuthMuatanMasuk) -> crate::Result<()> { + return Ok(()); + } + + pub fn keluar(&self) -> crate::Result<()> { + return Ok(()); } } diff --git a/src/lib.rs b/src/lib.rs index c11c2e7..89748c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,8 @@ use desktop::Auth; #[cfg(mobile)] use mobile::Auth; +use crate::commands::{keluar, masuk}; + /// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the auth APIs. pub trait AuthExt { fn auth(&self) -> &Auth; @@ -33,7 +35,7 @@ impl> crate::AuthExt for T { /// Initializes the plugin. pub fn init() -> TauriPlugin { Builder::new("auth") - .invoke_handler(tauri::generate_handler![commands::autentikasi]) + .invoke_handler(tauri::generate_handler![masuk, keluar]) .setup(|app, api| { #[cfg(mobile)] let auth = mobile::init(app, api)?; diff --git a/src/mobile.rs b/src/mobile.rs index 8746200..bfe78cc 100644 --- a/src/mobile.rs +++ b/src/mobile.rs @@ -15,7 +15,7 @@ pub fn init( api: PluginApi, ) -> crate::Result> { #[cfg(target_os = "android")] - let handle = api.register_android_plugin("", "ExamplePlugin")?; + let handle = api.register_android_plugin("com.plugin.auth", "AuthPlugin")?; #[cfg(target_os = "ios")] let handle = api.register_ios_plugin(init_plugin_auth)?; Ok(Auth(handle)) @@ -25,7 +25,14 @@ pub fn init( pub struct Auth(PluginHandle); impl Auth { - pub fn autentikasi(&self) -> crate::Result> { - return self.0.run_mobile_plugin("autentikasi").map_err(Into::into); + pub fn masuk(&self, payload: SAuthMuatanMasuk) -> crate::Result<()> { + return self + .0 + .run_mobile_plugin("masuk", payload) + .map_err(Into::into); + } + + pub fn keluar(&self) -> crate::Result> { + return self.0.run_mobile_plugin("keluar", ()).map_err(Into::into); } } diff --git a/src/models.rs b/src/models.rs index 8b13789..bf5e83d 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1 +1,7 @@ +use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SAuthMuatanMasuk { + pub id_klien: String, +}