more refactoring into seperate handlers
This commit is contained in:
68
backend/auth/auth.go
Normal file
68
backend/auth/auth.go
Normal 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
30
backend/auth/auth_test.go
Normal 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
72
backend/auth/email.go
Normal 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
107
backend/auth/handler.go
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user