mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-11-21 19:03:26 +01:00
Startup queries, foreign keys
This commit is contained in:
parent
3280c2c440
commit
60f1882bec
14 changed files with 148 additions and 69 deletions
11
cmd/serve.go
11
cmd/serve.go
|
@ -49,6 +49,7 @@ var flagsServe = append(
|
|||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
|
||||
|
@ -77,6 +78,8 @@ var flagsServe = append(
|
|||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
|
||||
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"}),
|
||||
)
|
||||
|
||||
var cmdServe = &cli.Command{
|
||||
|
@ -118,6 +121,7 @@ func execServe(c *cli.Context) error {
|
|||
cacheBatchSize := c.Int("cache-batch-size")
|
||||
cacheBatchTimeout := c.Duration("cache-batch-timeout")
|
||||
authFile := c.String("auth-file")
|
||||
authStartupQueries := c.String("auth-startup-queries")
|
||||
authDefaultAccess := c.String("auth-default-access")
|
||||
attachmentCacheDir := c.String("attachment-cache-dir")
|
||||
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
|
||||
|
@ -146,6 +150,8 @@ func execServe(c *cli.Context) error {
|
|||
behindProxy := c.Bool("behind-proxy")
|
||||
enableSignup := c.Bool("enable-signup")
|
||||
enableLogin := c.Bool("enable-login")
|
||||
enablePayments := c.Bool("enable-payments")
|
||||
enableReserveTopics := c.Bool("enable-reserve-topics")
|
||||
|
||||
// Check values
|
||||
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
||||
|
@ -182,6 +188,8 @@ 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) {
|
||||
return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or enable-payments if auth-file is not set")
|
||||
}
|
||||
|
||||
webRootIsApp := webRoot == "app"
|
||||
|
@ -245,6 +253,7 @@ func execServe(c *cli.Context) error {
|
|||
conf.CacheBatchSize = cacheBatchSize
|
||||
conf.CacheBatchTimeout = cacheBatchTimeout
|
||||
conf.AuthFile = authFile
|
||||
conf.AuthStartupQueries = authStartupQueries
|
||||
conf.AuthDefault = authDefault
|
||||
conf.AttachmentCacheDir = attachmentCacheDir
|
||||
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
|
||||
|
@ -274,6 +283,8 @@ func execServe(c *cli.Context) error {
|
|||
conf.EnableWeb = enableWeb
|
||||
conf.EnableSignup = enableSignup
|
||||
conf.EnableLogin = enableLogin
|
||||
conf.EnablePayments = enablePayments
|
||||
conf.EnableReserveTopics = enableReserveTopics
|
||||
conf.Version = c.App.Version
|
||||
|
||||
// Set up hot-reloading of config
|
||||
|
|
|
@ -268,6 +268,7 @@ func execUserList(c *cli.Context) error {
|
|||
|
||||
func createUserManager(c *cli.Context) (*user.Manager, error) {
|
||||
authFile := c.String("auth-file")
|
||||
authStartupQueries := c.String("auth-startup-queries")
|
||||
authDefaultAccess := c.String("auth-default-access")
|
||||
if authFile == "" {
|
||||
return nil, errors.New("option auth-file not set; auth is unconfigured for this server")
|
||||
|
@ -278,7 +279,7 @@ func createUserManager(c *cli.Context) (*user.Manager, error) {
|
|||
if err != nil {
|
||||
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
|
||||
}
|
||||
return user.NewManager(authFile, authDefault)
|
||||
return user.NewManager(authFile, authStartupQueries, authDefault)
|
||||
}
|
||||
|
||||
func readPasswordAndConfirm(c *cli.Context) (string, error) {
|
||||
|
|
|
@ -67,6 +67,7 @@ type Config struct {
|
|||
CacheBatchSize int
|
||||
CacheBatchTimeout time.Duration
|
||||
AuthFile string
|
||||
AuthStartupQueries string
|
||||
AuthDefault user.Permission
|
||||
AttachmentCacheDir string
|
||||
AttachmentTotalSizeLimit int64
|
||||
|
@ -104,11 +105,12 @@ type Config struct {
|
|||
VisitorAccountCreateLimitReplenish time.Duration
|
||||
BehindProxy bool
|
||||
EnableWeb bool
|
||||
EnableSignup bool
|
||||
EnableSignup bool // Enable creation of accounts via API and UI
|
||||
EnableLogin bool
|
||||
EnableEmailConfirm bool
|
||||
EnablePasswordReset bool
|
||||
EnablePayments bool
|
||||
EnableReserveTopics bool // Allow users with role "user" to own/reserve topics
|
||||
Version string // injected by App
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,9 @@ import (
|
|||
message cache duration
|
||||
Keep 10000 messages or keep X days?
|
||||
Attachment expiration based on plan
|
||||
plan:
|
||||
weirdness with admin and "default" account
|
||||
"account topic" sync mechanism
|
||||
purge accounts that were not logged into in X
|
||||
reset daily limits for users
|
||||
max token issue limit
|
||||
|
@ -165,7 +168,7 @@ func New(conf *Config) (*Server, error) {
|
|||
}
|
||||
var userManager *user.Manager
|
||||
if conf.AuthFile != "" {
|
||||
userManager, err = user.NewManager(conf.AuthFile, conf.AuthDefault)
|
||||
userManager, err = user.NewManager(conf.AuthFile, conf.AuthStartupQueries, conf.AuthDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -453,6 +456,8 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
|||
EnableLogin: s.config.EnableLogin,
|
||||
EnableSignup: s.config.EnableSignup,
|
||||
EnablePasswordReset: s.config.EnablePasswordReset,
|
||||
EnablePayments: s.config.EnablePayments,
|
||||
EnableReserveTopics: s.config.EnableReserveTopics,
|
||||
DisallowedTopics: disallowedTopics,
|
||||
}
|
||||
b, err := json.Marshal(response)
|
||||
|
|
|
@ -288,5 +288,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"`
|
||||
DisallowedTopics []string `json:"disallowed_topics"`
|
||||
}
|
||||
|
|
|
@ -59,7 +59,8 @@ const (
|
|||
write INT NOT NULL,
|
||||
owner_user_id INT,
|
||||
PRIMARY KEY (user_id, topic),
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS user_token (
|
||||
user_id INT NOT NULL,
|
||||
|
@ -75,6 +76,10 @@ const (
|
|||
INSERT INTO user (id, user, pass, role) VALUES (1, '*', '', 'anonymous') ON CONFLICT (id) DO NOTHING;
|
||||
`
|
||||
createTablesQueries = `BEGIN; ` + createTablesQueriesNoTx + ` COMMIT;`
|
||||
builtinStartupQueries = `
|
||||
PRAGMA foreign_keys = ON;
|
||||
`
|
||||
|
||||
selectUserByNameQuery = `
|
||||
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.emails_limit, p.topics_limit, p.attachment_file_size_limit, p.attachment_total_size_limit
|
||||
FROM user u
|
||||
|
@ -95,10 +100,7 @@ const (
|
|||
WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic
|
||||
ORDER BY u.user DESC
|
||||
`
|
||||
)
|
||||
|
||||
// Manager-related queries
|
||||
const (
|
||||
insertUserQuery = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
|
||||
selectUsernamesQuery = `
|
||||
SELECT user
|
||||
|
@ -150,7 +152,6 @@ const (
|
|||
updateTokenExpiryQuery = `UPDATE user_token SET expires = ? WHERE user_id = (SELECT id FROM user WHERE user = ?) AND token = ?`
|
||||
deleteTokenQuery = `DELETE FROM user_token WHERE user_id = (SELECT id FROM user WHERE user = ?) AND token = ?`
|
||||
deleteExpiredTokensQuery = `DELETE FROM user_token WHERE expires < ?`
|
||||
deleteUserTokensQuery = `DELETE FROM user_token WHERE user_id = (SELECT id FROM user WHERE user = ?)`
|
||||
)
|
||||
|
||||
// Schema management queries
|
||||
|
@ -191,12 +192,12 @@ type Manager struct {
|
|||
var _ Auther = (*Manager)(nil)
|
||||
|
||||
// NewManager creates a new Manager instance
|
||||
func NewManager(filename string, defaultAccess Permission) (*Manager, error) {
|
||||
return newManager(filename, defaultAccess, userTokenExpiryDuration, userStatsQueueWriterInterval)
|
||||
func NewManager(filename, startupQueries string, defaultAccess Permission) (*Manager, error) {
|
||||
return newManager(filename, startupQueries, defaultAccess, userTokenExpiryDuration, userStatsQueueWriterInterval)
|
||||
}
|
||||
|
||||
// NewManager creates a new Manager instance
|
||||
func newManager(filename string, defaultAccess Permission, tokenExpiryDuration, statsWriterInterval time.Duration) (*Manager, error) {
|
||||
func newManager(filename, startupQueries string, defaultAccess Permission, tokenExpiryDuration, statsWriterInterval time.Duration) (*Manager, error) {
|
||||
db, err := sql.Open("sqlite3", filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -204,6 +205,9 @@ func newManager(filename string, defaultAccess Permission, tokenExpiryDuration,
|
|||
if err := setupDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := runStartupQueries(db, startupQueries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manager := &Manager{
|
||||
db: db,
|
||||
defaultAccess: defaultAccess,
|
||||
|
@ -223,11 +227,12 @@ func (a *Manager) Authenticate(username, password string) (*User, error) {
|
|||
}
|
||||
user, err := a.User(username)
|
||||
if err != nil {
|
||||
bcrypt.CompareHashAndPassword([]byte(intentionalSlowDownHash),
|
||||
[]byte("intentional slow-down to avoid timing attacks"))
|
||||
log.Trace("authentication of user %s failed (1): %s", username, err.Error())
|
||||
bcrypt.CompareHashAndPassword([]byte(intentionalSlowDownHash), []byte("intentional slow-down to avoid timing attacks"))
|
||||
return nil, ErrUnauthenticated
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Hash), []byte(password)); err != nil {
|
||||
log.Trace("authentication of user %s failed (2): %s", username, err.Error())
|
||||
return nil, ErrUnauthenticated
|
||||
}
|
||||
return user, nil
|
||||
|
@ -407,21 +412,11 @@ func (a *Manager) RemoveUser(username string) error {
|
|||
if !AllowedUsername(username) {
|
||||
return ErrInvalidArgument
|
||||
}
|
||||
tx, err := a.db.Begin()
|
||||
if err != nil {
|
||||
// Rows in user_access, user_token, etc. are deleted via foreign keys
|
||||
if _, err := a.db.Exec(deleteUserQuery, username); err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(deleteUserAccessQuery, username); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(deleteUserTokensQuery, username); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(deleteUserQuery, username); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Users returns a list of users. It always also returns the Everyone user ("*").
|
||||
|
@ -666,6 +661,16 @@ func fromSQLWildcard(s string) string {
|
|||
return strings.ReplaceAll(s, "%", "*")
|
||||
}
|
||||
|
||||
func runStartupQueries(db *sql.DB, startupQueries string) error {
|
||||
if _, err := db.Exec(startupQueries); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(builtinStartupQueries); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupDB(db *sql.DB) error {
|
||||
// If 'schemaVersion' table does not exist, this must be a new database
|
||||
rowsSV, err := db.Query(selectSchemaVersionQuery)
|
||||
|
|
|
@ -12,5 +12,6 @@ var config = {
|
|||
enable_signup: true,
|
||||
enable_password_reset: false,
|
||||
enable_payments: true,
|
||||
enable_reserve_topics: true,
|
||||
disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"]
|
||||
};
|
||||
|
|
|
@ -177,6 +177,7 @@
|
|||
"account_usage_title": "Usage",
|
||||
"account_usage_of_limit": "of {{limit}}",
|
||||
"account_usage_unlimited": "Unlimited",
|
||||
"account_usage_limits_reset_daily": "Usage limits are reset daily at midnight (UTC)",
|
||||
"account_usage_plan_title": "Account type",
|
||||
"account_usage_plan_code_default": "Default",
|
||||
"account_usage_plan_code_unlimited": "Unlimited",
|
||||
|
@ -189,7 +190,7 @@
|
|||
"account_usage_topics_title": "Reserved topics",
|
||||
"account_usage_attachment_storage_title": "Attachment storage",
|
||||
"account_usage_attachment_storage_subtitle": "{{filesize}} per file",
|
||||
"account_usage_basis_ip_description": "Usage stats and limits for this account are based on your IP address, so they may be shared with other users.",
|
||||
"account_usage_basis_ip_description": "Usage stats and limits for this account are based on your IP address, so they may be shared with other users. Limits shown above are approximates based on the existing rate limits.",
|
||||
"account_delete_title": "Delete account",
|
||||
"account_delete_description": "Permanently delete your account",
|
||||
"account_delete_dialog_description": "This will permanently delete your account, including all data that is stored on the server. If you really want to proceed, please type '{{username}}' in the text box below.",
|
||||
|
@ -243,7 +244,7 @@
|
|||
"prefs_appearance_title": "Appearance",
|
||||
"prefs_appearance_language_title": "Language",
|
||||
"prefs_reservations_title": "Reserved topics",
|
||||
"prefs_reservations_description": "You may reserve topic names for personal use here, and define access to a topic for other users.",
|
||||
"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_add_button": "Add reserved topic",
|
||||
"prefs_reservations_edit_button": "Edit topic access",
|
||||
"prefs_reservations_delete_button": "Reset topic access",
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import routes from "../components/routes";
|
||||
|
||||
class Session {
|
||||
store(username, token) {
|
||||
localStorage.setItem("user", username);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import {useState} from 'react';
|
||||
import {LinearProgress, Stack, useMediaQuery} from "@mui/material";
|
||||
import {LinearProgress, Link, Stack, useMediaQuery} from "@mui/material";
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from "@mui/material/Typography";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
@ -21,7 +21,9 @@ 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';
|
||||
import {Pref, PrefGroup} from "./Pref";
|
||||
import db from "../app/db";
|
||||
|
||||
const Account = () => {
|
||||
if (!session.exists()) {
|
||||
|
@ -169,6 +171,15 @@ const Stats = () => {
|
|||
}
|
||||
const planCode = account.plan.code ?? "none";
|
||||
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 (
|
||||
<Card sx={{p: 3}} aria-label={t("account_usage_title")}>
|
||||
<Typography variant="h5" sx={{marginBottom: 2}}>
|
||||
|
@ -180,20 +191,37 @@ const Stats = () => {
|
|||
{account.role === "admin"
|
||||
? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
|
||||
: t(`account_usage_plan_code_${planCode}`)}
|
||||
{config.enable_payments && account.plan.upgradeable &&
|
||||
<em>{" "}
|
||||
<Link onClick={() => {}}>Upgrade</Link>
|
||||
</em>
|
||||
}
|
||||
</div>
|
||||
</Pref>
|
||||
<Pref title={t("account_usage_topics_title")}>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.topics}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.topics > 0 ? t("account_usage_of_limit", { limit: account.limits.topics }) : t("account_usage_unlimited")}</Typography>
|
||||
</div>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100}
|
||||
color={account?.role !== "admin" && account.stats.topics_remaining === 0 ? 'error' : 'primary'}
|
||||
/>
|
||||
{account.limits.topics > 0 &&
|
||||
<>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.topics}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.topics }) : t("account_usage_unlimited")}</Typography>
|
||||
</div>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100}
|
||||
color={barColor(account.stats.topics_remaining, account.limits.topics)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
{account.limits.topics === 0 &&
|
||||
<em>No reserved topics for this account</em>
|
||||
}
|
||||
</Pref>
|
||||
<Pref title={t("account_usage_messages_title")}>
|
||||
<Pref title={
|
||||
<>
|
||||
{t("account_usage_messages_title")}
|
||||
<Tooltip title={t("account_usage_limits_reset_daily")}><span><InfoIcon/></span></Tooltip>
|
||||
</>
|
||||
}>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
|
||||
|
@ -201,10 +229,15 @@ const Stats = () => {
|
|||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100}
|
||||
color={account?.role !== "admin" && account.stats.messages_remaining === 0 ? 'error' : 'primary'}
|
||||
color={account.role === "user" && account.stats.messages_remaining === 0 ? 'error' : 'primary'}
|
||||
/>
|
||||
</Pref>
|
||||
<Pref title={t("account_usage_emails_title")}>
|
||||
<Pref title={
|
||||
<>
|
||||
{t("account_usage_emails_title")}
|
||||
<Tooltip title={t("account_usage_limits_reset_daily")}><span><InfoIcon/></span></Tooltip>
|
||||
</>
|
||||
}>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
|
||||
|
@ -215,7 +248,14 @@ const Stats = () => {
|
|||
color={account?.role !== "admin" && account.stats.emails_remaining === 0 ? 'error' : 'primary'}
|
||||
/>
|
||||
</Pref>
|
||||
<Pref title={t("account_usage_attachment_storage_title")} subtitle={account.role !== "admin" ? t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) }) : null}>
|
||||
<Pref title={
|
||||
<>
|
||||
{t("account_usage_attachment_storage_title")}
|
||||
{account.role === "user" &&
|
||||
<Tooltip title={t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) })}><span><InfoIcon/></span></Tooltip>
|
||||
}
|
||||
</>
|
||||
}>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
|
||||
|
@ -236,6 +276,17 @@ const Stats = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const InfoIcon = () => {
|
||||
return (
|
||||
<InfoOutlinedIcon sx={{
|
||||
verticalAlign: "bottom",
|
||||
width: "18px",
|
||||
marginLeft: "4px",
|
||||
color: "gray"
|
||||
}}/>
|
||||
);
|
||||
}
|
||||
|
||||
const Delete = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
|
|
|
@ -89,7 +89,6 @@ const Layout = () => {
|
|||
|
||||
return (
|
||||
<Box sx={{display: 'flex'}}>
|
||||
<CssBaseline/>
|
||||
<ActionBar
|
||||
selected={selected}
|
||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||
|
|
|
@ -485,7 +485,7 @@ const Reservations = () => {
|
|||
const [dialogKey, setDialogKey] = useState(0);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
if (!session.exists() || !account || account.role === "admin") {
|
||||
if (!config.enable_reserve_topics || !session.exists() || !account || account.role === "admin") {
|
||||
return <></>;
|
||||
}
|
||||
const reservations = account.reservations || [];
|
||||
|
|
|
@ -76,7 +76,7 @@ const SubscribeDialog = (props) => {
|
|||
|
||||
const SubscribePage = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const { account } = useOutletContext();
|
||||
//const { account } = useOutletContext();
|
||||
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.topics_remaining || 0) > 0;
|
||||
//const reserveTopicEnabled = session.exists() && (account?.stats.topics_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")}
|
||||
</Button>
|
||||
</div>
|
||||
{session.exists() && !anotherServerVisible &&
|
||||
{config.enable_reserve_topics && session.exists() && !anotherServerVisible &&
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
variant="standard"
|
||||
control={
|
||||
<Checkbox
|
||||
fullWidth
|
||||
disabled={account.stats.topics_remaining}
|
||||
// disabled={account.stats.topics_remaining}
|
||||
checked={reserveTopicVisible}
|
||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||
inputProps={{
|
||||
|
|
|
@ -78,26 +78,30 @@ const SubscriptionSettingsDialog = (props) => {
|
|||
"aria-label": t("subscription_settings_dialog_display_name_placeholder")
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel
|
||||
fullWidth
|
||||
variant="standard"
|
||||
sx={{pt: 1}}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={reserveTopicVisible}
|
||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||
inputProps={{
|
||||
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
|
||||
}}
|
||||
{config.enable_reserve_topics && session.exists() &&
|
||||
<>
|
||||
<FormControlLabel
|
||||
fullWidth
|
||||
variant="standard"
|
||||
sx={{pt: 1}}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={reserveTopicVisible}
|
||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||
inputProps={{
|
||||
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={t("subscription_settings_dialog_reserve_topic_label")}
|
||||
/>
|
||||
}
|
||||
label={t("subscription_settings_dialog_reserve_topic_label")}
|
||||
/>
|
||||
{reserveTopicVisible &&
|
||||
<ReserveTopicSelect
|
||||
value={everyone}
|
||||
onChange={setEveryone}
|
||||
/>
|
||||
{reserveTopicVisible &&
|
||||
<ReserveTopicSelect
|
||||
value={everyone}
|
||||
onChange={setEveryone}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
|
|
Loading…
Reference in a new issue