Skip to content

Commit bc73419

Browse files
Merge pull request #351 from appdevforall/feat/ADFA-5044-settings-authentication
ADFA-5044: Settings → Authentication — manage per-service admin credentials
2 parents bf55db2 + 0178aa4 commit bc73419

39 files changed

Lines changed: 1455 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* ============================================================================
3+
* Name : CredentialsClient.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : ADFA-5044. App-side client for the dash-node service-credential store:
7+
* GET /k2go-api/credentials/<service> -> { username, isDefault, origin }
8+
* POST /k2go-api/credentials/<service> { username, password } -> { ok, verified }
9+
* DELETE /k2go-api/credentials/<service> -> reset to the box default
10+
* Drives the Settings -> Authentication screen so the user can keep the admin sign-ins
11+
* (Calibre-Web / Kolibri) that auto-login (ADFA-5043) relies on correct. The stored
12+
* password is never returned by the box (describe omits it); this only sets/reads the
13+
* username + default/custom state. Localhost-only; the box denies the API to 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.io.OutputStream;
28+
import java.net.HttpURLConnection;
29+
import java.net.URL;
30+
import java.nio.charset.StandardCharsets;
31+
32+
public final class CredentialsClient {
33+
private CredentialsClient() {}
34+
35+
private static final Handler MAIN = new Handler(Looper.getMainLooper());
36+
37+
public interface DescribeCb { void onOk(String username, boolean isDefault); void onErr(); }
38+
/** {@code verified} = the box confirmed the credentials against the live service (Kolibri today). */
39+
public interface SaveCb { void onOk(boolean verified); void onErr(int status); }
40+
public interface ResetCb { void onOk(String username, boolean isDefault); void onErr(); }
41+
42+
private static String url(String service) { return BoxEndpoints.API + "/credentials/" + service; }
43+
44+
/** Current stored username + whether it's still the box default (no password is ever returned). */
45+
public static void describe(String service, DescribeCb cb) {
46+
AppExecutors.get().io().execute(() -> {
47+
try {
48+
JSONObject o = new JSONObject(request("GET", url(service), null));
49+
final String user = o.optString("username", "");
50+
final boolean isDefault = o.optBoolean("isDefault", false);
51+
MAIN.post(() -> cb.onOk(user, isDefault));
52+
} catch (Exception e) {
53+
MAIN.post(cb::onErr);
54+
}
55+
});
56+
}
57+
58+
/** Save new credentials. The box verifies live where it can (Kolibri) and saves either way. */
59+
public static void save(String service, String username, String password, SaveCb cb) {
60+
AppExecutors.get().io().execute(() -> {
61+
int[] status = {0};
62+
try {
63+
JSONObject body = new JSONObject().put("username", username).put("password", password);
64+
JSONObject o = new JSONObject(request("POST", url(service), body, status));
65+
final boolean verified = o.optBoolean("verified", false);
66+
MAIN.post(() -> cb.onOk(verified));
67+
} catch (Exception e) {
68+
final int s = status[0];
69+
MAIN.post(() -> cb.onErr(s));
70+
}
71+
});
72+
}
73+
74+
/** Reset the service back to the box default (or the env override, if set). */
75+
public static void reset(String service, ResetCb cb) {
76+
AppExecutors.get().io().execute(() -> {
77+
try {
78+
JSONObject o = new JSONObject(request("DELETE", url(service), null));
79+
final String user = o.optString("username", "");
80+
final boolean isDefault = o.optBoolean("isDefault", false);
81+
MAIN.post(() -> cb.onOk(user, isDefault));
82+
} catch (Exception e) {
83+
MAIN.post(cb::onErr);
84+
}
85+
});
86+
}
87+
88+
private static String request(String method, String urlStr, JSONObject body) throws Exception {
89+
return request(method, urlStr, body, new int[1]);
90+
}
91+
92+
/** {@code statusOut[0]} receives the HTTP status so callers can map 401/403/… to a message. */
93+
private static String request(String method, String urlStr, JSONObject body, int[] statusOut) throws Exception {
94+
HttpURLConnection c = (HttpURLConnection) new URL(urlStr).openConnection();
95+
try {
96+
c.setUseCaches(false);
97+
c.setConnectTimeout(4000);
98+
c.setReadTimeout(15000); // POST may verify against the live service (Kolibri login)
99+
c.setRequestMethod(method);
100+
c.setRequestProperty("Accept", "application/json");
101+
if (body != null) {
102+
c.setDoOutput(true);
103+
c.setRequestProperty("Content-Type", "application/json");
104+
byte[] payload = body.toString().getBytes(StandardCharsets.UTF_8);
105+
try (OutputStream os = c.getOutputStream()) { os.write(payload); }
106+
}
107+
int code = c.getResponseCode();
108+
statusOut[0] = code;
109+
String text = readAll(code >= 200 && code < 400 ? c.getInputStream() : c.getErrorStream());
110+
if (code < 200 || code >= 400) throw new Exception("HTTP " + code);
111+
return text.isEmpty() ? "{}" : text;
112+
} finally {
113+
c.disconnect();
114+
}
115+
}
116+
117+
private static String readAll(InputStream is) throws Exception {
118+
if (is == null) return "";
119+
ByteArrayOutputStream buf = new ByteArrayOutputStream();
120+
byte[] chunk = new byte[4096];
121+
int n;
122+
while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n);
123+
is.close();
124+
return buf.toString(StandardCharsets.UTF_8.name());
125+
}
126+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
4848
i.putExtra(SetupLibraryActivity.EXTRA_BACKUP_RESTORE, true);
4949
ctx.startActivity(i);
5050
});
51+
// ADFA-5044: manage the box's per-service admin sign-ins (Books/Calibre-Web, Courses/Kolibri).
52+
SettingsUi.row(ctx, list, getString(R.string.k2go_settings_authentication), getString(R.string.k2go_settings_authentication_sub), null, v -> openSub("authentication"));
5153
SettingsUi.row(ctx, list, getString(R.string.k2go_settings_advanced), getString(R.string.k2go_settings_advanced_sub), null, v -> openSub("advanced"));
5254

5355
buildFooter(ctx, footer);

0 commit comments

Comments
 (0)