@rapidd/core 2.1.3 → 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 +3 -3
- package/README.md +5 -5
- package/config/app.json +1 -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/package.json +1 -1
- package/src/app.ts +28 -2
- package/src/auth/Auth.ts +4 -4
- package/src/index.ts +1 -1
- package/src/orm/QueryBuilder.ts +3 -2
- package/src/plugins/auth.ts +21 -21
- package/src/plugins/language.ts +5 -5
- package/src/plugins/response.ts +1 -1
- package/src/plugins/upload.ts +8 -7
- package/src/types.ts +3 -3
- package/src/utils/Logger.ts +80 -0
package/.env.example
CHANGED
|
@@ -40,10 +40,10 @@ DB_USER_PASSWORD_FIELD=
|
|
|
40
40
|
# Auto-detected from unique string fields; comma-separated (default: email)
|
|
41
41
|
DB_USER_IDENTIFIER_FIELDS=
|
|
42
42
|
# Comma-separated: bearer, basic, cookie, header (default: bearer)
|
|
43
|
-
|
|
44
|
-
# Cookie name for cookie auth
|
|
43
|
+
AUTH_STRATEGIES=bearer
|
|
44
|
+
# Cookie name for cookie auth strategy (default: token)
|
|
45
45
|
AUTH_COOKIE_NAME=token
|
|
46
|
-
# Header name for header auth
|
|
46
|
+
# Header name for header auth strategy (default: X-Auth-Token)
|
|
47
47
|
AUTH_CUSTOM_HEADER=X-Auth-Token
|
|
48
48
|
|
|
49
49
|
# ── API Settings ───────────────────────────────────────
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Every table gets full CRUD endpoints. Auth is enabled automatically when a user
|
|
|
53
53
|
|---|:---:|:---:|:---:|:---:|:---:|
|
|
54
54
|
| Full source code ownership | ✓ | — | — | — | ✓ |
|
|
55
55
|
| Schema-first (no UI) | ✓ | ✓ | ✓ | ✓ | — |
|
|
56
|
-
| REST API | ✓ |
|
|
56
|
+
| REST API | ✓ | partial | ✓ | ✓ | ✓ |
|
|
57
57
|
| Multi-database | ✓ | ✓ | — | — | ✓ |
|
|
58
58
|
| Built-in auth | ✓ | — | — | ✓ | ✓ |
|
|
59
59
|
| Per-model ACL | ✓ | ✓ | — | — | ✓ |
|
|
@@ -94,24 +94,24 @@ POST /auth/refresh { "refreshToken": "..." }
|
|
|
94
94
|
GET /auth/me Authorization: Bearer <token>
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Four
|
|
97
|
+
Four strategies — **bearer** (default), **basic**, **cookie**, and **custom header** — configurable globally via `AUTH_STRATEGIES` env var or per endpoint prefix in `config/app.json`:
|
|
98
98
|
|
|
99
99
|
```json
|
|
100
100
|
{
|
|
101
|
-
"
|
|
101
|
+
"endpointAuthStrategy": {
|
|
102
102
|
"/api/v1": ["basic", "bearer"],
|
|
103
103
|
"/api/v2": "bearer"
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
Set `null` for the global default, a string for a single
|
|
108
|
+
Set `null` for the global default, a string for a single strategy, or an array for multiple. Route-level config takes highest priority, then prefix match, then global default.
|
|
109
109
|
|
|
110
110
|
Multi-identifier login lets users authenticate with any unique field (email, username, phone) in a single endpoint.
|
|
111
111
|
|
|
112
112
|
**Production:** `JWT_SECRET` and `JWT_REFRESH_SECRET` must be set explicitly. The server refuses to start without them to prevent session invalidation on restart.
|
|
113
113
|
|
|
114
|
-
> **[Authentication wiki](https://github.com/MertDalbudak/rapidd/wiki/Authentication)** — session stores, route protection, per-endpoint
|
|
114
|
+
> **[Authentication wiki](https://github.com/MertDalbudak/rapidd/wiki/Authentication)** — session stores, route protection, per-endpoint strategy overrides
|
|
115
115
|
|
|
116
116
|
---
|
|
117
117
|
|
package/config/app.json
CHANGED
package/locales/ar_SA.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "الرمز لا يحتوي على معلومات البريد الإلكتروني",
|
|
176
176
|
"oauth_config_missing": "يجب تكوين معرف التطبيق والسر الخاص بـ {provider} في متغيرات البيئة",
|
|
177
177
|
"token_app_mismatch": "الرمز لا ينتمي إلى هذا التطبيق",
|
|
178
|
-
"oauth_email_permission_missing": "مستخدم {provider} لم يمنح إذن البريد الإلكتروني"
|
|
178
|
+
"oauth_email_permission_missing": "مستخدم {provider} لم يمنح إذن البريد الإلكتروني",
|
|
179
|
+
"internal_server_error": "حدث خطأ ما",
|
|
180
|
+
"duplicate_entry": "إدخال مكرر لـ {model}. السجل ذو {field}: '{value}' موجود بالفعل",
|
|
181
|
+
"authentication_not_available": "المصادقة غير متاحة",
|
|
182
|
+
"authentication_required": "المصادقة مطلوبة",
|
|
183
|
+
"user_and_password_required": "المستخدم وكلمة المرور مطلوبان",
|
|
184
|
+
"auth_not_configured": "المصادقة غير مهيأة",
|
|
185
|
+
"email_and_password_required": "البريد الإلكتروني وكلمة المرور مطلوبان",
|
|
186
|
+
"email_already_exists": "البريد الإلكتروني موجود بالفعل",
|
|
187
|
+
"user_registered": "تم تسجيل المستخدم بنجاح",
|
|
188
|
+
"invalid_composite_key": "مفتاح مركب غير صالح: متوقع {expected} حقول، تم استلام {received}",
|
|
189
|
+
"invalid_composite_key_format": "تنسيق مفتاح مركب غير صالح. متوقع كائن أو سلسلة نصية مفصولة بعلامة التلدة",
|
|
190
|
+
"no_permission_to_create": "لا توجد صلاحية لإنشاء {model}",
|
|
191
|
+
"no_permission_to_update": "لا توجد صلاحية لتحديث السجل",
|
|
192
|
+
"no_permission_to_delete": "لا توجد صلاحية لحذف السجل",
|
|
193
|
+
"field_not_nullable": "الحقل '{field}' لا يمكن أن يكون فارغاً",
|
|
194
|
+
"relation_not_included": "العلاقة '{relation}' مُشار إليها في الحقول لكنها غير مُضمّنة. {hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "تم تجاوز أقصى عمق تداخل وهو {depth}",
|
|
196
|
+
"no_file_uploaded": "لم يتم رفع أي ملف"
|
|
179
197
|
}
|
package/locales/de_DE.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "Token enthält keine E-Mail-Informationen",
|
|
176
176
|
"oauth_config_missing": "{provider} App-ID und Secret müssen in Umgebungsvariablen konfiguriert werden",
|
|
177
177
|
"token_app_mismatch": "Token gehört nicht zu dieser Anwendung",
|
|
178
|
-
"oauth_email_permission_missing": "{provider}-Benutzer hat keine E-Mail-Berechtigung erteilt"
|
|
178
|
+
"oauth_email_permission_missing": "{provider}-Benutzer hat keine E-Mail-Berechtigung erteilt",
|
|
179
|
+
"internal_server_error": "Etwas ist schiefgelaufen",
|
|
180
|
+
"duplicate_entry": "Doppelter Eintrag für {model}. Datensatz mit {field}: '{value}' existiert bereits",
|
|
181
|
+
"authentication_not_available": "Authentifizierung ist nicht verfügbar",
|
|
182
|
+
"authentication_required": "Authentifizierung erforderlich",
|
|
183
|
+
"user_and_password_required": "Benutzer und Passwort sind erforderlich",
|
|
184
|
+
"auth_not_configured": "Authentifizierung ist nicht konfiguriert",
|
|
185
|
+
"email_and_password_required": "E-Mail und Passwort sind erforderlich",
|
|
186
|
+
"email_already_exists": "E-Mail existiert bereits",
|
|
187
|
+
"user_registered": "Benutzer erfolgreich registriert",
|
|
188
|
+
"invalid_composite_key": "Ungültiger zusammengesetzter Schlüssel: {expected} Felder erwartet, {received} erhalten",
|
|
189
|
+
"invalid_composite_key_format": "Ungültiges Format für zusammengesetzten Schlüssel. Erwartet wird ein Objekt oder ein durch Tilde getrennter String",
|
|
190
|
+
"no_permission_to_create": "Keine Berechtigung zum Erstellen von {model}",
|
|
191
|
+
"no_permission_to_update": "Keine Berechtigung zum Aktualisieren des Datensatzes",
|
|
192
|
+
"no_permission_to_delete": "Keine Berechtigung zum Löschen des Datensatzes",
|
|
193
|
+
"field_not_nullable": "Feld '{field}' darf nicht null sein",
|
|
194
|
+
"relation_not_included": "Relation '{relation}' wird in Feldern referenziert, aber nicht eingeschlossen. {hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "Maximale Verschachtelungstiefe von {depth} überschritten",
|
|
196
|
+
"no_file_uploaded": "Keine Datei hochgeladen"
|
|
179
197
|
}
|
package/locales/en_US.json
CHANGED
|
@@ -176,5 +176,23 @@
|
|
|
176
176
|
"token_missing_email": "Token does not contain email information",
|
|
177
177
|
"oauth_config_missing": "{provider} App ID and Secret must be configured in environment variables",
|
|
178
178
|
"token_app_mismatch": "Token does not belong to this application",
|
|
179
|
-
"oauth_email_permission_missing": "{provider} user does not have email permission granted"
|
|
179
|
+
"oauth_email_permission_missing": "{provider} user does not have email permission granted",
|
|
180
|
+
"internal_server_error": "Something went wrong",
|
|
181
|
+
"duplicate_entry": "Duplicate entry for {model}. Record with {field}: '{value}' already exists",
|
|
182
|
+
"authentication_not_available": "Authentication is not available",
|
|
183
|
+
"authentication_required": "Authentication required",
|
|
184
|
+
"user_and_password_required": "User and password are required",
|
|
185
|
+
"auth_not_configured": "Authentication is not configured",
|
|
186
|
+
"email_and_password_required": "Email and password are required",
|
|
187
|
+
"email_already_exists": "Email already exists",
|
|
188
|
+
"user_registered": "User registered successfully",
|
|
189
|
+
"invalid_composite_key": "Invalid composite key: expected {expected} fields, received {received}",
|
|
190
|
+
"invalid_composite_key_format": "Invalid composite key format. Expected an object or tilde-separated string",
|
|
191
|
+
"no_permission_to_create": "No permission to create {model}",
|
|
192
|
+
"no_permission_to_update": "No permission to update record",
|
|
193
|
+
"no_permission_to_delete": "No permission to delete record",
|
|
194
|
+
"field_not_nullable": "Field '{field}' cannot be null",
|
|
195
|
+
"relation_not_included": "Relation '{relation}' is referenced in fields but not included. {hint}",
|
|
196
|
+
"max_nesting_depth_exceeded": "Maximum nesting depth of {depth} exceeded",
|
|
197
|
+
"no_file_uploaded": "No file uploaded"
|
|
180
198
|
}
|
package/locales/es_ES.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "El token no contiene información de correo electrónico",
|
|
176
176
|
"oauth_config_missing": "El ID de aplicación y el secreto de {provider} deben configurarse en las variables de entorno",
|
|
177
177
|
"token_app_mismatch": "El token no pertenece a esta aplicación",
|
|
178
|
-
"oauth_email_permission_missing": "El usuario de {provider} no ha otorgado permiso de correo electrónico"
|
|
178
|
+
"oauth_email_permission_missing": "El usuario de {provider} no ha otorgado permiso de correo electrónico",
|
|
179
|
+
"internal_server_error": "Algo salió mal",
|
|
180
|
+
"duplicate_entry": "Entrada duplicada para {model}. El registro con {field}: '{value}' ya existe",
|
|
181
|
+
"authentication_not_available": "La autenticación no está disponible",
|
|
182
|
+
"authentication_required": "Autenticación requerida",
|
|
183
|
+
"user_and_password_required": "Usuario y contraseña son requeridos",
|
|
184
|
+
"auth_not_configured": "La autenticación no está configurada",
|
|
185
|
+
"email_and_password_required": "Correo electrónico y contraseña son requeridos",
|
|
186
|
+
"email_already_exists": "El correo electrónico ya existe",
|
|
187
|
+
"user_registered": "Usuario registrado exitosamente",
|
|
188
|
+
"invalid_composite_key": "Clave compuesta inválida: se esperaban {expected} campos, se recibieron {received}",
|
|
189
|
+
"invalid_composite_key_format": "Formato de clave compuesta inválido. Se espera un objeto o una cadena separada por tildes",
|
|
190
|
+
"no_permission_to_create": "Sin permiso para crear {model}",
|
|
191
|
+
"no_permission_to_update": "Sin permiso para actualizar el registro",
|
|
192
|
+
"no_permission_to_delete": "Sin permiso para eliminar el registro",
|
|
193
|
+
"field_not_nullable": "El campo '{field}' no puede ser nulo",
|
|
194
|
+
"relation_not_included": "La relación '{relation}' está referenciada en los campos pero no incluida. {hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "Se excedió la profundidad máxima de anidación de {depth}",
|
|
196
|
+
"no_file_uploaded": "No se cargó ningún archivo"
|
|
179
197
|
}
|
package/locales/fr_FR.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "Le jeton ne contient pas d'informations d'e-mail",
|
|
176
176
|
"oauth_config_missing": "L'ID d'application et le secret de {provider} doivent être configurés dans les variables d'environnement",
|
|
177
177
|
"token_app_mismatch": "Le jeton n'appartient pas à cette application",
|
|
178
|
-
"oauth_email_permission_missing": "L'utilisateur {provider} n'a pas accordé la permission d'accès à l'e-mail"
|
|
178
|
+
"oauth_email_permission_missing": "L'utilisateur {provider} n'a pas accordé la permission d'accès à l'e-mail",
|
|
179
|
+
"internal_server_error": "Une erreur est survenue",
|
|
180
|
+
"duplicate_entry": "Entrée en double pour {model}. L'enregistrement avec {field} : '{value}' existe déjà",
|
|
181
|
+
"authentication_not_available": "L'authentification n'est pas disponible",
|
|
182
|
+
"authentication_required": "Authentification requise",
|
|
183
|
+
"user_and_password_required": "Utilisateur et mot de passe sont requis",
|
|
184
|
+
"auth_not_configured": "L'authentification n'est pas configurée",
|
|
185
|
+
"email_and_password_required": "E-mail et mot de passe sont requis",
|
|
186
|
+
"email_already_exists": "L'e-mail existe déjà",
|
|
187
|
+
"user_registered": "Utilisateur enregistré avec succès",
|
|
188
|
+
"invalid_composite_key": "Clé composite invalide : {expected} champs attendus, {received} reçus",
|
|
189
|
+
"invalid_composite_key_format": "Format de clé composite invalide. Un objet ou une chaîne séparée par des tildes est attendu",
|
|
190
|
+
"no_permission_to_create": "Pas de permission pour créer {model}",
|
|
191
|
+
"no_permission_to_update": "Pas de permission pour mettre à jour l'enregistrement",
|
|
192
|
+
"no_permission_to_delete": "Pas de permission pour supprimer l'enregistrement",
|
|
193
|
+
"field_not_nullable": "Le champ '{field}' ne peut pas être nul",
|
|
194
|
+
"relation_not_included": "La relation '{relation}' est référencée dans les champs mais non incluse. {hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "Profondeur d'imbrication maximale de {depth} dépassée",
|
|
196
|
+
"no_file_uploaded": "Aucun fichier téléchargé"
|
|
179
197
|
}
|
package/locales/it_IT.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "Il token non contiene informazioni e-mail",
|
|
176
176
|
"oauth_config_missing": "L'ID applicazione e il segreto di {provider} devono essere configurati nelle variabili d'ambiente",
|
|
177
177
|
"token_app_mismatch": "Il token non appartiene a questa applicazione",
|
|
178
|
-
"oauth_email_permission_missing": "L'utente {provider} non ha concesso il permesso e-mail"
|
|
178
|
+
"oauth_email_permission_missing": "L'utente {provider} non ha concesso il permesso e-mail",
|
|
179
|
+
"internal_server_error": "Qualcosa è andato storto",
|
|
180
|
+
"duplicate_entry": "Voce duplicata per {model}. Il record con {field}: '{value}' esiste già",
|
|
181
|
+
"authentication_not_available": "L'autenticazione non è disponibile",
|
|
182
|
+
"authentication_required": "Autenticazione richiesta",
|
|
183
|
+
"user_and_password_required": "Utente e password sono obbligatori",
|
|
184
|
+
"auth_not_configured": "L'autenticazione non è configurata",
|
|
185
|
+
"email_and_password_required": "E-mail e password sono obbligatori",
|
|
186
|
+
"email_already_exists": "L'e-mail esiste già",
|
|
187
|
+
"user_registered": "Utente registrato con successo",
|
|
188
|
+
"invalid_composite_key": "Chiave composita non valida: previsti {expected} campi, ricevuti {received}",
|
|
189
|
+
"invalid_composite_key_format": "Formato chiave composita non valido. Previsto un oggetto o una stringa separata da tilde",
|
|
190
|
+
"no_permission_to_create": "Nessun permesso per creare {model}",
|
|
191
|
+
"no_permission_to_update": "Nessun permesso per aggiornare il record",
|
|
192
|
+
"no_permission_to_delete": "Nessun permesso per eliminare il record",
|
|
193
|
+
"field_not_nullable": "Il campo '{field}' non può essere nullo",
|
|
194
|
+
"relation_not_included": "La relazione '{relation}' è referenziata nei campi ma non inclusa. {hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "Profondità massima di annidamento di {depth} superata",
|
|
196
|
+
"no_file_uploaded": "Nessun file caricato"
|
|
179
197
|
}
|
package/locales/ja_JP.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "トークンにメール情報が含まれていません",
|
|
176
176
|
"oauth_config_missing": "{provider}のアプリIDとシークレットを環境変数で設定する必要があります",
|
|
177
177
|
"token_app_mismatch": "トークンはこのアプリケーションに属していません",
|
|
178
|
-
"oauth_email_permission_missing": "{provider}ユーザーがメール許可を付与していません"
|
|
178
|
+
"oauth_email_permission_missing": "{provider}ユーザーがメール許可を付与していません",
|
|
179
|
+
"internal_server_error": "問題が発生しました",
|
|
180
|
+
"duplicate_entry": "{model}の重複エントリ。{field}: '{value}'のレコードは既に存在します",
|
|
181
|
+
"authentication_not_available": "認証は利用できません",
|
|
182
|
+
"authentication_required": "認証が必要です",
|
|
183
|
+
"user_and_password_required": "ユーザーとパスワードが必要です",
|
|
184
|
+
"auth_not_configured": "認証が設定されていません",
|
|
185
|
+
"email_and_password_required": "メールアドレスとパスワードが必要です",
|
|
186
|
+
"email_already_exists": "メールアドレスは既に存在します",
|
|
187
|
+
"user_registered": "ユーザーが正常に登録されました",
|
|
188
|
+
"invalid_composite_key": "無効な複合キー: {expected}フィールドが必要ですが、{received}を受信しました",
|
|
189
|
+
"invalid_composite_key_format": "無効な複合キー形式。オブジェクトまたはチルダ区切りの文字列が必要です",
|
|
190
|
+
"no_permission_to_create": "{model}を作成する権限がありません",
|
|
191
|
+
"no_permission_to_update": "レコードを更新する権限がありません",
|
|
192
|
+
"no_permission_to_delete": "レコードを削除する権限がありません",
|
|
193
|
+
"field_not_nullable": "フィールド'{field}'はnullにできません",
|
|
194
|
+
"relation_not_included": "リレーション'{relation}'はフィールドで参照されていますが、含まれていません。{hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "最大ネスト深度{depth}を超えました",
|
|
196
|
+
"no_file_uploaded": "ファイルがアップロードされていません"
|
|
179
197
|
}
|
package/locales/pt_BR.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "O token não contém informações de e-mail",
|
|
176
176
|
"oauth_config_missing": "O ID do aplicativo e o segredo do {provider} devem ser configurados nas variáveis de ambiente",
|
|
177
177
|
"token_app_mismatch": "O token não pertence a esta aplicação",
|
|
178
|
-
"oauth_email_permission_missing": "O usuário do {provider} não concedeu permissão de e-mail"
|
|
178
|
+
"oauth_email_permission_missing": "O usuário do {provider} não concedeu permissão de e-mail",
|
|
179
|
+
"internal_server_error": "Algo deu errado",
|
|
180
|
+
"duplicate_entry": "Entrada duplicada para {model}. Registro com {field}: '{value}' já existe",
|
|
181
|
+
"authentication_not_available": "A autenticação não está disponível",
|
|
182
|
+
"authentication_required": "Autenticação necessária",
|
|
183
|
+
"user_and_password_required": "Usuário e senha são obrigatórios",
|
|
184
|
+
"auth_not_configured": "A autenticação não está configurada",
|
|
185
|
+
"email_and_password_required": "E-mail e senha são obrigatórios",
|
|
186
|
+
"email_already_exists": "E-mail já existe",
|
|
187
|
+
"user_registered": "Usuário registrado com sucesso",
|
|
188
|
+
"invalid_composite_key": "Chave composta inválida: esperados {expected} campos, recebidos {received}",
|
|
189
|
+
"invalid_composite_key_format": "Formato de chave composta inválido. Esperado um objeto ou string separada por til",
|
|
190
|
+
"no_permission_to_create": "Sem permissão para criar {model}",
|
|
191
|
+
"no_permission_to_update": "Sem permissão para atualizar o registro",
|
|
192
|
+
"no_permission_to_delete": "Sem permissão para excluir o registro",
|
|
193
|
+
"field_not_nullable": "O campo '{field}' não pode ser nulo",
|
|
194
|
+
"relation_not_included": "A relação '{relation}' é referenciada nos campos mas não está incluída. {hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "Profundidade máxima de aninhamento de {depth} excedida",
|
|
196
|
+
"no_file_uploaded": "Nenhum arquivo enviado"
|
|
179
197
|
}
|
package/locales/ru_RU.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "Токен не содержит информацию об электронной почте",
|
|
176
176
|
"oauth_config_missing": "ID приложения и секрет {provider} должны быть настроены в переменных окружения",
|
|
177
177
|
"token_app_mismatch": "Токен не принадлежит этому приложению",
|
|
178
|
-
"oauth_email_permission_missing": "Пользователь {provider} не предоставил разрешение на доступ к электронной почте"
|
|
178
|
+
"oauth_email_permission_missing": "Пользователь {provider} не предоставил разрешение на доступ к электронной почте",
|
|
179
|
+
"internal_server_error": "Что-то пошло не так",
|
|
180
|
+
"duplicate_entry": "Дублирующая запись для {model}. Запись с {field}: '{value}' уже существует",
|
|
181
|
+
"authentication_not_available": "Аутентификация недоступна",
|
|
182
|
+
"authentication_required": "Требуется аутентификация",
|
|
183
|
+
"user_and_password_required": "Пользователь и пароль обязательны",
|
|
184
|
+
"auth_not_configured": "Аутентификация не настроена",
|
|
185
|
+
"email_and_password_required": "Электронная почта и пароль обязательны",
|
|
186
|
+
"email_already_exists": "Электронная почта уже существует",
|
|
187
|
+
"user_registered": "Пользователь успешно зарегистрирован",
|
|
188
|
+
"invalid_composite_key": "Недопустимый составной ключ: ожидалось {expected} полей, получено {received}",
|
|
189
|
+
"invalid_composite_key_format": "Недопустимый формат составного ключа. Ожидается объект или строка, разделенная тильдой",
|
|
190
|
+
"no_permission_to_create": "Нет разрешения на создание {model}",
|
|
191
|
+
"no_permission_to_update": "Нет разрешения на обновление записи",
|
|
192
|
+
"no_permission_to_delete": "Нет разрешения на удаление записи",
|
|
193
|
+
"field_not_nullable": "Поле '{field}' не может быть пустым",
|
|
194
|
+
"relation_not_included": "Связь '{relation}' указана в полях, но не включена. {hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "Превышена максимальная глубина вложенности {depth}",
|
|
196
|
+
"no_file_uploaded": "Файл не загружен"
|
|
179
197
|
}
|
package/locales/tr_TR.json
CHANGED
|
@@ -175,5 +175,23 @@
|
|
|
175
175
|
"token_missing_email": "Token e-posta bilgisi içermiyor",
|
|
176
176
|
"oauth_config_missing": "{provider} Uygulama ID'si ve Secret ortam değişkenlerinde yapılandırılmalıdır",
|
|
177
177
|
"token_app_mismatch": "Token bu uygulamaya ait değil",
|
|
178
|
-
"oauth_email_permission_missing": "{provider} kullanıcısı e-posta izni vermemiş"
|
|
178
|
+
"oauth_email_permission_missing": "{provider} kullanıcısı e-posta izni vermemiş",
|
|
179
|
+
"internal_server_error": "Bir şeyler ters gitti",
|
|
180
|
+
"duplicate_entry": "{model} için yinelenen kayıt. {field}: '{value}' değerine sahip kayıt zaten mevcut",
|
|
181
|
+
"authentication_not_available": "Kimlik doğrulama kullanılamıyor",
|
|
182
|
+
"authentication_required": "Kimlik doğrulama gerekli",
|
|
183
|
+
"user_and_password_required": "Kullanıcı ve şifre gerekli",
|
|
184
|
+
"auth_not_configured": "Kimlik doğrulama yapılandırılmamış",
|
|
185
|
+
"email_and_password_required": "E-posta ve şifre gerekli",
|
|
186
|
+
"email_already_exists": "E-posta zaten mevcut",
|
|
187
|
+
"user_registered": "Kullanıcı başarıyla kaydedildi",
|
|
188
|
+
"invalid_composite_key": "Geçersiz bileşik anahtar: {expected} alan bekleniyor, {received} alındı",
|
|
189
|
+
"invalid_composite_key_format": "Geçersiz bileşik anahtar formatı. Bir nesne veya tilde ile ayrılmış bir dize bekleniyor",
|
|
190
|
+
"no_permission_to_create": "{model} oluşturma izni yok",
|
|
191
|
+
"no_permission_to_update": "Kaydı güncelleme izni yok",
|
|
192
|
+
"no_permission_to_delete": "Kaydı silme izni yok",
|
|
193
|
+
"field_not_nullable": "'{field}' alanı null olamaz",
|
|
194
|
+
"relation_not_included": "'{relation}' ilişkisi alanlarda referans ediliyor ancak dahil edilmemiş. {hint}",
|
|
195
|
+
"max_nesting_depth_exceeded": "Maksimum iç içe geçme derinliği {depth} aşıldı",
|
|
196
|
+
"no_file_uploaded": "Dosya yüklenmedi"
|
|
179
197
|
}
|
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -138,9 +138,35 @@ export async function buildApp(options: RapiddOptions = {}): Promise<FastifyInst
|
|
|
138
138
|
await loadRoutes(app, routesPath);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// ── Request Logging ─────────────────────────────
|
|
142
|
+
app.addHook('onSend', async (request, _reply, payload) => {
|
|
143
|
+
if (typeof payload === 'string') {
|
|
144
|
+
(request as any)._responsePayload = payload;
|
|
145
|
+
}
|
|
146
|
+
return payload;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.addHook('onResponse', async (request, reply) => {
|
|
150
|
+
Logger.request({
|
|
151
|
+
method: request.method,
|
|
152
|
+
url: request.url,
|
|
153
|
+
status: reply.statusCode,
|
|
154
|
+
time: reply.elapsedTime,
|
|
155
|
+
ip: request.ip,
|
|
156
|
+
contentLength: request.headers['content-length'],
|
|
157
|
+
userId: request.user?.id,
|
|
158
|
+
userAgent: request.headers['user-agent'],
|
|
159
|
+
requestHeaders: request.headers,
|
|
160
|
+
requestBody: request.body,
|
|
161
|
+
responseHeaders: reply.getHeaders(),
|
|
162
|
+
responseBody: (request as any)._responsePayload,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
141
166
|
// ── 404 Handler ─────────────────────────────────
|
|
142
|
-
app.setNotFoundHandler((
|
|
143
|
-
|
|
167
|
+
app.setNotFoundHandler((request, reply) => {
|
|
168
|
+
const language = request.language || 'en_US';
|
|
169
|
+
reply.code(404).send({ status_code: 404, message: LanguageDict.get('record_not_found', null, language) });
|
|
144
170
|
});
|
|
145
171
|
|
|
146
172
|
// ── Graceful Shutdown ───────────────────────────
|
package/src/auth/Auth.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ErrorResponse } from '../core/errors';
|
|
|
6
6
|
import { createStore, SessionStoreManager } from './stores';
|
|
7
7
|
import { loadDMMF, findUserModel, findIdentifierFields, findPasswordField } from '../core/dmmf';
|
|
8
8
|
import { Logger } from '../utils/Logger';
|
|
9
|
-
import type { RapiddUser, AuthOptions,
|
|
9
|
+
import type { RapiddUser, AuthOptions, AuthStrategy, ISessionStore } from '../types';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Authentication class for user login, logout, and session management
|
|
@@ -27,7 +27,7 @@ import type { RapiddUser, AuthOptions, AuthMethod, ISessionStore } from '../type
|
|
|
27
27
|
export class Auth {
|
|
28
28
|
options: Required<Pick<AuthOptions, 'passwordField' | 'saltRounds'>> & AuthOptions & {
|
|
29
29
|
identifierFields: string[];
|
|
30
|
-
|
|
30
|
+
strategies: AuthStrategy[];
|
|
31
31
|
cookieName: string;
|
|
32
32
|
customHeaderName: string;
|
|
33
33
|
session: { ttl: number; store?: string };
|
|
@@ -69,8 +69,8 @@ export class Auth {
|
|
|
69
69
|
...options.jwt,
|
|
70
70
|
},
|
|
71
71
|
saltRounds: options.saltRounds || parseInt(process.env.AUTH_SALT_ROUNDS || '10', 10),
|
|
72
|
-
|
|
73
|
-
|| (process.env.
|
|
72
|
+
strategies: options.strategies
|
|
73
|
+
|| (process.env.AUTH_STRATEGIES?.split(',').map(s => s.trim()) as AuthStrategy[])
|
|
74
74
|
|| ['bearer'],
|
|
75
75
|
cookieName: options.cookieName || process.env.AUTH_COOKIE_NAME || 'token',
|
|
76
76
|
customHeaderName: options.customHeaderName || process.env.AUTH_CUSTOM_HEADER || 'X-Auth-Token',
|
package/src/index.ts
CHANGED
package/src/orm/QueryBuilder.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { prisma, prismaTransaction, getAcl } from '../core/prisma';
|
|
2
2
|
import { ErrorResponse } from '../core/errors';
|
|
3
|
+
import { LanguageDict } from '../core/i18n';
|
|
3
4
|
import { Logger } from '../utils/Logger';
|
|
4
5
|
import * as dmmf from '../core/dmmf';
|
|
5
6
|
import type {
|
|
@@ -2072,7 +2073,7 @@ class QueryBuilder {
|
|
|
2072
2073
|
let statusCode: number = error.status_code || 500;
|
|
2073
2074
|
let message: string = error instanceof ErrorResponse
|
|
2074
2075
|
? error.message
|
|
2075
|
-
: (process.env.NODE_ENV === 'production' ? '
|
|
2076
|
+
: (process.env.NODE_ENV === 'production' ? LanguageDict.get('internal_server_error') : (error.message || String(error)));
|
|
2076
2077
|
|
|
2077
2078
|
// Handle Prisma error codes
|
|
2078
2079
|
if (error?.code && PRISMA_ERROR_MAP[error.code]) {
|
|
@@ -2083,7 +2084,7 @@ class QueryBuilder {
|
|
|
2083
2084
|
if (error.code === 'P2002') {
|
|
2084
2085
|
const target = error.meta?.target;
|
|
2085
2086
|
const modelName = error.meta?.modelName;
|
|
2086
|
-
message =
|
|
2087
|
+
message = LanguageDict.get('duplicate_entry', { model: modelName, field: target, value: data[target as string] });
|
|
2087
2088
|
} else {
|
|
2088
2089
|
message = errorInfo.message!;
|
|
2089
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
|
@@ -70,7 +70,7 @@ const responsePlugin: FastifyPluginAsync = async (fastify) => {
|
|
|
70
70
|
const err = error as Error;
|
|
71
71
|
const message =
|
|
72
72
|
Object.getPrototypeOf(err).constructor === Error && process.env.NODE_ENV === 'production'
|
|
73
|
-
? '
|
|
73
|
+
? LanguageDict.get('internal_server_error', null, language)
|
|
74
74
|
: err.message || String(error);
|
|
75
75
|
|
|
76
76
|
Logger.error(error);
|
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
|
}
|
package/src/utils/Logger.ts
CHANGED
|
@@ -76,6 +76,32 @@ function formatDataPretty(data: unknown[]): string {
|
|
|
76
76
|
return '\n' + parts.join('\n');
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function formatHeaders(headers: Record<string, unknown>): string {
|
|
80
|
+
return Object.entries(headers)
|
|
81
|
+
.filter(([, v]) => v !== undefined)
|
|
82
|
+
.map(([k, v]) => ` ${k}: ${Array.isArray(v) ? v.join(', ') : v}`)
|
|
83
|
+
.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatBody(body: unknown): string {
|
|
87
|
+
if (body === null || body === undefined) return ' (empty)';
|
|
88
|
+
if (typeof body === 'string') {
|
|
89
|
+
try {
|
|
90
|
+
return ' ' + JSON.stringify(JSON.parse(body), null, 2).replace(/\n/g, '\n ');
|
|
91
|
+
} catch {
|
|
92
|
+
return ' ' + body;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (typeof body === 'object') {
|
|
96
|
+
try {
|
|
97
|
+
return ' ' + JSON.stringify(body, null, 2).replace(/\n/g, '\n ');
|
|
98
|
+
} catch {
|
|
99
|
+
return ' ' + String(body);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return ' ' + String(body);
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
function formatError(error: Error | string | unknown): { message: string; toString: string; stack: string } {
|
|
80
106
|
if (error instanceof Error) {
|
|
81
107
|
return {
|
|
@@ -152,6 +178,60 @@ export const Logger = {
|
|
|
152
178
|
console.error(output);
|
|
153
179
|
writeToFile('error.log', output);
|
|
154
180
|
},
|
|
181
|
+
|
|
182
|
+
request(info: {
|
|
183
|
+
method: string;
|
|
184
|
+
url: string;
|
|
185
|
+
status: number;
|
|
186
|
+
time: number;
|
|
187
|
+
ip?: string;
|
|
188
|
+
contentLength?: string;
|
|
189
|
+
userId?: string | number;
|
|
190
|
+
userAgent?: string;
|
|
191
|
+
requestHeaders?: Record<string, unknown>;
|
|
192
|
+
requestBody?: unknown;
|
|
193
|
+
responseHeaders?: Record<string, unknown>;
|
|
194
|
+
responseBody?: unknown;
|
|
195
|
+
}): void {
|
|
196
|
+
if (_silent) return;
|
|
197
|
+
|
|
198
|
+
const { method, url, status, time } = info;
|
|
199
|
+
const timeStr = `${time.toFixed(0)}ms`;
|
|
200
|
+
let output: string;
|
|
201
|
+
|
|
202
|
+
switch (_level) {
|
|
203
|
+
case 'essential':
|
|
204
|
+
output = `[${timestamp()}] ${method} ${url} ${status} ${timeStr}`;
|
|
205
|
+
break;
|
|
206
|
+
case 'fine':
|
|
207
|
+
output = `[${timestamp()}] ${method} ${url} ${status} ${timeStr} | ${info.ip || '-'}${info.userId ? ` | user:${info.userId}` : ''}`;
|
|
208
|
+
break;
|
|
209
|
+
case 'finest': {
|
|
210
|
+
const lines = [`[${timestamp()}] ${method} ${url} ${status} ${timeStr} | ${info.ip || '-'}${info.userId ? ` | user:${info.userId}` : ''}`];
|
|
211
|
+
if (info.requestHeaders) {
|
|
212
|
+
lines.push(' ── Request Headers');
|
|
213
|
+
lines.push(formatHeaders(info.requestHeaders));
|
|
214
|
+
}
|
|
215
|
+
if (info.requestBody !== undefined && info.requestBody !== null) {
|
|
216
|
+
lines.push(' ── Request Body');
|
|
217
|
+
lines.push(formatBody(info.requestBody));
|
|
218
|
+
}
|
|
219
|
+
if (info.responseHeaders) {
|
|
220
|
+
lines.push(' ── Response Headers');
|
|
221
|
+
lines.push(formatHeaders(info.responseHeaders));
|
|
222
|
+
}
|
|
223
|
+
if (info.responseBody !== undefined && info.responseBody !== null) {
|
|
224
|
+
lines.push(' ── Response Body');
|
|
225
|
+
lines.push(formatBody(info.responseBody));
|
|
226
|
+
}
|
|
227
|
+
output = lines.join('\n');
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(output);
|
|
233
|
+
writeToFile('access.log', output);
|
|
234
|
+
},
|
|
155
235
|
};
|
|
156
236
|
|
|
157
237
|
export default Logger;
|