Skip to content

Commit a2c8a51

Browse files
feat: complete oneshot admin UI lockdown and tests
Co-authored-by: MinecraftFuns <25814618+MinecraftFuns@users.noreply.github.com> Agent-Logs-Url: https://github.com/BTreeMap/OneShot/sessions/ea07d94d-5875-4bb9-92b6-20551f2697bb
1 parent b1f80c2 commit a2c8a51

2 files changed

Lines changed: 53 additions & 8 deletions

File tree

api/app/uploads/router.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,29 @@
3232
LOCAL_UPLOAD_DIR = Path(os.getenv("LOCAL_UPLOAD_DIR", "./uploads"))
3333
ONESHOT_PUBLIC_DOMAIN = os.getenv("ONESHOT_PUBLIC_DOMAIN", "localhost:5173")
3434
SMTP_HOST = os.getenv("ONESHOT_SMTP_HOST", "")
35-
SMTP_PORT = int(os.getenv("ONESHOT_SMTP_PORT", "587"))
3635
SMTP_USERNAME = os.getenv("ONESHOT_SMTP_USERNAME", "")
3736
SMTP_PASSWORD = os.getenv("ONESHOT_SMTP_PASSWORD", "")
3837
SMTP_FROM = os.getenv("ONESHOT_SMTP_FROM", "no-reply@oneshot.local")
38+
SMTP_USE_SSL = os.getenv("ONESHOT_SMTP_USE_SSL", "false").lower() in {
39+
"1",
40+
"true",
41+
"yes",
42+
}
43+
SMTP_TIMEOUT_SECONDS = float(os.getenv("ONESHOT_SMTP_TIMEOUT", "10"))
3944

4045
router = APIRouter()
4146
logger = logging.getLogger(__name__)
4247

4348

49+
def _smtp_port() -> int:
50+
raw = os.getenv("ONESHOT_SMTP_PORT", "587")
51+
try:
52+
return int(raw)
53+
except ValueError:
54+
logger.warning("Invalid ONESHOT_SMTP_PORT=%r; falling back to 587", raw)
55+
return 587
56+
57+
4458
class CreateOneShotTokenRequest(BaseModel):
4559
target_email: str | None = None
4660

@@ -73,10 +87,10 @@ def _oneshot_link(token_id: str) -> str:
7387
return f"https://{ONESHOT_PUBLIC_DOMAIN}/oneshot#token={token_id}"
7488

7589

76-
async def _send_oneshot_email(target_email: str, link: str) -> None:
90+
async def _send_oneshot_email(target_email: str, link: str) -> bool:
7791
if not SMTP_HOST:
7892
logger.warning("OneShot email dispatch skipped: ONESHOT_SMTP_HOST is not configured")
79-
return
93+
return False
8094

8195
message = EmailMessage()
8296
message["Subject"] = "Secure OneShot Upload Link"
@@ -92,13 +106,33 @@ async def _send_oneshot_email(target_email: str, link: str) -> None:
92106
)
93107

94108
try:
95-
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as smtp:
96-
smtp.starttls()
109+
if SMTP_USE_SSL:
110+
smtp_ctx = smtplib.SMTP_SSL(
111+
SMTP_HOST,
112+
_smtp_port(),
113+
timeout=SMTP_TIMEOUT_SECONDS,
114+
)
115+
else:
116+
smtp_ctx = smtplib.SMTP(
117+
SMTP_HOST,
118+
_smtp_port(),
119+
timeout=SMTP_TIMEOUT_SECONDS,
120+
)
121+
122+
with smtp_ctx as smtp:
123+
if not SMTP_USE_SSL:
124+
code, _message = smtp.starttls()
125+
if code not in {220, 250}:
126+
raise smtplib.SMTPException(
127+
f"STARTTLS handshake failed with SMTP code {code}"
128+
)
97129
if SMTP_USERNAME and SMTP_PASSWORD:
98130
smtp.login(SMTP_USERNAME, SMTP_PASSWORD)
99131
smtp.send_message(message)
132+
return True
100133
except Exception:
101134
logger.exception("Failed to dispatch OneShot email to %s", target_email)
135+
return False
102136

103137

104138
@router.post(

web/src/pages/Admin.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ export function Admin() {
1818
const [isLoading, setIsLoading] = useState(false);
1919
const [generatedLink, setGeneratedLink] = useState<string | null>(null);
2020
const [successMessage, setSuccessMessage] = useState<string | null>(null);
21+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
2122

2223
const onGenerate = async (e: FormEvent) => {
2324
e.preventDefault();
2425
setIsLoading(true);
2526
setGeneratedLink(null);
2627
setSuccessMessage(null);
28+
setErrorMessage(null);
2729
try {
2830
const response = await fetch("/api/admin/oneshot-tokens", {
2931
method: "POST",
@@ -33,9 +35,13 @@ export function Admin() {
3335
const data = (await response.json()) as {
3436
sent?: boolean;
3537
link?: string;
38+
detail?: string;
3639
};
3740
if (!response.ok) {
38-
throw new Error("Failed to generate one-shot link");
41+
throw new Error(
42+
data.detail ??
43+
`Failed to generate one-shot link (status: ${response.status})`,
44+
);
3945
}
4046
if (data.sent) {
4147
setSuccessMessage("Link successfully dispatched to user email.");
@@ -44,8 +50,12 @@ export function Admin() {
4450
if (data.link) {
4551
setGeneratedLink(data.link);
4652
}
47-
} catch {
48-
// Keep UI state unchanged on failure; generate flow can be retried.
53+
} catch (e) {
54+
setErrorMessage(
55+
e instanceof Error
56+
? e.message
57+
: "Failed to generate one-shot link. Please try again.",
58+
);
4959
} finally {
5060
setIsLoading(false);
5161
}
@@ -97,6 +107,7 @@ export function Admin() {
97107
</CardHeader>
98108
<CardContent className="space-y-4">
99109
{successMessage && <Alert variant="success">{successMessage}</Alert>}
110+
{errorMessage && <Alert variant="error">{errorMessage}</Alert>}
100111
<form onSubmit={onGenerate} className="space-y-4">
101112
<Input
102113
id="target-email"

0 commit comments

Comments
 (0)