|
| 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 | +} |
0 commit comments