1
0
Fork 0
mirror of https://github.com/binwiederhier/ntfy.git synced 2024-12-26 11:42:29 +01:00

Embed new web UI into server

This commit is contained in:
Philipp Heckel 2022-03-05 20:24:10 -05:00
parent 1a3816c1ff
commit e27d5719f0
38 changed files with 64 additions and 110 deletions

1
.gitignore vendored
View file

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

View file

@ -44,6 +44,28 @@ docs-deps: .PHONY
docs: docs-deps docs: docs-deps
mkdocs build 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 # Test/check targets
check: test fmt-check vet lint staticcheck check: test fmt-check vet lint staticcheck
@ -94,7 +116,7 @@ staticcheck: .PHONY
# Building targets # 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 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; } 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 goreleaser build --snapshot --rm-dist --debug
build-simple: clean build-simple: clean
mkdir -p dist/ntfy_linux_amd64 server/docs mkdir -p dist/ntfy_linux_amd64 server/docs server/site
touch server/docs/dummy touch server/docs/index.html
touch server/site/app.html
export CGO_ENABLED=1 export CGO_ENABLED=1
go build \ go build \
-o dist/ntfy_linux_amd64/ntfy \ -o dist/ntfy_linux_amd64/ntfy \

View file

@ -13,7 +13,6 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"heckel.io/ntfy/auth" "heckel.io/ntfy/auth"
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
"html/template"
"io" "io"
"log" "log"
"net" "net"
@ -61,35 +60,31 @@ type handleFunc func(http.ResponseWriter, *http.Request, *visitor) error
var ( var (
// If changed, don't forget to update Android App and auth_sqlite.go // If changed, don't forget to update Android App and auth_sqlite.go
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No /! 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! 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$`) extTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic
ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`) jsonPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`) ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`) rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`) wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`) 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/.+`) staticRegex = regexp.MustCompile(`^/static/.+`)
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`) docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`) 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?://`) 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" //go:embed "example.html"
exampleSource string exampleSource string
//go:embed static //go:embed site
webStaticFs embed.FS webFs embed.FS
webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs} 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 //go:embed docs
docsStaticFs embed.FS 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) return s.limitRequests(s.handleFile)(w, r, v)
} else if r.Method == http.MethodOptions { } else if r.Method == http.MethodOptions {
return s.handleOptions(w, r) 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) { } 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) return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) { } 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) return s.limitRequests(s.authRead(s.handleSubscribeWS))(w, r, v)
} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v) 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 return errHTTPNotFound
} }
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error { func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
return indexTemplate.Execute(w, &indexPage{ r.URL.Path = webHomeIndex
Topic: r.URL.Path[1:], return s.handleStatic(w, r)
CacheDuration: s.config.CacheDuration,
})
} }
func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error { 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") _, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`+"\n")
return err 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 { 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 { 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 return nil
} }

View file

@ -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>

Before

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -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.

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

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

View file

@ -20,18 +20,15 @@
<!-- Previews in Google, Slack, WhatsApp, etc. --> <!-- Previews in Google, Slack, WhatsApp, etc. -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta property="og:site_name" content="ntfy.sh" /> <meta property="og:site_name" content="ntfy web" />
<meta property="og:title" content="ntfy.sh | Send push notifications to your phone or desktop via PUT/POST" /> <meta property="og:title" content="ntfy web | Web app to receive push notifications from scripts 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: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:image" content="%PUBLIC_URL%/static/img/ntfy.png" />
<meta property="og:url" content="https://ntfy.sh" /> <meta property="og:url" content="https://ntfy.sh" />
<!-- Never index --> <!-- Never index -->
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<!-- Server configuration -->
<script src="%PUBLIC_URL%/config.js"></script>
<!-- FIXME Roboto --> <!-- FIXME Roboto -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
</head> </head>

View file

@ -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"
}

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 297 KiB

View file

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View file

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 227 KiB

View file

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 225 KiB

View file

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View file

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 224 KiB

View file

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 270 KiB

View file

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View file

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

View file

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

View file

@ -251,7 +251,7 @@ const NothingHereYet = (props) => {
return ( return (
<VerticallyCenteredContainer maxWidth="xs"> <VerticallyCenteredContainer maxWidth="xs">
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}> <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. You haven't received any notifications for this topic yet.
</Typography> </Typography>
<Paragraph> <Paragraph>

View file

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