diff --git a/README.md b/README.md index ba9811c..bd0a564 100644 --- a/README.md +++ b/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: diff --git a/go.mod b/go.mod index a807893..9b896d9 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 18cf34a..e055b53 100644 --- a/go.sum +++ b/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= diff --git a/main.go b/main.go index a275b72..f9eeb8b 100644 --- a/main.go +++ b/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) } diff --git a/templates/change.en.gohtml b/templates/change.en.gohtml new file mode 100644 index 0000000..3aa79f2 --- /dev/null +++ b/templates/change.en.gohtml @@ -0,0 +1,98 @@ + + + + + + + Change Password + + + +

Change Password

+ +{{ if .Error }} +

{{ .Error }}

+{{ end }} + +{{ if .Success }} +

{{ .Success }}

+{{ else }} +

Enter your email, current password, and new password to change your password.

+
+ + + + + + + + + + +
+{{ end }} + +

Forgot your password? Use password reset

+ + \ No newline at end of file diff --git a/templates/change.gohtml b/templates/change.gohtml new file mode 100644 index 0000000..5550f84 --- /dev/null +++ b/templates/change.gohtml @@ -0,0 +1,98 @@ + + + + + + + Смена пароля + + + +

Смена пароля

+ +{{ if .Error }} +

{{ .Error }}

+{{ end }} + +{{ if .Success }} +

{{ .Success }}

+{{ else }} +

Введите ваш email, текущий пароль и новый пароль для смены.

+
+ + + + + + + + + + +
+{{ end }} + +

Забыли пароль? Воспользуйтесь сбросом пароля

+ + \ No newline at end of file