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:
parent
24c8a0c63d
commit
af0b7c5f9b
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 fü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? {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user