zero-http 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1250 -283
- package/documentation/config/db.js +25 -0
- package/documentation/config/middleware.js +44 -0
- package/documentation/config/tls.js +12 -0
- package/documentation/controllers/cookies.js +34 -0
- package/documentation/controllers/tasks.js +108 -0
- package/documentation/full-server.js +25 -184
- package/documentation/models/Task.js +21 -0
- package/documentation/public/data/api.json +404 -24
- package/documentation/public/data/docs.json +1139 -0
- package/documentation/public/data/examples.json +80 -2
- package/documentation/public/data/options.json +23 -8
- package/documentation/public/index.html +138 -99
- package/documentation/public/scripts/app.js +1 -3
- package/documentation/public/scripts/custom-select.js +189 -0
- package/documentation/public/scripts/data-sections.js +233 -250
- package/documentation/public/scripts/playground.js +270 -0
- package/documentation/public/scripts/ui.js +4 -3
- package/documentation/public/styles.css +56 -5
- package/documentation/public/vendor/icons/compress.svg +17 -17
- package/documentation/public/vendor/icons/database.svg +21 -0
- package/documentation/public/vendor/icons/env.svg +21 -0
- package/documentation/public/vendor/icons/fetch.svg +11 -14
- package/documentation/public/vendor/icons/security.svg +15 -0
- package/documentation/public/vendor/icons/sse.svg +12 -13
- package/documentation/public/vendor/icons/static.svg +12 -26
- package/documentation/public/vendor/icons/stream.svg +7 -13
- package/documentation/public/vendor/icons/validate.svg +17 -0
- package/documentation/routes/api.js +41 -0
- package/documentation/routes/core.js +20 -0
- package/documentation/routes/playground.js +29 -0
- package/documentation/routes/realtime.js +49 -0
- package/documentation/routes/uploads.js +71 -0
- package/index.js +62 -1
- package/lib/app.js +200 -8
- package/lib/body/json.js +28 -5
- package/lib/body/multipart.js +29 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/sendError.js +1 -0
- package/lib/body/text.js +1 -1
- package/lib/body/typeMatch.js +6 -2
- package/lib/body/urlencoded.js +5 -2
- package/lib/debug.js +345 -0
- package/lib/env/index.js +440 -0
- package/lib/errors.js +231 -0
- package/lib/http/request.js +219 -1
- package/lib/http/response.js +410 -6
- package/lib/middleware/compress.js +39 -6
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +13 -2
- package/lib/middleware/csrf.js +135 -0
- package/lib/middleware/errorHandler.js +90 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +7 -2
- package/lib/middleware/rateLimit.js +12 -1
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +95 -11
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +257 -0
- package/lib/orm/adapters/json.js +215 -0
- package/lib/orm/adapters/memory.js +383 -0
- package/lib/orm/adapters/mongo.js +444 -0
- package/lib/orm/adapters/mysql.js +272 -0
- package/lib/orm/adapters/postgres.js +394 -0
- package/lib/orm/adapters/sql-base.js +142 -0
- package/lib/orm/adapters/sqlite.js +311 -0
- package/lib/orm/index.js +276 -0
- package/lib/orm/model.js +895 -0
- package/lib/orm/query.js +807 -0
- package/lib/orm/schema.js +172 -0
- package/lib/router/index.js +136 -47
- package/lib/sse/stream.js +15 -3
- package/lib/ws/connection.js +19 -3
- package/lib/ws/handshake.js +3 -0
- package/lib/ws/index.js +3 -1
- package/lib/ws/room.js +222 -0
- package/package.json +15 -5
- package/types/app.d.ts +120 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +147 -0
- package/types/fetch.d.ts +43 -0
- package/types/index.d.ts +135 -0
- package/types/middleware.d.ts +292 -0
- package/types/orm.d.ts +610 -0
- package/types/request.d.ts +99 -0
- package/types/response.d.ts +142 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +119 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { Database } = require('../..');
|
|
3
|
+
const Task = require('../models/Task');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Connect to the database, register all models, and sync tables.
|
|
7
|
+
* Uses SQLite with a Database/ folder for file‑based persistence.
|
|
8
|
+
* Returns the Database instance for reuse.
|
|
9
|
+
*/
|
|
10
|
+
function initDatabase()
|
|
11
|
+
{
|
|
12
|
+
const db = Database.connect('sqlite', {
|
|
13
|
+
filename: path.join(__dirname, '..', 'Database', 'tasks.db'),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Register models
|
|
17
|
+
db.register(Task);
|
|
18
|
+
|
|
19
|
+
// Sync tables (non-blocking)
|
|
20
|
+
db.sync().then(() => console.log('ORM: SQLite tasks.db ready'));
|
|
21
|
+
|
|
22
|
+
return db;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { initDatabase };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const {
|
|
3
|
+
cors, json, urlencoded, text,
|
|
4
|
+
static: serveStatic, logger, compress,
|
|
5
|
+
helmet, timeout, requestId, cookieParser
|
|
6
|
+
} = require('../..');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register the standard middleware stack on the app.
|
|
10
|
+
* Order matters — security & utility first, then parsers, then static.
|
|
11
|
+
*/
|
|
12
|
+
function applyMiddleware(app)
|
|
13
|
+
{
|
|
14
|
+
app.use(logger({ format: 'dev' }));
|
|
15
|
+
app.use(requestId());
|
|
16
|
+
app.use(helmet({
|
|
17
|
+
contentSecurityPolicy: {
|
|
18
|
+
directives: {
|
|
19
|
+
defaultSrc: ["'self'"],
|
|
20
|
+
baseUri: ["'self'"],
|
|
21
|
+
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
22
|
+
scriptSrcAttr: ["'none'"],
|
|
23
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
24
|
+
connectSrc: ["'self'", 'wss:', 'ws:'],
|
|
25
|
+
imgSrc: ["'self'", 'data:'],
|
|
26
|
+
fontSrc: ["'self'", 'https:', 'data:'],
|
|
27
|
+
formAction: ["'self'"],
|
|
28
|
+
frameAncestors: ["'self'"],
|
|
29
|
+
objectSrc: ["'none'"],
|
|
30
|
+
upgradeInsecureRequests: []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
app.use(cors());
|
|
35
|
+
app.use(compress());
|
|
36
|
+
app.use(timeout(30000));
|
|
37
|
+
app.use(cookieParser());
|
|
38
|
+
app.use(json());
|
|
39
|
+
app.use(urlencoded());
|
|
40
|
+
app.use(text());
|
|
41
|
+
app.use(serveStatic(path.join(__dirname, '..', 'public')));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { applyMiddleware };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const CERT_PATH = '/www/server/panel/vhost/cert/z-http.com/fullchain.pem';
|
|
4
|
+
const KEY_PATH = '/www/server/panel/vhost/cert/z-http.com/privkey.pem';
|
|
5
|
+
|
|
6
|
+
const hasCerts = fs.existsSync(CERT_PATH) && fs.existsSync(KEY_PATH);
|
|
7
|
+
|
|
8
|
+
const tlsOpts = hasCerts
|
|
9
|
+
? { cert: fs.readFileSync(CERT_PATH), key: fs.readFileSync(KEY_PATH) }
|
|
10
|
+
: undefined;
|
|
11
|
+
|
|
12
|
+
module.exports = { hasCerts, tlsOpts };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
exports.list = (req, res) =>
|
|
2
|
+
{
|
|
3
|
+
res.json({
|
|
4
|
+
cookies: req.cookies || {},
|
|
5
|
+
signedCookies: req.signedCookies || {},
|
|
6
|
+
});
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
exports.set = (req, res) =>
|
|
10
|
+
{
|
|
11
|
+
const { name, value, options } = req.body || {};
|
|
12
|
+
if (!name || typeof name !== 'string')
|
|
13
|
+
return res.status(400).json({ error: 'name is required' });
|
|
14
|
+
|
|
15
|
+
const val = value != null ? value : '';
|
|
16
|
+
const opts = {};
|
|
17
|
+
if (options)
|
|
18
|
+
{
|
|
19
|
+
if (options.maxAge != null) opts.maxAge = Number(options.maxAge);
|
|
20
|
+
if (options.httpOnly != null) opts.httpOnly = Boolean(options.httpOnly);
|
|
21
|
+
if (options.secure != null) opts.secure = Boolean(options.secure);
|
|
22
|
+
if (options.sameSite) opts.sameSite = String(options.sameSite);
|
|
23
|
+
if (options.path) opts.path = String(options.path);
|
|
24
|
+
if (options.signed) opts.signed = true;
|
|
25
|
+
}
|
|
26
|
+
res.cookie(name, typeof val === 'object' ? val : String(val), opts);
|
|
27
|
+
res.json({ set: name, value: val, options: opts });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
exports.clear = (req, res) =>
|
|
31
|
+
{
|
|
32
|
+
res.clearCookie(req.params.name);
|
|
33
|
+
res.json({ cleared: req.params.name });
|
|
34
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const Task = require('../models/Task');
|
|
2
|
+
|
|
3
|
+
const VALID_STATUSES = ['pending', 'in-progress', 'done'];
|
|
4
|
+
|
|
5
|
+
function clampPriority(v) { return Math.max(0, Math.min(5, Number(v) || 0)); }
|
|
6
|
+
|
|
7
|
+
exports.list = async (req, res) =>
|
|
8
|
+
{
|
|
9
|
+
try
|
|
10
|
+
{
|
|
11
|
+
let q = Task.query();
|
|
12
|
+
if (req.query.status) q = q.where('status', '=', req.query.status);
|
|
13
|
+
if (req.query.priority) q = q.where('priority', '>=', Number(req.query.priority));
|
|
14
|
+
if (req.query.scope) q = q.scope(req.query.scope);
|
|
15
|
+
if (req.query.search) q = q.whereLike('title', `%${req.query.search}%`);
|
|
16
|
+
q = q.orderBy(req.query.sort || 'createdAt', req.query.order || 'desc');
|
|
17
|
+
const tasks = await q.exec();
|
|
18
|
+
const count = await Task.count();
|
|
19
|
+
res.json({ tasks, total: count });
|
|
20
|
+
}
|
|
21
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
exports.create = async (req, res) =>
|
|
25
|
+
{
|
|
26
|
+
try
|
|
27
|
+
{
|
|
28
|
+
const { title, status, priority } = req.body || {};
|
|
29
|
+
if (!title || typeof title !== 'string' || !title.trim())
|
|
30
|
+
return res.status(400).json({ error: 'title is required' });
|
|
31
|
+
|
|
32
|
+
const task = await Task.create({
|
|
33
|
+
title: title.trim().slice(0, 200),
|
|
34
|
+
status: VALID_STATUSES.includes(status) ? status : 'pending',
|
|
35
|
+
priority: clampPriority(priority),
|
|
36
|
+
});
|
|
37
|
+
res.status(201).json({ task });
|
|
38
|
+
}
|
|
39
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
exports.update = async (req, res) =>
|
|
43
|
+
{
|
|
44
|
+
try
|
|
45
|
+
{
|
|
46
|
+
const task = await Task.findById(Number(req.params.id));
|
|
47
|
+
if (!task) return res.status(404).json({ error: 'not found' });
|
|
48
|
+
|
|
49
|
+
const updates = {};
|
|
50
|
+
if (req.body.title != null) updates.title = String(req.body.title).trim().slice(0, 200);
|
|
51
|
+
if (req.body.status != null && VALID_STATUSES.includes(req.body.status)) updates.status = req.body.status;
|
|
52
|
+
if (req.body.priority != null) updates.priority = clampPriority(req.body.priority);
|
|
53
|
+
await task.update(updates);
|
|
54
|
+
res.json({ task });
|
|
55
|
+
}
|
|
56
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
exports.remove = async (req, res) =>
|
|
60
|
+
{
|
|
61
|
+
try
|
|
62
|
+
{
|
|
63
|
+
const task = await Task.findById(Number(req.params.id));
|
|
64
|
+
if (!task) return res.status(404).json({ error: 'not found' });
|
|
65
|
+
await task.delete();
|
|
66
|
+
res.json({ deleted: true, id: Number(req.params.id) });
|
|
67
|
+
}
|
|
68
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
exports.restore = async (req, res) =>
|
|
72
|
+
{
|
|
73
|
+
try
|
|
74
|
+
{
|
|
75
|
+
const rows = await Task.query().withDeleted().where('id', '=', Number(req.params.id)).exec();
|
|
76
|
+
const task = rows[0];
|
|
77
|
+
if (!task) return res.status(404).json({ error: 'not found' });
|
|
78
|
+
if (task.restore) await task.restore();
|
|
79
|
+
res.json({ task });
|
|
80
|
+
}
|
|
81
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
exports.removeAll = async (req, res) =>
|
|
85
|
+
{
|
|
86
|
+
try
|
|
87
|
+
{
|
|
88
|
+
const all = await Task.find();
|
|
89
|
+
for (const t of all) await t.delete();
|
|
90
|
+
res.json({ deleted: all.length });
|
|
91
|
+
}
|
|
92
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
exports.stats = async (req, res) =>
|
|
96
|
+
{
|
|
97
|
+
try
|
|
98
|
+
{
|
|
99
|
+
const total = await Task.count();
|
|
100
|
+
const pending = await Task.count({ status: 'pending' });
|
|
101
|
+
const inProgress = await Task.count({ status: 'in-progress' });
|
|
102
|
+
const done = await Task.count({ status: 'done' });
|
|
103
|
+
const avgPriority = await Task.query().avg('priority');
|
|
104
|
+
const maxPriority = await Task.query().max('priority');
|
|
105
|
+
res.json({ total, pending, inProgress, done, avgPriority: avgPriority || 0, maxPriority: maxPriority || 0 });
|
|
106
|
+
}
|
|
107
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
108
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zero-http full-server example
|
|
3
|
-
*
|
|
3
|
+
* Clean entry point — config, middleware, and routes are in separate modules.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// --- Crash protection ---
|
|
@@ -8,208 +8,49 @@ process.on('uncaughtException', (err) => { console.error('[UNCAUGHT]', err); });
|
|
|
8
8
|
process.on('unhandledRejection', (err) => { console.error('[UNHANDLED]', err); });
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const
|
|
12
|
-
const os = require('os');
|
|
13
|
-
const { createApp, cors, json, urlencoded, text, raw, multipart, static: serveStatic, fetch, logger, compress, Router } = require('..');
|
|
11
|
+
const { createApp, env, fetch } = require('..');
|
|
14
12
|
|
|
15
|
-
// ---
|
|
16
|
-
|
|
17
|
-
app.use(logger({ format: 'dev' }));
|
|
18
|
-
app.use(cors());
|
|
19
|
-
app.use(compress());
|
|
20
|
-
app.use(json());
|
|
21
|
-
app.use(urlencoded());
|
|
22
|
-
app.use(text());
|
|
23
|
-
app.use(serveStatic(path.join(__dirname, 'public')));
|
|
24
|
-
|
|
25
|
-
// --- Controllers ---
|
|
26
|
-
const rootController = require('./controllers/root');
|
|
27
|
-
const headersController = require('./controllers/headers');
|
|
28
|
-
const echoController = require('./controllers/echo');
|
|
29
|
-
const uploadsController = require('./controllers/uploads');
|
|
30
|
-
|
|
31
|
-
// --- Core Routes ---
|
|
32
|
-
app.get('/', rootController.getRoot);
|
|
33
|
-
app.get('/headers', headersController.getHeaders);
|
|
34
|
-
app.post('/echo-json', echoController.echoJson);
|
|
35
|
-
app.post('/echo', echoController.echo);
|
|
36
|
-
app.post('/echo-urlencoded', echoController.echoUrlencoded);
|
|
37
|
-
app.post('/echo-text', echoController.echoText);
|
|
38
|
-
app.post('/echo-raw', raw(), echoController.echoRaw);
|
|
39
|
-
|
|
40
|
-
// --- Uploads and Trash ---
|
|
41
|
-
const uploadsDir = path.join(__dirname, 'uploads');
|
|
42
|
-
uploadsController.ensureUploadsDir(uploadsDir);
|
|
43
|
-
|
|
44
|
-
// Serve uploaded files from /uploads path using built-in path-prefix middleware
|
|
45
|
-
app.use('/uploads', serveStatic(uploadsDir));
|
|
46
|
-
|
|
47
|
-
// Upload, delete, restore, and trash routes
|
|
48
|
-
app.post('/upload', multipart({ maxFileSize: 5 * 1024 * 1024, dir: uploadsDir }), uploadsController.upload(uploadsDir));
|
|
49
|
-
app.delete('/uploads/:name', uploadsController.deleteUpload(uploadsDir));
|
|
50
|
-
app.delete('/uploads', uploadsController.deleteAllUploads(uploadsDir));
|
|
51
|
-
app.post('/uploads/:name/restore', uploadsController.restoreUpload(uploadsDir));
|
|
52
|
-
app.get('/uploads-trash-list', uploadsController.listTrash(uploadsDir));
|
|
53
|
-
app.delete('/uploads-trash/:name', uploadsController.deleteTrashItem(uploadsDir));
|
|
54
|
-
app.delete('/uploads-trash', uploadsController.emptyTrash(uploadsDir));
|
|
55
|
-
|
|
56
|
-
// --- Trash Retention ---
|
|
57
|
-
const TRASH_RETENTION_DAYS = Number(process.env.TRASH_RETENTION_DAYS || 7);
|
|
58
|
-
const TRASH_RETENTION_MS = TRASH_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
59
|
-
|
|
60
|
-
function autoEmptyTrash()
|
|
61
|
-
{
|
|
62
|
-
try
|
|
63
|
-
{
|
|
64
|
-
const trash = path.join(uploadsDir, '.trash');
|
|
65
|
-
if (!fs.existsSync(trash)) return;
|
|
66
|
-
const now = Date.now();
|
|
67
|
-
const removed = [];
|
|
68
|
-
for (const f of fs.readdirSync(trash))
|
|
69
|
-
{
|
|
70
|
-
try
|
|
71
|
-
{
|
|
72
|
-
const p = path.join(trash, f);
|
|
73
|
-
const st = fs.statSync(p);
|
|
74
|
-
if (now - st.mtimeMs > TRASH_RETENTION_MS)
|
|
75
|
-
{
|
|
76
|
-
fs.unlinkSync(p);
|
|
77
|
-
removed.push(f);
|
|
78
|
-
try { fs.unlinkSync(path.join(trash, '.thumbs', f + '-thumb.svg')); } catch (e) { }
|
|
79
|
-
}
|
|
80
|
-
} catch (e) { }
|
|
81
|
-
}
|
|
82
|
-
if (removed.length) console.log(`autoEmptyTrash: removed ${removed.length} file(s)`);
|
|
83
|
-
} catch (e) { console.error('autoEmptyTrash error:', e); }
|
|
84
|
-
}
|
|
13
|
+
// --- Environment ---
|
|
14
|
+
env.load(path.join(__dirname, '..', '.env'));
|
|
85
15
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// --- Uploads Listings ---
|
|
90
|
-
app.get('/uploads-list', uploadsController.listUploads(uploadsDir));
|
|
91
|
-
app.get('/uploads-all', uploadsController.listAll(uploadsDir));
|
|
92
|
-
|
|
93
|
-
// --- Temp Cleanup ---
|
|
94
|
-
const cleanupController = require('./controllers/cleanup');
|
|
95
|
-
app.post('/cleanup', cleanupController.cleanup(path.join(os.tmpdir(), 'zero-http-uploads')));
|
|
96
|
-
|
|
97
|
-
// --- Proxy ---
|
|
98
|
-
const proxyController = require('./controllers/proxy');
|
|
99
|
-
const proxyFetch = (typeof globalThis !== 'undefined' && globalThis.fetch) || fetch;
|
|
100
|
-
app.get('/proxy', proxyController.proxy(proxyFetch));
|
|
101
|
-
|
|
102
|
-
// --- WebSocket Chat ---
|
|
103
|
-
const wsClients = new Set();
|
|
104
|
-
|
|
105
|
-
app.ws('/ws/chat', { maxPayload: 64 * 1024, pingInterval: 25000 }, (ws, req) =>
|
|
106
|
-
{
|
|
107
|
-
ws.data.name = ws.query.name || 'anon';
|
|
108
|
-
wsClients.add(ws);
|
|
109
|
-
ws.send(JSON.stringify({ type: 'system', text: 'Welcome, ' + ws.data.name + '!' }));
|
|
110
|
-
|
|
111
|
-
// Broadcast join
|
|
112
|
-
for (const c of wsClients)
|
|
113
|
-
{
|
|
114
|
-
if (c !== ws && c.readyState === 1)
|
|
115
|
-
c.send(JSON.stringify({ type: 'system', text: ws.data.name + ' joined' }));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
ws.on('message', (msg) =>
|
|
119
|
-
{
|
|
120
|
-
// Broadcast to all clients including sender
|
|
121
|
-
const payload = JSON.stringify({ type: 'message', name: ws.data.name, text: String(msg) });
|
|
122
|
-
for (const c of wsClients)
|
|
123
|
-
{
|
|
124
|
-
if (c.readyState === 1) c.send(payload);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
ws.on('close', () =>
|
|
129
|
-
{
|
|
130
|
-
wsClients.delete(ws);
|
|
131
|
-
for (const c of wsClients)
|
|
132
|
-
{
|
|
133
|
-
if (c.readyState === 1)
|
|
134
|
-
c.send(JSON.stringify({ type: 'system', text: ws.data.name + ' left' }));
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// --- Server-Sent Events ---
|
|
140
|
-
const sseClients = new Set();
|
|
141
|
-
|
|
142
|
-
app.get('/sse/events', (req, res) =>
|
|
143
|
-
{
|
|
144
|
-
const sse = res.sse({ retry: 5000, autoId: true, keepAlive: 30000 });
|
|
145
|
-
sseClients.add(sse);
|
|
146
|
-
sse.send({ type: 'connected', clients: sseClients.size });
|
|
147
|
-
|
|
148
|
-
sse.on('close', () => sseClients.delete(sse));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
app.post('/sse/broadcast', (req, res) =>
|
|
152
|
-
{
|
|
153
|
-
const data = req.body || {};
|
|
154
|
-
for (const sse of sseClients)
|
|
155
|
-
{
|
|
156
|
-
sse.event('broadcast', data);
|
|
157
|
-
}
|
|
158
|
-
res.json({ sent: sseClients.size });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// --- Router Demo (API sub-app) ---
|
|
162
|
-
const apiRouter = Router();
|
|
163
|
-
|
|
164
|
-
apiRouter.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() }));
|
|
165
|
-
apiRouter.get('/info', (req, res) => res.json({
|
|
166
|
-
secure: req.secure,
|
|
167
|
-
protocol: req.protocol,
|
|
168
|
-
ip: req.ip,
|
|
169
|
-
method: req.method,
|
|
170
|
-
url: req.url
|
|
171
|
-
}));
|
|
16
|
+
// --- App ---
|
|
17
|
+
const app = createApp();
|
|
172
18
|
|
|
173
|
-
|
|
19
|
+
// --- Middleware ---
|
|
20
|
+
const { applyMiddleware } = require('./config/middleware');
|
|
21
|
+
applyMiddleware(app);
|
|
174
22
|
|
|
175
|
-
// ---
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
23
|
+
// --- Routes ---
|
|
24
|
+
require('./routes/core')(app);
|
|
25
|
+
require('./routes/uploads')(app);
|
|
26
|
+
require('./routes/realtime')(app);
|
|
27
|
+
require('./routes/playground')(app);
|
|
28
|
+
require('./routes/api')(app);
|
|
181
29
|
|
|
182
|
-
// --- TLS
|
|
183
|
-
const
|
|
184
|
-
const keyPath = '/www/server/panel/vhost/cert/z-http.com/privkey.pem';
|
|
185
|
-
const hasCerts = fs.existsSync(certPath) && fs.existsSync(keyPath);
|
|
30
|
+
// --- TLS ---
|
|
31
|
+
const { hasCerts, tlsOpts } = require('./config/tls');
|
|
186
32
|
|
|
187
|
-
|
|
188
|
-
? { cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) }
|
|
189
|
-
: undefined;
|
|
190
|
-
|
|
191
|
-
// --- Server Startup ---
|
|
33
|
+
// --- Start ---
|
|
192
34
|
const port = process.env.PORT || 7273;
|
|
193
|
-
|
|
35
|
+
app.listen(port, tlsOpts, () =>
|
|
194
36
|
{
|
|
195
37
|
const proto = hasCerts ? 'https' : 'http';
|
|
196
38
|
console.log(`zero-http full-server listening on ${proto}://localhost:${port}`);
|
|
197
39
|
if (process.argv.includes('--test')) runTests(port).catch(console.error);
|
|
198
40
|
});
|
|
199
41
|
|
|
200
|
-
|
|
201
42
|
/** Quick smoke tests using built-in fetch */
|
|
202
43
|
async function runTests(port)
|
|
203
44
|
{
|
|
204
45
|
const proto = hasCerts ? 'https' : 'http';
|
|
205
|
-
const base
|
|
46
|
+
const base = `${proto}://localhost:${port}`;
|
|
206
47
|
console.log('running smoke tests against', base);
|
|
207
48
|
|
|
208
49
|
const doReq = async (label, promise) =>
|
|
209
50
|
{
|
|
210
51
|
try
|
|
211
52
|
{
|
|
212
|
-
const r
|
|
53
|
+
const r = await promise;
|
|
213
54
|
const ct = r.headers.get('content-type') || '';
|
|
214
55
|
const body = ct.includes('json') ? await r.json() : await r.text();
|
|
215
56
|
console.log(` ${label}`, r.status, JSON.stringify(body));
|
|
@@ -217,10 +58,10 @@ async function runTests(port)
|
|
|
217
58
|
catch (e) { console.error(` ${label} error:`, e.message); }
|
|
218
59
|
};
|
|
219
60
|
|
|
220
|
-
await doReq('GET /',
|
|
221
|
-
await doReq('POST /echo-json',
|
|
61
|
+
await doReq('GET /', fetch(base + '/'));
|
|
62
|
+
await doReq('POST /echo-json', fetch(base + '/echo-json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ a: 1 }) }));
|
|
222
63
|
await doReq('POST /echo-urlencoded', fetch(base + '/echo-urlencoded', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'foo=bar' }));
|
|
223
|
-
await doReq('POST /echo-text',
|
|
224
|
-
await doReq('GET /headers',
|
|
64
|
+
await doReq('POST /echo-text', fetch(base + '/echo-text', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: 'hello' }));
|
|
65
|
+
await doReq('GET /headers', fetch(base + '/headers'));
|
|
225
66
|
console.log('smoke tests complete');
|
|
226
67
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { Model, TYPES } = require('../..');
|
|
2
|
+
|
|
3
|
+
class Task extends Model
|
|
4
|
+
{
|
|
5
|
+
static table = 'tasks';
|
|
6
|
+
static timestamps = true;
|
|
7
|
+
static softDelete = true;
|
|
8
|
+
static schema = {
|
|
9
|
+
id: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },
|
|
10
|
+
title: { type: TYPES.STRING, required: true, maxLength: 200 },
|
|
11
|
+
status: { type: TYPES.STRING, default: 'pending', enum: ['pending', 'in-progress', 'done'] },
|
|
12
|
+
priority: { type: TYPES.INTEGER, default: 0, min: 0, max: 5 },
|
|
13
|
+
};
|
|
14
|
+
static scopes = {
|
|
15
|
+
active: (q) => q.where('status', '!=', 'done'),
|
|
16
|
+
done: (q) => q.where('status', '=', 'done'),
|
|
17
|
+
highPriority: (q) => q.where('priority', '>=', 3),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = Task;
|