1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-03 21:40:09 +05:30

Fix security issues from review

Notifications:
- VISIBILITY_PRIVATE on all channels and notifications
  (hides task text from lockscreen)
- Remove setFullScreenIntent (requires USE_FULL_SCREEN_INTENT
  permission; p1 channel already bypasses DND)

Auth:
- Clear cached token and server URL in memory on logout
  via AuthRepository.onLogout callback

Offline queue:
- Replace manual JSON string interpolation with
  kotlinx.serialization JsonObject/JsonPrimitive
  (prevents JSON injection from memo content)

CI/CD:
- Pin all GitHub Actions to commit SHAs
- Add permissions: contents: read to build workflow
- Decode keystore via env var instead of inline expansion
- Sanitize tag name through env var in release upload
- Fix test task name: testAndroidHostTest

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context)
This commit is contained in:
2026-06-05 15:08:16 +05:30
parent 6b1d798c95
commit e4c19c2d7c
11 changed files with 55 additions and 21 deletions
+6
View File
@@ -36,11 +36,17 @@ kotlin {
sourceSets {
commonMain.dependencies {
@Suppress("DEPRECATION")
implementation(compose.runtime)
@Suppress("DEPRECATION")
implementation(compose.foundation)
@Suppress("DEPRECATION")
implementation(compose.material3)
@Suppress("DEPRECATION")
implementation(compose.materialIconsExtended)
@Suppress("DEPRECATION")
implementation(compose.ui)
@Suppress("DEPRECATION")
implementation(compose.components.resources)
implementation(libs.androidx.lifecycle.viewmodel)
@@ -28,17 +28,17 @@ object TaskNotificationManager {
manager.createNotificationChannel(NotificationChannel(CHANNEL_P1, "P1 — Urgent", NotificationManager.IMPORTANCE_HIGH).apply {
description = "High priority — alarm sound, strong vibration, wakes screen"
enableVibration(true); vibrationPattern = longArrayOf(0, 500, 200, 500, 200, 500)
setSound(alarmUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC; setShowBadge(true); setBypassDnd(true)
setSound(alarmUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE; setShowBadge(true); setBypassDnd(true)
})
manager.createNotificationChannel(NotificationChannel(CHANNEL_P2, "P2 — Medium", NotificationManager.IMPORTANCE_HIGH).apply {
description = "Medium priority — notification sound, vibration"
enableVibration(true); vibrationPattern = longArrayOf(0, 300, 200, 300)
setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC; setShowBadge(true)
setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE; setShowBadge(true)
})
manager.createNotificationChannel(NotificationChannel(CHANNEL_P3, "P3 — Low", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "Low priority — notification sound, short vibration"
enableVibration(true); vibrationPattern = longArrayOf(0, 200)
setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE
})
manager.createNotificationChannel(NotificationChannel(CHANNEL_DEFAULT, "No Priority", NotificationManager.IMPORTANCE_LOW).apply {
description = "No priority — silent notification"
@@ -89,7 +89,7 @@ object TaskNotificationManager {
.setShowWhen(true)
.setPriority(notifPriority)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setAutoCancel(true)
.setOnlyAlertOnce(false)
.setContentIntent(pendingOpen)
@@ -99,7 +99,6 @@ object TaskNotificationManager {
when (priority) {
1 -> {
builder.setFullScreenIntent(pendingOpen, true)
builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM))
builder.setVibrate(longArrayOf(0, 500, 200, 500, 200, 500))
}
@@ -2,12 +2,14 @@ package com.avinal.memos
import androidx.compose.runtime.Composable
import coil3.ImageLoader
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.setSingletonImageLoaderFactory
import coil3.network.ktor3.KtorNetworkFetcherFactory
import com.avinal.memos.ui.navigation.AppNavHost
import com.avinal.memos.ui.theme.NikkiTheme
import com.avinal.memos.util.LocalAppDependencies
@OptIn(ExperimentalCoilApi::class)
@Composable
fun App(sharedText: String? = null) {
val deps = LocalAppDependencies.current
@@ -39,7 +39,11 @@ class AppDependencies(
)
}
val authRepository: AuthRepository by lazy { AuthRepository(apiClient, tokenStore) }
val authRepository: AuthRepository by lazy {
AuthRepository(apiClient, tokenStore).also {
it.onLogout = { cachedToken = null; cachedServerUrl = null }
}
}
val memoRepository: MemoRepository by lazy {
MemoRepository(apiClient, database.memoDao()) {
com.avinal.memos.util.triggerReminderCheck()
@@ -52,5 +52,8 @@ class AuthRepository(
suspend fun logout() {
tokenStore.clear()
_currentUser.value = null
onLogout?.invoke()
}
var onLogout: (() -> Unit)? = null
}
@@ -127,7 +127,12 @@ class MemoRepository(
is ApiResult.NetworkError -> {
pendingSyncDao?.insert(PendingSyncEntity(
memoId = null, action = "CREATE",
payload = """{"content":"${content.replace("\"", "\\\"")}","visibility":"${visibility.toApiString()}"}""",
payload = kotlinx.serialization.json.Json.encodeToString(
kotlinx.serialization.json.JsonObject(mapOf(
"content" to kotlinx.serialization.json.JsonPrimitive(content),
"visibility" to kotlinx.serialization.json.JsonPrimitive(visibility.toApiString()),
))
),
createdAt = nowMillis(),
))
result
@@ -155,14 +160,14 @@ class MemoRepository(
}
is ApiResult.Error -> result
is ApiResult.NetworkError -> {
val payloadParts = buildList {
if (content != null) add(""""content":"${content.replace("\"", "\\\"")}"""")
if (visibility != null) add(""""visibility":"${visibility.toApiString()}"""")
if (pinned != null) add(""""pinned":$pinned""")
val fields = buildMap<String, kotlinx.serialization.json.JsonElement> {
if (content != null) put("content", kotlinx.serialization.json.JsonPrimitive(content))
if (visibility != null) put("visibility", kotlinx.serialization.json.JsonPrimitive(visibility.toApiString()))
if (pinned != null) put("pinned", kotlinx.serialization.json.JsonPrimitive(pinned))
}
pendingSyncDao?.insert(PendingSyncEntity(
memoId = id, action = "UPDATE",
payload = "{${payloadParts.joinToString(",")}}",
payload = kotlinx.serialization.json.Json.encodeToString(kotlinx.serialization.json.JsonObject(fields)),
createdAt = nowMillis(),
))
result
@@ -1,3 +1,4 @@
@file:Suppress("DEPRECATION")
package com.avinal.memos.ui.components
import androidx.compose.animation.animateContentSize
@@ -236,7 +237,7 @@ private fun InlineEditor(
confirmButton = {
TextButton(onClick = {
dateState.selectedDateMillis?.let { ms ->
val d = kotlinx.datetime.Instant.fromEpochMilliseconds(ms)
val d = Instant.fromEpochMilliseconds(ms)
.toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date
onContentChange(content.trimEnd() + " $d")
}
@@ -375,7 +375,7 @@ fun MemoListScreen(
confirmButton = {
TextButton(onClick = {
dateState.selectedDateMillis?.let { ms ->
val d = kotlinx.datetime.Instant.fromEpochMilliseconds(ms)
val d = kotlin.time.Instant.fromEpochMilliseconds(ms)
.toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date
val r = composeField.text.trimEnd() + " $d"; composeField = TextFieldValue(r, TextRange(r.length))
}
@@ -41,7 +41,7 @@ import com.avinal.memos.domain.ReminderUnit
import com.avinal.memos.domain.Task
import com.avinal.memos.parser.TaskParser
import com.avinal.memos.ui.theme.LocalAccentColor
import kotlinx.datetime.Instant
import kotlin.time.Instant
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn