feat: add support for changing password

This commit is contained in:
hugmouse 2025-08-15 12:41:48 +02:00
parent 733287e36b
commit 2607bbee82
6 changed files with 363 additions and 37 deletions

View file

@ -5,14 +5,24 @@
Runs an HTTP server that serves a password reset form. Runs an HTTP server that serves a password reset form.
It should be installed on the same server where Maddy is running. It should be installed on the same server where Maddy is running.
Essentially, this is a simple web-based wrapper for Maddy's CLI
that you can customize to your specific case.
## How It Works ## How It Works
It runs the `maddy creds password -p` command to change a user's password. The service provides two ways to change passwords:
1. **Password Reset**: Sends a reset link via email
2. **Password Change**: Allows users to change their password using their current password
Both methods use the `maddy creds password -p` command to change passwords. Password verification retrieves the bcrypt hash from Maddy's database and verifies it directly.
### Use Cases ### Use Cases
- You are currently logged into your mailbox and want to reset the password. - **Password Reset**: User forgot their password and needs to reset it via email
- For example, you registered a user, and the user wants to change their password. - This is more for the case when you have an active email session, but want to change your current password via special reset link
- **Password Change**: User knows their current password and wants to change it directly
- For example, you registered a user, and the user wants to change their password
## Installation ## Installation
@ -35,8 +45,34 @@ Make sure to configure it first! The first compilation will take a moderate amou
By default, the web server starts on `:1323`. Make sure you hide it behind a reverse proxy. By default, the web server starts on `:1323`. Make sure you hide it behind a reverse proxy.
You will probably need to edit the `reset.gohtml` template to suit your needs. ### Available translations
For now, it contains a reset page in Russian for my hobby mail service.
Templates are available in Russian and English languages.
To use English, replace references to `.gohtml` with `.en.gohtml`.
### Available Routes
Pages do not rely on JavaScript, so you can trigger them via Curl or some other client.
- **Password Reset**:
- `GET /reset` - Display password reset form
- `POST /reset` - Submit email for reset link
- `GET /reset/:token` - Display new password form (from email link)
- `POST /reset/:token` - Submit new password
- **Password Change**:
- `GET /change` - Display password change form
- `POST /change` - Submit current and new passwords
### Templates
You will probably need to edit the templates to suit your needs:
- **Password Reset**: `reset.gohtml` (Russian), `reset.en.gohtml` (English)
- **Password Change**: `change.gohtml` (Russian), `change.en.gohtml` (English)
By default, the reset templates are configured for Russian, but English templates are also available.
The only way to change the configuration is to modify the constants in the `main.go` file: The only way to change the configuration is to modify the constants in the `main.go` file:

12
go.mod
View file

@ -3,26 +3,29 @@ module github.com/hugmouse/maddy-password-reset
go 1.20 go 1.20
require ( require (
github.com/akyoto/cache v1.0.6 // indirect github.com/akyoto/cache v1.0.6
github.com/labstack/echo/v4 v4.10.0
golang.org/x/crypto v0.2.0
modernc.org/sqlite v1.20.4
)
require (
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/labstack/echo/v4 v4.10.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.2.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.4.0 // indirect golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.5.0 // indirect
golang.org/x/time v0.2.0 // indirect golang.org/x/time v0.2.0 // indirect
golang.org/x/tools v0.1.12 // indirect golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect
@ -30,7 +33,6 @@ require (
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect modernc.org/memory v1.4.0 // indirect
modernc.org/opt v0.1.3 // indirect modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.20.4 // indirect
modernc.org/strutil v1.1.3 // indirect modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect modernc.org/token v1.0.1 // indirect
) )

38
go.sum
View file

@ -1,11 +1,14 @@
github.com/akyoto/cache v1.0.6 h1:5XGVVYoi2i+DZLLPuVIXtsNIJ/qaAM16XT0LaBaXd2k= github.com/akyoto/cache v1.0.6 h1:5XGVVYoi2i+DZLLPuVIXtsNIJ/qaAM16XT0LaBaXd2k=
github.com/akyoto/cache v1.0.6/go.mod h1:WfxTRqKhfgAG71Xh6E3WLpjhBtZI37O53G4h5s+3iM4= github.com/akyoto/cache v1.0.6/go.mod h1:WfxTRqKhfgAG71Xh6E3WLpjhBtZI37O53G4h5s+3iM4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@ -20,66 +23,49 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
@ -92,5 +78,7 @@ modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=

108
main.go
View file

@ -24,6 +24,7 @@ import (
"github.com/hugmouse/maddy-password-reset/templates" "github.com/hugmouse/maddy-password-reset/templates"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"golang.org/x/crypto/bcrypt"
"html/template" "html/template"
"io" "io"
"log" "log"
@ -88,7 +89,7 @@ const (
HTTPServerPort = 1323 HTTPServerPort = 1323
// DebugBypassMailSending - if true, skips SMTP checks and sending, logs reset link instead. // DebugBypassMailSending - if true, skips SMTP checks and sending, logs reset link instead.
DebugBypassMailSending = false DebugBypassMailSending = true
) )
// --------------------------------------------------------------- // ---------------------------------------------------------------
@ -168,6 +169,18 @@ const (
logMsgMaddyExecFailedFmt = "[Maddy] Failed to execute Maddy password reset command for %s: %v. Output: %s" logMsgMaddyExecFailedFmt = "[Maddy] Failed to execute Maddy password reset command for %s: %v. Output: %s"
logMsgMaddyExecSuccessFmt = "[Maddy] Successfully reset password for user %s. Output: %s" logMsgMaddyExecSuccessFmt = "[Maddy] Successfully reset password for user %s. Output: %s"
// Password Change Process
logMsgChangeInvalidEmailFmt = "[Handler POST /change] Invalid email address provided '%s': %v"
logMsgChangeEmptyCurrentPasswordFmt = "[Handler POST /change] Empty current password provided for user %s"
logMsgChangeEmptyNewPasswordFmt = "[Handler POST /change] Empty new password provided for user %s"
logMsgChangeUserNotFoundFmt = "[Handler POST /change] User email %q not found in Maddy database"
logMsgChangeVerifyAttemptFmt = "[Change] Attempting to verify current password for user %s"
logMsgChangeVerifyFailedFmt = "[Change] Failed to verify current password for user %s: %v. Output: %s"
logMsgChangeVerifySuccessFmt = "[Change] Current password verified for user %s"
logMsgChangePasswordAttemptFmt = "[Change] Attempting to change password for user %s"
logMsgChangePasswordFailedFmt = "[Change] Failed to change password for user %s: %v. Output: %s"
logMsgChangePasswordSuccessFmt = "[Change] Successfully changed password for user %s"
// Other // Other
logMsgRandNumGenFailedCriticalFmt = "CRITICAL: Failed to generate random number: %v" logMsgRandNumGenFailedCriticalFmt = "CRITICAL: Failed to generate random number: %v"
) )
@ -211,6 +224,30 @@ func isValidEmailAddress(email string) error {
return nil return nil
} }
func verifyCurrentPassword(db *sql.DB, email, currentPassword string) error {
log.Printf(logMsgChangeVerifyAttemptFmt, email)
var storedPasswordHash string
dbErr := db.QueryRow("SELECT value FROM passwords WHERE key = ?", email).Scan(&storedPasswordHash)
if dbErr != nil {
if errors.Is(dbErr, sql.ErrNoRows) {
log.Printf(logMsgChangeUserNotFoundFmt, email)
return fmt.Errorf("user not found")
}
log.Printf(logMsgDbQueryErrorFmt, email, dbErr)
return fmt.Errorf("database error")
}
if err := bcrypt.CompareHashAndPassword([]byte(storedPasswordHash), []byte(currentPassword)); err != nil {
log.Printf(logMsgChangeVerifyFailedFmt, email, err, "bcrypt comparison failed")
return fmt.Errorf("current password is incorrect")
}
log.Printf(logMsgChangeVerifySuccessFmt, email)
return nil
}
func validateConfiguration() { func validateConfiguration() {
if MaddyPath == "" { if MaddyPath == "" {
log.Fatalln(logMsgConfigMaddyPathEmpty) log.Fatalln(logMsgConfigMaddyPathEmpty)
@ -469,8 +506,75 @@ func main() {
}) })
}) })
// Password change routes
e.GET("/change", func(c echo.Context) error {
return c.Render(http.StatusOK, "change.gohtml", nil)
})
e.POST("/change", func(c echo.Context) error {
email := c.FormValue("email")
currentPassword := c.FormValue("current_password")
newPassword := c.FormValue("new_password")
if err := isValidEmailAddress(email); err != nil {
log.Printf(logMsgChangeInvalidEmailFmt, email, err)
return c.Render(http.StatusBadRequest, "change.gohtml", map[string]any{
"Error": "Invalid email address format provided.",
})
}
if currentPassword == "" {
log.Printf(logMsgChangeEmptyCurrentPasswordFmt, email)
return c.Render(http.StatusBadRequest, "change.gohtml", map[string]any{
"Error": "Current password is required.",
})
}
if newPassword == "" {
log.Printf(logMsgChangeEmptyNewPasswordFmt, email)
return c.Render(http.StatusBadRequest, "change.gohtml", map[string]any{
"Error": "New password is required.",
})
}
var dummy int
dbErr := db.QueryRow("SELECT 1 FROM passwords WHERE key = ?", email).Scan(&dummy)
if dbErr != nil {
if errors.Is(dbErr, sql.ErrNoRows) {
log.Printf(logMsgChangeUserNotFoundFmt, email)
} else {
log.Printf(logMsgDbQueryErrorFmt, email, dbErr)
}
return c.Render(http.StatusBadRequest, "change.gohtml", map[string]any{
"Error": "Invalid email address or current password.",
})
}
if err := verifyCurrentPassword(db, email, currentPassword); err != nil {
return c.Render(http.StatusBadRequest, "change.gohtml", map[string]any{
"Error": "Invalid email address or current password.",
})
}
log.Printf(logMsgChangePasswordAttemptFmt, email)
maddyExecCommand := exec.Command("maddy", "creds", "password", "-p", newPassword, email)
output, execErr := maddyExecCommand.CombinedOutput()
if execErr != nil {
log.Printf(logMsgChangePasswordFailedFmt, email, execErr, string(output))
return c.Render(http.StatusInternalServerError, "change.gohtml", map[string]any{
"Error": "Failed to change password due to a server error.",
})
}
log.Printf(logMsgChangePasswordSuccessFmt, email)
return c.Render(http.StatusOK, "change.gohtml", map[string]any{
"Success": "Password successfully changed! You can now log in with your new password.",
})
})
serverAddr := ":" + strconv.Itoa(HTTPServerPort) serverAddr := ":" + strconv.Itoa(HTTPServerPort)
log.Printf(logMsgEchoStartFmt, serverAddr) // Use Printf for format string log.Printf(logMsgEchoStartFmt, serverAddr)
if err := e.Start(serverAddr); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := e.Start(serverAddr); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf(logMsgEchoStartFailedFmt, err) log.Fatalf(logMsgEchoStartFailedFmt, err)
} }

View file

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Change Password</title>
<style>
body {
padding: 1rem;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
input {
margin-top: 1rem;
padding: 1rem;
background: transparent;
border: 1px solid black;
color: black;
width: 100%;
max-width: 300px;
}
label {
display: block;
margin-top: 1rem;
}
button {
margin-top: 1rem;
padding: 1rem 2rem;
background: transparent;
border: 1px solid black;
color: black;
cursor: pointer;
}
button:hover {
background: black;
color: white;
}
.success {
color: green;
font-weight: bold;
}
.error {
color: red;
font-weight: bold;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #222;
color: white;
}
input, button {
border: 1px solid #ffffff;
color: #ffffff;
}
button:hover {
background: white;
color: black;
}
}
</style>
</head>
<body>
<h1>Change Password</h1>
{{ if .Error }}
<p class="error">{{ .Error }}</p>
{{ end }}
{{ if .Success }}
<p class="success">{{ .Success }}</p>
{{ else }}
<p>Enter your email, current password, and new password to change your password.</p>
<form action="/change" method="post">
<label for="email">Email address</label>
<input type="email" name="email" id="email" placeholder="Enter your email address" required>
<label for="current_password">Current password</label>
<input type="password" name="current_password" id="current_password" placeholder="Enter your current password" required>
<label for="new_password">New password</label>
<input type="password" name="new_password" id="new_password" placeholder="Enter your new password" required>
<button type="submit">Change Password</button>
</form>
{{ end }}
<p><a href="/reset">Forgot your password? Use password reset</a></p>
</body>
</html>

98
templates/change.gohtml Normal file
View file

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Смена пароля</title>
<style>
body {
padding: 1rem;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
input {
margin-top: 1rem;
padding: 1rem;
background: transparent;
border: 1px solid black;
color: black;
width: 100%;
max-width: 300px;
}
label {
display: block;
margin-top: 1rem;
}
button {
margin-top: 1rem;
padding: 1rem 2rem;
background: transparent;
border: 1px solid black;
color: black;
cursor: pointer;
}
button:hover {
background: black;
color: white;
}
.success {
color: green;
font-weight: bold;
}
.error {
color: red;
font-weight: bold;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #222;
color: white;
}
input, button {
border: 1px solid #ffffff;
color: #ffffff;
}
button:hover {
background: white;
color: black;
}
}
</style>
</head>
<body>
<h1>Смена пароля</h1>
{{ if .Error }}
<p class="error">{{ .Error }}</p>
{{ end }}
{{ if .Success }}
<p class="success">{{ .Success }}</p>
{{ else }}
<p>Введите ваш email, текущий пароль и новый пароль для смены.</p>
<form action="/change" method="post">
<label for="email">Email адрес</label>
<input type="email" name="email" id="email" placeholder="Введите свой email адрес" required>
<label for="current_password">Текущий пароль</label>
<input type="password" name="current_password" id="current_password" placeholder="Введите текущий пароль" required>
<label for="new_password">Новый пароль</label>
<input type="password" name="new_password" id="new_password" placeholder="Введите новый пароль" required>
<button type="submit">Сменить пароль</button>
</form>
{{ end }}
<p><a href="/reset">Забыли пароль? Воспользуйтесь сбросом пароля</a></p>
</body>
</html>