Incorporate koreader registration

This commit is contained in:
jpirnay
2026-03-23 12:35:01 +01:00
parent 68bf80e6b9
commit 75350b0a3e
6 changed files with 159 additions and 23 deletions
+7 -2
View File
@@ -102,14 +102,19 @@ STR_AUTHENTICATING: "Authenticating..."
STR_AUTH_SUCCESS: "Successfully authenticated!" STR_AUTH_SUCCESS: "Successfully authenticated!"
STR_KOREADER_AUTH: "KOReader Auth" STR_KOREADER_AUTH: "KOReader Auth"
STR_SYNC_READY: "KOReader sync is ready to use" STR_SYNC_READY: "KOReader sync is ready to use"
STR_AUTH_FAILED: "Authentication Failed" STR_AUTH_FAILED: "Authentication failed"
STR_REGISTER: "Register"
STR_REGISTERING: "Registering..."
STR_REGISTER_SUCCESS: "Account created successfully!"
STR_REGISTER_FAILED: "Registration failed"
STR_USERNAME_TAKEN: "Username already taken"
STR_DONE: "Done" STR_DONE: "Done"
STR_CLEAR_CACHE_WARNING_1: "This will clear all cached book data." STR_CLEAR_CACHE_WARNING_1: "This will clear all cached book data."
STR_CLEAR_CACHE_WARNING_2: "All reading progress will be lost!" STR_CLEAR_CACHE_WARNING_2: "All reading progress will be lost!"
STR_CLEAR_CACHE_WARNING_3: "Books will need to be re-indexed" STR_CLEAR_CACHE_WARNING_3: "Books will need to be re-indexed"
STR_CLEAR_CACHE_WARNING_4: "when opened again." STR_CLEAR_CACHE_WARNING_4: "when opened again."
STR_CLEARING_CACHE: "Clearing cache..." STR_CLEARING_CACHE: "Clearing cache..."
STR_CACHE_CLEARED: "Cache Cleared" STR_CACHE_CLEARED: "Cache cleared"
STR_ITEMS_REMOVED: "items removed" STR_ITEMS_REMOVED: "items removed"
STR_FAILED_LOWER: "failed" STR_FAILED_LOWER: "failed"
STR_CLEAR_CACHE_FAILED: "Failed to clear cache" STR_CLEAR_CACHE_FAILED: "Failed to clear cache"
+65
View File
@@ -28,6 +28,67 @@ void addAuthHeaders(HTTPClient& http) {
bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; } bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; }
} // namespace } // namespace
KOReaderSyncClient::Error KOReaderSyncClient::registerUser() {
if (!KOREADER_STORE.hasCredentials()) {
LOG_DBG("KOSync", "No credentials configured");
return NO_CREDENTIALS;
}
std::string url = KOREADER_STORE.getBaseUrl() + "/users/create";
LOG_DBG("KOSync", "Registering user: %s", url.c_str());
HTTPClient http;
std::unique_ptr<WiFiClientSecure> secureClient;
WiFiClient plainClient;
if (isHttpsUrl(url)) {
secureClient.reset(new WiFiClientSecure);
secureClient->setInsecure();
http.begin(*secureClient, url.c_str());
} else {
http.begin(plainClient, url.c_str());
}
http.addHeader("Accept", "application/vnd.koreader.v1+json");
http.addHeader("Content-Type", "application/json");
JsonDocument doc;
doc["username"] = KOREADER_STORE.getUsername();
doc["password"] = KOREADER_STORE.getMd5Password();
std::string body;
serializeJson(doc, body);
LOG_DBG("KOSync", "Register request body: <redacted credentials>");
const int httpCode = http.POST(body.c_str());
const String responseBody = http.getString();
http.end();
LOG_DBG("KOSync", "Register response: %d | body: %s", httpCode, responseBody.c_str());
if (httpCode == 201) {
return OK;
} else if (httpCode == 200) {
// Some server implementations return 200 when the user already exists
return USER_EXISTS;
} else if (httpCode == 402) {
// Both "user already exists" (error 2002) and "registration disabled" (error 2005)
// return HTTP 402 on the original kosync server. Distinguish them by body text.
String lowerBody = responseBody;
lowerBody.toLowerCase();
if (lowerBody.indexOf("already") >= 0) {
return USER_EXISTS;
}
return REGISTRATION_DISABLED;
} else if (httpCode == 409) {
// korrosync returns 409 for existing users
return USER_EXISTS;
} else if (httpCode < 0) {
return NETWORK_ERROR;
}
return SERVER_ERROR;
}
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
LOG_DBG("KOSync", "No credentials configured"); LOG_DBG("KOSync", "No credentials configured");
@@ -195,6 +256,10 @@ const char* KOReaderSyncClient::errorString(Error error) {
return "JSON parse error"; return "JSON parse error";
case NOT_FOUND: case NOT_FOUND:
return "No progress found"; return "No progress found";
case USER_EXISTS:
return "Username is already taken";
case REGISTRATION_DISABLED:
return "Registration is disabled on this server";
default: default:
return "Unknown error"; return "Unknown error";
} }
+22 -4
View File
@@ -19,9 +19,10 @@ struct KOReaderProgress {
* Base URL: https://sync.koreader.rocks:443/ * Base URL: https://sync.koreader.rocks:443/
* *
* API Endpoints: * API Endpoints:
* GET /users/auth - Authenticate (validate credentials) * POST /users/create - Register a new user
* GET /syncs/progress/:document - Get progress for a document * GET /users/auth - Authenticate (validate credentials)
* PUT /syncs/progress - Update progress for a document * GET /syncs/progress/:document - Get progress for a document
* PUT /syncs/progress - Update progress for a document
* *
* Authentication: * Authentication:
* x-auth-user: username * x-auth-user: username
@@ -29,7 +30,24 @@ struct KOReaderProgress {
*/ */
class KOReaderSyncClient { class KOReaderSyncClient {
public: public:
enum Error { OK = 0, NO_CREDENTIALS, NETWORK_ERROR, AUTH_FAILED, SERVER_ERROR, JSON_ERROR, NOT_FOUND }; enum Error {
OK = 0,
NO_CREDENTIALS,
NETWORK_ERROR,
AUTH_FAILED,
SERVER_ERROR,
JSON_ERROR,
NOT_FOUND,
USER_EXISTS,
REGISTRATION_DISABLED
};
/**
* Register a new user account with the sync server.
* Uses credentials already stored in KOReaderCredentialStore.
* @return OK on success, USER_EXISTS if taken, REGISTRATION_DISABLED if server disallows it
*/
static Error registerUser();
/** /**
* Authenticate with the sync server (validate credentials). * Authenticate with the sync server (validate credentials).
@@ -24,12 +24,21 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
{ {
RenderLock lock(*this); RenderLock lock(*this);
state = AUTHENTICATING; if (mode == Mode::REGISTER) {
statusMessage = tr(STR_AUTHENTICATING); state = REGISTERING;
statusMessage = tr(STR_REGISTERING);
} else {
state = AUTHENTICATING;
statusMessage = tr(STR_AUTHENTICATING);
}
} }
requestUpdate(); requestUpdate();
performAuthentication(); if (mode == Mode::REGISTER) {
performRegistration();
} else {
performAuthentication();
}
} }
void KOReaderAuthActivity::performAuthentication() { void KOReaderAuthActivity::performAuthentication() {
@@ -48,6 +57,25 @@ void KOReaderAuthActivity::performAuthentication() {
requestUpdate(); requestUpdate();
} }
void KOReaderAuthActivity::performRegistration() {
const auto result = KOReaderSyncClient::registerUser();
{
RenderLock lock(*this);
if (result == KOReaderSyncClient::OK) {
state = SUCCESS;
statusMessage = tr(STR_REGISTER_SUCCESS);
} else if (result == KOReaderSyncClient::USER_EXISTS) {
state = USER_EXISTS;
errorMessage = KOReaderSyncClient::errorString(result);
} else {
state = FAILED;
errorMessage = KOReaderSyncClient::errorString(result);
}
}
requestUpdate();
}
void KOReaderAuthActivity::onEnter() { void KOReaderAuthActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
@@ -83,13 +111,18 @@ void KOReaderAuthActivity::render(RenderLock&&) {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
if (state == AUTHENTICATING) { if (state == AUTHENTICATING || state == REGISTERING) {
renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str());
} else if (state == SUCCESS) { } else if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD); const char* successMsg = (mode == Mode::REGISTER) ? tr(STR_REGISTER_SUCCESS) : tr(STR_AUTH_SUCCESS);
renderer.drawCenteredText(UI_10_FONT_ID, top, successMsg, true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY)); renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY));
} else if (state == USER_EXISTS) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_USERNAME_TAKEN), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str());
} else if (state == FAILED) { } else if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD); const char* failedMsg = (mode == Mode::REGISTER) ? tr(STR_REGISTER_FAILED) : tr(STR_AUTH_FAILED);
renderer.drawCenteredText(UI_10_FONT_ID, top, failedMsg, true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str());
} }
@@ -99,7 +132,7 @@ void KOReaderAuthActivity::render(RenderLock&&) {
} }
void KOReaderAuthActivity::loop() { void KOReaderAuthActivity::loop() {
if (state == SUCCESS || state == FAILED) { if (state == SUCCESS || state == FAILED || state == USER_EXISTS) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back) || if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
finish(); finish();
+10 -6
View File
@@ -5,27 +5,31 @@
#include "activities/Activity.h" #include "activities/Activity.h"
/** /**
* Activity for testing KOReader credentials. * Activity for authenticating or registering a KOReader sync account.
* Connects to WiFi and authenticates with the KOReader sync server. * Connects to WiFi, then either logs in or creates a new account on the server.
*/ */
class KOReaderAuthActivity final : public Activity { class KOReaderAuthActivity final : public Activity {
public: public:
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) enum class Mode { LOGIN, REGISTER };
: Activity("KOReaderAuth", renderer, mappedInput) {}
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, Mode mode = Mode::LOGIN)
: Activity("KOReaderAuth", renderer, mappedInput), mode(mode) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; } bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING || state == REGISTERING; }
private: private:
enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED }; enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, REGISTERING, SUCCESS, FAILED, USER_EXISTS };
Mode mode;
State state = WIFI_SELECTION; State state = WIFI_SELECTION;
std::string statusMessage; std::string statusMessage;
std::string errorMessage; std::string errorMessage;
void onWifiSelectionComplete(bool success); void onWifiSelectionComplete(bool success);
void performAuthentication(); void performAuthentication();
void performRegistration();
}; };
@@ -13,9 +13,9 @@
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
constexpr int MENU_ITEMS = 5; constexpr int MENU_ITEMS = 6;
const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL,
StrId::STR_DOCUMENT_MATCHING, StrId::STR_AUTHENTICATE}; StrId::STR_DOCUMENT_MATCHING, StrId::STR_AUTHENTICATE, StrId::STR_REGISTER};
} // namespace } // namespace
void KOReaderSettingsActivity::onEnter() { void KOReaderSettingsActivity::onEnter() {
@@ -107,7 +107,16 @@ void KOReaderSettingsActivity::handleSelection() {
// Can't authenticate without credentials - just show message briefly // Can't authenticate without credentials - just show message briefly
return; return;
} }
startActivityForResult(std::make_unique<KOReaderAuthActivity>(renderer, mappedInput), [](const ActivityResult&) {}); startActivityForResult(std::make_unique<KOReaderAuthActivity>(renderer, mappedInput, KOReaderAuthActivity::Mode::LOGIN),
[](const ActivityResult&) {});
} else if (selectedIndex == 5) {
// Register
if (!KOREADER_STORE.hasCredentials()) {
return;
}
startActivityForResult(
std::make_unique<KOReaderAuthActivity>(renderer, mappedInput, KOReaderAuthActivity::Mode::REGISTER),
[](const ActivityResult&) {});
} }
} }
@@ -141,6 +150,8 @@ void KOReaderSettingsActivity::render(RenderLock&&) {
: std::string(tr(STR_BINARY)); : std::string(tr(STR_BINARY));
} else if (index == 4) { } else if (index == 4) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
} else if (index == 5) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
} }
return std::string(tr(STR_NOT_SET)); return std::string(tr(STR_NOT_SET));
}, },