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:
parent
e4aba1950b
commit
82742a1ea3
3 changed files with 76 additions and 60 deletions
|
|
@ -20,8 +20,8 @@ android {
|
||||||
applicationId = "nl.vonas.mediacluster.tv"
|
applicationId = "nl.vonas.mediacluster.tv"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 105
|
versionCode = 106
|
||||||
versionName = "0.14.42"
|
versionName = "0.14.43"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,11 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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).
|
* Berekende hoogte van de Home overlay-topnav (geen subtabs).
|
||||||
*
|
*
|
||||||
* TopNavBar overlay-row (compact, zonder underline):
|
* TopNavBar overlay-row:
|
||||||
* - vertical padding: 10.dp + 10.dp
|
* - vertical padding: 14.dp + 14.dp
|
||||||
* - tab-tekst ~21.dp → veilig 56.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 {
|
object HomeChrome {
|
||||||
val TopNavHeight = 56.dp
|
val TopNavHeight = 76.dp
|
||||||
/** Lucht tussen onderkant menu en badge. */
|
/** Lucht tussen onderkant menu en badge. */
|
||||||
val BadgeGap = 12.dp
|
val BadgeGap = 12.dp
|
||||||
val BadgeTop = TopNavHeight + BadgeGap
|
val BadgeTop = TopNavHeight + BadgeGap // 88.dp
|
||||||
/** Horizontale schermmarge (links = rechts). */
|
/** Horizontale schermmarge (links = rechts). */
|
||||||
val EdgeInset = 22.dp
|
val EdgeInset = 22.dp
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +118,7 @@ fun TopNavBar(
|
||||||
compact -> 0.42f
|
compact -> 0.42f
|
||||||
else -> 1f
|
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(
|
val downLink = Modifier.then(
|
||||||
if (contentDownFocus != null) {
|
if (contentDownFocus != null) {
|
||||||
Modifier.focusProperties { down = contentDownFocus }
|
Modifier.focusProperties { down = contentDownFocus }
|
||||||
|
|
@ -178,16 +177,13 @@ fun TopNavBar(
|
||||||
color = McColors.Brand,
|
color = McColors.Brand,
|
||||||
fontSize = if (compact && !overlay) 22.sp else 28.sp,
|
fontSize = if (compact && !overlay) 22.sp else 28.sp,
|
||||||
fontWeight = FontWeight.Black,
|
fontWeight = FontWeight.Black,
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
.height(if (compact && !overlay) 32.dp else 36.dp)
|
|
||||||
.padding(end = 12.dp)
|
|
||||||
.wrapContentHeight(Alignment.CenterVertically),
|
|
||||||
)
|
)
|
||||||
textTabs.forEach { tab ->
|
textTabs.forEach { tab ->
|
||||||
val active = selected == tab
|
val active = selected == tab
|
||||||
val color by animateColorAsState(
|
val color by animateColorAsState(
|
||||||
when {
|
when {
|
||||||
active -> McColors.Brand
|
active -> Color.White
|
||||||
overlay -> Color(0xFFE8E8E8)
|
overlay -> Color(0xFFE8E8E8)
|
||||||
else -> Color(0xFF808080)
|
else -> Color(0xFF808080)
|
||||||
},
|
},
|
||||||
|
|
@ -196,24 +192,21 @@ fun TopNavBar(
|
||||||
val fr = tabFocus[tab]
|
val fr = tabFocus[tab]
|
||||||
Surface(
|
Surface(
|
||||||
onClick = { onSelect(tab) },
|
onClick = { onSelect(tab) },
|
||||||
modifier = downLink
|
modifier = downLink.then(
|
||||||
.height(if (compact && !overlay) 32.dp else 36.dp)
|
if (fr != null) Modifier.focusRequester(fr) else Modifier,
|
||||||
.then(if (fr != null) Modifier.focusRequester(fr) else Modifier),
|
),
|
||||||
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
|
||||||
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.04f),
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f),
|
||||||
colors = ClickableSurfaceDefaults.colors(
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = if (active) Color(0x33FFFFFF) else Color.Transparent,
|
||||||
focusedContainerColor = Color(0x33FFFFFF),
|
focusedContainerColor = Color(0x44FFFFFF),
|
||||||
contentColor = color,
|
contentColor = color,
|
||||||
// Actieve tab blijft rood, ook met focus
|
focusedContentColor = Color.White,
|
||||||
focusedContentColor = if (active) McColors.Brand else Color.White,
|
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Box(
|
Column(
|
||||||
Modifier
|
Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||||
.fillMaxHeight()
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
tab.label,
|
tab.label,
|
||||||
|
|
@ -221,6 +214,14 @@ fun TopNavBar(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = if (active) FontWeight.Bold else FontWeight.Medium,
|
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
|
val searchActive = selected == MainTab.Search
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onSearchClick,
|
onClick = onSearchClick,
|
||||||
modifier = downLink
|
modifier = downLink.focusRequester(searchFocus),
|
||||||
.height(if (compact && !overlay) 32.dp else 36.dp)
|
|
||||||
.focusRequester(searchFocus),
|
|
||||||
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
|
||||||
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.04f),
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f),
|
||||||
colors = ClickableSurfaceDefaults.colors(
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = if (searchActive) Color(0x33FFFFFF) else Color.Transparent,
|
||||||
focusedContainerColor = Color(0x33FFFFFF),
|
focusedContainerColor = Color(0x44FFFFFF),
|
||||||
contentColor = if (searchActive) McColors.Brand else Color.White,
|
contentColor = Color.White,
|
||||||
focusedContentColor = if (searchActive) McColors.Brand else Color.White,
|
focusedContentColor = Color.White,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Box(
|
Column(
|
||||||
Modifier
|
Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
.fillMaxHeight()
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Search,
|
imageVector = Icons.Filled.Search,
|
||||||
contentDescription = "Zoeken",
|
contentDescription = "Zoeken",
|
||||||
modifier = Modifier.size(16.dp),
|
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
|
val settingsActive = selected == MainTab.Settings
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onSettingsClick,
|
onClick = onSettingsClick,
|
||||||
modifier = downLink
|
modifier = downLink.focusRequester(settingsFocus),
|
||||||
.height(if (compact && !overlay) 32.dp else 36.dp)
|
|
||||||
.focusRequester(settingsFocus),
|
|
||||||
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
|
||||||
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.04f),
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f),
|
||||||
colors = ClickableSurfaceDefaults.colors(
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = if (settingsActive) Color(0x33FFFFFF) else Color.Transparent,
|
||||||
focusedContainerColor = Color(0x33FFFFFF),
|
focusedContainerColor = Color(0x44FFFFFF),
|
||||||
contentColor = if (settingsActive) McColors.Brand else Color.White,
|
contentColor = Color.White,
|
||||||
focusedContentColor = if (settingsActive) McColors.Brand else Color.White,
|
focusedContentColor = Color.White,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Box(
|
Column(
|
||||||
Modifier
|
Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
.fillMaxHeight()
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Settings,
|
imageVector = Icons.Filled.Settings,
|
||||||
contentDescription = "Instellingen",
|
contentDescription = "Instellingen",
|
||||||
modifier = Modifier.size(16.dp),
|
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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -388,14 +388,21 @@ export class ViewerProfileService {
|
||||||
let bucket = pinAttempts.get(key);
|
let bucket = pinAttempts.get(key);
|
||||||
if (!bucket || bucket.resetAt < now) {
|
if (!bucket || bucket.resetAt < now) {
|
||||||
bucket = { count: 0, resetAt: now + PIN_ATTEMPT_WINDOW_MS };
|
bucket = { count: 0, resetAt: now + PIN_ATTEMPT_WINDOW_MS };
|
||||||
|
pinAttempts.set(key, bucket);
|
||||||
}
|
}
|
||||||
if (bucket.count >= PIN_MAX_ATTEMPTS) {
|
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) {
|
if (!ok) {
|
||||||
bucket.count += 1;
|
bucket.count += 1;
|
||||||
pinAttempts.set(key, bucket);
|
|
||||||
throw new AppError("FORBIDDEN", "Onjuiste PIN", 403);
|
throw new AppError("FORBIDDEN", "Onjuiste PIN", 403);
|
||||||
}
|
}
|
||||||
pinAttempts.delete(key);
|
pinAttempts.delete(key);
|
||||||
|
|
@ -403,7 +410,7 @@ export class ViewerProfileService {
|
||||||
|
|
||||||
await prisma.viewerDevice.update({
|
await prisma.viewerDevice.update({
|
||||||
where: { id: deviceId },
|
where: { id: deviceId },
|
||||||
data: { activeProfileId: profileId, lastSeenAt: new Date() },
|
data: { activeProfileId: profile.id, lastSeenAt: new Date() },
|
||||||
});
|
});
|
||||||
return { profile: publicProfile(profile) };
|
return { profile: publicProfile(profile) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue