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

View File

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

View File

@ -9,7 +9,7 @@ import org.koin.core.qualifier.named
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 {
// Single in-memory token manager
@ -26,9 +26,9 @@ val authModule = module {
// LoginViewModel
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> {
// 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>()
object : TokenProvider {
override fun getAccessToken(): String? {

View File

@ -35,7 +35,7 @@ fun AppHeader(
// Authentication buttons
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 ->
val isAdmin = userPermissions.any { it.contains("DELETE") }
Text(
@ -55,7 +55,7 @@ fun AppHeader(
}
}
} else {
// Show login button
// Show the login button
onNavigateToLogin?.let { loginAction ->
TextButton(
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
// --- 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.
private val LightColorScheme = lightColorScheme(

View File

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

View File

@ -132,7 +132,7 @@ val networkModule = module {
// [baseClient] REQUEST: .../token
// 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!
// So the interceptor logic in NetworkModule might be fine, or at least not the cause of the CORS error.
@ -140,7 +140,7 @@ val networkModule = module {
// We will use a safe lazy resolution pattern.
} catch (_: Exception) {
// ignore
// ignore
}
execute(request)
}
@ -150,19 +150,19 @@ val networkModule = module {
client.plugin(HttpSend).intercept { request ->
try {
// Attempt to resolve TokenProvider from the capturing scope
val tokenProvider = try {
koinScope.get<TokenProvider>()
} catch (_: Exception) {
null
}
// Attempt to resolve TokenProvider from the capturing scope
val tokenProvider = try {
koinScope.get<TokenProvider>()
} catch (_: Exception) {
null
}
val token = tokenProvider?.getAccessToken()
if (token != null) {
request.header("Authorization", "Bearer $token")
}
val token = tokenProvider?.getAccessToken()
if (token != null) {
request.header("Authorization", "Bearer $token")
}
} catch (e: Exception) {
println("[apiClient] Error injecting auth header: $e")
println("[apiClient] Error injecting auth header: $e")
}
execute(request)
}

View File

@ -26,9 +26,9 @@ actual object PlatformConfig {
}
if (!origin.isNullOrBlank()) {
val resolvedUrl = origin.removeSuffix("/") + "/api"
console.log("[PlatformConfig] Resolved API_BASE_URL from window.location.origin: $resolvedUrl")
return resolvedUrl
val resolvedUrl = origin.removeSuffix("/") + "/api"
console.log("[PlatformConfig] Resolved API_BASE_URL from window.location.origin: $resolvedUrl")
return resolvedUrl
}
// 3) Fallback to the local gateway directly (e.g. for tests without window)

View File

@ -10,7 +10,8 @@ import io.ktor.client.call.body
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 {

View File

@ -5,21 +5,24 @@ import at.mocode.frontend.core.sync.SyncableRepository
import at.mocode.ping.api.PingEvent
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(
private val db: AppDatabase
) : 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? {
println("PingEventRepositoryImpl: getLatestSince called - using corrected async implementation")
// FIX: Use .awaitAsOneOrNull() for async drivers instead of the blocking .executeAsOneOrNull()
return db.appDatabaseQueries.selectLatestPingEventId().awaitAsOneOrNull()
println("PingEventRepositoryImpl: getLatestSince called - using corrected async implementation")
// FIX: Verwenden Sie .awaitAsOneOrNull() für asynchrone Treiber anstelle des blockierenden .executeAsOneOrNull().
return db.appDatabaseQueries.selectLatestPingEventId().awaitAsOneOrNull()
}
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 {
items.forEach { event ->
db.appDatabaseQueries.upsertPingEvent(

View File

@ -11,19 +11,18 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module
/**
* Consolidated Koin module for the Ping Feature (Clean Architecture).
* Replaces the old 'clients.pingfeature' module.
* Konsolidiertes Koin-Modul für die Ping-Funktion (Clean Architecture).
*/
val pingFeatureModule = module {
// 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"))) }
// 2. Repository (Data Layer)
single { PingEventRepositoryImpl(get<AppDatabase>()) }
// 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> {
PingSyncServiceImpl(
syncManager = get(),
@ -32,7 +31,7 @@ val pingFeatureModule = module {
}
// 4. ViewModel (Presentation Layer)
// Injects API and Domain Service
// Injects API und Domain Service
factory {
PingViewModel(
apiClient = get(),

View File

@ -5,14 +5,14 @@ import at.mocode.frontend.core.sync.SyncableRepository
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 {
suspend fun syncPings()
}
/**
* Implementation of PingSyncService using the generic SyncManager.
* Implementierung des PingSyncService unter Verwendung des generischen SyncManager.
*/
class PingSyncServiceImpl(
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.theme.Dimens
// --- Refactored PingScreen using Design System ---
@Composable
fun PingScreen(
viewModel: PingViewModel,

View File

@ -15,7 +15,7 @@ import kotlin.test.assertEquals
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 {
val client = HttpClient(mockEngine) {
install(ContentNegotiation) {