@technomoron/api-server-base 1.1.6 → 1.1.8
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.txt +1 -0
- package/dist/cjs/api-server-base.cjs +56 -1
- package/dist/cjs/api-server-base.d.ts +7 -0
- package/dist/cjs/auth-storage.d.ts +2 -0
- package/dist/esm/api-server-base.d.ts +7 -0
- package/dist/esm/api-server-base.js +56 -1
- package/dist/esm/auth-storage.d.ts +2 -0
- package/package.json +9 -4
package/README.txt
CHANGED
|
@@ -109,6 +109,7 @@ accessCookie (string, default 'dat') Access token cookie name.
|
|
|
109
109
|
refreshCookie (string, default 'drt') Refresh token cookie name.
|
|
110
110
|
accessExpiry (number, default 60 * 15) Access token lifetime in seconds.
|
|
111
111
|
refreshExpiry (number, default 30 * 24 * 60 * 60 * 1000) Refresh token lifetime in milliseconds.
|
|
112
|
+
sessionRefreshExpiry (number, default 24 * 60 * 60) Session token lifetime in seconds when clients opt out of "remember me" cookies.
|
|
112
113
|
authApi (boolean, default false) Toggle you can use when mounting auth routes.
|
|
113
114
|
devMode (boolean, default false) Custom hook for development only features.
|
|
114
115
|
debug (boolean, default false) When true the server logs inbound requests via dumpRequest.
|
|
@@ -326,6 +326,7 @@ function fillConfig(config) {
|
|
|
326
326
|
refreshCookie: config.refreshCookie ?? 'drt',
|
|
327
327
|
accessExpiry: config.accessExpiry ?? 60 * 15,
|
|
328
328
|
refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60,
|
|
329
|
+
sessionRefreshExpiry: config.sessionRefreshExpiry ?? 24 * 60 * 60,
|
|
329
330
|
authApi: config.authApi ?? false,
|
|
330
331
|
devMode: config.devMode ?? false,
|
|
331
332
|
hydrateGetBody: config.hydrateGetBody ?? true,
|
|
@@ -335,7 +336,9 @@ function fillConfig(config) {
|
|
|
335
336
|
class ApiServer {
|
|
336
337
|
constructor(config = {}) {
|
|
337
338
|
this.currReq = null;
|
|
339
|
+
this.apiNotFoundHandler = null;
|
|
338
340
|
this.config = fillConfig(config);
|
|
341
|
+
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
339
342
|
this.storageAdapter = auth_storage_js_1.nullAuthStorage;
|
|
340
343
|
this.moduleAdapter = auth_module_js_1.nullAuthModule;
|
|
341
344
|
this.app = (0, express_1.default)();
|
|
@@ -345,6 +348,7 @@ class ApiServer {
|
|
|
345
348
|
}
|
|
346
349
|
this.middlewares();
|
|
347
350
|
// addSwaggerUi(this.app);
|
|
351
|
+
this.installApiNotFoundHandler();
|
|
348
352
|
}
|
|
349
353
|
authStorage(storage) {
|
|
350
354
|
this.storageAdapter = storage;
|
|
@@ -493,6 +497,56 @@ class ApiServer {
|
|
|
493
497
|
};
|
|
494
498
|
this.app.use((0, cors_1.default)(corsOptions));
|
|
495
499
|
}
|
|
500
|
+
normalizeApiBasePath(path) {
|
|
501
|
+
if (!path || typeof path !== 'string') {
|
|
502
|
+
return '/api';
|
|
503
|
+
}
|
|
504
|
+
const trimmed = path.trim();
|
|
505
|
+
if (!trimmed) {
|
|
506
|
+
return '/api';
|
|
507
|
+
}
|
|
508
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
509
|
+
if (withLeadingSlash.length === 1) {
|
|
510
|
+
return withLeadingSlash;
|
|
511
|
+
}
|
|
512
|
+
return withLeadingSlash.replace(/\/+$/, '') || '/api';
|
|
513
|
+
}
|
|
514
|
+
installApiNotFoundHandler() {
|
|
515
|
+
if (this.apiNotFoundHandler) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
this.apiNotFoundHandler = (req, res) => {
|
|
519
|
+
const payload = {
|
|
520
|
+
code: 404,
|
|
521
|
+
message: this.describeMissingEndpoint(req),
|
|
522
|
+
data: null,
|
|
523
|
+
errors: {}
|
|
524
|
+
};
|
|
525
|
+
res.status(404).json(payload);
|
|
526
|
+
};
|
|
527
|
+
this.app.use(this.apiBasePath, this.apiNotFoundHandler);
|
|
528
|
+
}
|
|
529
|
+
ensureApiNotFoundOrdering() {
|
|
530
|
+
this.installApiNotFoundHandler();
|
|
531
|
+
if (!this.apiNotFoundHandler) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const stack = this.app?._router?.stack;
|
|
535
|
+
if (!stack) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const index = stack.findIndex((layer) => layer.handle === this.apiNotFoundHandler);
|
|
539
|
+
if (index === -1 || index === stack.length - 1) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const [layer] = stack.splice(index, 1);
|
|
543
|
+
stack.push(layer);
|
|
544
|
+
}
|
|
545
|
+
describeMissingEndpoint(req) {
|
|
546
|
+
const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
|
|
547
|
+
const target = req.originalUrl || req.url || this.apiBasePath;
|
|
548
|
+
return `No such endpoint: ${method} ${target}`;
|
|
549
|
+
}
|
|
496
550
|
start() {
|
|
497
551
|
this.app
|
|
498
552
|
.listen({
|
|
@@ -705,7 +759,7 @@ class ApiServer {
|
|
|
705
759
|
this.authModule(module);
|
|
706
760
|
}
|
|
707
761
|
module.checkConfig();
|
|
708
|
-
const base = this.
|
|
762
|
+
const base = this.apiBasePath;
|
|
709
763
|
const ns = module.namespace;
|
|
710
764
|
const mountPath = `${base}${ns}`;
|
|
711
765
|
module.mountpath = mountPath;
|
|
@@ -717,6 +771,7 @@ class ApiServer {
|
|
|
717
771
|
}
|
|
718
772
|
});
|
|
719
773
|
this.app.use(mountPath, router);
|
|
774
|
+
this.ensureApiNotFoundOrdering();
|
|
720
775
|
return this;
|
|
721
776
|
}
|
|
722
777
|
dumpRequest(apiReq) {
|
|
@@ -92,6 +92,7 @@ export interface ApiServerConf {
|
|
|
92
92
|
refreshCookie: string;
|
|
93
93
|
accessExpiry: number;
|
|
94
94
|
refreshExpiry: number;
|
|
95
|
+
sessionRefreshExpiry: number;
|
|
95
96
|
authApi: boolean;
|
|
96
97
|
devMode: boolean;
|
|
97
98
|
hydrateGetBody: boolean;
|
|
@@ -101,8 +102,10 @@ export declare class ApiServer {
|
|
|
101
102
|
app: Application;
|
|
102
103
|
currReq: ApiRequest | null;
|
|
103
104
|
readonly config: ApiServerConf;
|
|
105
|
+
private readonly apiBasePath;
|
|
104
106
|
private storageAdapter;
|
|
105
107
|
private moduleAdapter;
|
|
108
|
+
private apiNotFoundHandler;
|
|
106
109
|
constructor(config?: Partial<ApiServerConf>);
|
|
107
110
|
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
108
111
|
/**
|
|
@@ -134,6 +137,10 @@ export declare class ApiServer {
|
|
|
134
137
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
135
138
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
136
139
|
private middlewares;
|
|
140
|
+
private normalizeApiBasePath;
|
|
141
|
+
private installApiNotFoundHandler;
|
|
142
|
+
private ensureApiNotFoundOrdering;
|
|
143
|
+
private describeMissingEndpoint;
|
|
137
144
|
start(): this;
|
|
138
145
|
private verifyJWT;
|
|
139
146
|
private authenticate;
|
|
@@ -10,6 +10,8 @@ export interface AuthTokenMetadata {
|
|
|
10
10
|
os?: string;
|
|
11
11
|
scope?: string | string[];
|
|
12
12
|
loginType?: string;
|
|
13
|
+
refreshTtlSeconds?: number;
|
|
14
|
+
sessionCookie?: boolean;
|
|
13
15
|
revokeSessions?: 'device' | 'domain' | 'client' | 'user';
|
|
14
16
|
}
|
|
15
17
|
export interface AuthTokenData extends AuthTokenMetadata {
|
|
@@ -92,6 +92,7 @@ export interface ApiServerConf {
|
|
|
92
92
|
refreshCookie: string;
|
|
93
93
|
accessExpiry: number;
|
|
94
94
|
refreshExpiry: number;
|
|
95
|
+
sessionRefreshExpiry: number;
|
|
95
96
|
authApi: boolean;
|
|
96
97
|
devMode: boolean;
|
|
97
98
|
hydrateGetBody: boolean;
|
|
@@ -101,8 +102,10 @@ export declare class ApiServer {
|
|
|
101
102
|
app: Application;
|
|
102
103
|
currReq: ApiRequest | null;
|
|
103
104
|
readonly config: ApiServerConf;
|
|
105
|
+
private readonly apiBasePath;
|
|
104
106
|
private storageAdapter;
|
|
105
107
|
private moduleAdapter;
|
|
108
|
+
private apiNotFoundHandler;
|
|
106
109
|
constructor(config?: Partial<ApiServerConf>);
|
|
107
110
|
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
108
111
|
/**
|
|
@@ -134,6 +137,10 @@ export declare class ApiServer {
|
|
|
134
137
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
135
138
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
136
139
|
private middlewares;
|
|
140
|
+
private normalizeApiBasePath;
|
|
141
|
+
private installApiNotFoundHandler;
|
|
142
|
+
private ensureApiNotFoundOrdering;
|
|
143
|
+
private describeMissingEndpoint;
|
|
137
144
|
start(): this;
|
|
138
145
|
private verifyJWT;
|
|
139
146
|
private authenticate;
|
|
@@ -318,6 +318,7 @@ function fillConfig(config) {
|
|
|
318
318
|
refreshCookie: config.refreshCookie ?? 'drt',
|
|
319
319
|
accessExpiry: config.accessExpiry ?? 60 * 15,
|
|
320
320
|
refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60,
|
|
321
|
+
sessionRefreshExpiry: config.sessionRefreshExpiry ?? 24 * 60 * 60,
|
|
321
322
|
authApi: config.authApi ?? false,
|
|
322
323
|
devMode: config.devMode ?? false,
|
|
323
324
|
hydrateGetBody: config.hydrateGetBody ?? true,
|
|
@@ -327,7 +328,9 @@ function fillConfig(config) {
|
|
|
327
328
|
export class ApiServer {
|
|
328
329
|
constructor(config = {}) {
|
|
329
330
|
this.currReq = null;
|
|
331
|
+
this.apiNotFoundHandler = null;
|
|
330
332
|
this.config = fillConfig(config);
|
|
333
|
+
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
331
334
|
this.storageAdapter = nullAuthStorage;
|
|
332
335
|
this.moduleAdapter = nullAuthModule;
|
|
333
336
|
this.app = express();
|
|
@@ -337,6 +340,7 @@ export class ApiServer {
|
|
|
337
340
|
}
|
|
338
341
|
this.middlewares();
|
|
339
342
|
// addSwaggerUi(this.app);
|
|
343
|
+
this.installApiNotFoundHandler();
|
|
340
344
|
}
|
|
341
345
|
authStorage(storage) {
|
|
342
346
|
this.storageAdapter = storage;
|
|
@@ -485,6 +489,56 @@ export class ApiServer {
|
|
|
485
489
|
};
|
|
486
490
|
this.app.use(cors(corsOptions));
|
|
487
491
|
}
|
|
492
|
+
normalizeApiBasePath(path) {
|
|
493
|
+
if (!path || typeof path !== 'string') {
|
|
494
|
+
return '/api';
|
|
495
|
+
}
|
|
496
|
+
const trimmed = path.trim();
|
|
497
|
+
if (!trimmed) {
|
|
498
|
+
return '/api';
|
|
499
|
+
}
|
|
500
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
501
|
+
if (withLeadingSlash.length === 1) {
|
|
502
|
+
return withLeadingSlash;
|
|
503
|
+
}
|
|
504
|
+
return withLeadingSlash.replace(/\/+$/, '') || '/api';
|
|
505
|
+
}
|
|
506
|
+
installApiNotFoundHandler() {
|
|
507
|
+
if (this.apiNotFoundHandler) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
this.apiNotFoundHandler = (req, res) => {
|
|
511
|
+
const payload = {
|
|
512
|
+
code: 404,
|
|
513
|
+
message: this.describeMissingEndpoint(req),
|
|
514
|
+
data: null,
|
|
515
|
+
errors: {}
|
|
516
|
+
};
|
|
517
|
+
res.status(404).json(payload);
|
|
518
|
+
};
|
|
519
|
+
this.app.use(this.apiBasePath, this.apiNotFoundHandler);
|
|
520
|
+
}
|
|
521
|
+
ensureApiNotFoundOrdering() {
|
|
522
|
+
this.installApiNotFoundHandler();
|
|
523
|
+
if (!this.apiNotFoundHandler) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const stack = this.app?._router?.stack;
|
|
527
|
+
if (!stack) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const index = stack.findIndex((layer) => layer.handle === this.apiNotFoundHandler);
|
|
531
|
+
if (index === -1 || index === stack.length - 1) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const [layer] = stack.splice(index, 1);
|
|
535
|
+
stack.push(layer);
|
|
536
|
+
}
|
|
537
|
+
describeMissingEndpoint(req) {
|
|
538
|
+
const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
|
|
539
|
+
const target = req.originalUrl || req.url || this.apiBasePath;
|
|
540
|
+
return `No such endpoint: ${method} ${target}`;
|
|
541
|
+
}
|
|
488
542
|
start() {
|
|
489
543
|
this.app
|
|
490
544
|
.listen({
|
|
@@ -697,7 +751,7 @@ export class ApiServer {
|
|
|
697
751
|
this.authModule(module);
|
|
698
752
|
}
|
|
699
753
|
module.checkConfig();
|
|
700
|
-
const base = this.
|
|
754
|
+
const base = this.apiBasePath;
|
|
701
755
|
const ns = module.namespace;
|
|
702
756
|
const mountPath = `${base}${ns}`;
|
|
703
757
|
module.mountpath = mountPath;
|
|
@@ -709,6 +763,7 @@ export class ApiServer {
|
|
|
709
763
|
}
|
|
710
764
|
});
|
|
711
765
|
this.app.use(mountPath, router);
|
|
766
|
+
this.ensureApiNotFoundOrdering();
|
|
712
767
|
return this;
|
|
713
768
|
}
|
|
714
769
|
dumpRequest(apiReq) {
|
|
@@ -10,6 +10,8 @@ export interface AuthTokenMetadata {
|
|
|
10
10
|
os?: string;
|
|
11
11
|
scope?: string | string[];
|
|
12
12
|
loginType?: string;
|
|
13
|
+
refreshTtlSeconds?: number;
|
|
14
|
+
sessionCookie?: boolean;
|
|
13
15
|
revokeSessions?: 'device' | 'domain' | 'client' | 'user';
|
|
14
16
|
}
|
|
15
17
|
export interface AuthTokenData extends AuthTokenMetadata {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technomoron/api-server-base",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "Api Server Skeleton / Base Class",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -28,14 +28,14 @@
|
|
|
28
28
|
"scrub": "rm -rf ./node_modules/ pnpm-lock.yaml ./dist/",
|
|
29
29
|
"build:cjs": "tsc --project tsconfig/tsconfig.cjs.json && node scripts/prepare-cjs.cjs",
|
|
30
30
|
"build:esm": "tsc --project tsconfig/tsconfig.esm.json",
|
|
31
|
-
"build": "
|
|
31
|
+
"build": "node scripts/run-builds.cjs",
|
|
32
32
|
"test": "vitest run",
|
|
33
33
|
"test:watch": "vitest --watch",
|
|
34
|
-
"prepublishOnly": "
|
|
34
|
+
"prepublishOnly": "node scripts/run-builds.cjs",
|
|
35
35
|
"lint": "eslint --ext .js,.ts,.vue ./",
|
|
36
36
|
"lintfix": "eslint --fix --ext .js,.ts,.vue ./",
|
|
37
37
|
"format": "eslint --fix --ext .js,.ts,.vue ./ && prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\"",
|
|
38
|
-
"cleanbuild": "rm -rf ./dist/ && eslint --fix --ext .js,.ts,.vue ./ && prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\" &&
|
|
38
|
+
"cleanbuild": "rm -rf ./dist/ && eslint --fix --ext .js,.ts,.vue ./ && prettier --log-level warn --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\" && node scripts/run-builds.cjs",
|
|
39
39
|
"pretty": "prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\""
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
"typescript": "^5.9.3",
|
|
66
66
|
"vitest": "^3.2.4"
|
|
67
67
|
},
|
|
68
|
+
"pnpm": {
|
|
69
|
+
"onlyBuiltDependencies": [
|
|
70
|
+
"esbuild"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
68
73
|
"files": [
|
|
69
74
|
"dist/",
|
|
70
75
|
"package.json"
|