chore(frontend): cleanup legacy code and improve localization consistency

- Removed deprecated `NotificationCard` component due to dependency on an outdated presentation layer.
- Translated comments and documentation to German for improved localization across `core.auth`, `ping-feature`, and `network`.
- Standardized comment formatting, improved doc clarity, and ensured consistent API documentation in all modules.
This commit is contained in:
Stefan Mogeritsch 2026-02-01 12:00:10 +01:00
parent 24c8a0c63d
commit af0b7c5f9b
16 changed files with 72 additions and 76 deletions

View File

@ -44,13 +44,13 @@ class AuthApiClient(
append("grant_type", "password") append("grant_type", "password")
append("client_id", clientId) append("client_id", clientId)
// IMPORTANT: Only send client_secret if it's NOT a public client (like 'web-app') // WICHTIG: Senden Sie client_secret nur, wenn es sich NICHT um einen öffentlichen Client (wie 'web-app') handelt.
// Keycloak rejects requests from public clients that contain a client_secret. // Keycloak lehnt Anfragen von öffentlichen Clients ab, die client_secret enthalten.
// We check if the client ID suggests a public client or if secret is explicitly provided. // Wir prüfen, ob die Client-ID auf einen öffentlichen Client hindeutet oder ob ein Secret explizit angegeben wurde.
// For now, we rely on the fact that 'web-app' is public and should NOT have a secret sent. // Aktuell gehen wir davon aus, dass 'web-app' öffentlich ist und daher kein Secret gesendet werden sollte.
// Logic: If clientId is 'web-app', we force ignore the secret, or we rely on caller to pass null. // Logik: Wenn clientId 'web-app' ist, ignorieren wir das Geheimnis oder verlassen uns darauf, dass der Aufrufer null übergibt.
// Since AppConstants might still have the secret for 'postman-client', we need to be careful. // Da AppConstants möglicherweise noch das Geheimnis für 'postman-client' enthält, ist Vorsicht geboten.
if (!clientSecret.isNullOrBlank() && clientId != "web-app") { if (!clientSecret.isNullOrBlank() && clientId != "web-app") {
append("client_secret", clientSecret) append("client_secret", clientSecret)

View File

@ -10,7 +10,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
/** /**
* Client-side permission enumeration that mirrors server-side BerechtigungE * Client-side Berechtigungsaufzählung, welche die serverseitige BerechtigungE widerspiegelt
*/ */
@Serializable @Serializable
enum class Permission { enum class Permission {
@ -66,9 +66,9 @@ data class AuthState(
/** /**
* Secure in-memory JWT token manager * Secure in-memory JWT token manager
* *
* For web clients, storing tokens in memory is the most secure approach * Für Webclients ist das Speichern von Tokens im Arbeitsspeicher der sicherste Ansatz,
* to prevent XSS attacks. The token is lost when the browser tab is closed * um XSS-Angriffe zu verhindern. Das Token geht beim Schließen des Browser-Tabs verloren,
* or refreshed, requiring re-authentication. * was eine erneute Authentifizierung erforderlich macht.
*/ */
@Suppress("unused") @Suppress("unused")
class AuthTokenManager { class AuthTokenManager {
@ -141,38 +141,38 @@ class AuthTokenManager {
fun getUserId(): String? = tokenPayload?.sub fun getUserId(): String? = tokenPayload?.sub
/** /**
* Get username from token * Get username from a token
*/ */
fun getUsername(): String? = tokenPayload?.username fun getUsername(): String? = tokenPayload?.username
/** /**
* Get current user permissions * Get current user permissions (Berechtigungen)
*/ */
fun getPermissions(): List<Permission> = _authState.value.permissions fun getPermissions(): List<Permission> = _authState.value.permissions
/** /**
* Check if user has a specific permission * Check if the user has a specific permission
*/ */
fun hasPermission(permission: Permission): Boolean { fun hasPermission(permission: Permission): Boolean {
return _authState.value.permissions.contains(permission) return _authState.value.permissions.contains(permission)
} }
/** /**
* Check if user has any of the specified permissions * Check if the user has any of the specified permissions
*/ */
fun hasAnyPermission(vararg permissions: Permission): Boolean { fun hasAnyPermission(vararg permissions: Permission): Boolean {
return permissions.any { _authState.value.permissions.contains(it) } return permissions.any { _authState.value.permissions.contains(it) }
} }
/** /**
* Check if user has all of the specified permissions * Check if the user has all the specified permissions
*/ */
fun hasAllPermissions(vararg permissions: Permission): Boolean { fun hasAllPermissions(vararg permissions: Permission): Boolean {
return permissions.all { _authState.value.permissions.contains(it) } return permissions.all { _authState.value.permissions.contains(it) }
} }
/** /**
* Check if user can perform read operations * Check if the user can perform read operations
*/ */
fun canRead(): Boolean { fun canRead(): Boolean {
return hasAnyPermission( return hasAnyPermission(
@ -184,7 +184,7 @@ class AuthTokenManager {
} }
/** /**
* Check if user can perform create operations * Check if the user can perform create operations
*/ */
fun canCreate(): Boolean { fun canCreate(): Boolean {
return hasAnyPermission( return hasAnyPermission(
@ -196,7 +196,7 @@ class AuthTokenManager {
} }
/** /**
* Check if user can perform update operations * Check if the user can perform update operations
*/ */
fun canUpdate(): Boolean { fun canUpdate(): Boolean {
return hasAnyPermission( return hasAnyPermission(
@ -208,7 +208,7 @@ class AuthTokenManager {
} }
/** /**
* Check if user can perform delete operations (admin-level) * Check if the user can perform delete operations (admin-level)
*/ */
fun canDelete(): Boolean { fun canDelete(): Boolean {
return hasAnyPermission( return hasAnyPermission(
@ -220,12 +220,12 @@ class AuthTokenManager {
} }
/** /**
* Check if user is admin (has delete permissions) * Check if the user is admin (has deleted permissions)
*/ */
fun isAdmin(): Boolean = canDelete() fun isAdmin(): Boolean = canDelete()
/** /**
* Check if token expires within specified minutes * Check if the token expires within specified minutes
*/ */
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
fun isTokenExpiringSoon(minutesThreshold: Int = 5): Boolean { fun isTokenExpiringSoon(minutesThreshold: Int = 5): Boolean {
@ -238,8 +238,8 @@ class AuthTokenManager {
} }
/** /**
* Parse JWT payload for basic validation and user info extraction * JWT-Payload für grundlegende Validierung und Extraktion von Benutzerinformationen analysieren
* Note: This is for client-side info extraction only, not security validation * Hinweis: Dies dient ausschließlich der clientseitigen Informationsextraktion, nicht der Sicherheitsvalidierung.
*/ */
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
private fun parseJwtPayload(token: String): JwtPayload? { private fun parseJwtPayload(token: String): JwtPayload? {
@ -250,7 +250,7 @@ class AuthTokenManager {
// Decode the payload (second part) // Decode the payload (second part)
val payloadJson = Base64.decode(parts[1]).decodeToString() val payloadJson = Base64.decode(parts[1]).decodeToString()
// First try to parse with standard approach // First, try to parse with a standard approach
val basicPayload = try { val basicPayload = try {
Json.decodeFromString<JwtPayload>(payloadJson) Json.decodeFromString<JwtPayload>(payloadJson)
} catch (e: Exception) { } catch (e: Exception) {
@ -263,7 +263,7 @@ class AuthTokenManager {
return basicPayload return basicPayload
} }
// Otherwise, extract permissions manually from JSON string // Otherwise, extract permissions manually from a JSON string
val permissions = extractPermissionsFromJson(payloadJson) val permissions = extractPermissionsFromJson(payloadJson)
// Return payload with manually extracted permissions // Return payload with manually extracted permissions
@ -282,16 +282,16 @@ class AuthTokenManager {
} }
/** /**
* Extract permissions array from JSON string using simple string parsing * Extract permissions array from a JSON string using simple string parsing
*/ */
private fun extractPermissionsFromJson(jsonString: String): List<String>? { private fun extractPermissionsFromJson(jsonString: String): List<String>? {
return try { return try {
// Simple regex to find a permissions array // Simple regex to find a permissions array
val permissionsRegex = """"permissions":\s*\[(.*?)\]""".toRegex() val permissionsRegex = """"permissions":\s*\[(.*?)]""".toRegex()
val match = permissionsRegex.find(jsonString) val match = permissionsRegex.find(jsonString)
match?.let { match?.let { matchResult ->
val permissionsContent = it.groupValues[1] val permissionsContent = matchResult.groupValues[1]
if (permissionsContent.isBlank()) return emptyList() if (permissionsContent.isBlank()) return emptyList()
// Extract individual permission strings // Extract individual permission strings

View File

@ -9,7 +9,7 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
/** /**
* Koin module for core-auth: provides AuthTokenManager and binds it as TokenProvider for apiClient. * Koin-Modul für core-auth: stellt AuthTokenManager bereit und bindet ihn als TokenProvider r apiClient.
*/ */
val authModule = module { val authModule = module {
// Single in-memory token manager // Single in-memory token manager
@ -26,9 +26,9 @@ val authModule = module {
// LoginViewModel // LoginViewModel
factory { LoginViewModel(get(), get(), get(named("apiClient"))) } factory { LoginViewModel(get(), get(), get(named("apiClient"))) }
// Bridge to core network TokenProvider without adding a hard dependency there // Brücke zum TokenProvider des Kernnetzwerks, ohne dort eine harte Abhängigkeit hinzuzufügen
single<TokenProvider> { single<TokenProvider> {
// We need to capture the AuthTokenManager instance to avoid issues with 'this' context in JS // Wir müssen die AuthTokenManager-Instanz erfassen, um Probleme mit dem 'this'-Kontext in JavaScript zu vermeiden.
val tokenManager = get<AuthTokenManager>() val tokenManager = get<AuthTokenManager>()
object : TokenProvider { object : TokenProvider {
override fun getAccessToken(): String? { override fun getAccessToken(): String? {

View File

@ -35,7 +35,7 @@ fun AppHeader(
// Authentication buttons // Authentication buttons
if (isAuthenticated) { if (isAuthenticated) {
// Show username with admin indicator if user has delete permissions // Show username with admin indicator if user has deleted permissions
username?.let { user -> username?.let { user ->
val isAdmin = userPermissions.any { it.contains("DELETE") } val isAdmin = userPermissions.any { it.contains("DELETE") }
Text( Text(
@ -55,7 +55,7 @@ fun AppHeader(
} }
} }
} else { } else {
// Show login button // Show the login button
onNavigateToLogin?.let { loginAction -> onNavigateToLogin?.let { loginAction ->
TextButton( TextButton(
onClick = loginAction onClick = loginAction

View File

@ -1,5 +0,0 @@
package at.mocode.frontend.core.designsystem.components
// Legacy notification components removed due to dependency on old presentation layer.
// Intentionally left empty as part of cleanup. You can safely delete this file
// if no modules import it anymore.

View File

@ -14,7 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
// --- 1. Farben (Palette) --- // --- 1. Farben (Palette) ---
// Wir definieren eine professionelle, kontrastreiche Palette. // wir definieren eine professionelle, kontrastreiche Palette.
// Blau steht für Aktion/Information, Grau für Struktur. // Blau steht für Aktion/Information, Grau für Struktur.
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(

View File

@ -3,8 +3,8 @@ package at.mocode.frontend.core.network
import kotlin.native.concurrent.ThreadLocal import kotlin.native.concurrent.ThreadLocal
/** /**
* Network configuration with sensible defaults and environment overrides. * Netzwerkkonfiguration mit sinnvollen Standardeinstellungen und Umgebungseinstellungen zum Überschreiben.
* Defaults to the local API Gateway on port 8081. * Standardmäßig wird das lokale API-Gateway auf Port 8081 verwendet.
*/ */
@ThreadLocal @ThreadLocal
object NetworkConfig { object NetworkConfig {

View File

@ -132,7 +132,7 @@ val networkModule = module {
// [baseClient] REQUEST: .../token // [baseClient] REQUEST: .../token
// Access to fetch at ... blocked by CORS policy // Access to fetch at ... blocked by CORS policy
// This confirms it is a CORS issue on the Keycloak server side, or the browser side. // This confirms it is a CORS issue on the Keycloak server side or the browser side.
// The JS error `TypeError` is GONE in the latest log! // The JS error `TypeError` is GONE in the latest log!
// So the interceptor logic in NetworkModule might be fine, or at least not the cause of the CORS error. // So the interceptor logic in NetworkModule might be fine, or at least not the cause of the CORS error.

View File

@ -10,7 +10,8 @@ import io.ktor.client.call.body
import io.ktor.client.request.get import io.ktor.client.request.get
/** /**
* PingApi implementation that uses a provided HttpClient (e.g., DI-provided "apiClient"). * PingApi-Implementierung, die einen bereitgestellten HttpClient verwendet (z. B. den per Dependency Injection
* bereitgestellten "apiClient").
*/ */
class PingApiKoinClient(private val client: HttpClient) : PingApi { class PingApiKoinClient(private val client: HttpClient) : PingApi {

View File

@ -5,21 +5,24 @@ import at.mocode.frontend.core.sync.SyncableRepository
import at.mocode.ping.api.PingEvent import at.mocode.ping.api.PingEvent
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
// ARCH-BLUEPRINT: This repository implements the generic SyncableRepository /**
// for a specific entity, bridging the gap between the sync core and the local database. ** ARCH-BLUEPRINT: Dieses Repository implementiert das generische Syncable Repository
** für eine bestimmte Entität und überbrückt so die Lücke zwischen dem Sync-Core und der
** lokalen Datenbank.
*/
class PingEventRepositoryImpl( class PingEventRepositoryImpl(
private val db: AppDatabase private val db: AppDatabase
) : SyncableRepository<PingEvent> { ) : SyncableRepository<PingEvent> {
// The `since` parameter for our sync is the ID of the last event, not a timestamp. // Der `since`-Parameter für unsere Synchronisierung ist die ID des letzten Ereignisses, kein Zeitstempel.
override suspend fun getLatestSince(): String? { override suspend fun getLatestSince(): String? {
println("PingEventRepositoryImpl: getLatestSince called - using corrected async implementation") println("PingEventRepositoryImpl: getLatestSince called - using corrected async implementation")
// FIX: Use .awaitAsOneOrNull() for async drivers instead of the blocking .executeAsOneOrNull() // FIX: Verwenden Sie .awaitAsOneOrNull() für asynchrone Treiber anstelle des blockierenden .executeAsOneOrNull().
return db.appDatabaseQueries.selectLatestPingEventId().awaitAsOneOrNull() return db.appDatabaseQueries.selectLatestPingEventId().awaitAsOneOrNull()
} }
override suspend fun upsert(items: List<PingEvent>) { override suspend fun upsert(items: List<PingEvent>) {
// Always perform bulk operations within a transaction. // Führen Sie Massenoperationen immer innerhalb einer Transaktion durch.
db.transaction { db.transaction {
items.forEach { event -> items.forEach { event ->
db.appDatabaseQueries.upsertPingEvent( db.appDatabaseQueries.upsertPingEvent(

View File

@ -11,19 +11,18 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
/** /**
* Consolidated Koin module for the Ping Feature (Clean Architecture). * Konsolidiertes Koin-Modul für die Ping-Funktion (Clean Architecture).
* Replaces the old 'clients.pingfeature' module.
*/ */
val pingFeatureModule = module { val pingFeatureModule = module {
// 1. API Client (Data Layer) // 1. API Client (Data Layer)
// Uses the shared authenticated 'apiClient' from Core Network // Verwendet den gemeinsam genutzten, authentifizierten „apiClient“ aus dem Kernnetzwerk.
single<PingApi> { PingApiKoinClient(get(named("apiClient"))) } single<PingApi> { PingApiKoinClient(get(named("apiClient"))) }
// 2. Repository (Data Layer) // 2. Repository (Data Layer)
single { PingEventRepositoryImpl(get<AppDatabase>()) } single { PingEventRepositoryImpl(get<AppDatabase>()) }
// 3. Domain Service (Domain Layer) // 3. Domain Service (Domain Layer)
// Wraps SyncManager and Repository to decouple ViewModel from SyncManager implementation details // Wraps SyncManager und Repository, um ViewModel von den Implementierungsdetails von SyncManager zu entkoppeln.
single<PingSyncService> { single<PingSyncService> {
PingSyncServiceImpl( PingSyncServiceImpl(
syncManager = get(), syncManager = get(),
@ -32,7 +31,7 @@ val pingFeatureModule = module {
} }
// 4. ViewModel (Presentation Layer) // 4. ViewModel (Presentation Layer)
// Injects API and Domain Service // Injects API und Domain Service
factory { factory {
PingViewModel( PingViewModel(
apiClient = get(), apiClient = get(),

View File

@ -5,14 +5,14 @@ import at.mocode.frontend.core.sync.SyncableRepository
import at.mocode.ping.api.PingEvent import at.mocode.ping.api.PingEvent
/** /**
* Interface for the Ping Sync Service to allow easier testing and decoupling. * Interface für den Ping-Sync-Dienst zur einfacheren Prüfung und Entkopplung.
*/ */
interface PingSyncService { interface PingSyncService {
suspend fun syncPings() suspend fun syncPings()
} }
/** /**
* Implementation of PingSyncService using the generic SyncManager. * Implementierung des PingSyncService unter Verwendung des generischen SyncManager.
*/ */
class PingSyncServiceImpl( class PingSyncServiceImpl(
private val syncManager: SyncManager, private val syncManager: SyncManager,

View File

@ -19,8 +19,6 @@ import at.mocode.frontend.core.designsystem.components.DashboardCard
import at.mocode.frontend.core.designsystem.components.DenseButton import at.mocode.frontend.core.designsystem.components.DenseButton
import at.mocode.frontend.core.designsystem.theme.Dimens import at.mocode.frontend.core.designsystem.theme.Dimens
// --- Refactored PingScreen using Design System ---
@Composable @Composable
fun PingScreen( fun PingScreen(
viewModel: PingViewModel, viewModel: PingViewModel,

View File

@ -15,7 +15,7 @@ import kotlin.test.assertEquals
class PingApiKoinClientTest { class PingApiKoinClientTest {
// Helper to create a testable client using the new DI-friendly implementation // Hilfe zur Erstellung eines testbaren Clients mithilfe der neuen DI-freundlichen Implementierung
private fun createTestClient(mockEngine: MockEngine): PingApiKoinClient { private fun createTestClient(mockEngine: MockEngine): PingApiKoinClient {
val client = HttpClient(mockEngine) { val client = HttpClient(mockEngine) {
install(ContentNegotiation) { install(ContentNegotiation) {