Add ability to deactivate license

This commit is contained in:
Gregory Schier
2025-03-05 07:13:19 -08:00
parent 9ead45d67a
commit 7a1a0689b0
16 changed files with 218 additions and 36 deletions

File diff suppressed because one or more lines are too long

View File

@@ -5572,6 +5572,11 @@
"type": "string",
"const": "yaak-license:allow-check"
},
{
"description": "Enables the deactivate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:allow-deactivate"
},
{
"description": "Denies the activate command without any pre-configured scope.",
"type": "string",
@@ -5582,6 +5587,11 @@
"type": "string",
"const": "yaak-license:deny-check"
},
{
"description": "Denies the deactivate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:deny-deactivate"
},
{
"description": "Default permissions for the plugin",
"type": "string",

View File

@@ -5572,6 +5572,11 @@
"type": "string",
"const": "yaak-license:allow-check"
},
{
"description": "Enables the deactivate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:allow-deactivate"
},
{
"description": "Denies the activate command without any pre-configured scope.",
"type": "string",
@@ -5582,6 +5587,11 @@
"type": "string",
"const": "yaak-license:deny-check"
},
{
"description": "Denies the deactivate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:deny-deactivate"
},
{
"description": "Default permissions for the plugin",
"type": "string",

View File

@@ -8,4 +8,6 @@ export type ActivateLicenseResponsePayload = { activationId: string, };
export type CheckActivationResponsePayload = { active: boolean, };
export type DeactivateLicenseRequestPayload = { licenseKey: string, appVersion: string, appPlatform: string, };
export type LicenseCheckStatus = { "type": "personal_use", trial_ended: string, } | { "type": "commercial_use" } | { "type": "invalid_license" } | { "type": "trialing", end: string, };

View File

@@ -1,4 +1,4 @@
const COMMANDS: &[&str] = &["activate", "check"];
const COMMANDS: &[&str] = &["activate", "deactivate", "check"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();

View File

@@ -14,6 +14,12 @@ export function useLicense() {
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
});
const deactivate = useMutation<void, string, void>({
mutationKey: ['license.deactivate'],
mutationFn: () => invoke('plugin:yaak-license|deactivate'),
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
});
// Check the license again after a license is activated
useEffect(() => {
const unlisten = listen('license-activated', async () => {
@@ -27,12 +33,14 @@ export function useLicense() {
const CHECK_QUERY_KEY = ['license.check'];
const check = useQuery<void, string, LicenseCheckStatus>({
refetchInterval: 1000 * 60 * 60 * 12, // Refetch every 12 hours
refetchOnWindowFocus: false,
queryKey: CHECK_QUERY_KEY,
queryFn: () => invoke('plugin:yaak-license|check'),
});
return {
activate,
deactivate,
check,
} as const;
}

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-deactivate"
description = "Enables the deactivate command without any pre-configured scope."
commands.allow = ["deactivate"]
[[permission]]
identifier = "deny-deactivate"
description = "Denies the deactivate command without any pre-configured scope."
commands.deny = ["deactivate"]

View File

@@ -4,6 +4,7 @@ Default permissions for the plugin
- `allow-check`
- `allow-activate`
- `allow-deactivate`
## Permission Table
@@ -63,6 +64,32 @@ Enables the check command without any pre-configured scope.
Denies the check command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:allow-deactivate`
</td>
<td>
Enables the deactivate command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:deny-deactivate`
</td>
<td>
Denies the deactivate command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -1,3 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-check", "allow-activate"]
permissions = ["allow-check", "allow-activate", "allow-deactivate"]

View File

@@ -314,6 +314,16 @@
"type": "string",
"const": "deny-check"
},
{
"description": "Enables the deactivate command without any pre-configured scope.",
"type": "string",
"const": "allow-deactivate"
},
{
"description": "Denies the deactivate command without any pre-configured scope.",
"type": "string",
"const": "deny-deactivate"
},
{
"description": "Default permissions for the plugin",
"type": "string",

View File

@@ -1,5 +1,5 @@
use crate::errors::Result;
use crate::{activate_license, check_license, ActivateLicenseRequestPayload, LicenseCheckStatus};
use crate::{activate_license, check_license, deactivate_license, get_activation_id, ActivateLicenseRequestPayload, CheckActivationRequestPayload, DeactivateLicenseRequestPayload, LicenseCheckStatus};
use log::{debug, info};
use std::string::ToString;
use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
@@ -7,7 +7,10 @@ use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
#[command]
pub async fn check<R: Runtime>(app_handle: AppHandle<R>) -> Result<LicenseCheckStatus> {
debug!("Checking license");
check_license(&app_handle).await
check_license(&app_handle, CheckActivationRequestPayload{
app_platform: get_os().to_string(),
app_version: app_handle.package_info().version.to_string(),
}).await
}
#[command]
@@ -24,6 +27,19 @@ pub async fn activate<R: Runtime>(license_key: &str, window: WebviewWindow<R>) -
.await
}
#[command]
pub async fn deactivate<R: Runtime>(window: WebviewWindow<R>) -> Result<()> {
info!("Deactivating activation");
deactivate_license(
&window,
DeactivateLicenseRequestPayload {
app_platform: get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
},
)
.await
}
fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"

View File

@@ -8,9 +8,11 @@ mod commands;
mod errors;
mod license;
use crate::commands::{activate, check};
use crate::commands::{activate, check, deactivate};
pub use license::*;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-license").invoke_handler(generate_handler![check, activate]).build()
Builder::new("yaak-license")
.invoke_handler(generate_handler![check, activate, deactivate])
.build()
}

View File

@@ -5,7 +5,7 @@ use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::ops::Add;
use std::time::Duration;
use tauri::{is_dev, AppHandle, Emitter, Runtime, WebviewWindow};
use tauri::{is_dev, AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use ts_rs::TS;
use yaak_models::queries::UpdateSource;
@@ -17,7 +17,8 @@ const TRIAL_SECONDS: u64 = 3600 * 24 * 30;
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct CheckActivationRequestPayload {
pub activation_id: String,
pub app_version: String,
pub app_platform: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
@@ -36,6 +37,14 @@ pub struct ActivateLicenseRequestPayload {
pub app_platform: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "license.ts")]
pub struct DeactivateLicenseRequestPayload {
pub app_version: String,
pub app_platform: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "license.ts")]
@@ -56,7 +65,7 @@ pub async fn activate_license<R: Runtime>(
p: ActivateLicenseRequestPayload,
) -> Result<()> {
let client = reqwest::Client::new();
let response = client.post(build_url("/activate")).json(&p).send().await?;
let response = client.post(build_url("/licenses/activate")).json(&p).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
@@ -86,6 +95,44 @@ pub async fn activate_license<R: Runtime>(
Ok(())
}
pub async fn deactivate_license<R: Runtime>(
window: &WebviewWindow<R>,
p: DeactivateLicenseRequestPayload,
) -> Result<()> {
let activation_id = get_activation_id(window).await;
let client = reqwest::Client::new();
let path = format!("/licenses/activations/{}/deactivate", activation_id);
let response = client.post(build_url(&path)).json(&p).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
return Err(ClientError {
message: body.message,
error: body.error,
});
}
if response.status().is_server_error() {
return Err(ServerError);
}
yaak_models::queries::delete_key_value(
window,
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
&UpdateSource::Window,
)
.await;
if let Err(e) = window.emit("license-deactivated", true) {
warn!("Failed to emit deactivate-license event: {}", e);
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "license.ts")]
@@ -96,15 +143,8 @@ pub enum LicenseCheckStatus {
Trialing { end: NaiveDateTime },
}
pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<LicenseCheckStatus> {
let activation_id = yaak_models::queries::get_key_value_string(
app_handle,
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
"",
)
.await;
pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>, payload: CheckActivationRequestPayload) -> Result<LicenseCheckStatus> {
let activation_id = get_activation_id(app_handle).await;
let settings = yaak_models::queries::get_or_create_settings(app_handle).await;
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
@@ -122,10 +162,8 @@ pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Lice
info!("Checking license activation");
// A license has been activated, so let's check the license server
let client = reqwest::Client::new();
let payload = CheckActivationRequestPayload {
activation_id: activation_id.clone(),
};
let response = client.post(build_url("/check")).json(&payload).send().await?;
let path = format!("/licenses/activations/{activation_id}/check");
let response = client.post(build_url(&path)).json(&payload).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
@@ -151,8 +189,13 @@ pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Lice
fn build_url(path: &str) -> String {
if is_dev() {
format!("http://localhost:9444/licenses{path}")
format!("http://localhost:9444{path}")
} else {
format!("https://license.yaak.app/licenses{path}")
format!("https://license.yaak.app{path}")
}
}
pub async fn get_activation_id<R: Runtime>(mgr: &impl Manager<R>) -> String {
yaak_models::queries::get_key_value_string(mgr, KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
.await
}

View File

@@ -134,6 +134,31 @@ pub async fn set_key_value_raw<R: Runtime>(
(m, existing.is_none())
}
pub async fn delete_key_value<R: Runtime>(
w: &WebviewWindow<R>,
namespace: &str,
key: &str,
update_source: &UpdateSource,
) {
let kv = match get_key_value_raw(w, namespace, key).await {
None => return,
Some(m) => m,
};
let dbm = &*w.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::delete()
.from_table(KeyValueIden::Table)
.cond_where(
Cond::all()
.add(Expr::col(KeyValueIden::Namespace).eq(namespace))
.add(Expr::col(KeyValueIden::Key).eq(key)),
)
.build_rusqlite(SqliteQueryBuilder);
db.execute(sql.as_str(), &*params.as_params()).expect("Failed to delete PluginKeyValue");
emit_deleted_model(w, &AnyModel::KeyValue(kv.to_owned()), update_source);
}
pub async fn list_key_values_raw<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<KeyValue>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();