package ui import ( "gioui.org/layout" "gioui.org/widget" "gioui.org/widget/material" "git.wxccs.org/iceking2nd/winauth-go/internal/authenticator" "git.wxccs.org/iceking2nd/winauth-go/internal/global" "git.wxccs.org/iceking2nd/winauth-go/internal/i18n" ) // addTOTPDialog is the shared "Add ..." form used by Google, Microsoft and // Okta Verify — vendors that all use the same {Name, Base32-secret} input // surface and only differ by display title, default name, and factory // function. type addTOTPDialog struct { title string defaultName string factory func() authenticator.Authenticator nameEd widget.Editor secretEd widget.Editor showSec widget.Bool okBtn widget.Clickable cancelBtn widget.Clickable errorMsg string } func newAddGoogleDialog() *addTOTPDialog { return newAddTOTPDialog( i18n.T("dialog_add_google_title"), i18n.T("vendor_google"), func() authenticator.Authenticator { return authenticator.NewGoogleAuthenticator() }, ) } func newAddMicrosoftDialog() *addTOTPDialog { return newAddTOTPDialog( i18n.T("dialog_add_microsoft_title"), i18n.T("vendor_microsoft"), func() authenticator.Authenticator { return authenticator.NewMicrosoftAuthenticator() }, ) } func newAddOktaDialog() *addTOTPDialog { return newAddTOTPDialog( i18n.T("dialog_add_okta_title"), i18n.T("vendor_okta"), func() authenticator.Authenticator { return authenticator.NewOktaVerifyAuthenticator() }, ) } func newAddTOTPDialog(title, defaultName string, factory func() authenticator.Authenticator) *addTOTPDialog { d := &addTOTPDialog{title: title, defaultName: defaultName, factory: factory} d.nameEd.SingleLine = true d.secretEd.SingleLine = true d.secretEd.Mask = '*' return d } // enrollable is implemented by authenticators whose secret is provided as a // raw Base32 string by the user (Google / Microsoft / Okta). type enrollable interface { authenticator.Authenticator Enroll(secret string) error } func (d *addTOTPDialog) Layout( gtx layout.Context, th *material.Theme, onDone func(authenticator.Authenticator, string), ) layout.Dimensions { const fn = "internal.ui.addTOTPDialog.Layout" if d.showSec.Update(gtx) { if d.showSec.Value { d.secretEd.Mask = 0 } else { d.secretEd.Mask = '*' } } if d.okBtn.Clicked(gtx) { secret := d.secretEd.Text() if secret == "" { d.errorMsg = i18n.T("msg_empty_secret") } else { a := d.factory() if en, ok := a.(enrollable); ok { if err := en.Enroll(secret); err != nil { global.Log.WithField("func", fn).WithError(err).Warn("enroll failed") d.errorMsg = i18n.T("msg_invalid_secret") } else { name := d.nameEd.Text() if name == "" { name = d.defaultName } onDone(a, name) return layout.Dimensions{Size: gtx.Constraints.Max} } } else { d.errorMsg = i18n.T("msg_invalid_secret") } } } if d.cancelBtn.Clicked(gtx) { onDone(nil, "") return layout.Dimensions{Size: gtx.Constraints.Max} } body := func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(labeledEditor(th, i18n.T("label_name"), &d.nameEd, d.defaultName)), layout.Rigid(labeledEditor(th, i18n.T("label_secret_key"), &d.secretEd, "ABCDEF...")), layout.Rigid(material.CheckBox(th, &d.showSec, i18n.T("label_show_secret")).Layout), layout.Rigid(errorLabel(th, d.errorMsg)), ) } return modalCard(gtx, th, d.title, i18n.T("btn_ok"), i18n.T("btn_cancel"), &d.okBtn, &d.cancelBtn, body) }