From cf085e93b7abc53b8ecc78c07210384e0ff8d516 Mon Sep 17 00:00:00 2001 From: Ewy~ Date: Tue, 31 Mar 2026 17:18:35 +0200 Subject: initial commit --- pages/create.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++ pages/create.gohtml | 22 ++++++++++++++ pages/get.go | 53 ++++++++++++++++++++++++++++++++ pages/landing.go | 19 ++++++++++++ pages/landing.gohtml | 40 +++++++++++++++++++++++++ pages/receive.go | 5 ++++ 6 files changed, 224 insertions(+) create mode 100644 pages/create.go create mode 100644 pages/create.gohtml create mode 100644 pages/get.go create mode 100644 pages/landing.go create mode 100644 pages/landing.gohtml create mode 100644 pages/receive.go (limited to 'pages') diff --git a/pages/create.go b/pages/create.go new file mode 100644 index 0000000..03f4159 --- /dev/null +++ b/pages/create.go @@ -0,0 +1,85 @@ +package pages + +import ( + "delayed.link/storage" + _ "embed" + "html/template" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +func CreateLink(w http.ResponseWriter, r *http.Request) *storage.Link { + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return nil + } + form := r.PostForm + + minutesInFutureStr := form.Get("minutes_in_future") + if strings.TrimSpace(minutesInFutureStr) == "" { + http.Error(w, "minutes_in_future is required", http.StatusBadRequest) + return nil + } + minutesInFuture, err := strconv.Atoi(minutesInFutureStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return nil + } + if minutesInFuture < 0 { + http.Error(w, "minutes_in_future has to be > 0", http.StatusBadRequest) + } + numberOfOpensStr := form.Get("number_of_opens") + if strings.TrimSpace(minutesInFutureStr) == "" { + http.Error(w, "number_of_opens is required", http.StatusBadRequest) + return nil + } + numberOfOpens, err := strconv.Atoi(numberOfOpensStr) + if numberOfOpens < 0 || numberOfOpens > 100 { + http.Error(w, "number_of_opens needs to be between 1 and 100", http.StatusBadRequest) + return nil + } + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return nil + } + payload := form.Get("payload") + //TODO: Verify payload + if _, err := url.Parse(payload); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return nil + } + + l := storage.Link{ + Target: payload, + OpensFrom: time.Now().Add(time.Duration(minutesInFuture) * time.Minute), + OpensLeft: numberOfOpens, + } + + id, err := l.Save(storage.Current) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return nil + } + (&l).Id = id + + return &l +} + +//go:embed create.gohtml +var recContent string +var recTmpl = template.Must(template.New("delayed.link").Parse(recContent)) + +func Create(w http.ResponseWriter, r *http.Request) { + l := CreateLink(w, r) + if l == nil { + return + } + err := recTmpl.Execute(w, l) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/pages/create.gohtml b/pages/create.gohtml new file mode 100644 index 0000000..ac9890c --- /dev/null +++ b/pages/create.gohtml @@ -0,0 +1,22 @@ + + + + + + + + delayed.link: created + + +

+ This link will unlock at {{.OpensFrom}} +

+

+ It will be able to be opened {{ .OpensLeft }} times. +

+

+ This page is completely ephemeral. Once you close it, you cannot access the link again. +

+ + \ No newline at end of file diff --git a/pages/get.go b/pages/get.go new file mode 100644 index 0000000..7f7df2f --- /dev/null +++ b/pages/get.go @@ -0,0 +1,53 @@ +package pages + +import ( + "delayed.link/storage" + _ "embed" + "errors" + "fmt" + "github.com/google/uuid" + "net/http" + "time" +) + +func Get(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + uid, err := uuid.Parse(id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + l, err := storage.Current.Load(uid) + if err != nil && errors.Is(err, storage.NotFoundError) { + // if not found error, return 404 + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } else if err != nil { + // else return internal error + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + valid := l.Valid() + if errors.Is(valid, storage.NotFoundError) { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if errors.Is(valid, storage.NoOpensError) { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if errors.Is(valid, storage.NotYetOpenError) { + http.Error(w, http.StatusText(http.StatusLocked), http.StatusLocked) + msg := fmt.Sprintf("\n\nThis link will be usable in %v minutes.", int(l.OpensFrom.Sub(time.Now()).Minutes())) + w.Write([]byte(msg)) + return + } + + err = l.Use() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + http.Redirect(w, r, l.Target, http.StatusFound) +} diff --git a/pages/landing.go b/pages/landing.go new file mode 100644 index 0000000..d8c4f54 --- /dev/null +++ b/pages/landing.go @@ -0,0 +1,19 @@ +package pages + +import ( + _ "embed" + "html/template" + "net/http" +) + +//go:embed landing.gohtml +var landContent string +var landTmpl = template.Must(template.New("delayed.link").Parse(landContent)) + +func Land(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + err := landTmpl.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/pages/landing.gohtml b/pages/landing.gohtml new file mode 100644 index 0000000..ceb55d9 --- /dev/null +++ b/pages/landing.gohtml @@ -0,0 +1,40 @@ + + + + + + delayed.link + + + +
+
+ + + + +
+
+ + + \ No newline at end of file diff --git a/pages/receive.go b/pages/receive.go new file mode 100644 index 0000000..023a20b --- /dev/null +++ b/pages/receive.go @@ -0,0 +1,5 @@ +package pages + +import ( + _ "embed" +) -- cgit v1.3