Skip to content

Commit ff5c407

Browse files
committed
feat: terminal-friendly API — text/plain, /push alias, X-Passphrase header
1 parent bcedcc6 commit ff5c407

File tree

1 file changed

+48
-9
lines changed

1 file changed

+48
-9
lines changed

server/server.js

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,16 @@ app.use(helmet({
6363

6464
app.use((req, res, next) => {
6565
if (req.path === '/upload') return next();
66+
67+
const ct = req.headers['content-type'] || '';
68+
69+
if (ct.startsWith('text/plain')) {
70+
return express.text({ limit: '10mb' })(req, res, next);
71+
}
72+
6673
express.json()(req, res, next);
6774
});
75+
6876
app.use(cookieParser());
6977
app.use(requireAuth);
7078
app.use("/uploads", (req, res, next) => {
@@ -225,19 +233,28 @@ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
225233
const sessions = new Map();
226234

227235
function requireAuth(req, res, next) {
228-
if (!config.auth.passphrase) return next(); // no passphrase set, skip
236+
if (!config.auth.passphrase) return next();
229237

230-
// Allow the login route itself through
231238
if (req.path === "/login" || req.path === "/info" || req.path === "/health") return next();
232239

240+
// Terminal / API clients — accept passphrase via header
241+
const headerPass = req.headers["x-passphrase"];
242+
if (headerPass && headerPass === config.auth.passphrase) return next();
233243

234-
// Check cookie holds a valid session token
244+
// Browser clients — check session cookie
235245
const cookie = req.cookies[COOKIE_NAME];
236246
if (cookie && sessions.has(cookie)) return next();
237247

238-
// Not authenticated
239248
if (req.path.startsWith("/socket.io")) return next();
240-
if (req.headers["content-type"] === "application/json" || req.xhr) {
249+
250+
// Return JSON 401 for any non-browser request
251+
const ct = req.headers["content-type"] || "";
252+
const isApiRequest = ct.startsWith("application/json") ||
253+
ct.startsWith("text/plain") ||
254+
req.xhr ||
255+
req.headers["x-passphrase"] !== undefined;
256+
257+
if (isApiRequest) {
241258
return res.status(401).json({ error: "Unauthorized" });
242259
}
243260

@@ -453,9 +470,27 @@ app.use((err, req, res, next) => {
453470
next(err);
454471
});
455472

456-
/* TEXT/LINK */
457-
app.post("/text", dropLimiter, (req, res) => {
458-
const { content, channel, uploader } = req.body;
473+
/* TEXT/LINK — accepts application/json and text/plain */
474+
function handleTextPost(req, res) {
475+
let content, channel, uploader;
476+
477+
if (req.is("text/plain")) {
478+
// Terminal path — body is raw string, metadata comes from headers
479+
content = typeof req.body === "string" ? req.body : String(req.body);
480+
channel = (req.headers["x-channel"] || "general").trim();
481+
uploader = (req.headers["x-uploader"] || "terminal").trim();
482+
} else {
483+
// Browser / JSON path — existing behaviour unchanged
484+
({ content, channel, uploader } = req.body || {});
485+
}
486+
487+
if (!content || !content.trim()) {
488+
return res.status(400).json({ error: "Content is required" });
489+
}
490+
491+
if (!channel) {
492+
return res.status(400).json({ error: "Channel is required" });
493+
}
459494

460495
const item = {
461496
type: "text",
@@ -476,7 +511,11 @@ app.post("/text", dropLimiter, (req, res) => {
476511
res.json(item);
477512
}
478513
);
479-
});
514+
}
515+
516+
app.post("/text", dropLimiter, handleTextPost);
517+
app.post("/push", dropLimiter, handleTextPost); // terminal-friendly alias
518+
480519

481520
/* DELETE ITEM */
482521
app.delete("/item/:id", (req, res) => {

0 commit comments

Comments
 (0)