BrainMinder/cmd/web/handlers.go

438 lines
11 KiB
Go
Raw Normal View History

2024-08-22 10:13:16 +02:00
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)
}
}