Embed new web UI into server
1
.gitignore
vendored
|
@ -2,6 +2,7 @@ dist/
|
|||
build/
|
||||
.idea/
|
||||
server/docs/
|
||||
server/site/
|
||||
tools/fbsend/fbsend
|
||||
playground/
|
||||
*.iml
|
||||
|
|
29
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 \
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.5 KiB |
|
@ -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.
|
|
@ -1,3 +0,0 @@
|
|||
var config = {
|
||||
defaultBaseUrl: 'https://ntfy.sh'
|
||||
};
|
Before Width: | Height: | Size: 3.8 KiB |
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 297 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 227 KiB |
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 225 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 270 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
@ -1,2 +1,5 @@
|
|||
const config = window.config;
|
||||
//const config = window.config;
|
||||
const config = {
|
||||
defaultBaseUrl: "https://ntfy.sh"
|
||||
};
|
||||
export default config;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|