diff --git a/cmd/tailpaste/main.go b/cmd/tailpaste/main.go index 1a46ed1..192421a 100644 --- a/cmd/tailpaste/main.go +++ b/cmd/tailpaste/main.go @@ -61,7 +61,7 @@ func main() { q.Set("filename", filepath.Base(*fname)) q.Set("content", string(data)) - req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(q.Encode)) + req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(q.Encode())) if err != nil { log.Fatalf("can't make HTTP request: %v", err) } diff --git a/cmd/web/main.go b/cmd/web/main.go index a871f7f..93f8805 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -12,8 +12,10 @@ import ( "net/http" "os" "path/filepath" + "strconv" "strings" "text/template" + "time" "github.com/google/uuid" "github.com/tailscale/sqlite" @@ -68,8 +70,7 @@ func (s *Server) TailnetIndex(w http.ResponseWriter, r *http.Request) { ui, err := upsertUserInfo(r.Context(), s.db, s.lc, r.RemoteAddr) if err != nil { - log.Printf("%s: error fetching user info: %v", r.RemoteAddr, err) - http.Error(w, "can't fetch user info", http.StatusBadRequest) + s.ShowError(w, r, err, http.StatusInternalServerError) return } @@ -147,6 +148,7 @@ func (s *Server) TailnetSubmitPaste(w http.ResponseWriter, r *http.Request) { q := ` INSERT INTO pastes ( id + , created_at , user_id , filename , data @@ -156,19 +158,20 @@ VALUES , ?2 , ?3 , ?4 + , ?5 )` _, err = s.db.ExecContext( r.Context(), q, id, + time.Now(), userInfo.UserProfile.ID, fname, data, ) if err != nil { - log.Printf("%s: %v", r.RemoteAddr, err) - http.Error(w, err.Error(), http.StatusInternalServerError) + s.ShowError(w, r, err, http.StatusInternalServerError) return } @@ -184,6 +187,126 @@ VALUES } +func (s *Server) TailnetPasteIndex(w http.ResponseWriter, r *http.Request) { + userInfo, err := upsertUserInfo(r.Context(), s.db, s.lc, r.RemoteAddr) + if err != nil { + s.ShowError(w, r, err, http.StatusInternalServerError) + return + } + + _ = userInfo + + type JoinedPasteInfo struct { + ID string `json:"id"` + Filename string `json:"fname"` + CreatedAt string `json:"created_at"` + PasterDisplayName string `json:"created_by"` + } + + q := ` +SELECT p.id + , p.filename + , p.created_at + , u.display_name +FROM pastes p +INNER JOIN users u + ON p.user_id = u.id +LIMIT 25 +OFFSET ?1 +` + + uq := r.URL.Query() + page := uq.Get("page") + if page == "" { + page = "0" + } + + pageNum, err := strconv.Atoi(page) + if err != nil { + log.Printf("%s: invalid ?page: %s: %v", r.RemoteAddr, page, err) + pageNum = 0 + } + + rows, err := s.db.Query(q, clampToZero(pageNum)) + if err != nil { + s.ShowError(w, r, err, http.StatusInternalServerError) + return + } + + jpis := make([]JoinedPasteInfo, 0, 25) + + defer rows.Close() + for rows.Next() { + jpi := JoinedPasteInfo{} + + err := rows.Scan(&jpi.ID, &jpi.Filename, &jpi.CreatedAt, &jpi.PasterDisplayName) + if err != nil { + s.ShowError(w, r, err, http.StatusInternalServerError) + return + } + + jpis = append(jpis, jpi) + } + + if len(jpis) == 0 { + + } + + var prev, next *int + + if pageNum != 0 { + i := pageNum - 1 + prev = &i + } + if len(jpis) == 25 { + i := pageNum + 1 + next = &i + } + + err = s.tmpls.ExecuteTemplate(w, "listpaste.tmpl", struct { + UserInfo *tailcfg.UserProfile + Title string + Pastes []JoinedPasteInfo + Prev *int + Next *int + Page int + }{ + UserInfo: userInfo.UserProfile, + Title: "Pastes", + Pastes: jpis, + Prev: prev, + Next: next, + Page: pageNum, + }) + if err != nil { + log.Printf("%s: %v", r.RemoteAddr, err) + } +} + +func (s *Server) ShowError(w http.ResponseWriter, r *http.Request, err error, code int) { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(code) + + log.Printf("%s: %v", r.RemoteAddr, err) + + if err := s.tmpls.ExecuteTemplate(w, "error.tmpl", struct { + Title, Error string + UserInfo any + }{ + Title: "Oh noes!", + Error: err.Error(), + }); err != nil { + log.Printf("%s: %v", r.RemoteAddr, err) + } +} + +func clampToZero(i int) int { + if i <= 0 { + return 0 + } + return i +} + func (s *Server) ShowPost(w http.ResponseWriter, r *http.Request) { ui, _ := upsertUserInfo(r.Context(), s.db, s.lc, r.RemoteAddr) var up *tailcfg.UserProfile @@ -208,6 +331,7 @@ func (s *Server) ShowPost(w http.ResponseWriter, r *http.Request) { q := ` SELECT p.filename + , p.created_at , p.data , u.id , u.login_name @@ -220,11 +344,11 @@ WHERE p.id = ?1` row := s.db.QueryRowContext(r.Context(), q, id) var fname, data, userID, userLoginName, userDisplayName, userProfilePicURL string + var createdAt string - err := row.Scan(&fname, &data, &userID, &userLoginName, &userDisplayName, &userProfilePicURL) + err := row.Scan(&fname, &createdAt, &data, &userID, &userLoginName, &userDisplayName, &userProfilePicURL) if err != nil { - log.Printf("%s: looking up %s: %v", r.RemoteAddr, id, err) - http.Error(w, fmt.Sprintf("can't find paste %s: %v", id, err), http.StatusInternalServerError) + s.ShowError(w, r, fmt.Errorf("can't find paste %s: %w", id, err), http.StatusInternalServerError) return } @@ -252,6 +376,7 @@ WHERE p.id = ?1` err = s.tmpls.ExecuteTemplate(w, "showpaste.tmpl", struct { UserInfo *tailcfg.UserProfile Title string + CreatedAt string PasterDisplayName string PasterProfilePicURL string ID string @@ -259,6 +384,7 @@ WHERE p.id = ?1` }{ UserInfo: up, Title: fname, + CreatedAt: createdAt, PasterDisplayName: userDisplayName, PasterProfilePicURL: userProfilePicURL, ID: id, @@ -309,6 +435,7 @@ func main() { tailnetMux := http.NewServeMux() tailnetMux.Handle("/static/", http.FileServer(http.FS(staticFiles))) tailnetMux.HandleFunc("/paste/", srv.ShowPost) + tailnetMux.HandleFunc("/paste/list", srv.TailnetPasteIndex) tailnetMux.HandleFunc("/api/post", srv.TailnetSubmitPaste) tailnetMux.HandleFunc("/", srv.TailnetIndex) diff --git a/cmd/web/schema.sql b/cmd/web/schema.sql index f55c216..44a1435 100644 --- a/cmd/web/schema.sql +++ b/cmd/web/schema.sql @@ -4,6 +4,7 @@ PRAGMA journal_mode=WAL; -- Paste data CREATE TABLE IF NOT EXISTS pastes ( id TEXT PRIMARY KEY NOT NULL + , created_at TEXT NOT NULL -- RFC 3339 timestamp , user_id TEXT NOT NULL , filename TEXT NOT NULL , data TEXT NOT NULL diff --git a/cmd/web/tmpl/base.tmpl b/cmd/web/tmpl/base.tmpl index ee2e893..7246b9b 100644 --- a/cmd/web/tmpl/base.tmpl +++ b/cmd/web/tmpl/base.tmpl @@ -9,7 +9,7 @@
diff --git a/cmd/web/tmpl/create.tmpl b/cmd/web/tmpl/create.tmpl index adc8e93..74b3e52 100644 --- a/cmd/web/tmpl/create.tmpl +++ b/cmd/web/tmpl/create.tmpl @@ -6,4 +6,8 @@ + +
+
+See all pastes {{template "footer" .}} diff --git a/cmd/web/tmpl/error.tmpl b/cmd/web/tmpl/error.tmpl new file mode 100644 index 0000000..8b81703 --- /dev/null +++ b/cmd/web/tmpl/error.tmpl @@ -0,0 +1,9 @@ +{{template "header" .}} +Oh noes! There was an error when trying to do this thing: + +
+{{.Error}}
+
+ +This is almost certainly not what was anticipated, and if you think you are seeing this in error, please contact your administrator or Tailscale support for help with the program itself. +{{template "footer" .}} diff --git a/cmd/web/tmpl/listpaste.tmpl b/cmd/web/tmpl/listpaste.tmpl new file mode 100644 index 0000000..24ed5fe --- /dev/null +++ b/cmd/web/tmpl/listpaste.tmpl @@ -0,0 +1,11 @@ +{{template "header" .}} + + + +

{{if .Prev}}Prev - {{end}} Page {{.Page}} {{if .Next}} - Next{{end}}

+ +{{template "footer" .}} diff --git a/cmd/web/tmpl/notfound.tmpl b/cmd/web/tmpl/notfound.tmpl index e3b833b..24a9671 100644 --- a/cmd/web/tmpl/notfound.tmpl +++ b/cmd/web/tmpl/notfound.tmpl @@ -1,3 +1,3 @@ {{template "header" .}} -

The URL you requested could not be found. Please check your URL and hang up to try your call again. +

The URL you requested could not be found. Please check your URL and hang up to try your call again.

{{template "footer" .}} diff --git a/cmd/web/tmpl/showpaste.tmpl b/cmd/web/tmpl/showpaste.tmpl index 2b77342..7d2aeed 100644 --- a/cmd/web/tmpl/showpaste.tmpl +++ b/cmd/web/tmpl/showpaste.tmpl @@ -1,6 +1,8 @@ {{template "header" .}} -
{{.PasterDisplayName}}
+
+ +

Created at {{.CreatedAt}} by {{.PasterDisplayName}}


 {{.Data}}