diff --git a/package-lock.json b/package-lock.json index a1e7dd570..fe331a040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "@types/url-parse": "^1.4.11", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", + "com.foxdebug.acode.rk.auth": "file:src/plugins/auth", "com.foxdebug.acode.rk.customtabs": "file:src/plugins/custom-tabs", "com.foxdebug.acode.rk.exec.proot": "file:src/plugins/proot", "com.foxdebug.acode.rk.exec.terminal": "file:src/plugins/terminal", @@ -3983,6 +3984,10 @@ "dev": true, "license": "MIT" }, + "node_modules/com.foxdebug.acode.rk.auth": { + "resolved": "src/plugins/auth", + "link": true + }, "node_modules/com.foxdebug.acode.rk.customtabs": { "resolved": "src/plugins/custom-tabs", "link": true @@ -8709,6 +8714,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "src/plugins/auth": { + "name": "com.foxdebug.acode.rk.auth", + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, "src/plugins/browser": { "name": "cordova-plugin-browser", "version": "1.0.0", @@ -8781,6 +8792,12 @@ "dev": true, "license": "ISC" }, + "src/plugins/secrets": { + "name": "com.foxdebug.acode.rk.secrets", + "version": "1.0.0", + "extraneous": true, + "license": "MIT" + }, "src/plugins/server": { "name": "cordova-plugin-server", "version": "1.0.0", diff --git a/package.json b/package.json index 6f81a0789..ac8ee9b78 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "cordova-plugin-system": {}, "com.foxdebug.acode.rk.exec.proot": {}, "com.foxdebug.acode.rk.exec.terminal": {}, - "com.foxdebug.acode.rk.customtabs": {} + "com.foxdebug.acode.rk.customtabs": {}, + "com.foxdebug.acode.rk.auth": {} }, "platforms": [ "android" @@ -64,6 +65,7 @@ "@types/url-parse": "^1.4.11", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", + "com.foxdebug.acode.rk.auth": "file:src/plugins/auth", "com.foxdebug.acode.rk.customtabs": "file:src/plugins/custom-tabs", "com.foxdebug.acode.rk.exec.proot": "file:src/plugins/proot", "com.foxdebug.acode.rk.exec.terminal": "file:src/plugins/terminal", diff --git a/src/lib/auth.js b/src/lib/auth.js index 4d0763a41..c8911be88 100644 --- a/src/lib/auth.js +++ b/src/lib/auth.js @@ -1,7 +1,5 @@ -import fsOperation from "fileSystem"; import toast from "components/toast"; import { addIntentHandler } from "handlers/intent"; -import constants from "./constants"; const loginEvents = { listeners: new Set(), @@ -18,70 +16,16 @@ const loginEvents = { }, }; -async function checkTokenFileExists() { - return await fsOperation(`${DATA_STORAGE}.acode_token`).exists(); -} - -async function saveToken(token) { - try { - if (await checkTokenFileExists()) { - await fsOperation(`${DATA_STORAGE}.acode_token`).writeFile(token); - } else { - await fsOperation(DATA_STORAGE).createFile(".acode_token", token); - } - return true; - } catch (error) { - console.error("Failed to save token", error); - return false; - } -} - -async function getToken() { - try { - if (await checkTokenFileExists()) { - const token = await fsOperation(`${DATA_STORAGE}.acode_token`).readFile( - "utf8", - ); - return token; - } - return null; - } catch (error) { - console.error("Failed to get token", error); - return null; - } -} - -async function deleteToken() { - try { - if (await checkTokenFileExists()) { - await fsOperation(`${DATA_STORAGE}.acode_token`).delete(); - return true; - } - return false; - } catch (error) { - console.error("Failed to delete token", error); - return false; - } -} - class AuthService { constructor() { addIntentHandler(this.onIntentReceiver.bind(this)); } - openLoginUrl() { - try { - system.openInBrowser("https://acode.app/login?redirect=app"); - } catch (error) { - console.error("Failed while opening login page.", error); - } - } - async onIntentReceiver(event) { try { if (event?.module === "user" && event?.action === "login") { if (event?.value) { - saveToken(event.value); + this._exec("saveToken", [event.value]); toast("Logged in successfully"); setTimeout(() => { @@ -96,10 +40,32 @@ class AuthService { } } + /** + * Helper to wrap cordova.exec in a Promise + */ + _exec(action, args = []) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, "Authenticator", action, args); + }); + } + + async openLoginUrl() { + const url = "https://acode.app/login?redirect=app"; + + try { + await new Promise((resolve, reject) => { + CustomTabs.open(url, { showTitle: true }, resolve, reject); + }); + } catch (error) { + console.error("CustomTabs failed, opening system browser.", error); + system.openInBrowser(url); + } + } + async logout() { try { - const result = await deleteToken(); - return result; + await this._exec("logout"); + return true; } catch (error) { console.error("Failed to logout.", error); return false; @@ -108,69 +74,20 @@ class AuthService { async isLoggedIn() { try { - const token = await getToken(); - if (!token) return false; - - return new Promise((resolve, reject) => { - cordova.plugin.http.sendRequest( - `${constants.API_BASE}/login`, - { - method: "GET", - headers: { - "x-auth-token": token, - }, - }, - (response) => { - resolve(true); - }, - async (error) => { - if (error.status === 401) { - await deleteToken(); - resolve(false); - } else { - console.error("Failed to check login status.", error); - resolve(false); - } - }, - ); - }); + // Native checks EncryptedPrefs and validates with API internally + await this._exec("isLoggedIn"); + return true; } catch (error) { - console.error("Failed to check login status.", error); + console.error(error); + // error is typically the status code (0 if no token, 401 if invalid) return false; } } async getUserInfo() { try { - const token = await getToken(); - if (!token) return null; - - return new Promise((resolve, reject) => { - cordova.plugin.http.sendRequest( - `${constants.API_BASE}/login`, - { - method: "GET", - headers: { - "x-auth-token": token, - }, - }, - async (response) => { - if (response.status === 200) { - resolve(JSON.parse(response.data)); - } - resolve(null); - }, - async (error) => { - if (error.status === 401) { - await deleteToken(); - resolve(null); - } else { - console.error("Failed to fetch user data.", error); - resolve(null); - } - }, - ); - }); + const data = await this._exec("getUserInfo"); + return typeof data === "string" ? JSON.parse(data) : data; } catch (error) { console.error("Failed to fetch user data.", error); return null; @@ -187,42 +104,7 @@ class AuthService { } if (userData.name) { - const nameParts = userData.name.split(" "); - let initials = ""; - - if (nameParts.length >= 2) { - initials = `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase(); - } else { - initials = nameParts[0][0].toUpperCase(); - } - - // Create a data URL for text-based avatar - const canvas = document.createElement("canvas"); - canvas.width = 100; - canvas.height = 100; - const ctx = canvas.getContext("2d"); - - // Set background - // Array of colors to choose from - const colors = [ - "#2196F3", - "#9C27B0", - "#E91E63", - "#009688", - "#4CAF50", - "#FF9800", - ]; - ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)]; - ctx.fillRect(0, 0, 100, 100); - - // Add text - ctx.fillStyle = "#ffffff"; - ctx.font = "bold 40px Arial"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(initials, 50, 50); - - return canvas.toDataURL(); + return this._generateInitialsAvatar(userData.name); } return null; @@ -231,6 +113,42 @@ class AuthService { return null; } } + + _generateInitialsAvatar(name) { + const nameParts = name.split(" "); + const initials = + nameParts.length >= 2 + ? `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase() + : nameParts[0][0].toUpperCase(); + + const canvas = document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + const ctx = canvas.getContext("2d"); + + const colors = [ + "#2196F3", + "#9C27B0", + "#E91E63", + "#009688", + "#4CAF50", + "#FF9800", + ]; + ctx.fillStyle = + colors[ + name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0) % + colors.length + ]; + ctx.fillRect(0, 0, 100, 100); + + ctx.fillStyle = "#ffffff"; + ctx.font = "bold 40px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(initials, 50, 50); + + return canvas.toDataURL(); + } } export default new AuthService(); diff --git a/src/plugins/auth/package.json b/src/plugins/auth/package.json new file mode 100644 index 000000000..584bc37c9 --- /dev/null +++ b/src/plugins/auth/package.json @@ -0,0 +1,17 @@ +{ + "name": "com.foxdebug.acode.rk.auth", + "version": "1.0.0", + "description": "Authentication plugin", + "cordova": { + "id": "com.foxdebug.acode.rk.auth", + "platforms": [ + "android" + ] + }, + "keywords": [ + "ecosystem:cordova", + "cordova-android" + ], + "author": "@RohitKushvaha01", + "license": "MIT" +} \ No newline at end of file diff --git a/src/plugins/auth/plugin.xml b/src/plugins/auth/plugin.xml new file mode 100644 index 000000000..e6e3e448c --- /dev/null +++ b/src/plugins/auth/plugin.xml @@ -0,0 +1,20 @@ + + + Authentication + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/auth/src/android/Authenticator.java b/src/plugins/auth/src/android/Authenticator.java new file mode 100644 index 000000000..a89fee05e --- /dev/null +++ b/src/plugins/auth/src/android/Authenticator.java @@ -0,0 +1,148 @@ +package com.foxdebug.acode.rk.auth; + +import android.util.Log; // Required for logging +import com.foxdebug.acode.rk.auth.EncryptedPreferenceManager; +import org.apache.cordova.*; +import org.json.JSONArray; +import org.json.JSONException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Scanner; + +public class Authenticator extends CordovaPlugin { + // Standard practice: use a TAG for easy filtering in Logcat + private static final String TAG = "AcodeAuth"; + private static final String PREFS_FILENAME = "acode_auth_secure"; + private static final String KEY_TOKEN = "auth_token"; + private EncryptedPreferenceManager prefManager; + + @Override + protected void pluginInitialize() { + Log.d(TAG, "Initializing Authenticator Plugin..."); + this.prefManager = new EncryptedPreferenceManager(this.cordova.getContext(), PREFS_FILENAME); + } + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + Log.i(TAG, "Native Action Called: " + action); + + switch (action) { + case "logout": + this.logout(callbackContext); + return true; + case "isLoggedIn": + this.isLoggedIn(callbackContext); + return true; + case "getUserInfo": + this.getUserInfo(callbackContext); + return true; + case "saveToken": + String token = args.getString(0); + Log.d(TAG, "Saving new token..."); + prefManager.setString(KEY_TOKEN, token); + callbackContext.success(); + return true; + default: + Log.w(TAG, "Attempted to call unknown action: " + action); + return false; + } + } + + private void logout(CallbackContext callbackContext) { + Log.d(TAG, "Logging out, removing token."); + prefManager.remove(KEY_TOKEN); + if (callbackContext != null) callbackContext.success(); + } + + private void isLoggedIn(CallbackContext callbackContext) { + String token = prefManager.getString(KEY_TOKEN, null); + if (token == null) { + Log.d(TAG, "isLoggedIn check: No token found in preferences."); + callbackContext.error(0); + return; + } + + Log.d(TAG, "isLoggedIn check: Token found, validating with server..."); + final String tokenToValidate = token; // Make effectively final for lambda + + cordova.getThreadPool().execute(() -> { + try { + String result = validateToken(tokenToValidate); + if (result != null) { + Log.i(TAG, "Token validation successful."); + callbackContext.success(); + } else { + Log.w(TAG, "Token validation failed (result was null)."); + callbackContext.error(401); + } + } catch (Exception e) { + Log.e(TAG, "CRITICAL error in isLoggedIn thread: " + e.getMessage(), e); + callbackContext.error("Internal Plugin Error: " + e.getMessage()); + } + }); + } + + private void getUserInfo(CallbackContext callbackContext) { + Log.d(TAG, "getUserInfo: Fetching token..."); + String token = prefManager.getString(KEY_TOKEN, null); + + if (token == null) { + Log.e(TAG, "getUserInfo: No token found."); + callbackContext.error(0); + return; + } + + final String tokenToValidate = token; + cordova.getThreadPool().execute(() -> { + try { + String response = validateToken(tokenToValidate); + if (response != null) { + Log.d(TAG, "getUserInfo: Successfully fetched user info."); + callbackContext.success(response); + } else { + Log.e(TAG, "getUserInfo: Validation failed or unauthorized."); + callbackContext.error("Unauthorized"); + } + } catch (Exception e) { + Log.e(TAG, "Error in getUserInfo: " + e.getMessage(), e); + callbackContext.error("Error: " + e.getMessage()); + } + }); + } + + private String validateToken(String token) { + HttpURLConnection conn = null; + try { + Log.d(TAG, "Network Request: Connecting to https://acode.app/api/login"); + URL url = new URL("https://acode.app/api/login"); // Changed from /api to /api/login + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("x-auth-token", token); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); // Add read timeout too + + int code = conn.getResponseCode(); + Log.d(TAG, "Server responded with status code: " + code); + + if (code == 200) { + Scanner s = new Scanner(conn.getInputStream(), "UTF-8").useDelimiter("\\A"); + String response = s.hasNext() ? s.next() : ""; + Log.d(TAG, "Response received: " + response); // Debug log + return response; + } else if (code == 401) { + Log.w(TAG, "401 Unauthorized: Logging user out native-side."); + logout(null); + } else { + Log.w(TAG, "Unexpected status code: " + code); + } + } catch (Exception e) { + Log.e(TAG, "Network Exception in validateToken: " + e.getMessage(), e); + e.printStackTrace(); // Print full stack trace for debugging + } finally { + if (conn != null) conn.disconnect(); + } + return null; + } + + +} \ No newline at end of file diff --git a/src/plugins/auth/src/android/EncryptedPreferenceManager.java b/src/plugins/auth/src/android/EncryptedPreferenceManager.java new file mode 100644 index 000000000..fa0939773 --- /dev/null +++ b/src/plugins/auth/src/android/EncryptedPreferenceManager.java @@ -0,0 +1,64 @@ +package com.foxdebug.acode.rk.auth; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.MasterKeys; +import java.io.IOException; +import java.security.GeneralSecurityException; + +public class EncryptedPreferenceManager { + private SharedPreferences sharedPreferences; + + /** + * @param context The Android Context + * @param prefName The custom name for your preference file (e.g., "user_session") + */ + public EncryptedPreferenceManager(Context context, String prefName) { + try { + String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC); + + // EncryptedSharedPreferences handles encryption of both Keys and Values + sharedPreferences = EncryptedSharedPreferences.create( + prefName, + masterKeyAlias, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ); + } catch (GeneralSecurityException | IOException e) { + // Fallback to standard private preferences if hardware-backed encryption fails + sharedPreferences = context.getSharedPreferences(prefName, Context.MODE_PRIVATE); + } + } + + // --- Reusable Methods --- + + public void setString(String key, String value) { + sharedPreferences.edit().putString(key, value).apply(); + } + + public String getString(String key, String defaultValue) { + return sharedPreferences.getString(key, defaultValue); + } + + public void setInt(String key, int value) { + sharedPreferences.edit().putInt(key, value).apply(); + } + + public int getInt(String key, int defaultValue) { + return sharedPreferences.getInt(key, defaultValue); + } + + public void remove(String key) { + sharedPreferences.edit().remove(key).apply(); + } + + public boolean exists(String key) { + return sharedPreferences.contains(key); + } + + public void clear() { + sharedPreferences.edit().clear().apply(); + } +} \ No newline at end of file