mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-16 23:33:10 -06:00
fix(android): server-provisioned settings PIN replaces hardcoded 0000
- Remove stray brace that broke compilation (MainActivity line 985) - Server generates unique 6-digit PIN per device during pairing - PIN stored in encrypted SharedPreferences (ServerConfig.settingsPin) - Fallback: generate random PIN locally if server doesn't send one - Include settings_pin in device:paired on pair + reconnect - DB migration: settings_pin column on devices table - Hint changed from hardcoded 0000 to generic 'PIN' string
This commit is contained in:
parent
c7f1eed63f
commit
58f27d56e8
|
|
@ -77,8 +77,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
private val backTapTimes = mutableListOf<Long>()
|
private val backTapTimes = mutableListOf<Long>()
|
||||||
private var backTapRunnable: Runnable? = null
|
private var backTapRunnable: Runnable? = null
|
||||||
private val TAP_WINDOW_MS = 1800L
|
private val TAP_WINDOW_MS = 1800L
|
||||||
private val DEFAULT_SETTINGS_PIN = "0000"
|
|
||||||
private val PREF_SETTINGS_PIN = "settings_pin"
|
|
||||||
// Connection-failure auto-prompt threshold.
|
// Connection-failure auto-prompt threshold.
|
||||||
private var failureBannerShown = false
|
private var failureBannerShown = false
|
||||||
|
|
||||||
|
|
@ -852,12 +850,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showPinDialog() {
|
private fun showPinDialog() {
|
||||||
val prefs = getSharedPreferences("remote_display", MODE_PRIVATE)
|
|
||||||
val storedPin = prefs.getString(PREF_SETTINGS_PIN, DEFAULT_SETTINGS_PIN) ?: DEFAULT_SETTINGS_PIN
|
|
||||||
|
|
||||||
val input = EditText(this).apply {
|
val input = EditText(this).apply {
|
||||||
inputType = android.text.InputType.TYPE_CLASS_NUMBER or android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
inputType = android.text.InputType.TYPE_CLASS_NUMBER or android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||||
hint = DEFAULT_SETTINGS_PIN
|
hint = getString(R.string.settings_pin_hint)
|
||||||
setSingleLine()
|
setSingleLine()
|
||||||
}
|
}
|
||||||
val container = FrameLayout(this).apply {
|
val container = FrameLayout(this).apply {
|
||||||
|
|
@ -870,7 +865,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
.setTitle(getString(R.string.settings_pin_title))
|
.setTitle(getString(R.string.settings_pin_title))
|
||||||
.setView(container)
|
.setView(container)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (input.text.toString() == storedPin) {
|
if (input.text.toString() == config.settingsPin) {
|
||||||
showSettingsDialog()
|
showSettingsDialog()
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, getString(R.string.settings_pin_wrong), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.settings_pin_wrong), Toast.LENGTH_SHORT).show()
|
||||||
|
|
@ -982,7 +977,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToProvisioning(url: String? = null) {
|
private fun navigateToProvisioning(url: String? = null) {
|
||||||
try { wsService?.disconnect() } catch (_: Exception) {}
|
try { wsService?.disconnect() } catch (_: Exception) {}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,20 @@ class ServerConfig(context: Context) {
|
||||||
get() = prefs.getString("device_name", "Unnamed Display") ?: "Unnamed Display"
|
get() = prefs.getString("device_name", "Unnamed Display") ?: "Unnamed Display"
|
||||||
set(value) = prefs.edit().putString("device_name", value).apply()
|
set(value) = prefs.edit().putString("device_name", value).apply()
|
||||||
|
|
||||||
|
// Provisioned by the server during pairing — a unique 6-digit PIN for the hidden
|
||||||
|
// settings menu on each device. If the server doesn't send one (backward compat),
|
||||||
|
// generate a random PIN locally on first access so every device still has a unique gate.
|
||||||
|
var settingsPin: String
|
||||||
|
get() {
|
||||||
|
val stored = prefs.getString("settings_pin", null)
|
||||||
|
if (stored != null) return stored
|
||||||
|
// First access, no server-provided PIN — generate one locally
|
||||||
|
val generated = (100000..999999).random().toString()
|
||||||
|
prefs.edit().putString("settings_pin", generated).apply()
|
||||||
|
return generated
|
||||||
|
}
|
||||||
|
set(value) = prefs.edit().putString("settings_pin", value).apply()
|
||||||
|
|
||||||
val isProvisioned: Boolean
|
val isProvisioned: Boolean
|
||||||
get() = deviceId.isNotEmpty() && serverUrl.isNotEmpty()
|
get() = deviceId.isNotEmpty() && serverUrl.isNotEmpty()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,13 @@ class WebSocketService : Service() {
|
||||||
val name = data.optString("name", "Display")
|
val name = data.optString("name", "Display")
|
||||||
config.setPaired(true)
|
config.setPaired(true)
|
||||||
config.deviceName = name
|
config.deviceName = name
|
||||||
|
// Server-provisioned settings PIN — unique per device, stored encrypted.
|
||||||
|
// If the server doesn't send one (old server), ServerConfig generates a
|
||||||
|
// random PIN on first access so the gate is never left with a hardcoded default.
|
||||||
|
val pin = data.optString("settings_pin", "")
|
||||||
|
if (pin.isNotEmpty()) {
|
||||||
|
config.settingsPin = pin
|
||||||
|
}
|
||||||
Log.i("WebSocketService", "Paired as: $name")
|
Log.i("WebSocketService", "Paired as: $name")
|
||||||
handler.post { try { onPaired?.invoke(id, name) } catch (e: Throwable) { Log.e("WebSocketService", "onPaired cb: ${e.message}") } }
|
handler.post { try { onPaired?.invoke(id, name) } catch (e: Throwable) { Log.e("WebSocketService", "onPaired cb: ${e.message}") } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<!-- Hidden settings menu (triggered by 2x BACK or ESC, PIN-gated) -->
|
<!-- Hidden settings menu (triggered by 2x BACK or ESC, PIN-gated) -->
|
||||||
<string name="settings_pin_title">Enter PIN</string>
|
<string name="settings_pin_title">Enter PIN</string>
|
||||||
<string name="settings_pin_wrong">Wrong PIN</string>
|
<string name="settings_pin_wrong">Wrong PIN</string>
|
||||||
|
<string name="settings_pin_hint">PIN</string>
|
||||||
<string name="settings_title">Settings</string>
|
<string name="settings_title">Settings</string>
|
||||||
<string name="settings_change_server">Change server URL</string>
|
<string name="settings_change_server">Change server URL</string>
|
||||||
<string name="settings_reconfigure">Reconfigure device (re-pair)</string>
|
<string name="settings_reconfigure">Reconfigure device (re-pair)</string>
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,9 @@ const migrations = [
|
||||||
// register gate on its next reconnect (no restart). Hand-settable by direct SQLite:
|
// register gate on its next reconnect (no restart). Hand-settable by direct SQLite:
|
||||||
// UPDATE devices SET blocked = 1 WHERE id = '<device_id>'; (0 to unblock)
|
// UPDATE devices SET blocked = 1 WHERE id = '<device_id>'; (0 to unblock)
|
||||||
"ALTER TABLE devices ADD COLUMN blocked INTEGER NOT NULL DEFAULT 0",
|
"ALTER TABLE devices ADD COLUMN blocked INTEGER NOT NULL DEFAULT 0",
|
||||||
|
// settings_pin: 6-digit PIN for the in-app hidden settings menu, provisioned by
|
||||||
|
// the server during pairing so each device gets a unique PIN (never a hardcoded default).
|
||||||
|
"ALTER TABLE devices ADD COLUMN settings_pin TEXT",
|
||||||
];
|
];
|
||||||
// Apply each ALTER idempotently. A "duplicate column name" / "already exists"
|
// Apply each ALTER idempotently. A "duplicate column name" / "already exists"
|
||||||
// error means the column is already present (expected on a migrated DB) - benign.
|
// error means the column is already present (expected on a migrated DB) - benign.
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ CREATE TABLE IF NOT EXISTS devices (
|
||||||
user_id TEXT REFERENCES users(id),
|
user_id TEXT REFERENCES users(id),
|
||||||
name TEXT NOT NULL DEFAULT 'Unnamed Display',
|
name TEXT NOT NULL DEFAULT 'Unnamed Display',
|
||||||
pairing_code TEXT UNIQUE,
|
pairing_code TEXT UNIQUE,
|
||||||
|
settings_pin TEXT,
|
||||||
status TEXT NOT NULL DEFAULT 'offline',
|
status TEXT NOT NULL DEFAULT 'offline',
|
||||||
blocked INTEGER NOT NULL DEFAULT 0,
|
blocked INTEGER NOT NULL DEFAULT 0,
|
||||||
last_heartbeat INTEGER,
|
last_heartbeat INTEGER,
|
||||||
|
|
|
||||||
|
|
@ -745,15 +745,18 @@ app.post('/api/provision/pair', requireAuth, resolveTenancy, checkDeviceLimit, (
|
||||||
pairLockout.reset(ip); // a valid claim forgives prior failed attempts from this IP
|
pairLockout.reset(ip); // a valid claim forgives prior failed attempts from this IP
|
||||||
|
|
||||||
const deviceName = name || 'Display ' + (db.prepare('SELECT COUNT(*) as count FROM devices WHERE user_id = ?').get(req.user.id).count + 1);
|
const deviceName = name || 'Display ' + (db.prepare('SELECT COUNT(*) as count FROM devices WHERE user_id = ?').get(req.user.id).count + 1);
|
||||||
db.prepare("UPDATE devices SET pairing_code = NULL, name = ?, user_id = ?, workspace_id = ?, status = 'online', updated_at = strftime('%s','now') WHERE id = ?")
|
// Generate a random 6-digit PIN for the hidden settings menu — each device gets a
|
||||||
.run(deviceName, req.user.id, req.workspaceId, device.id);
|
// unique PIN provisioned by the server (never a hardcoded default).
|
||||||
|
const settingsPin = String(Math.floor(100000 + Math.random() * 900000));
|
||||||
|
db.prepare("UPDATE devices SET pairing_code = NULL, name = ?, user_id = ?, workspace_id = ?, status = 'online', settings_pin = ?, updated_at = strftime('%s','now') WHERE id = ?")
|
||||||
|
.run(deviceName, req.user.id, req.workspaceId, settingsPin, device.id);
|
||||||
|
|
||||||
// Link fingerprint to user
|
// Link fingerprint to user
|
||||||
db.prepare("UPDATE device_fingerprints SET user_id = ?, device_id = ? WHERE device_id = ?")
|
db.prepare("UPDATE device_fingerprints SET user_id = ?, device_id = ? WHERE device_id = ?")
|
||||||
.run(req.user.id, device.id, device.id);
|
.run(req.user.id, device.id, device.id);
|
||||||
|
|
||||||
// Notify the device via WebSocket
|
// Notify the device via WebSocket
|
||||||
deviceNs.to(device.id).emit('device:paired', { device_id: device.id, name: deviceName });
|
deviceNs.to(device.id).emit('device:paired', { device_id: device.id, name: deviceName, settings_pin: settingsPin });
|
||||||
|
|
||||||
const updated = db.prepare('SELECT * FROM devices WHERE id = ?').get(device.id);
|
const updated = db.prepare('SELECT * FROM devices WHERE id = ?').get(device.id);
|
||||||
require('./lib/device-sanitize').stripDeviceSecrets(updated); // never leak device_token to clients
|
require('./lib/device-sanitize').stripDeviceSecrets(updated); // never leak device_token to clients
|
||||||
|
|
|
||||||
|
|
@ -401,7 +401,11 @@ module.exports = function setupDeviceSocket(io) {
|
||||||
socket.emit('device:registered', { device_id: existing.device_id, device_token: newToken, status: 'online' });
|
socket.emit('device:registered', { device_id: existing.device_id, device_token: newToken, status: 'online' });
|
||||||
// If device was already claimed by a user, tell the player it's paired
|
// If device was already claimed by a user, tell the player it's paired
|
||||||
if (oldDevice.user_id) {
|
if (oldDevice.user_id) {
|
||||||
socket.emit('device:paired', { name: oldDevice.name || 'Display' });
|
socket.emit('device:paired', {
|
||||||
|
device_id: oldDevice.id,
|
||||||
|
name: oldDevice.name || 'Display',
|
||||||
|
settings_pin: oldDevice.settings_pin || undefined
|
||||||
|
});
|
||||||
}
|
}
|
||||||
currentDeviceId = existing.device_id;
|
currentDeviceId = existing.device_id;
|
||||||
heartbeat.registerConnection(existing.device_id, socket.id);
|
heartbeat.registerConnection(existing.device_id, socket.id);
|
||||||
|
|
@ -532,7 +536,7 @@ module.exports = function setupDeviceSocket(io) {
|
||||||
// — never received it and sat on the Connect page forever showing the URL (Bold #143).
|
// — never received it and sat on the Connect page forever showing the URL (Bold #143).
|
||||||
// Re-send the exact event the client already listens for; no client change needed.
|
// Re-send the exact event the client already listens for; no client change needed.
|
||||||
if (device.user_id) {
|
if (device.user_id) {
|
||||||
socket.emit('device:paired', { device_id, name: device.name || 'Display' });
|
socket.emit('device:paired', { device_id, name: device.name || 'Display', settings_pin: device.settings_pin || undefined });
|
||||||
}
|
}
|
||||||
logDeviceStatus(device_id, 'online');
|
logDeviceStatus(device_id, 'online');
|
||||||
// Flush any commands/playlist-updates queued while this device was offline.
|
// Flush any commands/playlist-updates queued while this device was offline.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue