UI work, config.js stuff

This commit is contained in:
binwiederhier 2022-12-21 13:19:07 -05:00
parent 2b833413cf
commit d982ce13f5
18 changed files with 173 additions and 131 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

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

View file

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

View file

@ -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> }
<Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)}>Sign up</Button> {!session.exists() && config.enableSignup &&
</> <Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)}>Sign up</Button>
} }
<Menu <Menu
anchorEl={anchorEl} anchorEl={anchorEl}

View file

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

View 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;

View file

@ -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);
session.store(user.username, token); if (token) {
window.location.href = routes.app; console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`);
session.store(user.username, token);
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 (
<AvatarBox>
<Typography sx={{ typography: 'h6' }}>{t("Login is disabled")}</Typography>
</AvatarBox>
);
}
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' }}>
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>
); );
} }

View file

@ -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}
@ -83,7 +83,7 @@ const MessageBar = (props) => {
margin="dense" margin="dense"
placeholder={t("message_bar_type_message")} placeholder={t("message_bar_type_message")}
aria-label={t("message_bar_type_message")} aria-label={t("message_bar_type_message")}
role="textbox" role="textbox"
type="text" type="text"
fullWidth fullWidth
variant="standard" variant="standard"

View file

@ -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
}); });
} }

View file

@ -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 = () => {
&lt; Return to sign in &lt; Return to sign in
</NavLink> </NavLink>
</Typography> </Typography>
</Box> </AvatarBox>
); );
} }

View file

@ -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 (
<AvatarBox>
<Typography sx={{ typography: 'h6' }}>{t("Signup is disabled")}</Typography>
</AvatarBox>
);
}
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' }}>
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>
<Typography sx={{mb: 4}}> {config.enableLogin &&
<NavLink to={routes.login} variant="body1"> <Typography sx={{mb: 4}}>
Already have an account? Sign in! <NavLink to={routes.login} variant="body1">
</NavLink> {t("Already have an account? Sign in!")}
</Typography> </NavLink>
</Box> </Typography>
}
</AvatarBox>
); );
} }

View file

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

View file

@ -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
}); });

View file

@ -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}`;