UI work, config.js stuff
This commit is contained in:
parent
2b833413cf
commit
d982ce13f5
18 changed files with 173 additions and 131 deletions
|
@ -26,7 +26,7 @@ type fileCache struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFileCache(dir string, totalSizeLimit int64, fileSizeLimit int64) (*fileCache, error) {
|
func newFileCache(dir string, totalSizeLimit int64) (*fileCache, error) {
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,6 @@ func newFileCache(dir string, totalSizeLimit int64, fileSizeLimit int64) (*fileC
|
||||||
dir: dir,
|
dir: dir,
|
||||||
totalSizeCurrent: size,
|
totalSizeCurrent: size,
|
||||||
totalSizeLimit: totalSizeLimit,
|
totalSizeLimit: totalSizeLimit,
|
||||||
fileSizeLimit: fileSizeLimit,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +54,7 @@ func (c *fileCache) Write(id string, in io.Reader, limiters ...util.Limiter) (in
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
limiters = append(limiters, util.NewFixedLimiter(c.Remaining()), util.NewFixedLimiter(c.fileSizeLimit))
|
limiters = append(limiters, util.NewFixedLimiter(c.Remaining()))
|
||||||
limitWriter := util.NewLimitWriter(f, limiters...)
|
limitWriter := util.NewLimitWriter(f, limiters...)
|
||||||
size, err := io.Copy(limitWriter, in)
|
size, err := io.Copy(limitWriter, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -36,15 +36,17 @@ import (
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO
|
TODO
|
||||||
|
use token auth in "SubscribeDialog"
|
||||||
|
upload files based on user limit
|
||||||
publishXHR + poll should pick current user, not from userManager
|
publishXHR + poll should pick current user, not from userManager
|
||||||
expire tokens
|
expire tokens
|
||||||
auto-refresh tokens from UI
|
auto-refresh tokens from UI
|
||||||
reserve topics
|
reserve topics
|
||||||
rate limit for signup (2 per 24h)
|
rate limit for signup (2 per 24h)
|
||||||
handle invalid session token
|
handle invalid session token
|
||||||
update disallowed topics
|
|
||||||
purge accounts that were not logged into in X
|
purge accounts that were not logged into in X
|
||||||
sync subscription display name
|
sync subscription display name
|
||||||
|
store users
|
||||||
Pages:
|
Pages:
|
||||||
- Home
|
- Home
|
||||||
- Password reset
|
- Password reset
|
||||||
|
@ -103,7 +105,7 @@ var (
|
||||||
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", "app", "settings"} // If updated, also update in Android app
|
disallowedTopics = []string{"docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"} // If updated, also update in Android and web app
|
||||||
urlRegex = regexp.MustCompile(`^https?://`)
|
urlRegex = regexp.MustCompile(`^https?://`)
|
||||||
|
|
||||||
//go:embed site
|
//go:embed site
|
||||||
|
@ -152,7 +154,7 @@ func New(conf *Config) (*Server, error) {
|
||||||
}
|
}
|
||||||
var fileCache *fileCache
|
var fileCache *fileCache
|
||||||
if conf.AttachmentCacheDir != "" {
|
if conf.AttachmentCacheDir != "" {
|
||||||
fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit, conf.AttachmentFileSizeLimit)
|
fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -423,9 +425,13 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||||
w.Header().Set("Content-Type", "text/javascript")
|
w.Header().Set("Content-Type", "text/javascript")
|
||||||
_, err := io.WriteString(w, fmt.Sprintf(`// Generated server configuration
|
_, err := io.WriteString(w, fmt.Sprintf(`// Generated server configuration
|
||||||
var config = {
|
var config = {
|
||||||
|
baseUrl: window.location.origin,
|
||||||
appRoot: "%s",
|
appRoot: "%s",
|
||||||
disallowedTopics: [%s]
|
enableLogin: %t,
|
||||||
};`, appRoot, disallowedTopicsStr))
|
enableSignup: %t,
|
||||||
|
enableResetPassword: %t,
|
||||||
|
disallowedTopics: [%s],
|
||||||
|
};`, appRoot, s.config.EnableLogin, s.config.EnableSignup, s.config.EnableResetPassword, disallowedTopicsStr))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -799,7 +805,12 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
||||||
if m.Message == "" {
|
if m.Message == "" {
|
||||||
m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)
|
m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)
|
||||||
}
|
}
|
||||||
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(stats.AttachmentTotalSizeRemaining))
|
limiters := []util.Limiter{
|
||||||
|
v.BandwidthLimiter(),
|
||||||
|
util.NewFixedLimiter(stats.AttachmentFileSizeLimit),
|
||||||
|
util.NewFixedLimiter(stats.AttachmentTotalSizeRemaining),
|
||||||
|
}
|
||||||
|
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...)
|
||||||
if err == util.ErrLimitReached {
|
if err == util.ErrLimitReached {
|
||||||
return errHTTPEntityTooLargeAttachmentTooLarge
|
return errHTTPEntityTooLargeAttachmentTooLarge
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
// Configuration injected by the ntfy server.
|
// THIS FILE IS JUST AN EXAMPLE
|
||||||
//
|
//
|
||||||
// This file is just an example. It is removed during the build process.
|
// It is removed during the build process. The actual config is dynamically
|
||||||
// The actual config is dynamically generated server-side.
|
// generated server-side and served by the ntfy server.
|
||||||
|
//
|
||||||
|
// During web development, you may change values here for rapid testing.
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
|
baseUrl: "http://localhost:2586", // window.location.origin FIXME update before merging
|
||||||
appRoot: "/app",
|
appRoot: "/app",
|
||||||
disallowedTopics: ["docs", "static", "file", "app", "settings"]
|
enableLogin: true,
|
||||||
|
enableSignup: true,
|
||||||
|
enableResetPassword: false,
|
||||||
|
disallowedTopics: ["docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"]
|
||||||
};
|
};
|
||||||
|
|
|
@ -144,7 +144,9 @@
|
||||||
"account_type_default": "Default",
|
"account_type_default": "Default",
|
||||||
"account_type_unlimited": "Unlimited",
|
"account_type_unlimited": "Unlimited",
|
||||||
"account_type_none": "None",
|
"account_type_none": "None",
|
||||||
"account_type_hobbyist": "Hobbyist",
|
"account_type_pro": "Pro",
|
||||||
|
"account_type_business": "Business",
|
||||||
|
"account_type_business_plus": "Business Plus",
|
||||||
"prefs_notifications_title": "Notifications",
|
"prefs_notifications_title": "Notifications",
|
||||||
"prefs_notifications_sound_title": "Notification sound",
|
"prefs_notifications_sound_title": "Notification sound",
|
||||||
"prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",
|
"prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",
|
||||||
|
|
|
@ -125,7 +125,9 @@ class Api {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: maybeWithBasicAuth({}, user)
|
headers: maybeWithBasicAuth({}, user)
|
||||||
});
|
});
|
||||||
if (response.status !== 200) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
return false;
|
||||||
|
} else if (response.status !== 200) {
|
||||||
throw new Error(`Unexpected server response ${response.status}`);
|
throw new Error(`Unexpected server response ${response.status}`);
|
||||||
}
|
}
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
|
@ -47,7 +47,7 @@ export const disallowedTopic = (topic) => {
|
||||||
export const topicDisplayName = (subscription) => {
|
export const topicDisplayName = (subscription) => {
|
||||||
if (subscription.displayName) {
|
if (subscription.displayName) {
|
||||||
return subscription.displayName;
|
return subscription.displayName;
|
||||||
} else if (subscription.baseUrl === window.location.origin) {
|
} else if (subscription.baseUrl === config.baseUrl) {
|
||||||
return subscription.topic;
|
return subscription.topic;
|
||||||
}
|
}
|
||||||
return topicShortUrl(subscription.baseUrl, subscription.topic);
|
return topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||||
|
|
|
@ -147,7 +147,7 @@ const ChangePassword = () => {
|
||||||
};
|
};
|
||||||
const handleDialogSubmit = async (newPassword) => {
|
const handleDialogSubmit = async (newPassword) => {
|
||||||
try {
|
try {
|
||||||
await api.changePassword("http://localhost:2586", session.token(), newPassword);
|
await api.changePassword(config.baseUrl, session.token(), newPassword);
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
console.debug(`[Account] Password changed`);
|
console.debug(`[Account] Password changed`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -230,7 +230,7 @@ const DeleteAccount = () => {
|
||||||
};
|
};
|
||||||
const handleDialogSubmit = async (newPassword) => {
|
const handleDialogSubmit = async (newPassword) => {
|
||||||
try {
|
try {
|
||||||
await api.deleteAccount("http://localhost:2586", session.token());
|
await api.deleteAccount(config.baseUrl, session.token());
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
console.debug(`[Account] Account deleted`);
|
console.debug(`[Account] Account deleted`);
|
||||||
// TODO delete local storage
|
// TODO delete local storage
|
||||||
|
|
|
@ -118,7 +118,7 @@ const SettingsIcons = (props) => {
|
||||||
handleClose(event);
|
handleClose(event);
|
||||||
await subscriptionManager.remove(props.subscription.id);
|
await subscriptionManager.remove(props.subscription.id);
|
||||||
if (session.exists() && props.subscription.remoteId) {
|
if (session.exists() && props.subscription.remoteId) {
|
||||||
await api.deleteAccountSubscription("http://localhost:2586", session.token(), props.subscription.remoteId);
|
await api.deleteAccountSubscription(config.baseUrl, session.token(), props.subscription.remoteId);
|
||||||
}
|
}
|
||||||
const newSelected = await subscriptionManager.first(); // May be undefined
|
const newSelected = await subscriptionManager.first(); // May be undefined
|
||||||
if (newSelected) {
|
if (newSelected) {
|
||||||
|
@ -259,9 +259,8 @@ const ProfileIcon = (props) => {
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await api.logout("http://localhost:2586"/*window.location.origin*/, session.token());
|
await api.logout(config.baseUrl, session.token());
|
||||||
session.reset();
|
session.reset();
|
||||||
window.location.href = routes.app;
|
window.location.href = routes.app;
|
||||||
};
|
};
|
||||||
|
@ -273,11 +272,11 @@ const ProfileIcon = (props) => {
|
||||||
<AccountCircleIcon/>
|
<AccountCircleIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
{!session.exists() &&
|
{!session.exists() && config.enableLogin &&
|
||||||
<>
|
<Button color="inherit" variant="text" onClick={() => navigate(routes.login)} sx={{m: 1}}>Sign in</Button>
|
||||||
<Button color="inherit" variant="outlined" onClick={() => navigate(routes.login)}>Sign in</Button>
|
}
|
||||||
|
{!session.exists() && config.enableSignup &&
|
||||||
<Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)}>Sign up</Button>
|
<Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)}>Sign up</Button>
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
|
|
|
@ -87,7 +87,7 @@ const Layout = () => {
|
||||||
const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0;
|
const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0;
|
||||||
const [selected] = (subscriptions || []).filter(s => {
|
const [selected] = (subscriptions || []).filter(s => {
|
||||||
return (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic)
|
return (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic)
|
||||||
|| (window.location.origin === s.baseUrl && params.topic === s.topic)
|
|| (config.baseUrl === s.baseUrl && params.topic === s.topic)
|
||||||
});
|
});
|
||||||
|
|
||||||
useConnectionListeners(subscriptions, users);
|
useConnectionListeners(subscriptions, users);
|
||||||
|
@ -96,7 +96,7 @@ const Layout = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const acc = await api.getAccount("http://localhost:2586", session.token());
|
const acc = await api.getAccount(config.baseUrl, session.token());
|
||||||
if (acc) {
|
if (acc) {
|
||||||
setAccount(acc);
|
setAccount(acc);
|
||||||
if (acc.language) {
|
if (acc.language) {
|
||||||
|
|
29
web/src/components/AvatarBox.js
Normal file
29
web/src/components/AvatarBox.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {Avatar} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import logo from "../img/ntfy2.svg";
|
||||||
|
|
||||||
|
const AvatarBox = (props) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }}
|
||||||
|
src={logo}
|
||||||
|
variant="rounded"
|
||||||
|
/>
|
||||||
|
{props.children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarBox;
|
|
@ -1,17 +1,20 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Checkbox, FormControlLabel, Grid, Link} from "@mui/material";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import api from "../app/Api";
|
import api from "../app/Api";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
import logo from "../img/ntfy2.svg";
|
|
||||||
import {NavLink} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
|
import AvatarBox from "./AvatarBox";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [error, setError] = useState("");
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const data = new FormData(event.currentTarget);
|
const data = new FormData(event.currentTarget);
|
||||||
|
@ -19,31 +22,36 @@ const Login = () => {
|
||||||
username: data.get('username'),
|
username: data.get('username'),
|
||||||
password: data.get('password'),
|
password: data.get('password'),
|
||||||
}
|
}
|
||||||
const token = await api.login("http://localhost:2586"/*window.location.origin*/, user);
|
try {
|
||||||
console.log(`[Api] User auth for user ${user.username} successful, token is ${token}`);
|
const token = await api.login(config.baseUrl, user);
|
||||||
|
if (token) {
|
||||||
|
console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`);
|
||||||
session.store(user.username, token);
|
session.store(user.username, token);
|
||||||
window.location.href = routes.app;
|
window.location.href = routes.app;
|
||||||
|
} else {
|
||||||
|
console.log(`[Login] User auth for user ${user.username} failed, access denied`);
|
||||||
|
setError(t("Login failed: Invalid username or password"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[Login] User auth for user ${user.username} failed`, e);
|
||||||
|
if (e && e.message) {
|
||||||
|
setError(e.message);
|
||||||
|
} else {
|
||||||
|
setError(t("Unknown error. Check logs for details."))
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
if (!config.enableLogin) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<AvatarBox>
|
||||||
sx={{
|
<Typography sx={{ typography: 'h6' }}>{t("Login is disabled")}</Typography>
|
||||||
display: 'flex',
|
</AvatarBox>
|
||||||
flexGrow: 1,
|
);
|
||||||
justifyContent: 'center',
|
}
|
||||||
flexDirection: 'column',
|
return (
|
||||||
alignContent: 'center',
|
<AvatarBox>
|
||||||
alignItems: 'center',
|
|
||||||
height: '100vh'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }}
|
|
||||||
src={logo}
|
|
||||||
variant="rounded"
|
|
||||||
/>
|
|
||||||
<Typography sx={{ typography: 'h6' }}>
|
<Typography sx={{ typography: 'h6' }}>
|
||||||
Sign in to your ntfy account
|
{t("Sign in to your ntfy account")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}>
|
<Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -51,7 +59,7 @@ const Login = () => {
|
||||||
required
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
id="username"
|
id="username"
|
||||||
label="Username"
|
label={t("Username")}
|
||||||
name="username"
|
name="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
@ -60,7 +68,7 @@ const Login = () => {
|
||||||
required
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
name="password"
|
name="password"
|
||||||
label="Password"
|
label={t("Password")}
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
|
@ -71,14 +79,25 @@ const Login = () => {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{mt: 2, mb: 2}}
|
sx={{mt: 2, mb: 2}}
|
||||||
>
|
>
|
||||||
Sign in
|
{t("Sign in")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{error &&
|
||||||
|
<Box sx={{
|
||||||
|
mb: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<WarningAmberIcon color="error" sx={{mr: 1}}/>
|
||||||
|
<Typography sx={{color: 'error.main'}}>{error}</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
<Box sx={{width: "100%"}}>
|
<Box sx={{width: "100%"}}>
|
||||||
<div style={{float: "left"}}><NavLink to={routes.resetPassword} variant="body1">Reset password</NavLink></div>
|
{config.enableResetPassword && <div style={{float: "left"}}><NavLink to={routes.resetPassword} variant="body1">{t("Reset password")}</NavLink></div>}
|
||||||
<div style={{float: "right"}}><NavLink to={routes.signup} variant="body1">Sign up</NavLink></div>
|
{config.enableSignup && <div style={{float: "right"}}><NavLink to={routes.signup} variant="body1">{t("Sign up")}</NavLink></div>}
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</AvatarBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ const Messaging = (props) => {
|
||||||
<PublishDialog
|
<PublishDialog
|
||||||
key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
|
key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
|
||||||
openMode={dialogOpenMode}
|
openMode={dialogOpenMode}
|
||||||
baseUrl={subscription?.baseUrl ?? window.location.origin}
|
baseUrl={subscription?.baseUrl ?? config.baseUrl}
|
||||||
topic={subscription?.topic ?? ""}
|
topic={subscription?.topic ?? ""}
|
||||||
message={message}
|
message={message}
|
||||||
onClose={handleDialogClose}
|
onClose={handleDialogClose}
|
||||||
|
|
|
@ -73,7 +73,7 @@ const Sound = () => {
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await prefs.setSound(ev.target.value);
|
await prefs.setSound(ev.target.value);
|
||||||
if (session.exists()) {
|
if (session.exists()) {
|
||||||
await api.updateAccountSettings("http://localhost:2586", session.token(), {
|
await api.updateAccountSettings(config.baseUrl, session.token(), {
|
||||||
notification: {
|
notification: {
|
||||||
sound: ev.target.value
|
sound: ev.target.value
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ const MinPriority = () => {
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await prefs.setMinPriority(ev.target.value);
|
await prefs.setMinPriority(ev.target.value);
|
||||||
if (session.exists()) {
|
if (session.exists()) {
|
||||||
await api.updateAccountSettings("http://localhost:2586", session.token(), {
|
await api.updateAccountSettings(config.baseUrl, session.token(), {
|
||||||
notification: {
|
notification: {
|
||||||
min_priority: ev.target.value
|
min_priority: ev.target.value
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ const DeleteAfter = () => {
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await prefs.setDeleteAfter(ev.target.value);
|
await prefs.setDeleteAfter(ev.target.value);
|
||||||
if (session.exists()) {
|
if (session.exists()) {
|
||||||
await api.updateAccountSettings("http://localhost:2586", session.token(), {
|
await api.updateAccountSettings(config.baseUrl, session.token(), {
|
||||||
notification: {
|
notification: {
|
||||||
delete_after: ev.target.value
|
delete_after: ev.target.value
|
||||||
}
|
}
|
||||||
|
@ -467,7 +467,7 @@ const Language = () => {
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await i18n.changeLanguage(ev.target.value);
|
await i18n.changeLanguage(ev.target.value);
|
||||||
if (session.exists()) {
|
if (session.exists()) {
|
||||||
await api.updateAccountSettings("http://localhost:2586", session.token(), {
|
await api.updateAccountSettings(config.baseUrl, session.token(), {
|
||||||
language: ev.target.value
|
language: ev.target.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Link} from "@mui/material";
|
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import api from "../app/Api";
|
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import session from "../app/Session";
|
|
||||||
import logo from "../img/ntfy2.svg";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import {NavLink} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
|
import AvatarBox from "./AvatarBox";
|
||||||
|
|
||||||
const ResetPassword = () => {
|
const ResetPassword = () => {
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
|
@ -16,22 +13,7 @@ const ResetPassword = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<AvatarBox>
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexGrow: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '100vh'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }}
|
|
||||||
src={logo}
|
|
||||||
variant="rounded"
|
|
||||||
/>
|
|
||||||
<Typography sx={{ typography: 'h6' }}>
|
<Typography sx={{ typography: 'h6' }}>
|
||||||
Reset password
|
Reset password
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -59,7 +41,7 @@ const ResetPassword = () => {
|
||||||
< Return to sign in
|
< Return to sign in
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</AvatarBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,52 +1,41 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Link} from "@mui/material";
|
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import api from "../app/Api";
|
import api from "../app/Api";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
import logo from "../img/ntfy2.svg";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import {NavLink} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
|
import AvatarBox from "./AvatarBox";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
|
||||||
const Signup = () => {
|
const Signup = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const data = new FormData(event.currentTarget);
|
const data = new FormData(event.currentTarget);
|
||||||
const username = data.get('username');
|
|
||||||
const password = data.get('password');
|
|
||||||
const user = {
|
const user = {
|
||||||
username: username,
|
username: data.get('username'),
|
||||||
password: password
|
password: data.get('password')
|
||||||
}; // FIXME omg so awful
|
};
|
||||||
|
await api.createAccount(config.baseUrl, user.username, user.password);
|
||||||
await api.createAccount("http://localhost:2586"/*window.location.origin*/, username, password);
|
const token = await api.login(config.baseUrl, user);
|
||||||
const token = await api.login("http://localhost:2586"/*window.location.origin*/, user);
|
|
||||||
console.log(`[Api] User auth for user ${user.username} successful, token is ${token}`);
|
console.log(`[Api] User auth for user ${user.username} successful, token is ${token}`);
|
||||||
session.store(user.username, token);
|
session.store(user.username, token);
|
||||||
window.location.href = routes.app;
|
window.location.href = routes.app;
|
||||||
};
|
};
|
||||||
|
if (!config.enableSignup) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<AvatarBox>
|
||||||
sx={{
|
<Typography sx={{ typography: 'h6' }}>{t("Signup is disabled")}</Typography>
|
||||||
display: 'flex',
|
</AvatarBox>
|
||||||
flexGrow: 1,
|
);
|
||||||
justifyContent: 'center',
|
}
|
||||||
flexDirection: 'column',
|
return (
|
||||||
alignContent: 'center',
|
<AvatarBox>
|
||||||
alignItems: 'center',
|
|
||||||
height: '100vh'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }}
|
|
||||||
src={logo}
|
|
||||||
variant="rounded"
|
|
||||||
/>
|
|
||||||
<Typography sx={{ typography: 'h6' }}>
|
<Typography sx={{ typography: 'h6' }}>
|
||||||
Create a ntfy account
|
{t("Create a ntfy account")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}>
|
<Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -83,15 +72,17 @@ const Signup = () => {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{mt: 2, mb: 2}}
|
sx={{mt: 2, mb: 2}}
|
||||||
>
|
>
|
||||||
Sign up
|
{t("Sign up")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
{config.enableLogin &&
|
||||||
<Typography sx={{mb: 4}}>
|
<Typography sx={{mb: 4}}>
|
||||||
<NavLink to={routes.login} variant="body1">
|
<NavLink to={routes.login} variant="body1">
|
||||||
Already have an account? Sign in!
|
{t("Already have an account? Sign in!")}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
}
|
||||||
|
</AvatarBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,10 @@ const SubscribeDialog = (props) => {
|
||||||
const [showLoginPage, setShowLoginPage] = useState(false);
|
const [showLoginPage, setShowLoginPage] = useState(false);
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const handleSuccess = async () => {
|
const handleSuccess = async () => {
|
||||||
const actualBaseUrl = (baseUrl) ? baseUrl : window.location.origin;
|
const actualBaseUrl = (baseUrl) ? baseUrl : config.baseUrl;
|
||||||
const subscription = await subscriptionManager.add(actualBaseUrl, topic);
|
const subscription = await subscriptionManager.add(actualBaseUrl, topic);
|
||||||
if (session.exists()) {
|
if (session.exists()) {
|
||||||
const remoteSubscription = await api.addAccountSubscription("http://localhost:2586", session.token(), {
|
const remoteSubscription = await api.addAccountSubscription(config.baseUrl, session.token(), {
|
||||||
base_url: actualBaseUrl,
|
base_url: actualBaseUrl,
|
||||||
topic: topic
|
topic: topic
|
||||||
});
|
});
|
||||||
|
@ -63,11 +63,11 @@ const SubscribePage = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
|
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
|
||||||
const [errorText, setErrorText] = useState("");
|
const [errorText, setErrorText] = useState("");
|
||||||
const baseUrl = (anotherServerVisible) ? props.baseUrl : window.location.origin;
|
const baseUrl = (anotherServerVisible) ? props.baseUrl : config.baseUrl;
|
||||||
const topic = props.topic;
|
const topic = props.topic;
|
||||||
const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
|
const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
|
||||||
const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
|
const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
|
||||||
.filter(s => s !== window.location.origin);
|
.filter(s => s !== config.baseUrl);
|
||||||
const handleSubscribe = async () => {
|
const handleSubscribe = async () => {
|
||||||
const user = await userManager.get(baseUrl); // May be undefined
|
const user = await userManager.get(baseUrl); // May be undefined
|
||||||
const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
|
const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
|
||||||
|
@ -94,7 +94,7 @@ const SubscribePage = (props) => {
|
||||||
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
|
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
|
||||||
return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
|
return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
|
||||||
} else {
|
} else {
|
||||||
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(window.location.origin, topic));
|
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.baseUrl, topic));
|
||||||
return validTopic(topic) && !isExistingTopicUrl;
|
return validTopic(topic) && !isExistingTopicUrl;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -152,7 +152,7 @@ const SubscribePage = (props) => {
|
||||||
renderInput={ (params) =>
|
renderInput={ (params) =>
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
placeholder={window.location.origin}
|
placeholder={config.baseUrl}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
aria-label={t("subscribe_dialog_subscribe_base_url_label")}
|
aria-label={t("subscribe_dialog_subscribe_base_url_label")}
|
||||||
/>
|
/>
|
||||||
|
@ -172,7 +172,7 @@ const LoginPage = (props) => {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [errorText, setErrorText] = useState("");
|
const [errorText, setErrorText] = useState("");
|
||||||
const baseUrl = (props.baseUrl) ? props.baseUrl : window.location.origin;
|
const baseUrl = (props.baseUrl) ? props.baseUrl : config.baseUrl;
|
||||||
const topic = props.topic;
|
const topic = props.topic;
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
const user = {baseUrl, username, password};
|
const user = {baseUrl, username, password};
|
||||||
|
|
|
@ -59,12 +59,12 @@ export const useAutoSubscribe = (subscriptions, selected) => {
|
||||||
setHasRun(true);
|
setHasRun(true);
|
||||||
const eligible = params.topic && !selected && !disallowedTopic(params.topic);
|
const eligible = params.topic && !selected && !disallowedTopic(params.topic);
|
||||||
if (eligible) {
|
if (eligible) {
|
||||||
const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : window.location.origin;
|
const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : config.baseUrl;
|
||||||
console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
|
console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
|
||||||
(async () => {
|
(async () => {
|
||||||
const subscription = await subscriptionManager.add(baseUrl, params.topic);
|
const subscription = await subscriptionManager.add(baseUrl, params.topic);
|
||||||
if (session.exists()) {
|
if (session.exists()) {
|
||||||
const remoteSubscription = await api.addAccountSubscription("http://localhost:2586", session.token(), {
|
const remoteSubscription = await api.addAccountSubscription(config.baseUrl, session.token(), {
|
||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
topic: params.topic
|
topic: params.topic
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import config from "../app/config";
|
import config from "../app/config";
|
||||||
import {shortUrl} from "../app/utils";
|
import {shortUrl} from "../app/utils";
|
||||||
|
|
||||||
|
// Remember to also update the "disallowedTopics" list!
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
home: "/",
|
home: "/",
|
||||||
pricing: "/pricing",
|
pricing: "/pricing",
|
||||||
|
@ -13,7 +15,7 @@ const routes = {
|
||||||
subscription: "/:topic",
|
subscription: "/:topic",
|
||||||
subscriptionExternal: "/:baseUrl/:topic",
|
subscriptionExternal: "/:baseUrl/:topic",
|
||||||
forSubscription: (subscription) => {
|
forSubscription: (subscription) => {
|
||||||
if (subscription.baseUrl !== window.location.origin) {
|
if (subscription.baseUrl !== config.baseUrl) {
|
||||||
return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`;
|
return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`;
|
||||||
}
|
}
|
||||||
return `/${subscription.topic}`;
|
return `/${subscription.topic}`;
|
||||||
|
|
Loading…
Reference in a new issue