mirror of
https://github.com/hugmouse/maddy-password-reset.git
synced 2025-08-25 12:26:11 +00:00
feat: add support for changing password
This commit is contained in:
parent
733287e36b
commit
2607bbee82
6 changed files with 363 additions and 37 deletions
46
README.md
46
README.md
|
@ -5,14 +5,24 @@
|
|||
Runs an HTTP server that serves a password reset form.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
- You are currently logged into your mailbox and want to reset the password.
|
||||
- For example, you registered a user, and the user wants to change their password.
|
||||
- **Password Reset**: User forgot their password and needs to reset it via email
|
||||
- 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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
You will probably need to edit the `reset.gohtml` template to suit your needs.
|
||||
For now, it contains a reset page in Russian for my hobby mail service.
|
||||
### Available translations
|
||||
|
||||
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:
|
||||
|
||||
|
|
12
go.mod
12
go.mod
|
@ -3,26 +3,29 @@ module github.com/hugmouse/maddy-password-reset
|
|||
go 1.20
|
||||
|
||||
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/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/google/uuid v1.3.0 // 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/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // 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/net v0.4.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/time v0.2.0 // 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
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
|
@ -30,7 +33,6 @@ require (
|
|||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.4.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sqlite v1.20.4 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
|
|
38
go.sum
38
go.sum
|
@ -1,11 +1,14 @@
|
|||
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/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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
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/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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
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/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/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.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
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/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
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/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-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-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
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/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/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
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/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
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/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/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
|
||||
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/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
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/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
|
||||
|
|
108
main.go
108
main.go
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/hugmouse/maddy-password-reset/templates"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -88,7 +89,7 @@ const (
|
|||
HTTPServerPort = 1323
|
||||
|
||||
// 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"
|
||||
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
|
||||
logMsgRandNumGenFailedCriticalFmt = "CRITICAL: Failed to generate random number: %v"
|
||||
)
|
||||
|
@ -211,6 +224,30 @@ func isValidEmailAddress(email string) error {
|
|||
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() {
|
||||
if MaddyPath == "" {
|
||||
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)
|
||||
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) {
|
||||
log.Fatalf(logMsgEchoStartFailedFmt, err)
|
||||
}
|
||||
|
|
98
templates/change.en.gohtml
Normal file
98
templates/change.en.gohtml
Normal 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
98
templates/change.gohtml
Normal 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>
|
Loading…
Add table
Reference in a new issue