Let a client account expire on a date

Staff can give a client an expiry date on the create and edit screens,
and through /api/v1/clients. When the date passes, the client is refused
at sign-in and on their next request, and their API access ends too.
Files and history stay, and a later date (or none) brings them back.

Access is checked through one predicate, User::maySignIn(), at every
door: sign-in, the web session, API tokens and the two-factor
challenge. An hourly sweep also switches `active` off, so the list,
its filter and seat counts agree. The sweep is not what enforces it,
so a scheduler that is not running cannot keep an account open.

An account cannot be active with a date that has passed. Reactivating
an expired client needs a new date in the same save.

The day-means-end-of-day-where-you-are rule moved out of FileExpiry
into a shared DateInput, so file and account expiry read dates the
same way.

Requested by @Drardollan in #1310.
This commit is contained in:
ignacionelson
2026-09-13 14:57:16 -03:00
parent 7c7ba7cd53
commit c21658f6f7
25 changed files with 761 additions and 51 deletions
+25
View File
@@ -808,6 +808,14 @@
],
"minimum": 0
},
"expires_at": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "When the account stops working; omit or send null for never.\nA bare date (`2026-12-31`) means the end of that day in the\ntoken owner's timezone; a full timestamp is used as given.\nMust be in the future."
},
"custom_field_values": {
"type": "array",
"items": {
@@ -945,6 +953,14 @@
],
"minimum": 0
},
"expires_at": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "Send null to remove the expiry. Read the same way as on\ncreate. An account cannot be active with a date that has\npassed, so reactivating an expired client needs a new date\n(or null) in the same request."
},
"custom_field_values": {
"type": "array",
"items": {
@@ -3534,6 +3550,7 @@
"client.invitation_redeemed",
"client.invitation_revoked",
"client.invitation_resent",
"client.expired",
"file.uploaded",
"file.updated",
"file.deleted",
@@ -3730,6 +3747,13 @@
"type": "boolean",
"description": "Whether, not what: the state of the second factor is what a\ncaller needs to see before removing it. The secret and the\nrecovery codes stay where they are."
},
"expires_at": {
"type": [
"string",
"null"
],
"description": "Null when the account never expires. Once this passes the\nclient can no longer sign in, and `active` turns false within\nthe hour."
},
"created_at": {
"type": [
"string",
@@ -3810,6 +3834,7 @@
"active",
"account_requested",
"two_factor_enabled",
"expires_at",
"created_at",
"updated_at",
"storage",