Skip to content

Commit f969f2f

Browse files
authored
refactor: simplify license access (#14)
1 parent dcdf2e5 commit f969f2f

12 files changed

Lines changed: 40 additions & 226 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This repository contains the public goilerplate CLI and the shared API contract.
77
## Brand
88

99
- Always write the product name as `goilerplate` in lowercase, including at the start of sentences and headings.
10-
- Keep technical identifiers such as `GOILERPLATE_TOKEN` and `X-Goilerplate-Version` unchanged.
10+
- Keep technical identifiers such as `X-Goilerplate-Version` unchanged.
1111

1212
## Engineering
1313

README.md

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ goilerplate new
2929

3030
`new` opens a small terminal wizard. Choose a project name, module path, database, frontend, and the features you need. Review the command, press Enter, and get an ordinary Go repository on your machine.
3131

32-
Prefer explicit flags for scripts or CI:
32+
Prefer explicit flags for repeatable commands:
3333

3434
```text
3535
goilerplate new --name Acme --module example.com/acme ./acme
@@ -38,7 +38,7 @@ goilerplate new --edition paid --name Acme --module example.com/acme --framework
3838

3939
The Free edition is a fixed htmx, SQLite, and SMTP foundation. Paid generation adds htmx or Datastar, PostgreSQL, payment providers, teams, OAuth, storage, content, and the Projects example.
4040

41-
The website builder produces the same flags. The terminal wizard, copied commands, and CI all use one generation path.
41+
The website builder produces the same flags. The terminal wizard and copied commands use one generation path.
4242

4343
## Update a project
4444

@@ -65,16 +65,7 @@ goilerplate license invite <license-id> developer@example.com
6565
goilerplate license remove <license-id> <user-or-invitation-id>
6666
```
6767

68-
`whoami` prints the license ID. Owners invite or remove people. A company always keeps at least one Owner.
69-
70-
CI uses a separate generation-only token:
71-
72-
```text
73-
goilerplate token create <license-id> deploy
74-
GOILERPLATE_TOKEN="$GOILERPLATE_TOKEN" goilerplate new --module example.com/acme ./acme
75-
```
76-
77-
Store the token in the CI provider's secret store. It cannot manage people or other tokens.
68+
`whoami` prints `Access: Free` or `Access: Paid`. Paid users also see their license IDs. Owners invite or remove people. A company always keeps at least one Owner.
7869

7970
## Useful commands
8071

api/api.go

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ type ConfirmLicenseClaimRequest struct {
4040
}
4141

4242
type WhoAmIResponse struct {
43-
Account Account `json:"account"`
44-
Licenses []License `json:"licenses"`
45-
EffectiveLicenseID string `json:"effective_license_id,omitempty"`
43+
Account Account `json:"account"`
44+
Licenses []License `json:"licenses"`
4645
}
4746

4847
type Account struct {
@@ -53,23 +52,13 @@ type Account struct {
5352

5453
type License struct {
5554
ID string `json:"id"`
56-
Tier LicenseTier `json:"tier"`
5755
Status LicenseStatus `json:"status"`
5856
Role LicenseRole `json:"role"`
5957
}
6058

61-
type LicenseTier string
62-
63-
const (
64-
LicenseTierFree LicenseTier = "free"
65-
LicenseTierPaid LicenseTier = "paid"
66-
LicenseTierGrandfathered LicenseTier = "grandfathered"
67-
)
68-
6959
type LicenseStatus string
7060

7161
const (
72-
LicenseStatusPending LicenseStatus = "pending"
7362
LicenseStatusActive LicenseStatus = "active"
7463
LicenseStatusRevoked LicenseStatus = "revoked"
7564
)
@@ -109,26 +98,6 @@ type InviteLicenseMemberResponse struct {
10998
Joined bool `json:"joined"`
11099
}
111100

112-
type CreateLicenseTokenRequest struct {
113-
Name string `json:"name"`
114-
}
115-
116-
type CreateLicenseTokenResponse struct {
117-
Token LicenseToken `json:"token"`
118-
Value string `json:"value"`
119-
}
120-
121-
type LicenseTokensResponse struct {
122-
Tokens []LicenseToken `json:"tokens"`
123-
}
124-
125-
type LicenseToken struct {
126-
ID string `json:"id"`
127-
Name string `json:"name"`
128-
RevokedAt *time.Time `json:"revoked_at,omitempty"`
129-
CreatedAt time.Time `json:"created_at"`
130-
}
131-
132101
type DeleteAccountRequest struct {
133102
ConfirmGitHubLogin string `json:"confirm_github_login"`
134103
}

cmd/goilerplate/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ func run(ctx context.Context) error {
5050
GitHubClientID: githubClientID,
5151
DefaultAPIURL: "https://goilerplate.com",
5252
APIURLOverride: strings.TrimSpace(os.Getenv("GOILERPLATE_API_URL")),
53-
MachineToken: strings.TrimSpace(os.Getenv("GOILERPLATE_TOKEN")),
5453
Version: buildVersion(),
5554
FetchReleases: func(ctx context.Context) ([]github.Release, error) {
5655
return github.ListReleases(ctx, httpClient)

internal/cli/app.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ type ServiceClient interface {
3232
LicenseMembers(context.Context, string, string) (api.LicenseMembersResponse, error)
3333
InviteLicenseMember(context.Context, string, string, api.InviteLicenseMemberRequest) (api.InviteLicenseMemberResponse, error)
3434
RemoveLicenseMember(context.Context, string, string, string) error
35-
CreateLicenseToken(context.Context, string, string, string) (api.CreateLicenseTokenResponse, error)
36-
LicenseTokens(context.Context, string, string) (api.LicenseTokensResponse, error)
37-
RevokeLicenseToken(context.Context, string, string, string) error
3835
DeleteAccount(context.Context, string, string) error
3936
Generate(context.Context, string, api.GenerateRequest, io.Writer) (string, error)
4037
UpdateTree(context.Context, string, api.GenerateRequest, io.Writer) (string, error)
@@ -50,7 +47,6 @@ type App struct {
5047
GitHubClientID string
5148
DefaultAPIURL string
5249
APIURLOverride string
53-
MachineToken string
5450
Version string
5551
WorkingDirectory string
5652
RunNewProjectWizard func(context.Context) ([]string, error)
@@ -103,8 +99,6 @@ func (a *App) Run(ctx context.Context, arguments []string) error {
10399
return a.claim(ctx, arguments[1:])
104100
case "license":
105101
return a.license(ctx, arguments[1:])
106-
case "token":
107-
return a.token(ctx, arguments[1:])
108102
case "account":
109103
return a.account(ctx, arguments[1:])
110104
case "changelog":
@@ -139,7 +133,6 @@ func (a *App) help(arguments []string) error {
139133
"logout": {"goilerplate logout", "Revoke the current goilerplate session."},
140134
"claim": {"goilerplate claim <purchase-email>", "goilerplate claim --code <code>"},
141135
"license": {"goilerplate license members|invite|remove ...", "Manage people on a company license."},
142-
"token": {"goilerplate token create|list|revoke ...", "Manage generation-only CI keys."},
143136
"account": {"goilerplate account delete --confirm <github-login>", "Delete the current account."},
144137
"changelog": {"goilerplate changelog", "Show published GitHub release notes."},
145138
"doctor": {"goilerplate doctor", "Check tools and configuration for a generated project."},
@@ -228,13 +221,20 @@ func (a *App) whoAmI(ctx context.Context, arguments []string) error {
228221
return err
229222
}
230223
fmt.Fprintf(a.Out, "@%s <%s>\n", identity.Account.GitHubLogin, identity.Account.Email)
231-
fmt.Fprintln(a.Out, " LICENSE ID TIER STATUS ROLE")
224+
access := "Free"
232225
for _, license := range identity.Licenses {
233-
marker := " "
234-
if license.ID == identity.EffectiveLicenseID {
235-
marker = "*"
226+
if license.Status == api.LicenseStatusActive {
227+
access = "Paid"
228+
break
236229
}
237-
fmt.Fprintf(a.Out, "%s %s %s %s %s\n", marker, license.ID, license.Tier, license.Status, license.Role)
230+
}
231+
fmt.Fprintf(a.Out, "Access: %s\n", access)
232+
if len(identity.Licenses) == 0 {
233+
return nil
234+
}
235+
fmt.Fprintln(a.Out, " LICENSE ID STATUS ROLE")
236+
for _, license := range identity.Licenses {
237+
fmt.Fprintf(a.Out, " %s %s %s\n", license.ID, license.Status, license.Role)
238238
}
239239
return nil
240240
}
@@ -286,7 +286,6 @@ func (a *App) printUsage() {
286286
fmt.Fprintln(a.Out, " claim Connect a purchase made with another email")
287287
fmt.Fprintln(a.Out, " whoami Show the current account and licenses")
288288
fmt.Fprintln(a.Out, " license Invite, list, or remove license members")
289-
fmt.Fprintln(a.Out, " token Create, list, or revoke CI keys")
290289
fmt.Fprintln(a.Out, " account delete Delete the current account")
291290
fmt.Fprintln(a.Out, " changelog Show published release notes")
292291
fmt.Fprintln(a.Out, " doctor Check a generated project's local tools")

internal/cli/app_test.go

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -160,23 +160,21 @@ func TestLoginRevokesNewSessionWhenSavingFails(t *testing.T) {
160160
}
161161
}
162162

163-
func TestWhoAmIShowsEffectiveLicense(t *testing.T) {
163+
func TestWhoAmIShowsPaidAccessAndLicenses(t *testing.T) {
164164
output := &bytes.Buffer{}
165165
store := &memoryStore{configuration: config.Config{APIURL: "https://goilerplate.com", SessionToken: "session"}}
166166
service := &fakeService{who: api.WhoAmIResponse{
167167
Account: api.Account{GitHubLogin: "axadrn", Email: "hello@example.com"},
168168
Licenses: []api.License{
169-
{ID: "free", Tier: api.LicenseTierFree, Status: api.LicenseStatusActive, Role: api.LicenseRoleOwner},
170-
{ID: "paid", Tier: api.LicenseTierPaid, Status: api.LicenseStatusActive, Role: api.LicenseRoleMember},
169+
{ID: "paid", Status: api.LicenseStatusActive, Role: api.LicenseRoleMember},
171170
},
172-
EffectiveLicenseID: "paid",
173171
}}
174172
app := testApp(output, store, &fakeDevice{}, service)
175173

176174
if err := app.Run(context.Background(), []string{"whoami"}); err != nil {
177175
t.Fatal(err)
178176
}
179-
if !strings.Contains(output.String(), "* paid paid active member") {
177+
if !strings.Contains(output.String(), "Access: Paid") || !strings.Contains(output.String(), "paid active member") {
180178
t.Fatalf("output = %q", output.String())
181179
}
182180
}
@@ -208,25 +206,19 @@ func TestLogoutPreservesTokenWhenRevocationFails(t *testing.T) {
208206
}
209207
}
210208

211-
func TestLicenseAndTokenCommandsUseExplicitLicenseIDs(t *testing.T) {
209+
func TestLicenseCommandsUseExplicitLicenseIDs(t *testing.T) {
212210
output := &bytes.Buffer{}
213211
store := &memoryStore{configuration: config.Config{APIURL: "https://goilerplate.com", SessionToken: "session"}}
214212
service := &fakeService{
215213
members: api.LicenseMembersResponse{Members: []api.LicenseMember{{
216214
UserID: "user-1", GitHubLogin: "developer", Email: "developer@example.com", Role: api.LicenseRoleMember,
217215
}}},
218-
createdToken: api.CreateLicenseTokenResponse{
219-
Token: api.LicenseToken{ID: "token-1", Name: "deploy"}, Value: "gok_secret",
220-
},
221216
}
222217
app := testApp(output, store, &fakeDevice{}, service)
223218
if err := app.Run(context.Background(), []string{"license", "members", "license-1"}); err != nil {
224219
t.Fatal(err)
225220
}
226-
if err := app.Run(context.Background(), []string{"token", "create", "license-1", "deploy"}); err != nil {
227-
t.Fatal(err)
228-
}
229-
if !strings.Contains(output.String(), "@developer") || !strings.Contains(output.String(), "gok_secret") || !strings.Contains(output.String(), "shown only once") {
221+
if !strings.Contains(output.String(), "@developer") {
230222
t.Fatalf("output = %q", output.String())
231223
}
232224
}
@@ -274,7 +266,7 @@ func TestClaimValidatesArgumentsBeforeLogin(t *testing.T) {
274266
}
275267

276268
func TestHelpCoversEveryCommand(t *testing.T) {
277-
for _, command := range []string{"new", "update", "login", "whoami", "logout", "claim", "license", "token", "account", "changelog", "doctor", "version"} {
269+
for _, command := range []string{"new", "update", "login", "whoami", "logout", "claim", "license", "account", "changelog", "doctor", "version"} {
278270
t.Run(command, func(t *testing.T) {
279271
output := &bytes.Buffer{}
280272
app := testApp(output, &memoryStore{}, &fakeDevice{}, &fakeService{})
@@ -433,21 +425,6 @@ func TestNewWithoutArgumentsRequiresInteractiveTerminal(t *testing.T) {
433425
}
434426
}
435427

436-
func TestNewUsesMachineTokenWithoutPersonalLogin(t *testing.T) {
437-
store := &memoryStore{}
438-
service := &fakeService{generatedVersion: "v3.0.0", archive: cliTestArchive(t, "go.mod", "module example.com/acme")}
439-
app := testApp(&bytes.Buffer{}, store, &fakeDevice{}, service)
440-
app.MachineToken = "gok_machine"
441-
if err := app.Run(context.Background(), []string{
442-
"new", "--module", "example.com/acme", filepath.Join(t.TempDir(), "acme"),
443-
}); err != nil {
444-
t.Fatal(err)
445-
}
446-
if service.generateToken != "gok_machine" || service.receivedGitHubToken != "" {
447-
t.Fatalf("generate token = %q, GitHub token = %q", service.generateToken, service.receivedGitHubToken)
448-
}
449-
}
450-
451428
func TestNewRejectsPaidModulesForFreeBeforeCallingService(t *testing.T) {
452429
service := &fakeService{}
453430
app := testApp(&bytes.Buffer{}, &memoryStore{}, &fakeDevice{}, service)
@@ -591,11 +568,8 @@ type fakeService struct {
591568
updateRequests []api.GenerateRequest
592569
generateCalled bool
593570
members api.LicenseMembersResponse
594-
tokens api.LicenseTokensResponse
595-
createdToken api.CreateLicenseTokenResponse
596571
invited api.InviteLicenseMemberResponse
597572
removedMember string
598-
revokedToken string
599573
deletedAccount string
600574
claimEmail string
601575
claimCode string
@@ -663,19 +637,6 @@ func (s *fakeService) RemoveLicenseMember(_ context.Context, _ string, _ string,
663637
return nil
664638
}
665639

666-
func (s *fakeService) CreateLicenseToken(context.Context, string, string, string) (api.CreateLicenseTokenResponse, error) {
667-
return s.createdToken, nil
668-
}
669-
670-
func (s *fakeService) LicenseTokens(context.Context, string, string) (api.LicenseTokensResponse, error) {
671-
return s.tokens, nil
672-
}
673-
674-
func (s *fakeService) RevokeLicenseToken(_ context.Context, _ string, _ string, tokenID string) error {
675-
s.revokedToken = tokenID
676-
return nil
677-
}
678-
679640
func (s *fakeService) DeleteAccount(_ context.Context, _ string, confirmation string) error {
680641
s.deletedAccount = confirmation
681642
return nil

internal/cli/license.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (a *App) license(ctx context.Context, arguments []string) error {
7070
if err := client.RemoveLicenseMember(ctx, token, arguments[1], arguments[2]); err != nil {
7171
return err
7272
}
73-
fmt.Fprintln(a.Out, "Access removed. Company CI keys were revoked when an active member was removed.")
73+
fmt.Fprintln(a.Out, "Access removed")
7474
return nil
7575
default:
7676
return fmt.Errorf("unknown license command %q", arguments[0])

internal/cli/new.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,14 @@ func (a *App) newProject(ctx context.Context, arguments []string) error {
7979
if err != nil {
8080
return err
8181
}
82-
authorizationToken := strings.TrimSpace(a.MachineToken)
83-
if authorizationToken == "" {
84-
if configuration.SessionToken == "" {
85-
if err := a.login(ctx, nil); err != nil {
86-
return err
87-
}
88-
configuration, err = a.Store.Load()
89-
if err != nil {
90-
return err
91-
}
82+
if configuration.SessionToken == "" {
83+
if err := a.login(ctx, nil); err != nil {
84+
return err
85+
}
86+
configuration, err = a.Store.Load()
87+
if err != nil {
88+
return err
9289
}
93-
authorizationToken = configuration.SessionToken
9490
}
9591
client, err := a.NewService(a.apiURL(configuration))
9692
if err != nil {
@@ -108,7 +104,7 @@ func (a *App) newProject(ctx context.Context, arguments []string) error {
108104
if a.Version != "" && a.Version != "dev" {
109105
templateVersion = a.Version
110106
}
111-
generatedVersion, err := client.Generate(ctx, authorizationToken, api.GenerateRequest{
107+
generatedVersion, err := client.Generate(ctx, configuration.SessionToken, api.GenerateRequest{
112108
TemplateVersion: templateVersion,
113109
Answers: answers,
114110
}, archive)

0 commit comments

Comments
 (0)