package main import ( "net/http" "strconv" "strings" "github.com/carlosframework/rastrillo/flash" "amadan.net/rastrillo/idear" ) // boardView is what board.html renders against. type boardView struct { Posts []Post CanDelete bool } // board is GET /, mounted inside idear's Require. // // idear.From(r) is the viewer Require resolved — never nil here, and // nil anywhere Require is not mounted, which is the check below. The // role is read off that Member and never off the session or a form. func (a *app) board(w http.ResponseWriter, r *http.Request) { m := idear.From(r) if m == nil { // Defence in depth: this can only be a mount bug, and it is // answered with the app's own 404 rather than a panic. a.logger.Error("board: no viewer; / must be mounted inside idear's Require") a.renderNotFound(w, r) return } var posts []Post if err := a.db.WithContext(r.Context()).Order("id DESC").Find(&posts).Error; err != nil { a.logger.Error("board: listing posts", "err", err) } a.render(w, r, 0, "board", boardView{ Posts: posts, // The template hides the delete button for a plain Member. // The HIDING IS NOT THE ENFORCEMENT — the route itself stacks // RequireRole(RoleAdmin) inside Require (app.go), and that is // what refuses a Member who posts to it anyway. CanDelete: m.Role.AtLeast(idear.RoleAdmin), }) } // createPost is POST /posts. Any active member may post. // // The author comes from the VIEWER, never from the form — the same // rule idear applies to role. A form field named author_id would // otherwise be a way to write posts under someone else's name. func (a *app) createPost(w http.ResponseWriter, r *http.Request) { m := idear.From(r) if m == nil { a.renderNotFound(w, r) return } body := strings.TrimSpace(r.PostFormValue("body")) if body == "" { flash.Set(w, "error", "A post needs some words in it.") http.Redirect(w, r, "/", http.StatusSeeOther) return } p := Post{AuthorID: m.ID, Author: m.Email, Body: body} if err := a.db.WithContext(r.Context()).Create(&p).Error; err != nil { a.logger.Error("createPost", "err", err) flash.Set(w, "error", "Something went wrong. Please try again.") http.Redirect(w, r, "/", http.StatusSeeOther) return } flash.Set(w, "notice", "Posted.") http.Redirect(w, r, "/", http.StatusSeeOther) } // deletePost is POST /posts/{id}/delete — Admin and above, enforced by // the RequireRole stacked inside Require in app.go, not here. func (a *app) deletePost(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chiParam(r, "id"), 10, 64) if err != nil || id <= 0 { // A row that is not there is answered exactly like a row that // never existed: the app's own 404, the same one idear's // non-members get. a.renderNotFound(w, r) return } res := a.db.WithContext(r.Context()).Where("id = ?", id).Delete(&Post{}) if res.Error != nil { a.logger.Error("deletePost", "err", res.Error) flash.Set(w, "error", "Something went wrong. Please try again.") http.Redirect(w, r, "/", http.StatusSeeOther) return } if res.RowsAffected == 0 { a.renderNotFound(w, r) return } flash.Set(w, "notice", "Post deleted.") http.Redirect(w, r, "/", http.StatusSeeOther) } // chiParam reads a path wildcard. r.PathValue works because chi v5 // populates it from its own route context, which is the same source // idear's handlers read {id} and {token} from. func chiParam(r *http.Request, name string) string { return r.PathValue(name) }