@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 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
- AUTH_METHODS=bearer
44
- # Cookie name for cookie auth method (default: token)
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 method (default: X-Auth-Token)
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 methods — **bearer** (default), **basic**, **cookie**, and **custom header** — configurable globally via `AUTH_METHODS` env var or per endpoint prefix in `config/app.json`:
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
- "endpointAuthMethod": {
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 method, or an array for multiple. Route-level config takes highest priority, then prefix match, then global default.
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 method overrides
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
@@ -151,7 +151,7 @@
151
151
  "name": "Support Team"
152
152
  }
153
153
  },
154
- "endpointAuthMethod": {
154
+ "endpointAuthStrategy": {
155
155
  "/api/v1": null
156
156
  },
157
157
  "languages": [
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.1.3",
6
+ "version": "2.1.4",
7
7
  "description": "Code-first REST API framework for TypeScript. Database in, API out.",
8
8
  "main": "dist/main.js",
9
9
  "bin": {
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((_request, reply) => {
143
- reply.code(404).send({ status_code: 404, message: 'Not found' });
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, AuthMethod, ISessionStore } from '../types';
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
- methods: AuthMethod[];
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
- methods: options.methods
73
- || (process.env.AUTH_METHODS?.split(',').map(s => s.trim()) as AuthMethod[])
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
@@ -74,7 +74,7 @@ export { buildApp } from './app';
74
74
  // ── Types ────────────────────────────────────────────────────────────────────
75
75
  export type {
76
76
  RapiddUser,
77
- AuthMethod,
77
+ AuthStrategy,
78
78
  RouteAuthConfig,
79
79
  ModelOptions,
80
80
  GetManyResult,
@@ -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' ? 'Something went wrong' : (error.message || String(error)));
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 = `Duplicate entry for ${modelName}. Record with ${target}: '${data[target as string]}' already exists`;
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
  }
@@ -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, AuthMethod, RouteAuthConfig } from '../types';
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 endpointAuthMethod from config/app.json (prefix → method(s) mapping)
40
- let endpointAuthMethod: Record<string, AuthMethod | AuthMethod[] | null> = {};
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.endpointAuthMethod) {
44
- endpointAuthMethod = appConfig.endpointAuthMethod;
43
+ if (appConfig.endpointAuthStrategy) {
44
+ endpointAuthStrategy = appConfig.endpointAuthStrategy;
45
45
  }
46
46
  } catch {
47
- // No app.json or no endpointAuthMethod — use global default
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(endpointAuthMethod)
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 methods (checked in order).
55
- // Priority: route config > endpointAuthMethod prefix match > global default
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 methods: AuthMethod[];
60
- if (routeAuth?.methods) {
61
- methods = routeAuth.methods;
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 = endpointAuthMethod[matchedPrefix];
65
+ const value = endpointAuthStrategy[matchedPrefix];
66
66
  if (value === null) {
67
- methods = auth.options.methods;
67
+ strategies = auth.options.strategies;
68
68
  } else if (typeof value === 'string') {
69
- methods = [value];
69
+ strategies = [value];
70
70
  } else {
71
- methods = value;
71
+ strategies = value;
72
72
  }
73
73
  } else {
74
- methods = auth.options.methods;
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 method of methods) {
83
+ for (const strategy of strategies) {
84
84
  if (user) break;
85
85
 
86
- switch (method) {
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.methods.includes('cookie')) {
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.methods.includes('cookie')) {
144
+ if (auth.options.strategies.includes('cookie')) {
145
145
  reply.clearCookie(auth.options.cookieName, { path: '/' });
146
146
  }
147
147
 
@@ -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. "en-GB" → "en_US")
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('-')[0];
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 {
@@ -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
- ? 'Something went wrong'
73
+ ? LanguageDict.get('internal_server_error', null, language)
74
74
  : err.message || String(error);
75
75
 
76
76
  Logger.error(error);
@@ -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; error?: string } {
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, error: 'Invalid filename' };
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, error: `File type '${file.mimetype}' not allowed` };
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, error: `Extension '${ext}' does not match MIME type '${file.mimetype}'` };
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 Error(`File size exceeds limit of ${Math.round(maxSize / 1024 / 1024)}MB`));
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 Error(validation.error);
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 Error(validation.error);
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 AuthMethod = 'bearer' | 'basic' | 'cookie' | 'header';
13
+ export type AuthStrategy = 'bearer' | 'basic' | 'cookie' | 'header';
14
14
 
15
15
  export interface RouteAuthConfig {
16
- methods?: AuthMethod[];
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
- methods?: AuthMethod[];
35
+ strategies?: AuthStrategy[];
36
36
  cookieName?: string;
37
37
  customHeaderName?: string;
38
38
  }
@@ -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;