feat(user): user registration form
nclazz/soundwerft/pipeline/head This commit looks good
Details
nclazz/soundwerft/pipeline/head This commit looks good
Details
closes: #55master
parent
fa4a9c2861
commit
a00d768d9c
|
@ -62,7 +62,7 @@ func postLogin(h *handler.Handler) echo.HandlerFunc {
|
|||
sess.Values[handler.SessionKeyUser] = &handler.SessionUser{
|
||||
ID: usr.ID,
|
||||
Username: usr.Name.String(),
|
||||
IsAdmin: true,
|
||||
IsAdmin: usr.IsAdmin,
|
||||
Email: usr.Email.Address,
|
||||
}
|
||||
if err := h.SaveSession(c); err != nil {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package user
|
||||
|
||||
import "github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
import (
|
||||
"errors"
|
||||
"git.l--n.de/nclazz/soundwerft/api/handler"
|
||||
"git.l--n.de/nclazz/soundwerft/internal/app/user"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
MsgIncorrectUsernameOrPassword = &i18n.Message{
|
||||
|
@ -57,7 +62,7 @@ var (
|
|||
}
|
||||
MsgConfirmPasswordLabel = &i18n.Message{
|
||||
ID: "User.ConfirmPasswordLabel",
|
||||
Other: "Confirm new password",
|
||||
Other: "Confirm password",
|
||||
}
|
||||
MsgPasswordSaved = &i18n.Message{
|
||||
ID: "User.PasswordSaved",
|
||||
|
@ -76,4 +81,74 @@ var (
|
|||
ID: "User.ErrWrongPasswordConfirmation",
|
||||
Other: "The provided password did not match the new password",
|
||||
}
|
||||
MsgCreateAccount = &i18n.Message{
|
||||
ID: "User.CreateAccount",
|
||||
Other: "Create Account",
|
||||
}
|
||||
MsgPasswordLabel = &i18n.Message{
|
||||
ID: "User.PasswordLabel",
|
||||
Other: "Password",
|
||||
}
|
||||
MsgAlreadyHaveAccount = &i18n.Message{
|
||||
ID: "User.AlreadyHaveAccount",
|
||||
Other: "Already have an account?",
|
||||
}
|
||||
MsgLoginNow = &i18n.Message{
|
||||
ID: "User.LoginNow",
|
||||
Other: "Login now",
|
||||
}
|
||||
MsgErrCreateAccountFailed = &i18n.Message{
|
||||
ID: "User.ErrRegistration",
|
||||
Other: "Failed to create account. Please correct your input and try again",
|
||||
}
|
||||
MsgErrUsernameAlreadyExists = &i18n.Message{
|
||||
ID: "User.ErrUsernameAlreadyExists",
|
||||
Other: "Another user with this username already exists",
|
||||
}
|
||||
MsgErrEmailAlreadyExists = &i18n.Message{
|
||||
ID: "User.ErrEmailAlreadyExists",
|
||||
Other: "Another user with this email address already exists",
|
||||
}
|
||||
MsgErrInvalidName = &i18n.Message{
|
||||
ID: "User.ErrInvalidName",
|
||||
Other: "Not a valid name. Must only include alphanumeric characters or _",
|
||||
}
|
||||
// TODO needs a different approach when implementing policies in #64
|
||||
MsgErrInvalidPassword = &i18n.Message{
|
||||
ID: "User.ErrInvalidPassword",
|
||||
Other: "Not a valid password",
|
||||
}
|
||||
MsgErrInvalidEmail = &i18n.Message{
|
||||
ID: "User.ErrInvalidEmail",
|
||||
Other: "Not a valid email address",
|
||||
}
|
||||
MsgErrUserNotFound = &i18n.Message{
|
||||
ID: "User.ErrUserNotFound",
|
||||
Other: "User does not exist",
|
||||
}
|
||||
)
|
||||
|
||||
func TranslateError(h *handler.Handler, err error) string {
|
||||
if errors.Is(err, user.ErrInvalidName) {
|
||||
return h.TranslateMessage(MsgErrInvalidName)
|
||||
}
|
||||
if errors.Is(err, user.ErrInvalidPassword) {
|
||||
return h.TranslateMessage(MsgErrInvalidPassword)
|
||||
}
|
||||
if errors.Is(err, user.ErrInvalidEmail) {
|
||||
return h.TranslateMessage(MsgErrInvalidEmail)
|
||||
}
|
||||
if errors.Is(err, user.ErrUsernameAlreadyExists) {
|
||||
return h.TranslateMessage(MsgErrUsernameAlreadyExists)
|
||||
}
|
||||
if errors.Is(err, user.ErrEmailAlreadyExists) {
|
||||
return h.TranslateMessage(MsgErrEmailAlreadyExists)
|
||||
}
|
||||
if errors.Is(err, user.ErrRegistrationFailed) {
|
||||
return h.TranslateMessage(MsgErrCreateAccountFailed)
|
||||
}
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return h.TranslateMessage(MsgErrUserNotFound)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.l--n.de/nclazz/soundwerft/api/handler"
|
||||
"git.l--n.de/nclazz/soundwerft/api/web/templates"
|
||||
userApi "git.l--n.de/nclazz/soundwerft/internal/app/user"
|
||||
"git.l--n.de/nclazz/soundwerft/pkg/errx"
|
||||
"github.com/labstack/echo/v4"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func registerFormView(c echo.Context, h *handler.Handler) templates.RegisterFormView {
|
||||
return templates.RegisterFormView{
|
||||
Title: h.TranslateMessage(MsgCreateAccount),
|
||||
MsgUsernameLabel: h.TranslateMessage(MsgUsernameLabel),
|
||||
MsgEmailLabel: h.TranslateMessage(MsgEmailLabel),
|
||||
MsgPasswordLabel: h.TranslateMessage(MsgPasswordLabel),
|
||||
MsgPasswordConfirmLabel: h.TranslateMessage(MsgConfirmPasswordLabel),
|
||||
MsgAlreadyHaveAccount: h.TranslateMessage(MsgAlreadyHaveAccount),
|
||||
MsgLoginNow: h.TranslateMessage(MsgLoginNow),
|
||||
MsgRegister: h.TranslateMessage(MsgCreateAccount),
|
||||
}
|
||||
}
|
||||
|
||||
func getRegisterPage(h *handler.Handler) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return h.Render(c, templates.RegisterPage(h.View(c), registerFormView(c, h)))
|
||||
}
|
||||
}
|
||||
|
||||
func postRegister(h *handler.Handler) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
name := c.FormValue("name")
|
||||
email := c.FormValue("email")
|
||||
password := c.FormValue("password")
|
||||
passwordConfirm := c.FormValue("password_confirm")
|
||||
log := h.Logger(c).With(slog.Group("form", "name", name, "email", email))
|
||||
view := registerFormView(c, h)
|
||||
view.Name = name
|
||||
view.Email = email
|
||||
if password != passwordConfirm {
|
||||
view.Err = h.TranslateMessage(MsgErrCreateAccountFailed)
|
||||
view.PasswordConfirmErr = h.TranslateMessage(MsgErrWrongPasswordConfirmation)
|
||||
return h.Render(c, templates.RegisterForm(h.View(c), view))
|
||||
}
|
||||
|
||||
log.Info("register user")
|
||||
user, err := h.Services.Users.RegisterUser(ctx, userApi.RegisterUserCommand{
|
||||
Name: name,
|
||||
Email: email,
|
||||
Password: password,
|
||||
IsAdmin: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("register user failed", "error", err)
|
||||
view.Err = h.TranslateMessage(MsgErrCreateAccountFailed)
|
||||
mapRegistrationErrs(h, &view, err)
|
||||
return h.Render(c, templates.RegisterForm(h.View(c), view))
|
||||
}
|
||||
log.Info("registered user", slog.Group("user", "id", user.ID, "name", user.Name, "email", user.Email))
|
||||
// TODO add flash message for confirmation
|
||||
return h.HardRedirect(c, h.View(c).URLString("/login"))
|
||||
}
|
||||
}
|
||||
|
||||
func mapRegistrationErrs(h *handler.Handler, view *templates.RegisterFormView, err error) {
|
||||
var errs errx.ErrMap
|
||||
if errors.As(err, &errs) {
|
||||
view.NameErr = TranslateError(h, errs.Get("name"))
|
||||
view.EmailErr = TranslateError(h, errs.Get("email"))
|
||||
view.PasswordErr = TranslateError(h, errs.Get("password"))
|
||||
return
|
||||
}
|
||||
if errors.Is(err, userApi.ErrUsernameAlreadyExists) {
|
||||
view.NameErr = TranslateError(h, err)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, userApi.ErrEmailAlreadyExists) {
|
||||
view.EmailErr = TranslateError(h, err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -16,4 +16,9 @@ func Routes(h *handler.Handler) {
|
|||
g.PUT("/name", putName(h))
|
||||
g.PUT("/email", putEmail(h))
|
||||
g.PUT("/password", putPassword(h))
|
||||
|
||||
g = h.Echo.Group("/register")
|
||||
g.GET("", getRegisterPage(h))
|
||||
g.GET("/", getRegisterPage(h))
|
||||
g.POST("", postRegister(h))
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ templ LoginForm(view web.View, form LoginFormView) {
|
|||
})
|
||||
<div class="d-grid gap-2">
|
||||
<input type="submit" class="btn btn-primary btn-lg" value="Login"/>
|
||||
<p>{ form.MsgNoAccountYet } <a href={ view.URL("/signup") } hx-boost="true"
|
||||
<p>{ form.MsgNoAccountYet } <a href={ view.URL("/register") } hx-boost="true"
|
||||
hx-select="#main"
|
||||
hx-target="#main">{ form.MsgSignUpNow }</a>
|
||||
</p>
|
||||
|
|
|
@ -64,12 +64,12 @@ templ DocumentHeader(view web.View) {
|
|||
hx-select="#main">
|
||||
Login
|
||||
</a>
|
||||
<a href={ view.URL("/signup") }
|
||||
<a href={ view.URL("/register") }
|
||||
class="nav-item btn btn-primary"
|
||||
hx-boost="true"
|
||||
hx-target="#main"
|
||||
hx-select="#main">
|
||||
Signup
|
||||
Register
|
||||
</a>
|
||||
}else {
|
||||
@UserMenu(view)
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"git.l--n.de/nclazz/soundwerft/api/web"
|
||||
"git.l--n.de/nclazz/soundwerft/api/web/templates/partials"
|
||||
)
|
||||
|
||||
type RegisterFormView struct {
|
||||
Err string
|
||||
Title string
|
||||
Name string
|
||||
Email string
|
||||
NameErr string
|
||||
EmailErr string
|
||||
PasswordErr string
|
||||
PasswordConfirmErr string
|
||||
MsgUsernameLabel string
|
||||
MsgEmailLabel string
|
||||
MsgPasswordLabel string
|
||||
MsgPasswordConfirmLabel string
|
||||
MsgAlreadyHaveAccount string
|
||||
MsgLoginNow string
|
||||
MsgRegister string
|
||||
}
|
||||
|
||||
|
||||
templ RegisterPage(view web.View, form RegisterFormView) {
|
||||
@partials.Document(view) {
|
||||
@RegisterForm(view, form)
|
||||
}
|
||||
}
|
||||
|
||||
templ RegisterForm(view web.View, form RegisterFormView) {
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-3">
|
||||
<h4>{ form.Title }</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-3">
|
||||
<form class="form"
|
||||
method="post"
|
||||
action={view.URL("/register")}
|
||||
hx-boost="true"
|
||||
hx-target="#main"
|
||||
hx-replace-url="true">
|
||||
|
||||
if form.Err != "" {
|
||||
@partials.Alert("danger") {
|
||||
{ form.Err }
|
||||
}
|
||||
}
|
||||
@partials.FormInput(partials.InputAttrs{
|
||||
Name: "name",
|
||||
Label: form.MsgUsernameLabel,
|
||||
Value: form.Name,
|
||||
Placeholder: form.MsgUsernameLabel,
|
||||
Required: true,
|
||||
HasErr: form.NameErr != "",
|
||||
Err: form.NameErr,
|
||||
})
|
||||
@partials.FormInput(partials.InputAttrs{
|
||||
Name: "email",
|
||||
Label: form.MsgEmailLabel,
|
||||
Value: form.Email,
|
||||
Placeholder: form.MsgEmailLabel,
|
||||
Required: true,
|
||||
HasErr: form.EmailErr != "",
|
||||
Err: form.EmailErr,
|
||||
})
|
||||
@partials.FormInput(partials.InputAttrs{
|
||||
Name: "password",
|
||||
Label: form.MsgPasswordLabel,
|
||||
Type: "password",
|
||||
Placeholder: form.MsgPasswordLabel,
|
||||
Required: true,
|
||||
HasErr: form.PasswordErr != "",
|
||||
Err: form.PasswordErr,
|
||||
})
|
||||
@partials.FormInput(partials.InputAttrs{
|
||||
Name: "password_confirm",
|
||||
Label: form.MsgPasswordConfirmLabel,
|
||||
Type: "password",
|
||||
Placeholder: form.MsgPasswordConfirmLabel,
|
||||
Required: true,
|
||||
HasErr: form.PasswordConfirmErr != "",
|
||||
Err: form.PasswordConfirmErr,
|
||||
})
|
||||
<div class="d-grid gap-2">
|
||||
<input type="submit" class="btn btn-primary btn-lg" value={ form.MsgRegister }/>
|
||||
<p>{ form.MsgAlreadyHaveAccount } <a href={ view.URL("/login") } hx-boost="true"
|
||||
hx-select="#main"
|
||||
hx-target="#main">{ form.MsgLoginNow }</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -52,6 +52,6 @@ func main() {
|
|||
if adminExists {
|
||||
slog.Info("admin user already exists")
|
||||
}
|
||||
ps.Start(ctx)
|
||||
go ps.Start(ctx)
|
||||
errx.MustExec(server.Start())
|
||||
}
|
||||
|
|
|
@ -2,48 +2,17 @@ package user
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidName = errors.New("invalid name")
|
||||
ErrInvalidPassword = errors.New("invalid password")
|
||||
ErrInvalidEmail = errors.New("invalid email")
|
||||
ErrUsernameAlreadyExists = errors.New("username already exists")
|
||||
ErrEmailAlreadyExists = errors.New("email already exists")
|
||||
ErrRegistrationFailed = errors.New("user registration failed")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
)
|
||||
var (
|
||||
TranslationErrInvalidName = &i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: "User.ErrInvalidName",
|
||||
Other: "Username is invalid",
|
||||
},
|
||||
}
|
||||
TranslationErrInvalidPassword = &i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: "User.ErrInvalidPassword",
|
||||
Other: "Password is invalid",
|
||||
},
|
||||
}
|
||||
TranslationErrUsernameAlreadyExists = &i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: "User.ErrUsernameAlreadyExists",
|
||||
Other: "Username already exists",
|
||||
},
|
||||
}
|
||||
TranslationErrEmailAlreadyExists = &i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: "User.ErrEmailAlreadyExists",
|
||||
Other: "Email already exists",
|
||||
},
|
||||
}
|
||||
TranslationErrRegistrationFailed = &i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: "User.RegistrationFailed",
|
||||
Other: "Registration has failed",
|
||||
},
|
||||
}
|
||||
ErrUserError = errors.New("user")
|
||||
ErrInvalidName = fmt.Errorf("%w: invalid name", ErrUserError)
|
||||
ErrInvalidPassword = fmt.Errorf("%w: invalid password", ErrUserError)
|
||||
ErrInvalidEmail = fmt.Errorf("%w: invalid email", ErrUserError)
|
||||
ErrUsernameAlreadyExists = fmt.Errorf("%w: username already exists", ErrUserError)
|
||||
ErrEmailAlreadyExists = fmt.Errorf("%w: email already exists", ErrUserError)
|
||||
ErrRegistrationFailed = fmt.Errorf("%w: user registration failed", ErrUserError)
|
||||
ErrUserNotFound = fmt.Errorf("%w: user not found", ErrUserError)
|
||||
ErrInvalidCredentials = fmt.Errorf("%w: invalid credentials", ErrUserError)
|
||||
)
|
||||
|
|
|
@ -22,6 +22,14 @@ func (m ErrMap) Get(key string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m ErrMap) GetString(key string) string {
|
||||
err := m.Get(key)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *ErrMap) Has(key string) bool {
|
||||
_, ok := (*m)[key]
|
||||
|
||||
|
|
|
@ -1,25 +1,31 @@
|
|||
Save = "Save"
|
||||
"User.ConfirmPasswordLabel" = "Confirm new password"
|
||||
"User.AlreadyHaveAccount" = "Already have an account?"
|
||||
"User.ConfirmPasswordLabel" = "Confirm password"
|
||||
"User.CreateAccount" = "Create Account"
|
||||
"User.CurrentPasswordLabel" = "Current password"
|
||||
"User.EmailLabel" = "Email address"
|
||||
"User.EmailSaved" = "Successfully saved email!"
|
||||
"User.ErrEmailAlreadyExists" = "Email already exists"
|
||||
"User.ErrEmailAlreadyExists" = "Another user with this email address already exists"
|
||||
"User.ErrIncorrectUsernameOrPassword" = "Incorrect username or password."
|
||||
"User.ErrInvalidName" = "Username is invalid"
|
||||
"User.ErrInvalidPassword" = "Password is invalid"
|
||||
"User.ErrInvalidEmail" = "Not a valid email address"
|
||||
"User.ErrInvalidName" = "Not a valid name. Must only include alphanumeric characters or _"
|
||||
"User.ErrInvalidPassword" = "Not a valid password"
|
||||
"User.ErrPasswordNotVerified" = "The provided password was not correct"
|
||||
"User.ErrPasswordSave" = "Failed to update your password. Please correct your input and try again"
|
||||
"User.ErrUsernameAlreadyExists" = "Username already exists"
|
||||
"User.ErrRegistration" = "Failed to create account. Please correct your input and try again"
|
||||
"User.ErrUserNotFound" = "User does not exist"
|
||||
"User.ErrUsernameAlreadyExists" = "Another user with this username already exists"
|
||||
"User.ErrWrongPasswordConfirmation" = "The provided password did not match the new password"
|
||||
"User.ForgotPassword" = "Forgot Password"
|
||||
"User.LoginKeepLoggedIn" = "Keep logged in"
|
||||
"User.LoginNow" = "Login now"
|
||||
"User.LoginPasswordLabel" = "Password"
|
||||
"User.LoginUsernameLabel" = "Username or email address"
|
||||
"User.NewPasswordLabel" = "New password"
|
||||
"User.NoAccountYet" = "No account yet?"
|
||||
"User.PasswordLabel" = "Password"
|
||||
"User.PasswordSaved" = "Successfully updated password!"
|
||||
"User.RegisterNow" = "Register now"
|
||||
"User.RegistrationFailed" = "Registration has failed"
|
||||
"User.UsernameLabel" = "Username"
|
||||
"User.UsernameSaved" = "Successfully saved username!"
|
||||
|
||||
|
|
Loading…
Reference in New Issue