diff --git a/CHANGELOG.md b/CHANGELOG.md index efeb7170..c6da0310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ FEATURE: New metrics infrastructure based on OpenZiti usage events (https://gith FEATURE: New limits implementation based on the new metrics infrastructure (https://github.com/openziti/zrok/issues/235). See the [v0.4 Limits Guide](docs/guides/metrics-and-limits/configuring-limits.md) for more information. +FEATURE: The invite mechanism has been reworked to improve user experience. The configuration has been moved to the `admin` stanza of the controller configuration and now includes a boolean flag indicating whether or not the instance allows new invitations to be created, and also includes contact details for requesting a new invite. These values are used by the `zrok invite` command to provide a smoother end-user invite experience https://github.com/openziti/zrok/issues/229) + CHANGE: The controller configuration version bumps from `v: 2` to `v: 3` to support all of the new `v0.4` functionality. See the [example ctrl.yml](etc/ctrl.yml) for details on the new configuration. CHANGE: The underlying database store now utilizes a `deleted` flag on all tables to implement "soft deletes". This was necessary for the new metrics infrastructure, where we need to account for metrics data that arrived after the lifetime of a share or environment; and also we're going to need this for limits, where we need to see historical information about activity in the past (https://github.com/openziti/zrok/issues/262) diff --git a/cmd/zrok/invite.go b/cmd/zrok/invite.go index 11e14e00..ed33cf26 100644 --- a/cmd/zrok/invite.go +++ b/cmd/zrok/invite.go @@ -62,7 +62,13 @@ func (cmd *inviteCommand) run(_ *cobra.Command, _ []string) { } if md != nil { - cmd.tui.RequireToken(md.GetPayload().RegistrationRequiresToken) + if !md.GetPayload().InvitesOpen { + apiEndpoint, _ := zrd.ApiEndpoint() + tui.Error(fmt.Sprintf("'%v' is not currently accepting new users", apiEndpoint), nil) + } + cmd.tui.invitesOpen = md.GetPayload().InvitesOpen + cmd.tui.RequiresInviteToken(md.GetPayload().RequiresInviteToken) + cmd.tui.invitesContact = md.GetPayload().InviteTokenContact } if _, err := tea.NewProgram(&cmd.tui).Run(); err != nil { @@ -97,14 +103,16 @@ func (cmd *inviteCommand) endpointError(apiEndpoint, _ string) { } type inviteTui struct { - focusIndex int - msg string - emailInputs []textinput.Model - tokenInput textinput.Model - cursorMode textinput.CursorMode - done bool - requireToken bool - maxIndex int + focusIndex int + msg string + emailInputs []textinput.Model + tokenInput textinput.Model + cursorMode textinput.CursorMode + done bool + invitesOpen bool + requireInviteToken bool + invitesContact string + maxIndex int msgOk string msgMismatch string @@ -208,7 +216,7 @@ func (m *inviteTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.emailInputs[i].PromptStyle = m.noStyle m.emailInputs[i].TextStyle = m.noStyle } - if m.requireToken { + if m.requireInviteToken { if m.focusIndex == 2 { cmds[2] = m.tokenInput.Focus() m.tokenInput.PromptStyle = m.focusedStyle @@ -234,7 +242,7 @@ func (m *inviteTui) updateInputs(msg tea.Msg) tea.Cmd { for i := range m.emailInputs { m.emailInputs[i], cmds[i] = m.emailInputs[i].Update(msg) } - if m.requireToken { + if m.requireInviteToken { m.tokenInput, cmds[2] = m.tokenInput.Update(msg) } return tea.Batch(cmds...) @@ -242,14 +250,19 @@ func (m *inviteTui) updateInputs(msg tea.Msg) tea.Cmd { func (m inviteTui) View() string { var b strings.Builder + b.WriteString(fmt.Sprintf("\n%v\n\n", m.msg)) + if m.requireInviteToken && m.invitesContact != "" { + b.WriteString(fmt.Sprintf("If you don't already have one, request an invite token at: %v\n\n", m.invitesContact)) + } + for i := 0; i < len(m.emailInputs); i++ { b.WriteString(m.emailInputs[i].View()) b.WriteRune('\n') } - if m.requireToken { + if m.requireInviteToken { b.WriteString(m.tokenInput.View()) b.WriteRune('\n') } @@ -263,8 +276,8 @@ func (m inviteTui) View() string { return b.String() } -func (m *inviteTui) RequireToken(require bool) { - m.requireToken = require +func (m *inviteTui) RequiresInviteToken(require bool) { + m.requireInviteToken = require if require { m.maxIndex = 3 } else { diff --git a/controller/config/config.go b/controller/config/config.go index 9f54f241..7f717aa1 100644 --- a/controller/config/config.go +++ b/controller/config/config.go @@ -31,8 +31,11 @@ type Config struct { } type AdminConfig struct { - Secrets []string `cf:"+secret"` - TouLink string + Secrets []string `cf:"+secret"` + TouLink string + InvitesOpen bool + InviteTokenStrategy string + InviteTokenContact string } type EndpointConfig struct { @@ -42,7 +45,6 @@ type EndpointConfig struct { type RegistrationConfig struct { RegistrationUrlTemplate string - TokenStrategy string } type ResetPasswordConfig struct { diff --git a/controller/configuration.go b/controller/configuration.go index 0c4fc298..4e31810f 100644 --- a/controller/configuration.go +++ b/controller/configuration.go @@ -19,18 +19,14 @@ func newConfigurationHandler(cfg *config.Config) *configurationHandler { } func (ch *configurationHandler) Handle(_ metadata.ConfigurationParams) middleware.Responder { - tou := "" - if cfg.Admin != nil { - tou = cfg.Admin.TouLink - } - tokenRequired := false - if cfg.Registration != nil { - tokenRequired = cfg.Registration.TokenStrategy == "store" - } data := &rest_model_zrok.Configuration{ - Version: build.String(), - TouLink: tou, - RegistrationRequiresToken: tokenRequired, + Version: build.String(), + InvitesOpen: cfg.Admin != nil && cfg.Admin.InvitesOpen, + RequiresInviteToken: cfg.Registration != nil && cfg.Admin.InviteTokenStrategy == "store", + } + if cfg.Admin != nil { + data.TouLink = cfg.Admin.TouLink + data.InviteTokenContact = cfg.Admin.InviteTokenContact } return metadata.NewConfigurationOK().WithPayload(data) } diff --git a/controller/invite.go b/controller/invite.go index ab2f80f0..d8badfde 100644 --- a/controller/invite.go +++ b/controller/invite.go @@ -38,7 +38,7 @@ func (h *inviteHandler) Handle(params account.InviteParams) middleware.Responder } defer func() { _ = tx.Rollback() }() - if h.cfg.Registration != nil && h.cfg.Registration.TokenStrategy == "store" { + if h.cfg.Admin != nil && h.cfg.Admin.InviteTokenStrategy == "store" { inviteToken, err := str.FindInviteTokenByToken(params.Body.Token, tx) if err != nil { logrus.Errorf("cannot get invite token '%v' for '%v': %v", params.Body.Token, params.Body.Email, err) diff --git a/etc/ctrl.yml b/etc/ctrl.yml index 4c71f607..4e9af002 100644 --- a/etc/ctrl.yml +++ b/etc/ctrl.yml @@ -19,9 +19,22 @@ admin: # secrets: - 77623cad-1847-4d6d-8ffe-37defc33c909 - # if `tou_link` is present, the frontend will display the "Terms of Use" link on the login and registration forms + # + # If `tou_link` is present, the frontend will display the "Terms of Use" link on the login and registration forms # tou_link: 'Terms and Conditions' + # + # To allow open invites to your `zrok` instance, set `invites_open` to `true` + # + invites_open: true + # + # Set `token_strategy` to `store` to require an invite token. + # + #token_strategy: store + # + # Set `invite_token_contact` to include an email address or a URL where an invite token can be requested + # + invite_token_contact: invites@zrok.io # The `bridge` section configures the `zrok controller metrics bridge`, specifying the source and sink where OpenZiti # `fabric.usage` events are consumed and then sent into `zrok`. For production environments, we recommend that you use @@ -131,10 +144,6 @@ metrics: # registration: registration_url_template: https://zrok.server.com/register - # - # Set `token_strategy` to `store` to require an invite token. - # - #token_strategy: store # Configure the generated URL for password resets. The reset token will be appended to this URL. # diff --git a/rest_model_zrok/configuration.go b/rest_model_zrok/configuration.go index e5103c43..8486455a 100644 --- a/rest_model_zrok/configuration.go +++ b/rest_model_zrok/configuration.go @@ -17,8 +17,14 @@ import ( // swagger:model configuration type Configuration struct { - // registration requires token - RegistrationRequiresToken bool `json:"registrationRequiresToken,omitempty"` + // invite token contact + InviteTokenContact string `json:"inviteTokenContact,omitempty"` + + // invites open + InvitesOpen bool `json:"invitesOpen,omitempty"` + + // requires invite token + RequiresInviteToken bool `json:"requiresInviteToken,omitempty"` // tou link TouLink string `json:"touLink,omitempty"` diff --git a/rest_server_zrok/embedded_spec.go b/rest_server_zrok/embedded_spec.go index 0773e3de..78bdfeee 100644 --- a/rest_server_zrok/embedded_spec.go +++ b/rest_server_zrok/embedded_spec.go @@ -1056,7 +1056,13 @@ func init() { "configuration": { "type": "object", "properties": { - "registrationRequiresToken": { + "inviteTokenContact": { + "type": "string" + }, + "invitesOpen": { + "type": "boolean" + }, + "requiresInviteToken": { "type": "boolean" }, "touLink": { @@ -2611,7 +2617,13 @@ func init() { "configuration": { "type": "object", "properties": { - "registrationRequiresToken": { + "inviteTokenContact": { + "type": "string" + }, + "invitesOpen": { + "type": "boolean" + }, + "requiresInviteToken": { "type": "boolean" }, "touLink": { diff --git a/specs/zrok.yml b/specs/zrok.yml index b5ddcaea..cf43aa67 100644 --- a/specs/zrok.yml +++ b/specs/zrok.yml @@ -672,8 +672,12 @@ definitions: type: string touLink: type: string - registrationRequiresToken: + invitesOpen: type: boolean + requiresInviteToken: + type: boolean + inviteTokenContact: + type: string createFrontendRequest: type: object diff --git a/ui/src/api/types.js b/ui/src/api/types.js index c326434d..0f00f787 100644 --- a/ui/src/api/types.js +++ b/ui/src/api/types.js @@ -31,7 +31,9 @@ * * @property {string} version * @property {string} touLink - * @property {boolean} registrationRequiresToken + * @property {boolean} invitesOpen + * @property {boolean} requiresInviteToken + * @property {string} inviteTokenContact */ /**