more refactoring into seperate handlers

This commit is contained in:
2025-08-25 13:16:40 +01:00
parent 10cea769bf
commit a78f766122
9 changed files with 367 additions and 372 deletions

68
backend/auth/auth.go Normal file
View File

@@ -0,0 +1,68 @@
package auth
import (
"errors"
"math/rand"
"time"
)
type Code struct {
Code string
Valid time.Time
}
type Auth struct {
codes map[string]Code
mailer Mailer
}
var letterRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
func (a *Auth) CreateCode(email string) error {
code := randString(10)
if err := a.mailer.SendCode(email, code); err != nil {
return err
}
a.codes[email] = Code{
Code: code,
Valid: time.Now().Add(time.Minute),
}
return nil
}
func (a *Auth) IsCodeValid(email string, code string) bool {
existingCode, exists := a.codes[email]
if !exists {
return false
}
return existingCode.Valid.After(time.Now()) && existingCode.Code == code
}
func (a *Auth) UseCode(email string, code string) error {
if valid := a.IsCodeValid(email, code); !valid {
return errors.New("This code is invalid.")
}
delete(a.codes, email)
return nil
}
func CreateAuth(mailer Mailer) Auth {
return Auth{
codes: make(map[string]Code),
mailer: mailer,
}
}

30
backend/auth/auth_test.go Normal file
View File

@@ -0,0 +1,30 @@
package auth
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
type TestMail struct{}
func (m TestMail) SendCode(to string, code string) error {
return nil
}
var testMailer = TestMail{}
func TestCreateCode(t *testing.T) {
require := require.New(t)
auth := CreateAuth(testMailer)
err := auth.CreateCode("test")
require.NoError(err)
code, exists := auth.codes["test"]
require.True(exists)
require.True(code.Valid.After(time.Now()))
require.True(auth.IsCodeValid("test", code.Code))
}

72
backend/auth/email.go Normal file
View File

@@ -0,0 +1,72 @@
package auth
import (
"fmt"
"os"
"github.com/wneessen/go-mail"
)
type MailClient struct {
client *mail.Client
}
type TestMailClient struct{}
type Mailer interface {
SendCode(to string, code string) error
}
func (m MailClient) getMessage() (*mail.Msg, error) {
message := mail.NewMsg()
if err := message.From("auth@johncosta.tech"); err != nil {
return nil, err
}
return message, nil
}
func (m MailClient) SendCode(to string, code string) error {
msg, err := m.getMessage()
if err != nil {
return err
}
if err := msg.To(to); err != nil {
return err
}
msg.Subject("Login to Haystack")
msg.SetBodyString(mail.TypeTextPlain, code)
return m.client.DialAndSend(msg)
}
func (m TestMailClient) SendCode(to string, code string) error {
fmt.Printf("Email: %s | Code %s\n", to, code)
return nil
}
func CreateMailClient() (Mailer, error) {
mode := os.Getenv("MODE")
if mode == "DEV" {
return TestMailClient{}, nil
}
client, err := mail.NewClient(
"smtp.mailbox.org",
mail.WithTLSPortPolicy(mail.TLSMandatory),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(os.Getenv("EMAIL_USERNAME")),
mail.WithPassword(os.Getenv("EMAIL_PASSWORD")),
)
if err != nil {
return MailClient{}, err
}
return MailClient{
client: client,
}, nil
}

107
backend/auth/handler.go Normal file
View File

@@ -0,0 +1,107 @@
package auth
import (
"database/sql"
"net/http"
"os"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/middleware"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/v5"
)
type AuthHandler struct {
logger *log.Logger
user models.UserModel
auth Auth
}
type loginBody struct {
Email string `json:"email"`
}
type codeBody struct {
Email string `json:"email"`
Code string `json:"code"`
}
type codeReturn struct {
Access string `json:"access"`
Refresh string `json:"refresh"`
}
func (h *AuthHandler) login(body loginBody, w http.ResponseWriter, r *http.Request) {
// TODO: validate email
err := h.auth.CreateCode(body.Email)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not create a code", w)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *AuthHandler) code(body codeBody, w http.ResponseWriter, r *http.Request) {
if err := h.auth.UseCode(body.Email, body.Code); err != nil {
middleware.WriteErrorBadRequest(h.logger, "email or code are incorrect", w)
return
}
// TODO: we should only keep emails around for a little bit.
// Time to first login should be less than 10 minutes.
// So actually, they shouldn't be written to our database.
if exists := h.user.DoesUserExist(r.Context(), body.Email); !exists {
h.user.Save(r.Context(), model.Users{
Email: body.Email,
})
}
uuid, err := h.user.GetUserIdFromEmail(r.Context(), body.Email)
if err != nil {
middleware.WriteErrorBadRequest(h.logger, "failed to get user", w)
return
}
refresh := middleware.CreateRefreshToken(uuid)
access := middleware.CreateAccessToken(uuid)
codeReturn := codeReturn{
Access: access,
Refresh: refresh,
}
middleware.WriteJsonOrError(h.logger, codeReturn, w)
}
func (h *AuthHandler) CreateRoutes(r chi.Router) {
h.logger.Info("Mounting auth router")
r.Group(func(r chi.Router) {
r.Use(middleware.SetJson)
r.Post("/login", middleware.WithValidatedPost(h.login))
r.Post("/code", middleware.WithValidatedPost(h.code))
})
}
func CreateAuthHandler(db *sql.DB) AuthHandler {
userModel := models.NewUserModel(db)
logger := log.New(os.Stdout).WithPrefix("Auth")
mailer, err := CreateMailClient()
if err != nil {
panic(err)
}
auth := CreateAuth(mailer)
return AuthHandler{
logger,
userModel,
auth,
}
}