Skip to content

Commit c0fd8e2

Browse files
Merge pull request #349 from appdevforall/feat/ADFA-5043-app-webview-auto-login
ADFA-5043: auto-login into Books/Courses in the WebView via injected session cookie
2 parents 026c998 + 8c1e7b4 commit c0fd8e2

3 files changed

Lines changed: 153 additions & 2 deletions

File tree

‎controller/app/src/main/java/org/iiab/controller/PortalActivity.java‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,64 @@ public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
338338
// (gated in KiwixManageController#onPageFinished).
339339
kiwixMgr = new org.iiab.controller.redesign.KiwixManageController(this, webView);
340340

341-
// Native architecture: content is served locally; load it directly.
342-
webView.loadUrl(finalTargetUrl);
341+
// ADFA-5043: Books (Calibre-Web) / Courses (Kolibri) auto-login as box admin — fetch a session
342+
// cookie, inject it into the WebView CookieManager, THEN load, so the card opens already
343+
// authenticated. Degrades gracefully: if the service isn't installed/ready, just load without it.
344+
String authService = getIntent().getStringExtra("AUTH_SERVICE");
345+
if (authService != null && !authService.isEmpty()) {
346+
autoLoginThenLoad(authService, finalTargetUrl);
347+
} else {
348+
// Native architecture: content is served locally; load it directly.
349+
webView.loadUrl(finalTargetUrl);
350+
}
351+
}
352+
353+
/** ADFA-5043: get an admin session cookie for the service, inject it, then load the page. On any
354+
* failure (service absent/not ready) load without a cookie — the card still opens. */
355+
private void autoLoginThenLoad(String service, String targetUrl) {
356+
showAuthOverlay();
357+
org.iiab.controller.redesign.AuthClient.session(service, new org.iiab.controller.redesign.AuthClient.SessionCb() {
358+
@Override public void onOk(String cookie) {
359+
if (isFinishing() || isDestroyed()) return; // ADFA-5043: left mid-sign-in; don't touch dead views
360+
android.webkit.CookieManager cm = android.webkit.CookieManager.getInstance();
361+
cm.setAcceptCookie(true);
362+
// Host-wide on the box origin so every request under the service prefix carries it.
363+
for (String pair : cookie.split(";")) {
364+
String p = pair.trim();
365+
if (!p.isEmpty()) cm.setCookie(BoxEndpoints.BASE + "/", p + "; path=/");
366+
}
367+
cm.flush();
368+
hideAuthOverlay();
369+
webView.loadUrl(targetUrl);
370+
}
371+
@Override public void onErr() {
372+
if (isFinishing() || isDestroyed()) return; // ADFA-5043: left mid-sign-in; don't touch dead views
373+
hideAuthOverlay();
374+
webView.loadUrl(targetUrl);
375+
}
376+
});
377+
}
378+
379+
private android.view.View authOverlay;
380+
381+
private void showAuthOverlay() {
382+
android.widget.FrameLayout f = new android.widget.FrameLayout(this);
383+
f.setClickable(true); // swallow taps while signing in
384+
android.widget.ProgressBar pb = new android.widget.ProgressBar(this);
385+
f.addView(pb, new android.widget.FrameLayout.LayoutParams(
386+
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
387+
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, android.view.Gravity.CENTER));
388+
authOverlay = f;
389+
addContentView(f, new android.widget.FrameLayout.LayoutParams(
390+
android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
391+
android.widget.FrameLayout.LayoutParams.MATCH_PARENT));
392+
}
393+
394+
private void hideAuthOverlay() {
395+
if (authOverlay == null) return;
396+
android.view.ViewParent parent = authOverlay.getParent();
397+
if (parent instanceof android.view.ViewGroup) ((android.view.ViewGroup) parent).removeView(authOverlay);
398+
authOverlay = null;
343399
}
344400

345401
/**
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* ============================================================================
3+
* Name : AuthClient.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : ADFA-5043. App-side client for the dash-node auto-login endpoint:
7+
* GET /k2go-api/auth/<service>/session -> { service, cookie }
8+
* The server logs into Calibre-Web ("books"/"calibre") or Kolibri ("kolibri") with the
9+
* stored admin credentials and returns the session cookie; the app injects it into the
10+
* WebView CookieManager so the card opens already authenticated. The password never
11+
* reaches the app. On any failure (service not installed/ready) the caller just opens the
12+
* card without a cookie. Only reachable on-device (localhost); the box denies the API to
13+
* remote clients.
14+
* ============================================================================
15+
*/
16+
package org.iiab.controller.redesign;
17+
18+
import android.os.Handler;
19+
import android.os.Looper;
20+
21+
import org.iiab.controller.config.BoxEndpoints;
22+
import org.iiab.controller.util.AppExecutors;
23+
import org.json.JSONObject;
24+
25+
import java.io.ByteArrayOutputStream;
26+
import java.io.InputStream;
27+
import java.net.HttpURLConnection;
28+
import java.net.URL;
29+
import java.nio.charset.StandardCharsets;
30+
31+
public final class AuthClient {
32+
private AuthClient() {}
33+
34+
private static final Handler MAIN = new Handler(Looper.getMainLooper());
35+
36+
public interface SessionCb {
37+
/** {@code cookie} is a ready-to-use Cookie header ("name=value; name2=value2"). */
38+
void onOk(String cookie);
39+
/** The session couldn't be obtained (service not installed/ready, bad creds, etc.). */
40+
void onErr();
41+
}
42+
43+
/** Ask the box for a signed-in session cookie for a service ("books"/"calibre" or "kolibri"). */
44+
public static void session(String service, SessionCb cb) {
45+
AppExecutors.get().io().execute(() -> {
46+
try {
47+
String url = BoxEndpoints.API + "/auth/" + service + "/session";
48+
JSONObject o = new JSONObject(httpGet(url));
49+
final String cookie = o.optString("cookie", "");
50+
if (cookie.isEmpty()) MAIN.post(cb::onErr);
51+
else MAIN.post(() -> cb.onOk(cookie));
52+
} catch (Exception e) {
53+
MAIN.post(cb::onErr);
54+
}
55+
});
56+
}
57+
58+
private static String httpGet(String urlStr) throws Exception {
59+
HttpURLConnection c = (HttpURLConnection) new URL(urlStr).openConnection();
60+
try {
61+
c.setUseCaches(false);
62+
c.setConnectTimeout(4000);
63+
// The server does a login handshake with the local service; a missing service fails fast.
64+
c.setReadTimeout(12000);
65+
c.setRequestProperty("Accept", "application/json");
66+
int code = c.getResponseCode();
67+
String text = readAll(code >= 200 && code < 400 ? c.getInputStream() : c.getErrorStream());
68+
if (code < 200 || code >= 400) throw new Exception("HTTP " + code);
69+
return text;
70+
} finally {
71+
c.disconnect();
72+
}
73+
}
74+
75+
private static String readAll(InputStream is) throws Exception {
76+
if (is == null) return "";
77+
ByteArrayOutputStream buf = new ByteArrayOutputStream();
78+
byte[] chunk = new byte[4096];
79+
int n;
80+
while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n);
81+
is.close();
82+
return buf.toString(StandardCharsets.UTF_8.name());
83+
}
84+
}

‎controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ private void onCardClick(Card c) {
231231
if (c.state == GREEN) {
232232
Intent i = new Intent(requireContext(), PortalActivity.class);
233233
i.putExtra("TARGET_URL", BoxEndpoints.BASE + "/" + c.endpoint + "/");
234+
// ADFA-5043: Books (Calibre-Web) and Courses (Kolibri) auto-login as box admin in the WebView.
235+
String authService = authServiceFor(c.endpoint);
236+
if (authService != null) i.putExtra("AUTH_SERVICE", authService);
234237
startActivity(i);
235238
} else if (ModuleCards.byEndpoint(c.endpoint) != null) { // ADFA-4958: module -> action sheet
236239
openSheet(c);
@@ -244,6 +247,14 @@ private void onCardClick(Card c) {
244247
}
245248
}
246249

250+
/** ADFA-5043: card endpoint → auto-login service name (server credential store), or null if the
251+
* card has no admin login. */
252+
private static String authServiceFor(String endpoint) {
253+
if ("kolibri".equals(endpoint)) return "kolibri";
254+
if ("books".equals(endpoint)) return "calibre";
255+
return null;
256+
}
257+
247258
// ADFA-4958: the module action sheet is the single contextual surface for a module card.
248259
private void openSheet(Card c) {
249260
ModuleActionSheet.State s;

0 commit comments

Comments
 (0)