mirror of
https://github.com/openziti/zrok.git
synced 2025-02-21 20:51:06 +01:00
added maintenance cleanup for expired password reset requests
This commit is contained in:
parent
a07c4a519c
commit
2bbf404bae
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -1,11 +1,192 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>zrok forgot password</title>
|
||||
</head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>Welcome to zrok!</title>
|
||||
<meta name="description" content="Please click to create your zrok account.">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Russo+One&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 25;
|
||||
font-family: 'JetBrains Mono', Consolas, 'Courier New', monospace;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #ffffff;
|
||||
background-color: #3b2693;
|
||||
|
||||
}
|
||||
|
||||
tt,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: 'JetBrains Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: 'Russo One', sans-serif;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #00d7e4;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #00d7e4;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active {
|
||||
color: #ff0100;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
line-height: 1.3em;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: 'JetBrains Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.3em;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.17em;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
p code,
|
||||
li code {
|
||||
font-size: .875em;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
display: inline-block;
|
||||
font-size: 5em;
|
||||
line-height: 48px;
|
||||
color: #ffffff;
|
||||
margin: 0 .25em 0 0;
|
||||
}
|
||||
|
||||
.claim {
|
||||
font-size: 2em;
|
||||
margin: 0.5em 0 1em 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 62em;
|
||||
margin: 2em auto;
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cta {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin: .25em;
|
||||
padding: 10px 16px;
|
||||
font-size: 1.15em;
|
||||
line-height: 1.33;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #FFF;
|
||||
background-color: #ff0100;
|
||||
border-color: #ff0100;
|
||||
}
|
||||
|
||||
a.btn-primary:link,
|
||||
a.btn-primary:visited {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
a.btn-primary:hover,
|
||||
a.btn-primary:active {
|
||||
background-color: #cf0100;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #ddd;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
a.btn-secondary:link,
|
||||
a.btn-secondary:visited {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
a.btn-secondary:hover,
|
||||
a.btn-secondary:hover {
|
||||
background-color: #ccc;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.about {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.about td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.about td:first-child {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
img {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 320px) {
|
||||
body {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>We see you requested a forgot password request, {{ .EmailAddress }}!</h1>
|
||||
<p>Please click this <a href="{{ .ForgotPasswordUrl }}">link</a> to change your <code>zrok</code> account password.</p>
|
||||
<p>Please click this to change your <code>zrok</code> account password.</p>
|
||||
<div><a class="btn btn-primary" href="{{ .ForgotPasswordUrl }}">Reset Passwrod</a></div>
|
||||
</body>
|
||||
</html>
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
-- +migrate up
|
||||
-- +migrate Up
|
||||
|
||||
--
|
||||
-- password_reset_requests
|
||||
|
@ -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 (
|
||||
<Navigate to="/" />
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<img alt="ziggy" src={"/ziggy.svg"} width={200}/>
|
||||
</Row>
|
||||
<Row>
|
||||
<h1>Password Reset</h1>
|
||||
</Row>
|
||||
<Row>
|
||||
Password reset successful! You can now return to the login page and login.
|
||||
</Row>
|
||||
<Row>
|
||||
<div>
|
||||
<Link to="/" className="">
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 = <h2 className={"errorMessage"}>Forgot Password Failed!</h2>;
|
||||
|
||||
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) => {
|
||||
<Form.Control
|
||||
type={"email"}
|
||||
placeholder={"Email Address"}
|
||||
onChange={t => { setMessage(null); setEmail(t.target.value); }}
|
||||
onChange={t => { setEmail(t.target.value); }}
|
||||
value={email}
|
||||
/>
|
||||
</Form.Group>
|
||||
@ -58,16 +52,30 @@ const SendRequest = (props) => {
|
||||
<Button variant={"light"} type={"submit"}>Forgot Password</Button>
|
||||
</Form>
|
||||
</Row>
|
||||
<Row>
|
||||
{message}
|
||||
</Row>
|
||||
</Container>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>Make landing page to expect an email or something similar</div>
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<img alt="ziggy" src={"/ziggy.svg"} width={200}/>
|
||||
</Row>
|
||||
<Row>
|
||||
<h1>Reset Password</h1>
|
||||
</Row>
|
||||
<Row>
|
||||
We will get back to you shortly with a link to reset your password!
|
||||
</Row>
|
||||
<Row>
|
||||
<div>
|
||||
<Link to="/" className="">
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -66,13 +66,13 @@ const Login = (props) => {
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<div className="text-right">
|
||||
<Button variant={"light"} type={"submit"}>Log In</Button>
|
||||
|
||||
<div>
|
||||
<Link to="/forgotpassword" className="">
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Button variant={"light"} type={"submit"}>Log In</Button>
|
||||
</Form>
|
||||
</Row>
|
||||
<Row>
|
||||
|
Loading…
Reference in New Issue
Block a user