mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-08 21:05:21 +00:00
Update privacy policy, code review
This commit is contained in:
+8
-6
@@ -1,6 +1,6 @@
|
|||||||
# Privacy policy
|
# Privacy policy
|
||||||
|
|
||||||
**Last updated:** March 31, 2026
|
**Last updated:** June 15, 2026
|
||||||
|
|
||||||
This privacy policy describes how ntfy ("we", "us", or "our") collects, uses, and handles your information
|
This privacy policy describes how ntfy ("we", "us", or "our") collects, uses, and handles your information
|
||||||
when you use the ntfy.sh service, web app, and mobile applications (Android and iOS).
|
when you use the ntfy.sh service, web app, and mobile applications (Android and iOS).
|
||||||
@@ -19,8 +19,9 @@ If you create an account on ntfy.sh, we collect:
|
|||||||
|
|
||||||
- **Username** - A unique identifier you choose
|
- **Username** - A unique identifier you choose
|
||||||
- **Password** - Stored as a secure bcrypt hash (we never store your plaintext password)
|
- **Password** - Stored as a secure bcrypt hash (we never store your plaintext password)
|
||||||
- **Email address** - If you subscribe to a paid plan (for billing purposes via Stripe), or if you add a verified
|
- **Email address** - If you add an email address to your account for account recovery and password resets, for use
|
||||||
email address for use with the email notification feature
|
with the email notification feature, or if you subscribe to a paid plan (for billing purposes via Stripe). Email
|
||||||
|
addresses you add to your account are verified by sending a confirmation link.
|
||||||
- **Phone number** - Only if you enable the phone call notification feature (verified via SMS/call)
|
- **Phone number** - Only if you enable the phone call notification feature (verified via SMS/call)
|
||||||
|
|
||||||
You can use ntfy without creating an account. Anonymous usage is fully supported.
|
You can use ntfy without creating an account. Anonymous usage is fully supported.
|
||||||
@@ -77,9 +78,10 @@ Your phone number is shared with Twilio to deliver these services. Twilio's
|
|||||||
|
|
||||||
### Amazon SES (email delivery)
|
### Amazon SES (email delivery)
|
||||||
|
|
||||||
If you use the email notification feature (`X-Email` header), we use Amazon Simple Email Service (SES) to
|
If you use the email notification feature (`X-Email` header), or when ntfy sends account-related emails (email
|
||||||
deliver emails. The recipient email address and message content are transmitted through Amazon's infrastructure.
|
address verification and password reset links), we use Amazon Simple Email Service (SES) to deliver emails. The
|
||||||
Amazon's [privacy policy](https://aws.amazon.com/privacy/) applies.
|
recipient email address and message content are transmitted through Amazon's infrastructure. Amazon's
|
||||||
|
[privacy policy](https://aws.amazon.com/privacy/) applies.
|
||||||
|
|
||||||
### Stripe (payments)
|
### Stripe (payments)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -167,7 +167,7 @@ var (
|
|||||||
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions", "https://ntfy.sh/docs/publish/#limitations", nil}
|
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||||
errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations", nil}
|
errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||||
errHTTPTooManyRequestsLimitAttachmentBandwidth = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth reached", "https://ntfy.sh/docs/publish/#limitations", nil}
|
errHTTPTooManyRequestsLimitAttachmentBandwidth = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth reached", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||||
errHTTPTooManyRequestsLimitAccountCreation = &errHTTP{42906, http.StatusTooManyRequests, "limit reached: too many accounts created", "https://ntfy.sh/docs/publish/#limitations", nil} // FIXME document limit
|
errHTTPTooManyRequestsLimitAccountActions = &errHTTP{42906, http.StatusTooManyRequests, "limit reached: too many account requests", "https://ntfy.sh/docs/publish/#limitations", nil} // FIXME document limit
|
||||||
errHTTPTooManyRequestsLimitReservations = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", "", nil}
|
errHTTPTooManyRequestsLimitReservations = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", "", nil}
|
||||||
errHTTPTooManyRequestsLimitMessages = &errHTTP{42908, http.StatusTooManyRequests, "limit reached: daily message quota reached", "https://ntfy.sh/docs/publish/#limitations", nil}
|
errHTTPTooManyRequestsLimitMessages = &errHTTP{42908, http.StatusTooManyRequests, "limit reached: daily message quota reached", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||||
errHTTPTooManyRequestsLimitAuthFailure = &errHTTP{42909, http.StatusTooManyRequests, "limit reached: too many auth failures", "https://ntfy.sh/docs/publish/#limitations", nil} // FIXME document limit
|
errHTTPTooManyRequestsLimitAuthFailure = &errHTTP{42909, http.StatusTooManyRequests, "limit reached: too many auth failures", "https://ntfy.sh/docs/publish/#limitations", nil} // FIXME document limit
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
|
|||||||
} else if u != nil {
|
} else if u != nil {
|
||||||
return errHTTPUnauthorized // Cannot create account from user context
|
return errHTTPUnauthorized // Cannot create account from user context
|
||||||
}
|
}
|
||||||
if !v.AccountCreationAllowed() {
|
if !v.AccountActionAllowed() {
|
||||||
return errHTTPTooManyRequestsLimitAccountCreation
|
return errHTTPTooManyRequestsLimitAccountActions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit, false)
|
newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit, false)
|
||||||
@@ -50,7 +50,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.AccountCreated()
|
v.AccountActionPerformed()
|
||||||
// If an email was provided and email sending is configured, start verification (best-effort).
|
// If an email was provided and email sending is configured, start verification (best-effort).
|
||||||
// The address becomes the primary email on verify (the new account has no primary yet); a
|
// The address becomes the primary email on verify (the new account has no primary yet); a
|
||||||
// failure to send must not fail signup, so we only log it.
|
// failure to send must not fail signup, so we only log it.
|
||||||
@@ -801,10 +801,10 @@ func (s *Server) handleAccountPasswordResetRequest(w http.ResponseWriter, r *htt
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Rate limit via the shared per-visitor account-creation bucket (no new limiter/config)
|
// Rate limit via the shared per-visitor account-creation bucket (no new limiter/config)
|
||||||
if !v.AccountCreationAllowed() {
|
if !v.AccountActionAllowed() {
|
||||||
return errHTTPTooManyRequestsLimitAccountCreation
|
return errHTTPTooManyRequestsLimitAccountActions
|
||||||
}
|
}
|
||||||
v.AccountCreated() // Consume a token on every request (including no-match), to throttle probing
|
v.AccountActionPerformed() // Consume a token on every request (including no-match), to throttle probing
|
||||||
identifier := strings.TrimSpace(req.Identifier)
|
identifier := strings.TrimSpace(req.Identifier)
|
||||||
if identifier != "" && s.config.BaseURL != "" {
|
if identifier != "" && s.config.BaseURL != "" {
|
||||||
if userID, email, ok := s.resolveResetTarget(identifier); ok {
|
if userID, email, ok := s.resolveResetTarget(identifier); ok {
|
||||||
|
|||||||
+7
-5
@@ -66,7 +66,7 @@ type visitor struct {
|
|||||||
subscriptionLimiter *util.FixedLimiter // Fixed limiter for active subscriptions (ongoing connections)
|
subscriptionLimiter *util.FixedLimiter // Fixed limiter for active subscriptions (ongoing connections)
|
||||||
topicCreationLimiter *rate.Limiter // Rate limiter for inserting new topics into the in-memory topic map
|
topicCreationLimiter *rate.Limiter // Rate limiter for inserting new topics into the in-memory topic map
|
||||||
bandwidthLimiter *util.RateLimiter // Limiter for attachment bandwidth downloads
|
bandwidthLimiter *util.RateLimiter // Limiter for attachment bandwidth downloads
|
||||||
accountLimiter *rate.Limiter // Rate limiter for account creation, may be nil
|
accountLimiter *rate.Limiter // Rate limiter for account actions (signup, password-reset requests), may be nil
|
||||||
authLimiter *rate.Limiter // Limiter for incorrect login attempts, may be nil
|
authLimiter *rate.Limiter // Limiter for incorrect login attempts, may be nil
|
||||||
firebase time.Time // Next allowed Firebase message
|
firebase time.Time // Next allowed Firebase message
|
||||||
seen time.Time // Last seen time of this visitor (needed for removal of stale visitors)
|
seen time.Time // Last seen time of this visitor (needed for removal of stale visitors)
|
||||||
@@ -280,8 +280,9 @@ func (v *visitor) AuthFailed() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountCreationAllowed returns true if a new account can be created
|
// AccountActionAllowed returns true if a rate-limited account action (signup or password-reset
|
||||||
func (v *visitor) AccountCreationAllowed() bool {
|
// request) is currently allowed for this visitor
|
||||||
|
func (v *visitor) AccountActionAllowed() bool {
|
||||||
v.mu.RLock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.RUnlock()
|
defer v.mu.RUnlock()
|
||||||
if v.accountLimiter == nil || (v.accountLimiter != nil && v.accountLimiter.Tokens() < 1) {
|
if v.accountLimiter == nil || (v.accountLimiter != nil && v.accountLimiter.Tokens() < 1) {
|
||||||
@@ -290,8 +291,9 @@ func (v *visitor) AccountCreationAllowed() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountCreated decreases the account limiter. This is to be called after an account was created.
|
// AccountActionPerformed decreases the account limiter. This is to be called after a rate-limited
|
||||||
func (v *visitor) AccountCreated() {
|
// account action (signup or password-reset request).
|
||||||
|
func (v *visitor) AccountActionPerformed() {
|
||||||
v.mu.RLock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.RUnlock()
|
defer v.mu.RUnlock()
|
||||||
if v.accountLimiter != nil {
|
if v.accountLimiter != nil {
|
||||||
|
|||||||
+3
-3
@@ -1561,10 +1561,10 @@ func (a *Manager) SetPrimaryEmail(userID, email string) error {
|
|||||||
// emailed link. Only the hash is persisted; the raw token is never stored. email is the
|
// emailed link. Only the hash is persisted; the raw token is never stored. email is the
|
||||||
// address being verified for email_verify, and "" for password_reset.
|
// address being verified for email_verify, and "" for password_reset.
|
||||||
func (a *Manager) CreateMagicLink(kind MagicLinkKind, userID, email string, ttl time.Duration) (string, error) {
|
func (a *Manager) CreateMagicLink(kind MagicLinkKind, userID, email string, ttl time.Duration) (string, error) {
|
||||||
raw := generateLinkToken()
|
token := generateLinkToken()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
m := &MagicLink{
|
m := &MagicLink{
|
||||||
TokenHash: hashToken(raw),
|
TokenHash: hashToken(token),
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Email: email,
|
Email: email,
|
||||||
@@ -1574,7 +1574,7 @@ func (a *Manager) CreateMagicLink(kind MagicLinkKind, userID, email string, ttl
|
|||||||
if err := a.AddMagicLink(m); err != nil {
|
if err := a.AddMagicLink(m); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return raw, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MagicLinkByToken looks up a magic link by its raw token (hashing it first). See MagicLinkByHash.
|
// MagicLinkByToken looks up a magic link by its raw token (hashing it first). See MagicLinkByHash.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"login_title": "Sign in to your ntfy account",
|
"login_title": "Sign in to your ntfy account",
|
||||||
"login_form_button_submit": "Sign in",
|
"login_form_button_submit": "Sign in",
|
||||||
"login_link_signup": "Sign up",
|
"login_link_signup": "Sign up",
|
||||||
"login_link_forgot_password": "Forgot password?",
|
"login_link_forgot_password": "Forgot password",
|
||||||
"reset_password_request_title": "Reset password",
|
"reset_password_request_title": "Reset password",
|
||||||
"reset_password_request_description": "Enter your username or email address. If an account exists, a link to reset your password will be emailed. This only works if you already added a primary email address and verified it.",
|
"reset_password_request_description": "Enter your username or email address. If an account exists, a link to reset your password will be emailed. This only works if you already added a primary email address and verified it.",
|
||||||
"reset_password_request_identifier_label": "Username or email",
|
"reset_password_request_identifier_label": "Username or email",
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ export class TopicReservedError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountCreateLimitReachedError extends Error {
|
export class AccountActionLimitReachedError extends Error {
|
||||||
static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation
|
static CODE = 42906; // errHTTPTooManyRequestsLimitAccountActions
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Account creation limit reached");
|
super("Account action limit reached");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +75,8 @@ export const throwAppError = async (response) => {
|
|||||||
throw new UserExistsError();
|
throw new UserExistsError();
|
||||||
} else if (error.code === TopicReservedError.CODE) {
|
} else if (error.code === TopicReservedError.CODE) {
|
||||||
throw new TopicReservedError();
|
throw new TopicReservedError();
|
||||||
} else if (error.code === AccountCreateLimitReachedError.CODE) {
|
} else if (error.code === AccountActionLimitReachedError.CODE) {
|
||||||
throw new AccountCreateLimitReachedError();
|
throw new AccountActionLimitReachedError();
|
||||||
} else if (error.code === IncorrectPasswordError.CODE) {
|
} else if (error.code === IncorrectPasswordError.CODE) {
|
||||||
throw new IncorrectPasswordError();
|
throw new IncorrectPasswordError();
|
||||||
} else if (error.code === EmailVerificationCodeInvalidError.CODE) {
|
} else if (error.code === EmailVerificationCodeInvalidError.CODE) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import accountApi from "../app/AccountApi";
|
|||||||
import AvatarBox from "./AvatarBox";
|
import AvatarBox from "./AvatarBox";
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import { AccountCreateLimitReachedError, UserExistsError } from "../app/errors";
|
import { AccountActionLimitReachedError, UserExistsError } from "../app/errors";
|
||||||
|
|
||||||
const Signup = () => {
|
const Signup = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -34,7 +34,7 @@ const Signup = () => {
|
|||||||
console.log(`[Signup] Signup for user ${user.username} failed`, e);
|
console.log(`[Signup] Signup for user ${user.username} failed`, e);
|
||||||
if (e instanceof UserExistsError) {
|
if (e instanceof UserExistsError) {
|
||||||
setError(t("signup_error_username_taken", { username: e.username }));
|
setError(t("signup_error_username_taken", { username: e.username }));
|
||||||
} else if (e instanceof AccountCreateLimitReachedError) {
|
} else if (e instanceof AccountActionLimitReachedError) {
|
||||||
setError(t("signup_error_creation_limit_reached"));
|
setError(t("signup_error_creation_limit_reached"));
|
||||||
} else {
|
} else {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user