@radatek/microserver 2.0.2 → 2.1.0

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 2.0.2
3
+ * @version 2.1.0
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -8,6 +8,7 @@
8
8
  import http from 'http';
9
9
  import net from 'net';
10
10
  import { Readable } from 'stream';
11
+ import fs from 'fs';
11
12
  import { EventEmitter } from 'events';
12
13
  export declare class Warning extends Error {
13
14
  constructor(text: string);
@@ -40,7 +41,6 @@ export declare abstract class Plugin {
40
41
  name?: string;
41
42
  priority?: number;
42
43
  handler?(req: ServerRequest, res: ServerResponse, next: Function): void;
43
- controller?: typeof Controller;
44
44
  routes?: () => Routes | Routes;
45
45
  constructor(router: Router, ...args: any);
46
46
  }
@@ -65,8 +65,8 @@ export declare class ServerRequest extends http.IncomingMessage {
65
65
  baseUrl: string;
66
66
  /** Original url */
67
67
  originalUrl?: string;
68
- /** GET parameters */
69
- get: {
68
+ /** Query parameters */
69
+ query: {
70
70
  [key: string]: string;
71
71
  };
72
72
  /** Router named parameters */
@@ -117,17 +117,20 @@ export declare class ServerResponse extends http.ServerResponse {
117
117
  headersOnly: boolean;
118
118
  private constructor();
119
119
  /** Send error reponse */
120
- error(error: string | number | Error, text?: string): void;
120
+ error(error: string | number | Error): void;
121
121
  /** Sets Content-Type acording to data and sends response */
122
122
  send(data?: string | Buffer | Error | Readable | object): void;
123
123
  /** Send json response */
124
124
  json(data: any): void;
125
125
  /** Send json response in form { success: false, error: err } */
126
- jsonError(error: string | number | object | Error, code?: number): void;
126
+ jsonError(error: string | number | object | Error): void;
127
127
  /** Send json response in form { success: true, ... } */
128
- jsonSuccess(data?: object | string, code?: number): void;
128
+ jsonSuccess(data?: object | string): void;
129
129
  /** Send redirect response to specified URL with optional status code (default: 302) */
130
130
  redirect(code: number | string, url?: string): void;
131
+ /** Set status code */
132
+ status(code: number): this;
133
+ download(path: string, filename?: string): void;
131
134
  }
132
135
  /** WebSocket options */
133
136
  export interface WebSocketOptions {
@@ -376,7 +379,7 @@ export declare class MicroServer extends EventEmitter {
376
379
  listen(config?: ListenConfig): Promise<unknown>;
377
380
  /** bind middleware or create one from string like: 'redirect:302,https://redirect.to', 'error:422', 'param:name=value', 'acl:users/get', 'model:User', 'group:Users', 'user:admin' */
378
381
  bind(fn: string | Function | object): Function;
379
- /** Add middleware, routes, etc.. see {Router.add} */
382
+ /** Add middleware, routes, etc.. see {router.use} */
380
383
  use(...args: any): MicroServer;
381
384
  /** Default server handler */
382
385
  handler(req: ServerRequest, res: ServerResponse): void;
@@ -389,17 +392,19 @@ export declare class MicroServer extends EventEmitter {
389
392
  handlerUpgrade(req: ServerRequest, socket: net.Socket, head: any): void;
390
393
  /** Close server instance */
391
394
  close(): Promise<void>;
392
- /** Add route, alias to `server.router.add('GET ' + url, ...args)` */
395
+ /** Add route, alias to `server.router.use(url, ...args)` */
396
+ all(url: string, ...args: any): MicroServer;
397
+ /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
393
398
  get(url: string, ...args: any): MicroServer;
394
- /** Add route, alias to `server.router.add('POST ' + url, ...args)` */
399
+ /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
395
400
  post(url: string, ...args: any): MicroServer;
396
- /** Add route, alias to `server.router.add('PUT ' + url, ...args)` */
401
+ /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
397
402
  put(url: string, ...args: any): MicroServer;
398
- /** Add route, alias to `server.router.add('PATCH ' + url, ...args)` */
403
+ /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
399
404
  patch(url: string, ...args: any): MicroServer;
400
- /** Add route, alias to `server.router.add('DELETE ' + url, ...args)` */
405
+ /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
401
406
  delete(url: string, ...args: any): MicroServer;
402
- /** Add websocket handler, alias to `server.router.add('WEBSOCKET ' + url, ...args)` */
407
+ /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
403
408
  websocket(url: string, ...args: any): MicroServer;
404
409
  /** Add router hook, alias to `server.router.hook(url, ...args)` */
405
410
  hook(url: string, ...args: any): MicroServer;
@@ -429,6 +434,26 @@ export interface StaticOptions {
429
434
  /** Max file age in seconds */
430
435
  maxAge?: number;
431
436
  }
437
+ export interface ServeFileOptions {
438
+ /** path */
439
+ path: string;
440
+ /** root */
441
+ root?: string;
442
+ /** file name */
443
+ filename?: string;
444
+ /** file mime type */
445
+ mimeType?: string;
446
+ /** last modified date */
447
+ lastModified?: boolean;
448
+ /** etag */
449
+ etag?: boolean;
450
+ /** max age */
451
+ maxAge?: number;
452
+ /** range */
453
+ range?: boolean;
454
+ /** stat */
455
+ stats?: fs.Stats;
456
+ }
432
457
  /** Proxy plugin options */
433
458
  export interface ProxyPluginOptions {
434
459
  /** Base path */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 2.0.2
3
+ * @version 2.1.0
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -12,7 +12,7 @@ import tls from 'tls';
12
12
  import querystring from 'querystring';
13
13
  import { Readable } from 'stream';
14
14
  import fs from 'fs';
15
- import path from 'path';
15
+ import path, { basename, extname } from 'path';
16
16
  import crypto from 'crypto';
17
17
  import zlib from 'zlib';
18
18
  import { EventEmitter } from 'events';
@@ -92,8 +92,8 @@ export class ServerRequest extends http.IncomingMessage {
92
92
  this.pathname = pathname;
93
93
  this.path = pathname.slice(pathname.lastIndexOf('/'));
94
94
  this.baseUrl = pathname.slice(0, pathname.length - this.path.length);
95
- this.get = {};
96
- parsedUrl.searchParams.forEach((v, k) => this.get[k] = v);
95
+ this.query = {};
96
+ parsedUrl.searchParams.forEach((v, k) => this.query[k] = v);
97
97
  }
98
98
  /** Rewrite request url */
99
99
  rewrite(url) {
@@ -313,8 +313,9 @@ export class ServerResponse extends http.ServerResponse {
313
313
  this.statusCode = 200;
314
314
  }
315
315
  /** Send error reponse */
316
- error(error, text) {
316
+ error(error) {
317
317
  let code = 0;
318
+ let text;
318
319
  if (error instanceof Error) {
319
320
  if ('statusCode' in error)
320
321
  code = error.statusCode;
@@ -322,7 +323,7 @@ export class ServerResponse extends http.ServerResponse {
322
323
  }
323
324
  else if (typeof error === 'number') {
324
325
  code = error;
325
- text = text || commonCodes[code] || 'Error';
326
+ text = commonCodes[code] || 'Error';
326
327
  }
327
328
  else
328
329
  text = error.toString();
@@ -399,19 +400,17 @@ export class ServerResponse extends http.ServerResponse {
399
400
  this.send(data);
400
401
  }
401
402
  /** Send json response in form { success: false, error: err } */
402
- jsonError(error, code) {
403
+ jsonError(error) {
403
404
  this.isJson = true;
404
- this.statusCode = code || 200;
405
405
  if (typeof error === 'number')
406
- [code, error] = [error, http.STATUS_CODES[error] || 'Error'];
406
+ error = http.STATUS_CODES[error] || 'Error';
407
407
  if (error instanceof Error)
408
408
  return this.json(error);
409
409
  this.json(typeof error === 'string' ? { success: false, error } : { success: false, ...error });
410
410
  }
411
411
  /** Send json response in form { success: true, ... } */
412
- jsonSuccess(data, code) {
412
+ jsonSuccess(data) {
413
413
  this.isJson = true;
414
- this.statusCode = code || 200;
415
414
  if (data instanceof Error)
416
415
  return this.json(data);
417
416
  this.json(typeof data === 'string' ? { success: true, message: data } : { success: true, ...data });
@@ -427,6 +426,18 @@ export class ServerResponse extends http.ServerResponse {
427
426
  this.statusCode = code || 302;
428
427
  this.end();
429
428
  }
429
+ /** Set status code */
430
+ status(code) {
431
+ this.statusCode = code;
432
+ return this;
433
+ }
434
+ download(path, filename) {
435
+ StaticPlugin.serveFile(this.req, this, {
436
+ path: path,
437
+ filename: filename || basename(path),
438
+ mimeType: StaticPlugin.mimeTypes[extname(path)] || 'application/octet-stream'
439
+ });
440
+ }
430
441
  }
431
442
  const EMPTY_BUFFER = Buffer.alloc(0);
432
443
  const DEFLATE_TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
@@ -1193,8 +1204,6 @@ export class Router extends EventEmitter {
1193
1204
  else
1194
1205
  this.use(plugin.routes);
1195
1206
  }
1196
- if (plugin.controller)
1197
- this.use(plugin.controller);
1198
1207
  return this;
1199
1208
  }
1200
1209
  /** Add hook */
@@ -1438,7 +1447,7 @@ export class MicroServer extends EventEmitter {
1438
1447
  throw new Error('Invalid middleware: ' + String.toString.call(fn));
1439
1448
  return fn.bind(this);
1440
1449
  }
1441
- /** Add middleware, routes, etc.. see {Router.add} */
1450
+ /** Add middleware, routes, etc.. see {router.use} */
1442
1451
  use(...args) {
1443
1452
  this.router.use(...args);
1444
1453
  return this;
@@ -1587,32 +1596,37 @@ export class MicroServer extends EventEmitter {
1587
1596
  this.emit('close');
1588
1597
  });
1589
1598
  }
1590
- /** Add route, alias to `server.router.add('GET ' + url, ...args)` */
1599
+ /** Add route, alias to `server.router.use(url, ...args)` */
1600
+ all(url, ...args) {
1601
+ this.router.use(url, ...args);
1602
+ return this;
1603
+ }
1604
+ /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
1591
1605
  get(url, ...args) {
1592
1606
  this.router.use('GET ' + url, ...args);
1593
1607
  return this;
1594
1608
  }
1595
- /** Add route, alias to `server.router.add('POST ' + url, ...args)` */
1609
+ /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
1596
1610
  post(url, ...args) {
1597
1611
  this.router.use('POST ' + url, ...args);
1598
1612
  return this;
1599
1613
  }
1600
- /** Add route, alias to `server.router.add('PUT ' + url, ...args)` */
1614
+ /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
1601
1615
  put(url, ...args) {
1602
1616
  this.router.use('PUT ' + url, ...args);
1603
1617
  return this;
1604
1618
  }
1605
- /** Add route, alias to `server.router.add('PATCH ' + url, ...args)` */
1619
+ /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
1606
1620
  patch(url, ...args) {
1607
1621
  this.router.use('PATCH ' + url, ...args);
1608
1622
  return this;
1609
1623
  }
1610
- /** Add route, alias to `server.router.add('DELETE ' + url, ...args)` */
1624
+ /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
1611
1625
  delete(url, ...args) {
1612
1626
  this.router.use('DELETE ' + url, ...args);
1613
1627
  return this;
1614
1628
  }
1615
- /** Add websocket handler, alias to `server.router.add('WEBSOCKET ' + url, ...args)` */
1629
+ /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
1616
1630
  websocket(url, ...args) {
1617
1631
  this.router.use('WEBSOCKET ' + url, ...args);
1618
1632
  return this;
@@ -1697,7 +1711,7 @@ class StaticPlugin extends Plugin {
1697
1711
  options = {};
1698
1712
  if (typeof options === 'string')
1699
1713
  options = { path: options };
1700
- this.mimeTypes = { ...StaticPlugin.mimeTypes, ...options.mimeTypes };
1714
+ this.mimeTypes = options.mimeTypes ? { ...StaticPlugin.mimeTypes, ...options.mimeTypes } : Object.freeze(StaticPlugin.mimeTypes);
1701
1715
  this.root = path.resolve((options.root || options?.path || 'public').replace(/^\//, '')) + path.sep;
1702
1716
  this.ignore = (options.ignore || []).map((p) => path.normalize(path.join(this.root, p)) + path.sep);
1703
1717
  this.index = options.index || 'index.html';
@@ -1714,7 +1728,7 @@ class StaticPlugin extends Plugin {
1714
1728
  let filename = path.normalize(path.join(this.root, (req.params && req.params.path) || req.pathname));
1715
1729
  if (!filename.startsWith(this.root)) // check root access
1716
1730
  return next();
1717
- const firstch = path.basename(filename)[0];
1731
+ const firstch = basename(filename)[0];
1718
1732
  if (firstch === '.' || firstch === '_') // hidden file
1719
1733
  return next();
1720
1734
  if (filename.endsWith(path.sep))
@@ -1736,27 +1750,61 @@ class StaticPlugin extends Plugin {
1736
1750
  req.filename = filename;
1737
1751
  return handler.call(this, req, res, next);
1738
1752
  }
1739
- const etagMatch = req.headers['if-none-match'];
1740
- const etagTime = req.headers['if-modified-since'];
1741
- const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1742
- res.setHeader('Content-Type', mimeType);
1743
- if (this.lastModified || req.params.lastModified)
1753
+ StaticPlugin.serveFile(req, res, {
1754
+ path: filename,
1755
+ mimeType,
1756
+ stats
1757
+ });
1758
+ });
1759
+ }
1760
+ static serveFile(req, res, options) {
1761
+ const filePath = options.root ? path.join(options.root, options.path) : options.path;
1762
+ const statRes = (err, stats) => {
1763
+ if (err)
1764
+ return res.error(err);
1765
+ if (!stats.isFile())
1766
+ return res.error(404);
1767
+ if (!res.getHeader('Content-Type')) {
1768
+ if (options.mimeType)
1769
+ res.setHeader('Content-Type', options.mimeType);
1770
+ else
1771
+ res.setHeader('Content-Type', this.mimeTypes[path.extname(options.path)] || 'application/octet-stream');
1772
+ }
1773
+ if (options.filename)
1774
+ res.setHeader('Content-Disposition', 'attachment; filename="' + options.filename + '"');
1775
+ if (options.lastModified !== false)
1744
1776
  res.setHeader('Last-Modified', stats.mtime.toUTCString());
1745
- if (this.etag || req.params.etag)
1746
- res.setHeader('Etag', etag);
1747
- if (this.maxAge || req.params.maxAge)
1748
- res.setHeader('Cache-Control', 'max-age=' + (this.maxAge || req.params.maxAge));
1777
+ res.setHeader('Content-Length', stats.size);
1778
+ if (options.etag !== false) {
1779
+ const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1780
+ if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === stats.mtime.toUTCString()) {
1781
+ res.statusCode = 304;
1782
+ res.headersOnly = true;
1783
+ }
1784
+ }
1785
+ if (options.maxAge)
1786
+ res.setHeader('Cache-Control', 'max-age=' + options.maxAge);
1749
1787
  if (res.headersOnly) {
1750
- res.setHeader('Content-Length', stats.size);
1751
- return res.end();
1788
+ res.end();
1789
+ return;
1752
1790
  }
1753
- if (etagMatch === etag || etagTime === stats.mtime.toUTCString()) {
1754
- res.statusCode = 304;
1755
- return res.end();
1791
+ const streamOptions = { start: 0, end: stats.size - 1 };
1792
+ if (options.range !== false) {
1793
+ const range = req.headers['range'];
1794
+ if (range && range.startsWith('bytes=')) {
1795
+ const parts = range.slice(6).split('-');
1796
+ streamOptions.start = parseInt(parts[0]) || 0;
1797
+ streamOptions.end = parts[1] ? parseInt(parts[1]) : stats.size - 1;
1798
+ res.setHeader('Content-Range', `bytes ${streamOptions.start}-${streamOptions.end}/${stats.size}`);
1799
+ res.setHeader('Content-Length', streamOptions.end - streamOptions.start + 1);
1800
+ }
1756
1801
  }
1757
- res.setHeader('Content-Length', stats.size);
1758
- fs.createReadStream(filename).pipe(res);
1759
- });
1802
+ fs.createReadStream(filePath, streamOptions).pipe(res);
1803
+ };
1804
+ if (!options.stats)
1805
+ fs.stat(filePath, statRes);
1806
+ else
1807
+ statRes(null, options.stats);
1760
1808
  }
1761
1809
  }
1762
1810
  /** Default mime types */
@@ -1770,12 +1818,17 @@ StaticPlugin.mimeTypes = {
1770
1818
  '.css': 'text/css',
1771
1819
  '.png': 'image/png',
1772
1820
  '.jpg': 'image/jpeg',
1773
- '.mp3': 'audio/mpeg',
1774
1821
  '.svg': 'image/svg+xml',
1822
+ '.mp3': 'audio/mpeg',
1823
+ '.ogg': 'audio/ogg',
1824
+ '.mp4': 'video/mp4',
1775
1825
  '.pdf': 'application/pdf',
1776
1826
  '.woff': 'application/x-font-woff',
1777
1827
  '.woff2': 'application/x-font-woff2',
1778
- '.ttf': 'application/x-font-ttf'
1828
+ '.ttf': 'application/x-font-ttf',
1829
+ '.gz': 'application/gzip',
1830
+ '.zip': 'application/zip',
1831
+ '.tgz': 'application/gzip',
1779
1832
  };
1780
1833
  MicroServer.plugins.static = StaticPlugin;
1781
1834
  export class ProxyPlugin extends Plugin {
@@ -2201,7 +2254,7 @@ class AuthPlugin extends Plugin {
2201
2254
  if (sid)
2202
2255
  token = sid.slice(sid.indexOf('=') + 1);
2203
2256
  if (!token)
2204
- token = req.get.token;
2257
+ token = req.query.token;
2205
2258
  if (token) {
2206
2259
  const now = new Date().getTime();
2207
2260
  let usr, expire;
@@ -2794,7 +2847,7 @@ export class Model {
2794
2847
  /** Microserver middleware */
2795
2848
  handler(req, res) {
2796
2849
  res.isJson = true;
2797
- let filter, filterStr = req.get.filter;
2850
+ let filter, filterStr = req.query.filter;
2798
2851
  if (filterStr) {
2799
2852
  try {
2800
2853
  if (!filterStr.startsWith('{'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radatek/microserver",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "HTTP MicroServer",
5
5
  "author": "Darius Kisonas",
6
6
  "license": "MIT",
package/readme.md CHANGED
@@ -1,35 +1,36 @@
1
1
  ## HTTP MicroServer
2
2
 
3
- Lightweight all-in-one http web server
3
+ Lightweight all-in-one http web server without dependencies
4
4
 
5
5
  Features:
6
6
  - fast REST API router
7
7
  - form/json body decoder
8
8
  - file upload
9
+ - websockets
9
10
  - authentication
10
11
  - plain/hashed passwords
11
12
  - virtual hosts
12
13
  - static files
14
+ - rewrite
15
+ - redirect
13
16
  - reverse proxy routes
14
17
  - trust ip for reverse proxy
15
18
  - json file storage with autosave
16
- - websockets (single file module from ws package)
17
19
  - tls with automatic certificate reload
18
20
  - data model with validation and mongodb interface
19
- - simple file/memory storage adapter for model
21
+ - simple file/memory storage
20
22
  - promises as middleware
21
23
  - controller class
22
24
  - access rights per route
23
25
  - access rights per model field
24
- - rewrite
25
- - redirect
26
- - express like interface
27
26
 
28
27
  ### Usage examples:
29
28
 
30
29
  Simple router:
31
30
 
32
31
  ```ts
32
+ import { MicroServer, AccessDenied } from '@radatek/microserver'
33
+
33
34
  const server = new MicroServer({
34
35
  listen: 8080,
35
36
  auth: {
@@ -47,7 +48,7 @@ server.use('POST /api/login',
47
48
  (req: ServerRequset, res: ServerResponse) =>
48
49
  {
49
50
  const user = await req.auth.login(req.body.user, req.body.password)
50
- return user ? {user} : 403
51
+ return user ? {user} : new AccessDenied()
51
52
  })
52
53
  server.use('GET /api/protected', 'acl:auth',
53
54
  (req: ServerRequset, res: ServerResponse) =>
@@ -55,49 +56,14 @@ server.use('GET /api/protected', 'acl:auth',
55
56
  server.use('static', {root:'public'})
56
57
  ```
57
58
 
58
- Using `Controller` class:
59
-
60
- ```ts
61
- class RestApi extends Controller {
62
- static acl = '' // default acl
63
-
64
- gethello(id) {
65
- return {message:'Hello ' + id + '!'}
66
- }
67
-
68
- async postlogin() {
69
- const user = await this.auth.login(this.body.user, this.body.password)
70
- return user ? {user} : 403
71
- }
72
-
73
- static 'acl:protected' = 'user'
74
- static 'url:protected' = 'GET /protected'
75
- protected() {
76
- return {message:'Protected'}
77
- }
78
- }
79
-
80
- const server = new MicroServer({
81
- listen: 8080,
82
- auth: {
83
- users: {
84
- usr: {
85
- password: 'secret',
86
- acl: {user: true}
87
- }
88
- }
89
- }
90
- })
91
- server.use('/api', RestApi)
92
- ```
93
-
94
- `Model.handler` automatically detects usage for standard functions: get,insert,update,delete
59
+ Using data schema:
95
60
 
96
61
  ```js
97
- import { MicroServer, Model, MicroCollection, FileStore } from '@dariuski/microserver'
62
+ import { MicroServer, Model, MicroCollection, FileStore } from '@radatek/microserver'
98
63
 
99
64
  const usersCollection = new MicroCollection({ store: new FileStore({ dir: 'data' }), name: 'users' })
100
- //const usersCollection = await db.collection('users')
65
+ // or using MicroDB collection
66
+ const usersCollection = await db.collection('users')
101
67
 
102
68
  const userProfile = new Model({
103
69
  _id: 'string',
@@ -115,7 +81,7 @@ const server = new MicroServer({
115
81
  }
116
82
  })
117
83
 
118
- userProfile.insert({name: 'admin', password: 'secret', role: 'admin', acl: {'user/*': true}})
84
+ await userProfile.insert({name: 'admin', password: 'secret', role: 'admin', acl: {'user/*': true}})
119
85
 
120
86
  server.use('POST /login', async (req) => {
121
87
  const user = await req.auth.login(req.body.user, req.body.password)
@@ -134,3 +100,40 @@ server.use('PUT /admin/user/:id', 'acl:user/update', userProfile)
134
100
  // delete user if has acl 'user/update'
135
101
  server.use('DELETE /admin/user/:id', 'acl:user/delete', userProfile)
136
102
  ```
103
+
104
+ Using controller:
105
+
106
+ ```ts
107
+ const server = new MicroServer({
108
+ listen: 8080,
109
+ auth: {
110
+ users: {
111
+ usr: {
112
+ password: 'secret',
113
+ acl: {user: true}
114
+ }
115
+ }
116
+ }
117
+ })
118
+
119
+ class RestApi extends Controller {
120
+ static acl = '' // default acl
121
+
122
+ gethello(id) {
123
+ return {message:'Hello ' + id + '!'}
124
+ }
125
+
126
+ async postlogin() {
127
+ const user = await this.auth.login(this.body.user, this.body.password)
128
+ return user ? {user} : 403
129
+ }
130
+
131
+ static 'acl:protected' = 'user'
132
+ static 'url:protected' = 'GET /protected'
133
+ protected() {
134
+ return {message:'Protected'}
135
+ }
136
+ }
137
+
138
+ server.use('/api', RestApi)
139
+ ```