Compare commits

...

3 Commits

Author SHA1 Message Date
binwiederhier
74ea78fbdc Make require-login work 2025-05-24 14:46:18 -04:00
binwiederhier
1561251028 Merge branch 'main' of github.com:binwiederhier/ntfy into feat_optional_require_login 2025-05-24 14:27:40 -04:00
Thea Tischbein
03aeb707f2 feat: Add optional web app flag which requires a login for every action 2025-05-05 11:39:53 +02:00
11 changed files with 70 additions and 33 deletions

View File

@@ -62,6 +62,7 @@ var flagsServe = append(
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "require-login", Aliases: []string{"require_login"}, EnvVars: []string{"NTFY_REQUIRE_LOGIN"}, Value: false, Usage: "all actions via the web app requires a login"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
@@ -158,6 +159,7 @@ func execServe(c *cli.Context) error {
webRoot := c.String("web-root") webRoot := c.String("web-root")
enableSignup := c.Bool("enable-signup") enableSignup := c.Bool("enable-signup")
enableLogin := c.Bool("enable-login") enableLogin := c.Bool("enable-login")
requireLogin := c.Bool("require-login")
enableReservations := c.Bool("enable-reservations") enableReservations := c.Bool("enable-reservations")
upstreamBaseURL := c.String("upstream-base-url") upstreamBaseURL := c.String("upstream-base-url")
upstreamAccessToken := c.String("upstream-access-token") upstreamAccessToken := c.String("upstream-access-token")
@@ -407,6 +409,7 @@ func execServe(c *cli.Context) error {
conf.BillingContact = billingContact conf.BillingContact = billingContact
conf.EnableSignup = enableSignup conf.EnableSignup = enableSignup
conf.EnableLogin = enableLogin conf.EnableLogin = enableLogin
conf.RequireLogin = requireLogin
conf.EnableReservations = enableReservations conf.EnableReservations = enableReservations
conf.EnableMetrics = enableMetrics conf.EnableMetrics = enableMetrics
conf.MetricsListenHTTP = metricsListenHTTP conf.MetricsListenHTTP = metricsListenHTTP

View File

@@ -1425,6 +1425,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API | | `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
| `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API | | `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
| `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) | | `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
| `require-login` | `NTFY_REQUIRE_LOGIN` | *boolean* (`true` or `false`) | `false` | All actions via the web app require a login |
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments | | `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe | | `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact | | `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |

View File

@@ -150,6 +150,7 @@ type Config struct {
BillingContact string BillingContact string
EnableSignup bool // Enable creation of accounts via API and UI EnableSignup bool // Enable creation of accounts via API and UI
EnableLogin bool EnableLogin bool
RequireLogin bool
EnableReservations bool // Allow users with role "user" to own/reserve topics EnableReservations bool // Allow users with role "user" to own/reserve topics
EnableMetrics bool EnableMetrics bool
AccessControlAllowOrigin string // CORS header field to restrict access from web clients AccessControlAllowOrigin string // CORS header field to restrict access from web clients
@@ -240,6 +241,7 @@ func NewConfig() *Config {
EnableSignup: false, EnableSignup: false,
EnableLogin: false, EnableLogin: false,
EnableReservations: false, EnableReservations: false,
RequireLogin: false,
AccessControlAllowOrigin: "*", AccessControlAllowOrigin: "*",
Version: "", Version: "",
WebPushPrivateKey: "", WebPushPrivateKey: "",

View File

@@ -586,6 +586,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
EnableCalls: s.config.TwilioAccount != "", EnableCalls: s.config.TwilioAccount != "",
EnableEmails: s.config.SMTPSenderFrom != "", EnableEmails: s.config.SMTPSenderFrom != "",
EnableReservations: s.config.EnableReservations, EnableReservations: s.config.EnableReservations,
RequireLogin: s.config.RequireLogin,
EnableWebPush: s.config.WebPushPublicKey != "", EnableWebPush: s.config.WebPushPublicKey != "",
BillingContact: s.config.BillingContact, BillingContact: s.config.BillingContact,
WebPushPublicKey: s.config.WebPushPublicKey, WebPushPublicKey: s.config.WebPushPublicKey,

View File

@@ -214,10 +214,12 @@
# - enable-signup allows users to sign up via the web app, or API # - enable-signup allows users to sign up via the web app, or API
# - enable-login allows users to log in via the web app, or API # - enable-login allows users to log in via the web app, or API
# - enable-reservations allows users to reserve topics (if their tier allows it) # - enable-reservations allows users to reserve topics (if their tier allows it)
# - require-login all user actions via the web app require a login
# #
# enable-signup: false # enable-signup: false
# enable-login: false # enable-login: false
# enable-reservations: false # enable-reservations: false
# require-login: false
# Server URL of a Firebase/APNS-connected ntfy server (likely "https://ntfy.sh"). # Server URL of a Firebase/APNS-connected ntfy server (likely "https://ntfy.sh").
# #

View File

@@ -401,6 +401,7 @@ type apiConfigResponse struct {
BaseURL string `json:"base_url"` BaseURL string `json:"base_url"`
AppRoot string `json:"app_root"` AppRoot string `json:"app_root"`
EnableLogin bool `json:"enable_login"` EnableLogin bool `json:"enable_login"`
RequireLogin bool `json:"require_login"`
EnableSignup bool `json:"enable_signup"` EnableSignup bool `json:"enable_signup"`
EnablePayments bool `json:"enable_payments"` EnablePayments bool `json:"enable_payments"`
EnableCalls bool `json:"enable_calls"` EnableCalls bool `json:"enable_calls"`

View File

@@ -9,6 +9,7 @@ var config = {
base_url: window.location.origin, // Change to test against a different server base_url: window.location.origin, // Change to test against a different server
app_root: "/", app_root: "/",
enable_login: true, enable_login: true,
require_login: true,
enable_signup: true, enable_signup: true,
enable_payments: false, enable_payments: false,
enable_reservations: true, enable_reservations: true,

View File

@@ -97,6 +97,8 @@
"notifications_none_for_any_description": "To send notifications to a topic, simply PUT or POST to the topic URL. Here's an example using one of your topics.", "notifications_none_for_any_description": "To send notifications to a topic, simply PUT or POST to the topic URL. Here's an example using one of your topics.",
"notifications_no_subscriptions_title": "It looks like you don't have any subscriptions yet.", "notifications_no_subscriptions_title": "It looks like you don't have any subscriptions yet.",
"notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.", "notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.",
"notifications_no_subscriptions_login_title": "This page requires a Login.",
"notifications_no_subscriptions_login_description": "Click \"{{linktext}}\" to login into your account.",
"notifications_example": "Example", "notifications_example": "Example",
"notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.", "notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.",
"display_name_dialog_title": "Change display name", "display_name_dialog_title": "Change display name",

View File

@@ -135,7 +135,7 @@ const NavList = (props) => {
{showNotificationContextNotSupportedBox && <NotificationContextNotSupportedAlert />} {showNotificationContextNotSupportedBox && <NotificationContextNotSupportedAlert />}
{showNotificationIOSInstallRequired && <NotificationIOSInstallRequiredAlert />} {showNotificationIOSInstallRequired && <NotificationIOSInstallRequiredAlert />}
{alertVisible && <Divider />} {alertVisible && <Divider />}
{!showSubscriptionsList && ( {!showSubscriptionsList && (session.exists() || !config.require_login) && (
<ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.app_root}> <ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.app_root}>
<ListItemIcon> <ListItemIcon>
<ChatBubble /> <ChatBubble />
@@ -164,30 +164,39 @@ const NavList = (props) => {
<ListItemText primary={t("nav_button_account")} /> <ListItemText primary={t("nav_button_account")} />
</ListItemButton> </ListItemButton>
)} )}
<ListItemButton onClick={() => navigate(routes.settings)} selected={location.pathname === routes.settings}> {session.exists() ||
<ListItemIcon> (!config.require_login && (
<SettingsIcon /> <ListItemButton onClick={() => navigate(routes.settings)} selected={location.pathname === routes.settings}>
</ListItemIcon> <ListItemIcon>
<ListItemText primary={t("nav_button_settings")} /> <SettingsIcon />
</ListItemButton> </ListItemIcon>
<ListItemText primary={t("nav_button_settings")} />
</ListItemButton>
))}
<ListItemButton onClick={() => openUrl("/docs")}> <ListItemButton onClick={() => openUrl("/docs")}>
<ListItemIcon> <ListItemIcon>
<ArticleIcon /> <ArticleIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={t("nav_button_documentation")} /> <ListItemText primary={t("nav_button_documentation")} />
</ListItemButton> </ListItemButton>
<ListItemButton onClick={() => props.onPublishMessageClick()}> {session.exists() ||
<ListItemIcon> (!config.require_login && (
<Send /> <ListItemButton onClick={() => props.onPublishMessageClick()}>
</ListItemIcon> <ListItemIcon>
<ListItemText primary={t("nav_button_publish_message")} /> <Send />
</ListItemButton> </ListItemIcon>
<ListItemButton onClick={() => setSubscribeDialogOpen(true)}> <ListItemText primary={t("nav_button_publish_message")} />
<ListItemIcon> </ListItemButton>
<AddIcon /> ))}
</ListItemIcon> {session.exists() ||
<ListItemText primary={t("nav_button_subscribe")} /> (!config.require_login && (
</ListItemButton> <ListItemButton onClick={() => setSubscribeDialogOpen(true)}>
<ListItemIcon>
<AddIcon />
</ListItemIcon>
<ListItemText primary={t("nav_button_subscribe")} />
</ListItemButton>
))}
{showUpgradeBanner && ( {showUpgradeBanner && (
// The text background gradient didn't seem to do well with switching between light/dark mode, // The text background gradient didn't seem to do well with switching between light/dark mode,
// So adding a `key` forces React to replace the entire component when the theme changes // So adding a `key` forces React to replace the entire component when the theme changes

View File

@@ -37,6 +37,7 @@ import priority5 from "../img/priority-5.svg";
import logoOutline from "../img/ntfy-outline.svg"; import logoOutline from "../img/ntfy-outline.svg";
import AttachmentIcon from "./AttachmentIcon"; import AttachmentIcon from "./AttachmentIcon";
import { useAutoSubscribe } from "./hooks"; import { useAutoSubscribe } from "./hooks";
import session from "../app/Session";
const priorityFiles = { const priorityFiles = {
1: priority1, 1: priority1,
@@ -635,12 +636,20 @@ const NoSubscriptions = () => {
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}> <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} /> <img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} />
<br /> <br />
{t("notifications_no_subscriptions_title")} {!session.exists() && !config.require_login && t("notifications_no_subscriptions_title")}
{!session.exists() && config.require_login && t("notifications_no_subscriptions_login_title")}
</Typography> </Typography>
<Paragraph> <Paragraph>
{t("notifications_no_subscriptions_description", { {!session.exists() &&
linktext: t("nav_button_subscribe"), !config.require_login &&
})} t("notifications_no_subscriptions_description", {
linktext: t("nav_button_subscribe"),
})}
{!session.exists() &&
config.require_login &&
t("notifications_no_subscriptions_login_description", {
linktext: t("action_bar_sign_in"),
})}
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
<ForMoreDetails /> <ForMoreDetails />

View File

@@ -65,16 +65,22 @@ const maybeUpdateAccountSettings = async (payload) => {
} }
}; };
const Preferences = () => ( const Preferences = () => {
<Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}> if (!session.exists() || !config.require_login) {
<Stack spacing={3}> window.location.href = routes.app;
<Notifications /> return <></>;
<Reservations /> }
<Users /> return (
<Appearance /> <Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}>
</Stack> <Stack spacing={3}>
</Container> <Notifications />
); <Reservations />
<Users />
<Appearance />
</Stack>
</Container>
);
};
const Notifications = () => { const Notifications = () => {
const { t } = useTranslation(); const { t } = useTranslation();