~erock/pico

5c65c99d3583045a20db62ae6edbefef3977f79e — Eric Bower 3 months ago 141b0dd
feat: allow registration flag

I made it so when a user presses 'esc' in the account creation screen
it will quit the app instead of going to the main cms screen.

Also added better error handling during account creation.

Implements: https://todo.sr.ht/~erock/pico.sh/21
Fixes: https://todo.sr.ht/~erock/pico.sh/41
Fixes: https://todo.sr.ht/~erock/pico.sh/1
M lists/config.go => lists/config.go +13 -11
@@ 14,6 14,7 @@ func NewConfigSite() *shared.ConfigSite {
	customdomains := shared.GetEnv("LISTS_CUSTOMDOMAINS", "0")
	port := shared.GetEnv("LISTS_WEB_PORT", "3000")
	protocol := shared.GetEnv("LISTS_PROTOCOL", "https")
	allowRegister := shared.GetEnv("LISTS_ALLOW_REGISTER", "1")
	dbURL := shared.GetEnv("DATABASE_URL", "")
	subdomainsEnabled := false
	if subdomains == "1" {


@@ 35,17 36,18 @@ func NewConfigSite() *shared.ConfigSite {
		SubdomainsEnabled:    subdomainsEnabled,
		CustomdomainsEnabled: customdomainsEnabled,
		ConfigCms: config.ConfigCms{
			Domain:      domain,
			Email:       email,
			Port:        port,
			Protocol:    protocol,
			DbURL:       dbURL,
			Description: "A microblog for your lists.",
			IntroText:   intro,
			Space:       "lists",
			AllowedExt:  []string{".txt"},
			HiddenPosts: []string{"_header.txt", "_readme.txt"},
			Logger:      shared.CreateLogger(),
			Domain:        domain,
			Email:         email,
			Port:          port,
			Protocol:      protocol,
			DbURL:         dbURL,
			Description:   "A microblog for your lists.",
			IntroText:     intro,
			Space:         "lists",
			AllowedExt:    []string{".txt"},
			HiddenPosts:   []string{"_header.txt", "_readme.txt"},
			Logger:        shared.CreateLogger(),
			AllowRegister: allowRegister == "1",
		},
	}
}

M pastes/config.go => pastes/config.go +11 -9
@@ 15,6 15,7 @@ func NewConfigSite() *shared.ConfigSite {
	port := shared.GetEnv("PASTES_WEB_PORT", "3000")
	dbURL := shared.GetEnv("DATABASE_URL", "")
	protocol := shared.GetEnv("PASTES_PROTOCOL", "https")
	allowRegister := shared.GetEnv("PASTES_ALLOW_REGISTER", "1")
	subdomainsEnabled := false
	if subdomains == "1" {
		subdomainsEnabled = true


@@ 35,15 36,16 @@ func NewConfigSite() *shared.ConfigSite {
		SubdomainsEnabled:    subdomainsEnabled,
		CustomdomainsEnabled: customdomainsEnabled,
		ConfigCms: config.ConfigCms{
			Domain:      domain,
			Port:        port,
			Protocol:    protocol,
			Email:       email,
			DbURL:       dbURL,
			Description: "a pastebin for hackers.",
			IntroText:   intro,
			Space:       "pastes",
			Logger:      shared.CreateLogger(),
			Domain:        domain,
			Port:          port,
			Protocol:      protocol,
			Email:         email,
			DbURL:         dbURL,
			Description:   "a pastebin for hackers.",
			IntroText:     intro,
			Space:         "pastes",
			Logger:        shared.CreateLogger(),
			AllowRegister: allowRegister == "1",
		},
	}
}

M prose/config.go => prose/config.go +13 -11
@@ 14,6 14,7 @@ func NewConfigSite() *shared.ConfigSite {
	customdomains := shared.GetEnv("PROSE_CUSTOMDOMAINS", "0")
	port := shared.GetEnv("PROSE_WEB_PORT", "3000")
	protocol := shared.GetEnv("PROSE_PROTOCOL", "https")
	allowRegister := shared.GetEnv("PROSE_ALLOW_REGISTER", "1")
	dbURL := shared.GetEnv("DATABASE_URL", "")
	subdomainsEnabled := false
	if subdomains == "1" {


@@ 35,17 36,18 @@ func NewConfigSite() *shared.ConfigSite {
		SubdomainsEnabled:    subdomainsEnabled,
		CustomdomainsEnabled: customdomainsEnabled,
		ConfigCms: config.ConfigCms{
			Domain:      domain,
			Email:       email,
			Port:        port,
			Protocol:    protocol,
			DbURL:       dbURL,
			Description: "a blog platform for hackers.",
			IntroText:   intro,
			Space:       "prose",
			AllowedExt:  []string{".md"},
			HiddenPosts: []string{"_readme.md", "_styles.css"},
			Logger:      shared.CreateLogger(),
			Domain:        domain,
			Email:         email,
			Port:          port,
			Protocol:      protocol,
			DbURL:         dbURL,
			Description:   "a blog platform for hackers.",
			IntroText:     intro,
			Space:         "prose",
			AllowedExt:    []string{".md"},
			HiddenPosts:   []string{"_readme.md", "_styles.css"},
			Logger:        shared.CreateLogger(),
			AllowRegister: allowRegister == "1",
		},
	}
}

M wish/cms/config/config.go => wish/cms/config/config.go +12 -11
@@ 10,17 10,18 @@ type ConfigURL interface {
}

type ConfigCms struct {
	Domain      string
	Port        string
	Email       string
	Protocol    string
	DbURL       string
	Description string
	IntroText   string
	Space       string
	AllowedExt  []string
	HiddenPosts []string
	Logger      *zap.SugaredLogger
	Domain        string
	Port          string
	Email         string
	Protocol      string
	DbURL         string
	Description   string
	IntroText     string
	Space         string
	AllowedExt    []string
	HiddenPosts   []string
	Logger        *zap.SugaredLogger
	AllowRegister bool
}

func NewConfigCms() *ConfigCms {

M wish/cms/db/db.go => wish/cms/db/db.go +4 -2
@@ 6,7 6,9 @@ import (
	"time"
)

var ErrNameTaken = errors.New("name taken")
var ErrNameTaken = errors.New("username has already been claimed")
var ErrNameDenied = errors.New("username is on the denylist")
var ErrNameInvalid = errors.New("username has invalid characters in it")

type PublicKey struct {
	ID        string     `json:"id"`


@@ 100,7 102,7 @@ type DB interface {
	FindUserForNameAndKey(name string, key string) (*User, error)
	FindUserForKey(name string, key string) (*User, error)
	FindUser(userID string) (*User, error)
	ValidateName(name string) bool
	ValidateName(name string) (bool, error)
	SetUserName(userID string, name string) error

	FindPosts() ([]*Post, error)

M wish/cms/db/postgres/storage.go => wish/cms/db/postgres/storage.go +12 -7
@@ 4,6 4,7 @@ import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"math"
	"strings"
	"time"


@@ 337,17 338,20 @@ func (me *PsqlDB) FindUser(userID string) (*db.User, error) {
	return user, nil
}

func (me *PsqlDB) ValidateName(name string) bool {
func (me *PsqlDB) ValidateName(name string) (bool, error) {
	lower := strings.ToLower(name)
	if slices.Contains(db.DenyList, lower) {
		return false
		return false, fmt.Errorf("%s is invalid: %w", lower, db.ErrNameDenied)
	}
	v := db.NameValidator.MatchString(lower)
	if !v {
		return false
		return false, fmt.Errorf("%s is invalid: %w", lower, db.ErrNameInvalid)
	}
	user, _ := me.FindUserForName(lower)
	return user == nil
	if user == nil {
		return true, nil
	}
	return false, fmt.Errorf("%s is invalid: %w", lower, db.ErrNameTaken)
}

func (me *PsqlDB) FindUserForName(name string) (*db.User, error) {


@@ 376,11 380,12 @@ func (me *PsqlDB) FindUserForNameAndKey(name string, key string) (*db.User, erro

func (me *PsqlDB) SetUserName(userID string, name string) error {
	lowerName := strings.ToLower(name)
	if !me.ValidateName(lowerName) {
		return errors.New("name is already taken")
	valid, err := me.ValidateName(lowerName)
	if !valid {
		return err
	}

	_, err := me.Db.Exec(sqlUpdateUserName, lowerName, userID)
	_, err = me.Db.Exec(sqlUpdateUserName, lowerName, userID)
	return err
}


M wish/cms/ui/account/create.go => wish/cms/ui/account/create.go +14 -9
@@ 1,6 1,7 @@
package account

import (
	"errors"
	"fmt"
	"strings"



@@ 134,12 135,9 @@ func Update(msg tea.Msg, m CreateModel) (CreateModel, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyCtrlC: // quit
		case tea.KeyCtrlC, tea.KeyEscape:
			m.Quit = true
			return m, nil
		case tea.KeyEscape: // exit this mini-app
			m.Done = true
			return m, nil

		default:
			// Ignore keys if we're submitting


@@ 240,6 238,10 @@ func Update(msg tea.Msg, m CreateModel) (CreateModel, tea.Cmd) {

// View renders current view from the model.
func View(m CreateModel) string {
	if !m.cfg.AllowRegister {
		return "Registration is closed for this service.  Press 'esc' to exit."
	}

	s := fmt.Sprintf("%s\n\n%s\n\n", m.cfg.Description, m.cfg.IntroText)
	s += "Enter a username\n\n"
	s += m.input.View() + "\n\n"


@@ 288,10 290,15 @@ func createAccount(m CreateModel) tea.Cmd {
			return NameInvalidMsg{}
		}

		valid, err := m.dbpool.ValidateName(m.newName)
		// Validate before resetting the session to potentially save some
		// network traffic and keep things feeling speedy.
		if !m.dbpool.ValidateName(m.newName) {
			return NameInvalidMsg{}
		if !valid {
			if errors.Is(err, db.ErrNameTaken) {
				return NameTakenMsg{}
			} else {
				return NameInvalidMsg{}
			}
		}

		user, err := registerUser(m)


@@ 300,9 307,7 @@ func createAccount(m CreateModel) tea.Cmd {
		}

		err = m.dbpool.SetUserName(user.ID, m.newName)
		if err == db.ErrNameTaken {
			return NameTakenMsg{}
		} else if err != nil {
		if err != nil {
			return errMsg{err}
		}


M wish/cms/ui/username/username.go => wish/cms/ui/username/username.go +10 -6
@@ 1,6 1,7 @@
package username

import (
	"errors"
	"fmt"
	"strings"



@@ 262,16 263,19 @@ func spinnerView(m Model) string {
// Attempt to update the username on the server.
func setName(m Model) tea.Cmd {
	return func() tea.Msg {
		valid, err := m.dbpool.ValidateName(m.newName)
		// Validate before resetting the session to potentially save some
		// network traffic and keep things feeling speedy.
		if !m.dbpool.ValidateName(m.newName) {
			return NameInvalidMsg{}
		if !valid {
			if errors.Is(err, db.ErrNameTaken) {
				return NameTakenMsg{}
			} else {
				return NameInvalidMsg{}
			}
		}

		err := m.dbpool.SetUserName(m.user.ID, m.newName)
		if err == db.ErrNameTaken {
			return NameTakenMsg{}
		} else if err != nil {
		err = m.dbpool.SetUserName(m.user.ID, m.newName)
		if err != nil {
			return errMsg{err}
		}