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")
|
@POST("v1/admin/product-keys/delete")
|
||||||
suspend fun deleteProductKey(@Body body: DeleteProductKeyRequest): DeleteProductKeyResponse
|
suspend fun deleteProductKey(@Body body: DeleteProductKeyRequest): DeleteProductKeyResponse
|
||||||
|
|
||||||
|
@POST("v1/admin/revoke")
|
||||||
|
suspend fun revokeLicense(@Body body: RevokeLicenseRequest): RevokeLicenseResponse
|
||||||
|
|
||||||
@GET("v1/admin/stats/downloads")
|
@GET("v1/admin/stats/downloads")
|
||||||
suspend fun getDownloadStats(): DownloadStatsOverview
|
suspend fun getDownloadStats(): DownloadStatsOverview
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,14 @@ data class DeleteProductKeyResponse(
|
|||||||
val deleted: String? = null,
|
val deleted: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class RevokeLicenseRequest(
|
||||||
|
val key: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RevokeLicenseResponse(
|
||||||
|
val ok: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
data class DownloadStatsMonth(
|
data class DownloadStatsMonth(
|
||||||
val month: String,
|
val month: String,
|
||||||
val windows: Int,
|
val windows: Int,
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ class LicenseRepository(
|
|||||||
createApi(settings).deleteProductKey(DeleteProductKeyRequest(key))
|
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 {
|
suspend fun getDownloadStatsOverview(settings: AppSettings): Result<DownloadStatsOverview> = runCatching {
|
||||||
createApi(settings).getDownloadStats()
|
createApi(settings).getDownloadStats()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ data class LicenseListUiState(
|
|||||||
val licenses: List<LicenseEntry> = emptyList(),
|
val licenses: List<LicenseEntry> = emptyList(),
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val deletingKey: String? = null,
|
val deletingKey: String? = null,
|
||||||
|
val revokingKey: String? = null,
|
||||||
val error: 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) {
|
fun updatePid(value: String) {
|
||||||
_generateState.update { it.copy(pid = value, error = null) }
|
_generateState.update { it.copy(pid = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import ru.mailib.ttrpg.licensemanager.ui.formatLicenseExpiry
|
|||||||
fun LicenseListScreen(viewModel: LicenseViewModel) {
|
fun LicenseListScreen(viewModel: LicenseViewModel) {
|
||||||
val state by viewModel.listState.collectAsStateWithLifecycle()
|
val state by viewModel.listState.collectAsStateWithLifecycle()
|
||||||
var confirmDelete by remember { mutableStateOf<LicenseEntry?>(null) }
|
var confirmDelete by remember { mutableStateOf<LicenseEntry?>(null) }
|
||||||
|
var confirmRevoke by remember { mutableStateOf<LicenseEntry?>(null) }
|
||||||
val refreshState = rememberPullRefreshState(
|
val refreshState = rememberPullRefreshState(
|
||||||
refreshing = state.isLoading,
|
refreshing = state.isLoading,
|
||||||
onRefresh = viewModel::loadLicenses,
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -120,7 +146,9 @@ fun LicenseListScreen(viewModel: LicenseViewModel) {
|
|||||||
LicenseCard(
|
LicenseCard(
|
||||||
license = license,
|
license = license,
|
||||||
isDeleting = state.deletingKey == license.key,
|
isDeleting = state.deletingKey == license.key,
|
||||||
|
isRevoking = state.revokingKey == license.key,
|
||||||
onDelete = { confirmDelete = license },
|
onDelete = { confirmDelete = license },
|
||||||
|
onRevoke = { confirmRevoke = license },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +167,9 @@ fun LicenseListScreen(viewModel: LicenseViewModel) {
|
|||||||
private fun LicenseCard(
|
private fun LicenseCard(
|
||||||
license: LicenseEntry,
|
license: LicenseEntry,
|
||||||
isDeleting: Boolean,
|
isDeleting: Boolean,
|
||||||
|
isRevoking: Boolean,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
|
onRevoke: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -172,10 +202,27 @@ private fun LicenseCard(
|
|||||||
if (license.activatedDevices.isNotEmpty()) {
|
if (license.activatedDevices.isNotEmpty()) {
|
||||||
DetailRow("deviceId", license.activatedDevices.joinToString(", "))
|
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(
|
OutlinedButton(
|
||||||
onClick = onDelete,
|
onClick = onDelete,
|
||||||
enabled = !isDeleting,
|
enabled = !isDeleting && !isRevoking,
|
||||||
) {
|
) {
|
||||||
if (isDeleting) {
|
if (isDeleting) {
|
||||||
CircularProgressIndicator(strokeWidth = 2.dp)
|
CircularProgressIndicator(strokeWidth = 2.dp)
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
<string name="settings">Настройки</string>
|
<string name="settings">Настройки</string>
|
||||||
|
|
||||||
<string name="delete">Удалить</string>
|
<string name="delete">Удалить</string>
|
||||||
|
<string name="revoke">Отозвать</string>
|
||||||
<string name="cancel">Отмена</string>
|
<string name="cancel">Отмена</string>
|
||||||
<string name="delete_key_title">Удалить ключ?</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="no_licenses">Лицензий пока нет</string>
|
||||||
<string name="created_at">Создан</string>
|
<string name="created_at">Создан</string>
|
||||||
<string name="devices">Устройства</string>
|
<string name="devices">Устройства</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user