1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
"""
Instance-wide settings stored in the `hub_settings` table.
One reader per concern, so callers never touch raw strings or key names. A
missing row means the built-in default — an upgrade never changes behaviour on
its own, and a downgrade that drops the table just returns to defaults.
"""
from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub.db.models import HubSetting
# Whether a member may create a group that is listed in the public directory and
# open for anyone to join. Off makes the hub private-groups-only.
ALLOW_PUBLIC_GROUPS = "allow_public_groups"
_DEFAULTS: dict[str, str] = {
ALLOW_PUBLIC_GROUPS: "true",
}
# ── Mail bounds ──────────────────────────────────────────────────────────────
#
# Stored here rather than read from `hub.toml` at each use, because an operator
# has to be able to change them while the hub is serving: the hour the budget
# runs out is exactly when nobody wants to edit a file and restart. The TOML
# values are the defaults these fall back to, so an instance that never touches
# the panel behaves as its configuration file says.
MAIL_KEYS = (
"destination_cooldown_seconds",
"destination_daily_cap",
"hourly_budget",
"hourly_reserved_for_recovery",
"verification_resend_cooldown",
"reset_cooldown",
"email_change_cooldown",
)
# What a value may be. A cooldown of zero disables it, which is a legitimate
# thing for an operator to want; a budget of zero would stop the hub sending
# anything at all, which is not, so those start at one. The upper bounds are
# there because this is a number typed into a web form.
MAIL_BOUNDS: dict[str, tuple[int, int]] = {
"destination_cooldown_seconds": (0, 86_400),
"destination_daily_cap": (1, 1_000),
"hourly_budget": (1, 100_000),
"hourly_reserved_for_recovery": (0, 100_000),
"verification_resend_cooldown": (0, 86_400),
"reset_cooldown": (0, 86_400),
"email_change_cooldown": (0, 2_592_000), # 30 days
}
_mail_defaults: dict[str, int] = {}
def set_mail_defaults(mail_cfg) -> None:
"""Record what `hub.toml` said. Called once, at startup."""
global _mail_defaults
_mail_defaults = {k: int(getattr(mail_cfg, k)) for k in MAIL_KEYS}
def mail_default(key: str) -> int:
return _mail_defaults.get(key, 0)
def clamp_mail_value(key: str, value: int) -> int:
low, high = MAIL_BOUNDS[key]
return max(low, min(high, int(value)))
async def get_int(db: AsyncSession, key: str, fallback: int) -> int:
raw = await get_raw(db, key)
if raw is None:
return fallback
try:
return int(raw)
except ValueError:
# A row that cannot be read is not a reason to send without a bound.
return fallback
async def mail_limits(db: AsyncSession) -> dict[str, int]:
"""Every mail bound, stored value or configured default."""
return {k: await get_int(db, f"mail.{k}", mail_default(k)) for k in MAIL_KEYS}
# ── Sign-in lockout ──────────────────────────────────────────────────────────
#
# After `max_failures` wrong passphrases for one username, sign-in with a
# passphrase is refused for `lockout_minutes` (`login_throttle.py`). Zero
# failures turns the lockout off; a zero-minute lockout would be the same thing
# said less clearly, so the duration starts at one.
LOGIN_KEYS = ("max_failures", "lockout_minutes")
LOGIN_DEFAULTS: dict[str, int] = {
"max_failures": 4,
"lockout_minutes": 60,
}
LOGIN_BOUNDS: dict[str, tuple[int, int]] = {
"max_failures": (0, 100),
"lockout_minutes": (1, 10_080), # a week
}
def clamp_login_value(key: str, value: int) -> int:
low, high = LOGIN_BOUNDS[key]
return max(low, min(high, int(value)))
async def login_limits(db: AsyncSession) -> dict[str, int]:
"""Both lockout numbers, stored value or built-in default."""
return {k: clamp_login_value(k, await get_int(db, f"login.{k}", LOGIN_DEFAULTS[k]))
for k in LOGIN_KEYS}
async def get_raw(db: AsyncSession, key: str) -> str | None:
row = await db.get(HubSetting, key)
return row.value if row else None
async def set_raw(db: AsyncSession, key: str, value: str) -> None:
"""Upsert. The caller owns the commit."""
row = await db.get(HubSetting, key)
if row:
row.value = value
else:
db.add(HubSetting(key=key, value=value))
async def get_bool(db: AsyncSession, key: str) -> bool:
raw = await get_raw(db, key)
if raw is None:
raw = _DEFAULTS.get(key, "false")
return raw == "true"
async def public_groups_allowed(db: AsyncSession) -> bool:
return await get_bool(db, ALLOW_PUBLIC_GROUPS)
|