Files
spend-sparrow/handler/auth.go

380 lines
9.9 KiB
Go

package handler
import (
"me-fit/log"
"me-fit/service"
"me-fit/template/auth"
"me-fit/types"
"me-fit/utils"
"errors"
"net/http"
"net/url"
"time"
)
type HandlerAuth interface {
Handle(router *http.ServeMux)
}
type HandlerAuthImpl struct {
service service.AuthService
render *Render
}
func NewHandlerAuth(service service.AuthService, render *Render) HandlerAuth {
return HandlerAuthImpl{
service: service,
render: render,
}
}
func (handler HandlerAuthImpl) Handle(router *http.ServeMux) {
router.Handle("/auth/signin", handler.handleSignInPage())
router.Handle("/api/auth/signin", handler.handleSignIn())
router.Handle("/auth/signup", handler.handleSignUpPage())
router.Handle("/auth/verify", handler.handleSignUpVerifyPage())
router.Handle("/api/auth/verify-resend", handler.handleVerifyResendComp())
router.Handle("/auth/verify-email", handler.handleSignUpVerifyResponsePage())
router.Handle("/api/auth/signup", handler.handleSignUp())
router.Handle("/api/auth/signout", handler.handleSignOut())
router.Handle("/auth/delete-account", handler.handleDeleteAccountPage())
router.Handle("/api/auth/delete-account", handler.handleDeleteAccountComp())
router.Handle("/auth/change-password", handler.handleChangePasswordPage())
router.Handle("/api/auth/change-password", handler.handleChangePasswordComp())
router.Handle("/auth/reset-password", handler.handleResetPasswordPage())
router.Handle("/api/auth/reset-password", handler.handleForgotPasswordComp())
router.Handle("/api/auth/reset-password-actual", handler.handleForgotPasswordResponseComp())
}
var (
securityWaitDuration = 250 * time.Millisecond
)
func (handler HandlerAuthImpl) handleSignInPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, _ := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if user != nil {
if !user.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
} else {
utils.DoRedirect(w, r, "/")
}
return
}
comp := auth.SignInOrUpComp(true)
handler.render.RenderLayout(r, w, comp, nil)
}
}
func (handler HandlerAuthImpl) handleSignIn() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*service.User, error) {
var email = r.FormValue("email")
var password = r.FormValue("password")
session, err := handler.service.SignIn(email, password)
if err != nil {
return nil, err
}
cookie := http.Cookie{
Name: "id",
Value: session.Id,
MaxAge: 60 * 60 * 8, // 8 hours
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
http.SetCookie(w, &cookie)
return session.User, nil
})
if err != nil {
if err == service.ErrInvaidCredentials {
utils.TriggerToast(w, r, "error", "Invalid email or password")
http.Error(w, "Invalid email or password", http.StatusUnauthorized)
} else {
log.Error("Error signing in: %v", err)
http.Error(w, "An error occurred", http.StatusInternalServerError)
}
return
}
if user.EmailVerified {
utils.DoRedirect(w, r, "/")
} else {
utils.DoRedirect(w, r, "/auth/verify")
}
}
}
func (handler HandlerAuthImpl) handleSignUpPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, _ := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if user != nil {
if !user.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
} else {
utils.DoRedirect(w, r, "/")
}
return
}
signUpComp := auth.SignInOrUpComp(false)
handler.render.RenderLayout(r, w, signUpComp, nil)
}
}
func (handler HandlerAuthImpl) handleSignUpVerifyPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, _ := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
if user.EmailVerified {
utils.DoRedirect(w, r, "/")
return
}
signIn := auth.VerifyComp()
handler.render.RenderLayout(r, w, signIn, user)
}
}
func (handler HandlerAuthImpl) handleVerifyResendComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if err != nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
go handler.service.SendVerificationMail(user.Id, user.Email)
_, err = w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>"))
if err != nil {
log.Error("Could not write response: %v", err)
}
}
}
func (handler HandlerAuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
err := handler.service.VerifyUserEmail(token)
if err != nil {
utils.DoRedirect(w, r, "/auth/signin")
} else {
utils.DoRedirect(w, r, "/")
}
}
}
func (handler HandlerAuthImpl) handleSignUp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var email = r.FormValue("email")
var password = r.FormValue("password")
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) {
user, err := handler.service.SignUp(email, password)
if err != nil {
return nil, err
}
go handler.service.SendVerificationMail(user.Id, user.Email)
return nil, nil
})
if err != nil {
if errors.Is(err, types.ErrInternal) {
utils.TriggerToast(w, r, "error", "An error occurred")
return
} else if errors.Is(err, service.ErrInvalidEmail) {
utils.TriggerToast(w, r, "error", "The email provided is invalid")
return
}
// If the "service.ErrAccountExists", then just continue
}
utils.TriggerToast(w, r, "success", "A link to activate your account has been emailed to the address provided.")
}
}
func (handler HandlerAuthImpl) handleSignOut() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := handler.service.SignOut(utils.GetSessionID(r))
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c := http.Cookie{
Name: "id",
Value: "",
MaxAge: -1,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
http.SetCookie(w, &c)
utils.DoRedirect(w, r, "/")
}
}
func (handler HandlerAuthImpl) handleDeleteAccountPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// An unverified email should be able to delete their account
user, err := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if err != nil {
utils.DoRedirect(w, r, "/auth/signin")
}
comp := auth.DeleteAccountComp()
handler.render.RenderLayout(r, w, comp, user)
}
}
func (handler HandlerAuthImpl) handleDeleteAccountComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if err != nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
password := r.FormValue("password")
_, err = handler.service.SignIn(user.Email, password)
if err != nil {
utils.TriggerToast(w, r, "error", "Password not correct")
return
}
err = handler.service.DeleteAccount(user)
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
utils.DoRedirect(w, r, "/")
}
}
func (handler HandlerAuthImpl) handleChangePasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
isPasswordReset := r.URL.Query().Has("token")
user, _ := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if user == nil && !isPasswordReset {
utils.DoRedirect(w, r, "/auth/signin")
return
}
comp := auth.ChangePasswordComp(isPasswordReset)
handler.render.RenderLayout(r, w, comp, user)
}
}
func (handler HandlerAuthImpl) handleChangePasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if err != nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
currPass := r.FormValue("current-password")
newPass := r.FormValue("new-password")
err = handler.service.ChangePassword(user, currPass, newPass)
if err != nil {
utils.TriggerToast(w, r, "error", "Password not correct")
return
}
utils.TriggerToast(w, r, "success", "Password changed")
}
}
func (handler HandlerAuthImpl) handleResetPasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := handler.service.GetUserFromSessionId(utils.GetSessionID(r))
if err != nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
comp := auth.ResetPasswordComp()
handler.render.RenderLayout(r, w, comp, user)
}
}
func (handler HandlerAuthImpl) handleForgotPasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
if email == "" {
utils.TriggerToast(w, r, "error", "Please enter an email")
return
}
err := handler.service.SendForgotPasswordMail(email)
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error")
} else {
utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent")
}
}
}
func (handler HandlerAuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
pageUrl, err := url.Parse(r.Header.Get("HX-Current-URL"))
if err != nil {
log.Error("Could not get current URL: %v", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
token := pageUrl.Query().Get("token")
if token == "" {
utils.TriggerToast(w, r, "error", "No token")
return
}
newPass := r.FormValue("new-password")
err = handler.service.ForgotPassword(token, newPass)
if err != nil {
utils.TriggerToast(w, r, "error", err.Error())
} else {
utils.TriggerToast(w, r, "success", "Password changed")
}
}
}