fix: migrate sigin to testable code #181
This commit is contained in:
194
service/auth.go
194
service/auth.go
@@ -11,6 +11,7 @@ import (
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"me-fit/db"
|
||||
"me-fit/template"
|
||||
@@ -27,6 +28,8 @@ import (
|
||||
var (
|
||||
ErrInvaidCredentials = errors.New("Invalid email or password")
|
||||
ErrPasswordComplexity = errors.New("Password needs to be 8 characters long, contain at least one number, one special, one uppercase and one lowercase character")
|
||||
ErrInvalidEmail = errors.New("Invalid email")
|
||||
ErrAccountExists = errors.New("Account already exists")
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@@ -45,20 +48,25 @@ func NewUser(user *db.User) *User {
|
||||
|
||||
type ServiceAuth interface {
|
||||
SignIn(email string, password string) (*User, error)
|
||||
SignUp(email string, password string) (*User, error)
|
||||
SendVerificationMail(user *User)
|
||||
}
|
||||
|
||||
type ServiceAuthImpl struct {
|
||||
dbAuth db.DbAuth
|
||||
dbAuth db.DbAuth
|
||||
serverSettings *types.ServerSettings
|
||||
mailService MailService
|
||||
}
|
||||
|
||||
func NewServiceAuthImpl(dbAuth db.DbAuth) *ServiceAuthImpl {
|
||||
func NewServiceAuthImpl(dbAuth db.DbAuth, serverSettings *types.ServerSettings) *ServiceAuthImpl {
|
||||
return &ServiceAuthImpl{
|
||||
dbAuth: dbAuth,
|
||||
dbAuth: dbAuth,
|
||||
serverSettings: serverSettings,
|
||||
mailService: NewMailService(serverSettings),
|
||||
}
|
||||
}
|
||||
|
||||
func (service ServiceAuthImpl) SignIn(email string, password string) (*User, error) {
|
||||
|
||||
user, err := service.dbAuth.GetUser(email)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrUserNotFound) {
|
||||
@@ -77,30 +85,79 @@ func (service ServiceAuthImpl) SignIn(email string, password string) (*User, err
|
||||
return NewUser(user), nil
|
||||
}
|
||||
|
||||
// TODO
|
||||
func (service ServiceAuthImpl) SignUp(email string, password string) (*User, error) {
|
||||
_, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidEmail
|
||||
}
|
||||
|
||||
func HandleSignUpPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := utils.GetUserFromSession(db, r)
|
||||
err = checkPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
userComp := UserInfoComp(nil)
|
||||
signUpComp := auth.SignInOrUpComp(false)
|
||||
err := template.Layout(signUpComp, userComp, serverSettings.Environment).Render(r.Context(), w)
|
||||
userId, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
utils.LogError("Could not generate UUID", err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.LogError("Failed to render sign up page", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
salt := make([]byte, 16)
|
||||
_, err = rand.Read(salt)
|
||||
if err != nil {
|
||||
utils.LogError("Could not generate salt", err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
} else if !user.EmailVerified {
|
||||
utils.DoRedirect(w, r, "/auth/verify")
|
||||
hash := GetHashPassword(password, salt)
|
||||
|
||||
dbUser := db.NewUser(userId, email, false, nil, false, hash, salt, time.Now())
|
||||
|
||||
err = service.dbAuth.InsertUser(dbUser)
|
||||
if err != nil {
|
||||
if err == db.ErrUserExists {
|
||||
return nil, ErrAccountExists
|
||||
} else {
|
||||
utils.DoRedirect(w, r, "/")
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
}
|
||||
|
||||
return NewUser(dbUser), nil
|
||||
}
|
||||
|
||||
func (service ServiceAuthImpl) SendVerificationMail(user *User) {
|
||||
var token string
|
||||
|
||||
token, err := service.dbAuth.GetEmailVerificationToken(user.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
token, err := utils.RandomToken()
|
||||
if err != nil {
|
||||
utils.LogError("Could not generate token", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = service.dbAuth.InsertEmailVerificationToken(user.Id, token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var w strings.Builder
|
||||
err = tempMail.Register(service.serverSettings.BaseUrl, token).Render(context.Background(), &w)
|
||||
if err != nil {
|
||||
utils.LogError("Could not render welcome email", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.mailService.SendMail(user.Email, "Welcome to ME-FIT", w.String())
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
func HandleSignUpVerifyPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := utils.GetUserFromSession(db, r)
|
||||
@@ -227,69 +284,6 @@ func UserInfoComp(user *types.User) templ.Component {
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSignUpComp(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var email = r.FormValue("email")
|
||||
var password = r.FormValue("password")
|
||||
|
||||
_, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid email", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = checkPassword(password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
utils.LogError("Could not generate UUID", err)
|
||||
auth.Error("Internal Server Error").Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
salt := make([]byte, 16)
|
||||
_, err = rand.Read(salt)
|
||||
if err != nil {
|
||||
utils.LogError("Could not generate salt", err)
|
||||
auth.Error("Internal Server Error").Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
hash := GetHashPassword(password, salt)
|
||||
|
||||
_, err = db.Exec("INSERT INTO user (user_uuid, email, email_verified, is_admin, password, salt, created_at) VALUES (?, ?, FALSE, FALSE, ?, ?, datetime())", userId, email, hash, salt)
|
||||
if err != nil {
|
||||
// This does leak information about the email being in use, though not specifically stated
|
||||
// It needs to be refacoteres to "If the email is not already in use, an email has been send to your address", or something
|
||||
// The happy path, currently a redirect, needs to send the same message!
|
||||
// Then it is also important to have the same compute time in both paths
|
||||
// Otherwise an attacker could guess emails when comparing the response time
|
||||
if strings.Contains(err.Error(), "email") {
|
||||
auth.Error("Bad Request").Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
utils.LogError("Could not insert user", err)
|
||||
auth.Error("Internal Server Error").Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
|
||||
err = TryCreateSessionAndSetCookie(r, w, db, userId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Send verification email as a goroutine
|
||||
go sendVerificationEmail(db, userId.String(), email, serverSettings)
|
||||
|
||||
utils.DoRedirect(w, r, "/auth/verify")
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSignOutComp(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := utils.GetUserFromSession(db, r)
|
||||
@@ -394,7 +388,8 @@ func HandleVerifyResendComp(db *sql.DB, serverSettings *types.ServerSettings) ht
|
||||
return
|
||||
}
|
||||
|
||||
go sendVerificationEmail(db, user.Id.String(), user.Email, serverSettings)
|
||||
// TODO
|
||||
// go sendVerificationEmail(db, user.Id.String(), user.Email, serverSettings)
|
||||
|
||||
w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>"))
|
||||
}
|
||||
@@ -566,39 +561,6 @@ func HandleResetPasswordComp(db *sql.DB, serverSettings *types.ServerSettings) h
|
||||
}
|
||||
}
|
||||
|
||||
func sendVerificationEmail(db *sql.DB, userId string, email string, serverSettings *types.ServerSettings) {
|
||||
|
||||
var token string
|
||||
err := db.QueryRow("SELECT token FROM user_token WHERE user_uuid = ? AND type = 'email_verify'", userId).Scan(&token)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
utils.LogError("Could not get token", err)
|
||||
return
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
token, err := utils.RandomToken()
|
||||
if err != nil {
|
||||
utils.LogError("Could not generate token", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.Exec("INSERT INTO user_token (user_uuid, type, token, created_at) VALUES (?, 'email_verify', ?, datetime())", userId, token)
|
||||
if err != nil {
|
||||
utils.LogError("Could not insert token", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var w strings.Builder
|
||||
err = tempMail.Register(serverSettings.BaseUrl, token).Render(context.Background(), &w)
|
||||
if err != nil {
|
||||
utils.LogError("Could not render welcome email", err)
|
||||
return
|
||||
}
|
||||
mailService := NewMailService(serverSettings)
|
||||
mailService.SendMail(email, "Welcome to ME-FIT", w.String())
|
||||
}
|
||||
|
||||
func TryCreateSessionAndSetCookie(r *http.Request, w http.ResponseWriter, db *sql.DB, user_uuid uuid.UUID) error {
|
||||
sessionId, err := utils.RandomToken()
|
||||
if err != nil {
|
||||
|
||||
@@ -2,7 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"me-fit/db"
|
||||
m "me-fit/mocks"
|
||||
"me-fit/mocks"
|
||||
"me-fit/types"
|
||||
|
||||
"errors"
|
||||
@@ -18,7 +18,6 @@ func TestSignIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
salt := []byte("salt")
|
||||
verifiedAt := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
user := db.NewUser(
|
||||
uuid.New(),
|
||||
"test@test.de",
|
||||
@@ -30,10 +29,10 @@ func TestSignIn(t *testing.T) {
|
||||
time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
mockDbAuth := m.NewMockDbAuth(t)
|
||||
mockDbAuth := mocks.NewMockDbAuth(t)
|
||||
mockDbAuth.EXPECT().GetUser("test@test.de").Return(user, nil)
|
||||
|
||||
underTest := NewServiceAuthImpl(mockDbAuth)
|
||||
underTest := NewServiceAuthImpl(mockDbAuth, &types.ServerSettings{})
|
||||
|
||||
actualUser, err := underTest.SignIn(user.Email, "password")
|
||||
if err != nil {
|
||||
@@ -54,6 +53,7 @@ func TestSignIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
salt := []byte("salt")
|
||||
verifiedAt := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
user := db.NewUser(
|
||||
uuid.New(),
|
||||
"test@test.de",
|
||||
@@ -65,10 +65,10 @@ func TestSignIn(t *testing.T) {
|
||||
time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
mockDbAuth := m.NewMockDbAuth(t)
|
||||
mockDbAuth := mocks.NewMockDbAuth(t)
|
||||
mockDbAuth.EXPECT().GetUser(user.Email).Return(user, nil)
|
||||
|
||||
underTest := NewServiceAuthImpl(mockDbAuth)
|
||||
underTest := NewServiceAuthImpl(mockDbAuth, &types.ServerSettings{})
|
||||
|
||||
_, err := underTest.SignIn("test@test.de", "wrong password")
|
||||
if err != ErrInvaidCredentials {
|
||||
@@ -77,10 +77,11 @@ func TestSignIn(t *testing.T) {
|
||||
})
|
||||
t.Run("should return ErrInvalidCretentials if user has not been found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockDbAuth := m.NewMockDbAuth(t)
|
||||
|
||||
mockDbAuth := mocks.NewMockDbAuth(t)
|
||||
mockDbAuth.EXPECT().GetUser("test").Return(nil, db.ErrUserNotFound)
|
||||
|
||||
underTest := NewServiceAuthImpl(mockDbAuth)
|
||||
underTest := NewServiceAuthImpl(mockDbAuth, &types.ServerSettings{})
|
||||
|
||||
_, err := underTest.SignIn("test", "test")
|
||||
if err != ErrInvaidCredentials {
|
||||
@@ -89,10 +90,11 @@ func TestSignIn(t *testing.T) {
|
||||
})
|
||||
t.Run("should forward ErrInternal on any other error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockDbAuth := m.NewMockDbAuth(t)
|
||||
|
||||
mockDbAuth := mocks.NewMockDbAuth(t)
|
||||
mockDbAuth.EXPECT().GetUser("test").Return(nil, errors.New("Some error"))
|
||||
|
||||
underTest := NewServiceAuthImpl(mockDbAuth)
|
||||
underTest := NewServiceAuthImpl(mockDbAuth, &types.ServerSettings{})
|
||||
|
||||
_, err := underTest.SignIn("test", "test")
|
||||
if err != types.ErrInternal {
|
||||
|
||||
Reference in New Issue
Block a user