Monorepo: Gitea CI, docs, auth/sync, backend APIs, gitignore

- Add Gitea Actions workflow; point README to gitea.d-bis.org/Sankofa_Phoenix/SMOA
- Expand .gitignore for Spring H2 data, secrets, Kotlin .kotlin/, tooling
- Track docs/api/generated ReDoc bundle; refresh api docs README
- Android: network/auth/sync, UI shell, tests; backend credentials/integrity APIs
- Docs, scripts (generate-api-docs), modules and core updates

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-23 20:19:24 -07:00
parent f97e23592c
commit a2dc194a49
234 changed files with 10008 additions and 1149 deletions
+9 -2
View File
@@ -1,8 +1,8 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
@@ -29,6 +29,14 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
hilt {
enableAggregatingTask = true
}
}
kapt {
correctErrorTypes = true
}
dependencies {
@@ -52,4 +60,3 @@ dependencies {
testImplementation(Dependencies.coroutinesTest)
testImplementation(Dependencies.truth)
}
@@ -25,11 +25,12 @@ class BrowserService @Inject constructor(
* Navigate to URL with security checks.
*/
suspend fun navigateToURL(url: String): Result<String> {
// Enforce VPN requirement
try {
vpnManager.enforceVPNRequirement()
} catch (e: VPNRequiredException) {
return Result.failure(e)
if (vpnManager.isBrowserVPNEnforced()) {
try {
vpnManager.enforceVPNRequirement()
} catch (e: VPNRequiredException) {
return Result.failure(e)
}
}
// Check URL allow-list
@@ -13,8 +13,12 @@ class URLFilter @Inject constructor() {
private val allowedPaths = mutableMapOf<String, Set<String>>()
init {
// Default allow-list (can be configured via policy)
// Add default mission/agency resources here
// Demo / dev defaults — replace with policy-backed list in production
addAllowedDomain("example.com")
addAllowedDomain("www.wikipedia.org")
addAllowedDomain("developer.android.com")
addAllowedDomain("source.android.com")
addAllowedDomain("duckduckgo.com")
}
/**
@@ -1,22 +1,54 @@
package com.smoa.modules.browser.ui
import android.annotation.SuppressLint
import android.os.Build
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.smoa.core.security.ScreenProtection
import com.smoa.modules.browser.domain.BrowserService
import com.smoa.modules.browser.domain.URLFilter
import kotlinx.coroutines.launch
private const val DEFAULT_START_URL = "https://example.com"
/**
* Controlled browser screen with VPN enforcement and URL filtering.
*/
@SuppressLint("SetJavaScriptEnabled")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BrowserScreen(
@@ -25,19 +57,26 @@ fun BrowserScreen(
screenProtection: ScreenProtection,
modifier: Modifier = Modifier
) {
// Enable screen protection
screenProtection.EnableScreenProtection()
var currentURL by remember { mutableStateOf("") }
var urlInput by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
// WebView state
var webView by remember { mutableStateOf<WebView?>(null) }
// Navigate to URL
fun normalizeUrl(raw: String): String {
val trimmed = raw.trim()
if (trimmed.isEmpty()) return trimmed
return if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
"https://$trimmed"
} else {
trimmed
}
}
suspend fun navigateToURL(url: String) {
isLoading = true
errorMessage = null
@@ -45,14 +84,18 @@ fun BrowserScreen(
browserService.navigateToURL(url)
.onSuccess { allowedURL ->
currentURL = allowedURL
urlInput = allowedURL
isLoading = true
webView?.loadUrl(allowedURL)
}
.onFailure { error ->
errorMessage = error.message
}
.also {
isLoading = false
}
}
LaunchedEffect(webView) {
if (webView == null) return@LaunchedEffect
navigateToURL(DEFAULT_START_URL)
}
Column(
@@ -60,7 +103,38 @@ fun BrowserScreen(
.fillMaxSize()
.padding(8.dp)
) {
// URL bar
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { if (webView?.canGoBack() == true) webView?.goBack() },
enabled = webView?.canGoBack() == true
) {
Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
}
IconButton(
onClick = { if (webView?.canGoForward() == true) webView?.goForward() },
enabled = webView?.canGoForward() == true
) {
Icon(Icons.Filled.ArrowForward, contentDescription = "Forward")
}
IconButton(
onClick = {
val target = normalizeUrl(urlInput).ifBlank { currentURL }
if (target.isNotBlank()) {
scope.launch { navigateToURL(target) }
} else {
webView?.reload()
}
},
enabled = webView != null
) {
Icon(Icons.Default.Refresh, contentDescription = "Reload")
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -77,16 +151,9 @@ fun BrowserScreen(
Button(
onClick = {
if (urlInput.isNotBlank()) {
// Add https:// if no protocol specified
val url = if (!urlInput.startsWith("http://") && !urlInput.startsWith("https://")) {
"https://$urlInput"
} else {
urlInput
}
// Navigate (this would need to be in a coroutine scope)
// For now, just update the URL
currentURL = url
val url = normalizeUrl(urlInput)
if (url.isNotBlank()) {
scope.launch { navigateToURL(url) }
}
},
enabled = !isLoading && urlInput.isNotBlank()
@@ -95,9 +162,15 @@ fun BrowserScreen(
}
}
Text(
text = "Allowed: ${urlFilter.getAllowedDomains().joinToString()}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 4.dp)
)
Spacer(modifier = Modifier.height(8.dp))
// Error message
errorMessage?.let { error ->
Card(
colors = CardDefaults.cardColors(
@@ -114,13 +187,11 @@ fun BrowserScreen(
Spacer(modifier = Modifier.height(8.dp))
}
// Loading indicator
if (isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(8.dp))
}
// WebView
AndroidView(
factory = { ctx ->
WebView(ctx).apply {
@@ -133,19 +204,46 @@ fun BrowserScreen(
settings.displayZoomControls = false
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
// Check if URL is allowed before loading
url?.let {
if (!browserService.isURLAllowed(it)) {
errorMessage = "URL not in allow-list: $it"
return true // Block navigation
}
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url?.toString() ?: return false
if (!browserService.isURLAllowed(url)) {
errorMessage = "URL not in allow-list: $url"
return true
}
return false
}
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return false
}
val u = url ?: return false
if (!browserService.isURLAllowed(u)) {
errorMessage = "URL not in allow-list: $u"
return true
}
return false
}
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
isLoading = true
url?.let { urlInput = it }
}
override fun onPageFinished(view: WebView?, url: String?) {
isLoading = false
url?.let {
currentURL = it
urlInput = it
}
return false // Allow navigation
}
}
webView = this
}
},
update = { wv ->
if (webView !== wv) {
webView = wv
}
},
modifier = Modifier
@@ -154,4 +252,3 @@ fun BrowserScreen(
)
}
}
@@ -33,7 +33,7 @@ class BrowserServiceTest {
fun `navigateToURL should fail when VPN not connected`() = runTest {
// Given
val url = "https://example.com"
every { vpnManager.isVPNRequired() } returns true
every { vpnManager.isBrowserVPNEnforced() } returns true
every { vpnManager.isVPNConnected() } returns false
every { vpnManager.enforceVPNRequirement() } throws VPNRequiredException("VPN required")
@@ -49,8 +49,7 @@ class BrowserServiceTest {
fun `navigateToURL should fail when URL not in allow-list`() = runTest {
// Given
val url = "https://blocked.com"
every { vpnManager.isVPNRequired() } returns true
every { vpnManager.isVPNConnected() } returns true
every { vpnManager.isBrowserVPNEnforced() } returns false
every { urlFilter.isAllowed(url) } returns false
// When
@@ -65,8 +64,9 @@ class BrowserServiceTest {
fun `navigateToURL should succeed for allowed URL with VPN`() = runTest {
// Given
val url = "https://allowed.com"
every { vpnManager.isVPNRequired() } returns true
every { vpnManager.isBrowserVPNEnforced() } returns true
every { vpnManager.isVPNConnected() } returns true
every { vpnManager.enforceVPNRequirement() } returns Unit
every { urlFilter.isAllowed(url) } returns true
// When