Skip to content

Commit c3fe251

Browse files
rodrimaiamtojek
andauthored
feat: add license expiration warning (#7264)
* wip: add expiration warning * Use GraceAt * show expiration warning for trial accounts * fix test * only show license banner for users with deployment permission --------- Co-authored-by: Marcin Tojek <marcin@coder.com>
1 parent 3eb7f06 commit c3fe251

File tree

4 files changed

+143
-3
lines changed

4 files changed

+143
-3
lines changed

enterprise/coderd/coderd_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func TestEntitlements(t *testing.T) {
5555
codersdk.FeatureAdvancedTemplateScheduling: 1,
5656
codersdk.FeatureWorkspaceProxy: 1,
5757
},
58+
GraceAt: time.Now().Add(59 * 24 * time.Hour),
5859
})
5960
res, err := client.Entitlements(context.Background())
6061
require.NoError(t, err)

enterprise/coderd/license/license.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/ed25519"
66
"fmt"
7+
"math"
78
"time"
89

910
"github.com/golang-jwt/jwt/v4"
@@ -70,6 +71,23 @@ func Entitlements(
7071
// LicenseExpires we must be in grace period.
7172
entitlement = codersdk.EntitlementGracePeriod
7273
}
74+
75+
// Add warning if license is expiring soon
76+
daysToExpire := int(math.Ceil(claims.LicenseExpires.Sub(now).Hours() / 24))
77+
isTrial := entitlements.Trial
78+
showWarningDays := 30
79+
if isTrial {
80+
showWarningDays = 7
81+
}
82+
isExpiringSoon := daysToExpire > 0 && daysToExpire < showWarningDays
83+
if isExpiringSoon {
84+
day := "day"
85+
if daysToExpire > 1 {
86+
day = "days"
87+
}
88+
entitlements.Warnings = append(entitlements.Warnings, fmt.Sprintf("Your license expires in %d %s.", daysToExpire, day))
89+
}
90+
7391
for featureName, featureValue := range claims.Features {
7492
// Can this be negative?
7593
if featureValue <= 0 {

enterprise/coderd/license/license_test.go

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,123 @@ func TestEntitlements(t *testing.T) {
102102
fmt.Sprintf("%s is enabled but your license for this feature is expired.", codersdk.FeatureAuditLog.Humanize()),
103103
)
104104
})
105+
t.Run("Expiration warning", func(t *testing.T) {
106+
t.Parallel()
107+
db := dbfake.New()
108+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
109+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
110+
Features: license.Features{
111+
codersdk.FeatureUserLimit: 100,
112+
codersdk.FeatureAuditLog: 1,
113+
},
114+
115+
GraceAt: time.Now().AddDate(0, 0, 2),
116+
ExpiresAt: time.Now().AddDate(0, 0, 5),
117+
}),
118+
Exp: time.Now().AddDate(0, 0, 5),
119+
})
120+
121+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
122+
123+
require.NoError(t, err)
124+
require.True(t, entitlements.HasLicense)
125+
require.False(t, entitlements.Trial)
126+
127+
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
128+
require.Contains(
129+
t, entitlements.Warnings,
130+
"Your license expires in 2 days.",
131+
)
132+
})
133+
134+
t.Run("Expiration warning for license expiring in 1 day", func(t *testing.T) {
135+
t.Parallel()
136+
db := dbfake.New()
137+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
138+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
139+
Features: license.Features{
140+
codersdk.FeatureUserLimit: 100,
141+
codersdk.FeatureAuditLog: 1,
142+
},
143+
144+
GraceAt: time.Now().AddDate(0, 0, 1),
145+
ExpiresAt: time.Now().AddDate(0, 0, 5),
146+
}),
147+
Exp: time.Now().AddDate(0, 0, 5),
148+
})
149+
150+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
151+
152+
require.NoError(t, err)
153+
require.True(t, entitlements.HasLicense)
154+
require.False(t, entitlements.Trial)
155+
156+
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
157+
require.Contains(
158+
t, entitlements.Warnings,
159+
"Your license expires in 1 day.",
160+
)
161+
})
162+
163+
t.Run("Expiration warning for trials", func(t *testing.T) {
164+
t.Parallel()
165+
db := dbfake.New()
166+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
167+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
168+
Features: license.Features{
169+
codersdk.FeatureUserLimit: 100,
170+
codersdk.FeatureAuditLog: 1,
171+
},
172+
173+
Trial: true,
174+
GraceAt: time.Now().AddDate(0, 0, 8),
175+
ExpiresAt: time.Now().AddDate(0, 0, 5),
176+
}),
177+
Exp: time.Now().AddDate(0, 0, 5),
178+
})
179+
180+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
181+
182+
require.NoError(t, err)
183+
require.True(t, entitlements.HasLicense)
184+
require.True(t, entitlements.Trial)
185+
186+
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
187+
require.NotContains( // it should not contain a warning since it is a trial license
188+
t, entitlements.Warnings,
189+
"Your license expires in 8 days.",
190+
)
191+
})
192+
193+
t.Run("Expiration warning for non trials", func(t *testing.T) {
194+
t.Parallel()
195+
db := dbfake.New()
196+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
197+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
198+
Features: license.Features{
199+
codersdk.FeatureUserLimit: 100,
200+
codersdk.FeatureAuditLog: 1,
201+
},
202+
203+
GraceAt: time.Now().AddDate(0, 0, 30),
204+
ExpiresAt: time.Now().AddDate(0, 0, 5),
205+
}),
206+
Exp: time.Now().AddDate(0, 0, 5),
207+
})
208+
209+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
210+
211+
require.NoError(t, err)
212+
require.True(t, entitlements.HasLicense)
213+
require.False(t, entitlements.Trial)
214+
215+
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
216+
require.NotContains( // it should not contain a warning since it is a trial license
217+
t, entitlements.Warnings,
218+
"Your license expires in 30 days.",
219+
)
220+
})
221+
105222
t.Run("SingleLicenseNotEntitled", func(t *testing.T) {
106223
t.Parallel()
107224
db := dbfake.New()
@@ -164,16 +281,18 @@ func TestEntitlements(t *testing.T) {
164281
Features: license.Features{
165282
codersdk.FeatureUserLimit: 10,
166283
},
284+
GraceAt: time.Now().Add(59 * 24 * time.Hour),
167285
}),
168-
Exp: time.Now().Add(time.Hour),
286+
Exp: time.Now().Add(60 * 24 * time.Hour),
169287
})
170288
db.InsertLicense(context.Background(), database.InsertLicenseParams{
171289
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
172290
Features: license.Features{
173291
codersdk.FeatureUserLimit: 1,
174292
},
293+
GraceAt: time.Now().Add(59 * 24 * time.Hour),
175294
}),
176-
Exp: time.Now().Add(time.Hour),
295+
Exp: time.Now().Add(60 * 24 * time.Hour),
177296
})
178297
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty)
179298
require.NoError(t, err)

site/src/components/Dashboard/DashboardLayout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ export const DashboardLayout: FC = () => {
2525
})
2626
const { error: updateCheckError, updateCheck } = updateCheckState.context
2727

28+
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
29+
2830
return (
2931
<DashboardProvider>
3032
<ServiceBanner />
31-
<LicenseBanner />
33+
{canViewDeployment && <LicenseBanner />}
3234

3335
<div className={styles.site}>
3436
<Navbar />

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy