rastrillo / idear Public

Clone
git clone https://amadan.net/rastrillo/idear

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "net/http"
5 "strconv"
6 "strings"
7
8 "github.com/carlosframework/rastrillo/flash"
9
10 "amadan.net/rastrillo/idear"
11)
12
13// boardView is what board.html renders against.
14type boardView struct {
15 Posts []Post
16 CanDelete bool
17}
18
19// board is GET /, mounted inside idear's Require.
20//
21// idear.From(r) is the viewer Require resolved — never nil here, and
22// nil anywhere Require is not mounted, which is the check below. The
23// role is read off that Member and never off the session or a form.
24func (a *app) board(w http.ResponseWriter, r *http.Request) {
25 m := idear.From(r)
26 if m == nil {
27 // Defence in depth: this can only be a mount bug, and it is
28 // answered with the app's own 404 rather than a panic.
29 a.logger.Error("board: no viewer; / must be mounted inside idear's Require")
30 a.renderNotFound(w, r)
31 return
32 }
33 var posts []Post
34 if err := a.db.WithContext(r.Context()).Order("id DESC").Find(&posts).Error; err != nil {
35 a.logger.Error("board: listing posts", "err", err)
36 }
37 a.render(w, r, 0, "board", boardView{
38 Posts: posts,
39 // The template hides the delete button for a plain Member.
40 // The HIDING IS NOT THE ENFORCEMENT — the route itself stacks
41 // RequireRole(RoleAdmin) inside Require (app.go), and that is
42 // what refuses a Member who posts to it anyway.
43 CanDelete: m.Role.AtLeast(idear.RoleAdmin),
44 })
45}
46
47// createPost is POST /posts. Any active member may post.
48//
49// The author comes from the VIEWER, never from the form — the same
50// rule idear applies to role. A form field named author_id would
51// otherwise be a way to write posts under someone else's name.
52func (a *app) createPost(w http.ResponseWriter, r *http.Request) {
53 m := idear.From(r)
54 if m == nil {
55 a.renderNotFound(w, r)
56 return
57 }
58 body := strings.TrimSpace(r.PostFormValue("body"))
59 if body == "" {
60 flash.Set(w, "error", "A post needs some words in it.")
61 http.Redirect(w, r, "/", http.StatusSeeOther)
62 return
63 }
64 p := Post{AuthorID: m.ID, Author: m.Email, Body: body}
65 if err := a.db.WithContext(r.Context()).Create(&p).Error; err != nil {
66 a.logger.Error("createPost", "err", err)
67 flash.Set(w, "error", "Something went wrong. Please try again.")
68 http.Redirect(w, r, "/", http.StatusSeeOther)
69 return
70 }
71 flash.Set(w, "notice", "Posted.")
72 http.Redirect(w, r, "/", http.StatusSeeOther)
73}
74
75// deletePost is POST /posts/{id}/delete — Admin and above, enforced by
76// the RequireRole stacked inside Require in app.go, not here.
77func (a *app) deletePost(w http.ResponseWriter, r *http.Request) {
78 id, err := strconv.ParseInt(chiParam(r, "id"), 10, 64)
79 if err != nil || id <= 0 {
80 // A row that is not there is answered exactly like a row that
81 // never existed: the app's own 404, the same one idear's
82 // non-members get.
83 a.renderNotFound(w, r)
84 return
85 }
86 res := a.db.WithContext(r.Context()).Where("id = ?", id).Delete(&Post{})
87 if res.Error != nil {
88 a.logger.Error("deletePost", "err", res.Error)
89 flash.Set(w, "error", "Something went wrong. Please try again.")
90 http.Redirect(w, r, "/", http.StatusSeeOther)
91 return
92 }
93 if res.RowsAffected == 0 {
94 a.renderNotFound(w, r)
95 return
96 }
97 flash.Set(w, "notice", "Post deleted.")
98 http.Redirect(w, r, "/", http.StatusSeeOther)
99}
100
101// chiParam reads a path wildcard. r.PathValue works because chi v5
102// populates it from its own route context, which is the same source
103// idear's handlers read {id} and {token} from.
104func chiParam(r *http.Request, name string) string { return r.PathValue(name) }
105