@rapidd/core 2.1.2 → 2.1.4
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/.env.example +9 -3
- package/README.md +26 -35
- package/bin/cli.js +26 -35
- package/config/app.json +1 -1
- package/dockerfile +9 -1
- package/locales/ar_SA.json +19 -1
- package/locales/de_DE.json +19 -1
- package/locales/en_US.json +19 -1
- package/locales/es_ES.json +19 -1
- package/locales/fr_FR.json +19 -1
- package/locales/it_IT.json +19 -1
- package/locales/ja_JP.json +19 -1
- package/locales/pt_BR.json +19 -1
- package/locales/ru_RU.json +19 -1
- package/locales/tr_TR.json +19 -1
- package/main.ts +5 -5
- package/package.json +1 -1
- package/src/app.ts +33 -6
- package/src/auth/Auth.ts +9 -8
- package/src/auth/stores/RedisStore.ts +6 -5
- package/src/auth/stores/index.ts +7 -6
- package/src/core/env.ts +7 -1
- package/src/core/i18n.ts +3 -2
- package/src/index.ts +3 -1
- package/src/orm/QueryBuilder.ts +5 -3
- package/src/plugins/auth.ts +21 -21
- package/src/plugins/language.ts +5 -5
- package/src/plugins/response.ts +4 -3
- package/src/plugins/upload.ts +8 -7
- package/src/types.ts +3 -3
- package/src/utils/Logger.ts +237 -0
- package/src/utils/index.ts +3 -0
package/src/auth/Auth.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { authPrisma } from '../core/prisma';
|
|
|
5
5
|
import { ErrorResponse } from '../core/errors';
|
|
6
6
|
import { createStore, SessionStoreManager } from './stores';
|
|
7
7
|
import { loadDMMF, findUserModel, findIdentifierFields, findPasswordField } from '../core/dmmf';
|
|
8
|
-
import
|
|
8
|
+
import { Logger } from '../utils/Logger';
|
|
9
|
+
import type { RapiddUser, AuthOptions, AuthStrategy, ISessionStore } from '../types';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Authentication class for user login, logout, and session management
|
|
@@ -26,7 +27,7 @@ import type { RapiddUser, AuthOptions, AuthMethod, ISessionStore } from '../type
|
|
|
26
27
|
export class Auth {
|
|
27
28
|
options: Required<Pick<AuthOptions, 'passwordField' | 'saltRounds'>> & AuthOptions & {
|
|
28
29
|
identifierFields: string[];
|
|
29
|
-
|
|
30
|
+
strategies: AuthStrategy[];
|
|
30
31
|
cookieName: string;
|
|
31
32
|
customHeaderName: string;
|
|
32
33
|
session: { ttl: number; store?: string };
|
|
@@ -68,8 +69,8 @@ export class Auth {
|
|
|
68
69
|
...options.jwt,
|
|
69
70
|
},
|
|
70
71
|
saltRounds: options.saltRounds || parseInt(process.env.AUTH_SALT_ROUNDS || '10', 10),
|
|
71
|
-
|
|
72
|
-
|| (process.env.
|
|
72
|
+
strategies: options.strategies
|
|
73
|
+
|| (process.env.AUTH_STRATEGIES?.split(',').map(s => s.trim()) as AuthStrategy[])
|
|
73
74
|
|| ['bearer'],
|
|
74
75
|
cookieName: options.cookieName || process.env.AUTH_COOKIE_NAME || 'token',
|
|
75
76
|
customHeaderName: options.customHeaderName || process.env.AUTH_CUSTOM_HEADER || 'X-Auth-Token',
|
|
@@ -99,7 +100,7 @@ export class Auth {
|
|
|
99
100
|
try {
|
|
100
101
|
await loadDMMF();
|
|
101
102
|
} catch {
|
|
102
|
-
|
|
103
|
+
Logger.warn('Auth: could not load DMMF, auto-detection skipped');
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -109,7 +110,7 @@ export class Auth {
|
|
|
109
110
|
// Check if a model name was explicitly configured
|
|
110
111
|
if (!this.options.userModel) {
|
|
111
112
|
this._authDisabled = true;
|
|
112
|
-
|
|
113
|
+
Logger.log('Auth: no user table detected, auth disabled');
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
// Model name was set but not found in DMMF — warn but don't disable
|
|
@@ -142,7 +143,7 @@ export class Auth {
|
|
|
142
143
|
throw new Error('[Auth] JWT_SECRET is required in production. Generate one with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"');
|
|
143
144
|
}
|
|
144
145
|
this.options.jwt.secret = crypto.randomBytes(32).toString('hex');
|
|
145
|
-
|
|
146
|
+
Logger.warn('Auth: no JWT_SECRET set, using auto-generated secret (sessions won\'t persist across restarts)');
|
|
146
147
|
}
|
|
147
148
|
if (!this.options.jwt.refreshSecret) {
|
|
148
149
|
if (process.env.NODE_ENV === 'production') {
|
|
@@ -173,7 +174,7 @@ export class Auth {
|
|
|
173
174
|
this._userModel = (authPrisma as any)[match];
|
|
174
175
|
return this._userModel;
|
|
175
176
|
}
|
|
176
|
-
|
|
177
|
+
Logger.warn('Auth: userModel not found in Prisma models', { userModel: modelName });
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
for (const name of ['users', 'user', 'Users', 'User']) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Redis from 'ioredis';
|
|
2
2
|
import { ISessionStore } from './ISessionStore';
|
|
3
|
+
import { Logger } from '../../utils/Logger';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Redis session store using ioredis.
|
|
@@ -36,11 +37,11 @@ export class RedisStore extends ISessionStore {
|
|
|
36
37
|
connectTimeout: 5000,
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
this.client.on('connect', () =>
|
|
40
|
-
this.client.on('ready', () =>
|
|
41
|
-
this.client.on('error', (err: Error) =>
|
|
42
|
-
this.client.on('close', () =>
|
|
43
|
-
this.client.on('reconnecting', () =>
|
|
40
|
+
this.client.on('connect', () => Logger.log('RedisStore connected'));
|
|
41
|
+
this.client.on('ready', () => Logger.log('RedisStore ready'));
|
|
42
|
+
this.client.on('error', (err: Error) => Logger.error(err));
|
|
43
|
+
this.client.on('close', () => Logger.warn('RedisStore connection closed'));
|
|
44
|
+
this.client.on('reconnecting', () => Logger.log('RedisStore reconnecting'));
|
|
44
45
|
|
|
45
46
|
this._initialized = true;
|
|
46
47
|
return this.client;
|
package/src/auth/stores/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ISessionStore } from './ISessionStore';
|
|
2
2
|
import { MemoryStore } from './MemoryStore';
|
|
3
3
|
import { RedisStore } from './RedisStore';
|
|
4
|
+
import { Logger } from '../../utils/Logger';
|
|
4
5
|
|
|
5
6
|
const builtInStores: Record<string, new (options?: any) => ISessionStore> = {
|
|
6
7
|
memory: MemoryStore,
|
|
@@ -41,7 +42,7 @@ export class SessionStoreManager extends ISessionStore {
|
|
|
41
42
|
|
|
42
43
|
const StoreClass = builtInStores[this.storeName];
|
|
43
44
|
if (!StoreClass) {
|
|
44
|
-
|
|
45
|
+
Logger.warn('SessionStore: unknown store, using memory', { store: this.storeName });
|
|
45
46
|
this._primaryStore = this._fallbackStore;
|
|
46
47
|
this._initialized = true;
|
|
47
48
|
return;
|
|
@@ -50,7 +51,7 @@ export class SessionStoreManager extends ISessionStore {
|
|
|
50
51
|
try {
|
|
51
52
|
this._primaryStore = new StoreClass({ ttl: this.ttl });
|
|
52
53
|
} catch (err) {
|
|
53
|
-
|
|
54
|
+
Logger.warn('SessionStore: failed to create store, using memory', { store: this.storeName, error: (err as Error).message });
|
|
54
55
|
this._primaryStore = this._fallbackStore;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -70,15 +71,15 @@ export class SessionStoreManager extends ISessionStore {
|
|
|
70
71
|
try {
|
|
71
72
|
const isHealthy = await this._primaryStore!.isHealthy();
|
|
72
73
|
if (isHealthy && this._usingFallback) {
|
|
73
|
-
|
|
74
|
+
Logger.log('SessionStore: recovered, switching back from memory', { store: this.storeName });
|
|
74
75
|
this._usingFallback = false;
|
|
75
76
|
} else if (!isHealthy && !this._usingFallback) {
|
|
76
|
-
|
|
77
|
+
Logger.warn('SessionStore: unavailable, switching to memory', { store: this.storeName });
|
|
77
78
|
this._usingFallback = true;
|
|
78
79
|
}
|
|
79
80
|
} catch {
|
|
80
81
|
if (!this._usingFallback) {
|
|
81
|
-
|
|
82
|
+
Logger.warn('SessionStore: health check failed, switching to memory', { store: this.storeName });
|
|
82
83
|
this._usingFallback = true;
|
|
83
84
|
}
|
|
84
85
|
}
|
|
@@ -98,7 +99,7 @@ export class SessionStoreManager extends ISessionStore {
|
|
|
98
99
|
try {
|
|
99
100
|
return await (this._primaryStore as any)[operation](...args);
|
|
100
101
|
} catch (err) {
|
|
101
|
-
|
|
102
|
+
Logger.warn('SessionStore: operation failed, switching to memory', { store: this.storeName, operation, error: (err as Error).message });
|
|
102
103
|
this._usingFallback = true;
|
|
103
104
|
return (this._fallbackStore as any)[operation](...args);
|
|
104
105
|
}
|
package/src/core/env.ts
CHANGED
|
@@ -51,6 +51,10 @@ export interface EnvConfig {
|
|
|
51
51
|
|
|
52
52
|
// Proxy
|
|
53
53
|
TRUST_PROXY?: boolean;
|
|
54
|
+
|
|
55
|
+
// Logging
|
|
56
|
+
LOG_LEVEL: 'essential' | 'fine' | 'finest';
|
|
57
|
+
LOG_DIR: string;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
const REQUIRED_VARS = [
|
|
@@ -76,7 +80,9 @@ const DEFAULTS: Partial<Record<keyof EnvConfig, string | number | boolean>> = {
|
|
|
76
80
|
REDIS_PORT: 6379,
|
|
77
81
|
REDIS_DB_RATE_LIMIT: 0,
|
|
78
82
|
REDIS_DB_AUTH: 1,
|
|
79
|
-
RLS_NAMESPACE: 'app'
|
|
83
|
+
RLS_NAMESPACE: 'app',
|
|
84
|
+
LOG_LEVEL: 'essential',
|
|
85
|
+
LOG_DIR: 'logs',
|
|
80
86
|
};
|
|
81
87
|
|
|
82
88
|
/**
|
package/src/core/i18n.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdirSync } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { Logger } from '../utils/Logger';
|
|
3
4
|
|
|
4
5
|
const ROOT = process.env.ROOT || '';
|
|
5
6
|
const DEFAULT_STRINGS_PATH = ROOT ? path.join(ROOT, 'locales') : './locale';
|
|
@@ -40,14 +41,14 @@ export class LanguageDict {
|
|
|
40
41
|
this._dictionaries[langCode] = require(dictPath);
|
|
41
42
|
this._available.push(langCode);
|
|
42
43
|
} catch (error) {
|
|
43
|
-
|
|
44
|
+
Logger.error(error as Error, { langCode });
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
this._initialized = true;
|
|
49
50
|
} catch (error) {
|
|
50
|
-
|
|
51
|
+
Logger.error(error as Error);
|
|
51
52
|
this._dictionaries = {};
|
|
52
53
|
this._available = [];
|
|
53
54
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export { modelMiddleware } from './core/middleware';
|
|
|
44
44
|
// ── Utilities ────────────────────────────────────────────────────────────────
|
|
45
45
|
export { ApiClient, ApiClientError } from './utils/ApiClient';
|
|
46
46
|
export { Mailer } from './utils/Mailer';
|
|
47
|
+
export { Logger } from './utils/Logger';
|
|
47
48
|
export { env } from './utils';
|
|
48
49
|
|
|
49
50
|
// ── Environment ──────────────────────────────────────────────────────────────
|
|
@@ -73,7 +74,7 @@ export { buildApp } from './app';
|
|
|
73
74
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
74
75
|
export type {
|
|
75
76
|
RapiddUser,
|
|
76
|
-
|
|
77
|
+
AuthStrategy,
|
|
77
78
|
RouteAuthConfig,
|
|
78
79
|
ModelOptions,
|
|
79
80
|
GetManyResult,
|
|
@@ -110,3 +111,4 @@ export type {
|
|
|
110
111
|
} from './plugins/upload';
|
|
111
112
|
|
|
112
113
|
export type { EnvConfig } from './core/env';
|
|
114
|
+
export type { LogLevel } from './utils/Logger';
|
package/src/orm/QueryBuilder.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { prisma, prismaTransaction, getAcl } from '../core/prisma';
|
|
2
2
|
import { ErrorResponse } from '../core/errors';
|
|
3
|
+
import { LanguageDict } from '../core/i18n';
|
|
4
|
+
import { Logger } from '../utils/Logger';
|
|
3
5
|
import * as dmmf from '../core/dmmf';
|
|
4
6
|
import type {
|
|
5
7
|
RelationConfig,
|
|
@@ -2065,13 +2067,13 @@ class QueryBuilder {
|
|
|
2065
2067
|
* Handle Prisma errors and convert to standardized error responses
|
|
2066
2068
|
*/
|
|
2067
2069
|
static errorHandler(error: any, data: Record<string, unknown> = {}): QueryErrorResponse {
|
|
2068
|
-
|
|
2070
|
+
Logger.error(error);
|
|
2069
2071
|
|
|
2070
2072
|
// Default values
|
|
2071
2073
|
let statusCode: number = error.status_code || 500;
|
|
2072
2074
|
let message: string = error instanceof ErrorResponse
|
|
2073
2075
|
? error.message
|
|
2074
|
-
: (process.env.NODE_ENV === 'production' ? '
|
|
2076
|
+
: (process.env.NODE_ENV === 'production' ? LanguageDict.get('internal_server_error') : (error.message || String(error)));
|
|
2075
2077
|
|
|
2076
2078
|
// Handle Prisma error codes
|
|
2077
2079
|
if (error?.code && PRISMA_ERROR_MAP[error.code]) {
|
|
@@ -2082,7 +2084,7 @@ class QueryBuilder {
|
|
|
2082
2084
|
if (error.code === 'P2002') {
|
|
2083
2085
|
const target = error.meta?.target;
|
|
2084
2086
|
const modelName = error.meta?.modelName;
|
|
2085
|
-
message =
|
|
2087
|
+
message = LanguageDict.get('duplicate_entry', { model: modelName, field: target, value: data[target as string] });
|
|
2086
2088
|
} else {
|
|
2087
2089
|
message = errorInfo.message!;
|
|
2088
2090
|
}
|
package/src/plugins/auth.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { FastifyPluginAsync, FastifyRequest } from 'fastify';
|
|
|
3
3
|
import fp from 'fastify-plugin';
|
|
4
4
|
import { Auth } from '../auth/Auth';
|
|
5
5
|
import { ErrorResponse } from '../core/errors';
|
|
6
|
-
import type { RapiddUser, AuthOptions,
|
|
6
|
+
import type { RapiddUser, AuthOptions, AuthStrategy, RouteAuthConfig } from '../types';
|
|
7
7
|
|
|
8
8
|
interface AuthPluginOptions {
|
|
9
9
|
auth?: Auth;
|
|
@@ -36,42 +36,42 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Load
|
|
40
|
-
let
|
|
39
|
+
// Load endpointAuthStrategy from config/app.json (prefix → strategy mapping)
|
|
40
|
+
let endpointAuthStrategy: Record<string, AuthStrategy | AuthStrategy[] | null> = {};
|
|
41
41
|
try {
|
|
42
42
|
const appConfig = require(path.join(process.cwd(), 'config', 'app.json'));
|
|
43
|
-
if (appConfig.
|
|
44
|
-
|
|
43
|
+
if (appConfig.endpointAuthStrategy) {
|
|
44
|
+
endpointAuthStrategy = appConfig.endpointAuthStrategy;
|
|
45
45
|
}
|
|
46
46
|
} catch {
|
|
47
|
-
// No app.json or no
|
|
47
|
+
// No app.json or no endpointAuthStrategy — use global default
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Pre-sort prefixes by length (longest first) for correct matching
|
|
51
|
-
const sortedPrefixes = Object.keys(
|
|
51
|
+
const sortedPrefixes = Object.keys(endpointAuthStrategy)
|
|
52
52
|
.sort((a, b) => b.length - a.length);
|
|
53
53
|
|
|
54
|
-
// Parse auth on every request using configured
|
|
55
|
-
// Priority: route config >
|
|
54
|
+
// Parse auth on every request using configured strategies (checked in order).
|
|
55
|
+
// Priority: route config > endpointAuthStrategy prefix match > global default
|
|
56
56
|
fastify.addHook('onRequest', async (request) => {
|
|
57
57
|
const routeAuth = (request.routeOptions?.config as any)?.auth as RouteAuthConfig | undefined;
|
|
58
58
|
|
|
59
|
-
let
|
|
60
|
-
if (routeAuth?.
|
|
61
|
-
|
|
59
|
+
let strategies: AuthStrategy[];
|
|
60
|
+
if (routeAuth?.strategies) {
|
|
61
|
+
strategies = routeAuth.strategies;
|
|
62
62
|
} else {
|
|
63
63
|
const matchedPrefix = sortedPrefixes.find(p => request.url.startsWith(p));
|
|
64
64
|
if (matchedPrefix) {
|
|
65
|
-
const value =
|
|
65
|
+
const value = endpointAuthStrategy[matchedPrefix];
|
|
66
66
|
if (value === null) {
|
|
67
|
-
|
|
67
|
+
strategies = auth.options.strategies;
|
|
68
68
|
} else if (typeof value === 'string') {
|
|
69
|
-
|
|
69
|
+
strategies = [value];
|
|
70
70
|
} else {
|
|
71
|
-
|
|
71
|
+
strategies = value;
|
|
72
72
|
}
|
|
73
73
|
} else {
|
|
74
|
-
|
|
74
|
+
strategies = auth.options.strategies;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -80,10 +80,10 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
80
80
|
|
|
81
81
|
let user: RapiddUser | null = null;
|
|
82
82
|
|
|
83
|
-
for (const
|
|
83
|
+
for (const strategy of strategies) {
|
|
84
84
|
if (user) break;
|
|
85
85
|
|
|
86
|
-
switch (
|
|
86
|
+
switch (strategy) {
|
|
87
87
|
case 'bearer': {
|
|
88
88
|
const h = request.headers.authorization;
|
|
89
89
|
if (h?.startsWith('Bearer ')) {
|
|
@@ -125,7 +125,7 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
125
125
|
fastify.post('/auth/login', async (request, reply) => {
|
|
126
126
|
const result = await auth.login(request.body as { user: string; password: string });
|
|
127
127
|
|
|
128
|
-
if (auth.options.
|
|
128
|
+
if (auth.options.strategies.includes('cookie')) {
|
|
129
129
|
reply.setCookie(auth.options.cookieName, result.accessToken, {
|
|
130
130
|
path: '/',
|
|
131
131
|
httpOnly: true,
|
|
@@ -141,7 +141,7 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
141
141
|
fastify.post('/auth/logout', async (request, reply) => {
|
|
142
142
|
const result = await auth.logout(request.headers.authorization);
|
|
143
143
|
|
|
144
|
-
if (auth.options.
|
|
144
|
+
if (auth.options.strategies.includes('cookie')) {
|
|
145
145
|
reply.clearCookie(auth.options.cookieName, { path: '/' });
|
|
146
146
|
}
|
|
147
147
|
|
package/src/plugins/language.ts
CHANGED
|
@@ -37,22 +37,22 @@ function resolveLanguage(headerValue: string): string {
|
|
|
37
37
|
.split(',')
|
|
38
38
|
.map((lang: string) => {
|
|
39
39
|
const parts = lang.trim().split(';');
|
|
40
|
-
const code = parts[0].trim();
|
|
40
|
+
const code = parts[0].trim().replace(/-/g, '_');
|
|
41
41
|
const quality = parts[1] ? parseFloat(parts[1].replace('q=', '')) : 1.0;
|
|
42
42
|
return { code, quality };
|
|
43
43
|
})
|
|
44
44
|
.sort((a, b) => b.quality - a.quality);
|
|
45
45
|
|
|
46
|
-
// Exact match
|
|
46
|
+
// Exact match (e.g. "de_DE" header → "de_DE" locale)
|
|
47
47
|
for (const lang of languages) {
|
|
48
48
|
const match = ALLOWED_LANGUAGES.find((a: string) => a.toLowerCase() === lang.code);
|
|
49
49
|
if (match) return match;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// Language family match (e.g. "
|
|
52
|
+
// Language family match (e.g. "en_GB" header → "en_US" locale)
|
|
53
53
|
for (const lang of languages) {
|
|
54
|
-
const prefix = lang.code.split('
|
|
55
|
-
const match = ALLOWED_LANGUAGES.find((a: string) => a.toLowerCase().startsWith(prefix + '
|
|
54
|
+
const prefix = lang.code.split('_')[0];
|
|
55
|
+
const match = ALLOWED_LANGUAGES.find((a: string) => a.toLowerCase().startsWith(prefix + '_'));
|
|
56
56
|
if (match) return match;
|
|
57
57
|
}
|
|
58
58
|
} catch {
|
package/src/plugins/response.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';
|
|
|
2
2
|
import fp from 'fastify-plugin';
|
|
3
3
|
import { ErrorResponse, ErrorBasicResponse } from '../core/errors';
|
|
4
4
|
import { LanguageDict } from '../core/i18n';
|
|
5
|
+
import { Logger } from '../utils/Logger';
|
|
5
6
|
import type { ListMeta } from '../types';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -37,7 +38,7 @@ const responsePlugin: FastifyPluginAsync = async (fastify) => {
|
|
|
37
38
|
const request = this.request;
|
|
38
39
|
const language = request?.language || 'en_US';
|
|
39
40
|
const error = new ErrorResponse(statusCode, message, data as Record<string, unknown> | null);
|
|
40
|
-
|
|
41
|
+
Logger.error(message, { statusCode });
|
|
41
42
|
return this.code(statusCode).send(error.toJSON(language));
|
|
42
43
|
});
|
|
43
44
|
|
|
@@ -69,10 +70,10 @@ const responsePlugin: FastifyPluginAsync = async (fastify) => {
|
|
|
69
70
|
const err = error as Error;
|
|
70
71
|
const message =
|
|
71
72
|
Object.getPrototypeOf(err).constructor === Error && process.env.NODE_ENV === 'production'
|
|
72
|
-
? '
|
|
73
|
+
? LanguageDict.get('internal_server_error', null, language)
|
|
73
74
|
: err.message || String(error);
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
Logger.error(error);
|
|
76
77
|
return reply.code(status).send({ status_code: status, message });
|
|
77
78
|
});
|
|
78
79
|
};
|
package/src/plugins/upload.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { pipeline } from 'stream/promises';
|
|
|
5
5
|
import { Transform } from 'stream';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import path from 'path';
|
|
8
|
+
import { ErrorResponse } from '../core/errors';
|
|
8
9
|
|
|
9
10
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
10
11
|
|
|
@@ -85,9 +86,9 @@ function getAllowedTypes(option: UploadOptions['allowedTypes']): AllowedType[] {
|
|
|
85
86
|
function validateFile(
|
|
86
87
|
file: { mimetype: string; filename: string },
|
|
87
88
|
allowedTypes: AllowedType[]
|
|
88
|
-
): { valid: boolean;
|
|
89
|
+
): { valid: boolean; errorKey?: string; errorData?: Record<string, unknown> } {
|
|
89
90
|
if (file.filename.includes('..') || file.filename.includes('/') || file.filename.includes('\\')) {
|
|
90
|
-
return { valid: false,
|
|
91
|
+
return { valid: false, errorKey: 'invalid_file_name' };
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
if (allowedTypes.length === 0) {
|
|
@@ -98,11 +99,11 @@ function validateFile(
|
|
|
98
99
|
const allowedType = allowedTypes.find(t => t.mime === file.mimetype);
|
|
99
100
|
|
|
100
101
|
if (!allowedType) {
|
|
101
|
-
return { valid: false,
|
|
102
|
+
return { valid: false, errorKey: 'file_type_not_allowed', errorData: { mimetype: file.mimetype } };
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
if (!allowedType.extensions.includes(ext)) {
|
|
105
|
-
return { valid: false,
|
|
106
|
+
return { valid: false, errorKey: 'file_extension_not_allowed', errorData: { extension: ext, mimetype: file.mimetype } };
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
return { valid: true };
|
|
@@ -114,7 +115,7 @@ function createSizeTracker(maxSize: number): { tracker: Transform; getSize: () =
|
|
|
114
115
|
transform(chunk, encoding, callback) {
|
|
115
116
|
size += chunk.length;
|
|
116
117
|
if (size > maxSize) {
|
|
117
|
-
callback(new
|
|
118
|
+
callback(new ErrorResponse(400, 'file_size_exceeds_limit', { limit: Math.round(maxSize / 1024 / 1024) }));
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
121
|
callback(null, chunk);
|
|
@@ -229,7 +230,7 @@ async function uploadPluginImpl(
|
|
|
229
230
|
|
|
230
231
|
const validation = validateFile(data, allowedTypes);
|
|
231
232
|
if (!validation.valid) {
|
|
232
|
-
throw new
|
|
233
|
+
throw new ErrorResponse(400, validation.errorKey!, validation.errorData);
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
const { tempPath, size } = await saveToTemp(data.file, tempDir, data.filename, maxFileSize);
|
|
@@ -260,7 +261,7 @@ async function uploadPluginImpl(
|
|
|
260
261
|
|
|
261
262
|
const validation = validateFile(part, allowedTypes);
|
|
262
263
|
if (!validation.valid) {
|
|
263
|
-
throw new
|
|
264
|
+
throw new ErrorResponse(400, validation.errorKey!, validation.errorData);
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
const { tempPath, size } = await saveToTemp(part.file, tempDir, part.filename, maxFileSize);
|
package/src/types.ts
CHANGED
|
@@ -10,10 +10,10 @@ export interface RapiddUser {
|
|
|
10
10
|
[key: string]: unknown;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export type
|
|
13
|
+
export type AuthStrategy = 'bearer' | 'basic' | 'cookie' | 'header';
|
|
14
14
|
|
|
15
15
|
export interface RouteAuthConfig {
|
|
16
|
-
|
|
16
|
+
strategies?: AuthStrategy[];
|
|
17
17
|
cookieName?: string;
|
|
18
18
|
customHeaderName?: string;
|
|
19
19
|
}
|
|
@@ -32,7 +32,7 @@ export interface AuthOptions {
|
|
|
32
32
|
refreshExpiry?: string;
|
|
33
33
|
};
|
|
34
34
|
saltRounds?: number;
|
|
35
|
-
|
|
35
|
+
strategies?: AuthStrategy[];
|
|
36
36
|
cookieName?: string;
|
|
37
37
|
customHeaderName?: string;
|
|
38
38
|
}
|