Skip to content

Commit d362ae7

Browse files
feat: send slack dms for password resets
1 parent fd4601b commit d362ae7

File tree

6 files changed

+137
-5
lines changed

6 files changed

+137
-5
lines changed

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ type appConfig struct {
108108
type securityConfig struct {
109109
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
110110
SignupCaptcha bool `yaml:"signup_captcha" default:"false" env:"WAKAPI_SIGNUP_CAPTCHA"`
111+
AirtableAPIKey string `yaml:"airtable_api_key" env:"WAKAPI_AIRTABLE_API_KEY"`
111112
InviteCodes bool `yaml:"invite_codes" default:"true" env:"WAKAPI_INVITE_CODES"`
112113
ExposeMetrics bool `yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS"`
113114
EnableProxy bool `yaml:"enable_proxy" default:"false" env:"WAKAPI_ENABLE_PROXY"` // only intended for production instance at wakapi.dev

models/user.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type SetPasswordRequest struct {
8080

8181
type ResetPasswordRequest struct {
8282
Email string `schema:"email"`
83+
Slack bool `schema:"slack"`
8384
}
8485

8586
type CredentialsReset struct {

models/view/login.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package view
22

33
type LoginViewModel struct {
44
SharedViewModel
5-
TotalUsers int
6-
AllowSignup bool
7-
CaptchaId string
8-
InviteCode string
5+
TotalUsers int
6+
AllowSignup bool
7+
CaptchaId string
8+
InviteCode string
9+
SlackEnabled bool
910
}
1011

1112
type SetPasswordViewModel struct {

routes/login.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,44 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
407407
} else {
408408
go func(user *models.User) {
409409
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
410+
if h.config.Security.AirtableAPIKey != "" && resetRequest.Slack {
411+
msgtext := fmt.Sprintf("Arr `%s`! Looks liek ye requested a password reset from hackatime for the email `%s`! If ye didn't request this then sombardy is trying to hack thee account and you should steer clear of below button :tw_crossed_swords:", func() string {
412+
if user.Name == "" {
413+
return "matey"
414+
} else {
415+
return user.Name
416+
}
417+
}(), user.Email)
418+
msg := fmt.Sprintf("A password reset was requested for the email %s; you can use the following link to reset your password: %s", user.Email, link)
419+
blocks := `[
420+
{
421+
"type": "section",
422+
"text": {
423+
"type": "mrkdwn",
424+
"text": "` + msgtext + `"
425+
}
426+
},
427+
{
428+
"type": "actions",
429+
"elements": [
430+
{
431+
"type": "button",
432+
"text": {
433+
"type": "plain_text",
434+
"text": "Reset Password"
435+
},
436+
"style": "primary",
437+
"url": "` + link + `"
438+
}
439+
]
440+
}
441+
]`
442+
if err := utils.SendSlackMessage(h.config.Security.AirtableAPIKey, "U062UG485EE", msg, blocks); err != nil {
443+
conf.Log().Request(r).Error("failed to send slack message", "error", err)
444+
} else {
445+
slog.Info("sent slack message", "userID", user.ID)
446+
}
447+
}
410448
if err := h.mailSrvc.SendPasswordReset(user, link); err != nil {
411449
conf.Log().Request(r).Error("failed to send password reset mail", "userID", user.ID, "error", err)
412450
} else {
@@ -430,6 +468,7 @@ func (h *LoginHandler) buildViewModel(r *http.Request, w http.ResponseWriter, wi
430468
TotalUsers: int(numUsers),
431469
AllowSignup: h.config.IsDev() || h.config.Security.AllowSignup,
432470
InviteCode: r.URL.Query().Get("invite"),
471+
SlackEnabled: h.config.Security.AirtableAPIKey != "",
433472
}
434473

435474
if withCaptcha {

utils/slack.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package utils
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
)
10+
11+
func SendSlackMessage(airtableAPIKey string, userID string, message string, blocksJSON string) error {
12+
record := map[string]interface{}{
13+
"fields": map[string]interface{}{
14+
"requester_identifier": "Hackatime Reset Password",
15+
"target_slack_id": userID,
16+
"message_text": message,
17+
"message_blocks": blocksJSON,
18+
"unfurl_links": true,
19+
"unfurl_media": true,
20+
"send_success": false,
21+
},
22+
}
23+
24+
payload := map[string]interface{}{
25+
"records": []interface{}{record},
26+
}
27+
28+
jsonPayload, err := json.Marshal(payload)
29+
if err != nil {
30+
return fmt.Errorf("error marshaling payload: %v", err)
31+
}
32+
33+
req, err := http.NewRequest("POST", "https://middleman.hackclub.com/airtable/v0/appTeNFYcUiYfGcR6/arrpheus_message_requests", bytes.NewBuffer(jsonPayload))
34+
if err != nil {
35+
return fmt.Errorf("error creating request: %v", err)
36+
}
37+
38+
req.Header.Set("Content-Type", "application/json")
39+
req.Header.Set("Accept", "application/json")
40+
req.Header.Set("Authorization", "Bearer "+airtableAPIKey)
41+
req.Header.Set("User-Agent", "waka.hackclub.com (reset password)")
42+
43+
client := &http.Client{}
44+
resp, err := client.Do(req)
45+
if err != nil {
46+
return fmt.Errorf("error sending request: %v", err)
47+
}
48+
defer resp.Body.Close()
49+
50+
body, err := io.ReadAll(resp.Body)
51+
if err != nil {
52+
return fmt.Errorf("error reading response body: %v", err)
53+
}
54+
55+
var result struct {
56+
Records []struct{} `json:"records"`
57+
Error struct {
58+
Type string `json:"type"`
59+
Message string `json:"message"`
60+
} `json:"error,omitempty"`
61+
}
62+
63+
if err := json.Unmarshal(body, &result); err != nil {
64+
return fmt.Errorf("error parsing response: %v", err)
65+
}
66+
67+
if result.Error.Type != "" {
68+
return fmt.Errorf("Airtable error: %s - %s", result.Error.Type, result.Error.Message)
69+
}
70+
71+
return nil
72+
}

views/reset-password.tpl.html

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
33
{{ template "head.tpl.html" . }}
44

@@ -54,6 +54,24 @@
5454
autofocus
5555
/>
5656
</div>
57+
{{ if .SlackEnabled }}
58+
<div class="mb-4">
59+
<div class="flex items-center">
60+
<input
61+
type="checkbox"
62+
id="slack"
63+
name="slack"
64+
class="w-4 h-4 rounded bg-transparent border-text-secondary dark:border-text-dark-secondary"
65+
/>
66+
<label
67+
for="slack"
68+
class="ml-2 text-text-secondary dark:text-text-dark-secondary"
69+
>Send me a Slack dm with the password link as
70+
well</label
71+
>
72+
</div>
73+
</div>
74+
{{ end }}
5775
<div class="flex justify-end items-center">
5876
<button type="submit" class="btn-primary">Reset</button>
5977
</div>

0 commit comments

Comments
 (0)