Skip to content

Commit 019c73c

Browse files
feat(auth): move authentication to native Android plugin with encrypted storage (#1794)
* move token logic of auth.js to native * Update src/plugins/auth/src/android/Authenticator.java * Update src/lib/auth.js * format * format * feat: use custom tabs when possible * format
1 parent ca86bfe commit 019c73c

File tree

7 files changed

+338
-152
lines changed

7 files changed

+338
-152
lines changed

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"cordova-plugin-system": {},
4040
"com.foxdebug.acode.rk.exec.proot": {},
4141
"com.foxdebug.acode.rk.exec.terminal": {},
42-
"com.foxdebug.acode.rk.customtabs": {}
42+
"com.foxdebug.acode.rk.customtabs": {},
43+
"com.foxdebug.acode.rk.auth": {}
4344
},
4445
"platforms": [
4546
"android"
@@ -64,6 +65,7 @@
6465
"@types/url-parse": "^1.4.11",
6566
"autoprefixer": "^10.4.21",
6667
"babel-loader": "^10.0.0",
68+
"com.foxdebug.acode.rk.auth": "file:src/plugins/auth",
6769
"com.foxdebug.acode.rk.customtabs": "file:src/plugins/custom-tabs",
6870
"com.foxdebug.acode.rk.exec.proot": "file:src/plugins/proot",
6971
"com.foxdebug.acode.rk.exec.terminal": "file:src/plugins/terminal",

src/lib/auth.js

Lines changed: 69 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import fsOperation from "fileSystem";
21
import toast from "components/toast";
32
import { addIntentHandler } from "handlers/intent";
4-
import constants from "./constants";
53

64
const loginEvents = {
75
listeners: new Set(),
@@ -18,70 +16,16 @@ const loginEvents = {
1816
},
1917
};
2018

21-
async function checkTokenFileExists() {
22-
return await fsOperation(`${DATA_STORAGE}.acode_token`).exists();
23-
}
24-
25-
async function saveToken(token) {
26-
try {
27-
if (await checkTokenFileExists()) {
28-
await fsOperation(`${DATA_STORAGE}.acode_token`).writeFile(token);
29-
} else {
30-
await fsOperation(DATA_STORAGE).createFile(".acode_token", token);
31-
}
32-
return true;
33-
} catch (error) {
34-
console.error("Failed to save token", error);
35-
return false;
36-
}
37-
}
38-
39-
async function getToken() {
40-
try {
41-
if (await checkTokenFileExists()) {
42-
const token = await fsOperation(`${DATA_STORAGE}.acode_token`).readFile(
43-
"utf8",
44-
);
45-
return token;
46-
}
47-
return null;
48-
} catch (error) {
49-
console.error("Failed to get token", error);
50-
return null;
51-
}
52-
}
53-
54-
async function deleteToken() {
55-
try {
56-
if (await checkTokenFileExists()) {
57-
await fsOperation(`${DATA_STORAGE}.acode_token`).delete();
58-
return true;
59-
}
60-
return false;
61-
} catch (error) {
62-
console.error("Failed to delete token", error);
63-
return false;
64-
}
65-
}
66-
6719
class AuthService {
6820
constructor() {
6921
addIntentHandler(this.onIntentReceiver.bind(this));
7022
}
7123

72-
openLoginUrl() {
73-
try {
74-
system.openInBrowser("https://acode.app/login?redirect=app");
75-
} catch (error) {
76-
console.error("Failed while opening login page.", error);
77-
}
78-
}
79-
8024
async onIntentReceiver(event) {
8125
try {
8226
if (event?.module === "user" && event?.action === "login") {
8327
if (event?.value) {
84-
saveToken(event.value);
28+
this._exec("saveToken", [event.value]);
8529
toast("Logged in successfully");
8630

8731
setTimeout(() => {
@@ -96,10 +40,32 @@ class AuthService {
9640
}
9741
}
9842

43+
/**
44+
* Helper to wrap cordova.exec in a Promise
45+
*/
46+
_exec(action, args = []) {
47+
return new Promise((resolve, reject) => {
48+
cordova.exec(resolve, reject, "Authenticator", action, args);
49+
});
50+
}
51+
52+
async openLoginUrl() {
53+
const url = "https://acode.app/login?redirect=app";
54+
55+
try {
56+
await new Promise((resolve, reject) => {
57+
CustomTabs.open(url, { showTitle: true }, resolve, reject);
58+
});
59+
} catch (error) {
60+
console.error("CustomTabs failed, opening system browser.", error);
61+
system.openInBrowser(url);
62+
}
63+
}
64+
9965
async logout() {
10066
try {
101-
const result = await deleteToken();
102-
return result;
67+
await this._exec("logout");
68+
return true;
10369
} catch (error) {
10470
console.error("Failed to logout.", error);
10571
return false;
@@ -108,69 +74,20 @@ class AuthService {
10874

10975
async isLoggedIn() {
11076
try {
111-
const token = await getToken();
112-
if (!token) return false;
113-
114-
return new Promise((resolve, reject) => {
115-
cordova.plugin.http.sendRequest(
116-
`${constants.API_BASE}/login`,
117-
{
118-
method: "GET",
119-
headers: {
120-
"x-auth-token": token,
121-
},
122-
},
123-
(response) => {
124-
resolve(true);
125-
},
126-
async (error) => {
127-
if (error.status === 401) {
128-
await deleteToken();
129-
resolve(false);
130-
} else {
131-
console.error("Failed to check login status.", error);
132-
resolve(false);
133-
}
134-
},
135-
);
136-
});
77+
// Native checks EncryptedPrefs and validates with API internally
78+
await this._exec("isLoggedIn");
79+
return true;
13780
} catch (error) {
138-
console.error("Failed to check login status.", error);
81+
console.error(error);
82+
// error is typically the status code (0 if no token, 401 if invalid)
13983
return false;
14084
}
14185
}
14286

14387
async getUserInfo() {
14488
try {
145-
const token = await getToken();
146-
if (!token) return null;
147-
148-
return new Promise((resolve, reject) => {
149-
cordova.plugin.http.sendRequest(
150-
`${constants.API_BASE}/login`,
151-
{
152-
method: "GET",
153-
headers: {
154-
"x-auth-token": token,
155-
},
156-
},
157-
async (response) => {
158-
if (response.status === 200) {
159-
resolve(JSON.parse(response.data));
160-
}
161-
resolve(null);
162-
},
163-
async (error) => {
164-
if (error.status === 401) {
165-
await deleteToken();
166-
resolve(null);
167-
} else {
168-
console.error("Failed to fetch user data.", error);
169-
resolve(null);
170-
}
171-
},
172-
);
173-
});
89+
const data = await this._exec("getUserInfo");
90+
return typeof data === "string" ? JSON.parse(data) : data;
17491
} catch (error) {
17592
console.error("Failed to fetch user data.", error);
17693
return null;
@@ -187,42 +104,7 @@ class AuthService {
187104
}
188105

189106
if (userData.name) {
190-
const nameParts = userData.name.split(" ");
191-
let initials = "";
192-
193-
if (nameParts.length >= 2) {
194-
initials = `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase();
195-
} else {
196-
initials = nameParts[0][0].toUpperCase();
197-
}
198-
199-
// Create a data URL for text-based avatar
200-
const canvas = document.createElement("canvas");
201-
canvas.width = 100;
202-
canvas.height = 100;
203-
const ctx = canvas.getContext("2d");
204-
205-
// Set background
206-
// Array of colors to choose from
207-
const colors = [
208-
"#2196F3",
209-
"#9C27B0",
210-
"#E91E63",
211-
"#009688",
212-
"#4CAF50",
213-
"#FF9800",
214-
];
215-
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
216-
ctx.fillRect(0, 0, 100, 100);
217-
218-
// Add text
219-
ctx.fillStyle = "#ffffff";
220-
ctx.font = "bold 40px Arial";
221-
ctx.textAlign = "center";
222-
ctx.textBaseline = "middle";
223-
ctx.fillText(initials, 50, 50);
224-
225-
return canvas.toDataURL();
107+
return this._generateInitialsAvatar(userData.name);
226108
}
227109

228110
return null;
@@ -231,6 +113,42 @@ class AuthService {
231113
return null;
232114
}
233115
}
116+
117+
_generateInitialsAvatar(name) {
118+
const nameParts = name.split(" ");
119+
const initials =
120+
nameParts.length >= 2
121+
? `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase()
122+
: nameParts[0][0].toUpperCase();
123+
124+
const canvas = document.createElement("canvas");
125+
canvas.width = 100;
126+
canvas.height = 100;
127+
const ctx = canvas.getContext("2d");
128+
129+
const colors = [
130+
"#2196F3",
131+
"#9C27B0",
132+
"#E91E63",
133+
"#009688",
134+
"#4CAF50",
135+
"#FF9800",
136+
];
137+
ctx.fillStyle =
138+
colors[
139+
name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0) %
140+
colors.length
141+
];
142+
ctx.fillRect(0, 0, 100, 100);
143+
144+
ctx.fillStyle = "#ffffff";
145+
ctx.font = "bold 40px Arial";
146+
ctx.textAlign = "center";
147+
ctx.textBaseline = "middle";
148+
ctx.fillText(initials, 50, 50);
149+
150+
return canvas.toDataURL();
151+
}
234152
}
235153

236154
export default new AuthService();

src/plugins/auth/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "com.foxdebug.acode.rk.auth",
3+
"version": "1.0.0",
4+
"description": "Authentication plugin",
5+
"cordova": {
6+
"id": "com.foxdebug.acode.rk.auth",
7+
"platforms": [
8+
"android"
9+
]
10+
},
11+
"keywords": [
12+
"ecosystem:cordova",
13+
"cordova-android"
14+
],
15+
"author": "@RohitKushvaha01",
16+
"license": "MIT"
17+
}

src/plugins/auth/plugin.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="com.foxdebug.acode.rk.auth" version="1.0.0">
3+
<name>Authentication</name>
4+
5+
6+
<platform name="android">
7+
<config-file parent="/*" target="res/xml/config.xml">
8+
<feature name="Authenticator">
9+
<param name="android-package" value="com.foxdebug.acode.rk.auth.Authenticator" />
10+
</feature>
11+
</config-file>
12+
13+
<framework src="androidx.security:security-crypto:1.1.0" />
14+
15+
<source-file src="src/android/Authenticator.java" target-dir="src/com/foxdebug/acode/rk/auth" />
16+
<source-file src="src/android/EncryptedPreferenceManager.java" target-dir="src/com/foxdebug/acode/rk/auth" />
17+
18+
19+
</platform>
20+
</plugin>

0 commit comments

Comments
 (0)