mirror of
https://github.com/openziti/zrok.git
synced 2025-01-10 16:08:20 +01:00
Merge pull request #313 from openziti/v0.4_invite_overhaul
New metadata driven invite process (#229)
This commit is contained in:
commit
de02cea5f3
@ -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)
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/openziti/zrok/rest_client_zrok/account"
|
||||
"github.com/openziti/zrok/rest_client_zrok/metadata"
|
||||
"github.com/openziti/zrok/rest_model_zrok"
|
||||
"github.com/openziti/zrok/tui"
|
||||
"github.com/openziti/zrok/util"
|
||||
@ -21,9 +22,8 @@ func init() {
|
||||
}
|
||||
|
||||
type inviteCommand struct {
|
||||
cmd *cobra.Command
|
||||
token string
|
||||
tui inviteTui
|
||||
cmd *cobra.Command
|
||||
tui inviteTui
|
||||
}
|
||||
|
||||
func newInviteCommand() *inviteCommand {
|
||||
@ -38,8 +38,6 @@ func newInviteCommand() *inviteCommand {
|
||||
}
|
||||
cmd.Run = command.run
|
||||
|
||||
cmd.Flags().StringVar(&command.token, "token", "", "Invite token required when zrok running in token store mode")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
@ -58,17 +56,33 @@ func (cmd *inviteCommand) run(_ *cobra.Command, _ []string) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
md, err := zrok.Metadata.Configuration(metadata.NewConfigurationParams())
|
||||
if err != nil {
|
||||
tui.Error("unable to get server metadata", err)
|
||||
}
|
||||
|
||||
if md != nil {
|
||||
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 {
|
||||
tui.Error("unable to run interface", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if cmd.tui.done {
|
||||
email := cmd.tui.inputs[0].Value()
|
||||
email := cmd.tui.emailInputs[0].Value()
|
||||
token := cmd.tui.tokenInput.Value()
|
||||
|
||||
req := account.NewInviteParams()
|
||||
req.Body = &rest_model_zrok.InviteRequest{
|
||||
Email: email,
|
||||
Token: cmd.token,
|
||||
Token: token,
|
||||
}
|
||||
_, err = zrok.Account.Invite(req)
|
||||
if err != nil {
|
||||
@ -83,18 +97,22 @@ func (cmd *inviteCommand) run(_ *cobra.Command, _ []string) {
|
||||
func (cmd *inviteCommand) endpointError(apiEndpoint, _ string) {
|
||||
fmt.Printf("%v\n\n", tui.SeriousBusiness.Render("there was a problem creating an invitation!"))
|
||||
fmt.Printf("you are trying to use the zrok service at: %v\n\n", tui.Code.Render(apiEndpoint))
|
||||
fmt.Printf("%v\n\n", tui.Attention.Render("should you be using a --token? check with your instance administrator!"))
|
||||
fmt.Printf("you can change your zrok service endpoint using this command:\n\n")
|
||||
fmt.Printf("%v\n\n", tui.Code.Render("$ zrok config set apiEndpoint <newEndpoint>"))
|
||||
fmt.Printf("(where newEndpoint is something like: %v)\n\n", tui.Code.Render("https://some.zrok.io"))
|
||||
}
|
||||
|
||||
type inviteTui struct {
|
||||
focusIndex int
|
||||
msg string
|
||||
inputs []textinput.Model
|
||||
cursorMode textinput.CursorMode
|
||||
done bool
|
||||
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
|
||||
@ -110,7 +128,8 @@ type inviteTui struct {
|
||||
|
||||
func newInviteTui() inviteTui {
|
||||
m := inviteTui{
|
||||
inputs: make([]textinput.Model, 2),
|
||||
emailInputs: make([]textinput.Model, 2),
|
||||
maxIndex: 2,
|
||||
}
|
||||
m.focusedStyle = tui.Attention.Copy()
|
||||
m.blurredStyle = tui.Code.Copy()
|
||||
@ -125,7 +144,7 @@ func newInviteTui() inviteTui {
|
||||
m.msgMismatch = m.errorStyle.Render("email is invalid or does not match confirmation...")
|
||||
|
||||
var t textinput.Model
|
||||
for i := range m.inputs {
|
||||
for i := range m.emailInputs {
|
||||
t = textinput.New()
|
||||
t.CursorStyle = m.cursorStyle
|
||||
t.CharLimit = 96
|
||||
@ -140,9 +159,13 @@ func newInviteTui() inviteTui {
|
||||
t.Placeholder = "Confirm Email"
|
||||
}
|
||||
|
||||
m.inputs[i] = t
|
||||
m.emailInputs[i] = t
|
||||
}
|
||||
|
||||
m.tokenInput = textinput.New()
|
||||
m.tokenInput.CursorStyle = m.cursorStyle
|
||||
m.tokenInput.Placeholder = "Token"
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
@ -158,8 +181,8 @@ func (m *inviteTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "tab", "shift+tab", "enter", "up", "down":
|
||||
s := msg.String()
|
||||
|
||||
if s == "enter" && m.focusIndex == len(m.inputs) {
|
||||
if util.IsValidEmail(m.inputs[0].Value()) && m.inputs[0].Value() == m.inputs[1].Value() {
|
||||
if s == "enter" && m.focusIndex == m.maxIndex {
|
||||
if util.IsValidEmail(m.emailInputs[0].Value()) && m.emailInputs[0].Value() == m.emailInputs[1].Value() {
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
@ -175,23 +198,34 @@ func (m *inviteTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.focusIndex++
|
||||
}
|
||||
|
||||
if m.focusIndex > len(m.inputs) {
|
||||
if m.focusIndex > m.maxIndex {
|
||||
m.focusIndex = 0
|
||||
} else if m.focusIndex < 0 {
|
||||
m.focusIndex = len(m.inputs)
|
||||
m.focusIndex = m.maxIndex
|
||||
}
|
||||
|
||||
cmds := make([]tea.Cmd, len(m.inputs))
|
||||
for i := 0; i <= len(m.inputs)-1; i++ {
|
||||
cmds := make([]tea.Cmd, m.maxIndex)
|
||||
for i := 0; i <= len(m.emailInputs)-1; i++ {
|
||||
if i == m.focusIndex {
|
||||
cmds[i] = m.inputs[i].Focus()
|
||||
m.inputs[i].PromptStyle = m.focusedStyle
|
||||
m.inputs[i].TextStyle = m.focusedStyle
|
||||
cmds[i] = m.emailInputs[i].Focus()
|
||||
m.emailInputs[i].PromptStyle = m.focusedStyle
|
||||
m.emailInputs[i].TextStyle = m.focusedStyle
|
||||
continue
|
||||
}
|
||||
m.inputs[i].Blur()
|
||||
m.inputs[i].PromptStyle = m.noStyle
|
||||
m.inputs[i].TextStyle = m.noStyle
|
||||
m.emailInputs[i].Blur()
|
||||
m.emailInputs[i].PromptStyle = m.noStyle
|
||||
m.emailInputs[i].TextStyle = m.noStyle
|
||||
}
|
||||
if m.requireInviteToken {
|
||||
if m.focusIndex == 2 {
|
||||
cmds[2] = m.tokenInput.Focus()
|
||||
m.tokenInput.PromptStyle = m.focusedStyle
|
||||
m.tokenInput.TextStyle = m.focusedStyle
|
||||
} else {
|
||||
m.tokenInput.Blur()
|
||||
m.tokenInput.PromptStyle = m.noStyle
|
||||
m.tokenInput.TextStyle = m.noStyle
|
||||
}
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
@ -204,29 +238,49 @@ func (m *inviteTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *inviteTui) updateInputs(msg tea.Msg) tea.Cmd {
|
||||
cmds := make([]tea.Cmd, len(m.inputs))
|
||||
for i := range m.inputs {
|
||||
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
|
||||
cmds := make([]tea.Cmd, m.maxIndex)
|
||||
for i := range m.emailInputs {
|
||||
m.emailInputs[i], cmds[i] = m.emailInputs[i].Update(msg)
|
||||
}
|
||||
if m.requireInviteToken {
|
||||
m.tokenInput, cmds[2] = m.tokenInput.Update(msg)
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m inviteTui) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(fmt.Sprintf("\n%v\n\n", m.msg))
|
||||
|
||||
for i := range m.inputs {
|
||||
b.WriteString(m.inputs[i].View())
|
||||
if i < len(m.inputs)-1 {
|
||||
b.WriteRune('\n')
|
||||
}
|
||||
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.requireInviteToken {
|
||||
b.WriteString(m.tokenInput.View())
|
||||
b.WriteRune('\n')
|
||||
}
|
||||
|
||||
button := &m.blurredButton
|
||||
if m.focusIndex == len(m.inputs) {
|
||||
if m.focusIndex == m.maxIndex {
|
||||
button = &m.focusedButton
|
||||
}
|
||||
_, _ = fmt.Fprintf(&b, "\n\n%s\n\n", *button)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m *inviteTui) RequiresInviteToken(require bool) {
|
||||
m.requireInviteToken = require
|
||||
if require {
|
||||
m.maxIndex = 3
|
||||
} else {
|
||||
m.maxIndex = 2
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -19,13 +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
|
||||
}
|
||||
data := &rest_model_zrok.Configuration{
|
||||
Version: build.String(),
|
||||
TouLink: tou,
|
||||
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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
19
etc/ctrl.yml
19
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: '<a href="https://google.com" target="_">Terms and Conditions</a>'
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
@ -17,6 +17,15 @@ import (
|
||||
// swagger:model configuration
|
||||
type Configuration struct {
|
||||
|
||||
// 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"`
|
||||
|
||||
|
@ -1056,6 +1056,15 @@ func init() {
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inviteTokenContact": {
|
||||
"type": "string"
|
||||
},
|
||||
"invitesOpen": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requiresInviteToken": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"touLink": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -2608,6 +2617,15 @@ func init() {
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inviteTokenContact": {
|
||||
"type": "string"
|
||||
},
|
||||
"invitesOpen": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requiresInviteToken": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"touLink": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -672,6 +672,12 @@ definitions:
|
||||
type: string
|
||||
touLink:
|
||||
type: string
|
||||
invitesOpen:
|
||||
type: boolean
|
||||
requiresInviteToken:
|
||||
type: boolean
|
||||
inviteTokenContact:
|
||||
type: string
|
||||
|
||||
createFrontendRequest:
|
||||
type: object
|
||||
|
@ -31,6 +31,9 @@
|
||||
*
|
||||
* @property {string} version
|
||||
* @property {string} touLink
|
||||
* @property {boolean} invitesOpen
|
||||
* @property {boolean} requiresInviteToken
|
||||
* @property {string} inviteTokenContact
|
||||
*/
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user