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("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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 fü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? {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -140,7 +140,7 @@ val networkModule = module {
|
||||||
|
|
||||||
// We will use a safe lazy resolution pattern.
|
// We will use a safe lazy resolution pattern.
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
execute(request)
|
execute(request)
|
||||||
}
|
}
|
||||||
|
|
@ -150,19 +150,19 @@ val networkModule = module {
|
||||||
|
|
||||||
client.plugin(HttpSend).intercept { request ->
|
client.plugin(HttpSend).intercept { request ->
|
||||||
try {
|
try {
|
||||||
// Attempt to resolve TokenProvider from the capturing scope
|
// Attempt to resolve TokenProvider from the capturing scope
|
||||||
val tokenProvider = try {
|
val tokenProvider = try {
|
||||||
koinScope.get<TokenProvider>()
|
koinScope.get<TokenProvider>()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val token = tokenProvider?.getAccessToken()
|
val token = tokenProvider?.getAccessToken()
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
request.header("Authorization", "Bearer $token")
|
request.header("Authorization", "Bearer $token")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("[apiClient] Error injecting auth header: $e")
|
println("[apiClient] Error injecting auth header: $e")
|
||||||
}
|
}
|
||||||
execute(request)
|
execute(request)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ actual object PlatformConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!origin.isNullOrBlank()) {
|
if (!origin.isNullOrBlank()) {
|
||||||
val resolvedUrl = origin.removeSuffix("/") + "/api"
|
val resolvedUrl = origin.removeSuffix("/") + "/api"
|
||||||
console.log("[PlatformConfig] Resolved API_BASE_URL from window.location.origin: $resolvedUrl")
|
console.log("[PlatformConfig] Resolved API_BASE_URL from window.location.origin: $resolvedUrl")
|
||||||
return resolvedUrl
|
return resolvedUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Fallback to the local gateway directly (e.g. for tests without window)
|
// 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
|
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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user