438 lines
11 KiB
Go
438 lines
11 KiB
Go
|
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)
|
||
|
}
|
||
|
}
|