| 1 | package main |
| 2 | |
| 3 | import ( |
| 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. |
| 14 | type 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. |
| 24 | func (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. |
| 52 | func (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. |
| 77 | func (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. |
| 104 | func chiParam(r *http.Request, name string) string { return r.PathValue(name) } |
| 105 | |