diff --git a/cmd/serve.go b/cmd/serve.go index cbfb141..8847f2c 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -79,7 +79,7 @@ var flagsServe = append( altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "xxx"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "xxx"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-payments", Aliases: []string{"enable_payments"}, EnvVars: []string{"NTFY_ENABLE_PAYMENTS"}, Value: false, Usage: "xxx"}), - altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reserve-topics", Aliases: []string{"enable_reserve_topics"}, EnvVars: []string{"NTFY_ENABLE_RESERVE_TOPICS"}, Value: false, Usage: "xxx"}), + altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "xxx"}), ) var cmdServe = &cli.Command{ @@ -151,7 +151,7 @@ func execServe(c *cli.Context) error { enableSignup := c.Bool("enable-signup") enableLogin := c.Bool("enable-login") enablePayments := c.Bool("enable-payments") - enableReserveTopics := c.Bool("enable-reserve-topics") + enableReservations := c.Bool("enable-reservations") // Check values if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) { @@ -188,7 +188,7 @@ func execServe(c *cli.Context) error { return errors.New("if upstream-base-url is set, base-url must also be set") } else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL { return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications") - } else if authFile == "" && (enableSignup || enableLogin || enableReserveTopics || enablePayments) { + } else if authFile == "" && (enableSignup || enableLogin || enableReservations || enablePayments) { return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or enable-payments if auth-file is not set") } @@ -284,7 +284,7 @@ func execServe(c *cli.Context) error { conf.EnableSignup = enableSignup conf.EnableLogin = enableLogin conf.EnablePayments = enablePayments - conf.EnableReserveTopics = enableReserveTopics + conf.EnableReservations = enableReservations conf.Version = c.App.Version // Set up hot-reloading of config diff --git a/go.sum b/go.sum index 12ffac0..0ad0057 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,6 @@ github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8b github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8= github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8= -github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -172,8 +170,6 @@ google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4Ho google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70= -google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= diff --git a/server/config.go b/server/config.go index 9879650..2037403 100644 --- a/server/config.go +++ b/server/config.go @@ -110,7 +110,7 @@ type Config struct { EnableEmailConfirm bool EnablePasswordReset bool EnablePayments bool - EnableReserveTopics bool // Allow users with role "user" to own/reserve topics + EnableReservations bool // Allow users with role "user" to own/reserve topics Version string // injected by App } diff --git a/server/server.go b/server/server.go index 5776080..bc26737 100644 --- a/server/server.go +++ b/server/server.go @@ -44,7 +44,6 @@ import ( UI: - flicker of upgrade banner - JS constants - - useContext for account Sync: - "account topic" sync mechanism - "mute" setting @@ -58,9 +57,7 @@ import ( Refactor: - rename /access -> /reservation Later: - - Password reset - Pricing - - change email */ // Server is the main server, providing the UI and API for ntfy @@ -457,10 +454,10 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi EnableSignup: s.config.EnableSignup, EnablePasswordReset: s.config.EnablePasswordReset, EnablePayments: s.config.EnablePayments, - EnableReserveTopics: s.config.EnableReserveTopics, + EnableReservations: s.config.EnableReservations, DisallowedTopics: disallowedTopics, } - b, err := json.Marshal(response) + b, err := json.MarshalIndent(response, "", " ") if err != nil { return err } diff --git a/server/types.go b/server/types.go index e766171..21022f3 100644 --- a/server/types.go +++ b/server/types.go @@ -292,6 +292,6 @@ type apiConfigResponse struct { EnableSignup bool `json:"enable_signup"` EnablePasswordReset bool `json:"enable_password_reset"` EnablePayments bool `json:"enable_payments"` - EnableReserveTopics bool `json:"enable_reserve_topics"` + EnableReservations bool `json:"enable_reservations"` DisallowedTopics []string `json:"disallowed_topics"` } diff --git a/web/public/config.js b/web/public/config.js index df73585..ffcc383 100644 --- a/web/public/config.js +++ b/web/public/config.js @@ -12,6 +12,6 @@ var config = { enable_signup: true, enable_password_reset: false, enable_payments: true, - enable_reserve_topics: true, + enable_reservations: true, disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"] }; diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 8f41fa7..0efc0a1 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -243,6 +243,7 @@ "prefs_appearance_language_title": "Language", "prefs_reservations_title": "Reserved topics", "prefs_reservations_description": "You can reserve topic names for personal use here. Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.", + "prefs_reservations_limit_reached": "You reached your reserved topics limit.", "prefs_reservations_add_button": "Add reserved topic", "prefs_reservations_edit_button": "Edit topic access", "prefs_reservations_delete_button": "Reset topic access", diff --git a/web/src/components/Account.js b/web/src/components/Account.js index c08e0d3..734577f 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -1,6 +1,6 @@ import * as React from 'react'; -import {useState} from 'react'; -import {LinearProgress, Link, Stack, useMediaQuery} from "@mui/material"; +import {useContext, useState} from 'react'; +import {LinearProgress, Stack, useMediaQuery} from "@mui/material"; import Tooltip from '@mui/material/Tooltip'; import Typography from "@mui/material/Typography"; import EditIcon from '@mui/icons-material/Edit'; @@ -18,7 +18,6 @@ import TextField from "@mui/material/TextField"; import DialogActions from "@mui/material/DialogActions"; import routes from "./routes"; import IconButton from "@mui/material/IconButton"; -import {useOutletContext} from "react-router-dom"; import {formatBytes} from "../app/utils"; import accountApi, {UnauthorizedError} from "../app/AccountApi"; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; @@ -28,6 +27,7 @@ import i18n from "i18next"; import humanizeDuration from "humanize-duration"; import UpgradeDialog from "./UpgradeDialog"; import CelebrationIcon from "@mui/icons-material/Celebration"; +import {AccountContext} from "./App"; const Account = () => { if (!session.exists()) { @@ -62,7 +62,7 @@ const Basics = () => { const Username = () => { const { t } = useTranslation(); - const { account } = useOutletContext(); + const { account } = useContext(AccountContext); const labelId = "prefUsername"; return ( @@ -169,23 +169,12 @@ const ChangePasswordDialog = (props) => { const Stats = () => { const { t } = useTranslation(); - const { account } = useOutletContext(); + const { account } = useContext(AccountContext); const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); - if (!account) { return <>; } - const normalize = (value, max) => Math.min(value / max * 100, 100); - const barColor = (remaining, limit) => { - if (account.role === "admin") { - return "primary"; - } else if (limit > 0 && remaining === 0) { - return "error"; - } - return "primary"; - }; - return ( @@ -238,7 +227,6 @@ const Stats = () => { 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100} - color={barColor(account.stats.reservations_remaining, account.limits.reservations)} /> } @@ -260,7 +248,6 @@ const Stats = () => { { }>
{account.stats.emails} - {account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")} + {account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}
0 ? normalize(account.stats.emails, account.limits.emails) : 100} - color={account?.role !== "admin" && account.stats.emails_remaining === 0 ? 'error' : 'primary'} + value={account.role === "user" ? normalize(account.stats.emails, account.limits.emails) : 100} />
{ >
{formatBytes(account.stats.attachment_total_size)} - {account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")} + {account.role === "user" ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}
0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} - color={account.role !== "admin" && account.stats.attachment_total_size_remaining === 0 ? 'error' : 'primary'} + value={account.role === "user" ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} />
- {account.limits.basis === "ip" && + {account.role === "user" && account.limits.basis === "ip" && {t("account_usage_basis_ip_description")} diff --git a/web/src/components/App.js b/web/src/components/App.js index 047a461..7bb3695 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -1,10 +1,10 @@ import * as React from 'react'; -import {Suspense, useEffect, useState} from 'react'; +import {createContext, Suspense, useContext, useEffect, useState} from 'react'; import Box from '@mui/material/Box'; import {ThemeProvider} from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import Toolbar from '@mui/material/Toolbar'; -import Notifications from "./Notifications"; +import {AllSubscriptions, SingleSubscription} from "./Notifications"; import theme from "./theme"; import Navigation from "./Navigation"; import ActionBar from "./ActionBar"; @@ -13,11 +13,11 @@ import Preferences from "./Preferences"; import {useLiveQuery} from "dexie-react-hooks"; import subscriptionManager from "../app/SubscriptionManager"; import userManager from "../app/UserManager"; -import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from "react-router-dom"; +import {BrowserRouter, Outlet, Route, Routes, useParams} from "react-router-dom"; import {expandUrl} from "../app/utils"; import ErrorBoundary from "./ErrorBoundary"; import routes from "./routes"; -import {useAccountListener, useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; +import {useAccountListener, useBackgroundProcesses, useConnectionListeners} from "./hooks"; import PublishDialog from "./PublishDialog"; import Messaging from "./Messaging"; import "./i18n"; // Translations! @@ -27,53 +27,45 @@ import Login from "./Login"; import Pricing from "./Pricing"; import Signup from "./Signup"; import Account from "./Account"; -import ResetPassword from "./ResetPassword"; + +export const AccountContext = createContext(null); const App = () => { + const [account, setAccount] = useState(null); return ( }> - - - - }/> - }/> - }/> - }/> - }/> - }> - }/> - }/> - }/> - }/> - }/> - - - + + + + + }/> + }/> + }/> + }/> + }> + }/> + }/> + }/> + }/> + }/> + + + + ); } -const AllSubscriptions = () => { - const { subscriptions } = useOutletContext(); - return ; -}; - -const SingleSubscription = () => { - const { subscriptions, selected } = useOutletContext(); - useAutoSubscribe(subscriptions, selected); - return ; -}; - const Layout = () => { const params = useParams(); + const { account, setAccount } = useContext(AccountContext); const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted()); const [sendDialogOpenMode, setSendDialogOpenMode] = useState(""); - const [account, setAccount] = useState(null); const users = useLiveQuery(() => userManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all()); const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0; @@ -94,7 +86,6 @@ const Layout = () => { onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} /> { />
- +
{ const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); + const { account } = useContext(AccountContext); const [subscribeDialogKey, setSubscribeDialogKey] = useState(0); const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); @@ -100,8 +102,8 @@ const NavList = (props) => { navigate(routes.account); }; - const isAdmin = props.account?.role === "admin"; - const isPaid = props.account?.tier?.paid; + const isAdmin = account?.role === "admin"; + const isPaid = account?.tier?.paid; const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid;// && (!props.account || !props.account.tier || !props.account.tier.paid || props.account); const showSubscriptionsList = props.subscriptions?.length > 0; const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js index b480402..10bcad8 100644 --- a/web/src/components/Notifications.js +++ b/web/src/components/Notifications.js @@ -19,7 +19,8 @@ import { formatBytes, formatMessage, formatShortDateTime, - formatTitle, maybeAppendActionErrors, + formatTitle, + maybeAppendActionErrors, openUrl, shortUrl, topicShortUrl, @@ -41,15 +42,27 @@ import priority5 from "../img/priority-5.svg"; import logoOutline from "../img/ntfy-outline.svg"; import AttachmentIcon from "./AttachmentIcon"; import {Trans, useTranslation} from "react-i18next"; +import {useOutletContext} from "react-router-dom"; +import {useAutoSubscribe} from "./hooks"; -const Notifications = (props) => { - if (props.mode === "all") { - return (props.subscriptions) ? : ; +export const AllSubscriptions = () => { + const { subscriptions } = useOutletContext(); + if (!subscriptions) { + return ; } - return (props.subscription) ? : ; -} + return ; +}; -const AllSubscriptions = (props) => { +export const SingleSubscription = () => { + const { subscriptions, selected } = useOutletContext(); + useAutoSubscribe(subscriptions, selected); + if (!selected) { + return ; + } + return ; +}; + +const AllSubscriptionsList = (props) => { const subscriptions = props.subscriptions; const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); if (notifications === null || notifications === undefined) { @@ -62,7 +75,7 @@ const AllSubscriptions = (props) => { return ; } -const SingleSubscription = (props) => { +const SingleSubscriptionList = (props) => { const subscription = props.subscription; const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); if (notifications === null || notifications === undefined) { @@ -533,5 +546,3 @@ const Loading = () => { ); }; - -export default Notifications; diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index e1777e2..fa26a86 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import {useEffect, useState} from 'react'; +import {useContext, useEffect, useState} from 'react'; import { Alert, CardActions, @@ -40,13 +40,11 @@ import session from "../app/Session"; import routes from "./routes"; import accountApi, {UnauthorizedError} from "../app/AccountApi"; import {Pref, PrefGroup} from "./Pref"; -import {useOutletContext} from "react-router-dom"; import LockIcon from "@mui/icons-material/Lock"; import {Public, PublicOff} from "@mui/icons-material"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; import DialogContentText from "@mui/material/DialogContentText"; import ReserveTopicSelect from "./ReserveTopicSelect"; +import {AccountContext} from "./App"; const Preferences = () => { return ( @@ -481,11 +479,11 @@ const Language = () => { const Reservations = () => { const { t } = useTranslation(); - const { account } = useOutletContext(); + const { account } = useContext(AccountContext); const [dialogKey, setDialogKey] = useState(0); const [dialogOpen, setDialogOpen] = useState(false); - if (!config.enable_reserve_topics || !session.exists() || !account || account.role === "admin") { + if (!config.enable_reservations || !session.exists() || !account || account.role === "admin") { return <>; } const reservations = account.reservations || []; @@ -522,14 +520,7 @@ const Reservations = () => { {t("prefs_reservations_description")} {reservations.length > 0 && } - {limitReached && - - You reached your reserved topics limit. - {config.enable_payments && - <>{" "}Upgrade - } - - } + {limitReached && {t("prefs_reservations_limit_reached")}} diff --git a/web/src/components/ResetPassword.js b/web/src/components/ResetPassword.js deleted file mode 100644 index bcf635e..0000000 --- a/web/src/components/ResetPassword.js +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; -import TextField from "@mui/material/TextField"; -import Button from "@mui/material/Button"; -import Box from "@mui/material/Box"; -import routes from "./routes"; -import Typography from "@mui/material/Typography"; -import {NavLink} from "react-router-dom"; -import AvatarBox from "./AvatarBox"; - -const ResetPassword = () => { - const handleSubmit = async (event) => { - // - }; - - return ( - - - Reset password - - - - - - - - < Return to sign in - - - - ); -} - -export default ResetPassword; diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js index 783d00e..460ee5d 100644 --- a/web/src/components/SubscribeDialog.js +++ b/web/src/components/SubscribeDialog.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import {useState} from 'react'; +import {useContext, useState} from 'react'; import Button from '@mui/material/Button'; import TextField from '@mui/material/TextField'; import Dialog from '@mui/material/Dialog'; @@ -19,7 +19,7 @@ import session from "../app/Session"; import routes from "./routes"; import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi"; import ReserveTopicSelect from "./ReserveTopicSelect"; -import {useOutletContext} from "react-router-dom"; +import {AccountContext} from "./App"; const publicBaseUrl = "https://ntfy.sh"; @@ -76,7 +76,7 @@ const SubscribeDialog = (props) => { const SubscribePage = (props) => { const { t } = useTranslation(); - //const { account } = useOutletContext(); + const { account } = useContext(AccountContext); const [reserveTopicVisible, setReserveTopicVisible] = useState(false); const [anotherServerVisible, setAnotherServerVisible] = useState(false); const [errorText, setErrorText] = useState(""); @@ -87,7 +87,7 @@ const SubscribePage = (props) => { const existingBaseUrls = Array .from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)])) .filter(s => s !== config.base_url); - //const reserveTopicEnabled = session.exists() && (account?.stats.reservations_remaining || 0) > 0; + const reserveTopicEnabled = session.exists() && account?.role === "user" && (account?.stats.reservations_remaining || 0) > 0; const handleSubscribe = async () => { const user = await userManager.get(baseUrl); // May be undefined @@ -177,14 +177,14 @@ const SubscribePage = (props) => { {t("subscribe_dialog_subscribe_button_generate_topic_name")} - {config.enable_reserve_topics && session.exists() && !anotherServerVisible && + {config.enable_reservations && session.exists() && !anotherServerVisible && setReserveTopicVisible(ev.target.checked)} inputProps={{ diff --git a/web/src/components/SubscriptionSettingsDialog.js b/web/src/components/SubscriptionSettingsDialog.js index 128dcca..85d77c7 100644 --- a/web/src/components/SubscriptionSettingsDialog.js +++ b/web/src/components/SubscriptionSettingsDialog.js @@ -78,7 +78,7 @@ const SubscriptionSettingsDialog = (props) => { "aria-label": t("subscription_settings_dialog_display_name_placeholder") }} /> - {config.enable_reserve_topics && session.exists() && + {config.enable_reservations && session.exists() && <> { const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); diff --git a/web/src/components/routes.js b/web/src/components/routes.js index 7f6589a..a7a2e76 100644 --- a/web/src/components/routes.js +++ b/web/src/components/routes.js @@ -8,7 +8,7 @@ const routes = { pricing: "/pricing", login: "/login", signup: "/signup", - resetPassword: "/reset-password", + resetPassword: "/reset-password", // Not used (yet) app: config.app_root, account: "/account", settings: "/settings",