mirror of
https://github.com/openziti/zrok.git
synced 2024-11-25 01:23:49 +01:00
Enhanced password requirements and relevant ui changes
This commit is contained in:
parent
d34e024b66
commit
64fbfbf1d3
@ -1,12 +1,13 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/openziti/zrok/controller/emailUi"
|
||||
"github.com/openziti/zrok/controller/env"
|
||||
"github.com/openziti/zrok/controller/limits"
|
||||
"github.com/openziti/zrok/controller/metrics"
|
||||
"github.com/openziti/zrok/controller/zrokEdgeSdk"
|
||||
"time"
|
||||
|
||||
"github.com/michaelquigley/cf"
|
||||
"github.com/openziti/zrok/controller/store"
|
||||
@ -28,6 +29,7 @@ type Config struct {
|
||||
ResetPassword *ResetPasswordConfig
|
||||
Store *store.Config
|
||||
Ziti *zrokEdgeSdk.Config
|
||||
PasswordRequirements *PaswordRequirementsConfig
|
||||
}
|
||||
|
||||
type AdminConfig struct {
|
||||
@ -47,6 +49,14 @@ type RegistrationConfig struct {
|
||||
RegistrationUrlTemplate string
|
||||
}
|
||||
|
||||
type PaswordRequirementsConfig struct {
|
||||
Length int
|
||||
RequireCapital bool
|
||||
RequireNumeric bool
|
||||
RequireSpecial bool
|
||||
ValidSpecialCharacters string
|
||||
}
|
||||
|
||||
type ResetPasswordConfig struct {
|
||||
ResetUrlTemplate string
|
||||
}
|
||||
@ -71,6 +81,13 @@ type ResetPasswordMaintenanceConfig struct {
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Limits: limits.DefaultConfig(),
|
||||
PasswordRequirements: &PaswordRequirementsConfig{
|
||||
Length: 8,
|
||||
RequireCapital: true,
|
||||
RequireNumeric: true,
|
||||
RequireSpecial: true,
|
||||
ValidSpecialCharacters: `!@$&*_-., "#%'()+/:;<=>?[\]^{|}~`,
|
||||
},
|
||||
Maintenance: &MaintenanceConfig{
|
||||
ResetPassword: &ResetPasswordMaintenanceConfig{
|
||||
ExpirationTimeout: time.Minute * 15,
|
||||
|
@ -28,5 +28,14 @@ func (ch *configurationHandler) Handle(_ metadata.ConfigurationParams) middlewar
|
||||
data.TouLink = cfg.Admin.TouLink
|
||||
data.InviteTokenContact = cfg.Admin.InviteTokenContact
|
||||
}
|
||||
if cfg.PasswordRequirements != nil {
|
||||
data.PasswordRequirements = &rest_model_zrok.PasswordRequirements{
|
||||
Length: int64(cfg.PasswordRequirements.Length),
|
||||
RequireCapital: cfg.PasswordRequirements.RequireCapital,
|
||||
RequireNumeric: cfg.PasswordRequirements.RequireNumeric,
|
||||
RequireSpecial: cfg.PasswordRequirements.RequireSpecial,
|
||||
ValidSpecialCharacters: cfg.PasswordRequirements.ValidSpecialCharacters,
|
||||
}
|
||||
}
|
||||
return metadata.NewConfigurationOK().WithPayload(data)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openziti/zrok/controller/config"
|
||||
"github.com/openziti/zrok/controller/limits"
|
||||
"github.com/openziti/zrok/controller/metrics"
|
||||
@ -34,8 +35,8 @@ func Run(inCfg *config.Config) error {
|
||||
api.KeyAuth = newZrokAuthenticator(cfg).authenticate
|
||||
api.AccountInviteHandler = newInviteHandler(cfg)
|
||||
api.AccountLoginHandler = account.LoginHandlerFunc(loginHandler)
|
||||
api.AccountRegisterHandler = newRegisterHandler()
|
||||
api.AccountResetPasswordHandler = newResetPasswordHandler()
|
||||
api.AccountRegisterHandler = newRegisterHandler(cfg)
|
||||
api.AccountResetPasswordHandler = newResetPasswordHandler(cfg)
|
||||
api.AccountResetPasswordRequestHandler = newResetPasswordRequestHandler()
|
||||
api.AccountVerifyHandler = newVerifyHandler()
|
||||
api.AdminCreateFrontendHandler = newCreateFrontendHandler()
|
||||
|
@ -2,16 +2,21 @@ package controller
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/openziti/zrok/controller/config"
|
||||
"github.com/openziti/zrok/controller/store"
|
||||
"github.com/openziti/zrok/rest_model_zrok"
|
||||
"github.com/openziti/zrok/rest_server_zrok/operations/account"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type registerHandler struct{}
|
||||
type registerHandler struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func newRegisterHandler() *registerHandler {
|
||||
return ®isterHandler{}
|
||||
func newRegisterHandler(cfg *config.Config) *registerHandler {
|
||||
return ®isterHandler{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
func (h *registerHandler) Handle(params account.RegisterParams) middleware.Responder {
|
||||
if params.Body == nil || params.Body.Token == "" || params.Body.Password == "" {
|
||||
@ -38,6 +43,12 @@ func (h *registerHandler) Handle(params account.RegisterParams) middleware.Respo
|
||||
logrus.Errorf("error creating token for request '%v' (%v): %v", params.Body.Token, ar.Email, err)
|
||||
return account.NewRegisterInternalServerError()
|
||||
}
|
||||
|
||||
if err := validatePassword(h.cfg, params.Body.Password); err != nil {
|
||||
logrus.Errorf("password not valid for request '%v', (%v): %v", params.Body.Token, ar.Email, err)
|
||||
return account.NewRegisterUnprocessableEntity().WithPayload(rest_model_zrok.ErrorMessage(err.Error()))
|
||||
}
|
||||
|
||||
hpwd, err := hashPassword(params.Body.Password)
|
||||
if err != nil {
|
||||
logrus.Errorf("error hashing password for request '%v' (%v): %v", params.Body.Token, ar.Email, err)
|
||||
|
@ -2,14 +2,20 @@ package controller
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/openziti/zrok/controller/config"
|
||||
"github.com/openziti/zrok/rest_model_zrok"
|
||||
"github.com/openziti/zrok/rest_server_zrok/operations/account"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type resetPasswordHandler struct{}
|
||||
type resetPasswordHandler struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func newResetPasswordHandler() *resetPasswordHandler {
|
||||
return &resetPasswordHandler{}
|
||||
func newResetPasswordHandler(cfg *config.Config) *resetPasswordHandler {
|
||||
return &resetPasswordHandler{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *resetPasswordHandler) Handle(params account.ResetPasswordParams) middleware.Responder {
|
||||
@ -41,6 +47,12 @@ func (handler *resetPasswordHandler) Handle(params account.ResetPasswordParams)
|
||||
logrus.Errorf("account '%v' for '%v' deleted", a.Email, a.Token)
|
||||
return account.NewResetPasswordNotFound()
|
||||
}
|
||||
|
||||
if err := validatePassword(handler.cfg, params.Body.Password); err != nil {
|
||||
logrus.Errorf("password not valid for request '%v', (%v): %v", params.Body.Token, a.Email, err)
|
||||
return account.NewResetPasswordUnprocessableEntity().WithPayload(rest_model_zrok.ErrorMessage(err.Error()))
|
||||
}
|
||||
|
||||
hpwd, err := hashPassword(params.Body.Password)
|
||||
if err != nil {
|
||||
logrus.Errorf("error hashing password for '%v' (%v): %v", params.Body.Token, a.Email, err)
|
||||
|
@ -1,13 +1,16 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
errors2 "github.com/go-openapi/errors"
|
||||
"github.com/jaevor/go-nanoid"
|
||||
"github.com/openziti/zrok/controller/config"
|
||||
"github.com/openziti/zrok/rest_model_zrok"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type zrokAuthenticator struct {
|
||||
@ -87,3 +90,43 @@ func realRemoteAddress(req *http.Request) string {
|
||||
func proxyUrl(shrToken, template string) string {
|
||||
return strings.Replace(template, "{token}", shrToken, -1)
|
||||
}
|
||||
|
||||
func validatePassword(cfg *config.Config, password string) error {
|
||||
if cfg.PasswordRequirements.Length > len(password) {
|
||||
return fmt.Errorf("password length: expected (%d), got (%d)", cfg.PasswordRequirements.Length, len(password))
|
||||
}
|
||||
if cfg.PasswordRequirements.RequireCapital {
|
||||
if !hasCapital(password) {
|
||||
return fmt.Errorf("password requires capital, found none")
|
||||
}
|
||||
}
|
||||
if cfg.PasswordRequirements.RequireNumeric {
|
||||
if !hasNumeric(password) {
|
||||
return fmt.Errorf("password requires numeric, found none")
|
||||
}
|
||||
}
|
||||
if cfg.PasswordRequirements.RequireSpecial {
|
||||
if !strings.ContainsAny(password, cfg.PasswordRequirements.ValidSpecialCharacters) {
|
||||
return fmt.Errorf("password requires special character, found none")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasCapital(check string) bool {
|
||||
for _, c := range check {
|
||||
if unicode.IsUpper(c) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasNumeric(check string) bool {
|
||||
for _, c := range check {
|
||||
if unicode.IsDigit(c) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
10
etc/ctrl.yml
10
etc/ctrl.yml
@ -145,6 +145,16 @@ metrics:
|
||||
registration:
|
||||
registration_url_template: https://zrok.server.com/register
|
||||
|
||||
# Configure password requirements for user accounts.
|
||||
#
|
||||
password_requirements:
|
||||
length: 8
|
||||
require_capital: true
|
||||
require_numeric: true
|
||||
require_special: true
|
||||
# Denote which characters satisfy the `require_special` requirement. Note the need to escape specific characters.
|
||||
valid_special_characters: "\"\\`'~!@#$%^&*()[],./"
|
||||
|
||||
# Configure the generated URL for password resets. The reset token will be appended to this URL.
|
||||
#
|
||||
reset_password:
|
||||
|
@ -35,6 +35,12 @@ func (o *RegisterReader) ReadResponse(response runtime.ClientResponse, consumer
|
||||
return nil, err
|
||||
}
|
||||
return nil, result
|
||||
case 422:
|
||||
result := NewRegisterUnprocessableEntity()
|
||||
if err := result.readResponse(response, consumer, o.formats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, result
|
||||
case 500:
|
||||
result := NewRegisterInternalServerError()
|
||||
if err := result.readResponse(response, consumer, o.formats); err != nil {
|
||||
@ -160,6 +166,67 @@ func (o *RegisterNotFound) readResponse(response runtime.ClientResponse, consume
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRegisterUnprocessableEntity creates a RegisterUnprocessableEntity with default headers values
|
||||
func NewRegisterUnprocessableEntity() *RegisterUnprocessableEntity {
|
||||
return &RegisterUnprocessableEntity{}
|
||||
}
|
||||
|
||||
/*
|
||||
RegisterUnprocessableEntity describes a response with status code 422, with default header values.
|
||||
|
||||
password validation failure
|
||||
*/
|
||||
type RegisterUnprocessableEntity struct {
|
||||
Payload rest_model_zrok.ErrorMessage
|
||||
}
|
||||
|
||||
// IsSuccess returns true when this register unprocessable entity response has a 2xx status code
|
||||
func (o *RegisterUnprocessableEntity) IsSuccess() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsRedirect returns true when this register unprocessable entity response has a 3xx status code
|
||||
func (o *RegisterUnprocessableEntity) IsRedirect() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsClientError returns true when this register unprocessable entity response has a 4xx status code
|
||||
func (o *RegisterUnprocessableEntity) IsClientError() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsServerError returns true when this register unprocessable entity response has a 5xx status code
|
||||
func (o *RegisterUnprocessableEntity) IsServerError() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCode returns true when this register unprocessable entity response a status code equal to that given
|
||||
func (o *RegisterUnprocessableEntity) IsCode(code int) bool {
|
||||
return code == 422
|
||||
}
|
||||
|
||||
func (o *RegisterUnprocessableEntity) Error() string {
|
||||
return fmt.Sprintf("[POST /register][%d] registerUnprocessableEntity %+v", 422, o.Payload)
|
||||
}
|
||||
|
||||
func (o *RegisterUnprocessableEntity) String() string {
|
||||
return fmt.Sprintf("[POST /register][%d] registerUnprocessableEntity %+v", 422, o.Payload)
|
||||
}
|
||||
|
||||
func (o *RegisterUnprocessableEntity) GetPayload() rest_model_zrok.ErrorMessage {
|
||||
return o.Payload
|
||||
}
|
||||
|
||||
func (o *RegisterUnprocessableEntity) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
|
||||
|
||||
// response payload
|
||||
if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRegisterInternalServerError creates a RegisterInternalServerError with default headers values
|
||||
func NewRegisterInternalServerError() *RegisterInternalServerError {
|
||||
return &RegisterInternalServerError{}
|
||||
|
@ -7,9 +7,12 @@ package account
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/strfmt"
|
||||
|
||||
"github.com/openziti/zrok/rest_model_zrok"
|
||||
)
|
||||
|
||||
// ResetPasswordReader is a Reader for the ResetPassword structure.
|
||||
@ -32,6 +35,12 @@ func (o *ResetPasswordReader) ReadResponse(response runtime.ClientResponse, cons
|
||||
return nil, err
|
||||
}
|
||||
return nil, result
|
||||
case 422:
|
||||
result := NewResetPasswordUnprocessableEntity()
|
||||
if err := result.readResponse(response, consumer, o.formats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, result
|
||||
case 500:
|
||||
result := NewResetPasswordInternalServerError()
|
||||
if err := result.readResponse(response, consumer, o.formats); err != nil {
|
||||
@ -145,6 +154,67 @@ func (o *ResetPasswordNotFound) readResponse(response runtime.ClientResponse, co
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewResetPasswordUnprocessableEntity creates a ResetPasswordUnprocessableEntity with default headers values
|
||||
func NewResetPasswordUnprocessableEntity() *ResetPasswordUnprocessableEntity {
|
||||
return &ResetPasswordUnprocessableEntity{}
|
||||
}
|
||||
|
||||
/*
|
||||
ResetPasswordUnprocessableEntity describes a response with status code 422, with default header values.
|
||||
|
||||
password validation failure
|
||||
*/
|
||||
type ResetPasswordUnprocessableEntity struct {
|
||||
Payload rest_model_zrok.ErrorMessage
|
||||
}
|
||||
|
||||
// IsSuccess returns true when this reset password unprocessable entity response has a 2xx status code
|
||||
func (o *ResetPasswordUnprocessableEntity) IsSuccess() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsRedirect returns true when this reset password unprocessable entity response has a 3xx status code
|
||||
func (o *ResetPasswordUnprocessableEntity) IsRedirect() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsClientError returns true when this reset password unprocessable entity response has a 4xx status code
|
||||
func (o *ResetPasswordUnprocessableEntity) IsClientError() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsServerError returns true when this reset password unprocessable entity response has a 5xx status code
|
||||
func (o *ResetPasswordUnprocessableEntity) IsServerError() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCode returns true when this reset password unprocessable entity response a status code equal to that given
|
||||
func (o *ResetPasswordUnprocessableEntity) IsCode(code int) bool {
|
||||
return code == 422
|
||||
}
|
||||
|
||||
func (o *ResetPasswordUnprocessableEntity) Error() string {
|
||||
return fmt.Sprintf("[POST /resetPassword][%d] resetPasswordUnprocessableEntity %+v", 422, o.Payload)
|
||||
}
|
||||
|
||||
func (o *ResetPasswordUnprocessableEntity) String() string {
|
||||
return fmt.Sprintf("[POST /resetPassword][%d] resetPasswordUnprocessableEntity %+v", 422, o.Payload)
|
||||
}
|
||||
|
||||
func (o *ResetPasswordUnprocessableEntity) GetPayload() rest_model_zrok.ErrorMessage {
|
||||
return o.Payload
|
||||
}
|
||||
|
||||
func (o *ResetPasswordUnprocessableEntity) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
|
||||
|
||||
// response payload
|
||||
if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewResetPasswordInternalServerError creates a ResetPasswordInternalServerError with default headers values
|
||||
func NewResetPasswordInternalServerError() *ResetPasswordInternalServerError {
|
||||
return &ResetPasswordInternalServerError{}
|
||||
|
@ -8,6 +8,7 @@ package rest_model_zrok
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
@ -23,6 +24,9 @@ type Configuration struct {
|
||||
// invites open
|
||||
InvitesOpen bool `json:"invitesOpen,omitempty"`
|
||||
|
||||
// password requirements
|
||||
PasswordRequirements *PasswordRequirements `json:"passwordRequirements,omitempty"`
|
||||
|
||||
// requires invite token
|
||||
RequiresInviteToken bool `json:"requiresInviteToken,omitempty"`
|
||||
|
||||
@ -35,11 +39,64 @@ type Configuration struct {
|
||||
|
||||
// Validate validates this configuration
|
||||
func (m *Configuration) Validate(formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.validatePasswordRequirements(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextValidate validates this configuration based on context it is used
|
||||
func (m *Configuration) validatePasswordRequirements(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.PasswordRequirements) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.PasswordRequirements != nil {
|
||||
if err := m.PasswordRequirements.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("passwordRequirements")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("passwordRequirements")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextValidate validate this configuration based on the context it is used
|
||||
func (m *Configuration) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.contextValidatePasswordRequirements(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Configuration) contextValidatePasswordRequirements(ctx context.Context, formats strfmt.Registry) error {
|
||||
|
||||
if m.PasswordRequirements != nil {
|
||||
if err := m.PasswordRequirements.ContextValidate(ctx, formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("passwordRequirements")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("passwordRequirements")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
62
rest_model_zrok/password_requirements.go
Normal file
62
rest_model_zrok/password_requirements.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
package rest_model_zrok
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
|
||||
// PasswordRequirements password requirements
|
||||
//
|
||||
// swagger:model passwordRequirements
|
||||
type PasswordRequirements struct {
|
||||
|
||||
// length
|
||||
Length int64 `json:"length,omitempty"`
|
||||
|
||||
// require capital
|
||||
RequireCapital bool `json:"requireCapital,omitempty"`
|
||||
|
||||
// require numeric
|
||||
RequireNumeric bool `json:"requireNumeric,omitempty"`
|
||||
|
||||
// require special
|
||||
RequireSpecial bool `json:"requireSpecial,omitempty"`
|
||||
|
||||
// valid special characters
|
||||
ValidSpecialCharacters string `json:"validSpecialCharacters,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this password requirements
|
||||
func (m *PasswordRequirements) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextValidate validates this password requirements based on context it is used
|
||||
func (m *PasswordRequirements) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary interface implementation
|
||||
func (m *PasswordRequirements) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return swag.WriteJSON(m)
|
||||
}
|
||||
|
||||
// UnmarshalBinary interface implementation
|
||||
func (m *PasswordRequirements) UnmarshalBinary(b []byte) error {
|
||||
var res PasswordRequirements
|
||||
if err := swag.ReadJSON(b, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = res
|
||||
return nil
|
||||
}
|
@ -754,6 +754,12 @@ func init() {
|
||||
"404": {
|
||||
"description": "request not found"
|
||||
},
|
||||
"422": {
|
||||
"description": "password validation failure",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/errorMessage"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error"
|
||||
}
|
||||
@ -782,6 +788,12 @@ func init() {
|
||||
"404": {
|
||||
"description": "request not found"
|
||||
},
|
||||
"422": {
|
||||
"description": "password validation failure",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/errorMessage"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error"
|
||||
}
|
||||
@ -1062,6 +1074,9 @@ func init() {
|
||||
"invitesOpen": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"passwordRequirements": {
|
||||
"$ref": "#/definitions/passwordRequirements"
|
||||
},
|
||||
"requiresInviteToken": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@ -1295,6 +1310,26 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordRequirements": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"requireCapital": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requireNumeric": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requireSpecial": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"validSpecialCharacters": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"principal": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -2315,6 +2350,12 @@ func init() {
|
||||
"404": {
|
||||
"description": "request not found"
|
||||
},
|
||||
"422": {
|
||||
"description": "password validation failure",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/errorMessage"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error"
|
||||
}
|
||||
@ -2343,6 +2384,12 @@ func init() {
|
||||
"404": {
|
||||
"description": "request not found"
|
||||
},
|
||||
"422": {
|
||||
"description": "password validation failure",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/errorMessage"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error"
|
||||
}
|
||||
@ -2623,6 +2670,9 @@ func init() {
|
||||
"invitesOpen": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"passwordRequirements": {
|
||||
"$ref": "#/definitions/passwordRequirements"
|
||||
},
|
||||
"requiresInviteToken": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@ -2856,6 +2906,26 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordRequirements": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"requireCapital": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requireNumeric": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requireSpecial": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"validSpecialCharacters": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"principal": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -83,6 +83,49 @@ func (o *RegisterNotFound) WriteResponse(rw http.ResponseWriter, producer runtim
|
||||
rw.WriteHeader(404)
|
||||
}
|
||||
|
||||
// RegisterUnprocessableEntityCode is the HTTP code returned for type RegisterUnprocessableEntity
|
||||
const RegisterUnprocessableEntityCode int = 422
|
||||
|
||||
/*
|
||||
RegisterUnprocessableEntity password validation failure
|
||||
|
||||
swagger:response registerUnprocessableEntity
|
||||
*/
|
||||
type RegisterUnprocessableEntity struct {
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload rest_model_zrok.ErrorMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewRegisterUnprocessableEntity creates RegisterUnprocessableEntity with default headers values
|
||||
func NewRegisterUnprocessableEntity() *RegisterUnprocessableEntity {
|
||||
|
||||
return &RegisterUnprocessableEntity{}
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the register unprocessable entity response
|
||||
func (o *RegisterUnprocessableEntity) WithPayload(payload rest_model_zrok.ErrorMessage) *RegisterUnprocessableEntity {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the register unprocessable entity response
|
||||
func (o *RegisterUnprocessableEntity) SetPayload(payload rest_model_zrok.ErrorMessage) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *RegisterUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(422)
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterInternalServerErrorCode is the HTTP code returned for type RegisterInternalServerError
|
||||
const RegisterInternalServerErrorCode int = 500
|
||||
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
|
||||
"github.com/openziti/zrok/rest_model_zrok"
|
||||
)
|
||||
|
||||
// ResetPasswordOKCode is the HTTP code returned for type ResetPasswordOK
|
||||
@ -61,6 +63,49 @@ func (o *ResetPasswordNotFound) WriteResponse(rw http.ResponseWriter, producer r
|
||||
rw.WriteHeader(404)
|
||||
}
|
||||
|
||||
// ResetPasswordUnprocessableEntityCode is the HTTP code returned for type ResetPasswordUnprocessableEntity
|
||||
const ResetPasswordUnprocessableEntityCode int = 422
|
||||
|
||||
/*
|
||||
ResetPasswordUnprocessableEntity password validation failure
|
||||
|
||||
swagger:response resetPasswordUnprocessableEntity
|
||||
*/
|
||||
type ResetPasswordUnprocessableEntity struct {
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload rest_model_zrok.ErrorMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewResetPasswordUnprocessableEntity creates ResetPasswordUnprocessableEntity with default headers values
|
||||
func NewResetPasswordUnprocessableEntity() *ResetPasswordUnprocessableEntity {
|
||||
|
||||
return &ResetPasswordUnprocessableEntity{}
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the reset password unprocessable entity response
|
||||
func (o *ResetPasswordUnprocessableEntity) WithPayload(payload rest_model_zrok.ErrorMessage) *ResetPasswordUnprocessableEntity {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the reset password unprocessable entity response
|
||||
func (o *ResetPasswordUnprocessableEntity) SetPayload(payload rest_model_zrok.ErrorMessage) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *ResetPasswordUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(422)
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
|
||||
// ResetPasswordInternalServerErrorCode is the HTTP code returned for type ResetPasswordInternalServerError
|
||||
const ResetPasswordInternalServerErrorCode int = 500
|
||||
|
||||
|
@ -72,6 +72,10 @@ paths:
|
||||
$ref: "#/definitions/registerResponse"
|
||||
404:
|
||||
description: request not found
|
||||
422:
|
||||
description: password validation failure
|
||||
schema:
|
||||
$ref: '#/definitions/errorMessage'
|
||||
500:
|
||||
description: internal server error
|
||||
|
||||
@ -90,6 +94,10 @@ paths:
|
||||
description: password reset
|
||||
404:
|
||||
description: request not found
|
||||
422:
|
||||
description: password validation failure
|
||||
schema:
|
||||
$ref: '#/definitions/errorMessage'
|
||||
500:
|
||||
description: internal server error
|
||||
|
||||
@ -678,6 +686,8 @@ definitions:
|
||||
type: boolean
|
||||
inviteTokenContact:
|
||||
type: string
|
||||
passwordRequirements:
|
||||
$ref: "#/definitions/passwordRequirements"
|
||||
|
||||
createFrontendRequest:
|
||||
type: object
|
||||
@ -841,6 +851,20 @@ definitions:
|
||||
items:
|
||||
$ref: "#/definitions/environmentAndResources"
|
||||
|
||||
passwordRequirements:
|
||||
type: object
|
||||
properties:
|
||||
length:
|
||||
type: integer
|
||||
requireCapital:
|
||||
type: boolean
|
||||
requireNumeric:
|
||||
type: boolean
|
||||
requireSpecial:
|
||||
type: boolean
|
||||
validSpecialCharacters:
|
||||
type: string
|
||||
|
||||
principal:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -34,6 +34,7 @@
|
||||
* @property {boolean} invitesOpen
|
||||
* @property {boolean} requiresInviteToken
|
||||
* @property {string} inviteTokenContact
|
||||
* @property {module:types.passwordRequirements} passwordRequirements
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -166,6 +167,17 @@
|
||||
* @property {module:types.environmentAndResources[]} environments
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef passwordRequirements
|
||||
* @memberof module:types
|
||||
*
|
||||
* @property {number} length
|
||||
* @property {boolean} requireCapital
|
||||
* @property {boolean} requireNumeric
|
||||
* @property {boolean} requireSpecial
|
||||
* @property {string} validSpecialCharacters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef principal
|
||||
* @memberof module:types
|
||||
|
99
ui/src/components/password.js
Normal file
99
ui/src/components/password.js
Normal file
@ -0,0 +1,99 @@
|
||||
import React, {useEffect, useState, Fragment} from "react";
|
||||
import {Button, Container, Form, Row} from "react-bootstrap";
|
||||
|
||||
const PasswordForm = (props) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirm, setConfirm] = useState('');
|
||||
|
||||
const passwordMismatchMessage = <h2 className={"errorMessage"}>Entered passwords do not match!</h2>
|
||||
const passwordTooShortMessage = <h2 className={"errorMessage"}>Entered password too short! ({props.passwordLength} characters, minimum)</h2>
|
||||
const passwordRequiresCapitalMessage = <h2 className={"errorMessage"}>Entered password requires a capital letter!</h2>
|
||||
const passwordRequiresNumericMessage = <h2 className={"errorMessage"}>Entered password requires a digit!</h2>
|
||||
const passwordRequiresSpecialMessage = <h2 className={"errorMessage"}>Entered password requires a special character! ({props.passwordValidSpecialCharacters.split("").join(" ")})</h2>
|
||||
|
||||
useEffect(() => {
|
||||
if (confirm === "" && password === "") {
|
||||
return
|
||||
}
|
||||
if (confirm.length < props.passwordLength) {
|
||||
props.setMessage(passwordTooShortMessage)
|
||||
return;
|
||||
}
|
||||
if (props.passwordRequireCapital && !/[A-Z]/.test(confirm)) {
|
||||
props.setMessage(passwordRequiresCapitalMessage)
|
||||
return;
|
||||
}
|
||||
if (props.passwordRequireNumeric && !/\d/.test(confirm)) {
|
||||
props.setMessage(passwordRequiresNumericMessage)
|
||||
return;
|
||||
}
|
||||
if (props.passwordRequireSpecial) {
|
||||
if (!props.passwordValidSpecialCharacters.split("").some(v => confirm.includes(v))) {
|
||||
props.setMessage(passwordRequiresSpecialMessage)
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (confirm != password) {
|
||||
props.setMessage(passwordMismatchMessage)
|
||||
return;
|
||||
}
|
||||
props.setParentPassword(password)
|
||||
}, [password, confirm])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
(props.passwordLength > 0 || props.passwordRequireCapital || props.passwordRequireNumeric || props.passwordRequireSpecial) &&
|
||||
<h2>Password Requirements</h2>
|
||||
}
|
||||
{
|
||||
props.passwordLength > 0 &&
|
||||
<Row>Minimum Length of {props.passwordLength} </Row>
|
||||
}
|
||||
{
|
||||
props.passwordRequireCapital &&
|
||||
<Row>Requires at least 1 Capital Letter</Row>
|
||||
}
|
||||
{
|
||||
props.passwordRequireNumeric &&
|
||||
<Row>Requires at least 1 Digit</Row>
|
||||
}
|
||||
{
|
||||
props.passwordRequireSpecial &&
|
||||
<Fragment>
|
||||
<Row>Requires at least 1 Special Character</Row>
|
||||
<Row>{props.passwordValidSpecialCharacters.split("").join(" ")}</Row>
|
||||
</Fragment>
|
||||
}
|
||||
<Container className={"fullscreen-body"}>
|
||||
<Form.Group controlId={"password"}>
|
||||
<Form.Control
|
||||
type={"password"}
|
||||
placeholder={"Set Password"}
|
||||
onChange={t => { props.setMessage(null); setPassword(t.target.value);}}
|
||||
value={password}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId={"confirm"}>
|
||||
<Form.Control
|
||||
type={"password"}
|
||||
placeholder={"Confirm Password"}
|
||||
onChange={t => { props.setMessage(null); setConfirm(t.target.value);}}
|
||||
value={confirm}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Container>
|
||||
</Fragment>
|
||||
)
|
||||
};
|
||||
|
||||
PasswordForm.defaultProps = {
|
||||
passwordLength: 0,
|
||||
passwordRequireCapital: false,
|
||||
passwordRequireNumeric: false,
|
||||
passwordRequireSpecial: false,
|
||||
passwordValidSpecialCharacters: ""
|
||||
}
|
||||
|
||||
export default PasswordForm;
|
@ -3,26 +3,33 @@ import * as account from "../api/account";
|
||||
import * as metadata from "../api/metadata"
|
||||
import Success from "./Success";
|
||||
import {Button, Container, Form, Row} from "react-bootstrap";
|
||||
import PasswordForm from "../components/password";
|
||||
|
||||
const SetPasswordForm = (props) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirm, setConfirm] = useState('');
|
||||
const [message, setMessage] = useState();
|
||||
const [authToken, setAuthToken] = useState('');
|
||||
const [complete, setComplete] = useState(false);
|
||||
const [tou, setTou] = useState();
|
||||
const [passwordLength, setPasswordLength] = useState(10);
|
||||
const [passwordRequireCapital, setPasswordRequireCapital] = useState(true);
|
||||
const [passwordRequireNumeric, setPasswordRequireNumeric] = useState(true);
|
||||
const [passwordRequireSpecial, setPasswordRequireSpecial] = useState(true);
|
||||
const [passwordValidSpecialCharacters, setPasswordValidSpecialCharacters] = useState("");
|
||||
|
||||
const passwordMismatchMessage = <h2 className={"errorMessage"}>Entered passwords do not match!</h2>
|
||||
const passwordTooShortMessage = <h2 className={"errorMessage"}>Entered password too short! (4 characters, minimum)</h2>
|
||||
const registerFailed = <h2 className={"errorMessage"}>Account creation failed!</h2>
|
||||
|
||||
useEffect(() => {
|
||||
metadata.configuration().then(resp => {
|
||||
console.log(resp)
|
||||
if(!resp.error) {
|
||||
if (resp.data.touLink !== null && resp.data.touLink.trim() !== "") {
|
||||
setTou(resp.data.touLink)
|
||||
}
|
||||
setPasswordLength(resp.data.passwordRequirements.length)
|
||||
setPasswordRequireCapital(resp.data.passwordRequirements.requireCapital)
|
||||
setPasswordRequireNumeric(resp.data.passwordRequirements.requireNumeric)
|
||||
setPasswordRequireSpecial(resp.data.passwordRequirements.requireSpecial)
|
||||
setPasswordValidSpecialCharacters(resp.data.passwordRequirements.validSpecialCharacters)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log("err", err);
|
||||
@ -31,14 +38,7 @@ const SetPasswordForm = (props) => {
|
||||
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
if(confirm.length < 4) {
|
||||
setMessage(passwordTooShortMessage);
|
||||
return;
|
||||
}
|
||||
if(confirm !== password) {
|
||||
setMessage(passwordMismatchMessage);
|
||||
return;
|
||||
}
|
||||
if (password !== undefined && password !== "") {
|
||||
account.register({body: {"token": props.token, "password": password}})
|
||||
.then(resp => {
|
||||
if(!resp.error) {
|
||||
@ -54,6 +54,7 @@ const SetPasswordForm = (props) => {
|
||||
console.log("resp", resp);
|
||||
setMessage(registerFailed);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if(!complete) {
|
||||
@ -72,23 +73,14 @@ const SetPasswordForm = (props) => {
|
||||
<Container className={"fullscreen-form"}>
|
||||
<Row>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group controlId={"password"}>
|
||||
<Form.Control
|
||||
type={"password"}
|
||||
placeholder={"Set Password"}
|
||||
onChange={t => { setMessage(null); setPassword(t.target.value); }}
|
||||
value={password}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId={"confirm"}>
|
||||
<Form.Control
|
||||
type={"password"}
|
||||
placeholder={"Confirm Password"}
|
||||
onChange={t => { setMessage(null); setConfirm(t.target.value); }}
|
||||
value={confirm}
|
||||
/>
|
||||
</Form.Group>
|
||||
<PasswordForm
|
||||
setMessage={setMessage}
|
||||
passwordLength={passwordLength}
|
||||
passwordRequireCapital={passwordRequireCapital}
|
||||
passwordRequireNumeric={passwordRequireNumeric}
|
||||
passwordRequireSpecial={passwordRequireSpecial}
|
||||
passwordValidSpecialCharacters={passwordValidSpecialCharacters}
|
||||
setParentPassword={setPassword}/>
|
||||
<Button variant={"light"} type={"submit"}>Register Account</Button>
|
||||
</Form>
|
||||
</Row>
|
||||
|
@ -1,29 +1,39 @@
|
||||
import {useState} from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import * as account from '../api/account';
|
||||
import * as metadata from "../api/metadata"
|
||||
import {Button, Container, Form, Row} from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import PasswordForm from "../components/password";
|
||||
|
||||
const SetNewPassword = (props) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirm, setConfirm] = useState('');
|
||||
const [message, setMessage] = useState();
|
||||
const [complete, setComplete] = useState(false);
|
||||
|
||||
const passwordMismatchMessage = <h2 className={"errorMessage"}>Entered passwords do not match!</h2>
|
||||
const passwordTooShortMessage = <h2 className={"errorMessage"}>Entered password too short! (4 characters, minimum)</h2>
|
||||
const [passwordLength, setPasswordLength] = useState(10);
|
||||
const [passwordRequireCapital, setPasswordRequireCapital] = useState(true);
|
||||
const [passwordRequireNumeric, setPasswordRequireNumeric] = useState(true);
|
||||
const [passwordRequireSpecial, setPasswordRequireSpecial] = useState(true);
|
||||
const [passwordValidSpecialCharacters, setPasswordValidSpecialCharacters] = useState("");
|
||||
|
||||
const errorMessage = <h2 className={"errorMessage"}>Reset Password Failed!</h2>;
|
||||
|
||||
useEffect(() => {
|
||||
metadata.configuration().then(resp => {
|
||||
if(!resp.error) {
|
||||
setPasswordLength(resp.data.passwordRequirements.length)
|
||||
setPasswordRequireCapital(resp.data.passwordRequirements.requireCapital)
|
||||
setPasswordRequireNumeric(resp.data.passwordRequirements.requireNumeric)
|
||||
setPasswordRequireSpecial(resp.data.passwordRequirements.requireSpecial)
|
||||
setPasswordValidSpecialCharacters(resp.data.passwordRequirements.validSpecialCharacters)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log("err", err);
|
||||
});
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
if(confirm.length < 4) {
|
||||
setMessage(passwordTooShortMessage);
|
||||
return;
|
||||
}
|
||||
if(confirm !== password) {
|
||||
setMessage(passwordMismatchMessage);
|
||||
return;
|
||||
}
|
||||
if (password !== undefined && password !== "") {
|
||||
account.resetPassword({body: {"token": props.token, "password": password}})
|
||||
.then(resp => {
|
||||
if(!resp.error) {
|
||||
@ -37,6 +47,7 @@ const SetNewPassword = (props) => {
|
||||
setMessage(errorMessage);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(!complete) {
|
||||
return (
|
||||
@ -51,23 +62,14 @@ const SetNewPassword = (props) => {
|
||||
<Container className={"fullscreen-form"}>
|
||||
<Row>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group controlId={"password"}>
|
||||
<Form.Control
|
||||
type={"password"}
|
||||
placeholder={"Set Password"}
|
||||
onChange={t => { setMessage(null); setPassword(t.target.value); }}
|
||||
value={password}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId={"confirm"}>
|
||||
<Form.Control
|
||||
type={"password"}
|
||||
placeholder={"Confirm Password"}
|
||||
onChange={t => { setMessage(null); setConfirm(t.target.value); }}
|
||||
value={confirm}
|
||||
/>
|
||||
</Form.Group>
|
||||
<PasswordForm
|
||||
setMessage={setMessage}
|
||||
passwordLength={passwordLength}
|
||||
passwordRequireCapital={passwordRequireCapital}
|
||||
passwordRequireNumeric={passwordRequireNumeric}
|
||||
passwordRequireSpecial={passwordRequireSpecial}
|
||||
passwordValidSpecialCharacters={passwordValidSpecialCharacters}
|
||||
setParentPassword={setPassword}/>
|
||||
|
||||
<Button variant={"light"} type={"submit"}>Reset Password</Button>
|
||||
</Form>
|
||||
|
Loading…
Reference in New Issue
Block a user