Improve the registration process, add feedback to the PWA's UX adn use an API for registration (#147)

Co-authored-by: James Mills <prologic@shortcircuit.net.au>
Reviewed-on: #147
Reviewed-by: xuu <xuu@noreply@mills.io>
Co-authored-by: James Mills <james@mills.io>
Co-committed-by: James Mills <james@mills.io>
pull/148/head
James Mills 6 months ago committed by xuu
parent 330701da86
commit 832fb124fe
  1. 1
      .gitignore
  2. 2
      Makefile
  3. 35
      client.go
  4. 22
      cmd/salty-chat/register.go
  5. 29
      internal/api.go
  6. 12
      internal/pwa/components/configuration.go
  7. 16
      internal/pwa/components/dialog.go
  8. 2
      internal/pwa/components/newchat.go
  9. 10
      internal/pwa/components/saltychat.go
  10. 21
      internal/server.go
  11. 7
      internal/tasks.go
  12. BIN
      internal/web/app.wasm
  13. 16
      log
  14. 2
      types.go

1
.gitignore vendored

@ -18,6 +18,7 @@
/data/*.db
/data/*.key
/data/*.json
/data/logs
/data/acme
/data/avatars

@ -37,7 +37,7 @@ certs: certs/minica-key.pem certs/minica.pem certs/_.home.arpa/key.pem certs/_.h
@/bin/sh -c 'cd certs && minica --domains salty.home.arpa'
dev : DEBUG=1
dev : certs ## Build debug version of salty-chat (CLI and TUI) and saltyd (Broker and PWA)
dev : certs pwa ## Build debug version of salty-chat (CLI and TUI) and saltyd (Broker and PWA)
@CGO_ENABLED=1 $(GOCMD) build ./cmd/salty-chat/...
@CGO_ENABLED=1 $(GOCMD) build -tags "embed" ./cmd/saltyd/...
@./saltyd -D -b :https --tls \

@ -16,7 +16,6 @@ import (
"git.mills.io/prologic/msgbus"
msgbus_client "git.mills.io/prologic/msgbus/client"
"github.com/avast/retry-go"
"github.com/keys-pub/keys"
log "github.com/sirupsen/logrus"
@ -395,22 +394,34 @@ func (cli *Client) SendToAddr(addr *Addr, msg string) error {
}
// Register sends a registration request to the service user of a Salty Broker
func (cli *Client) Register() error {
svc, err := cli.lookup.LookupAddr(fmt.Sprintf("%s@%s", ServiceUser, cli.Me().Domain))
func (cli *Client) Register(brokerURI string) error {
if brokerURI == "" {
log.Debugf("Looking up SRV record for _salty._tcp.%s", cli.Me().Domain)
target, err := resolver.LookupSRV("salty", "tcp", cli.Me().Domain)
if err != nil {
return fmt.Errorf("unable to find broker for %s: %w", cli.Me(), err)
}
brokerURI = fmt.Sprintf("https://%s", target)
}
u, err := url.Parse(brokerURI)
if err != nil {
return fmt.Errorf("error looking up service user on %s: %w", cli.Me().Domain, err)
return fmt.Errorf("error parsing broker uri %s: %w", brokerURI, err)
}
u.Path = "/api/v1/register"
if err := cli.SendToAddr(svc, "REGISTER"); err != nil {
return fmt.Errorf("error sending REGISTER to %s: %w", svc, err)
req := RegisterRequest{Hash: cli.Me().Hash(), Key: cli.Key().ID().String()}
data, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("error serializing register request: %w", err)
}
signed, err := salty.Sign(cli.key, data)
if err != nil {
return fmt.Errorf("error signing register request: %w", err)
}
if err := retry.Do(
func() error {
return cli.Me().Refresh()
},
); err != nil {
return fmt.Errorf("error registrating account: %w", err)
_, err = Request(http.MethodPost, u.String(), nil, bytes.NewBuffer(signed))
if err != nil {
return fmt.Errorf("error registering address: %w", err)
}
return nil

@ -12,7 +12,7 @@ import (
)
var registerCmd = &cobra.Command{
Use: "register nick@domain",
Use: "register nick@domain [broker]",
Aliases: []string{"reg"},
Short: "Creates a new user account and registers it with a Salty Broker",
Long: `This command registers creates a new identity for a new user, creating a new
@ -20,10 +20,11 @@ key pair and registering with a Salty Broker (in instance of saltyd). Effectivel
an automated account creation and registration process that combinaes make-user + manual
steps to setup and serve a valid Salty Discovery Endpoint (.well-known/salty/...).
NOTE: This is only spported on a Salty Broker.`,
Args: cobra.ExactArgs(1),
NOTE: This is only spported o a Salty Broker.`,
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
identity := viper.GetString("identity")
broker := viper.GetString("broker")
me := &saltyim.Addr{}
if sp := strings.Split(args[0], "@"); len(sp) > 1 {
@ -32,15 +33,23 @@ NOTE: This is only spported on a Salty Broker.`,
}
// XXX: What if me.IsZero()
register(me, identity)
register(me, identity, broker)
},
}
func init() {
rootCmd.AddCommand(registerCmd)
rootCmd.Flags().StringP(
"broker", "b", "",
"use the broker to register with",
)
viper.BindPFlag("broker", registerCmd.Flags().Lookup("broker"))
viper.SetDefault("broker", "")
}
func register(me *saltyim.Addr, identity string) {
func register(me *saltyim.Addr, identity, broker string) {
dir := filepath.Dir(identity)
dir = saltyim.FixUnixHome(dir)
@ -49,7 +58,6 @@ func register(me *saltyim.Addr, identity string) {
os.Exit(2)
}
identity = filepath.Join(dir, fmt.Sprintf("%s.key", me.User))
fmt.Printf("identity: %s\n", identity)
id, err := saltyim.CreateIdentity(saltyim.WithIdentityPath(identity), saltyim.WithIdentityAddr(me))
if err != nil {
@ -63,7 +71,7 @@ func register(me *saltyim.Addr, identity string) {
os.Exit(2)
}
if err := cli.Register(); err != nil {
if err := cli.Register(broker); err != nil {
fmt.Fprintf(os.Stderr, "error registering account: %s\n", err)
os.Exit(2)
}

@ -32,6 +32,7 @@ func (a *API) initRoutes() {
router := a.router.Group("/api/v1")
router.GET("/ping", a.PingEndpoint())
router.POST("/register", a.RegisterEndpoint())
// Lookup and Send support for Web / PWA clients
router.GET("/lookup/:addr", a.LookupEndpoint())
@ -49,6 +50,34 @@ func (a *API) PingEndpoint() httprouter.Handle {
}
}
// RegisterEndpoint ...
func (a *API) RegisterEndpoint() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
req, signer, err := saltyim.NewRegisterRequest(r.Body)
if err != nil {
log.WithError(err).Error("error parsing register request")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if signer != req.Key {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if err := CreateConfig(a.config, req.Hash, req.Key); err != nil {
log.WithError(err).Errorf("error creating config for %s", req.Hash)
if err == ErrAddressExists {
http.Error(w, "Already Exists", http.StatusConflict)
} else {
http.Error(w, "Error", http.StatusInternalServerError)
}
return
}
http.Error(w, "Endpoint Created", http.StatusCreated)
}
}
// LookupEndpoint ...
func (a *API) LookupEndpoint() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {

@ -23,6 +23,7 @@ type Configuration struct {
// to support removal
Contacts []string
dialog *ModalDialog
navigation *Navigation
// components
@ -50,7 +51,6 @@ func (c *Configuration) OnMount(ctx app.Context) {
}
func (c *Configuration) Render() app.UI {
topBar := &bar.TopAppBar{Title: "Salty IM",
Navigation: []app.HTMLButton{icon.MIMenu.Button().OnClick(func(ctx app.Context, e app.Event) {
c.navigation.drawer.ActionOpen(ctx)
@ -66,6 +66,7 @@ func (c *Configuration) Render() app.UI {
c.identity.WithCallback(func(in app.HTMLTextarea) {
in.OnChange(c.identity.ValueTo(&c.identity.Value))
})
c.dialog = &ModalDialog{}
c.navigation = &Navigation{}
}
@ -96,6 +97,7 @@ func (c *Configuration) Render() app.UI {
),
),
),
c.dialog,
)
}
@ -135,16 +137,19 @@ func (c *Configuration) registerIdentity() func(button app.HTMLButton) {
}
// not using client since that's not setup until we have an identity, might break the existing
// flow
log.Printf("identity;Addr(): %#v", identity.Addr())
registerClient, err := saltyim.NewClient(identity.Addr(), saltyim.WithClientIdentity(saltyim.WithIdentityBytes(identity.Contents())))
if err != nil { // TODO: pop dialog
log.Println("error", err)
return
}
err = registerClient.Register()
err = registerClient.Register(app.Getenv("BaseURL"))
if err != nil { // TODO: pop dialog
log.Println("error", err)
c.dialog.ShowError("error registering to broker: ", err.Error())
return
}
c.dialog.ShowDialog("Success", "🥳 Success!")
})
}
}
@ -155,17 +160,20 @@ func (c *Configuration) newIdentity() func(button app.HTMLButton) {
addr, err := saltyim.ParseAddr(c.user.Value)
if err != nil { // TODO: pop dialog
log.Println("error", err)
c.dialog.ShowError("error parsing address: ", err.Error())
return
}
identity, err := saltyim.CreateIdentity(saltyim.WithIdentityAddr(addr))
if err != nil { // TODO: pop dialog
log.Println("error", err)
c.dialog.ShowError("error creating identity: ", err.Error())
return
}
c.identity.Value = string(identity.Contents())
err = SetIdentityToState(ctx, identity)
if err != nil { // TODO: pop dialog
log.Println("error", err)
c.dialog.ShowError("error saving identity to storage: ", err.Error())
return
}
c.identity.Update()

@ -25,7 +25,7 @@ func (m *ModalDialog) Render() app.UI {
return m.dialog
}
func (m *ModalDialog) ShowDialog(msg ...string) {
func (m *ModalDialog) ShowError(msg ...string) {
if m.dialog == nil {
log.Debug("ModalDialog.dialog is nil, unable to display message", msg)
return
@ -37,3 +37,17 @@ func (m *ModalDialog) ShowDialog(msg ...string) {
m.dialog.Update()
m.dialog.Open()
}
func (m *ModalDialog) ShowDialog(title string, msg ...string) {
if m.dialog == nil {
log.Debug("ModalDialog.dialog is nil, unable to display message", msg)
return
}
m.dialog.Title = []app.UI{app.Div().Text(title)}
m.dialog.Content = []app.UI{}
for _, s := range msg {
m.dialog.Content = append(m.dialog.Content, app.Div().Text(s))
}
m.dialog.Update()
m.dialog.Open()
}

@ -63,7 +63,7 @@ func (n *NewChat) newChat() func(button app.HTMLButton) {
button.OnClick(func(ctx app.Context, e app.Event) {
addr, err := utils.LookupAddr(n.user.Value)
if err != nil {
n.dialog.ShowDialog("error", err.Error())
n.dialog.ShowError("error", err.Error())
return
}
storage.ContactsLocalStorage(ctx).Add(addr.String())

@ -99,13 +99,13 @@ func (h *SaltyChat) connect(ctx app.Context) {
identity, err := GetIdentityFromState(ctx)
if err != nil {
h.dialog.ShowDialog("missing identity, please configure", err.Error())
h.dialog.ShowError("missing identity, please configure", err.Error())
return
}
newClient, err := saltyim.NewClient(identity.Addr(), saltyim.WithClientIdentity(saltyim.WithIdentityBytes(identity.Contents())))
if err != nil {
h.dialog.ShowDialog("error setting up client", err.Error())
h.dialog.ShowError("error setting up client", err.Error())
return
}
newClient.SetSend(&saltyim.ProxySend{SendEndpoint: app.Getenv("SendEndpoint")})
@ -137,7 +137,7 @@ func (h *SaltyChat) incomingMessage(ctx app.Context, action app.Action) {
messageText := action.Tags.Get("text")
s, err := lextwt.ParseSalty(messageText)
if err != nil {
h.dialog.ShowDialog("incoming message error", err.Error())
h.dialog.ShowError("incoming message error", err.Error())
return
}
@ -166,7 +166,7 @@ func (h *SaltyChat) outgoingMessage(ctx app.Context, action app.Action) {
messageText := action.Tags.Get("text")
s, err := lextwt.ParseSalty(messageText)
if err != nil {
h.dialog.ShowDialog("outgoing message error", err.Error())
h.dialog.ShowError("outgoing message error", err.Error())
return
}
@ -251,7 +251,7 @@ func (h *SaltyChat) handleSendMessage(ctx app.Context, e app.Event) {
// Append(string(saltyim.PackMessage(client.Me(), msg)))
// h.chatBox.UpdateMessages(ctx)
} else {
h.dialog.ShowDialog("error sending message", err.Error())
h.dialog.ShowError("error sending message", err.Error())
}
})
}

@ -16,12 +16,10 @@ import (
"github.com/NYTimes/gziphandler"
"github.com/julienschmidt/httprouter"
"github.com/justinas/nosurf"
"github.com/keys-pub/keys"
"github.com/maxence-charriere/go-app/v9/pkg/app"
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
"github.com/unrolled/logger"
"go.yarn.social/lextwt"
"golang.org/x/crypto/acme/autocert"
"go.mills.io/saltyim"
@ -251,7 +249,9 @@ func (s *Server) setupServiceUser() error {
}
if err := CreateConfig(s.config, me.Hash(), id.Key().ID().String()); err != nil {
return err
if err != ErrAddressExists {
return err
}
}
svc, err := saltyim.NewService(me, id, svcUserState)
@ -260,20 +260,6 @@ func (s *Server) setupServiceUser() error {
}
s.svc = svc
svc.TextFunc("register", func(ctx context.Context, svc *saltyim.Service, key *keys.EdX25519PublicKey, msg *lextwt.SaltyText) error {
addr, err := saltyim.ParseAddr(msg.User.String())
if err != nil {
return err
}
err = CreateConfig(s.config, addr.Hash(), key.ID().String())
if err != nil {
return err
}
return svc.Respond(msg.User.String(), "OK")
})
return nil
}
@ -302,6 +288,7 @@ func (s *Server) initRoutes() {
Description: "Secure, easy, self-hosted messaging",
Env: map[string]string{
"BaseURL": strings.TrimSuffix(s.config.BaseURL, "/"),
"LookupEndpoint": strings.TrimSuffix(s.config.BaseURL, "/") + "/api/v1/lookup/",
"SendEndpoint": strings.TrimSuffix(s.config.BaseURL, "/") + "/api/v1/send",
},

@ -3,6 +3,7 @@ package internal
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@ -16,13 +17,17 @@ const (
avatarResolution = 80 // 80x80 px
)
var (
ErrAddressExists = errors.New("error: address already exists")
)
func CreateConfig(conf *Config, hash string, key string) error {
p := filepath.Join(conf.Data, wellknownPath)
p = saltyim.FixUnixHome(p)
fn := filepath.Join(p, fmt.Sprintf("%s.json", hash))
if FileExists(fn) {
return nil
return ErrAddressExists
}
if err := os.MkdirAll(p, 0755); err != nil {

BIN
internal/web/app.wasm (Stored with Git LFS)

Binary file not shown.

16
log

@ -0,0 +1,16 @@
time="2022-04-04T21:34:00+10:00" level=debug msg="me: &saltyim.Addr{User:\"\", Domain:\"\", key:(*keys.EdX25519PublicKey)(nil), endpoint:(*url.URL)(nil), discoveredDomain:\"\", avatar:\"\", capabilities:saltyim.Capabilities{AcceptEncoding:\"\"}, checkedAvatar:false}"
time="2022-04-04T21:34:00+10:00" level=debug msg="Looking up SRV record for _salty._tcp.home.arpa"
time="2022-04-04T21:34:00+10:00" level=debug msg="Using StandardResolver, looking up SRV _salty._tcp.home.arpa"
time="2022-04-04T21:34:00+10:00" level=debug msg="Discovered salty services salty.home.arpa"
time="2022-04-04T21:34:00+10:00" level=debug msg="map[Accept-Encoding:[br, gzip, deflate] Accept-Ranges:[bytes] Access-Control-Allow-Headers:[*] Access-Control-Allow-Origin:[*] Access-Control-Expose-Headers:[*] Content-Length:[142] Content-Type:[application/json] Date:[Mon, 04 Apr 2022 11:34:00 GMT] Last-Modified:[Mon, 04 Apr 2022 10:33:30 GMT] Set-Cookie:[csrf_token=f+OFHBvwJohmepf54/V3bPPhodKF7nAjC/hVszFmrCw=; Max-Age=31536000] Vary:[Accept-Encoding Cookie] X-Salty-Accept-Encoding:[br, gzip, deflate]]"
time="2022-04-04T21:34:00+10:00" level=debug msg="Discovered endpoint: https://salty.home.arpa/inbox/01FZT261JV2N2N8C5WCGA0GTZQ"
time="2022-04-04T21:34:00+10:00" level=debug msg="Discovered capability: accept-encoding: br, gzip, deflate"
time="2022-04-04T21:34:00+10:00" level=debug msg="Using identity foo.key with public key kex1py4x3mew0qyu2m47avjjxkuqx2a5d0k8dgsrfr6vxrf6cz7u3g0srawylg"
time="2022-04-04T21:34:00+10:00" level=debug msg="Salty Addr is @"
time="2022-04-04T21:34:00+10:00" level=debug msg="Endpoint is <nil>"
time="2022-04-04T21:34:00+10:00" level=debug msg="Looking up SRV record for _salty._tcp.home.arpa"
time="2022-04-04T21:34:00+10:00" level=debug msg="Using StandardResolver, looking up SRV _salty._tcp.home.arpa"
time="2022-04-04T21:34:00+10:00" level=debug msg="Discovered salty services salty.home.arpa"
time="2022-04-04T21:34:00+10:00" level=debug msg="map[Accept-Encoding:[br, gzip, deflate] Accept-Ranges:[bytes] Access-Control-Allow-Headers:[*] Access-Control-Allow-Origin:[*] Access-Control-Expose-Headers:[*] Content-Length:[142] Content-Type:[application/json] Date:[Mon, 04 Apr 2022 11:34:00 GMT] Last-Modified:[Mon, 04 Apr 2022 11:02:43 GMT] Set-Cookie:[csrf_token=+elnJfkI7HVkUIMSaMflOkSs6LJQwyb/luoOVrFogSk=; Max-Age=31536000] Vary:[Accept-Encoding Cookie] X-Salty-Accept-Encoding:[br, gzip, deflate]]"
time="2022-04-04T21:34:00+10:00" level=debug msg="Discovered endpoint: https://salty.home.arpa/inbox/01FZT3VH6538Q611ND6DH8RSQC"
time="2022-04-04T21:34:00+10:00" level=debug msg="Discovered capability: accept-encoding: br, gzip, deflate"

@ -10,7 +10,7 @@ import (
// RegisterRequest is the request used by clients to register to a broker
type RegisterRequest struct {
Addr *Addr
Hash string
Key string
}

Loading…
Cancel
Save