Standalone Shonar Desktop: vendor portable sources + local engine; decouple from ~/Projects/Shonar
- shared/ = portable Android-origin sources vendored from deferred/desktop-server (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only) - backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the old checkout, PYTHONPATH pins THIS backend's code over any editable install - repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh watches shared/ + backend/ - Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010, self-migrates, /healthz ok
This commit is contained in:
commit
76c867fca4
136 changed files with 21099 additions and 0 deletions
83
shared/com/shonar/provider/ShonarAuthStore.kt
Normal file
83
shared/com/shonar/provider/ShonarAuthStore.kt
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package com.shonar.provider
|
||||
|
||||
import com.shonar.settings.SettingsStore
|
||||
|
||||
/**
|
||||
* Secure persistence for the custom SHONAR server session.
|
||||
*
|
||||
* Stored (all in the Keystore-backed secure store, never in Room or logs):
|
||||
* - base URL origin (e.g. https://shonar.example.com)
|
||||
* - account email (hint only)
|
||||
* - access token + expiry
|
||||
* - refresh token + device id
|
||||
*
|
||||
* The user's password is NEVER stored here: it lives in memory for exactly
|
||||
* one login call (see [ProviderCredential.ShonarLogin]).
|
||||
*
|
||||
* Rotation discipline (backend M1 has refresh reuse detection): every
|
||||
* successful refresh overwrites the stored pair immediately, so a stored
|
||||
* refresh token is always the newest one the server has issued.
|
||||
*/
|
||||
class ShonarAuthStore(private val secure: SettingsStore) {
|
||||
|
||||
suspend fun save(session: ShonarSession) {
|
||||
secure.putString(KEY_BASE_URL, session.baseUrl)
|
||||
secure.putString(KEY_EMAIL, session.email)
|
||||
secure.putString(KEY_ACCESS, session.accessToken)
|
||||
secure.putString(KEY_REFRESH, session.refreshToken)
|
||||
secure.putString(KEY_EXPIRES_AT, session.expiresAtEpochSec.toString())
|
||||
if (session.deviceId != null) secure.putString(KEY_DEVICE_ID, session.deviceId)
|
||||
else secure.remove(KEY_DEVICE_ID)
|
||||
}
|
||||
|
||||
suspend fun load(): ShonarSession? {
|
||||
val baseUrl = secure.getString(KEY_BASE_URL) ?: return null
|
||||
val access = secure.getString(KEY_ACCESS) ?: return null
|
||||
val refresh = secure.getString(KEY_REFRESH) ?: return null
|
||||
return ShonarSession(
|
||||
baseUrl = baseUrl,
|
||||
email = secure.getString(KEY_EMAIL).orEmpty(),
|
||||
accessToken = access,
|
||||
refreshToken = refresh,
|
||||
expiresAtEpochSec = secure.getString(KEY_EXPIRES_AT)?.toLongOrNull(),
|
||||
deviceId = secure.getString(KEY_DEVICE_ID),
|
||||
)
|
||||
}
|
||||
|
||||
/** Drop tokens but keep the URL + email so re-login is one step. */
|
||||
suspend fun clearTokens() {
|
||||
secure.remove(KEY_ACCESS)
|
||||
secure.remove(KEY_REFRESH)
|
||||
secure.remove(KEY_EXPIRES_AT)
|
||||
secure.remove(KEY_DEVICE_ID)
|
||||
}
|
||||
|
||||
/** Forget everything, including which server was configured. */
|
||||
suspend fun clearAll() {
|
||||
secure.remove(KEY_BASE_URL)
|
||||
secure.remove(KEY_EMAIL)
|
||||
clearTokens()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFIX = "provider.custom-shonar."
|
||||
const val KEY_BASE_URL = PREFIX + "base_url"
|
||||
const val KEY_EMAIL = PREFIX + "email"
|
||||
const val KEY_ACCESS = PREFIX + "access_token"
|
||||
const val KEY_REFRESH = PREFIX + "refresh_token"
|
||||
const val KEY_EXPIRES_AT = PREFIX + "expires_at"
|
||||
const val KEY_DEVICE_ID = PREFIX + "device_id"
|
||||
}
|
||||
}
|
||||
|
||||
/** In-memory session handed between login/refresh calls and the store. */
|
||||
data class ShonarSession(
|
||||
val baseUrl: String,
|
||||
val email: String,
|
||||
val accessToken: String,
|
||||
val refreshToken: String,
|
||||
val expiresAtEpochSec: Long?,
|
||||
val deviceId: String?,
|
||||
) {
|
||||
override fun toString(): String = "ShonarSession(server=$baseUrl, account=$email, [redacted])"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue