Fix profile PIN verification and restore TV tab underline style.

verifyPassword args were swapped after the avatar rewrite, causing argon2 to throw on PIN select.
This commit is contained in:
Jos Vooges | STH 2026-09-13 03:12:32 +02:00
parent e4aba1950b
commit 82742a1ea3
3 changed files with 76 additions and 60 deletions

View file

@ -20,8 +20,8 @@ android {
applicationId = "nl.vonas.mediacluster.tv"
minSdk = 24
targetSdk = 36
versionCode = 105
versionName = "0.14.42"
versionCode = 106
versionName = "0.14.43"
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")

View file

@ -8,13 +8,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
@ -54,15 +52,16 @@ import androidx.compose.ui.layout.ContentScale
/**
* Berekende hoogte van de Home overlay-topnav (geen subtabs).
*
* TopNavBar overlay-row (compact, zonder underline):
* - vertical padding: 10.dp + 10.dp
* - tab-tekst ~21.dp → veilig 56.dp
* TopNavBar overlay-row:
* - vertical padding: 14.dp + 14.dp
* - tab-inhoud: 8.dp + ~21.dp tekst + 4.dp + 3.dp underline + 8.dp ≈ 44.dp
* - totaal ≈ 72.dp; TV Surface min. interactive ≈ 48.dp → veilig 76.dp
*/
object HomeChrome {
val TopNavHeight = 56.dp
val TopNavHeight = 76.dp
/** Lucht tussen onderkant menu en badge. */
val BadgeGap = 12.dp
val BadgeTop = TopNavHeight + BadgeGap
val BadgeTop = TopNavHeight + BadgeGap // 88.dp
/** Horizontale schermmarge (links = rechts). */
val EdgeInset = 22.dp
}
@ -119,7 +118,7 @@ fun TopNavBar(
compact -> 0.42f
else -> 1f
}
val vPad = if (compact && !overlay) 6.dp else 10.dp
val vPad = if (compact && !overlay) 8.dp else 14.dp
val downLink = Modifier.then(
if (contentDownFocus != null) {
Modifier.focusProperties { down = contentDownFocus }
@ -178,16 +177,13 @@ fun TopNavBar(
color = McColors.Brand,
fontSize = if (compact && !overlay) 22.sp else 28.sp,
fontWeight = FontWeight.Black,
modifier = Modifier
.height(if (compact && !overlay) 32.dp else 36.dp)
.padding(end = 12.dp)
.wrapContentHeight(Alignment.CenterVertically),
modifier = Modifier.padding(end = 16.dp),
)
textTabs.forEach { tab ->
val active = selected == tab
val color by animateColorAsState(
when {
active -> McColors.Brand
active -> Color.White
overlay -> Color(0xFFE8E8E8)
else -> Color(0xFF808080)
},
@ -196,24 +192,21 @@ fun TopNavBar(
val fr = tabFocus[tab]
Surface(
onClick = { onSelect(tab) },
modifier = downLink
.height(if (compact && !overlay) 32.dp else 36.dp)
.then(if (fr != null) Modifier.focusRequester(fr) else Modifier),
modifier = downLink.then(
if (fr != null) Modifier.focusRequester(fr) else Modifier,
),
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.04f),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f),
colors = ClickableSurfaceDefaults.colors(
containerColor = Color.Transparent,
focusedContainerColor = Color(0x33FFFFFF),
containerColor = if (active) Color(0x33FFFFFF) else Color.Transparent,
focusedContainerColor = Color(0x44FFFFFF),
contentColor = color,
// Actieve tab blijft rood, ook met focus
focusedContentColor = if (active) McColors.Brand else Color.White,
focusedContentColor = Color.White,
),
) {
Box(
Modifier
.fillMaxHeight()
.padding(horizontal = 8.dp),
contentAlignment = Alignment.Center,
Column(
Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
tab.label,
@ -221,6 +214,14 @@ fun TopNavBar(
fontSize = 16.sp,
fontWeight = if (active) FontWeight.Bold else FontWeight.Medium,
)
Spacer(Modifier.height(4.dp))
Box(
Modifier
.width(if (active) 28.dp else 0.dp)
.height(3.dp)
.clip(RoundedCornerShape(2.dp))
.background(if (active) McColors.Brand else Color.Transparent),
)
}
}
}
@ -228,29 +229,33 @@ fun TopNavBar(
val searchActive = selected == MainTab.Search
Surface(
onClick = onSearchClick,
modifier = downLink
.height(if (compact && !overlay) 32.dp else 36.dp)
.focusRequester(searchFocus),
modifier = downLink.focusRequester(searchFocus),
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.04f),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f),
colors = ClickableSurfaceDefaults.colors(
containerColor = Color.Transparent,
focusedContainerColor = Color(0x33FFFFFF),
contentColor = if (searchActive) McColors.Brand else Color.White,
focusedContentColor = if (searchActive) McColors.Brand else Color.White,
containerColor = if (searchActive) Color(0x33FFFFFF) else Color.Transparent,
focusedContainerColor = Color(0x44FFFFFF),
contentColor = Color.White,
focusedContentColor = Color.White,
),
) {
Box(
Modifier
.fillMaxHeight()
.padding(horizontal = 8.dp),
contentAlignment = Alignment.Center,
Column(
Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = "Zoeken",
modifier = Modifier.size(16.dp),
tint = Color.Unspecified,
tint = Color.White,
)
Spacer(Modifier.height(4.dp))
Box(
Modifier
.width(if (searchActive) 18.dp else 0.dp)
.height(3.dp)
.clip(RoundedCornerShape(2.dp))
.background(if (searchActive) McColors.Brand else Color.Transparent),
)
}
}
@ -259,29 +264,33 @@ fun TopNavBar(
val settingsActive = selected == MainTab.Settings
Surface(
onClick = onSettingsClick,
modifier = downLink
.height(if (compact && !overlay) 32.dp else 36.dp)
.focusRequester(settingsFocus),
modifier = downLink.focusRequester(settingsFocus),
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.04f),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f),
colors = ClickableSurfaceDefaults.colors(
containerColor = Color.Transparent,
focusedContainerColor = Color(0x33FFFFFF),
contentColor = if (settingsActive) McColors.Brand else Color.White,
focusedContentColor = if (settingsActive) McColors.Brand else Color.White,
containerColor = if (settingsActive) Color(0x33FFFFFF) else Color.Transparent,
focusedContainerColor = Color(0x44FFFFFF),
contentColor = Color.White,
focusedContentColor = Color.White,
),
) {
Box(
Modifier
.fillMaxHeight()
.padding(horizontal = 8.dp),
contentAlignment = Alignment.Center,
Column(
Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = "Instellingen",
modifier = Modifier.size(16.dp),
tint = Color.Unspecified,
tint = Color.White,
)
Spacer(Modifier.height(4.dp))
Box(
Modifier
.width(if (settingsActive) 18.dp else 0.dp)
.height(3.dp)
.clip(RoundedCornerShape(2.dp))
.background(if (settingsActive) McColors.Brand else Color.Transparent),
)
}
}

View file

@ -388,14 +388,21 @@ export class ViewerProfileService {
let bucket = pinAttempts.get(key);
if (!bucket || bucket.resetAt < now) {
bucket = { count: 0, resetAt: now + PIN_ATTEMPT_WINDOW_MS };
pinAttempts.set(key, bucket);
}
if (bucket.count >= PIN_MAX_ATTEMPTS) {
throw new AppError("RATE_LIMIT", "Te veel PIN-pogingen, probeer later", 429);
throw new AppError(
"RATE_LIMITED",
"Te veel PIN-pogingen. Probeer later opnieuw.",
429
);
}
const ok = pin ? await verifyPassword(String(pin), profile.pinHash) : false;
const ok = pin
? await verifyPassword(profile.pinHash, String(pin).replace(/\D/g, ""))
: false;
if (!ok) {
bucket.count += 1;
pinAttempts.set(key, bucket);
throw new AppError("FORBIDDEN", "Onjuiste PIN", 403);
}
pinAttempts.delete(key);
@ -403,7 +410,7 @@ export class ViewerProfileService {
await prisma.viewerDevice.update({
where: { id: deviceId },
data: { activeProfileId: profileId, lastSeenAt: new Date() },
data: { activeProfileId: profile.id, lastSeenAt: new Date() },
});
return { profile: publicProfile(profile) };
}