Use an 8-digit token for each car so it is easy to share.
This commit is contained in:
1 parent
0224e8f395
commit
b95cbefeab
6 files changed
+42
-15
No files matched your search
@@ -12,7 +12,7 @@ The gear icon opens **Car**. A new install already has the server address filled
|
||||
|
||||
**Sync on its own** sends a save right away and checks about every 30 seconds while the app is open. It stays quiet while the app is closed. **Sync now** sends every car on the phone.
|
||||
|
||||
Each car has its own token, shown on the **Car** screen. **Share token** sends that string to another phone. On the other phone, paste it under **Add a car from a token**. That phone then syncs that car. A different car gets a different token.
|
||||
Each car has its own 8-digit token, shown on the **Car** screen. **Share token** sends those digits to another phone. On the other phone, paste it under **Add a car from a token**. That phone then syncs that car. A different car gets a different token.
|
||||
|
||||
**Remove from this phone** takes the car off that phone only. The other phone and the server keep it.
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT INTO cars (carName, raceClass, discipline, token, updatedAt, syncRev, deleted, pending)
|
||||
VALUES ('$DEFAULT_CAR', '$DEFAULT_CLASS', '$DEFAULT_DISCIPLINE', lower(hex(randomblob(32))), 1, 0, 0, 1)
|
||||
VALUES ('$DEFAULT_CAR', '$DEFAULT_CLASS', '$DEFAULT_DISCIPLINE', printf('%08d', abs(random()) % 100000000), 1, 0, 0, 1)
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,15 @@ interface RaceDao {
|
||||
@Query("SELECT * FROM cars WHERE id = :id")
|
||||
suspend fun carById(id: Long): Car?
|
||||
|
||||
@Query("UPDATE race_nights SET pending = 1 WHERE carId = :carId")
|
||||
suspend fun markNightsPending(carId: Long)
|
||||
|
||||
@Query("UPDATE runs SET pending = 1 WHERE nightId IN (SELECT id FROM race_nights WHERE carId = :carId)")
|
||||
suspend fun markRunsPending(carId: Long)
|
||||
|
||||
@Query("UPDATE setup_sheets SET pending = 1 WHERE carId = :carId")
|
||||
suspend fun markSheetsPending(carId: Long)
|
||||
|
||||
@Query("SELECT * FROM cars WHERE token = :token LIMIT 1")
|
||||
suspend fun carByToken(token: String): Car?
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.racingapp.pitlog.data
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -24,28 +25,39 @@ suspend fun ensureCars(dao: RaceDao, preferences: SyncPreferences) {
|
||||
preferences.selectedCarId = id
|
||||
return
|
||||
}
|
||||
val first = cars.firstOrNull { !it.deleted } ?: cars.first()
|
||||
if (first.token.isBlank()) {
|
||||
val kept = preferences.token.trim().ifBlank { newCarToken() }
|
||||
val rev = if (kept == preferences.token.trim() && first.syncRev == 0L) preferences.rev else first.syncRev
|
||||
dao.updateCar(first.copy(token = kept, syncRev = rev, pending = true))
|
||||
for (car in cars) {
|
||||
if (isEightDigitToken(car.token)) continue
|
||||
val source = car.token.ifBlank { preferences.token.trim() }
|
||||
val next = if (source.isBlank()) newCarToken() else eightDigitToken(source)
|
||||
dao.updateCar(car.copy(token = next, syncRev = 0, pending = true))
|
||||
dao.markNightsPending(car.id)
|
||||
dao.markRunsPending(car.id)
|
||||
dao.markSheetsPending(car.id)
|
||||
}
|
||||
val first = cars.firstOrNull { !it.deleted } ?: cars.first()
|
||||
val selected = preferences.selectedCarId
|
||||
if (selected == 0L || cars.none { it.id == selected && !it.deleted }) {
|
||||
preferences.selectedCarId = first.id
|
||||
}
|
||||
}
|
||||
|
||||
fun newCarToken(): String {
|
||||
val bytes = ByteArray(32)
|
||||
SecureRandom().nextBytes(bytes)
|
||||
return bytes.joinToString("") { "%02x".format(it) }
|
||||
fun newCarToken(): String = "%08d".format(SecureRandom().nextInt(100_000_000))
|
||||
|
||||
fun eightDigitToken(current: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256").digest(current.toByteArray(Charsets.UTF_8))
|
||||
var value = 0L
|
||||
for (index in 0 until 4) {
|
||||
value = (value shl 8) or (digest[index].toLong() and 0xff)
|
||||
}
|
||||
return "%08d".format(value % 100_000_000L)
|
||||
}
|
||||
|
||||
fun isEightDigitToken(token: String): Boolean = token.length == 8 && token.all { it.isDigit() }
|
||||
|
||||
fun cleanCarToken(raw: String): String {
|
||||
val line = raw.trim().lineSequence().firstOrNull().orEmpty().trim()
|
||||
val withoutLabel = if (line.startsWith("token:", ignoreCase = true)) line.substringAfter(":").trim() else line
|
||||
return withoutLabel.lowercase()
|
||||
return withoutLabel.filter { !it.isWhitespace() }.lowercase()
|
||||
}
|
||||
|
||||
class Repository(
|
||||
@@ -125,7 +137,7 @@ class Repository(
|
||||
|
||||
suspend fun joinCar(raw: String): String {
|
||||
val token = cleanCarToken(raw)
|
||||
if (token.length < 16 || token.any { it !in '0'..'9' && it !in 'a'..'f' }) {
|
||||
if (!isEightDigitToken(token)) {
|
||||
return "That token doesn't look right."
|
||||
}
|
||||
val existing = dao.carByToken(token)
|
||||
|
||||
@@ -230,7 +230,7 @@ fun SettingsScreen(onBack: () -> Unit) {
|
||||
)
|
||||
SectionLabel("This car's token")
|
||||
Text(
|
||||
"Share this token with another phone, then on that phone use Add a car from a token. That phone syncs this car. A different car gets a different token, so it can sync with different phones.",
|
||||
"This token is 8 digits. Share it with another phone, then on that phone use Add a car from a token. That phone syncs this car. A different car gets a different token.",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
SelectionContainer {
|
||||
|
||||
+7
-1
@@ -52,6 +52,12 @@ def listen_port():
|
||||
return 8787
|
||||
|
||||
|
||||
def token_ok(bearer):
|
||||
if len(bearer) == 8 and bearer.isdigit():
|
||||
return True
|
||||
return len(bearer) >= 16 and all(character in "0123456789abcdef" for character in bearer)
|
||||
|
||||
|
||||
def legacy_token():
|
||||
inline = os.environ.get("PITLOG_TOKEN", "").strip().lower()
|
||||
if inline:
|
||||
@@ -386,7 +392,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
header = self.headers.get("Authorization", "")
|
||||
bearer = header[7:].strip().lower() if header.startswith("Bearer ") else ""
|
||||
if len(bearer) < 16 or any(character not in "0123456789abcdef" for character in bearer):
|
||||
if not token_ok(bearer):
|
||||
self._send(401, {"error": "The token was refused."})
|
||||
return
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
|
||||
Reference in new issue
Block a user