887 lines
24 KiB
Go
887 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Thomasorus/Ronbun-CMS/cmd/web/kaku"
|
|
"github.com/Thomasorus/Ronbun-CMS/internal/models"
|
|
"github.com/Thomasorus/Ronbun-CMS/internal/validator"
|
|
"github.com/gorilla/feeds"
|
|
"github.com/gosimple/slug"
|
|
)
|
|
|
|
// Handlers are like MVC controllers. They execute application logic and
|
|
// for writing http responses to headers and bodies.
|
|
|
|
// To inject dependencies into handlers (logger, cache templating), the handlers are
|
|
//defined as methods against the application struct created in main.go.
|
|
|
|
// Map html to struct values with the `form:"item"` from the validator package
|
|
type pageCreateForm struct {
|
|
Name string `form:"name"`
|
|
Summary string `form:"summary"`
|
|
Body string `form:"body"`
|
|
Host int `form:"host"`
|
|
Category string `form:"category"`
|
|
Private int `form:"private"`
|
|
Feed bool `form:"feed"`
|
|
Menu bool `form:"menu"`
|
|
Created string `form:"created"`
|
|
validator.Validator `form:"-"`
|
|
}
|
|
|
|
type pageEditForm struct {
|
|
Name string `form:"name"`
|
|
Summary string `form:"summary"`
|
|
Body string `form:"body"`
|
|
Host int `form:"host"`
|
|
Category string `form:"category"`
|
|
Private int `form:"private"`
|
|
Feed bool `form:"feed"`
|
|
Menu bool `form:"menu"`
|
|
Id int `form:"id"`
|
|
Slug string `form:"slug"`
|
|
Created string `form:"created"`
|
|
validator.Validator `form:"-"`
|
|
}
|
|
|
|
type userSignupForm struct {
|
|
Name string `form:"name"`
|
|
Email string `form:"email"`
|
|
Password string `form:"password"`
|
|
validator.Validator `form:"-"`
|
|
}
|
|
|
|
type userLoginInForm struct {
|
|
Email string `form:"email"`
|
|
Password string `form:"password"`
|
|
validator.Validator `form:"-"`
|
|
}
|
|
|
|
type searchForm struct {
|
|
Search string `form:"search"`
|
|
validator.Validator `form:"-"`
|
|
}
|
|
|
|
type accountPasswordUpdateForm struct {
|
|
CurrentPassword string `form:"currentPassword"`
|
|
NewPassword string `form:"newPassword"`
|
|
NewPasswordConfirmation string `form:"newPasswordConfirmation"`
|
|
validator.Validator `form:"-"`
|
|
}
|
|
|
|
// Serves the /og route
|
|
func (app *application) getEmbed(w http.ResponseWriter, r *http.Request) {
|
|
slug := r.PathValue("slug")
|
|
|
|
if slug == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
if data, ok := app.ogCache.Load(slug); ok {
|
|
w.Header().Set("Content-Type", "image/jpeg")
|
|
w.Write(data.([]byte))
|
|
return
|
|
}
|
|
|
|
var page models.Page
|
|
|
|
authenticated := app.isAuthenticated(r)
|
|
|
|
page, err := app.pages.Get(authenticated, slug)
|
|
if err != nil || page.Private == 2 {
|
|
if errors.Is(err, models.ErrNoRecord) {
|
|
http.NotFound(w, r)
|
|
} else {
|
|
app.serverError(w, r, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if slug == "hire" {
|
|
page.Name = "Let's work together!"
|
|
}
|
|
if slug == "home" {
|
|
page.Name = "Welcome!"
|
|
}
|
|
|
|
buf, err := generateOpenGraphImage(page)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
}
|
|
|
|
app.ogCache.Store(page.Slug, buf.Bytes())
|
|
w.Header().Set("Content-Type", "image/jpeg")
|
|
w.Header().Set("Cache-Control", "public, max-age=86400")
|
|
w.Write(buf.Bytes())
|
|
}
|
|
|
|
// Serves the /home route
|
|
func (app *application) home(w http.ResponseWriter, r *http.Request) {
|
|
data := app.newTemplateData(r)
|
|
data.Page.Name = "Home"
|
|
data.Page.Summary = "The personal website of Thomasorus"
|
|
data.Page.Category = "none"
|
|
data.Page.Slug = "/"
|
|
app.render(w, r, http.StatusOK, "home.tmpl", data)
|
|
}
|
|
|
|
// Serves the /search route
|
|
func (app *application) search(w http.ResponseWriter, r *http.Request) {
|
|
data := app.newTemplateData(r)
|
|
data.Page.Name = "Search"
|
|
data.Page.Summary = "Search for anything on this website"
|
|
data.Page.Category = "none"
|
|
data.Page.Slug = "search"
|
|
data.Form = searchForm{}
|
|
app.render(w, r, http.StatusOK, "search.tmpl", data)
|
|
}
|
|
|
|
// Serve the search POST route
|
|
func (app *application) searchPost(w http.ResponseWriter, r *http.Request) {
|
|
var form searchForm
|
|
|
|
err := app.decodePostForm(r, &form)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
form.CheckField(validator.NotBlank(form.Search), "search", "This field cannot be blank")
|
|
form.CheckField(validator.MinChars(form.Search, 3), "search", "This field must be at least 3 characters long")
|
|
|
|
if !form.Valid() {
|
|
data := app.newTemplateData(r)
|
|
data.Page.Name = "Search"
|
|
data.Page.Summary = "Search for anything on this website"
|
|
data.Page.Category = "none"
|
|
data.Page.Slug = "search"
|
|
data.Form = form
|
|
app.render(w, r, http.StatusUnprocessableEntity, "search.tmpl", data)
|
|
return
|
|
}
|
|
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
|
|
authenticated := app.isAuthenticated(r)
|
|
|
|
pages, err := app.pages.SearchInPages(authenticated, form.Search)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
data.Page.Name = "Search"
|
|
data.Page.Summary = "Search for anything on this website"
|
|
data.Page.Category = "none"
|
|
data.Page.Slug = "search"
|
|
data.Page.ChildrenMenu = pages
|
|
app.render(w, r, http.StatusOK, "search.tmpl", data)
|
|
}
|
|
|
|
// Srves the /feed route
|
|
func (app *application) feed(w http.ResponseWriter, r *http.Request) {
|
|
now := time.Now()
|
|
|
|
feed := &feeds.Feed{
|
|
Title: "Thomasorus",
|
|
Id: "https://thomasorus.com/feed",
|
|
Link: &feeds.Link{Href: `https://thomasorus.com/feed`, Type: "text/html", Rel: "self"},
|
|
Description: "The personal website of Thomasorus",
|
|
Author: &feeds.Author{Name: "Thomasorus", Email: "contact@thomasorus.com"},
|
|
Created: now,
|
|
}
|
|
|
|
pages, err := app.pages.GetRssFeedPages()
|
|
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
var feedItems []*feeds.Item
|
|
for i := 0; i < len(pages); i++ {
|
|
item := pages[i]
|
|
feedItems = append(feedItems,
|
|
&feeds.Item{
|
|
Id: `https://thomasorus.com/` + item.Slug,
|
|
Title: item.Name,
|
|
Link: &feeds.Link{Href: `https://thomasorus.com/` + item.Slug, Type: "text/html", Rel: "self"},
|
|
Created: item.Created,
|
|
Updated: item.Updated,
|
|
Content: string(kaku.Parse(item.Body).Render()),
|
|
Description: item.Summary,
|
|
})
|
|
}
|
|
|
|
feed.Items = feedItems
|
|
|
|
atom, err := feed.ToAtom()
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
w.Write([]byte(atom))
|
|
}
|
|
|
|
// Serves the /hire-me route
|
|
func (app *application) hireme(w http.ResponseWriter, r *http.Request) {
|
|
lang := strings.Split(r.Header.Get("Accept-Language"), ",")[0]
|
|
data := app.newTemplateData(r)
|
|
data.Page.Category = "hire"
|
|
data.Page.Slug = "hire"
|
|
|
|
if lang == "fr" {
|
|
data.Page.Name = "Travaillons ensemble"
|
|
} else {
|
|
data.Page.Name = "For Hire"
|
|
}
|
|
|
|
breadCrumb, err := app.pages.BuildBreadCrumb(data.Page.Host)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
data.Page.BreadCrumb = breadCrumb
|
|
|
|
if lang == "fr" {
|
|
app.render(w, r, http.StatusOK, "recrutement.tmpl", data)
|
|
} else {
|
|
app.render(w, r, http.StatusOK, "hireme.tmpl", data)
|
|
}
|
|
|
|
}
|
|
|
|
func (app *application) sitemap(w http.ResponseWriter, r *http.Request) {
|
|
// If page is sitemap, build it
|
|
data := app.newTemplateData(r)
|
|
data.Page.Name = "Sitemap"
|
|
data.Page.Category = "none"
|
|
data.Page.Slug = "sitemap"
|
|
|
|
authenticated := app.isAuthenticated(r)
|
|
|
|
pages, err := app.pages.IdNameSlugHost(authenticated)
|
|
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
var pagePtrs []*models.Page
|
|
for i := range pages {
|
|
pagePtrs = append(pagePtrs, &pages[i])
|
|
}
|
|
|
|
data.Page.Html = `<h1>Sitemap</h1>` + app.BuildTree(pagePtrs)
|
|
|
|
breadCrumb, err := app.pages.BuildBreadCrumb(data.Page.Host)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
data.Page.BreadCrumb = breadCrumb
|
|
|
|
app.render(w, r, http.StatusOK, "view.tmpl", data)
|
|
}
|
|
|
|
// Serves the /{slug}/ route
|
|
func (app *application) pageView(w http.ResponseWriter, r *http.Request) {
|
|
slug := r.PathValue("slug")
|
|
if slug == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
// Edge cases: redirect old .html pages to clean slugs
|
|
if strings.Contains(slug, ".html") {
|
|
slug = strings.Replace(slug, ".html", "", 1)
|
|
if slug == "home" {
|
|
http.Redirect(w, r, "/", http.StatusPermanentRedirect)
|
|
return
|
|
} else {
|
|
http.Redirect(w, r, "/"+slug, http.StatusPermanentRedirect)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Edge case: redirect feed.xml pages to /feed
|
|
if strings.Contains(slug, ".html") || strings.Contains(slug, ".xml") {
|
|
slug = strings.Replace(slug, ".xml", "", 1)
|
|
if slug == "feed" {
|
|
http.Redirect(w, r, "/feed", http.StatusPermanentRedirect)
|
|
}
|
|
}
|
|
|
|
authenticated := app.isAuthenticated(r)
|
|
|
|
page, err := app.pages.Get(authenticated, slug)
|
|
if err != nil {
|
|
if errors.Is(err, models.ErrNoRecord) {
|
|
http.NotFound(w, r)
|
|
} else {
|
|
app.serverError(w, r, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if page.Menu {
|
|
page.ChildrenMenu, err = app.pages.GetChildrenMenu(authenticated, page.ID)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
if page.Name == "Journal" {
|
|
sort.Slice(page.ChildrenMenu, func(i, j int) bool {
|
|
return page.ChildrenMenu[i].Created.After(page.ChildrenMenu[j].Created)
|
|
})
|
|
}
|
|
}
|
|
|
|
page.Html = kaku.Parse(page.Body).Render()
|
|
data := app.newTemplateData(r)
|
|
data.Page = page
|
|
|
|
breadCrumb, err := app.pages.BuildBreadCrumb(data.Page.Host)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
data.Page.BreadCrumb = breadCrumb
|
|
app.render(w, r, http.StatusOK, "view.tmpl", data)
|
|
}
|
|
|
|
// Serve the /page/create GET route
|
|
func (app *application) pageCreate(w http.ResponseWriter, r *http.Request) {
|
|
pages, err := app.pages.NamesAndSlugs()
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
data := app.newTemplateData(r)
|
|
data.Form = pageCreateForm{}
|
|
data.Pages = pages
|
|
data.Page.Name = "New page"
|
|
app.render(w, r, http.StatusOK, "create.tmpl", data)
|
|
}
|
|
|
|
// Serve the page/create POST route
|
|
func (app *application) pageCreatePost(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Decode the form
|
|
var form pageCreateForm
|
|
err = app.decodePostForm(r, &form)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check if another page already uses this slug
|
|
slug := slug.Make(r.PostForm.Get("name"))
|
|
slugIsFree, err := app.pages.SlugIsFree(slug, 0)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank")
|
|
form.CheckField(validator.SlugIsFree(slugIsFree), "name", "A page already exists with this title")
|
|
form.CheckField(validator.NotBlank(form.Body), "body", "This field cannot be blank")
|
|
form.CheckField(validator.NotBlank(form.Summary), "summary", "This field cannot be blank")
|
|
// form.CheckField(validator.NotBlankInt(form.Host), "host", "This field cannot be blank")
|
|
form.CheckField(validator.NotBlank(form.Category), "category", "This field cannot be blank")
|
|
|
|
if !form.Valid() {
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
pages, err := app.pages.NamesAndSlugs()
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
data.Pages = pages
|
|
app.render(w, r, http.StatusUnprocessableEntity, "create.tmpl", data)
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
created, err := time.Parse("2006-01-02T15:04", form.Created)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
err = app.pages.Insert(form.Name, slug, form.Host, form.Category, form.Summary, form.Body, created, now, form.Private, form.Menu, form.Feed)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
app.sessionManager.Put(r.Context(), "flash", "Page successfully created!")
|
|
|
|
http.Redirect(w, r, "/"+slug, http.StatusSeeOther)
|
|
}
|
|
|
|
// Serve the page/edit GET route
|
|
func (app *application) pageEdit(w http.ResponseWriter, r *http.Request) {
|
|
slug := r.PathValue("slug")
|
|
if slug == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
pages, err := app.pages.NamesAndSlugs()
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
authenticated := app.isAuthenticated(r)
|
|
|
|
page, err := app.pages.Get(authenticated, slug)
|
|
if err != nil {
|
|
if errors.Is(err, models.ErrNoRecord) {
|
|
http.NotFound(w, r)
|
|
} else {
|
|
app.serverError(w, r, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
var form pageEditForm
|
|
form.Name = page.Name
|
|
form.Summary = page.Summary
|
|
form.Body = page.Body
|
|
form.Created = page.Created.Format("2006-01-02T15:04")
|
|
form.Host = page.Host
|
|
form.Category = page.Category
|
|
form.Private = page.Private
|
|
form.Feed = page.Feed
|
|
form.Menu = page.Menu
|
|
form.Id = page.ID
|
|
form.Slug = slug
|
|
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
data.Pages = pages
|
|
data.Page.Name = "Edit page"
|
|
|
|
app.render(w, r, http.StatusOK, "edit.tmpl", data)
|
|
}
|
|
|
|
func (app *application) PageEditPost(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var form pageEditForm
|
|
err = app.decodePostForm(r, &form)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
slug := slug.Make(r.PostForm.Get("name"))
|
|
slugIsFree, err := app.pages.SlugIsFree(slug, form.Id)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank")
|
|
form.CheckField(validator.SlugIsFree(slugIsFree), "name", "A page already exists with this title")
|
|
form.CheckField(validator.NotBlank(form.Body), "body", "This field cannot be blank")
|
|
form.CheckField(validator.NotBlank(form.Summary), "summary", "This field cannot be blank")
|
|
// form.CheckField(validator.NotBlankInt(form.Host), "host", "This field cannot be blank")
|
|
form.CheckField(validator.NotBlank(form.Category), "category", "This field cannot be blank")
|
|
|
|
if !form.Valid() {
|
|
data := app.newTemplateData(r)
|
|
// form.Slug = pathSlug
|
|
data.Form = form
|
|
app.render(w, r, http.StatusUnprocessableEntity, "edit.tmpl", data)
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
created, err := time.Parse("2006-01-02T15:04", form.Created)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
err = app.pages.Update(form.Name, slug, form.Host, form.Category, form.Summary, form.Body, created, now, form.Private, form.Menu, form.Feed, form.Id)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
app.sessionManager.Put(r.Context(), "flash", "Page successfully updated!")
|
|
|
|
http.Redirect(w, r, "/"+slug, http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
func (app *application) PageDeletePost(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil || id < 1 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
err = r.ParseForm()
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Decode the form
|
|
var form pageCreateForm
|
|
err = app.decodePostForm(r, &form)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var redirect = ""
|
|
|
|
if form.Host != 0 {
|
|
hostPage, err := app.pages.GetHostSlug(form.Host)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
redirect = hostPage.Slug
|
|
}
|
|
|
|
err = app.pages.Delete(id)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
app.sessionManager.Put(r.Context(), "flash", "Children page successfully deleted!")
|
|
http.Redirect(w, r, "/"+redirect, http.StatusSeeOther)
|
|
}
|
|
|
|
//----- USER ROUTES ------ //
|
|
|
|
func (app *application) userSignup(w http.ResponseWriter, r *http.Request) {
|
|
// If a user already exists, refuse to serve the page
|
|
exists, err := app.users.AnyUserExist()
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
if exists {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
data := app.newTemplateData(r)
|
|
data.Form = userSignupForm{}
|
|
app.render(w, r, http.StatusOK, "signup.tmpl", data)
|
|
}
|
|
|
|
func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) {
|
|
// If a user already exists, refuse to serve the page
|
|
exists, err := app.users.AnyUserExist()
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
if exists {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
var form userSignupForm
|
|
|
|
err = app.decodePostForm(r, &form)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank")
|
|
form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
|
|
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
|
|
form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
|
|
form.CheckField(validator.MinChars(form.Password, 12), "password", "This field must be at least 12 characters long")
|
|
form.CheckField(validator.MaxBytes(form.Password, 72), "password", "This field must not be more than 72 bytes long")
|
|
|
|
if !form.Valid() {
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
app.render(w, r, http.StatusUnprocessableEntity, "signup.tmpl", data)
|
|
return
|
|
}
|
|
|
|
err = app.users.Insert(form.Name, form.Email, form.Password)
|
|
if err != nil {
|
|
if errors.Is(err, models.ErrDuplicateEmail) {
|
|
form.AddFieldError("email", "Email address is already used")
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
app.render(w, r, http.StatusUnprocessableEntity, "signup.tmpl", data)
|
|
} else {
|
|
app.serverError(w, r, err)
|
|
|
|
}
|
|
return
|
|
}
|
|
|
|
app.sessionManager.Put(r.Context(), "flash", "Your signup was successful. You can now log in.")
|
|
|
|
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
func (app *application) userLogin(w http.ResponseWriter, r *http.Request) {
|
|
data := app.newTemplateData(r)
|
|
data.Form = userLoginInForm{}
|
|
app.render(w, r, http.StatusOK, "login.tmpl", data)
|
|
}
|
|
|
|
func (app *application) userLoginPost(w http.ResponseWriter, r *http.Request) {
|
|
var form userLoginInForm
|
|
|
|
err := app.decodePostForm(r, &form)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
|
|
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
|
|
form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
|
|
|
|
if !form.Valid() {
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
app.render(w, r, http.StatusUnprocessableEntity, "login.tmpl", data)
|
|
return
|
|
}
|
|
|
|
id, err := app.users.Authenticate(form.Email, form.Password)
|
|
if err != nil {
|
|
if errors.Is(err, models.ErrInvalidCredentials) {
|
|
form.AddNonFieldError("Email or password is incorrect")
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
app.render(w, r, http.StatusUnprocessableEntity, "login.tmpl", data)
|
|
} else {
|
|
app.serverError(w, r, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
err = app.sessionManager.RenewToken(r.Context())
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
app.sessionManager.Put(r.Context(), "authenticatedUserID", id)
|
|
|
|
path := app.sessionManager.PopString(r.Context(), "redirectPathAfterLogin")
|
|
|
|
if path != "" {
|
|
http.Redirect(w, r, path, http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) userLogoutPost(w http.ResponseWriter, r *http.Request) {
|
|
err := app.sessionManager.RenewToken(r.Context())
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
app.sessionManager.Remove(r.Context(), "authenticatedUserID")
|
|
app.sessionManager.Put(r.Context(), "flash", "You've been logged out successfully!")
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) accountPasswordUpdate(w http.ResponseWriter, r *http.Request) {
|
|
data := app.newTemplateData(r)
|
|
data.Form = accountPasswordUpdateForm{}
|
|
|
|
app.render(w, r, http.StatusOK, "password.tmpl", data)
|
|
}
|
|
|
|
func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) {
|
|
var form accountPasswordUpdateForm
|
|
|
|
err := app.decodePostForm(r, &form)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
form.CheckField(validator.NotBlank(form.CurrentPassword), "currentPassword", "This field cannot be blank")
|
|
form.CheckField(validator.NotBlank(form.NewPassword), "newPassword", "This field cannot be blank")
|
|
form.CheckField(validator.MinChars(form.NewPassword, 12), "newPassword", "This field must be at least 12 characters long")
|
|
form.CheckField(validator.MaxBytes(form.NewPassword, 72), "password", "This field must not be more than 72 bytes long")
|
|
form.CheckField(validator.NotBlank(form.NewPasswordConfirmation), "newPasswordConfirmation", "This field cannot be blank")
|
|
form.CheckField(form.NewPassword == form.NewPasswordConfirmation, "newPasswordConfirmation", "Passwords do not match")
|
|
|
|
if !form.Valid() {
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
|
|
app.render(w, r, http.StatusUnprocessableEntity, "password.tmpl", data)
|
|
return
|
|
}
|
|
|
|
userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")
|
|
|
|
err = app.users.PasswordUpdate(userID, form.CurrentPassword, form.NewPassword)
|
|
if err != nil {
|
|
if errors.Is(err, models.ErrInvalidCredentials) {
|
|
form.AddFieldError("currentPassword", "Current password is incorrect")
|
|
|
|
data := app.newTemplateData(r)
|
|
data.Form = form
|
|
|
|
app.render(w, r, http.StatusUnprocessableEntity, "password.tmpl", data)
|
|
} else {
|
|
app.serverError(w, r, err)
|
|
}
|
|
return
|
|
}
|
|
app.sessionManager.Put(r.Context(), "flash", "Your password has been updated!")
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) uploadFiles(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
files := r.MultipartForm.File["files"]
|
|
if len(files) == 0 {
|
|
app.clientErrorMessage(w, "0 files", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filesDir := "./uploaded"
|
|
processor := NewImageProcessor()
|
|
var sanitizedFilenames []string
|
|
|
|
for _, fileHeader := range files {
|
|
safeFilename := app.sanitizeFilename(fileHeader.Filename)
|
|
|
|
if err := app.processFile(fileHeader, safeFilename, filesDir, processor); err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
extension := strings.ToLower(filepath.Ext(safeFilename))
|
|
if extension == ".heif" || extension == ".heic" {
|
|
baseName := strings.TrimSuffix(safeFilename, filepath.Ext(safeFilename))
|
|
safeFilename = baseName + ".jpg"
|
|
}
|
|
sanitizedFilenames = append(sanitizedFilenames, safeFilename)
|
|
|
|
}
|
|
|
|
// Return the sanitized filenames as JSON
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string][]string{"filenames": sanitizedFilenames})
|
|
}
|
|
|
|
func (app *application) deleteFile(w http.ResponseWriter, r *http.Request) {
|
|
slug := r.PathValue("slug")
|
|
if slug == "" {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filePath := filepath.Join("./uploaded", slug)
|
|
|
|
// Validate path is within uploaded directory
|
|
if !strings.HasPrefix(filepath.Clean(filePath), filepath.Clean("./uploaded")) {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Try to delete the original file directly
|
|
if err := os.Remove(filePath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
http.NotFound(w, r)
|
|
} else {
|
|
app.serverError(w, r, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// If it's an image file, also delete processed versions from processed folder
|
|
if app.isImageFile(slug) {
|
|
baseFilename := strings.TrimSuffix(slug, filepath.Ext(slug))
|
|
|
|
// Try to delete processed versions directly (ignore errors)
|
|
os.Remove(filepath.Join("./processed", baseFilename+".jpg"))
|
|
os.Remove(filepath.Join("./processed", baseFilename+".webp"))
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("File deleted successfully"))
|
|
}
|
|
|
|
func (app *application) prewiewText(w http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
parsed := kaku.Parse(string(body)).Render()
|
|
w.Write([]byte(parsed))
|
|
}
|
|
|
|
func (app *application) setTheme(w http.ResponseWriter, r *http.Request) {
|
|
theme := r.URL.Query().Get("theme")
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "theme",
|
|
Value: theme,
|
|
Path: "/",
|
|
MaxAge: 60 * 60 * 24 * 365,
|
|
SameSite: http.SameSiteLaxMode,
|
|
Secure: true,
|
|
HttpOnly: false,
|
|
})
|
|
|
|
next := r.URL.Query().Get("next")
|
|
if !app.isSafeLocalPath(next) {
|
|
next = "/"
|
|
}
|
|
http.Redirect(w, r, next, http.StatusSeeOther)
|
|
}
|