Răsfoiți Sursa

Embed new web UI into server

Philipp Heckel 4 ani în urmă
părinte
comite
e27d5719f0
38 a modificat fișierele cu 64 adăugiri și 110 ștergeri
  1. 1 0
      .gitignore
  2. 26 3
      Makefile
  3. 24 29
      server/server.go
  4. 0 1
      server/static/img/close.svg
  5. BIN
      server/static/img/favicon.png
  6. BIN
      server/static/img/ntfy.png
  7. 0 44
      web/README.md
  8. 0 3
      web/public/config.js
  9. BIN
      web/public/favicon.ico
  10. 4 6
      web/public/home.html
  11. 3 6
      web/public/index.html
  12. 0 15
      web/public/manifest.json
  13. 0 0
      web/public/static/css/home.css
  14. 0 0
      web/public/static/font/roboto-v29-latin-300.woff
  15. 0 0
      web/public/static/font/roboto-v29-latin-300.woff2
  16. 0 0
      web/public/static/font/roboto-v29-latin-500.woff
  17. 0 0
      web/public/static/font/roboto-v29-latin-500.woff2
  18. 0 0
      web/public/static/font/roboto-v29-latin-regular.woff
  19. 0 0
      web/public/static/font/roboto-v29-latin-regular.woff2
  20. 0 0
      web/public/static/img/android-video-overview.mp4
  21. 0 0
      web/public/static/img/android-video-subscribe-api.mp4
  22. 0 0
      web/public/static/img/badge-appstore.png
  23. 0 0
      web/public/static/img/badge-fdroid.png
  24. 0 0
      web/public/static/img/badge-googleplay.png
  25. 0 0
      web/public/static/img/basic-notification.png
  26. 0 0
      web/public/static/img/screenshot-curl.png
  27. 0 0
      web/public/static/img/screenshot-docs.png
  28. 0 0
      web/public/static/img/screenshot-phone-add.jpg
  29. 0 0
      web/public/static/img/screenshot-phone-detail.jpg
  30. 0 0
      web/public/static/img/screenshot-phone-main.jpg
  31. 0 0
      web/public/static/img/screenshot-phone-notification.jpg
  32. 0 0
      web/public/static/img/screenshot-phone-popover.png
  33. 0 0
      web/public/static/img/screenshot-web-detail.png
  34. 0 0
      web/public/static/js/home.js
  35. 4 1
      web/src/app/config.js
  36. 0 1
      web/src/components/App.js
  37. 1 1
      web/src/components/Notifications.js
  38. 1 0
      web/src/components/SubscribeDialog.js

+ 1 - 0
.gitignore

@@ -2,6 +2,7 @@ dist/
 build/
 .idea/
 server/docs/
+server/site/
 tools/fbsend/fbsend
 playground/
 *.iml

+ 26 - 3
Makefile

@@ -44,6 +44,28 @@ docs-deps: .PHONY
 docs: docs-deps
 	mkdocs build
 
+
+# Web app
+
+web-deps:
+	cd web && npm install
+
+web-build:
+	cd web \
+		&& npm run build \
+		&& mv build/index.html build/app.html \
+		&& rm -rf ../server/site \
+		&& mv build ../server/site \
+		&& rm \
+			../server/site/precache* \
+			../server/site/service-worker.js \
+			../server/site/asset-manifest.json \
+			../server/site/static/js/*.js.map \
+			../server/site/static/js/*.js.LICENSE.txt
+
+web: web-deps web-build
+
+
 # Test/check targets
 
 check: test fmt-check vet lint staticcheck
@@ -94,7 +116,7 @@ staticcheck: .PHONY
 
 # Building targets
 
-build-deps: docs
+build-deps: docs web
 	which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/v7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
 	which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
 
@@ -105,8 +127,9 @@ build-snapshot: build-deps
 	goreleaser build --snapshot --rm-dist --debug
 
 build-simple: clean
-	mkdir -p dist/ntfy_linux_amd64 server/docs
-	touch server/docs/dummy
+	mkdir -p dist/ntfy_linux_amd64 server/docs server/site
+	touch server/docs/index.html
+	touch server/site/app.html
 	export CGO_ENABLED=1
 	go build \
 		-o dist/ntfy_linux_amd64/ntfy \

+ 24 - 29
server/server.go

@@ -13,7 +13,6 @@ import (
 	"golang.org/x/sync/errgroup"
 	"heckel.io/ntfy/auth"
 	"heckel.io/ntfy/util"
-	"html/template"
 	"io"
 	"log"
 	"net"
@@ -61,35 +60,31 @@ type handleFunc func(http.ResponseWriter, *http.Request, *visitor) error
 
 var (
 	// If changed, don't forget to update Android App and auth_sqlite.go
-	topicRegex       = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)  // No /!
-	topicPathRegex   = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
-	jsonPathRegex    = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
-	ssePathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
-	rawPathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
-	wsPathRegex      = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
-	authPathRegex    = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
-	publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
+	topicRegex        = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)               // No /!
+	topicPathRegex    = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`)              // Regex must match JS & Android app!
+	extTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic
+	jsonPathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
+	ssePathRegex      = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
+	rawPathRegex      = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
+	wsPathRegex       = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
+	authPathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
+	publishPathRegex  = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
 
 	staticRegex      = regexp.MustCompile(`^/static/.+`)
 	docsRegex        = regexp.MustCompile(`^/docs(|/.*)$`)
 	fileRegex        = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
-	disallowedTopics = []string{"docs", "static", "file"} // If updated, also update in Android app
+	disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
 	attachURLRegex   = regexp.MustCompile(`^https?://`)
 
-	templateFnMap = template.FuncMap{
-		"durationToHuman": util.DurationToHuman,
-	}
-
-	//go:embed "index.gohtml"
-	indexSource   string
-	indexTemplate = template.Must(template.New("index").Funcs(templateFnMap).Parse(indexSource))
-
 	//go:embed "example.html"
 	exampleSource string
 
-	//go:embed static
-	webStaticFs       embed.FS
-	webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs}
+	//go:embed site
+	webFs        embed.FS
+	webFsCached  = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs}
+	webSiteDir   = "/site"
+	webHomeIndex = "/home.html" // Landing page, only if "web-index: home"
+	webAppIndex  = "/app.html"  // React app
 
 	//go:embed docs
 	docsStaticFs     embed.FS
@@ -284,8 +279,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.limitRequests(s.handleFile)(w, r, v)
 	} else if r.Method == http.MethodOptions {
 		return s.handleOptions(w, r)
-	} else if r.Method == http.MethodGet && topicPathRegex.MatchString(r.URL.Path) {
-		return s.handleTopic(w, r)
 	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
 		return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
 	} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
@@ -300,15 +293,15 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.limitRequests(s.authRead(s.handleSubscribeWS))(w, r, v)
 	} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
 		return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
+	} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || extTopicPathRegex.MatchString(r.URL.Path)) {
+		return s.handleTopic(w, r)
 	}
 	return errHTTPNotFound
 }
 
 func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
-	return indexTemplate.Execute(w, &indexPage{
-		Topic:         r.URL.Path[1:],
-		CacheDuration: s.config.CacheDuration,
-	})
+	r.URL.Path = webHomeIndex
+	return s.handleStatic(w, r)
 }
 
 func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
@@ -319,7 +312,8 @@ func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
 		_, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`+"\n")
 		return err
 	}
-	return s.handleHome(w, r)
+	r.URL.Path = webAppIndex
+	return s.handleStatic(w, r)
 }
 
 func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
@@ -339,7 +333,8 @@ func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error {
 }
 
 func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
-	http.FileServer(http.FS(webStaticFsCached)).ServeHTTP(w, r)
+	r.URL.Path = webSiteDir + r.URL.Path
+	http.FileServer(http.FS(webFsCached)).ServeHTTP(w, r)
 	return nil
 }
 

+ 0 - 1
server/static/img/close.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>

BIN
server/static/img/favicon.png


BIN
server/static/img/ntfy.png


+ 0 - 44
web/README.md

@@ -1,44 +0,0 @@
-# Create React App example
-
-## How to use
-
-Download the example [or clone the repo](https://github.com/mui/material-ui):
-
-<!-- #default-branch-switch -->
-
-```sh
-curl https://codeload.github.com/mui/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/create-react-app
-cd create-react-app
-```
-
-Install it and run:
-
-```sh
-npm install
-npm start
-```
-
-or:
-
-<!-- #default-branch-switch -->
-
-[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/mui/material-ui/tree/master/examples/create-react-app)
-
-<!-- #default-branch-switch -->
-
-[![Edit on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mui/material-ui/tree/master/examples/create-react-app)
-
-## The idea behind the example
-
-<!-- #default-branch-switch -->
-
-This example demonstrates how you can use [Create React App](https://github.com/facebookincubator/create-react-app).
-It includes `@mui/material` and its peer dependencies, including `emotion`, the default style engine in MUI v5.
-If you prefer, you can [use styled-components instead](https://mui.com/guides/interoperability/#styled-components).
-
-## What's next?
-
-<!-- #default-branch-switch -->
-
-You now have a working example project.
-You can head back to the documentation, continuing browsing it from the [templates](https://mui.com/getting-started/templates/) section.

+ 0 - 3
web/public/config.js

@@ -1,3 +0,0 @@
-var config = {
-    defaultBaseUrl: 'https://ntfy.sh'
-};

BIN
web/public/favicon.ico


+ 4 - 6
server/index.gohtml → web/public/home.html

@@ -1,11 +1,10 @@
-{{- /*gotype: heckel.io/ntfy/server.indexPage*/ -}}
 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
 
     <title>ntfy.sh | Send push notifications to your phone via PUT/POST</title>
-    <link rel="stylesheet" href="static/css/app.css" type="text/css">
+    <link rel="stylesheet" href="static/css/home.css" type="text/css">
 
     <!-- Mobile view -->
     <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
@@ -37,9 +36,9 @@
         <div id="name">ntfy</div>
         <ol>
             <li><a href="docs/">Getting started</a></li>
+            <li><a href="app">Web app</a></li>
             <li><a href="docs/subscribe/phone/">Android/iOS</a></li>
             <li><a href="docs/publish/">API</a></li>
-            <li><a href="docs/install/">Self-hosting</a></li>
             <li><a href="https://github.com/binwiederhier/ntfy">GitHub</a></li>
         </ol>
     </div>
@@ -90,7 +89,7 @@
         Here's what that looks like in the <a href="docs/subscribe/phone/">Android app</a>:
     </p>
     <figure>
-        <img src="static/img/priority-notification.png" style="max-height: 200px"/>
+        <img src="static/img/screenshot-phone-popover.png" style="max-height: 200px"/>
         <figcaption>Urgent notification with pop-over</figcaption>
     </figure>
 
@@ -170,7 +169,6 @@
     <center id="ironicCenterTagDontFreakOut"><i>Made with ❤️ by <a href="https://heckel.io">Philipp C. Heckel</a></i></center>
 </div>
 <div id="lightbox" class="lightbox"></div>
-<script src="static/js/emoji.js"></script>
-<script src="static/js/app.js"></script>
+<script src="static/js/home.js"></script>
 </body>
 </html>

+ 3 - 6
web/public/index.html

@@ -20,18 +20,15 @@
   <!-- Previews in Google, Slack, WhatsApp, etc. -->
   <meta property="og:type" content="website" />
   <meta property="og:locale" content="en_US" />
-  <meta property="og:site_name" content="ntfy.sh" />
-  <meta property="og:title" content="ntfy.sh | Send push notifications to your phone or desktop via PUT/POST" />
-  <meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
+  <meta property="og:site_name" content="ntfy web" />
+  <meta property="og:title" content="ntfy web | Web app to receive push notifications from scripts via PUT/POST" />
+  <meta property="og:description" content="ntfy lets you send push notifications via scripts from any computer or phone, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
   <meta property="og:image" content="%PUBLIC_URL%/static/img/ntfy.png" />
   <meta property="og:url" content="https://ntfy.sh" />
 
   <!-- Never index -->
   <meta name="robots" content="noindex, nofollow" />
 
-  <!-- Server configuration -->
-  <script src="%PUBLIC_URL%/config.js"></script>
-
   <!-- FIXME Roboto -->
   <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
 </head>

+ 0 - 15
web/public/manifest.json

@@ -1,15 +0,0 @@
-{
-  "short_name": "Your Orders",
-  "name": "Your Orders",
-  "icons": [
-    {
-      "src": "favicon.ico",
-      "sizes": "64x64 32x32 24x24 16x16",
-      "type": "image/x-icon"
-    }
-  ],
-  "start_url": ".",
-  "display": "standalone",
-  "theme_color": "#000000",
-  "background_color": "#ffffff"
-}

+ 0 - 0
server/static/css/app.css → web/public/static/css/home.css


+ 0 - 0
server/static/font/roboto-v29-latin-300.woff → web/public/static/font/roboto-v29-latin-300.woff


+ 0 - 0
server/static/font/roboto-v29-latin-300.woff2 → web/public/static/font/roboto-v29-latin-300.woff2


+ 0 - 0
server/static/font/roboto-v29-latin-500.woff → web/public/static/font/roboto-v29-latin-500.woff


+ 0 - 0
server/static/font/roboto-v29-latin-500.woff2 → web/public/static/font/roboto-v29-latin-500.woff2


+ 0 - 0
server/static/font/roboto-v29-latin-regular.woff → web/public/static/font/roboto-v29-latin-regular.woff


+ 0 - 0
server/static/font/roboto-v29-latin-regular.woff2 → web/public/static/font/roboto-v29-latin-regular.woff2


+ 0 - 0
server/static/img/android-video-overview.mp4 → web/public/static/img/android-video-overview.mp4


+ 0 - 0
server/static/img/android-video-subscribe-api.mp4 → web/public/static/img/android-video-subscribe-api.mp4


+ 0 - 0
server/static/img/badge-appstore.png → web/public/static/img/badge-appstore.png


+ 0 - 0
server/static/img/badge-fdroid.png → web/public/static/img/badge-fdroid.png


+ 0 - 0
server/static/img/badge-googleplay.png → web/public/static/img/badge-googleplay.png


+ 0 - 0
server/static/img/basic-notification.png → web/public/static/img/basic-notification.png


+ 0 - 0
server/static/img/screenshot-curl.png → web/public/static/img/screenshot-curl.png


+ 0 - 0
server/static/img/screenshot-docs.png → web/public/static/img/screenshot-docs.png


+ 0 - 0
server/static/img/screenshot-phone-add.jpg → web/public/static/img/screenshot-phone-add.jpg


+ 0 - 0
server/static/img/screenshot-phone-detail.jpg → web/public/static/img/screenshot-phone-detail.jpg


+ 0 - 0
server/static/img/screenshot-phone-main.jpg → web/public/static/img/screenshot-phone-main.jpg


+ 0 - 0
server/static/img/screenshot-phone-notification.jpg → web/public/static/img/screenshot-phone-notification.jpg


+ 0 - 0
server/static/img/priority-notification.png → web/public/static/img/screenshot-phone-popover.png


+ 0 - 0
server/static/img/screenshot-web-detail.png → web/public/static/img/screenshot-web-detail.png


+ 0 - 0
server/static/js/app.js → web/public/static/js/home.js


+ 4 - 1
web/src/app/config.js

@@ -1,2 +1,5 @@
-const config = window.config;
+//const config = window.config;
+const config = {
+    defaultBaseUrl: "https://ntfy.sh"
+};
 export default config;

+ 0 - 1
web/src/components/App.js

@@ -21,7 +21,6 @@ import {BrowserRouter, Route, Routes, useLocation, useNavigate} from "react-rout
 import {subscriptionRoute} from "../app/utils";
 
 // TODO support unsubscribed routes
-// TODO embed into ntfy server
 // TODO googlefonts
 // TODO new notification indicator
 // TODO sound

+ 1 - 1
web/src/components/Notifications.js

@@ -251,7 +251,7 @@ const NothingHereYet = (props) => {
     return (
         <VerticallyCenteredContainer maxWidth="xs">
             <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
-                <img src="static/img/ntfy-outline.svg" height="64" width="64" alt="No notifications"/><br />
+                <img src="/static/img/ntfy-outline.svg" height="64" width="64" alt="No notifications"/><br />
                 You haven't received any notifications for this topic yet.
             </Typography>
             <Paragraph>

+ 1 - 0
web/src/components/SubscribeDialog.js

@@ -109,6 +109,7 @@ const SubscribePage = (props) => {
                     margin="dense"
                     id="topic"
                     placeholder="Topic name, e.g. phil_alerts"
+                    inputProps={{ maxLength: 64 }}
                     value={props.topic}
                     onChange={ev => props.setTopic(ev.target.value)}
                     type="text"