fix: migrate sigin to testable code #181

This commit is contained in:
2024-10-04 23:25:57 +02:00
parent abd8663cf3
commit 5b4160b09f
6 changed files with 252 additions and 133 deletions

View File

@@ -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 {