Feature/license valid until - #6
Conversation
Allows manually issued licenses to carry a fixed expiry date (e.g. invoice-based one-year licenses). Empty keeps the existing behavior: trial plans expire after trial_days, others are perpetual.
POST /admin/licenses/:id/valid-until with an RFC 3339 timestamp sets the expiry; an empty value clears it (perpetual). Status is deliberately untouched — reinstating an expired license remains an explicit separate action.
The issue dialog gains an optional date field (empty = perpetual; trial plans keep their trial_days default). The detail view shows a pencil next to Valid Until for non-Stripe licenses — Stripe-billed expiry is renewal-managed, so editing it there stays hidden. Dates are sent as end-of-day UTC.
Previously the picked date became 23:59:59 UTC, which renders as a confusing mid-evening time for admins west of Greenwich. The date input's prefill is converted back the same way so editing doesn't shift the day.
|
I was about to add something like this myself, as it's half of what I need to support perpetual-fallback licensing. I'll submit a PR for the other half (putting the expiry date in the license) after this lands. |
|
@pjz There does not seem to be any active development on this repo, at least it's been a few weeks since I submitted this PR with no comments or feedback. It's a great project, hopefully it does not die. I have several other PRs I'd like to make. We shall see! |
|
@KurtJacobson If you make a public fork of this repo with your features, I'd likely base my license server off of it - I need this feature and one other (putting the expiry date into the signed license handed to the app) to be able to nicely implement a permanent-fallback licensing scheme, which is what I want to use. |
|
@pjz I actually already have "JetBrains" style permanent-fallback licensing implemented, is this what you are looking for? I call it "Perpetual + paid support". This scheme gates updates on a "support period" that's independent of the license itself. The license never expires, but the paid support/updates period does. Customers retain access to any release published while their support was active, but have to be current on their support to get updates. Here is more info: https://kurtjacobson.github.io/keygate/concepts/support-window/ And my fork is public: https://github.com/kurtjacobson/keygate Docker images are available: docker pull ghcr.io/kurtjacobson/keygate:main |
|
Sorry for the delay on this one. Went through it properly — built it in a container and exercised the endpoints. Everything behaves as documented: an explicit date wins over the trial default, empty stays perpetual, past and malformed dates are rejected on create, and the edit endpoint sets and clears correctly with an audit entry each time. Two follow-ups I'd like to do separately, neither blocking:
Happy to take the other PRs you mentioned. |
|
Merged in #12. I said I'd handle the two follow-ups separately, but ended up folding them in rather than leaving the endpoint half-guarded:
One thing worth knowing for next time: the branch failed Good PR. The timezone round-trip in |
What does this PR do?
Adds the ability to set, edit, and clear an expiration date on manually issued licenses:
POST /admin/licenses accepts an optional valid_until (RFC 3339, must be in the future). Empty preserves existing behavior: trial plans default to now + trial_days, everything else is perpetual. An explicit value takes precedence over the trial default.
New endpoint POST /admin/licenses/:id/valid-until sets or clears the expiry on an existing license (empty = perpetual). It deliberately does not touch license status — re-arming an expired license remains an explicit /reinstate action, so a date edit can't silently reactivate a revoked customer. Changes are audit-logged.
Admin UI: optional "Valid until" date field in the issue dialog, and an edit control on the detail view (hidden for Stripe-billed licenses, whose expiry is renewal-managed). Dates mean end-of-day 23:59:59 in the admin's local timezone. Licenses without an expiry now display "Perpetual" instead of a dash. English and Chinese strings included.
Existing expiry enforcement (internal/service/expiry.go) is untouched - this only adds ways to set the date it already honors.
Why?
valid_untilis currently only set automatically (trial plans via trial_days, subscriptions via Stripe renewals), so fixed-term manual licensing — e.g. an invoice-based one-year license - isn't possible. This makes easy in the API and the admin dashboard.How to test
valid_until: "2020-01-01T00:00:00Z" > 400 (past date); malformed timestamp > 400.valid_untilhas passed (existing enforcement).Checklist
go vet ./...passesgo test ./...passescd web && bun run build)Screenshots