diff --git a/server/file_cache.go b/server/file_cache.go
index 88de935d..9eae7ea6 100644
--- a/server/file_cache.go
+++ b/server/file_cache.go
@@ -26,7 +26,7 @@ type fileCache struct {
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 {
return nil, err
}
@@ -38,7 +38,6 @@ func newFileCache(dir string, totalSizeLimit int64, fileSizeLimit int64) (*fileC
dir: dir,
totalSizeCurrent: size,
totalSizeLimit: totalSizeLimit,
- fileSizeLimit: fileSizeLimit,
}, nil
}
@@ -55,7 +54,7 @@ func (c *fileCache) Write(id string, in io.Reader, limiters ...util.Limiter) (in
return 0, err
}
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...)
size, err := io.Copy(limitWriter, in)
if err != nil {
diff --git a/server/server.go b/server/server.go
index 835d6948..cbe1ca62 100644
--- a/server/server.go
+++ b/server/server.go
@@ -36,15 +36,17 @@ import (
/*
TODO
+ use token auth in "SubscribeDialog"
+ upload files based on user limit
publishXHR + poll should pick current user, not from userManager
expire tokens
auto-refresh tokens from UI
reserve topics
rate limit for signup (2 per 24h)
handle invalid session token
- update disallowed topics
purge accounts that were not logged into in X
sync subscription display name
+ store users
Pages:
- Home
- Password reset
@@ -103,7 +105,7 @@ var (
staticRegex = regexp.MustCompile(`^/static/.+`)
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
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?://`)
//go:embed site
@@ -152,7 +154,7 @@ func New(conf *Config) (*Server, error) {
}
var fileCache *fileCache
if conf.AttachmentCacheDir != "" {
- fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit, conf.AttachmentFileSizeLimit)
+ fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit)
if err != nil {
return nil, err
}
@@ -423,9 +425,13 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
w.Header().Set("Content-Type", "text/javascript")
_, err := io.WriteString(w, fmt.Sprintf(`// Generated server configuration
var config = {
+ baseUrl: window.location.origin,
appRoot: "%s",
- disallowedTopics: [%s]
-};`, appRoot, disallowedTopicsStr))
+ enableLogin: %t,
+ enableSignup: %t,
+ enableResetPassword: %t,
+ disallowedTopics: [%s],
+};`, appRoot, s.config.EnableLogin, s.config.EnableSignup, s.config.EnableResetPassword, disallowedTopicsStr))
return err
}
@@ -799,7 +805,12 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
if m.Message == "" {
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 {
return errHTTPEntityTooLargeAttachmentTooLarge
} else if err != nil {
diff --git a/web/public/config.js b/web/public/config.js
index 76c02041..c25cddc2 100644
--- a/web/public/config.js
+++ b/web/public/config.js
@@ -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.
-// The actual config is dynamically generated server-side.
+// It is removed during the build process. The actual config is dynamically
+// generated server-side and served by the ntfy server.
+//
+// During web development, you may change values here for rapid testing.
var config = {
+ baseUrl: "http://localhost:2586", // window.location.origin FIXME update before merging
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"]
};
diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json
index 691ad52a..7ca3082a 100644
--- a/web/public/static/langs/en.json
+++ b/web/public/static/langs/en.json
@@ -144,7 +144,9 @@
"account_type_default": "Default",
"account_type_unlimited": "Unlimited",
"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_sound_title": "Notification sound",
"prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",
diff --git a/web/src/app/Api.js b/web/src/app/Api.js
index 3d753b8f..1f93dc15 100644
--- a/web/src/app/Api.js
+++ b/web/src/app/Api.js
@@ -125,7 +125,9 @@ class Api {
const response = await fetch(url, {
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}`);
}
const json = await response.json();
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index 66c2b48d..fc2ad85f 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -47,7 +47,7 @@ export const disallowedTopic = (topic) => {
export const topicDisplayName = (subscription) => {
if (subscription.displayName) {
return subscription.displayName;
- } else if (subscription.baseUrl === window.location.origin) {
+ } else if (subscription.baseUrl === config.baseUrl) {
return subscription.topic;
}
return topicShortUrl(subscription.baseUrl, subscription.topic);
diff --git a/web/src/components/Account.js b/web/src/components/Account.js
index d694a0ec..1294e212 100644
--- a/web/src/components/Account.js
+++ b/web/src/components/Account.js
@@ -147,7 +147,7 @@ const ChangePassword = () => {
};
const handleDialogSubmit = async (newPassword) => {
try {
- await api.changePassword("http://localhost:2586", session.token(), newPassword);
+ await api.changePassword(config.baseUrl, session.token(), newPassword);
setDialogOpen(false);
console.debug(`[Account] Password changed`);
} catch (e) {
@@ -230,7 +230,7 @@ const DeleteAccount = () => {
};
const handleDialogSubmit = async (newPassword) => {
try {
- await api.deleteAccount("http://localhost:2586", session.token());
+ await api.deleteAccount(config.baseUrl, session.token());
setDialogOpen(false);
console.debug(`[Account] Account deleted`);
// TODO delete local storage
diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js
index 97025002..c5af9d92 100644
--- a/web/src/components/ActionBar.js
+++ b/web/src/components/ActionBar.js
@@ -118,7 +118,7 @@ const SettingsIcons = (props) => {
handleClose(event);
await subscriptionManager.remove(props.subscription.id);
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
if (newSelected) {
@@ -259,9 +259,8 @@ const ProfileIcon = (props) => {
const handleClose = () => {
setAnchorEl(null);
};
-
const handleLogout = async () => {
- await api.logout("http://localhost:2586"/*window.location.origin*/, session.token());
+ await api.logout(config.baseUrl, session.token());
session.reset();
window.location.href = routes.app;
};
@@ -273,11 +272,11 @@ const ProfileIcon = (props) => {
}
- {!session.exists() &&
- <>
-
-
- >
+ {!session.exists() && config.enableLogin &&
+
+ }
+ {!session.exists() && config.enableSignup &&
+
}