diff --git a/controller/config.go b/controller/config.go index 5a3c8c40..012827b6 100644 --- a/controller/config.go +++ b/controller/config.go @@ -69,6 +69,7 @@ type InfluxConfig struct { } type MaintenanceConfig struct { + Account *AccountMaintenanceConfig Registration *RegistrationMaintenanceConfig } @@ -78,6 +79,12 @@ type RegistrationMaintenanceConfig struct { BatchLimit int } +type AccountMaintenanceConfig struct { + ExpirationTimeout time.Duration + CheckFrequency time.Duration + BatchLimit int +} + const Unlimited = -1 type LimitsConfig struct { @@ -95,6 +102,11 @@ func DefaultConfig() *Config { ServiceName: "metrics", }, Maintenance: &MaintenanceConfig{ + Account: &AccountMaintenanceConfig{ + ExpirationTimeout: time.Minute * 15, + CheckFrequency: time.Minute * 15, + BatchLimit: 500, + }, Registration: &RegistrationMaintenanceConfig{ ExpirationTimeout: time.Hour * 24, CheckFrequency: time.Hour, diff --git a/controller/controller.go b/controller/controller.go index 7fdb9778..970035dc 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -80,8 +80,13 @@ func Run(inCfg *Config) error { cancel() }() - if cfg.Maintenance != nil && cfg.Maintenance.Registration != nil { - go newMaintenanceAgent(ctx, cfg.Maintenance).run() + if cfg.Maintenance != nil { + if cfg.Maintenance.Registration != nil { + go newRegistrationMaintenanceAgent(ctx, cfg.Maintenance.Registration).run() + } + if cfg.Maintenance.Account != nil { + go newAccountMaintenanceAgent(ctx, cfg.Maintenance.Account).run() + } } server := rest_server_zrok.NewServer(api) diff --git a/controller/emailUi/forgotPassword.gohtml b/controller/emailUi/forgotPassword.gohtml index c9de434c..6ad373bc 100644 --- a/controller/emailUi/forgotPassword.gohtml +++ b/controller/emailUi/forgotPassword.gohtml @@ -1,11 +1,192 @@ - - zrok forgot password - + + + Welcome to zrok! + + + + + + + +

We see you requested a forgot password request, {{ .EmailAddress }}!

-

Please click this link to change your zrok account password.

+

Please click this to change your zrok account password.

+
Reset Passwrod
\ No newline at end of file diff --git a/controller/maintenance.go b/controller/maintenance.go index d81233fb..21df582f 100644 --- a/controller/maintenance.go +++ b/controller/maintenance.go @@ -10,23 +10,23 @@ import ( "github.com/sirupsen/logrus" ) -type maintenanceAgent struct { - *MaintenanceConfig +type maintenanceRegistrationAgent struct { + *RegistrationMaintenanceConfig ctx context.Context } -func newMaintenanceAgent(ctx context.Context, cfg *MaintenanceConfig) *maintenanceAgent { - return &maintenanceAgent{ - MaintenanceConfig: cfg, - ctx: ctx, +func newRegistrationMaintenanceAgent(ctx context.Context, cfg *RegistrationMaintenanceConfig) *maintenanceRegistrationAgent { + return &maintenanceRegistrationAgent{ + RegistrationMaintenanceConfig: cfg, + ctx: ctx, } } -func (ma *maintenanceAgent) run() { - logrus.Info("starting") - defer logrus.Info("stopping") +func (ma *maintenanceRegistrationAgent) run() { + logrus.Infof("starting maintenance registration agent") + defer logrus.Info("stopping maintenance registration agent") - ticker := time.NewTicker(ma.Registration.CheckFrequency) + ticker := time.NewTicker(ma.CheckFrequency) for { select { case <-ma.ctx.Done(): @@ -44,15 +44,15 @@ func (ma *maintenanceAgent) run() { } } -func (ma *maintenanceAgent) deleteExpiredAccountRequests() error { +func (ma *maintenanceRegistrationAgent) deleteExpiredAccountRequests() error { tx, err := str.Begin() if err != nil { return err } defer func() { _ = tx.Rollback() }() - timeout := time.Now().UTC().Add(-ma.Registration.ExpirationTimeout) - accountRequests, err := str.FindExpiredAccountRequests(timeout, ma.Registration.BatchLimit, tx) + timeout := time.Now().UTC().Add(-ma.ExpirationTimeout) + accountRequests, err := str.FindExpiredAccountRequests(timeout, ma.BatchLimit, tx) if err != nil { return errors.Wrapf(err, "error finding expire account requests before %v", timeout) } @@ -76,3 +76,68 @@ func (ma *maintenanceAgent) deleteExpiredAccountRequests() error { return nil } + +type maintenanceAccountAgent struct { + *AccountMaintenanceConfig + ctx context.Context +} + +func newAccountMaintenanceAgent(ctx context.Context, cfg *AccountMaintenanceConfig) *maintenanceAccountAgent { + return &maintenanceAccountAgent{ + AccountMaintenanceConfig: cfg, + ctx: ctx, + } +} + +func (ma *maintenanceAccountAgent) run() { + logrus.Infof("starting maintenance account agent") + defer logrus.Info("stopping maintenance account agent") + + ticker := time.NewTicker(ma.CheckFrequency) + for { + select { + case <-ma.ctx.Done(): + { + ticker.Stop() + return + } + case <-ticker.C: + { + if err := ma.deleteExpiredForgetPasswordRequests(); err != nil { + logrus.Error(err) + } + } + } + } +} +func (ma *maintenanceAccountAgent) deleteExpiredForgetPasswordRequests() error { + tx, err := str.Begin() + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + timeout := time.Now().UTC().Add(-ma.ExpirationTimeout) + passwordResetRequests, err := str.FindExpiredPasswordResetRequests(timeout, ma.BatchLimit, tx) + if err != nil { + return errors.Wrapf(err, "error finding expired password reset requests before %v", timeout) + } + if len(passwordResetRequests) > 0 { + logrus.Infof("found %d expired password reset requests to remove", len(passwordResetRequests)) + acctStrings := make([]string, len(passwordResetRequests)) + ids := make([]int, len(passwordResetRequests)) + for i, acct := range passwordResetRequests { + ids[i] = acct.Id + acctStrings[i] = fmt.Sprintf("{id:%d}", acct.Id) + } + + logrus.Infof("deleting expired password reset requests: %v", strings.Join(acctStrings, ",")) + if err := str.DeleteMultiplePasswordResetRequests(ids, tx); err != nil { + return errors.Wrapf(err, "error deleting expired password reset requests before %v", timeout) + } + if err := tx.Commit(); err != nil { + return errors.Wrapf(err, "error committing expired password reset requests deletion") + } + } + return nil +} diff --git a/controller/store/password_reset_request.go b/controller/store/password_reset_request.go index b4dfb44a..2677f5ac 100644 --- a/controller/store/password_reset_request.go +++ b/controller/store/password_reset_request.go @@ -1,6 +1,10 @@ package store import ( + "fmt" + "strings" + "time" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" ) @@ -31,6 +35,33 @@ func (self *Store) FindPasswordResetRequestWithToken(token string, tx *sqlx.Tx) return prr, nil } +func (self *Store) FindExpiredPasswordResetRequests(before time.Time, limit int, tx *sqlx.Tx) ([]*PasswordResetRequest, error) { + var sql string + switch self.cfg.Type { + case "postgres": + sql = "select * from password_reset_requests where created_at < $1 limit %d for update" + + case "sqlite3": + sql = "select * from password_reset_requests where created_at < $1 limit %d" + default: + return nil, errors.Errorf("unknown database type '%v'", self.cfg.Type) + } + + rows, err := tx.Queryx(fmt.Sprintf(sql, limit), before) + if err != nil { + return nil, errors.Wrap(err, "error selecting expired password_reset_requests") + } + var prrs []*PasswordResetRequest + for rows.Next() { + prr := &PasswordResetRequest{} + if err := rows.StructScan(prr); err != nil { + return nil, errors.Wrap(err, "error scanning password_reset_request") + } + prrs = append(prrs, prr) + } + return prrs, nil +} + func (self *Store) DeletePasswordResetRequest(id int, tx *sqlx.Tx) error { stmt, err := tx.Prepare("delete from password_reset_requests where id = $1") if err != nil { @@ -42,3 +73,27 @@ func (self *Store) DeletePasswordResetRequest(id int, tx *sqlx.Tx) error { } return nil } + +func (self *Store) DeleteMultiplePasswordResetRequests(ids []int, tx *sqlx.Tx) error { + if len(ids) == 0 { + return nil + } + + anyIds := make([]any, len(ids)) + indexes := make([]string, len(ids)) + + for i, id := range ids { + anyIds[i] = id + indexes[i] = fmt.Sprintf("$%d", i+1) + } + + stmt, err := tx.Prepare(fmt.Sprintf("delete from password_reset_requests where id in (%s)", strings.Join(indexes, ","))) + if err != nil { + return errors.Wrap(err, "error preparing password_reset_requests delete multiple statement") + } + _, err = stmt.Exec(anyIds...) + if err != nil { + return errors.Wrap(err, "error executing password_reset_requests delete multiple statement") + } + return nil +} diff --git a/controller/store/sql/postgresql/006_v0_3_0_password_reset_requests.sql b/controller/store/sql/postgresql/006_v0_3_0_password_reset_requests.sql index 236741e1..b6c40061 100644 --- a/controller/store/sql/postgresql/006_v0_3_0_password_reset_requests.sql +++ b/controller/store/sql/postgresql/006_v0_3_0_password_reset_requests.sql @@ -1,4 +1,4 @@ --- +migrate up +-- +migrate Up -- -- password_reset_requests diff --git a/ui/src/console/forgotPassword/ResetPassword.js b/ui/src/console/forgotPassword/ResetPassword.js index d99b3b0e..dbe69b9b 100644 --- a/ui/src/console/forgotPassword/ResetPassword.js +++ b/ui/src/console/forgotPassword/ResetPassword.js @@ -1,7 +1,7 @@ import {useState} from "react"; import * as account from '../../api/account'; import {Button, Container, Form, Row} from "react-bootstrap"; -import { Navigate } from "react-router-dom"; +import { Link } from "react-router-dom"; const ResetPassword = (props) => { const [password, setPassword] = useState(''); @@ -30,11 +30,10 @@ const ResetPassword = (props) => { setMessage(undefined); setComplete(true); } else { - setMessage(errorMessage) + setMessage(errorMessage); } }) .catch(resp => { - console.log("reset password failed", resp); setMessage(errorMessage); }) } @@ -83,7 +82,24 @@ const ResetPassword = (props) => { } return ( - + + + ziggy + + +

Password Reset

+
+ + Password reset successful! You can now return to the login page and login. + + +
+ + Login + +
+
+
) } diff --git a/ui/src/console/forgotPassword/SendRequest.js b/ui/src/console/forgotPassword/SendRequest.js index b6e44d16..cd4d48f7 100644 --- a/ui/src/console/forgotPassword/SendRequest.js +++ b/ui/src/console/forgotPassword/SendRequest.js @@ -1,15 +1,12 @@ import { useState } from "react"; import * as account from '../../api/account'; import { Button, Container, Form, Row } from "react-bootstrap"; +import { Link } from "react-router-dom"; const SendRequest = (props) => { const [email, setEmail] = useState(''); - const [message, setMessage] = useState(); const [complete, setComplete] = useState(false); - - const errorMessage =

Forgot Password Failed!

; - const handleSubmit = async e => { e.preventDefault(); console.log(email); @@ -17,16 +14,13 @@ const SendRequest = (props) => { account.forgotPassword({ body: { "email": email } }) .then(resp => { if (!resp.error) { - console.log("Make landing page to expect and email or something similar") setComplete(true) } else { - console.log('forgot password failed') - setMessage(errorMessage); + setComplete(true) } }) .catch((resp) => { - console.log('forgot password failed', resp) - setMessage(errorMessage) + setComplete(true) }) }; @@ -50,7 +44,7 @@ const SendRequest = (props) => { { setMessage(null); setEmail(t.target.value); }} + onChange={t => { setEmail(t.target.value); }} value={email} /> @@ -58,16 +52,30 @@ const SendRequest = (props) => { - - {message} - ) } return ( -
Make landing page to expect an email or something similar
+ + + ziggy + + +

Reset Password

+
+ + We will get back to you shortly with a link to reset your password! + + +
+ + Login + +
+
+
) } diff --git a/ui/src/console/login/Login.js b/ui/src/console/login/Login.js index 385033f4..329fbdd3 100644 --- a/ui/src/console/login/Login.js +++ b/ui/src/console/login/Login.js @@ -66,13 +66,13 @@ const Login = (props) => { /> -
+ + +
Forgot Password?
- -