package main import ( "bytes" "net/http" "strconv" "time" "brainminder.speedtech.it/internal/password" "brainminder.speedtech.it/internal/request" "brainminder.speedtech.it/internal/response" "brainminder.speedtech.it/internal/token" "brainminder.speedtech.it/internal/validator" "brainminder.speedtech.it/models" "github.com/alexedwards/flow" ) func (app *application) home(w http.ResponseWriter, r *http.Request) { session, _ := app.sessionStore.Get(r, "session") current_notebook_id := session.Values["current_notebook_id"] params := r.URL.Query() if r.Method == http.MethodPost { err := r.ParseForm() if err != nil { app.serverError(w, r, err) return } current_notebook_id = r.PostForm.Get("current_notebook_id") session.Values["current_notebook_id"] = current_notebook_id session.Save(r, w) } var fullBuf = new(bytes.Buffer) data := app.newTemplateData(r) itemModel := models.NewItemModel(app.db) var notebook_id int64 = -1 if current_notebook_id != nil { notebook_id, _ = strconv.ParseInt(current_notebook_id.(string), 10, 64) } criteria := map[string]any{ "On_dashboard": 1, "notebook_id": notebook_id, } offset_str := r.URL.Query().Get("offset") if len(offset_str) == 0 { offset_str = "0" } offset, _ := strconv.ParseInt(offset_str, 10, 64) items, _, _ := itemModel.Find(criteria, offset) data["items"] = items data["offset"] = offset if r.Header.Get("HX-Request") == "true" { out := params.Get("out") if out == "items" { err := response.HXFragment(fullBuf, []string{"pages/home_items.tmpl"}, "home:items", data) if err != nil { app.serverError(w, r, err) } } else { err := response.HXFragment(fullBuf, []string{"pages/home.tmpl", "pages/home_items.tmpl"}, "page:content", data) if err != nil { app.serverError(w, r, err) } err = response.HXFragmentOOB(fullBuf, []string{"pages/home_title.tmpl"}, "page:title", data, "page-title") if err != nil { app.serverError(w, r, err) } } fullBuf.WriteTo(w) } else { err := response.Page(w, http.StatusOK, data, []string{"pages/home.tmpl", "pages/home_items.tmpl", "pages/home_title.tmpl"}) if err != nil { app.serverError(w, r, err) } } } func (app *application) signup(w http.ResponseWriter, r *http.Request) { var form struct { Email string `form:"Email"` Password string `form:"Password"` Validator validator.Validator `form:"-"` } switch r.Method { case http.MethodGet: data := app.newTemplateData(r) data["Form"] = form err := response.Page(w, http.StatusOK, data, []string{"pages/signup.tmpl"}) if err != nil { app.serverError(w, r, err) } case http.MethodPost: err := request.DecodePostForm(r, &form) if err != nil { app.badRequest(w, err) return } _, found, err := app.db.GetUserByEmail(form.Email) if err != nil { app.serverError(w, r, err) return } form.Validator.CheckField(form.Email != "", "Email", "Email is required") form.Validator.CheckField(validator.Matches(form.Email, validator.RgxEmail), "Email", "Must be a valid email address") form.Validator.CheckField(!found, "Email", "Email is already in use") form.Validator.CheckField(form.Password != "", "Password", "Password is required") form.Validator.CheckField(len(form.Password) >= 8, "Password", "Password is too short") form.Validator.CheckField(len(form.Password) <= 72, "Password", "Password is too long") form.Validator.CheckField(validator.NotIn(form.Password, password.CommonPasswords...), "Password", "Password is too common") if form.Validator.HasErrors() { data := app.newTemplateData(r) data["Form"] = form err := response.Page(w, http.StatusUnprocessableEntity, data, []string{"pages/signup.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } return } hashedPassword, err := password.Hash(form.Password) if err != nil { app.serverError(w, r, err) return } id, err := app.db.InsertUser(form.Email, hashedPassword) if err != nil { app.serverError(w, r, err) return } session, err := app.sessionStore.Get(r, "session") if err != nil { app.serverError(w, r, err) return } session.Values["userID"] = id err = session.Save(r, w) if err != nil { app.serverError(w, r, err) return } http.Redirect(w, r, "/", http.StatusSeeOther) } } func (app *application) login(w http.ResponseWriter, r *http.Request) { var form struct { Email string `form:"Email"` Password string `form:"Password"` Validator validator.Validator `form:"-"` } switch r.Method { case http.MethodGet: data := app.newTemplateData(r) data["Form"] = form err := response.Page(w, http.StatusOK, data, []string{"pages/login.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } case http.MethodPost: err := request.DecodePostForm(r, &form) if err != nil { app.badRequest(w, err) return } user, found, err := app.db.GetUserByEmail(form.Email) if err != nil { app.serverError(w, r, err) return } form.Validator.CheckField(form.Email != "", "Email", "Email is required") form.Validator.CheckField(found, "Email", "Email address could not be found") if found { passwordMatches, err := password.Matches(form.Password, user.HashedPassword) if err != nil { app.serverError(w, r, err) return } form.Validator.CheckField(form.Password != "", "Password", "Password is required") form.Validator.CheckField(passwordMatches, "Password", "Password is incorrect") } if form.Validator.HasErrors() { data := app.newTemplateData(r) data["Form"] = form err := response.Page(w, http.StatusUnprocessableEntity, data, []string{"pages/login.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } return } session, err := app.sessionStore.Get(r, "session") if err != nil { app.serverError(w, r, err) return } session.Values["userID"] = user.ID redirectPath, ok := session.Values["redirectPathAfterLogin"].(string) if ok { delete(session.Values, "redirectPathAfterLogin") } else { redirectPath = "/" } err = session.Save(r, w) if err != nil { app.serverError(w, r, err) return } http.Redirect(w, r, redirectPath, http.StatusSeeOther) } } func (app *application) logout(w http.ResponseWriter, r *http.Request) { session, err := app.sessionStore.Get(r, "session") if err != nil { app.serverError(w, r, err) return } delete(session.Values, "userID") err = session.Save(r, w) if err != nil { app.serverError(w, r, err) return } http.Redirect(w, r, "/login", http.StatusSeeOther) } func (app *application) forgottenPassword(w http.ResponseWriter, r *http.Request) { var form struct { Email string `form:"Email"` Validator validator.Validator `form:"-"` } switch r.Method { case http.MethodGet: data := app.newTemplateData(r) data["Form"] = form err := response.Page(w, http.StatusOK, data, []string{"pages/forgotten-password.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } case http.MethodPost: err := request.DecodePostForm(r, &form) if err != nil { app.badRequest(w, err) return } user, found, err := app.db.GetUserByEmail(form.Email) if err != nil { app.serverError(w, r, err) return } form.Validator.CheckField(form.Email != "", "Email", "Email is required") form.Validator.CheckField(validator.Matches(form.Email, validator.RgxEmail), "Email", "Must be a valid email address") form.Validator.CheckField(found, "Email", "No matching email found") if form.Validator.HasErrors() { data := app.newTemplateData(r) data["Form"] = form err := response.Page(w, http.StatusUnprocessableEntity, data, []string{"pages/forgotten-password.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } return } plaintextToken, err := token.New() if err != nil { app.serverError(w, r, err) return } hashedToken := token.Hash(plaintextToken) err = app.db.InsertPasswordReset(hashedToken, user.ID, 24*time.Hour) if err != nil { app.serverError(w, r, err) return } data := app.newEmailData() data["PlaintextToken"] = plaintextToken err = app.mailer.Send(user.Email, data, "forgotten-password.tmpl") if err != nil { app.serverError(w, r, err) return } http.Redirect(w, r, "/forgotten-password-confirmation", http.StatusSeeOther) } } func (app *application) forgottenPasswordConfirmation(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r) err := response.Page(w, http.StatusOK, data, []string{"pages/forgotten-password-confirmation.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } } func (app *application) passwordReset(w http.ResponseWriter, r *http.Request) { plaintextToken := flow.Param(r.Context(), "plaintextToken") hashedToken := token.Hash(plaintextToken) passwordReset, found, err := app.db.GetPasswordReset(hashedToken) if err != nil { app.serverError(w, r, err) return } if !found { data := app.newTemplateData(r) data["InvalidLink"] = true err := response.Page(w, http.StatusUnprocessableEntity, data, []string{"pages/password-reset.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } return } var form struct { NewPassword string `form:"NewPassword"` Validator validator.Validator `form:"-"` } switch r.Method { case http.MethodGet: data := app.newTemplateData(r) data["Form"] = form data["PlaintextToken"] = plaintextToken err := response.Page(w, http.StatusOK, data, []string{"pages/password-reset.tmpl"}) if err != nil { app.serverError(w, r, err) } case http.MethodPost: err := request.DecodePostForm(r, &form) if err != nil { app.badRequest(w, err) return } form.Validator.CheckField(form.NewPassword != "", "NewPassword", "New password is required") form.Validator.CheckField(len(form.NewPassword) >= 8, "NewPassword", "New password is too short") form.Validator.CheckField(len(form.NewPassword) <= 72, "NewPassword", "New password is too long") form.Validator.CheckField(validator.NotIn(form.NewPassword, password.CommonPasswords...), "NewPassword", "New password is too common") if form.Validator.HasErrors() { data := app.newTemplateData(r) data["Form"] = form data["PlaintextToken"] = plaintextToken err := response.Page(w, http.StatusUnprocessableEntity, data, []string{"pages/password-reset.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } return } hashedPassword, err := password.Hash(form.NewPassword) if err != nil { app.serverError(w, r, err) return } err = app.db.UpdateUserHashedPassword(passwordReset.UserID, hashedPassword) if err != nil { app.serverError(w, r, err) return } err = app.db.DeletePasswordResets(passwordReset.UserID) if err != nil { app.serverError(w, r, err) return } http.Redirect(w, r, "/password-reset-confirmation", http.StatusSeeOther) } } func (app *application) passwordResetConfirmation(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r) err := response.Page(w, http.StatusOK, data, []string{"pages/password-reset-confirmation.tmpl"}, "full") if err != nil { app.serverError(w, r, err) } }