Skip to content

Commit 3e93d51

Browse files
feat: add admin token reset password
1 parent 3282d9f commit 3e93d51

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-3
lines changed

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Clone the repo run `go build` and then `./hackatime -config config.yml`. More in
5252

5353
See our [Swagger API Documentation](https://waka.hackclub.com/swagger-ui).
5454

55+
### Signup
56+
5557
For signing up user programaticaly you can use the `/signup` endpoint with the admin token as Bearer and it will return a json object similar to the following:
5658

5759
```ts
@@ -80,6 +82,42 @@ console.log(await signup.json())
8082

8183
If the user already exists then you will get a `true` value in the `created` field.
8284

85+
### Reset Password
86+
87+
For resetting a user's password programmatically, you can use the `/reset-password` endpoint with the admin token as Bearer and it will return a json object containing the reset token and user ID:
88+
89+
```ts
90+
const resetPassword = await fetch('http://localhost:8888/reset-password', {
91+
method: 'POST',
92+
headers: {
93+
'Authorization': 'Bearer blahaji_rulz_da_world'
94+
},
95+
body: new URLSearchParams({
96+
'email': '[email protected]'
97+
})
98+
});
99+
100+
console.log(await resetPassword.json())
101+
```
102+
103+
```json
104+
{
105+
"user_id": "test",
106+
"reset_token": "d4e5f6-reset-token-g7h8i9"
107+
}
108+
```
109+
110+
If the user is not found, you will receive a 404 status code with an error message:
111+
112+
```json
113+
{
114+
"error": "user not found"
115+
}
116+
```
117+
118+
The reset token can then be used with the `/set-password` endpoint to complete the password reset process.
119+
120+
83121

84122
### WakaTime integration
85123

routes/login.go

+39-3
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,9 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
380380
loadTemplates()
381381
}
382382

383-
if !h.config.Mail.Enabled {
383+
adminTokenReset := r.Header.Get("Authorization") == "Bearer "+h.config.Security.AdminToken
384+
385+
if !h.config.Mail.Enabled && !adminTokenReset {
384386
w.WriteHeader(http.StatusNotImplemented)
385387
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w, false).WithError("mailing is disabled on this server"))
386388
return
@@ -389,22 +391,49 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
389391
var resetRequest models.ResetPasswordRequest
390392
if err := r.ParseForm(); err != nil {
391393
w.WriteHeader(http.StatusBadRequest)
394+
if adminTokenReset {
395+
json.NewEncoder(w).Encode(map[string]string{"error": "missing parameters"})
396+
return
397+
}
392398
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w, false).WithError("missing parameters"))
393399
return
394400
}
395401
if err := resetPasswordDecoder.Decode(&resetRequest, r.PostForm); err != nil {
396402
w.WriteHeader(http.StatusBadRequest)
403+
if adminTokenReset {
404+
json.NewEncoder(w).Encode(map[string]string{"error": "missing parameters"})
405+
return
406+
}
397407
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w, false).WithError("missing parameters"))
398408
return
399409
}
400410

401411
if user, err := h.userSrvc.GetUserByEmail(resetRequest.Email); user != nil && err == nil {
402412
if u, err := h.userSrvc.GenerateResetToken(user); err != nil {
403413
w.WriteHeader(http.StatusInternalServerError)
414+
if adminTokenReset {
415+
json.NewEncoder(w).Encode(map[string]string{"error": "failed to generate password reset token"})
416+
return
417+
}
404418
conf.Log().Request(r).Error("failed to generate password reset token", "error", err)
405419
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w, false).WithError("failed to generate password reset token"))
406420
return
407421
} else {
422+
// If admin token is present, return reset token and user ID
423+
if adminTokenReset {
424+
response := struct {
425+
UserID string `json:"user_id"`
426+
ResetToken string `json:"reset_token"`
427+
}{
428+
UserID: u.ID,
429+
ResetToken: u.ResetToken,
430+
}
431+
w.Header().Set("Content-Type", "application/json")
432+
w.WriteHeader(http.StatusOK)
433+
json.NewEncoder(w).Encode(response)
434+
return
435+
}
436+
408437
go func(user *models.User) {
409438
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
410439
if h.config.Security.AirtableAPIKey != "" && resetRequest.Slack && strings.HasPrefix(user.ID, "U") {
@@ -454,10 +483,17 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
454483
}
455484
} else {
456485
conf.Log().Request(r).Warn("password reset requested for unregistered address", "email", resetRequest.Email)
486+
if adminTokenReset {
487+
w.WriteHeader(http.StatusNotFound)
488+
json.NewEncoder(w).Encode(map[string]string{"error": "user not found"})
489+
return
490+
}
457491
}
458492

459-
routeutils.SetSuccess(r, w, "an e-mail was sent to you in case your e-mail address was registered")
460-
http.Redirect(w, r, h.config.Server.BasePath, http.StatusFound)
493+
if !adminTokenReset {
494+
routeutils.SetSuccess(r, w, "an e-mail was sent to you in case your e-mail address was registered")
495+
http.Redirect(w, r, h.config.Server.BasePath, http.StatusFound)
496+
}
461497
}
462498

463499
func (h *LoginHandler) buildViewModel(r *http.Request, w http.ResponseWriter, withCaptcha bool) *view.LoginViewModel {

0 commit comments

Comments
 (0)