Restore revoke action alongside delete in license list.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,9 @@ interface LicenseApi {
|
||||
@POST("v1/admin/product-keys/delete")
|
||||
suspend fun deleteProductKey(@Body body: DeleteProductKeyRequest): DeleteProductKeyResponse
|
||||
|
||||
@POST("v1/admin/revoke")
|
||||
suspend fun revokeLicense(@Body body: RevokeLicenseRequest): RevokeLicenseResponse
|
||||
|
||||
@GET("v1/admin/stats/downloads")
|
||||
suspend fun getDownloadStats(): DownloadStatsOverview
|
||||
|
||||
|
||||
@@ -77,6 +77,14 @@ data class DeleteProductKeyResponse(
|
||||
val deleted: String? = null,
|
||||
)
|
||||
|
||||
data class RevokeLicenseRequest(
|
||||
val key: String,
|
||||
)
|
||||
|
||||
data class RevokeLicenseResponse(
|
||||
val ok: Boolean,
|
||||
)
|
||||
|
||||
data class DownloadStatsMonth(
|
||||
val month: String,
|
||||
val windows: Int,
|
||||
|
||||
@@ -71,6 +71,10 @@ class LicenseRepository(
|
||||
createApi(settings).deleteProductKey(DeleteProductKeyRequest(key))
|
||||
}
|
||||
|
||||
suspend fun revokeLicense(settings: AppSettings, key: String): Result<Unit> = runCatching {
|
||||
createApi(settings).revokeLicense(RevokeLicenseRequest(key))
|
||||
}
|
||||
|
||||
suspend fun getDownloadStatsOverview(settings: AppSettings): Result<DownloadStatsOverview> = runCatching {
|
||||
createApi(settings).getDownloadStats()
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ data class LicenseListUiState(
|
||||
val licenses: List<LicenseEntry> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val deletingKey: String? = null,
|
||||
val revokingKey: String? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@@ -129,6 +130,25 @@ class LicenseViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
fun revokeLicense(key: String) {
|
||||
viewModelScope.launch {
|
||||
_listState.update { it.copy(revokingKey = key, error = null) }
|
||||
licenseRepository.revokeLicense(currentSettings(), key)
|
||||
.onSuccess {
|
||||
loadLicenses()
|
||||
_listState.update { it.copy(revokingKey = null) }
|
||||
}
|
||||
.onFailure { e ->
|
||||
_listState.update {
|
||||
it.copy(
|
||||
revokingKey = null,
|
||||
error = e.message ?: "Не удалось отозвать лицензию",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePid(value: String) {
|
||||
_generateState.update { it.copy(pid = value, error = null) }
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import ru.mailib.ttrpg.licensemanager.ui.formatLicenseExpiry
|
||||
fun LicenseListScreen(viewModel: LicenseViewModel) {
|
||||
val state by viewModel.listState.collectAsStateWithLifecycle()
|
||||
var confirmDelete by remember { mutableStateOf<LicenseEntry?>(null) }
|
||||
var confirmRevoke by remember { mutableStateOf<LicenseEntry?>(null) }
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = state.isLoading,
|
||||
onRefresh = viewModel::loadLicenses,
|
||||
@@ -81,6 +82,31 @@ fun LicenseListScreen(viewModel: LicenseViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
confirmRevoke?.let { license ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirmRevoke = null },
|
||||
title = { Text(stringResource(R.string.revoke_key_title)) },
|
||||
text = {
|
||||
Text(stringResource(R.string.revoke_key_message, license.key))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.revokeLicense(license.key)
|
||||
confirmRevoke = null
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.revoke))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { confirmRevoke = null }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -120,7 +146,9 @@ fun LicenseListScreen(viewModel: LicenseViewModel) {
|
||||
LicenseCard(
|
||||
license = license,
|
||||
isDeleting = state.deletingKey == license.key,
|
||||
isRevoking = state.revokingKey == license.key,
|
||||
onDelete = { confirmDelete = license },
|
||||
onRevoke = { confirmRevoke = license },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +167,9 @@ fun LicenseListScreen(viewModel: LicenseViewModel) {
|
||||
private fun LicenseCard(
|
||||
license: LicenseEntry,
|
||||
isDeleting: Boolean,
|
||||
isRevoking: Boolean,
|
||||
onDelete: () -> Unit,
|
||||
onRevoke: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -172,10 +202,27 @@ private fun LicenseCard(
|
||||
if (license.activatedDevices.isNotEmpty()) {
|
||||
DetailRow("deviceId", license.activatedDevices.joinToString(", "))
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (!license.revoked) {
|
||||
OutlinedButton(
|
||||
onClick = onRevoke,
|
||||
enabled = !isDeleting && !isRevoking,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
) {
|
||||
if (isRevoking) {
|
||||
CircularProgressIndicator(strokeWidth = 2.dp)
|
||||
} else {
|
||||
Text(stringResource(R.string.revoke))
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onDelete,
|
||||
enabled = !isDeleting,
|
||||
enabled = !isDeleting && !isRevoking,
|
||||
) {
|
||||
if (isDeleting) {
|
||||
CircularProgressIndicator(strokeWidth = 2.dp)
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
<string name="settings">Настройки</string>
|
||||
|
||||
<string name="delete">Удалить</string>
|
||||
<string name="revoke">Отозвать</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="delete_key_title">Удалить ключ?</string>
|
||||
<string name="delete_key_message">Это действие необратимо.</string>
|
||||
<string name="delete_key_message">Ключ будет удалён с сервера безвозвратно.</string>
|
||||
<string name="revoke_key_title">Отозвать лицензию?</string>
|
||||
<string name="revoke_key_message">Ключ %1$s перестанет работать у всех пользователей.</string>
|
||||
<string name="no_licenses">Лицензий пока нет</string>
|
||||
<string name="created_at">Создан</string>
|
||||
<string name="devices">Устройства</string>
|
||||
|
||||
Reference in New Issue
Block a user