Skip to content

Commit

Permalink
feat: send slack dms for password resets
Browse files Browse the repository at this point in the history
  • Loading branch information
taciturnaxolotl committed Jan 10, 2025
1 parent fd4601b commit d362ae7
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 5 deletions.
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type appConfig struct {
type securityConfig struct {
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
SignupCaptcha bool `yaml:"signup_captcha" default:"false" env:"WAKAPI_SIGNUP_CAPTCHA"`
AirtableAPIKey string `yaml:"airtable_api_key" env:"WAKAPI_AIRTABLE_API_KEY"`
InviteCodes bool `yaml:"invite_codes" default:"true" env:"WAKAPI_INVITE_CODES"`
ExposeMetrics bool `yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS"`
EnableProxy bool `yaml:"enable_proxy" default:"false" env:"WAKAPI_ENABLE_PROXY"` // only intended for production instance at wakapi.dev
Expand Down
1 change: 1 addition & 0 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type SetPasswordRequest struct {

type ResetPasswordRequest struct {
Email string `schema:"email"`
Slack bool `schema:"slack"`
}

type CredentialsReset struct {
Expand Down
9 changes: 5 additions & 4 deletions models/view/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package view

type LoginViewModel struct {
SharedViewModel
TotalUsers int
AllowSignup bool
CaptchaId string
InviteCode string
TotalUsers int
AllowSignup bool
CaptchaId string
InviteCode string
SlackEnabled bool
}

type SetPasswordViewModel struct {
Expand Down
39 changes: 39 additions & 0 deletions routes/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,44 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
} else {
go func(user *models.User) {
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
if h.config.Security.AirtableAPIKey != "" && resetRequest.Slack {
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 {
if user.Name == "" {
return "matey"
} else {
return user.Name
}
}(), user.Email)
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)
blocks := `[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "` + msgtext + `"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Reset Password"
},
"style": "primary",
"url": "` + link + `"
}
]
}
]`
if err := utils.SendSlackMessage(h.config.Security.AirtableAPIKey, "U062UG485EE", msg, blocks); err != nil {
conf.Log().Request(r).Error("failed to send slack message", "error", err)
} else {
slog.Info("sent slack message", "userID", user.ID)
}
}
if err := h.mailSrvc.SendPasswordReset(user, link); err != nil {
conf.Log().Request(r).Error("failed to send password reset mail", "userID", user.ID, "error", err)
} else {
Expand All @@ -430,6 +468,7 @@ func (h *LoginHandler) buildViewModel(r *http.Request, w http.ResponseWriter, wi
TotalUsers: int(numUsers),
AllowSignup: h.config.IsDev() || h.config.Security.AllowSignup,
InviteCode: r.URL.Query().Get("invite"),
SlackEnabled: h.config.Security.AirtableAPIKey != "",
}

if withCaptcha {
Expand Down
72 changes: 72 additions & 0 deletions utils/slack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package utils

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)

func SendSlackMessage(airtableAPIKey string, userID string, message string, blocksJSON string) error {
record := map[string]interface{}{
"fields": map[string]interface{}{
"requester_identifier": "Hackatime Reset Password",
"target_slack_id": userID,
"message_text": message,
"message_blocks": blocksJSON,
"unfurl_links": true,
"unfurl_media": true,
"send_success": false,
},
}

payload := map[string]interface{}{
"records": []interface{}{record},
}

jsonPayload, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error marshaling payload: %v", err)
}

req, err := http.NewRequest("POST", "https://middleman.hackclub.com/airtable/v0/appTeNFYcUiYfGcR6/arrpheus_message_requests", bytes.NewBuffer(jsonPayload))
if err != nil {
return fmt.Errorf("error creating request: %v", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+airtableAPIKey)
req.Header.Set("User-Agent", "waka.hackclub.com (reset password)")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error reading response body: %v", err)
}

var result struct {
Records []struct{} `json:"records"`
Error struct {
Type string `json:"type"`
Message string `json:"message"`
} `json:"error,omitempty"`
}

if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("error parsing response: %v", err)
}

if result.Error.Type != "" {
return fmt.Errorf("Airtable error: %s - %s", result.Error.Type, result.Error.Message)
}

return nil
}
20 changes: 19 additions & 1 deletion views/reset-password.tpl.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
{{ template "head.tpl.html" . }}

Expand Down Expand Up @@ -54,6 +54,24 @@
autofocus
/>
</div>
{{ if .SlackEnabled }}
<div class="mb-4">
<div class="flex items-center">
<input
type="checkbox"
id="slack"
name="slack"
class="w-4 h-4 rounded bg-transparent border-text-secondary dark:border-text-dark-secondary"
/>
<label
for="slack"
class="ml-2 text-text-secondary dark:text-text-dark-secondary"
>Send me a Slack dm with the password link as
well</label
>
</div>
</div>
{{ end }}
<div class="flex justify-end items-center">
<button type="submit" class="btn-primary">Reset</button>
</div>
Expand Down

0 comments on commit d362ae7

Please sign in to comment.