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.
|
**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.
|
**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(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO cars (carName, raceClass, discipline, token, updatedAt, syncRev, deleted, pending)
|
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(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,15 @@ interface RaceDao {
|
|||||||
@Query("SELECT * FROM cars WHERE id = :id")
|
@Query("SELECT * FROM cars WHERE id = :id")
|
||||||
suspend fun carById(id: Long): Car?
|
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")
|
@Query("SELECT * FROM cars WHERE token = :token LIMIT 1")
|
||||||
suspend fun carByToken(token: String): Car?
|
suspend fun carByToken(token: String): Car?
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.racingapp.pitlog.data
|
package com.racingapp.pitlog.data
|
||||||
|
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -24,28 +25,39 @@ suspend fun ensureCars(dao: RaceDao, preferences: SyncPreferences) {
|
|||||||
preferences.selectedCarId = id
|
preferences.selectedCarId = id
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val first = cars.firstOrNull { !it.deleted } ?: cars.first()
|
for (car in cars) {
|
||||||
if (first.token.isBlank()) {
|
if (isEightDigitToken(car.token)) continue
|
||||||
val kept = preferences.token.trim().ifBlank { newCarToken() }
|
val source = car.token.ifBlank { preferences.token.trim() }
|
||||||
val rev = if (kept == preferences.token.trim() && first.syncRev == 0L) preferences.rev else first.syncRev
|
val next = if (source.isBlank()) newCarToken() else eightDigitToken(source)
|
||||||
dao.updateCar(first.copy(token = kept, syncRev = rev, pending = true))
|
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
|
val selected = preferences.selectedCarId
|
||||||
if (selected == 0L || cars.none { it.id == selected && !it.deleted }) {
|
if (selected == 0L || cars.none { it.id == selected && !it.deleted }) {
|
||||||
preferences.selectedCarId = first.id
|
preferences.selectedCarId = first.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newCarToken(): String {
|
fun newCarToken(): String = "%08d".format(SecureRandom().nextInt(100_000_000))
|
||||||
val bytes = ByteArray(32)
|
|
||||||
SecureRandom().nextBytes(bytes)
|
fun eightDigitToken(current: String): String {
|
||||||
return bytes.joinToString("") { "%02x".format(it) }
|
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 {
|
fun cleanCarToken(raw: String): String {
|
||||||
val line = raw.trim().lineSequence().firstOrNull().orEmpty().trim()
|
val line = raw.trim().lineSequence().firstOrNull().orEmpty().trim()
|
||||||
val withoutLabel = if (line.startsWith("token:", ignoreCase = true)) line.substringAfter(":").trim() else line
|
val withoutLabel = if (line.startsWith("token:", ignoreCase = true)) line.substringAfter(":").trim() else line
|
||||||
return withoutLabel.lowercase()
|
return withoutLabel.filter { !it.isWhitespace() }.lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Repository(
|
class Repository(
|
||||||
@@ -125,7 +137,7 @@ class Repository(
|
|||||||
|
|
||||||
suspend fun joinCar(raw: String): String {
|
suspend fun joinCar(raw: String): String {
|
||||||
val token = cleanCarToken(raw)
|
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."
|
return "That token doesn't look right."
|
||||||
}
|
}
|
||||||
val existing = dao.carByToken(token)
|
val existing = dao.carByToken(token)
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ fun SettingsScreen(onBack: () -> Unit) {
|
|||||||
)
|
)
|
||||||
SectionLabel("This car's token")
|
SectionLabel("This car's token")
|
||||||
Text(
|
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,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
|
|||||||
+7
-1
@@ -52,6 +52,12 @@ def listen_port():
|
|||||||
return 8787
|
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():
|
def legacy_token():
|
||||||
inline = os.environ.get("PITLOG_TOKEN", "").strip().lower()
|
inline = os.environ.get("PITLOG_TOKEN", "").strip().lower()
|
||||||
if inline:
|
if inline:
|
||||||
@@ -386,7 +392,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
header = self.headers.get("Authorization", "")
|
header = self.headers.get("Authorization", "")
|
||||||
bearer = header[7:].strip().lower() if header.startswith("Bearer ") else ""
|
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."})
|
self._send(401, {"error": "The token was refused."})
|
||||||
return
|
return
|
||||||
length = int(self.headers.get("Content-Length", "0"))
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
|||||||
Reference in new issue
Block a user