diff --git a/messages/de.json b/messages/de.json index d34b9da1a3..e45aa0b351 100644 --- a/messages/de.json +++ b/messages/de.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Blockiert - Patch-Version-Updates deaktiviert", "action-disable-auto-update-under-native": "Blockiert - kann nicht unter das Niveau der Muttersprache herabgestuft werden", "action-disable-dev-build": "Blockiert - Entwicklerversionen deaktiviert", - "action-disable-emulator": "Blockiert - Emulator deaktiviert", - "action-disable-prod-build": "Blockiert – Produktionsbuilds deaktiviert", "action-disable-device": "Blockiert – Geräte-Updates deaktiviert", + "action-disable-emulator": "Blockiert - Emulator deaktiviert", "action-disable-platform-android": "Blockiert - Android-Plattform deaktiviert", "action-disable-platform-ios": "Blockiert - iOS Plattform deaktiviert", + "action-disable-prod-build": "Blockiert – Produktionsbuilds deaktiviert", "action-download-10": "Download-Fortschritt 10%", "action-download-20": "Download-Fortschritt 20%", "action-download-30": "Download-Fortschritt 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Admin-Dashboard", "admin-dashboard-construction": "Das Admin-Dashboard befindet sich im Aufbau. Komponenten werden in der nächsten Phase hinzugefügt.", "admin-dashboard-description": "Plattformweite Statistiken und Analysen", + "after": "Danach", "afternoon": "Nachmittag", "alert-2fa-disable": "Bestätigen Sie, dass Sie die 2FA deaktivieren möchten", "alert-2fa-required": "2FA ist erforderlich, um das Passwort zurückzusetzen.", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Sind Sie sicher, dass Sie diesen Schlüssel neu generieren möchten?", "alert-unknown-error": "Unbekannter Fehler, siehe Entwicklerkonsole", "all-apps": "Alle Apps", + "all-operations": "Alle Operationen", "all-organizations": "Alle Organisationen", + "all-tables": "Alle Tabellen", "allow-dev-build": "Erlaube Entwicklungsbau", "allow-develoment-bui": "Erlaube Entwicklungsgeräte", "allow-device-to-self": "Erlauben Sie Geräten, sich selbst zu dissoziieren/assoziiieren.", @@ -181,6 +184,24 @@ "at-least-one-number": "Mindestens eine Zahl", "at-least-one-uppercase-letter": "Mindestens ein Großbuchstabe", "at-least-two-special-characters": "Mindestens ein Sonderzeichen", + "audit-app_versions-delete": "Paket Gelöscht", + "audit-app_versions-insert": "Paket erstellt", + "audit-app_versions-update": "Bündel aktualisiert", + "audit-apps-delete": "App gelöscht", + "audit-apps-insert": "App erstellt", + "audit-apps-update": "App aktualisiert", + "audit-channels-delete": "Kanal gelöscht", + "audit-channels-insert": "Kanal erstellt", + "audit-channels-update": "Kanal aktualisiert", + "audit-log-details": "Audit-Log Details", + "audit-logs": "Audit-Logs", + "audit-logs-description": "Zeigen Sie eine Historie der Änderungen an Ihrer Organisation an, einschließlich Änderungen an Kanälen, Bundles und Teammitgliedern.", + "audit-org_users-delete": "Mitglied Entfernt", + "audit-org_users-insert": "Mitglied Hinzugefügt", + "audit-org_users-update": "Mitglied Aktualisiert", + "audit-orgs-delete": "Organisation Gelöscht", + "audit-orgs-insert": "Organisation Erstellt", + "audit-orgs-update": "Organisation aktualisiert", "available-channels": "Verfügbare Kanäle", "available-in-the-san": "Verfügbar in der Sandbox-App", "available-versions": "Verfügbare Pakete", @@ -196,6 +217,7 @@ "bandwidth-usage": "Bandbreitennutzung:", "bandwith-usage": "Bandbreitennutzung:", "base": "Basis", + "before": "Vorher", "best-plan": "Bester Plan", "bigger-app-size": "Größere App-Größe", "billed-annually-at": "Jährlich abgerechnet bei", @@ -314,8 +336,10 @@ "changed-app-name": "Erfolgreich den App-Namen geändert", "changed-app-retention": "Die Aufbewahrung der App wurde erfolgreich geändert.", "changed-expose-metadata": "Metadaten-Freigabe-Einstellung erfolgreich geändert", + "changed-fields": "Geänderte Felder", "changed-name": "Erfolgreich den Namen des API-Schlüssels geändert", "changed-password-suc": "Passwort erfolgreich geändert", + "changes": "Änderungen", "channel": "Kanal", "channel-ab-testing": "Aktivieren Sie AB-Tests", "channel-ab-testing-percentage": "Prozentsatz der Benutzer, die eine sekundäre Version erhalten", @@ -357,6 +381,7 @@ "clear-filters": "Filter löschen", "cli-doc": "CLI-Dokument", "cli-version": "CLI-Version", + "close": "Schließen", "commands": "Befehle", "comment": "Kommentar", "complete-all-fields": "Bitte füllen Sie alle Felder aus.", @@ -507,6 +532,7 @@ "daily-registrations": "Tägliche Registrierungen", "daily-uploads": "Tägliche Uploads", "dashboard": "Armaturenbrett", + "date": "Datum", "date-range": "Datumsbereich", "debug-api-description": "Verwenden Sie diesen Curl-Befehl, um die genaue API-Anfrage zu reproduzieren, die dieses Gerät stellt, um nach Updates zu suchen.", "debug-api-request": "Debuggen Sie API-Anfrage", @@ -548,6 +574,7 @@ "delete-org": "Organisation löschen", "delete-your-account": "Löschen Sie Ihr Konto", "deleted": "gelöscht", + "deleted-record": "Gelöschter Datensatz", "deletion-failed": "Löschung fehlgeschlagen", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Dies zeigt das Lesen von Eingabewerten aus Komponenten außerhalb des Dialogs", @@ -579,6 +606,7 @@ "deployments-title": "Gesamt", "deployments-trend": "Einsatz-Trend", "detailed-usage-plan": "Detaillierte Nutzung", + "details": "Einzelheiten", "device": "Gerät", "device-id": "Geräte-ID", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Geben Sie Ihr neues Passwort ein und bestätigen Sie es", "error": "Fehler", "error-checking-channels": "Fehler beim Lesen von Kanälen", + "error-fetching-audit-logs": "Fehler beim Abrufen der Audit-Logs", "error-fetching-builds": "Fehler beim Abrufen von Build-Anfragen", "error-fetching-deploy-history": "Fehler beim Abrufen der Bereitstellungshistorie", "error-fetching-members": "Fehler beim Abrufen von Mitgliedern", @@ -639,6 +668,8 @@ "fast-forward": "Schneller Vorlauf", "feel-magic-of-capgo": "Fühle die Magie von:", "filter-actions": "Aktionen", + "filter-by-operation": "Nach Operation filtern", + "filter-by-table": "Nach Tabelle filtern", "first-name": "Vorname", "first-name-required": "Vorname erforderlich", "force-version": "Versionskraft", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Ungültiger 2FA-Code, versuchen Sie es erneut!", "min-update-version": "Minimale Update-Version", "minor": "Minderjähriger", + "minutes-short": "{minutes}m", "misconfigured": "Falsch konfiguriert", "misconfigured-channels": "Einige Kanäle sind falsch konfiguriert. Updates werden für diese Kanäle fehlschlagen!", "missing-email": "Fehlende E-Mail", "missing-name": "Fehlender Name", "mo": "Mo", + "modified": "Modifiziert", "modify-org-info": "Sie können hier die Informationen der Organisation ändern.", "module-heading": "Module", "monthly-active": "Monatlich aktiv", @@ -782,6 +815,7 @@ "new-name-not-changed": "Der neue Name ist der gleiche wie der alte.", "new-name-to-long": "Der neue Name ist zu lang. Sie können nur 32 Zeichen verwenden.", "new-name-to-short": "Der Name des API-Schlüssels ist zu kurz. Er muss mindestens 4 Zeichen lang sein.", + "new-record": "Neuer Datensatz", "new-users": "Neue Benutzer", "next": "Nächster", "next-run": "Nächstes Update", @@ -800,6 +834,7 @@ "no-device-data": "Keine Gerätedaten verfügbar", "no-error-message": "Keine Fehlermeldung verfügbar", "no-manifest-bundle": "Kein Manifest", + "no-organization-selected": "Keine Organisation ausgewählt", "no-permission": "Unzureichende Berechtigungen", "no-permission-ask-super-admin": "ungenügende Berechtigung, bitte bitten Sie einen Super-Admin, dieses Paket unsicher zu löschen", "no-public-channel": "Die App hat keinen öffentlichen Kanal, wir können die Bereitstellung ohne diesen nicht zählen.", @@ -948,6 +983,7 @@ "reset-password": "Passwort zurücksetzen", "reset-spoofed-user": "Hör auf zu fälschen", "reset-your-password": "Setzen Sie Ihr Passwort zurück", + "resource": "Ressource", "retention": "Automatische Löschung von nicht genutzten Paketen (nach x Sekunden)", "retention-cannot-be-negative": "Beibehaltung kann keine negative Zahl sein", "retention-to-big": "Aufbewahrung kann nicht größer als 63113903 sein (2 Jahre)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Suche nach Name oder AppID", "search-by-name-or-bundle-id": "Suche nach Name oder Bundle-ID", "search-by-name-or-email": "Suche nach Name oder E-Mail", + "search-by-record-id": "Nach Datensatz-ID suchen", "search-by-version": "Suche nach Version", "search-channels": "Suchkanal", "search-versions": "Suchpaket", @@ -1114,6 +1151,7 @@ "version-link-fail": "Kann Bundle-Überschreibung nicht festlegen", "version-linked": "Versionslink", "version-name-missing": "Versionsname fehlt", + "view": "Anzeigen", "want-to-unlink": "Möchten Sie die Verknüpfung aufheben?", "warning-organizations-will-be-deleted": "Warnung: Organisationen werden gelöscht", "warning-organizations-will-be-deleted-message": "Sie sind der einzige Super -Administrator in den folgenden Organisationen. \nDiese Organisationen werden dauerhaft gelöscht, wenn Ihr Konto entfernt wird:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "Was möchten Sie mit der Foto-App machen?", "write-key": "Schreiben", "wrong-name-org-del": "Sie haben den Organisationsnamen nicht eingegeben. Sie sollten eingeben: %1", - "minutes-short": "{minutes}m", "x-hours-short": "{hours}h", "yearly": "Jährlich", "yes": "ja", diff --git a/messages/en.json b/messages/en.json index 154c47db0f..9853ae5fd4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -9,6 +9,8 @@ "CustomId": "Custom ID", "Filters": "Filters", "filter-actions": "Actions", + "filter-by-table": "Filter by table", + "filter-by-operation": "Filter by operation", "Information": "Information", "Logs": "Logs", "general": "General", @@ -121,6 +123,41 @@ "admin-dashboard-description": "Platform-wide statistics and analytics", "all-organizations": "All Organizations", "all-apps": "All Apps", + "all-tables": "All Tables", + "all-operations": "All Operations", + "audit-logs": "Audit Logs", + "audit-logs-description": "View a history of changes made to your organization, including modifications to apps, channels, bundles, and team members.", + "error-fetching-audit-logs": "Error fetching audit logs", + "search-by-record-id": "Search by record ID", + "changed-fields": "Changed Fields", + "changes": "Changes", + "new-record": "New Record", + "deleted-record": "Deleted Record", + "no-organization-selected": "No organization selected", + "audit-log-details": "Audit Log Details", + "date": "Date", + "resource": "Resource", + "details": "Details", + "modified": "Modified", + "audit-orgs-insert": "Organization Created", + "audit-orgs-update": "Organization Updated", + "audit-orgs-delete": "Organization Deleted", + "audit-apps-insert": "App Created", + "audit-apps-update": "App Updated", + "audit-apps-delete": "App Deleted", + "audit-channels-insert": "Channel Created", + "audit-channels-update": "Channel Updated", + "audit-channels-delete": "Channel Deleted", + "audit-app_versions-insert": "Bundle Created", + "audit-app_versions-update": "Bundle Updated", + "audit-app_versions-delete": "Bundle Deleted", + "audit-org_users-insert": "Member Added", + "audit-org_users-update": "Member Updated", + "audit-org_users-delete": "Member Removed", + "before": "Before", + "after": "After", + "close": "Close", + "view": "View", "date-range": "Date Range", "30-days": "Last 30 Days", "90-days": "Last 90 Days", diff --git a/messages/es.json b/messages/es.json index 6f24f10bd8..674be4a4dd 100644 --- a/messages/es.json +++ b/messages/es.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Bloqueado - actualizaciones de la versión del parche desactivadas", "action-disable-auto-update-under-native": "Bloqueado - no se puede degradar por debajo de lo nativo", "action-disable-dev-build": "Bloqueado - compilaciones de desarrollo desactivadas", - "action-disable-emulator": "Bloqueado - emulador desactivado", - "action-disable-prod-build": "Bloqueado: compilaciones de producción desactivadas", "action-disable-device": "Bloqueado: actualizaciones para dispositivos desactivadas", + "action-disable-emulator": "Bloqueado - emulador desactivado", "action-disable-platform-android": "Bloqueado - Plataforma Android deshabilitada", "action-disable-platform-ios": "Bloqueado - Plataforma iOS deshabilitada", + "action-disable-prod-build": "Bloqueado: compilaciones de producción desactivadas", "action-download-10": "Progreso de descarga 10%", "action-download-20": "Progreso de descarga 20%", "action-download-30": "Progreso de descarga 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Panel de Administración", "admin-dashboard-construction": "El panel de administración está en construcción. Los componentes se agregarán en la próxima fase.", "admin-dashboard-description": "Estadísticas y análisis a nivel de plataforma", + "after": "Después", "afternoon": "tarde", "alert-2fa-disable": "Confirma que quieres desactivar 2FA", "alert-2fa-required": "Se requiere 2FA para restablecer la contraseña", @@ -147,7 +148,9 @@ "alert-regenerate-key": "¿Estás seguro de que quieres regenerar esta clave?", "alert-unknown-error": "Error desconocido, consulta la consola de desarrollo", "all-apps": "Todas las aplicaciones", + "all-operations": "Todas las operaciones", "all-organizations": "Todas las Organizaciones", + "all-tables": "Todas las tablas", "allow-dev-build": "Permitir la construcción de desarrollo", "allow-develoment-bui": "Permitir dispositivos de desarrollo", "allow-device-to-self": "Permitir que los dispositivos se disocien/asocien por sí mismos", @@ -181,6 +184,24 @@ "at-least-one-number": "Al menos un número", "at-least-one-uppercase-letter": "Al menos una letra mayúscula", "at-least-two-special-characters": "Al menos un carácter especial", + "audit-app_versions-delete": "Paquete Eliminado", + "audit-app_versions-insert": "Paquete Creado", + "audit-app_versions-update": "Paquete Actualizado", + "audit-apps-delete": "Aplicación Eliminada", + "audit-apps-insert": "Aplicación Creada", + "audit-apps-update": "Aplicación Actualizada", + "audit-channels-delete": "Canal Eliminado", + "audit-channels-insert": "Canal Creado", + "audit-channels-update": "Canal Actualizado", + "audit-log-details": "Detalles del registro de auditoría", + "audit-logs": "Registros de auditoría", + "audit-logs-description": "Vea un historial de cambios realizados en su organización, incluyendo modificaciones a canales, paquetes y miembros del equipo.", + "audit-org_users-delete": "Miembro Eliminado", + "audit-org_users-insert": "Miembro Agregado", + "audit-org_users-update": "Miembro Actualizado", + "audit-orgs-delete": "Organización Eliminada", + "audit-orgs-insert": "Organización Creada", + "audit-orgs-update": "Organización Actualizada", "available-channels": "Canales disponibles", "available-in-the-san": "Disponible en la aplicación sandbox", "available-versions": "Paquetes disponibles", @@ -196,6 +217,7 @@ "bandwidth-usage": "Uso de ancho de banda:", "bandwith-usage": "Uso de ancho de banda:", "base": "Base", + "before": "Antes", "best-plan": "Mejor plan", "bigger-app-size": "Tamaño de aplicación más grande", "billed-annually-at": "Facturado anualmente en", @@ -314,8 +336,10 @@ "changed-app-name": "Nombre de la aplicación cambiado con éxito", "changed-app-retention": "Cambio exitoso de la retención de la aplicación", "changed-expose-metadata": "Configuración de exposición de metadatos cambiada exitosamente", + "changed-fields": "Campos modificados", "changed-name": "Nombre de apikey cambiado exitosamente", "changed-password-suc": "Contraseña cambiada con éxito", + "changes": "Cambios", "channel": "Canal", "channel-ab-testing": "Habilitar la prueba AB", "channel-ab-testing-percentage": "Porcentaje de usuarios que reciben la versión secundaria", @@ -357,6 +381,7 @@ "clear-filters": "Borrar Filtros", "cli-doc": "Documento CLI", "cli-version": "Versión de CLI", + "close": "Cerrar", "commands": "órdenes", "comment": "Comentario", "complete-all-fields": "Por favor complete todos los campos", @@ -507,6 +532,7 @@ "daily-registrations": "Registros Diarios", "daily-uploads": "Subidas diarias", "dashboard": "tablero de instrumentos", + "date": "Fecha", "date-range": "Rango de Fechas", "debug-api-description": "Utilice este comando curl para reproducir la exacta solicitud de API que este dispositivo hace para buscar actualizaciones", "debug-api-request": "Depurar Solicitud de API", @@ -548,6 +574,7 @@ "delete-org": "Eliminar organización", "delete-your-account": "Elimina tu cuenta", "deleted": "eliminado", + "deleted-record": "Registro eliminado", "deletion-failed": "La eliminación falló", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Esto demuestra la lectura de valores de entrada desde componentes fuera del diálogo", @@ -579,6 +606,7 @@ "deployments-title": "Total", "deployments-trend": "Tendencia de Implementaciones", "detailed-usage-plan": "Uso detallado", + "details": "Detalles", "device": "Dispositivo", "device-id": "ID del dispositivo", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Ingrese su nueva contraseña y confirme", "error": "Error", "error-checking-channels": "Error al leer los canales", + "error-fetching-audit-logs": "Error al obtener los registros de auditoría", "error-fetching-builds": "Error al buscar solicitudes de construcción", "error-fetching-deploy-history": "Error al buscar el historial de despliegue", "error-fetching-members": "Error al buscar miembros", @@ -639,6 +668,8 @@ "fast-forward": "Avance Rápido", "feel-magic-of-capgo": "Siente la magia de:", "filter-actions": "Acciones", + "filter-by-operation": "Filtrar por operación", + "filter-by-table": "Filtrar por tabla", "first-name": "Nombre de pila", "first-name-required": "Se requiere el primer nombre", "force-version": "Versión forzada", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Código 2FA inválido, ¡inténtalo de nuevo!", "min-update-version": "Versión de actualización mínima", "minor": "Menor", + "minutes-short": "{minutes}m", "misconfigured": "Mal configurado", "misconfigured-channels": "Algunos canales están mal configurados. ¡Las actualizaciones fallarán para esos canales!", "missing-email": "Correo electrónico faltante", "missing-name": "Nombre faltante", "mo": "Yo", + "modified": "Modificado", "modify-org-info": "Puedes modificar la información de la organización aquí.", "module-heading": "Módulos", "monthly-active": "Activo mensualmente", @@ -782,6 +815,7 @@ "new-name-not-changed": "El nuevo nombre es el mismo que el antiguo.", "new-name-to-long": "El nuevo nombre es demasiado largo. Solo puedes usar 32 caracteres.", "new-name-to-short": "El nombre de la clave API es demasiado corto. Debe tener al menos 4 caracteres de longitud.", + "new-record": "Nuevo registro", "new-users": "Nuevos Usuarios", "next": "Siguiente", "next-run": "Próxima actualización", @@ -800,6 +834,7 @@ "no-device-data": "No hay datos disponibles del dispositivo", "no-error-message": "No hay mensaje de error disponible", "no-manifest-bundle": "No hay manifiesto", + "no-organization-selected": "Ninguna organización seleccionada", "no-permission": "Permisos insuficientes", "no-permission-ask-super-admin": "permiso insuficiente, por favor pide a un super administrador que elimine este paquete de manera insegura", "no-public-channel": "La aplicación no tiene un canal público, no podemos contar la implementación sin él.", @@ -948,6 +983,7 @@ "reset-password": "Restablecer Contraseña", "reset-spoofed-user": "Deja de falsificar", "reset-your-password": "Restablece tu contraseña", + "resource": "Recurso", "retention": "Eliminar automáticamente los paquetes no utilizados (después de x segundos)", "retention-cannot-be-negative": "La retención no puede ser un número negativo", "retention-to-big": "La retención no puede ser mayor que 63113903 (2 años)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Busca por nombre o AppID", "search-by-name-or-bundle-id": "Busca por nombre o ID de paquete", "search-by-name-or-email": "Buscar por nombre o correo electrónico", + "search-by-record-id": "Buscar por ID de registro", "search-by-version": "Buscar por versión", "search-channels": "Buscar canal", "search-versions": "Buscar paquete", @@ -1114,6 +1151,7 @@ "version-link-fail": "No se puede establecer la anulación del paquete", "version-linked": "Enlace de versión", "version-name-missing": "Falta el nombre de la versión", + "view": "Ver", "want-to-unlink": "¿Quieres desvincular?", "warning-organizations-will-be-deleted": "Advertencia: Las Organizaciones Serán Eliminadas", "warning-organizations-will-be-deleted-message": "Eres el único super administrador en las siguientes organizaciones. Estas organizaciones se eliminarán permanentemente cuando se elimine tu cuenta:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "¿Qué te gustaría hacer con la aplicación de fotos?", "write-key": "Escribe", "wrong-name-org-del": "No has escrito el nombre de la organización. Se suponía que debías escribir: %1", - "minutes-short": "{minutes}m", "x-hours-short": "{hours}h", "yearly": "Anualmente", "yes": "sí", diff --git a/messages/fr.json b/messages/fr.json index 3d0a98cc94..7a956c010f 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Bloqué - mises à jour de la version du correctif désactivées", "action-disable-auto-update-under-native": "Bloqué - ne peut pas être rétrogradé en dessous du natif", "action-disable-dev-build": "Bloqué - constructions de développement désactivées", - "action-disable-emulator": "Bloqué - émulateur désactivé", - "action-disable-prod-build": "Bloqué – builds de production désactivés", "action-disable-device": "Bloqué – mises à jour des appareils désactivées", + "action-disable-emulator": "Bloqué - émulateur désactivé", "action-disable-platform-android": "Bloqué - Plateforme Android désactivée", "action-disable-platform-ios": "Bloqué - Plateforme iOS désactivée", + "action-disable-prod-build": "Bloqué – builds de production désactivés", "action-download-10": "Téléchargement en cours 10%", "action-download-20": "Téléchargement en cours 20%", "action-download-30": "Téléchargement en cours 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Tableau de bord administrateur", "admin-dashboard-construction": "Le tableau de bord administrateur est en construction. Les composants seront ajoutés dans la prochaine phase.", "admin-dashboard-description": "Statistiques et analyses à l'échelle de la plateforme", + "after": "Après", "afternoon": "après-midi", "alert-2fa-disable": "Confirmez que vous souhaitez désactiver 2FA", "alert-2fa-required": "La 2FA est nécessaire pour réinitialiser le mot de passe", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Êtes-vous sûr de vouloir régénérer cette clé ?", "alert-unknown-error": "Erreur inconnue, voir la console de développement", "all-apps": "Toutes les applications", + "all-operations": "Toutes les opérations", "all-organizations": "Toutes les Organisations", + "all-tables": "Toutes les tables", "allow-dev-build": "Permettre la construction de développement", "allow-develoment-bui": "Autoriser les appareils de développement", "allow-device-to-self": "Permettre aux appareils de s'auto-dissocier / s'associer", @@ -181,6 +184,24 @@ "at-least-one-number": "Au moins un nombre", "at-least-one-uppercase-letter": "Au moins une lettre majuscule", "at-least-two-special-characters": "Au moins un caractère spécial", + "audit-app_versions-delete": "Lot Supprimé", + "audit-app_versions-insert": "Lot Créé", + "audit-app_versions-update": "Paquet mis à jour", + "audit-apps-delete": "Application Supprimée", + "audit-apps-insert": "Application Créée", + "audit-apps-update": "Application mise à jour", + "audit-channels-delete": "Chaîne Supprimée", + "audit-channels-insert": "Chaîne créée", + "audit-channels-update": "Chaîne mise à jour", + "audit-log-details": "Détails du journal d'audit", + "audit-logs": "Journaux d'audit", + "audit-logs-description": "Consultez l'historique des modifications apportées à votre organisation, y compris les modifications des canaux, des bundles et des membres de l'équipe.", + "audit-org_users-delete": "Membre Supprimé", + "audit-org_users-insert": "Membre Ajouté", + "audit-org_users-update": "Membre Mis à Jour", + "audit-orgs-delete": "Organisation Supprimée", + "audit-orgs-insert": "Organisation Créée", + "audit-orgs-update": "Organisation mise à jour", "available-channels": "Canaux disponibles", "available-in-the-san": "Disponible dans l'application sandbox", "available-versions": "Bundles disponibles", @@ -196,6 +217,7 @@ "bandwidth-usage": "Utilisation de la bande passante:", "bandwith-usage": "Utilisation de la bande passante:", "base": "Base", + "before": "Avant", "best-plan": "Meilleur plan", "bigger-app-size": "Taille d'application plus grande", "billed-annually-at": "Facturé annuellement à", @@ -314,8 +336,10 @@ "changed-app-name": "Nom de l'application modifié avec succès", "changed-app-retention": "Changement réussi de la rétention de l'application", "changed-expose-metadata": "Paramètre d'exposition des métadonnées modifié avec succès", + "changed-fields": "Champs modifiés", "changed-name": "Changement de nom de l'apikey réussi", "changed-password-suc": "Mot de passe modifié avec succès", + "changes": "Modifications", "channel": "Canal", "channel-ab-testing": "Activer le test AB", "channel-ab-testing-percentage": "Pourcentage d'utilisateurs recevant la version secondaire", @@ -357,6 +381,7 @@ "clear-filters": "Effacer les filtres", "cli-doc": "Document CLI", "cli-version": "Version CLI", + "close": "Fermer", "commands": "commandes", "comment": "Commentaire", "complete-all-fields": "Veuillez remplir tous les champs", @@ -507,6 +532,7 @@ "daily-registrations": "Inscriptions Quotidiennes", "daily-uploads": "Téléchargements quotidiens", "dashboard": "tableau de bord", + "date": "Date", "date-range": "Plage de Dates", "debug-api-description": "Utilisez cette commande curl pour reproduire exactement la requête API que cet appareil fait pour vérifier les mises à jour", "debug-api-request": "Déboguer la demande d'API", @@ -548,6 +574,7 @@ "delete-org": "Supprimer l'organisation", "delete-your-account": "Supprimez votre compte", "deleted": "supprimé", + "deleted-record": "Enregistrement supprimé", "deletion-failed": "La suppression a échoué", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Cela démontre la lecture des valeurs d'entrée à partir de composants extérieurs au dialogue", @@ -579,6 +606,7 @@ "deployments-title": "Total", "deployments-trend": "Tendance des déploiements", "detailed-usage-plan": "Utilisation détaillée", + "details": "Détails", "device": "Appareil", "device-id": "ID de l'appareil", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Entrez votre nouveau mot de passe et confirmez", "error": "Erreur", "error-checking-channels": "Erreur de lecture des chaînes", + "error-fetching-audit-logs": "Erreur lors de la récupération des journaux d'audit", "error-fetching-builds": "Erreur lors de la récupération des demandes de construction", "error-fetching-deploy-history": "Erreur lors de la récupération de l'historique de déploiement", "error-fetching-members": "Erreur lors de la récupération des membres", @@ -639,6 +668,8 @@ "fast-forward": "Avance Rapide", "feel-magic-of-capgo": "Ressentez la magie de :", "filter-actions": "Actions", + "filter-by-operation": "Filtrer par opération", + "filter-by-table": "Filtrer par table", "first-name": "Prénom", "first-name-required": "Prénom requis", "force-version": "Version forcée", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Code 2FA invalide, essayez à nouveau !", "min-update-version": "Version de mise à jour minimale", "minor": "Mineur", + "minutes-short": "{minutes}m", "misconfigured": "Mal configuré", "misconfigured-channels": "Certains canaux sont mal configurés. Les mises à jour échoueront pour ces canaux!", "missing-email": "Email manquant", "missing-name": "Nom manquant", "mo": "Mo", + "modified": "Modifié", "modify-org-info": "Vous pouvez modifier les informations de l'organisation ici.", "module-heading": "Modules", "monthly-active": "Actif mensuellement", @@ -782,6 +815,7 @@ "new-name-not-changed": "Le nouveau nom est le même que l'ancien.", "new-name-to-long": "Le nouveau nom est trop long. Vous ne pouvez utiliser que 32 caractères.", "new-name-to-short": "Le nom de l'apikey est trop court. Il doit comporter au moins 4 caractères.", + "new-record": "Nouvel enregistrement", "new-users": "Nouveaux Utilisateurs", "next": "Suivant", "next-run": "Prochaine mise à jour", @@ -800,6 +834,7 @@ "no-device-data": "Aucune donnée de l'appareil disponible", "no-error-message": "Aucun message d'erreur disponible", "no-manifest-bundle": "Aucun manifeste", + "no-organization-selected": "Aucune organisation sélectionnée", "no-permission": "Autorisations insuffisantes", "no-permission-ask-super-admin": "autorisation insuffisante, veuillez demander à un super administrateur de supprimer ce paquet de manière non sécurisée", "no-public-channel": "L'application n'a pas de canal public, nous ne pouvons pas compter le déploiement sans cela", @@ -948,6 +983,7 @@ "reset-password": "Réinitialiser le mot de passe", "reset-spoofed-user": "Arrêtez le spoofing", "reset-your-password": "Réinitialisez votre mot de passe", + "resource": "Ressource", "retention": "Supprimer automatiquement les lots non utilisés (après x secondes)", "retention-cannot-be-negative": "La rétention ne peut pas être un nombre négatif", "retention-to-big": "La rétention ne peut pas être supérieure à 63113903 (2 ans)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Recherche par nom ou AppID", "search-by-name-or-bundle-id": "Recherche par nom ou ID de bundle", "search-by-name-or-email": "Recherche par nom ou email", + "search-by-record-id": "Rechercher par ID d'enregistrement", "search-by-version": "Recherche par version", "search-channels": "Chercher une chaîne", "search-versions": "Recherche de paquet", @@ -1114,6 +1151,7 @@ "version-link-fail": "Impossible de définir la substitution de bundle", "version-linked": "Lien de version", "version-name-missing": "Le nom de la version est manquant", + "view": "Voir", "want-to-unlink": "Voulez-vous dissocier?", "warning-organizations-will-be-deleted": "Avertissement: les organisations seront supprimées", "warning-organizations-will-be-deleted-message": "Vous êtes le seul super administrateur dans les organisations suivantes. Ces organisations seront supprimées en permanence lorsque votre compte sera supprimé:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "Que souhaitez-vous faire avec l'application photo ?", "write-key": "Écrire", "wrong-name-org-del": "Vous n'avez pas tapé le nom de l'organisation. Vous étiez censé taper : %1", - "minutes-short": "{minutes}m", "x-hours-short": "{hours}h", "yearly": "Annuellement", "yes": "oui", diff --git a/messages/hi.json b/messages/hi.json index 02e9909cc5..191bf7f96c 100644 --- a/messages/hi.json +++ b/messages/hi.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "अवरुद्ध - पैच संस्करण अपडेट निष्क्रिय कर दिए गए", "action-disable-auto-update-under-native": "अवरुद्ध - मूल से नीचे अवनति करने की अनुमति नहीं है", "action-disable-dev-build": "अवरुद्ध - डेवलपर संस्करणों को निष्क्रिय कर दिया गया है", - "action-disable-emulator": "अवरुद्ध - एमुलेटर निष्क्रिय किया गया है", - "action-disable-prod-build": "ब्लॉक किया गया - प्रोडक्शन बिल्ड निष्क्रिय", "action-disable-device": "ब्लॉक किया गया - डिवाइस अपडेट निष्क्रिय", + "action-disable-emulator": "अवरुद्ध - एमुलेटर निष्क्रिय किया गया है", "action-disable-platform-android": "अवरुद्ध - एंड्रॉयड प्लेटफॉर्म निष्क्रिय कर दिया गया है", "action-disable-platform-ios": "अवरुद्ध - iOS प्लेटफॉर्म निष्क्रिय कर दिया गया है", + "action-disable-prod-build": "ब्लॉक किया गया - प्रोडक्शन बिल्ड निष्क्रिय", "action-download-10": "डाउनलोड प्रगति 10%", "action-download-20": "डाउनलोड प्रगति 20%", "action-download-30": "डाउनलोड प्रगति 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "व्यवस्थापक डैशबोर्ड", "admin-dashboard-construction": "व्यवस्थापक डैशबोर्ड निर्माणाधीन है। कौम्पोनेंटस को अगले चरण में जोडा जाएगा।", "admin-dashboard-description": "प्लेटफॉर्म-व्यापी सांख्यिकी और विश्लेषण", + "after": "बाद में", "afternoon": "दोपहर", "alert-2fa-disable": "कृपया पुष्टि करें कि आप 2FA को निष्क्रिय करना चाहते हैं", "alert-2fa-required": "पासवर्ड रीसेट करने के लिए 2FA आवश्यक है", @@ -147,7 +148,9 @@ "alert-regenerate-key": "क्या आप वाकई इस कुंजी को पुनर्जनित करना चाहते हैं?", "alert-unknown-error": "अज्ञात त्रुटि, डेव कंसोल देखें", "all-apps": "सभी ऐप्स", + "all-operations": "सभी ऑपरेशन", "all-organizations": "सभी संगठन", + "all-tables": "सभी टेबल", "allow-dev-build": "विकास बिल्ड की अनुमति दें", "allow-develoment-bui": "विकास उपकरणों की अनुमति दें", "allow-device-to-self": "उपकरणों को स्वत: अलग/सहयुक्त होने की अनुमति दें", @@ -181,6 +184,24 @@ "at-least-one-number": "कम से कम एक संख्या", "at-least-one-uppercase-letter": "कम से कम एक अपरकेस अक्षर", "at-least-two-special-characters": "कम से कम एक विशेष वर्ण", + "audit-app_versions-delete": "बंडल हटा दिया गया", + "audit-app_versions-insert": "बंडल बनाया गया", + "audit-app_versions-update": "बंडल अपडेट हुआ", + "audit-apps-delete": "ऐप हटा दिया गया", + "audit-apps-insert": "ऐप बनाया गया", + "audit-apps-update": "ऐप अपडेट हुआ", + "audit-channels-delete": "चैनल हटा दिया गया", + "audit-channels-insert": "चैनल बनाया गया", + "audit-channels-update": "चैनल अपडेट हुआ", + "audit-log-details": "ऑडिट लॉग विवरण", + "audit-logs": "ऑडिट लॉग", + "audit-logs-description": "अपने संगठन में किए गए परिवर्तनों का इतिहास देखें, जिसमें चैनलों, बंडलों और टीम सदस्यों में संशोधन शामिल हैं।", + "audit-org_users-delete": "सदस्य हटाए गए", + "audit-org_users-insert": "सदस्य जोड़े गए", + "audit-org_users-update": "सदस्य अपडेट हुआ", + "audit-orgs-delete": "संगठन हटा दिया गया", + "audit-orgs-insert": "संगठन का निर्माण हुआ", + "audit-orgs-update": "संगठन अपडेट किया गया", "available-channels": "उपलब्ध चैनल", "available-in-the-san": "सैंडबॉक्स ऐप में उपलब्ध है", "available-versions": "उपलब्ध बंडल", @@ -196,6 +217,7 @@ "bandwidth-usage": "बैंडविड्थ उपयोग:", "bandwith-usage": "बैंडविड्थ उपयोग:", "base": "आधार", + "before": "पहले", "best-plan": "सर्वश्रेष्ठ योजना", "bigger-app-size": "बड़ा ऐप साइज", "billed-annually-at": "वार्षिक बिलिंग पर", @@ -314,8 +336,10 @@ "changed-app-name": "ऐप का नाम सफलतापूर्वक बदल दिया गया है", "changed-app-retention": "ऐप की रिटेंशन सफलतापूर्वक बदल दी गई है", "changed-expose-metadata": "मेटाडेटा एक्सपोज़ सेटिंग सफलतापूर्वक बदली गई", + "changed-fields": "बदले गए फ़ील्ड", "changed-name": "एपीआईकुंजी का नाम सफलतापूर्वक बदल गया", "changed-password-suc": "पासवर्ड सफलतापूर्वक बदल गया", + "changes": "परिवर्तन", "channel": "चैनल", "channel-ab-testing": "AB परीक्षण सक्षम करें", "channel-ab-testing-percentage": "उपयोगकर्ताओं का प्रतिशत जो द्वितीय संस्करण प्राप्त कर रहे हैं", @@ -357,6 +381,7 @@ "clear-filters": "फ़िल्टर हटाएं", "cli-doc": "CLI दस्तावेज़", "cli-version": "CLI संस्करण", + "close": "बंद करें", "commands": "आदेश", "comment": "टिप्पणी", "complete-all-fields": "कृपया सभी फ़ील्ड पूरे करें", @@ -507,6 +532,7 @@ "daily-registrations": "रोजाना पंजीकरण", "daily-uploads": "रोजाना अपलोड", "dashboard": "डैशबोर्ड", + "date": "तारीख", "date-range": "तिथि सीमा", "debug-api-description": "इस curl कमांड का उपयोग करें ताकि आप इस डिवाइस द्वारा अपडेट की जाँच के लिए मांगे जाने पर सही API ज़रूरतों को पुन: प्रक्षेपित (reproduce) कर सकें।", "debug-api-request": "API अनुरोध डीबग करें", @@ -548,6 +574,7 @@ "delete-org": "संगठन हटाएं", "delete-your-account": "अपना खाता हटाएं", "deleted": "हटाया गया", + "deleted-record": "हटाया गया रिकॉर्ड", "deletion-failed": "हटाने में विफल", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "यह बात साबित करता है कि डायलॉग के बाहर के कौम्पोनेन्टस से इनपुट मानों को पढ़ना।", @@ -579,6 +606,7 @@ "deployments-title": "कुल", "deployments-trend": "डिप्लॉयमेंट ट्रेंड", "detailed-usage-plan": "विस्तृत उपयोग", + "details": "विवरण", "device": "उपकरण", "device-id": "उपकरण आईडी", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "अपना नया पासवर्ड दर्ज करें और पुष्टि करें", "error": "त्रुटि", "error-checking-channels": "चैनल पढ़ने में त्रुटि", + "error-fetching-audit-logs": "ऑडिट लॉग प्राप्त करने में त्रुटि", "error-fetching-builds": "बिल्ड अनुरोध प्राप्त करने में त्रुटि", "error-fetching-deploy-history": "डिप्लॉयमेंट इतिहास प्राप्त करने में त्रुटि", "error-fetching-members": "सदस्यों को लाने में त्रुटि", @@ -639,6 +668,8 @@ "fast-forward": "तेजी से आगे बढ़ें", "feel-magic-of-capgo": "का जादू महसूस करें:", "filter-actions": "क्रियाएँ", + "filter-by-operation": "ऑपरेशन के अनुसार फ़िल्टर करें", + "filter-by-table": "तालिका के अनुसार फ़िल्टर करें", "first-name": "पहला नाम", "first-name-required": "पहला नाम आवश्यक है", "force-version": "बल संस्करण", @@ -758,11 +789,13 @@ "mfa-invalid-code": "अमान्य 2FA कोड, पुनः प्रयास करें!", "min-update-version": "न्यूनतम अद्यतन संस्करण", "minor": "मामूली", + "minutes-short": "{minutes}मि", "misconfigured": "गलत तरीके से कॉन्फ़िगर किया गया", "misconfigured-channels": "कुछ चैनल गलत तरीके से कॉन्फ़िगर किए गए हैं। उन चैनलों के लिए अपडेट्स विफल होंगे!", "missing-email": "ईमेल गुम हो गया", "missing-name": "नाम अनुपस्थित", "mo": "मो", + "modified": "संशोधित", "modify-org-info": "आप यहां संगठन की जानकारी को संशोधित कर सकते हैं।", "module-heading": "मॉड्यूल", "monthly-active": "मासिक सक्रिय", @@ -782,6 +815,7 @@ "new-name-not-changed": "नया नाम पुराने वाले के समान है।", "new-name-to-long": "नया नाम बहुत लंबा है। आप केवल 32 अक्षरों का उपयोग कर सकते हैं।", "new-name-to-short": "एपीआईकी नाम बहुत छोटा है। इसे कम से कम 4 वर्णों का होना चाहिए।", + "new-record": "नया रिकॉर्ड", "new-users": "नए उपयोगकर्ता", "next": "अगला", "next-run": "अगला अपडेट", @@ -800,6 +834,7 @@ "no-device-data": "कोई उपकरण डेटा उपलब्ध नहीं है", "no-error-message": "कोई त्रुटि संदेश उपलब्ध नहीं है", "no-manifest-bundle": "कोई मैनिफ़ेस्ट नहीं", + "no-organization-selected": "कोई संगठन चयनित नहीं", "no-permission": "अपर्याप्त अनुमतियाँ", "no-permission-ask-super-admin": "अपर्याप्त अनुमति, कृपया इस बंडल को सुरक्षित रूप से हटाने के लिए सुपर व्यवस्थापक से पूछें", "no-public-channel": "ऐप में कोई सार्वजनिक चैनल नहीं है, हम इसके बिना परिस्थापन की गणना नहीं कर सकते।", @@ -948,6 +983,7 @@ "reset-password": "पासवर्ड रीसेट करें", "reset-spoofed-user": "स्पूफिंग बंद करो", "reset-your-password": "अपना पासवर्ड रीसेट करें", + "resource": "संसाधन", "retention": "उपयोग नहीं होने पर बंडल्स को स्वत: हटाएं (x सेकंड के बाद)", "retention-cannot-be-negative": "संरक्षण एक ऋणात्मक संख्या नहीं हो सकता है", "retention-to-big": "संरक्षण 63113903 (2 वर्ष) से बड़ा नहीं हो सकता।", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "नाम या ऐपआईडी द्वारा खोजें", "search-by-name-or-bundle-id": "नाम या बंडल ID से खोजें", "search-by-name-or-email": "नाम या ईमेल द्वारा खोजें", + "search-by-record-id": "रिकॉर्ड आईडी द्वारा खोजें", "search-by-version": "संस्करण द्वारा खोजें", "search-channels": "चैनल खोजें", "search-versions": "बंडल खोजें", @@ -1114,6 +1151,7 @@ "version-link-fail": "बंडल ओवरराइड सेट नहीं कर सकते", "version-linked": "संस्करण लिंक", "version-name-missing": "संस्करण का नाम गुम है", + "view": "देखें", "want-to-unlink": "क्या आप अनलिंक करना चाहते हैं?", "warning-organizations-will-be-deleted": "चेतावनी: संगठनों को हटा दिया जाएगा", "warning-organizations-will-be-deleted-message": "आप निम्नलिखित संगठनों में एकमात्र सुपर व्यवस्थापक हैं। जब आपका खाता हटाया जाएगा,तो इन संगठनों को स्थायी रूप से हटा दिया जाएगा:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "आप ऐप फोटो के साथ क्या करना चाहेंगे?", "write-key": "लिखें", "wrong-name-org-del": "आपने संगठन का नाम नहीं टाइप किया। आपको %1 टाइप करना था।", - "minutes-short": "{minutes}मि", "x-hours-short": "{hours}h", "yearly": "वार्षिक", "yes": "हाँ", diff --git a/messages/id.json b/messages/id.json index 7142a03af8..a0e210e502 100644 --- a/messages/id.json +++ b/messages/id.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Diblokir - pembaruan versi patch dinonaktifkan", "action-disable-auto-update-under-native": "Diblokir - tidak dapat diturunkan di bawah versi asli", "action-disable-dev-build": "Diblokir - pembangunan dev dinonaktifkan", - "action-disable-emulator": "Diblokir - emulator dinonaktifkan", - "action-disable-prod-build": "Diblokir - build produksi dinonaktifkan", "action-disable-device": "Diblokir - pembaruan perangkat dinonaktifkan", + "action-disable-emulator": "Diblokir - emulator dinonaktifkan", "action-disable-platform-android": "Diblokir - Platform Android dinonaktifkan", "action-disable-platform-ios": "Diblokir - platform iOS dinonaktifkan", + "action-disable-prod-build": "Diblokir - build produksi dinonaktifkan", "action-download-10": "Progres unduhan 10%", "action-download-20": "Progres unduhan 20%", "action-download-30": "Progres unduhan 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Dasbor Admin", "admin-dashboard-construction": "Dasbor admin sedang dalam pembangunan. Komponen akan ditambahkan pada fase berikutnya.", "admin-dashboard-description": "Statistik dan analitik di seluruh platform", + "after": "Setelah", "afternoon": "sore", "alert-2fa-disable": "Konfirmasi bahwa Anda ingin menonaktifkan 2FA", "alert-2fa-required": "2FA diperlukan untuk mereset kata sandi", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Apakah Anda yakin ingin menghasilkan kunci ini lagi?", "alert-unknown-error": "Kesalahan tidak diketahui, lihat konsol pengembang", "all-apps": "Semua Aplikasi", + "all-operations": "Semua Operasi", "all-organizations": "Semua Organisasi", + "all-tables": "Semua Tabel", "allow-dev-build": "Izinkan pembangunan versi pengembangan", "allow-develoment-bui": "Izinkan pembangunan versi pengembangan", "allow-device-to-self": "Izinkan perangkat untuk memisahkan/mengasosiasikan diri sendiri", @@ -181,6 +184,24 @@ "at-least-one-number": "Setidaknya satu angka", "at-least-one-uppercase-letter": "Setidaknya satu huruf kapital", "at-least-two-special-characters": "Setidaknya satu karakter khusus", + "audit-app_versions-delete": "Paket Dihapus", + "audit-app_versions-insert": "Paket Dibuat", + "audit-app_versions-update": "Paket Diperbarui", + "audit-apps-delete": "Aplikasi Dihapus", + "audit-apps-insert": "Aplikasi Dibuat", + "audit-apps-update": "Aplikasi Diperbarui", + "audit-channels-delete": "Saluran Dihapus", + "audit-channels-insert": "Saluran Dibuat", + "audit-channels-update": "Saluran Diperbarui", + "audit-log-details": "Detail Log Audit", + "audit-logs": "Log Audit", + "audit-logs-description": "Lihat riwayat perubahan yang dilakukan pada organisasi Anda, termasuk modifikasi pada saluran, paket, dan anggota tim.", + "audit-org_users-delete": "Anggota Dihapus", + "audit-org_users-insert": "Anggota Ditambahkan", + "audit-org_users-update": "Anggota Diperbarui", + "audit-orgs-delete": "Organisasi Dihapus", + "audit-orgs-insert": "Organisasi Dibuat", + "audit-orgs-update": "Organisasi Diperbarui", "available-channels": "Saluran yang tersedia", "available-in-the-san": "Tersedia di aplikasi sandbox", "available-versions": "Paket yang tersedia", @@ -196,6 +217,7 @@ "bandwidth-usage": "Penggunaan bandwidth:", "bandwith-usage": "Penggunaan bandwidth:", "base": "Basis", + "before": "Sebelum", "best-plan": "Rencana terbaik", "bigger-app-size": "Ukuran aplikasi yang lebih besar", "billed-annually-at": "Ditagih setiap tahun pada", @@ -314,8 +336,10 @@ "changed-app-name": "Berhasil mengubah nama aplikasi", "changed-app-retention": "Berhasil mengubah retensi aplikasi", "changed-expose-metadata": "Berhasil mengubah pengaturan paparan metadata", + "changed-fields": "Bidang yang Diubah", "changed-name": "Berhasil mengubah nama apikey", "changed-password-suc": "Kata sandi berhasil diubah", + "changes": "Perubahan", "channel": "Saluran", "channel-ab-testing": "Aktifkan pengujian AB", "channel-ab-testing-percentage": "Persentase pengguna yang menerima versi sekunder", @@ -357,6 +381,7 @@ "clear-filters": "Hapus Filter", "cli-doc": "Dokumen CLI", "cli-version": "Versi CLI", + "close": "Tutup", "commands": "perintah", "comment": "Komentar", "complete-all-fields": "Harap lengkapi semua bidang", @@ -507,6 +532,7 @@ "daily-registrations": "Registrasi Harian", "daily-uploads": "Unggahan harian", "dashboard": "dasbor", + "date": "Tanggal", "date-range": "Rentang Tanggal", "debug-api-description": "Gunakan perintah curl ini untuk mereproduksi permintaan API yang tepat yang dibuat perangkat ini untuk memeriksa pembaruan", "debug-api-request": "Permintaan Debug API", @@ -548,6 +574,7 @@ "delete-org": "Hapus organisasi", "delete-your-account": "Hapus akun Anda", "deleted": "dihapus", + "deleted-record": "Rekaman Dihapus", "deletion-failed": "Penghapusan gagal", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Ini menunjukkan membaca nilai input dari komponen di luar dialog", @@ -579,6 +606,7 @@ "deployments-title": "Total", "deployments-trend": "Tren Penyebaran", "detailed-usage-plan": "Penggunaan secara detail", + "details": "Rincian", "device": "Perangkat", "device-id": "ID Perangkat", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Masukkan kata sandi baru Anda dan konfirmasi", "error": "Kesalahan", "error-checking-channels": "Kesalahan membaca saluran", + "error-fetching-audit-logs": "Kesalahan saat mengambil log audit", "error-fetching-builds": "Kesalahan dalam mengambil permintaan pembuatan", "error-fetching-deploy-history": "Kesalahan dalam mengambil riwayat penyebaran", "error-fetching-members": "Kesalahan dalam mengambil anggota", @@ -639,6 +668,8 @@ "fast-forward": "Maju Cepat", "feel-magic-of-capgo": "Rasakan sihir dari:", "filter-actions": "Tindakan", + "filter-by-operation": "Filter berdasarkan operasi", + "filter-by-table": "Filter berdasarkan tabel", "first-name": "Nama depan", "first-name-required": "Nama depan diperlukan", "force-version": "Versi paksa", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Kode 2FA tidak valid, coba lagi!", "min-update-version": "Versi pembaruan minimal", "minor": "Minor", + "minutes-short": "{minutes}m", "misconfigured": "Salah Konfigurasi", "misconfigured-channels": "Beberapa saluran dikonfigurasi dengan salah. Pembaruan akan gagal untuk saluran-saluran tersebut!", "missing-email": "Email hilang", "missing-name": "Nama hilang", "mo": "Mo", + "modified": "Dimodifikasi", "modify-org-info": "Anda dapat memodifikasi informasi organisasi di sini.", "module-heading": "Modul", "monthly-active": "Aktif bulanan", @@ -782,6 +815,7 @@ "new-name-not-changed": "Nama baru sama dengan nama lama", "new-name-to-long": "Nama baru terlalu panjang. Anda hanya dapat menggunakan 32 karakter.", "new-name-to-short": "Nama apikey terlalu pendek. Harus setidaknya 4 karakter panjangnya.", + "new-record": "Rekaman Baru", "new-users": "Pengguna Baru", "next": "Selanjutnya", "next-run": "Pembaruan selanjutnya", @@ -800,6 +834,7 @@ "no-device-data": "Tidak ada data perangkat tersedia", "no-error-message": "Tidak ada pesan kesalahan yang tersedia", "no-manifest-bundle": "Tidak ada manifest", + "no-organization-selected": "Tidak ada organisasi yang dipilih", "no-permission": "Izin tidak cukup", "no-permission-ask-super-admin": "izin tidak cukup, silakan minta super admin untuk menghapus bundel ini secara tidak aman", "no-public-channel": "Aplikasi ini tidak memiliki saluran publik, kami tidak dapat menghitung penyebaran tanpa itu", @@ -948,6 +983,7 @@ "reset-password": "Atur Ulang Kata Sandi", "reset-spoofed-user": "Berhenti melakukan spoofing", "reset-your-password": "Atur ulang kata sandi Anda", + "resource": "Sumber daya", "retention": "Hapus otomatis bundel yang tidak digunakan (setelah x detik)", "retention-cannot-be-negative": "Retensi tidak bisa menjadi angka negatif", "retention-to-big": "Retensi tidak bisa lebih besar dari 63113903 (2 tahun)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Cari berdasarkan nama atau AppID", "search-by-name-or-bundle-id": "Cari berdasarkan nama atau ID bundel", "search-by-name-or-email": "Cari berdasarkan nama atau email", + "search-by-record-id": "Cari berdasarkan ID rekaman", "search-by-version": "Cari berdasarkan versi", "search-channels": "Cari saluran", "search-versions": "Cari bundel", @@ -1114,6 +1151,7 @@ "version-link-fail": "Tidak dapat mengatur penimpaan bundel", "version-linked": "Tautan versi", "version-name-missing": "Nama versi hilang", + "view": "Lihat", "want-to-unlink": "Apakah Anda ingin membatalkan tautan?", "warning-organizations-will-be-deleted": "Peringatan: Organisasi akan dihapus", "warning-organizations-will-be-deleted-message": "Anda adalah satu -satunya admin super di organisasi berikut. \nOrganisasi -organisasi ini akan dihapus secara permanen saat akun Anda dihapus:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "Apa yang ingin Anda lakukan dengan aplikasi foto?", "write-key": "Tulis", "wrong-name-org-del": "Anda belum mengetik nama organisasi. Anda seharusnya mengetik: %1", - "minutes-short": "{minutes}m", "x-hours-short": "{hours}h", "yearly": "Tahunan", "yes": "ya", diff --git a/messages/it.json b/messages/it.json index d78de4c4dc..86f31224f7 100644 --- a/messages/it.json +++ b/messages/it.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Bloccato - aggiornamenti della versione patch disabilitati", "action-disable-auto-update-under-native": "Bloccato - non può essere declassato al di sotto del nativo", "action-disable-dev-build": "Bloccato - compilazioni dev disabilitate", - "action-disable-emulator": "Bloccato - emulatore disabilitato", - "action-disable-prod-build": "Bloccato - build di produzione disabilitate", "action-disable-device": "Bloccato - aggiornamenti dei dispositivi disabilitati", + "action-disable-emulator": "Bloccato - emulatore disabilitato", "action-disable-platform-android": "Bloccato - Piattaforma Android disabilitata", "action-disable-platform-ios": "Bloccato - Piattaforma iOS disabilitata", + "action-disable-prod-build": "Bloccato - build di produzione disabilitate", "action-download-10": "Progresso del download 10%", "action-download-20": "Progresso del download 20%", "action-download-30": "Progresso del download 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Pannello di Controllo Admin", "admin-dashboard-construction": "Il pannello di controllo dell'admin è in costruzione. I componenti verranno aggiunti nella prossima fase.", "admin-dashboard-description": "Statistiche e analisi su tutta la piattaforma", + "after": "Dopo", "afternoon": "pomeriggio", "alert-2fa-disable": "Conferma che vuoi disabilitare il 2FA", "alert-2fa-required": "È richiesto il 2FA per reimpostare la password", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Sei sicuro di voler rigenerare questa chiave?", "alert-unknown-error": "Errore sconosciuto, vedi console dev", "all-apps": "Tutte le App", + "all-operations": "Tutte le operazioni", "all-organizations": "Tutte le Organizzazioni", + "all-tables": "Tutte le tabelle", "allow-dev-build": "Consenti la costruzione dello sviluppo", "allow-develoment-bui": "Consenti dispositivi di sviluppo", "allow-device-to-self": "Consenti ai dispositivi di dissociarsi/associarsi autonomamente", @@ -181,6 +184,24 @@ "at-least-one-number": "Almeno un numero", "at-least-one-uppercase-letter": "Almeno una lettera maiuscola", "at-least-two-special-characters": "Almeno un carattere speciale", + "audit-app_versions-delete": "Pacchetto Eliminato", + "audit-app_versions-insert": "Pacchetto Creato", + "audit-app_versions-update": "Pacchetto Aggiornato", + "audit-apps-delete": "App Eliminata", + "audit-apps-insert": "App Creata", + "audit-apps-update": "App Aggiornata", + "audit-channels-delete": "Canale Eliminato", + "audit-channels-insert": "Canale Creato", + "audit-channels-update": "Canale Aggiornato", + "audit-log-details": "Dettagli del registro di audit", + "audit-logs": "Registri di audit", + "audit-logs-description": "Visualizza una cronologia delle modifiche apportate alla tua organizzazione, incluse le modifiche a canali, bundle e membri del team.", + "audit-org_users-delete": "Membro Rimosso", + "audit-org_users-insert": "Membro Aggiunto", + "audit-org_users-update": "Membro Aggiornato", + "audit-orgs-delete": "Organizzazione Eliminata", + "audit-orgs-insert": "Organizzazione Creata", + "audit-orgs-update": "Organizzazione Aggiornata", "available-channels": "Canali disponibili", "available-in-the-san": "Disponibile nell'app sandbox", "available-versions": "Pacchetti disponibili", @@ -196,6 +217,7 @@ "bandwidth-usage": "Utilizzo della larghezza di banda:", "bandwith-usage": "Utilizzo della larghezza di banda:", "base": "Base", + "before": "Prima", "best-plan": "Miglior piano", "bigger-app-size": "Dimensioni dell'app più grandi", "billed-annually-at": "Fatturato annualmente a", @@ -314,8 +336,10 @@ "changed-app-name": "Nome dell'app modificato con successo", "changed-app-retention": "Cambiato con successo la ritenzione dell'app", "changed-expose-metadata": "Impostazione di esposizione dei metadati modificata con successo", + "changed-fields": "Campi modificati", "changed-name": "Nome dell'apikey cambiato con successo", "changed-password-suc": "La password è stata cambiata con successo", + "changes": "Modifiche", "channel": "Canale", "channel-ab-testing": "Abilita il test AB", "channel-ab-testing-percentage": "Percentuale di utenti che ricevono la versione secondaria", @@ -357,6 +381,7 @@ "clear-filters": "Cancella Filtri", "cli-doc": "Documentazione CLI", "cli-version": "Versione CLI", + "close": "Chiudi", "commands": "comandi", "comment": "Commento", "complete-all-fields": "Si prega di completare tutti i campi", @@ -507,6 +532,7 @@ "daily-registrations": "Registrazioni Giornaliere", "daily-uploads": "Caricamenti quotidiani", "dashboard": "Dashboard", + "date": "Data", "date-range": "Intervallo di Date", "debug-api-description": "Utilizza questo comando curl per riprodurre la precisa richiesta API che questo dispositivo fa per controllare gli aggiornamenti", "debug-api-request": "Richiesta di debug API", @@ -548,6 +574,7 @@ "delete-org": "Elimina organizzazione", "delete-your-account": "Elimina il tuo account", "deleted": "eliminato", + "deleted-record": "Record eliminato", "deletion-failed": "Eliminazione fallita", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Questo dimostra la lettura dei valori di input da componenti esterni al dialogo", @@ -579,6 +606,7 @@ "deployments-title": "Totale", "deployments-trend": "Tendenze dei Deployments", "detailed-usage-plan": "Uso dettagliato", + "details": "Dettagli", "device": "Dispositivo", "device-id": "ID del dispositivo", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Inserisci la tua nuova password e conferma", "error": "Errore", "error-checking-channels": "Errore nella lettura dei canali", + "error-fetching-audit-logs": "Errore nel recupero dei registri di audit", "error-fetching-builds": "Errore nel recupero delle richieste di costruzione", "error-fetching-deploy-history": "Errore nel recupero della cronologia dei deployment", "error-fetching-members": "Errore nel recupero dei membri", @@ -639,6 +668,8 @@ "fast-forward": "Avanti Veloce", "feel-magic-of-capgo": "Senti la magia di:", "filter-actions": "Azioni", + "filter-by-operation": "Filtra per operazione", + "filter-by-table": "Filtra per tabella", "first-name": "Nome di battesimo", "first-name-required": "Nome richiesto", "force-version": "Versione forzata", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Codice 2FA non valido, riprova!", "min-update-version": "Versione di aggiornamento minima", "minor": "Minore", + "minutes-short": "{minutes}m", "misconfigured": "Configurato in modo errato", "misconfigured-channels": "Alcuni canali sono configurati in modo errato. Gli aggiornamenti falliranno per quei canali!", "missing-email": "Email mancante", "missing-name": "Nome mancante", "mo": "Mo", + "modified": "Modificato", "modify-org-info": "Puoi modificare le informazioni dell'organizzazione qui.", "module-heading": "Moduli", "monthly-active": "Attivo mensilmente", @@ -782,6 +815,7 @@ "new-name-not-changed": "Il nuovo nome è lo stesso del vecchio.", "new-name-to-long": "Il nuovo nome è troppo lungo. Puoi utilizzare solo 32 caratteri.", "new-name-to-short": "Il nome dell'apikey è troppo corto. Deve essere lungo almeno 4 caratteri.", + "new-record": "Nuovo record", "new-users": "Nuovi Utenti", "next": "Successivo", "next-run": "Prossimo aggiornamento", @@ -800,6 +834,7 @@ "no-device-data": "Nessun dato del dispositivo disponibile", "no-error-message": "Nessun messaggio di errore disponibile", "no-manifest-bundle": "Nessun manifesto", + "no-organization-selected": "Nessuna organizzazione selezionata", "no-permission": "Permessi insufficienti", "no-permission-ask-super-admin": "permesso insufficiente, si prega di chiedere a un super amministratore di eliminare questo pacchetto in modo non sicuro", "no-public-channel": "L'app non ha un canale pubblico, non possiamo contare il dispiegamento senza di esso.", @@ -948,6 +983,7 @@ "reset-password": "Reimposta Password", "reset-spoofed-user": "Smetti di falsificare", "reset-your-password": "Reimposta la tua password", + "resource": "Risorsa", "retention": "Elimina automaticamente i pacchetti non utilizzati (dopo x secondi)", "retention-cannot-be-negative": "La conservazione non può essere un numero negativo", "retention-to-big": "La conservazione non può essere maggiore di 63113903 (2 anni)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Cerca per nome o AppID", "search-by-name-or-bundle-id": "Cerca per nome o ID del pacchetto", "search-by-name-or-email": "Cerca per nome o email", + "search-by-record-id": "Cerca per ID record", "search-by-version": "Cerca per versione", "search-channels": "Cerca canale", "search-versions": "Cerca pacchetto", @@ -1114,6 +1151,7 @@ "version-link-fail": "Non può impostare la sovrascrittura del pacchetto", "version-linked": "Link della versione", "version-name-missing": "Il nome della versione manca", + "view": "Visualizza", "want-to-unlink": "Vuoi scollegare?", "warning-organizations-will-be-deleted": "ATTENZIONE: le organizzazioni verranno eliminate", "warning-organizations-will-be-deleted-message": "Sei l'unico super admin nelle seguenti organizzazioni. \nQueste organizzazioni verranno eliminate permanentemente quando il tuo account verrà rimosso:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "Cosa vorresti fare con l'app foto?", "write-key": "Scrivi", "wrong-name-org-del": "Non hai digitato il nome dell'organizzazione. Dovevi digitare: %1", - "minutes-short": "{minutes}m", "x-hours-short": "{hours}h", "yearly": "Annuale", "yes": "sì", diff --git a/messages/ja.json b/messages/ja.json index 813ac317c0..a78225bba8 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "ブロック済み - パッチバージョンの更新が無効化されました", "action-disable-auto-update-under-native": "ブロックされました - ネイティブ以下にダウングレードできません", "action-disable-dev-build": "ブロックされました - 開発ビルドは無効化されています", - "action-disable-emulator": "ブロックされました - エミュレーターが無効化されました", - "action-disable-prod-build": "ブロック - 本番ビルド無効", "action-disable-device": "ブロック - デバイス更新無効", + "action-disable-emulator": "ブロックされました - エミュレーターが無効化されました", "action-disable-platform-android": "ブロックされました - Androidプラットフォームが無効化されました", "action-disable-platform-ios": "ブロックされました - iOSプラットフォームが無効化されました", + "action-disable-prod-build": "ブロック - 本番ビルド無効", "action-download-10": "ダウンロード進行状況 10%", "action-download-20": "ダウンロード進行状況 20%", "action-download-30": "ダウンロード進行状況30%", @@ -119,6 +119,7 @@ "admin-dashboard": "管理者ダッシュボード", "admin-dashboard-construction": "管理者ダッシュボードは現在建設中です。コンポーネントは次のフェーズで追加されます。", "admin-dashboard-description": "プラットフォーム全体の統計と分析", + "after": "変更後", "afternoon": "午後", "alert-2fa-disable": "2FAを無効にすることを確認してください", "alert-2fa-required": "パスワードをリセットするには2FAが必要です", @@ -147,7 +148,9 @@ "alert-regenerate-key": "このキーを再生成してもよろしいですか?", "alert-unknown-error": "不明なエラー、開発者コンソールを参照してください", "all-apps": "すべてのアプリ", + "all-operations": "すべての操作", "all-organizations": "すべての組电", + "all-tables": "すべてのテーブル", "allow-dev-build": "開発ビルドを許可する", "allow-develoment-bui": "開発ビルドを許可する", "allow-device-to-self": "デバイスに自己分離/関連付けを許可する", @@ -181,6 +184,24 @@ "at-least-one-number": "少なくとも1つの数字", "at-least-one-uppercase-letter": "少なくとも1つの大文字", "at-least-two-special-characters": "少なくとも1つの特殊文字", + "audit-app_versions-delete": "バンドルが削除されました", + "audit-app_versions-insert": "バンドルが作成されました", + "audit-app_versions-update": "バンドルが更新されました", + "audit-apps-delete": "アプリが削除されました", + "audit-apps-insert": "アプリ作成済み", + "audit-apps-update": "アプリが更新されました", + "audit-channels-delete": "チャンネルが削除されました", + "audit-channels-insert": "チャンネルが作成されました", + "audit-channels-update": "チャンネルが更新されました", + "audit-log-details": "監査ログの詳細", + "audit-logs": "監査ログ", + "audit-logs-description": "チャンネル、バンドル、チームメンバーへの変更を含む、組織に加えられた変更の履歴を表示します。", + "audit-org_users-delete": "メンバーが削除されました", + "audit-org_users-insert": "メンバーが追加されました", + "audit-org_users-update": "メンバー更新", + "audit-orgs-delete": "組織削除", + "audit-orgs-insert": "組織設立", + "audit-orgs-update": "組織が更新されました", "available-channels": "利用可能なチャンネル", "available-in-the-san": "サンドボックスアプリで利用可能", "available-versions": "利用可能なバンドル", @@ -196,6 +217,7 @@ "bandwidth-usage": "帯域幅の使用量:", "bandwith-usage": "帯域幅の使用量:", "base": "ベース", + "before": "変更前", "best-plan": "最善の計画", "bigger-app-size": "より大きなアプリサイズ", "billed-annually-at": "年間で請求されます", @@ -314,8 +336,10 @@ "changed-app-name": "アプリ名の変更に成功しました", "changed-app-retention": "アプリの保持期間を正常に変更しました", "changed-expose-metadata": "メタデータ公開設定を正常に変更しました", + "changed-fields": "変更されたフィールド", "changed-name": "APIキーの名前を正常に変更しました", "changed-password-suc": "パスワードの変更に成功しました", + "changes": "変更内容", "channel": "チャンネル", "channel-ab-testing": "ABテストを有効にする", "channel-ab-testing-percentage": "二次バージョンを受け取るユーザーの割合", @@ -357,6 +381,7 @@ "clear-filters": "フィルターをクリアする", "cli-doc": "CLIドキュメント", "cli-version": "CLIバージョン", + "close": "閉じる", "commands": "コマンド", "comment": "コメント", "complete-all-fields": "すべてのフィールドを完了してください", @@ -507,6 +532,7 @@ "daily-registrations": "毎日の登録", "daily-uploads": "毎日のアップロード", "dashboard": "ダッシュボード", + "date": "日付", "date-range": "日付範囲", "debug-api-description": "このcurlコマンドを使用して、このデバイスがアップデートを確認するために行う正確なAPIリクエストを再現します。", "debug-api-request": "デバッグAPIリクエスト", @@ -548,6 +574,7 @@ "delete-org": "組織を削除する", "delete-your-account": "あなたのアカウントを削除してください", "deleted": "削除された", + "deleted-record": "削除されたレコード", "deletion-failed": "削除に失敗しました", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "これは、ダイアログの外部コンポーネントから入力値を読み取ることを示しています", @@ -579,6 +606,7 @@ "deployments-title": "合計", "deployments-trend": "デプロイメントのトレンド", "detailed-usage-plan": "詳細な使用法", + "details": "詳細", "device": "デバイス", "device-id": "デバイスID", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "新しいパスワードを入力して確認してください", "error": "エラー", "error-checking-channels": "チャンネルの読み取りエラー", + "error-fetching-audit-logs": "監査ログの取得エラー", "error-fetching-builds": "ビルドリクエストの取得エラー", "error-fetching-deploy-history": "デプロイメント履歴の取得エラー", "error-fetching-members": "メンバーの取得エラー", @@ -639,6 +668,8 @@ "fast-forward": "早送り", "feel-magic-of-capgo": "の魔法を感じてみてください:", "filter-actions": "アクション", + "filter-by-operation": "操作でフィルター", + "filter-by-table": "テーブルでフィルター", "first-name": "名前", "first-name-required": "名前が必要です", "force-version": "強制バージョン", @@ -758,11 +789,13 @@ "mfa-invalid-code": "無効な2FAコード、もう一度試してください!", "min-update-version": "最小限のアップデートバージョン", "minor": "マイナー", + "minutes-short": "{minutes}分", "misconfigured": "設定ミス", "misconfigured-channels": "一部のチャンネルが誤って設定されています。それらのチャンネルでは更新が失敗します!", "missing-email": "メールがありません", "missing-name": "名前がありません", "mo": "モ", + "modified": "修正された", "modify-org-info": "ここで組織の情報を変更することができます。", "module-heading": "モジュール", "monthly-active": "月間アクティブ", @@ -782,6 +815,7 @@ "new-name-not-changed": "新しい名前は古い名前と同じです。", "new-name-to-long": "新しい名前が長すぎます。32文字までしか使用できません。", "new-name-to-short": "APIキーの名前が短すぎます。少なくとも4文字以上である必要があります。", + "new-record": "新規レコード", "new-users": "新規ユーザー", "next": "次の", "next-run": "次のアップデート", @@ -800,6 +834,7 @@ "no-device-data": "デバイスのデータが利用できません", "no-error-message": "エラーメッセージは利用できません", "no-manifest-bundle": "マニフェストなし", + "no-organization-selected": "組織が選択されていません", "no-permission": "権限が不足しています", "no-permission-ask-super-admin": "権限が不足しています、このバンドルを安全でない方法で削除するにはスーパー管理者に依頼してください。", "no-public-channel": "アプリには公開チャンネルがなく、それなしではデプロイメントを数えることはできません", @@ -948,6 +983,7 @@ "reset-password": "パスワードをリセット", "reset-spoofed-user": "スプーフィングをやめてください", "reset-your-password": "パスワードをリセットしてください", + "resource": "リソース", "retention": "使用されていないバンドルを自動的に削除する(x秒後)", "retention-cannot-be-negative": "保持は負の数になることはできません", "retention-to-big": "保持期間は63113903(2年)より大きくすることはできません", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "名前またはAppIDで検索してください", "search-by-name-or-bundle-id": "名前またはバンドルIDで検索します", "search-by-name-or-email": "名前またはメールで検索", + "search-by-record-id": "レコードIDで検索", "search-by-version": "バージョンで検索", "search-channels": "チャンネルを検索", "search-versions": "バンドルを検索", @@ -1114,6 +1151,7 @@ "version-link-fail": "バンドルオーバーライドを設定できません", "version-linked": "バージョンリンク", "version-name-missing": "バージョン名がありません", + "view": "表示", "want-to-unlink": "リンクを解除しますか?", "warning-organizations-will-be-deleted": "警告:組織は削除されます", "warning-organizations-will-be-deleted-message": "あなたは次の組織で唯一のスーパー管理者です。\nこれらの組織は、アカウントが削除されると永久に削除されます。", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "アプリの写真で何をしたいですか?", "write-key": "書く", "wrong-name-org-del": "あなたは組織名を入力していません。あなたが入力するべきだったのは:%1です。", - "minutes-short": "{minutes}分", "x-hours-short": "{hours}h", "yearly": "毎年の", "yes": "はい", diff --git a/messages/ko.json b/messages/ko.json index b910d8ae95..b90a7bc34c 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "차단됨 - 패치 버전 업데이트 비활성화", "action-disable-auto-update-under-native": "차단됨 - 기본 버전 아래로 다운그레이드할 수 없습니다.", "action-disable-dev-build": "차단됨 - 개발 빌드 비활성화", - "action-disable-emulator": "차단됨 - 에뮬레이터 비활성화", - "action-disable-prod-build": "차단됨 - 프로덕션 빌드 비활성화됨", "action-disable-device": "차단됨 - 기기 업데이트 비활성화됨", + "action-disable-emulator": "차단됨 - 에뮬레이터 비활성화", "action-disable-platform-android": "차단됨 - 안드로이드 플랫폼 비활성화", "action-disable-platform-ios": "차단됨 - iOS 플랫폼 비활성화", + "action-disable-prod-build": "차단됨 - 프로덕션 빌드 비활성화됨", "action-download-10": "다운로드 진행률 10%", "action-download-20": "다운로드 진행률 20%", "action-download-30": "다운로드 진행률 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "관리자 대시보드", "admin-dashboard-construction": "관리자 대시보드가 구축 중입니다. 컴포넌트는 다음 단계에서 추가될 예정입니다.", "admin-dashboard-description": "플랫폼 전체 통계 및 분석", + "after": "변경 후", "afternoon": "오후", "alert-2fa-disable": "2FA를 비활성화하려는 것을 확인하십시오.", "alert-2fa-required": "비밀번호를 재설정하려면 2FA가 필요합니다.", @@ -147,7 +148,9 @@ "alert-regenerate-key": "이 키를 재생성하길 원하는 것이 확실한가요?", "alert-unknown-error": "알 수 없는 오류, 개발자 콘솔을 확인하세요", "all-apps": "모든 앱들", + "all-operations": "모든 작업", "all-organizations": "모든 조직", + "all-tables": "모든 테이블", "allow-dev-build": "개발 빌드 허용", "allow-develoment-bui": "개발 장치 허용", "allow-device-to-self": "장치가 자체적으로 분리/연결을 허용하십시오.", @@ -181,6 +184,24 @@ "at-least-one-number": "적어도 하나의 숫자", "at-least-one-uppercase-letter": "적어도 하나의 대문자", "at-least-two-special-characters": "적어도 하나의 특수 문자", + "audit-app_versions-delete": "번들 삭제됨", + "audit-app_versions-insert": "번들 생성됨", + "audit-app_versions-update": "번들 업데이트됨", + "audit-apps-delete": "앱 삭제됨", + "audit-apps-insert": "앱 생성됨", + "audit-apps-update": "앱 업데이트됨", + "audit-channels-delete": "채널이 삭제되었습니다", + "audit-channels-insert": "채널 생성됨", + "audit-channels-update": "채널 업데이트됨", + "audit-log-details": "감사 로그 상세", + "audit-logs": "감사 로그", + "audit-logs-description": "채널, 번들 및 팀 구성원에 대한 수정 사항을 포함하여 조직에 대한 변경 기록을 확인하세요.", + "audit-org_users-delete": "회원 제거됨", + "audit-org_users-insert": "회원 추가됨", + "audit-org_users-update": "회원 정보 업데이트됨", + "audit-orgs-delete": "조직 삭제됨", + "audit-orgs-insert": "조직 생성됨", + "audit-orgs-update": "조직 업데이트됨", "available-channels": "사용 가능한 채널들", "available-in-the-san": "샌드박스 앱에서 사용 가능합니다.", "available-versions": "사용 가능한 번들", @@ -196,6 +217,7 @@ "bandwidth-usage": "대역폭 사용량:", "bandwith-usage": "대역폭 사용량:", "base": "기지", + "before": "변경 전", "best-plan": "최고의 계획", "bigger-app-size": "더 큰 앱 크기", "billed-annually-at": "연간 청구 기준으로", @@ -314,8 +336,10 @@ "changed-app-name": "앱 이름 변경에 성공했습니다.", "changed-app-retention": "앱의 보존 기간을 성공적으로 변경했습니다.", "changed-expose-metadata": "메타데이터 노출 설정이 성공적으로 변경되었습니다", + "changed-fields": "변경된 필드", "changed-name": "API 키의 이름을 성공적으로 변경했습니다.", "changed-password-suc": "비밀번호 변경이 성공적으로 완료되었습니다.", + "changes": "변경 사항", "channel": "채널", "channel-ab-testing": "AB 테스팅 활성화", "channel-ab-testing-percentage": "보조 버전을 받는 사용자의 비율", @@ -357,6 +381,7 @@ "clear-filters": "필터 지우기", "cli-doc": "CLI 문서", "cli-version": "CLI 버전", + "close": "닫기", "commands": "명령어들", "comment": "댓글", "complete-all-fields": "모든 필드를 완성해 주세요.", @@ -507,6 +532,7 @@ "daily-registrations": "매일 등록", "daily-uploads": "매일 업로드", "dashboard": "대시보드", + "date": "날짜", "date-range": "날짜 범위", "debug-api-description": "이 curl 명령을 사용하여 이 장치가 업데이트를 확인하기 위해 만드는 정확한 API 요청을 복제하십시오.", "debug-api-request": "API 요청 디버그", @@ -548,6 +574,7 @@ "delete-org": "조직 삭제", "delete-your-account": "당신의 계정을 삭제하십시오", "deleted": "삭제된", + "deleted-record": "삭제된 레코드", "deletion-failed": "삭제 실패", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "이것은 대화 상자 외부의 구성 요소에서 입력 값을 읽는 것을 보여줍니다.", @@ -579,6 +606,7 @@ "deployments-title": "총계", "deployments-trend": "배포 추세", "detailed-usage-plan": "상세 사용법", + "details": "세부 사항", "device": "장치", "device-id": "장치 ID", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "새 비밀번호를 입력하고 확인하세요", "error": "오류", "error-checking-channels": "채널 읽기 오류", + "error-fetching-audit-logs": "감사 로그를 가져오는 중 오류 발생", "error-fetching-builds": "빌드 요청 가져오기 오류", "error-fetching-deploy-history": "배포 이력 가져오기 오류", "error-fetching-members": "멤버 가져오기 오류", @@ -639,6 +668,8 @@ "fast-forward": "빨리 감기", "feel-magic-of-capgo": "다음의 마법을 느껴보세요:", "filter-actions": "행동들", + "filter-by-operation": "작업별 필터", + "filter-by-table": "테이블별 필터", "first-name": "이름", "first-name-required": "이름이 필요합니다", "force-version": "강제 버전", @@ -758,11 +789,13 @@ "mfa-invalid-code": "잘못된 2FA 코드, 다시 시도하십시오!", "min-update-version": "최소 업데이트 버전", "minor": "소수의", + "minutes-short": "{minutes}분", "misconfigured": "잘못 설정된", "misconfigured-channels": "일부 채널이 잘못 구성되었습니다. 해당 채널에 대한 업데이트가 실패할 것입니다!", "missing-email": "이메일 누락", "missing-name": "이름이 누락되었습니다", "mo": "모", + "modified": "수정된", "modify-org-info": "여기에서 조직의 정보를 수정할 수 있습니다.", "module-heading": "모듈", "monthly-active": "월간 활성화", @@ -782,6 +815,7 @@ "new-name-not-changed": "새 이름은 이전 이름과 같습니다.", "new-name-to-long": "새 이름이 너무 깁니다. 32자만 사용할 수 있습니다.", "new-name-to-short": "API 키 이름이 너무 짧습니다. 최소 4자 이상이어야 합니다.", + "new-record": "새 레코드", "new-users": "새로운 사용자들", "next": "다음", "next-run": "다음 업데이트", @@ -800,6 +834,7 @@ "no-device-data": "사용 가능한 장치 데이터가 없습니다.", "no-error-message": "사용 가능한 오류 메시지가 없습니다", "no-manifest-bundle": "매니페스트 없음", + "no-organization-selected": "조직이 선택되지 않았습니다", "no-permission": "권한이 부족합니다", "no-permission-ask-super-admin": "권한이 부족하므로, 이 번들을 안전하지 않게 삭제하도록 슈퍼 관리자에게 요청하십시오.", "no-public-channel": "이 앱은 공개 채널이 없으므로, 이를 통하지 않고 배포를 계산할 수 없습니다.", @@ -948,6 +983,7 @@ "reset-password": "비밀번호 재설정", "reset-spoofed-user": "스푸핑을 중단하십시오", "reset-your-password": "비밀번호를 재설정하세요", + "resource": "자원", "retention": "사용하지 않는 번들 자동 삭제 (x 초 후)", "retention-cannot-be-negative": "보존은 음수가 될 수 없습니다", "retention-to-big": "보존은 63113903 (2년)보다 클 수 없습니다", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "이름 또는 앱ID로 검색하세요", "search-by-name-or-bundle-id": "이름 또는 번들 ID로 검색하세요.", "search-by-name-or-email": "이름 또는 이메일로 검색", + "search-by-record-id": "레코드 ID로 검색", "search-by-version": "버전으로 검색", "search-channels": "채널 검색", "search-versions": "번들 검색", @@ -1114,6 +1151,7 @@ "version-link-fail": "번들 오버라이드를 설정할 수 없습니다.", "version-linked": "버전 링크", "version-name-missing": "버전 이름이 누락되었습니다", + "view": "보기", "want-to-unlink": "연결을 해제하시겠습니까?", "warning-organizations-will-be-deleted": "경고 : 조직이 삭제됩니다", "warning-organizations-will-be-deleted-message": "귀하는 다음 조직에서 유일한 슈퍼 관리자입니다. \n계정이 제거되면 이러한 조직은 영구적으로 삭제됩니다.", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "앱 사진으로 무엇을 하고 싶으신가요?", "write-key": "쓰다", "wrong-name-org-del": "당신은 조직 이름을 입력하지 않았습니다. 당신이 입력해야 할 것은: %1입니다.", - "minutes-short": "{minutes}분", "x-hours-short": "{hours}h", "yearly": "매년", "yes": "네", diff --git a/messages/pl.json b/messages/pl.json index 9fad923f18..c4ea8a2992 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Zablokowane - aktualizacje wersji poprawki wyłączone", "action-disable-auto-update-under-native": "Zablokowane - nie można zdegradować poniżej poziomu natywnego", "action-disable-dev-build": "Zablokowane - wyłączone kompilacje deweloperskie", - "action-disable-emulator": "Zablokowane - emulator wyłączony", - "action-disable-prod-build": "Zablokowano - wersje produkcyjne wyłączone", "action-disable-device": "Zablokowano - aktualizacje urządzeń wyłączone", + "action-disable-emulator": "Zablokowane - emulator wyłączony", "action-disable-platform-android": "Zablokowane - platforma Android wyłączona", "action-disable-platform-ios": "Zablokowane - platforma iOS wyłączona", + "action-disable-prod-build": "Zablokowano - wersje produkcyjne wyłączone", "action-download-10": "Postęp pobierania 10%", "action-download-20": "Postęp pobierania 20%", "action-download-30": "Postęp pobierania 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Panel administracyjny", "admin-dashboard-construction": "Panel administracyjny jest w trakcie budowy. Komponenty zostaną dodane w następnej fazie.", "admin-dashboard-description": "Statystyki i analityka na poziomie platformy", + "after": "Po", "afternoon": "popołudnie", "alert-2fa-disable": "Potwierdź, że chcesz wyłączyć 2FA", "alert-2fa-required": "Wymagane jest uwierzytelnianie dwuskładnikowe do zresetowania hasła.", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Czy na pewno chcesz wygenerować ten klucz ponownie?", "alert-unknown-error": "Nieznany błąd, zobacz konsolę deweloperską", "all-apps": "Wszystkie aplikacje", + "all-operations": "Wszystkie operacje", "all-organizations": "Wszystkie Organizacje", + "all-tables": "Wszystkie tabele", "allow-dev-build": "Zezwól na budowę wersji rozwojowej", "allow-develoment-bui": "Zezwól na urządzenia deweloperskie", "allow-device-to-self": "Pozwól urządzeniom na samodzielne rozłączanie/łączenie się", @@ -181,6 +184,24 @@ "at-least-one-number": "Przynajmniej jedna liczba", "at-least-one-uppercase-letter": "Przynajmniej jedna duża litera", "at-least-two-special-characters": "Przynajmniej jeden znak specjalny", + "audit-app_versions-delete": "Pakiet Usunięty", + "audit-app_versions-insert": "Pakiet Utworzony", + "audit-app_versions-update": "Paczka Zaktualizowana", + "audit-apps-delete": "Aplikacja Usunięta", + "audit-apps-insert": "Aplikacja Stworzona", + "audit-apps-update": "Aplikacja zaktualizowana", + "audit-channels-delete": "Kanał Usunięty", + "audit-channels-insert": "Kanał Utworzony", + "audit-channels-update": "Kanał zaktualizowany", + "audit-log-details": "Szczegóły dziennika audytu", + "audit-logs": "Dzienniki audytu", + "audit-logs-description": "Zobacz historię zmian wprowadzonych w Twojej organizacji, w tym modyfikacje kanałów, pakietów i członków zespołu.", + "audit-org_users-delete": "Członek Usunięty", + "audit-org_users-insert": "Członek Dodany", + "audit-org_users-update": "Członek Zaktualizowany", + "audit-orgs-delete": "Organizacja Usunięta", + "audit-orgs-insert": "Organizacja Utworzona", + "audit-orgs-update": "Organizacja Zaktualizowana", "available-channels": "Dostępne kanały", "available-in-the-san": "Dostępne w aplikacji piaskownicy", "available-versions": "Dostępne pakiety", @@ -196,6 +217,7 @@ "bandwidth-usage": "Wykorzystanie przepustowości:", "bandwith-usage": "Użycie przepustowości:", "base": "Baza", + "before": "Przed", "best-plan": "Najlepszy plan", "bigger-app-size": "Większy rozmiar aplikacji", "billed-annually-at": "Opłata roczna wynosi", @@ -314,8 +336,10 @@ "changed-app-name": "Pomyślnie zmieniono nazwę aplikacji", "changed-app-retention": "Pomyślnie zmieniono retencję aplikacji", "changed-expose-metadata": "Pomyślnie zmieniono ustawienie udostępniania metadanych", + "changed-fields": "Zmienione pola", "changed-name": "Pomyślnie zmieniono nazwę apikey", "changed-password-suc": "Hasło zostało pomyślnie zmienione", + "changes": "Zmiany", "channel": "Kanał", "channel-ab-testing": "Włącz testowanie AB", "channel-ab-testing-percentage": "Procent użytkowników otrzymujących drugą wersję", @@ -357,6 +381,7 @@ "clear-filters": "Wyczyść filtry", "cli-doc": "Dokumentacja CLI", "cli-version": "Wersja CLI", + "close": "Zamknij", "commands": "polecenia", "comment": "Komentarz", "complete-all-fields": "Proszę wypełnić wszystkie pola", @@ -507,6 +532,7 @@ "daily-registrations": "Codzienne Rejestracje", "daily-uploads": "Codzienne przesyłanie", "dashboard": "deska rozdzielcza", + "date": "Data", "date-range": "Zakres dat", "debug-api-description": "Użyj tego polecenia curl, aby odtworzyć dokładne żądanie API, które to urządzenie wysyła w celu sprawdzenia aktualizacji", "debug-api-request": "Debugowanie żądania API", @@ -548,6 +574,7 @@ "delete-org": "Usuń organizację", "delete-your-account": "Usuń swoje konto", "deleted": "usunięty", + "deleted-record": "Usunięty rekord", "deletion-failed": "Usunięcie nie powiodło się", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "To pokazuje odczytywanie wartości wejściowych z komponentów znajdujących się poza oknem dialogowym.", @@ -579,6 +606,7 @@ "deployments-title": "Całkowity", "deployments-trend": "Trendy wdrażania", "detailed-usage-plan": "Szczegółowe użycie", + "details": "Szczegóły", "device": "Urządzenie", "device-id": "Identyfikator urządzenia", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Wprowadź swoje nowe hasło i potwierdź", "error": "Błąd", "error-checking-channels": "Błąd odczytu kanałów", + "error-fetching-audit-logs": "Błąd podczas pobierania dzienników audytu", "error-fetching-builds": "Błąd podczas pobierania żądań budowy", "error-fetching-deploy-history": "Błąd podczas pobierania historii wdrożeń", "error-fetching-members": "Błąd podczas pobierania członków", @@ -639,6 +668,8 @@ "fast-forward": "Szybko Naprzód", "feel-magic-of-capgo": "Poczuj magię:", "filter-actions": "Akcje", + "filter-by-operation": "Filtruj według operacji", + "filter-by-table": "Filtruj według tabeli", "first-name": "Imię", "first-name-required": "Wymagane imię", "force-version": "Wersja siłowa", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Nieprawidłowy kod 2FA, spróbuj ponownie!", "min-update-version": "Minimalna wersja aktualizacji", "minor": "Mniejszy", + "minutes-short": "{minutes}m", "misconfigured": "Nieprawidłowo skonfigurowany", "misconfigured-channels": "Niektóre kanały są źle skonfigurowane. Aktualizacje dla tych kanałów nie powiodą się!", "missing-email": "Brakujący email", "missing-name": "Brakujące imię", "mo": "Moje", + "modified": "Zmodyfikowany", "modify-org-info": "Możesz tutaj modyfikować informacje o organizacji.", "module-heading": "Moduły", "monthly-active": "Miesięcznie aktywny", @@ -782,6 +815,7 @@ "new-name-not-changed": "Nowa nazwa jest taka sama jak stara.", "new-name-to-long": "Nowa nazwa jest zbyt długa. Możesz użyć tylko 32 znaków.", "new-name-to-short": "Nazwa apikey jest zbyt krótka. Musi mieć co najmniej 4 znaki.", + "new-record": "Nowy rekord", "new-users": "Nowi Użytkownicy", "next": "Następny", "next-run": "Następna aktualizacja", @@ -800,6 +834,7 @@ "no-device-data": "Brak dostępnych danych urządzenia", "no-error-message": "Brak dostępnego komunikatu o błędzie", "no-manifest-bundle": "Brak manifestu", + "no-organization-selected": "Nie wybrano organizacji", "no-permission": "Niewystarczające uprawnienia", "no-permission-ask-super-admin": "niewystarczające uprawnienia, poproś super administratora o niebezpieczne usunięcie tego pakietu", "no-public-channel": "Aplikacja nie ma publicznego kanału, nie możemy liczyć wdrożeń bez niego.", @@ -948,6 +983,7 @@ "reset-password": "Zresetuj Hasło", "reset-spoofed-user": "Przestań podszywać się", "reset-your-password": "Zresetuj swoje hasło", + "resource": "Zasób", "retention": "Automatyczne usuwanie pakietów nie używanych (po x sekundach)", "retention-cannot-be-negative": "Zatrzymanie nie może być liczbą ujemną", "retention-to-big": "Zatrzymanie nie może być większe niż 63113903 (2 lata)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Wyszukaj według nazwy lub AppID", "search-by-name-or-bundle-id": "Wyszukaj według nazwy lub ID pakietu", "search-by-name-or-email": "Wyszukaj według nazwy lub e-maila", + "search-by-record-id": "Szukaj według ID rekordu", "search-by-version": "Szukaj według wersji", "search-channels": "Szukaj kanału", "search-versions": "Szukaj pakietu", @@ -1114,6 +1151,7 @@ "version-link-fail": "Nie można ustawić zastępstwa pakietu", "version-linked": "Link do wersji", "version-name-missing": "Brakuje nazwy wersji", + "view": "Zobacz", "want-to-unlink": "Czy chcesz odłączyć?", "warning-organizations-will-be-deleted": "OSTRZEŻENIE: Organizacje zostaną usunięte", "warning-organizations-will-be-deleted-message": "Jesteś jedynym super administratorem w następujących organizacjach. \nOrganizacje te zostaną na stałe usunięte po usunięciu konta:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "Co chciałbyś zrobić z aplikacją do zdjęć?", "write-key": "Napisz", "wrong-name-org-del": "Nie wpisałeś nazwy organizacji. Powinieneś był wpisać: %1", - "minutes-short": "{minutes}m", "x-hours-short": "{hours}h", "yearly": "Rocznie", "yes": "tak", diff --git a/messages/pt-br.json b/messages/pt-br.json index 0f7c3091be..a96890c622 100644 --- a/messages/pt-br.json +++ b/messages/pt-br.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Bloqueado - atualizações da versão do patch desativadas", "action-disable-auto-update-under-native": "Bloqueado - não pode ser rebaixado abaixo do nativo", "action-disable-dev-build": "Bloqueado - compilações de desenvolvimento desativadas", - "action-disable-emulator": "Bloqueado - emulador desativado", - "action-disable-prod-build": "Bloqueado - builds de produção desativados", "action-disable-device": "Bloqueado - atualizações de dispositivos desativadas", + "action-disable-emulator": "Bloqueado - emulador desativado", "action-disable-platform-android": "Bloqueado - Plataforma Android desativada", "action-disable-platform-ios": "Bloqueado - plataforma iOS desativada", + "action-disable-prod-build": "Bloqueado - builds de produção desativados", "action-download-10": "Progresso do download 10%", "action-download-20": "Progresso do download 20%", "action-download-30": "Progresso do download 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Painel de Controle do Administrador", "admin-dashboard-construction": "O painel de administração está em construção. Componentes serão adicionados na próxima fase.", "admin-dashboard-description": "Estatísticas e análises em toda a plataforma", + "after": "Depois", "afternoon": "tarde", "alert-2fa-disable": "Confirme que você deseja desativar o 2FA", "alert-2fa-required": "É necessário o 2FA para redefinir a senha", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Você tem certeza de que deseja regenerar essa chave?", "alert-unknown-error": "Erro desconhecido, veja o console do desenvolvedor", "all-apps": "Todos os Aplicativos", + "all-operations": "Todas as operações", "all-organizations": "Todas as Organizações", + "all-tables": "Todas as tabelas", "allow-dev-build": "Permitir construção de desenvolvimento", "allow-develoment-bui": "Permitir dispositivos de desenvolvimento", "allow-device-to-self": "Permita que os dispositivos se dissociem/associem automaticamente", @@ -181,6 +184,24 @@ "at-least-one-number": "Pelo menos um número", "at-least-one-uppercase-letter": "Pelo menos uma letra maiúscula", "at-least-two-special-characters": "Pelo menos um caractere especial", + "audit-app_versions-delete": "Pacote Excluído", + "audit-app_versions-insert": "Pacote Criado", + "audit-app_versions-update": "Pacote Atualizado", + "audit-apps-delete": "Aplicativo Excluído", + "audit-apps-insert": "Aplicativo Criado", + "audit-apps-update": "Aplicativo Atualizado", + "audit-channels-delete": "Canal Excluído", + "audit-channels-insert": "Canal Criado", + "audit-channels-update": "Canal Atualizado", + "audit-log-details": "Detalhes do log de auditoria", + "audit-logs": "Logs de auditoria", + "audit-logs-description": "Veja um histórico de alterações feitas na sua organização, incluindo modificações em canais, bundles e membros da equipe.", + "audit-org_users-delete": "Membro Removido", + "audit-org_users-insert": "Membro Adicionado", + "audit-org_users-update": "Membro Atualizado", + "audit-orgs-delete": "Organização Deletada", + "audit-orgs-insert": "Organização Criada", + "audit-orgs-update": "Organização Atualizada", "available-channels": "Canais disponíveis", "available-in-the-san": "Disponível no aplicativo sandbox", "available-versions": "Pacotes disponíveis", @@ -196,6 +217,7 @@ "bandwidth-usage": "Uso de largura de banda:", "bandwith-usage": "Uso de largura de banda:", "base": "Base", + "before": "Antes", "best-plan": "Melhor plano", "bigger-app-size": "Tamanho maior do aplicativo", "billed-annually-at": "Cobrado anualmente em", @@ -314,8 +336,10 @@ "changed-app-name": "Nome do aplicativo alterado com sucesso", "changed-app-retention": "Alteração bem-sucedida da retenção do aplicativo", "changed-expose-metadata": "Configuração de exposição de metadados alterada com sucesso", + "changed-fields": "Campos alterados", "changed-name": "Nome da apikey alterado com sucesso", "changed-password-suc": "Senha alterada com sucesso", + "changes": "Alterações", "channel": "Canal", "channel-ab-testing": "Habilite o teste AB", "channel-ab-testing-percentage": "Porcentagem de usuários recebendo a versão secundária", @@ -357,6 +381,7 @@ "clear-filters": "Limpar Filtros", "cli-doc": "Documento CLI", "cli-version": "Versão do CLI", + "close": "Fechar", "commands": "comandos", "comment": "Comentário", "complete-all-fields": "Por favor, preencha todos os campos", @@ -507,6 +532,7 @@ "daily-registrations": "Registros Diários", "daily-uploads": "Uploads diários", "dashboard": "painel de controle", + "date": "Data", "date-range": "Intervalo de Datas", "debug-api-description": "Use este comando curl para reproduzir a exata solicitação de API que este dispositivo faz para verificar atualizações", "debug-api-request": "Depurar Solicitação de API", @@ -548,6 +574,7 @@ "delete-org": "Excluir organização", "delete-your-account": "Exclua sua conta", "deleted": "excluído", + "deleted-record": "Registro excluído", "deletion-failed": "Falha na exclusão", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Isso demonstra a leitura de valores de entrada de componentes fora do diálogo", @@ -579,6 +606,7 @@ "deployments-title": "Total", "deployments-trend": "Tendência de Implantações", "detailed-usage-plan": "Uso detalhado", + "details": "Detalhes", "device": "Dispositivo", "device-id": "ID do Dispositivo", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Insira sua nova senha e confirme", "error": "Erro", "error-checking-channels": "Erro ao ler canais", + "error-fetching-audit-logs": "Erro ao buscar logs de auditoria", "error-fetching-builds": "Erro ao buscar solicitações de construção", "error-fetching-deploy-history": "Erro ao buscar o histórico de implantação", "error-fetching-members": "Erro ao buscar membros", @@ -639,6 +668,8 @@ "fast-forward": "Avanço Rápido", "feel-magic-of-capgo": "Sinta a magia de:", "filter-actions": "Ações", + "filter-by-operation": "Filtrar por operação", + "filter-by-table": "Filtrar por tabela", "first-name": "Primeiro nome", "first-name-required": "Nome próprio necessário", "force-version": "Versão forçada", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Código 2FA inválido, tente novamente!", "min-update-version": "Versão mínima de atualização", "minor": "Menor", + "minutes-short": "{minutes}m", "misconfigured": "Configuração incorreta", "misconfigured-channels": "Alguns canais estão configurados incorretamente. As atualizações falharão para esses canais!", "missing-email": "E-mail ausente", "missing-name": "Nome ausente", "mo": "Mo", + "modified": "Modificado", "modify-org-info": "Você pode modificar as informações da organização aqui.", "module-heading": "Módulos", "monthly-active": "Ativo mensalmente", @@ -782,6 +815,7 @@ "new-name-not-changed": "O novo nome é o mesmo que o antigo.", "new-name-to-long": "O novo nome é muito longo. Você só pode usar 32 caracteres.", "new-name-to-short": "O nome da apikey é muito curto. Deve ter pelo menos 4 caracteres.", + "new-record": "Novo registro", "new-users": "Novos Usuários", "next": "Próximo", "next-run": "Próxima atualização", @@ -800,6 +834,7 @@ "no-device-data": "Sem dados do dispositivo disponíveis", "no-error-message": "Nenhuma mensagem de erro disponível", "no-manifest-bundle": "Sem manifesto", + "no-organization-selected": "Nenhuma organização selecionada", "no-permission": "Permissões insuficientes", "no-permission-ask-super-admin": "permissão insuficiente, por favor peça a um super administrador para excluir este pacote de forma insegura", "no-public-channel": "O aplicativo não tem canal público, não podemos contar a implantação sem ele.", @@ -948,6 +983,7 @@ "reset-password": "Redefinir Senha", "reset-spoofed-user": "Pare de falsificar", "reset-your-password": "Redefina sua senha", + "resource": "Recurso", "retention": "Excluir automaticamente pacotes não utilizados (após x segundos)", "retention-cannot-be-negative": "A retenção não pode ser um número negativo", "retention-to-big": "A retenção não pode ser maior que 63113903 (2 anos)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Pesquise por nome ou AppID", "search-by-name-or-bundle-id": "Pesquise por nome ou ID do pacote", "search-by-name-or-email": "Pesquise por nome ou email", + "search-by-record-id": "Pesquisar por ID do registro", "search-by-version": "Pesquisar por versão", "search-channels": "Pesquisar canal", "search-versions": "Pesquisar pacote", @@ -1114,6 +1151,7 @@ "version-link-fail": "Não é possível definir a substituição do pacote", "version-linked": "Link da versão", "version-name-missing": "O nome da versão está faltando", + "view": "Visualizar", "want-to-unlink": "Você deseja desvincular?", "warning-organizations-will-be-deleted": "Aviso: as organizações serão excluídas", "warning-organizations-will-be-deleted-message": "Você é o único super administrador nas seguintes organizações. \nEssas organizações serão excluídas permanentemente quando sua conta for removida:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "O que você gostaria de fazer com o aplicativo de fotos?", "write-key": "Escreva", "wrong-name-org-del": "Você não digitou o nome da organização. Você deveria ter digitado: %1", - "minutes-short": "{minutes}m", "x-hours-short": "{hours}h", "yearly": "Anualmente", "yes": "sim", diff --git a/messages/ru.json b/messages/ru.json index d3a5e9da60..3884497a35 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Заблокировано - обновления патч-версий отключены", "action-disable-auto-update-under-native": "Заблокировано - невозможно понизить версию ниже родной", "action-disable-dev-build": "Заблокировано - сборки для разработчиков отключены", - "action-disable-emulator": "Заблокировано - эмулятор отключен", - "action-disable-prod-build": "Заблокировано — продакшен-сборки отключены", "action-disable-device": "Заблокировано — обновления устройств отключены", + "action-disable-emulator": "Заблокировано - эмулятор отключен", "action-disable-platform-android": "Заблокировано - платформа Android отключена", "action-disable-platform-ios": "Заблокировано - платформа iOS отключена", + "action-disable-prod-build": "Заблокировано — продакшен-сборки отключены", "action-download-10": "Прогресс загрузки 10%", "action-download-20": "Прогресс загрузки 20%", "action-download-30": "Прогресс загрузки 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Панель администратора", "admin-dashboard-construction": "Панель администратора находится в стадии разработки. Компоненты будут добавлены на следующем этапе.", "admin-dashboard-description": "Статистика и аналитика по всей платформе", + "after": "После", "afternoon": "после полудня", "alert-2fa-disable": "Подтвердите, что вы хотите отключить 2FA", "alert-2fa-required": "Для сброса пароля требуется 2FA", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Вы уверены, что хотите восстановить этот ключ?", "alert-unknown-error": "Неизвестная ошибка, смотрите консоль разработчика", "all-apps": "Все приложения", + "all-operations": "Все операции", "all-organizations": "Все Организации", + "all-tables": "Все таблицы", "allow-dev-build": "Разрешить сборку для разработки", "allow-develoment-bui": "Разрешить сборку для разработки", "allow-device-to-self": "Разрешить устройствам самостоятельно отсоединяться/подключаться", @@ -181,6 +184,24 @@ "at-least-one-number": "По крайней мере одно число", "at-least-one-uppercase-letter": "По крайней мере одна заглавная буква", "at-least-two-special-characters": "По крайней мере один специальный символ", + "audit-app_versions-delete": "Пакет Удален", + "audit-app_versions-insert": "Создан пакет", + "audit-app_versions-update": "Пакет Обновлен", + "audit-apps-delete": "Приложение удалено", + "audit-apps-insert": "Приложение Создано", + "audit-apps-update": "Приложение обновлено", + "audit-channels-delete": "Канал Удален", + "audit-channels-insert": "Канал Создан", + "audit-channels-update": "Канал Обновлен", + "audit-log-details": "Детали журнала аудита", + "audit-logs": "Журналы аудита", + "audit-logs-description": "Просмотрите историю изменений, внесенных в вашу организацию, включая изменения каналов, пакетов и членов команды.", + "audit-org_users-delete": "Участник Удален", + "audit-org_users-insert": "Участник Добавлен", + "audit-org_users-update": "Участник Обновлен", + "audit-orgs-delete": "Организация Удалена", + "audit-orgs-insert": "Организация Создана", + "audit-orgs-update": "Организация Обновлена", "available-channels": "Доступные каналы", "available-in-the-san": "Доступно в приложении-песочнице", "available-versions": "Доступные пакеты", @@ -196,6 +217,7 @@ "bandwidth-usage": "Использование пропускной способности:", "bandwith-usage": "Использование пропускной способности:", "base": "База", + "before": "До", "best-plan": "Лучший план", "bigger-app-size": "Больший размер приложения", "billed-annually-at": "Ежегодно выставляется счет на", @@ -314,8 +336,10 @@ "changed-app-name": "Успешно изменено имя приложения", "changed-app-retention": "Успешно изменено время хранения данных приложения", "changed-expose-metadata": "Настройка раскрытия метаданных успешно изменена", + "changed-fields": "Измененные поля", "changed-name": "Успешно изменено имя apikey", "changed-password-suc": "Пароль успешно изменен", + "changes": "Изменения", "channel": "Канал", "channel-ab-testing": "Включить AB тестирование", "channel-ab-testing-percentage": "Процент пользователей, получающих вторую версию", @@ -357,6 +381,7 @@ "clear-filters": "Очистить фильтры", "cli-doc": "Документация CLI", "cli-version": "Версия CLI", + "close": "Закрыть", "commands": "команды", "comment": "Комментарий", "complete-all-fields": "Пожалуйста, заполните все поля", @@ -507,6 +532,7 @@ "daily-registrations": "Ежедневные регистрации", "daily-uploads": "Ежедневные загрузки", "dashboard": "приборная панель", + "date": "Дата", "date-range": "Диапазон дат", "debug-api-description": "Используйте эту команду curl, чтобы воспроизвести точный API-запрос, который это устройство делает для проверки обновлений", "debug-api-request": "Отладка API запроса", @@ -548,6 +574,7 @@ "delete-org": "Удалить организацию", "delete-your-account": "Удалите свой аккаунт", "deleted": "удалено", + "deleted-record": "Удаленная запись", "deletion-failed": "Удаление не удалось", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Это демонстрирует чтение входных значений из компонентов за пределами диалога", @@ -579,6 +606,7 @@ "deployments-title": "Всего", "deployments-trend": "Тенденции развертывания", "detailed-usage-plan": "Подробное использование", + "details": "Детали", "device": "Устройство", "device-id": "ID устройства", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Введите ваш новый пароль и подтвердите его", "error": "Ошибка", "error-checking-channels": "Ошибка чтения каналов", + "error-fetching-audit-logs": "Ошибка при получении журналов аудита", "error-fetching-builds": "Ошибка при получении запросов на сборку", "error-fetching-deploy-history": "Ошибка при получении истории развертывания", "error-fetching-members": "Ошибка при получении списка участников", @@ -639,6 +668,8 @@ "fast-forward": "Быстрая перемотка", "feel-magic-of-capgo": "Почувствуй магию:", "filter-actions": "Действия", + "filter-by-operation": "Фильтровать по операции", + "filter-by-table": "Фильтровать по таблице", "first-name": "Имя", "first-name-required": "Требуется имя", "force-version": "Версия принудительно", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Неверный код 2FA, попробуйте снова!", "min-update-version": "Минимальная версия обновления", "minor": "Минорный", + "minutes-short": "{minutes}м", "misconfigured": "Неправильно настроенный", "misconfigured-channels": "Некоторые каналы настроены неправильно. Обновления для этих каналов не будут проходить!", "missing-email": "Отсутствует электронная почта", "missing-name": "Отсутствует имя", "mo": "Мо", + "modified": "Модифицированный", "modify-org-info": "Вы можете изменить информацию об организации здесь.", "module-heading": "Модули", "monthly-active": "Ежемесячно активные", @@ -782,6 +815,7 @@ "new-name-not-changed": "Новое имя такое же, как и старое.", "new-name-to-long": "Новое имя слишком длинное. Вы можете использовать только 32 символа.", "new-name-to-short": "Имя apikey слишком короткое. Оно должно быть длиной не менее 4 символов.", + "new-record": "Новая запись", "new-users": "Новые пользователи", "next": "Следующий", "next-run": "Следующее обновление", @@ -800,6 +834,7 @@ "no-device-data": "Нет данных об устройстве", "no-error-message": "Нет доступного сообщения об ошибке", "no-manifest-bundle": "Нет манифеста", + "no-organization-selected": "Организация не выбрана", "no-permission": "Недостаточно прав доступа", "no-permission-ask-super-admin": "недостаточно прав, пожалуйста, попросите суперадмина удалить этот пакет небезопасно", "no-public-channel": "У приложения нет общедоступного канала, мы не можем подсчитать развертывание без него.", @@ -948,6 +983,7 @@ "reset-password": "Сбросить Пароль", "reset-spoofed-user": "Прекратите подделку", "reset-your-password": "Сбросьте ваш пароль", + "resource": "Ресурс", "retention": "Автоматическое удаление неиспользуемых пакетов (через x секунд)", "retention-cannot-be-negative": "Удержание не может быть отрицательным числом", "retention-to-big": "Срок хранения не может превышать 63113903 (2 года)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Поиск по имени или AppID", "search-by-name-or-bundle-id": "Поиск по имени или ID пакета", "search-by-name-or-email": "Поиск по имени или электронной почте", + "search-by-record-id": "Поиск по ID записи", "search-by-version": "Поиск по версии", "search-channels": "Поиск канала", "search-versions": "Поиск пакета", @@ -1114,6 +1151,7 @@ "version-link-fail": "Не удается установить переопределение пакета", "version-linked": "Ссылка на версию", "version-name-missing": "Отсутствует имя версии", + "view": "Просмотр", "want-to-unlink": "Вы хотите отменить связь?", "warning-organizations-will-be-deleted": "Предупреждение: организации будут удалены", "warning-organizations-will-be-deleted-message": "Вы единственный супер администратор в следующих организациях. \nЭти организации будут навсегда удалены при удалении вашей учетной записи:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "Что вы хотите сделать с фото в приложении?", "write-key": "Пишите", "wrong-name-org-del": "Вы не ввели название организации. Вы должны были ввести: %1", - "minutes-short": "{minutes}м", "x-hours-short": "{hours}h", "yearly": "Ежегодно", "yes": "да", diff --git a/messages/tr.json b/messages/tr.json index bc73de797e..8e3772b74f 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Engellendi - yama sürüm güncellemeleri devre dışı bırakıldı", "action-disable-auto-update-under-native": "Engellenmiş - yerel sürümün altına düşüremezsiniz", "action-disable-dev-build": "Engellendi - geliştirme yapıları devre dışı bırakıldı", - "action-disable-emulator": "Engellendi - emülatör devre dışı bırakıldı", - "action-disable-prod-build": "Engellendi - prodüksiyon derlemeleri devre dışı", "action-disable-device": "Engellendi - cihaz güncellemeleri devre dışı", + "action-disable-emulator": "Engellendi - emülatör devre dışı bırakıldı", "action-disable-platform-android": "Engellendi - Android platformu devre dışı bırakıldı", "action-disable-platform-ios": "Engellendi - iOS platformu devre dışı bırakıldı", + "action-disable-prod-build": "Engellendi - prodüksiyon derlemeleri devre dışı", "action-download-10": "İndirme ilerlemesi %10", "action-download-20": "İndirme ilerlemesi %20", "action-download-30": "İndirme ilerlemesi %30", @@ -119,6 +119,7 @@ "admin-dashboard": "Yönetici Kontrol Paneli", "admin-dashboard-construction": "Yönetici kontrol paneli inşa halindedir. Bileşenler bir sonraki aşamada eklenecektir.", "admin-dashboard-description": "Platform geneli istatistikler ve analitikler", + "after": "Sonra", "afternoon": "öğleden sonra", "alert-2fa-disable": "2FA'yı devre dışı bırakmak istediğinizi onaylayın.", "alert-2fa-required": "Şifreyi sıfırlamak için 2FA gereklidir.", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Bu anahtarı yeniden oluşturmak istediğinizden emin misiniz?", "alert-unknown-error": "Bilinmeyen hata, dev konsoluna bakın", "all-apps": "Tüm Uygulamalar", + "all-operations": "Tüm İşlemler", "all-organizations": "Tüm Organizasyonlar", + "all-tables": "Tüm Tablolar", "allow-dev-build": "Geliştirme yapısına izin ver", "allow-develoment-bui": "Geliştirme cihazlarına izin ver", "allow-device-to-self": "Cihazların kendiliğinden ayrılmasına/bağlanmasına izin verin", @@ -181,6 +184,24 @@ "at-least-one-number": "En az bir sayı", "at-least-one-uppercase-letter": "En az bir büyük harf", "at-least-two-special-characters": "En az bir özel karakter", + "audit-app_versions-delete": "Paket Silindi", + "audit-app_versions-insert": "Paket Oluşturuldu", + "audit-app_versions-update": "Paket Güncellendi", + "audit-apps-delete": "Uygulama Silindi", + "audit-apps-insert": "Uygulama Oluşturuldu", + "audit-apps-update": "Uygulama Güncellendi", + "audit-channels-delete": "Kanal Silindi", + "audit-channels-insert": "Kanal Oluşturuldu", + "audit-channels-update": "Kanal Güncellendi", + "audit-log-details": "Denetim Günlüğü Detayları", + "audit-logs": "Denetim Günlükleri", + "audit-logs-description": "Kanallar, paketler ve ekip üyelerindeki değişiklikler dahil olmak üzere kuruluşunuzda yapılan değişikliklerin geçmişini görüntüleyin.", + "audit-org_users-delete": "Üye Kaldırıldı", + "audit-org_users-insert": "Üye Eklendi", + "audit-org_users-update": "Üye Güncellendi", + "audit-orgs-delete": "Organizasyon Silindi", + "audit-orgs-insert": "Organizasyon Oluşturuldu", + "audit-orgs-update": "Organizasyon Güncellendi", "available-channels": "Kullanılabilir kanallar", "available-in-the-san": "Kum havuzu uygulamasında mevcut", "available-versions": "Mevcut paketler", @@ -196,6 +217,7 @@ "bandwidth-usage": "Bant genişliği kullanımı:", "bandwith-usage": "Bant genişliği kullanımı:", "base": "Taban", + "before": "Önce", "best-plan": "En iyi plan", "bigger-app-size": "Daha büyük uygulama boyutu", "billed-annually-at": "Yıllık olarak faturalandırılır", @@ -314,8 +336,10 @@ "changed-app-name": "Uygulama adı başarıyla değiştirildi", "changed-app-retention": "Uygulamanın saklama süresi başarıyla değiştirildi", "changed-expose-metadata": "Meta veri paylaşım ayarı başarıyla değiştirildi", + "changed-fields": "Değiştirilen Alanlar", "changed-name": "Apikey'in adı başarıyla değiştirildi", "changed-password-suc": "Şifre başarıyla değiştirildi", + "changes": "Değişiklikler", "channel": "Kanal", "channel-ab-testing": "AB testini etkinleştir", "channel-ab-testing-percentage": "İkincil sürümü alan kullanıcıların yüzdesi", @@ -357,6 +381,7 @@ "clear-filters": "Filtreleri Temizle", "cli-doc": "CLI belgesi", "cli-version": "CLI sürümü", + "close": "Kapat", "commands": "komutlar", "comment": "Yorum", "complete-all-fields": "Lütfen tüm alanları doldurun.", @@ -507,6 +532,7 @@ "daily-registrations": "Günlük Kayıtlar", "daily-uploads": "Günlük yüklemeler", "dashboard": "gösterge paneli", + "date": "Tarih", "date-range": "Tarih Aralığı", "debug-api-description": "Bu curl komutunu kullanarak, bu cihazın güncellemeleri kontrol etmek için yaptığı tam API isteğini yeniden oluşturun.", "debug-api-request": "Hata Ayıklama API İsteği", @@ -548,6 +574,7 @@ "delete-org": "Organizasyonu sil", "delete-your-account": "Hesabını sil", "deleted": "silindi", + "deleted-record": "Silinen Kayıt", "deletion-failed": "Silme başarısız oldu", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Bu, diyalog dışındaki bileşenlerden giriş değerlerinin okunmasını gösterir.", @@ -579,6 +606,7 @@ "deployments-title": "Toplam", "deployments-trend": "Dağıtım Trendi", "detailed-usage-plan": "Ayrıntılı kullanım", + "details": "Detaylar", "device": "Cihaz", "device-id": "Cihaz Kimliği", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Yeni şifrenizi girin ve onaylayın", "error": "Hata", "error-checking-channels": "Kanalları okuma hatası", + "error-fetching-audit-logs": "Denetim günlükleri alınırken hata oluştu", "error-fetching-builds": "Yapı talepleri getirilirken hata oluştu", "error-fetching-deploy-history": "Dağıtım geçmişi getirme hatası", "error-fetching-members": "Üyeler getirilirken hata", @@ -639,6 +668,8 @@ "fast-forward": "Hızlı İleri Sar", "feel-magic-of-capgo": "Şunun büyüsünü hisset:", "filter-actions": "Eylemler", + "filter-by-operation": "İşleme göre filtrele", + "filter-by-table": "Tabloya göre filtrele", "first-name": "İsim", "first-name-required": "İsim gereklidir", "force-version": "Zorunlu sürüm", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Geçersiz 2FA kodu, tekrar deneyin!", "min-update-version": "Minimal güncelleme sürümü", "minor": "Küçük", + "minutes-short": "{minutes}dk", "misconfigured": "Yanlış yapılandırılmış", "misconfigured-channels": "Bazı kanallar yanlış yapılandırılmış. Bu kanallar için güncellemeler başarısız olacak!", "missing-email": "E-posta eksik", "missing-name": "İsim eksik", "mo": "Ben", + "modified": "Değiştirilmiş", "modify-org-info": "Buradan kuruluşun bilgilerini değiştirebilirsiniz.", "module-heading": "Modüller", "monthly-active": "Aylık aktif", @@ -782,6 +815,7 @@ "new-name-not-changed": "Yeni isim eskiyle aynı.", "new-name-to-long": "Yeni isim çok uzun. Sadece 32 karakter kullanabilirsiniz.", "new-name-to-short": "Apikey adı çok kısa. En az 4 karakter uzunluğunda olmalıdır.", + "new-record": "Yeni Kayıt", "new-users": "Yeni Kullanıcılar", "next": "Sonraki", "next-run": "Sonraki güncelleme", @@ -800,6 +834,7 @@ "no-device-data": "Cihaz verisi mevcut değil", "no-error-message": "Hata mesajı mevcut değil", "no-manifest-bundle": "Manifest yok", + "no-organization-selected": "Organizasyon seçilmedi", "no-permission": "Yetersiz izinler", "no-permission-ask-super-admin": "yetersiz izin, lütfen bu paketi güvensiz bir şekilde silmek için bir süper yöneticiye başvurun", "no-public-channel": "Uygulamanın herhangi bir halka açık kanalı yok, onun olmadan dağıtımı sayamayız.", @@ -948,6 +983,7 @@ "reset-password": "Şifreyi Sıfırla", "reset-spoofed-user": "Sahte yapmayı durdurun", "reset-your-password": "Şifrenizi sıfırlayın", + "resource": "Kaynak", "retention": "Kullanılmayan paketleri otomatik sil (x saniye sonra)", "retention-cannot-be-negative": "Tutma negatif bir sayı olamaz", "retention-to-big": "Tutma 63113903'den (2 yıl) daha büyük olamaz", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "İsme veya AppID'ye göre arama yapın", "search-by-name-or-bundle-id": "İsme veya paket ID'sine göre arama yapın", "search-by-name-or-email": "İsme veya e-postaya göre ara", + "search-by-record-id": "Kayıt kimliğine göre ara", "search-by-version": "Sürümle ara", "search-channels": "Kanal ara", "search-versions": "Arama paketi", @@ -1114,6 +1151,7 @@ "version-link-fail": "Paket geçersiz kılması ayarlanamıyor", "version-linked": "Sürüm bağlantısı", "version-name-missing": "Sürüm adı eksik", + "view": "Görüntüle", "want-to-unlink": "Bağlantıyı kaldırmak istiyor musunuz?", "warning-organizations-will-be-deleted": "Uyarı: Kuruluşlar silinecek", "warning-organizations-will-be-deleted-message": "Aşağıdaki kuruluşlarda tek süper yönetici sizsiniz. \nHesabınız kaldırıldığında bu kuruluşlar kalıcı olarak silinecektir:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "Uygulama fotoğrafıyla ne yapmak istersiniz?", "write-key": "Yaz", "wrong-name-org-del": "Organizasyon adını yazmadınız. Yazmanız gereken: %1", - "minutes-short": "{minutes}dk", "x-hours-short": "{hours}h", "yearly": "Yıllık", "yes": "evet", diff --git a/messages/vi.json b/messages/vi.json index 2bde8bc2b1..b6e6658bd1 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "Đã chặn - cập nhật phiên bản vá đã bị vô hiệu hóa", "action-disable-auto-update-under-native": "Bị chặn - không thể hạ cấp xuống dưới mức gốc", "action-disable-dev-build": "Đã chặn - xây dựng phiên bản dev đã bị vô hiệu hóa", - "action-disable-emulator": "Đã chặn - trình giả lập bị vô hiệu hóa", - "action-disable-prod-build": "Bị chặn - bản build sản xuất bị tắt", "action-disable-device": "Bị chặn - cập nhật thiết bị bị tắt", + "action-disable-emulator": "Đã chặn - trình giả lập bị vô hiệu hóa", "action-disable-platform-android": "Đã chặn - Nền tảng Android đã bị vô hiệu hóa", "action-disable-platform-ios": "Đã chặn - Nền tảng iOS đã bị vô hiệu hóa", + "action-disable-prod-build": "Bị chặn - bản build sản xuất bị tắt", "action-download-10": "Tiến trình tải xuống 10%", "action-download-20": "Tiến trình tải xuống 20%", "action-download-30": "Tiến trình tải xuống 30%", @@ -119,6 +119,7 @@ "admin-dashboard": "Bảng điều khiển quản trị", "admin-dashboard-construction": "Bảng điều khiển quản trị đang được xây dựng. Các thành phần sẽ được thêm vào trong giai đoạn tiếp theo.", "admin-dashboard-description": "Thống kê và phân tích trên toàn nền tảng", + "after": "Sau", "afternoon": "chiều", "alert-2fa-disable": "Xác nhận rằng bạn muốn vô hiệu hóa 2FA", "alert-2fa-required": "Yêu cầu 2FA để đặt lại mật khẩu", @@ -147,7 +148,9 @@ "alert-regenerate-key": "Bạn có chắc bạn muốn tạo lại khóa này không?", "alert-unknown-error": "Lỗi không xác định, xem bảng điều khiển phát triển", "all-apps": "Tất cả ứng dụng", + "all-operations": "Tất cả các thao tác", "all-organizations": "Tất cả các Tổ chức", + "all-tables": "Tất cả các bảng", "allow-dev-build": "Cho phép xây dựng phát triển", "allow-develoment-bui": "Cho phép xây dựng phát triển", "allow-device-to-self": "Cho phép thiết bị tự ngắt kết nối/kết nối", @@ -181,6 +184,24 @@ "at-least-one-number": "Ít nhất một số", "at-least-one-uppercase-letter": "Ít nhất một chữ cái in hoa", "at-least-two-special-characters": "Ít nhất một ký tự đặc biệt", + "audit-app_versions-delete": "Gói đã được xóa", + "audit-app_versions-insert": "Gói đã được tạo", + "audit-app_versions-update": "Gói đã được cập nhật", + "audit-apps-delete": "Ứng dụng đã bị xóa", + "audit-apps-insert": "Ứng dụng đã được tạo", + "audit-apps-update": "Ứng dụng đã được cập nhật", + "audit-channels-delete": "Kênh đã bị xóa", + "audit-channels-insert": "Kênh đã được tạo", + "audit-channels-update": "Kênh đã được cập nhật", + "audit-log-details": "Chi tiết nhật ký kiểm toán", + "audit-logs": "Nhật ký kiểm toán", + "audit-logs-description": "Xem lịch sử các thay đổi đã thực hiện trong tổ chức của bạn, bao gồm các sửa đổi đối với kênh, gói và thành viên nhóm.", + "audit-org_users-delete": "Thành viên đã bị xóa", + "audit-org_users-insert": "Thành viên đã được thêm", + "audit-org_users-update": "Thành viên đã cập nhật", + "audit-orgs-delete": "Tổ chức đã bị xóa", + "audit-orgs-insert": "Tổ chức Được Tạo", + "audit-orgs-update": "Tổ chức đã được cập nhật", "available-channels": "Các kênh có sẵn", "available-in-the-san": "Có sẵn trong ứng dụng sandbox", "available-versions": "Các gói hàng có sẵn", @@ -196,6 +217,7 @@ "bandwidth-usage": "Sử dụng băng thông:", "bandwith-usage": "Sử dụng băng thông:", "base": "Cơ sở", + "before": "Trước", "best-plan": "Kế hoạch tốt nhất", "bigger-app-size": "Kích thước ứng dụng lớn hơn", "billed-annually-at": "Được tính hàng năm tại", @@ -314,8 +336,10 @@ "changed-app-name": "Đã thay đổi tên ứng dụng thành công", "changed-app-retention": "Đã thay đổi thành công chính sách giữ người dùng của ứng dụng", "changed-expose-metadata": "Đã thay đổi cài đặt hiển thị siêu dữ liệu thành công", + "changed-fields": "Các trường đã thay đổi", "changed-name": "Đã thay đổi thành công tên của apikey", "changed-password-suc": "Đã thay đổi mật khẩu thành công", + "changes": "Thay đổi", "channel": "Kênh", "channel-ab-testing": "Kích hoạt kiểm thử AB", "channel-ab-testing-percentage": "Phần trăm người dùng nhận phiên bản thứ cấp", @@ -357,6 +381,7 @@ "clear-filters": "Xóa Bộ lọc", "cli-doc": "Tài liệu CLI", "cli-version": "Phiên bản CLI", + "close": "Đóng", "commands": "lệnh", "comment": "Bình luận", "complete-all-fields": "Vui lòng điền đầy đủ tất cả các mục", @@ -507,6 +532,7 @@ "daily-registrations": "Đăng ký hàng ngày", "daily-uploads": "Tải lên hàng ngày", "dashboard": "bảng điều khiển", + "date": "Ngày", "date-range": "Khoảng thời gian", "debug-api-description": "Sử dụng lệnh curl này để tái tạo chính xác yêu cầu API mà thiết bị này thực hiện để kiểm tra cập nhật", "debug-api-request": "Yêu cầu API Gỡ lỗi", @@ -548,6 +574,7 @@ "delete-org": "Xóa tổ chức", "delete-your-account": "Xóa tài khoản của bạn", "deleted": "đã xóa", + "deleted-record": "Bản ghi đã xóa", "deletion-failed": "Xóa thất bại", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "Điều này minh họa việc đọc các giá trị đầu vào từ các thành phần ngoài hộp thoại", @@ -579,6 +606,7 @@ "deployments-title": "Tổng cộng", "deployments-trend": "Xu hướng triển khai", "detailed-usage-plan": "Sử dụng chi tiết", + "details": "Chi tiết", "device": "Thiết bị", "device-id": "ID thiết bị", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "Nhập mật khẩu mới của bạn và xác nhận", "error": "Lỗi", "error-checking-channels": "Lỗi đọc kênh", + "error-fetching-audit-logs": "Lỗi khi tải nhật ký kiểm toán", "error-fetching-builds": "Lỗi khi tìm nạp yêu cầu xây dựng", "error-fetching-deploy-history": "Lỗi khi tải lịch sử triển khai", "error-fetching-members": "Lỗi khi tải thành viên", @@ -639,6 +668,8 @@ "fast-forward": "Tiến Nhanh", "feel-magic-of-capgo": "Cảm nhận sự ma thuật của:", "filter-actions": "Hành động", + "filter-by-operation": "Lọc theo thao tác", + "filter-by-table": "Lọc theo bảng", "first-name": "Tên đầu tiên", "first-name-required": "Yêu cầu tên đầu tiên", "force-version": "Phiên bản ép buộc", @@ -758,11 +789,13 @@ "mfa-invalid-code": "Mã 2FA không hợp lệ, hãy thử lại!", "min-update-version": "Phiên bản cập nhật tối thiểu", "minor": "Nhỏ hơn", + "minutes-short": "{minutes}ph", "misconfigured": "Cấu hình sai", "misconfigured-channels": "Một số kênh đã được cấu hình sai. Các bản cập nhật sẽ thất bại cho những kênh đó!", "missing-email": "Thiếu email", "missing-name": "Thiếu tên", "mo": "Mơ", + "modified": "Đã được sửa đổi", "modify-org-info": "Bạn có thể chỉnh sửa thông tin của tổ chức ở đây.", "module-heading": "Các mô-đun", "monthly-active": "Hoạt động hàng tháng", @@ -782,6 +815,7 @@ "new-name-not-changed": "Tên mới giống hệt như tên cũ", "new-name-to-long": "Tên mới quá dài. Bạn chỉ có thể sử dụng 32 ký tự", "new-name-to-short": "Tên apikey quá ngắn. Nó phải có ít nhất 4 ký tự.", + "new-record": "Bản ghi mới", "new-users": "Người dùng mới", "next": "Tiếp theo", "next-run": "Cập nhật tiếp theo", @@ -800,6 +834,7 @@ "no-device-data": "Không có dữ liệu thiết bị nào", "no-error-message": "Không có thông báo lỗi nào sẵn có", "no-manifest-bundle": "Không có manifest", + "no-organization-selected": "Chưa chọn tổ chức", "no-permission": "Quyền hạn không đủ", "no-permission-ask-super-admin": "quyền không đủ, vui lòng yêu cầu quản trị viên cao cấp để xóa gói này một cách không an toàn", "no-public-channel": "Ứng dụng không có kênh công khai, chúng tôi không thể đếm số lần triển khai mà không có nó", @@ -948,6 +983,7 @@ "reset-password": "Đặt lại mật khẩu", "reset-spoofed-user": "Dừng giả mạo", "reset-your-password": "Đặt lại mật khẩu của bạn", + "resource": "Tài nguyên", "retention": "Tự động xóa các gói không được sử dụng (sau x giây)", "retention-cannot-be-negative": "Giữ lại không thể là một số âm", "retention-to-big": "Thời gian lưu trữ không thể lớn hơn 63113903 (2 năm)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "Tìm kiếm theo tên hoặc AppID", "search-by-name-or-bundle-id": "Tìm kiếm theo tên hoặc ID gói", "search-by-name-or-email": "Tìm kiếm theo tên hoặc email", + "search-by-record-id": "Tìm theo ID bản ghi", "search-by-version": "Tìm kiếm theo phiên bản", "search-channels": "Tìm kênh", "search-versions": "Tìm kiếm gói", @@ -1114,6 +1151,7 @@ "version-link-fail": "Không thể đặt ghi đè bộ nhớ", "version-linked": "Liên kết phiên bản", "version-name-missing": "Tên phiên bản đang thiếu", + "view": "Xem", "want-to-unlink": "Bạn có muốn hủy liên kết không?", "warning-organizations-will-be-deleted": "Cảnh báo: Các tổ chức sẽ bị xóa", "warning-organizations-will-be-deleted-message": "Bạn là siêu quản trị viên duy nhất trong các tổ chức sau đây. \nCác tổ chức này sẽ bị xóa vĩnh viễn khi tài khoản của bạn bị xóa:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "Bạn muốn làm gì với ứng dụng hình ảnh?", "write-key": "Viết", "wrong-name-org-del": "Bạn chưa nhập tên tổ chức. Bạn cần phải nhập: %1", - "minutes-short": "{minutes}ph", "x-hours-short": "{hours}h", "yearly": "Hàng năm", "yes": "có", diff --git a/messages/zh-cn.json b/messages/zh-cn.json index a2c4d2373c..284c38945e 100644 --- a/messages/zh-cn.json +++ b/messages/zh-cn.json @@ -52,11 +52,11 @@ "action-disable-auto-update-to-patch": "已阻止 - 禁用了补丁版本更新", "action-disable-auto-update-under-native": "被阻止 - 无法降级到低于本地版本", "action-disable-dev-build": "被阻止 - 已禁用开发版本", - "action-disable-emulator": "已阻止 - 模拟器已禁用", - "action-disable-prod-build": "已阻止 - 生产构建已禁用", "action-disable-device": "已阻止 - 设备更新已禁用", + "action-disable-emulator": "已阻止 - 模拟器已禁用", "action-disable-platform-android": "已阻止 - Android平台已禁用", "action-disable-platform-ios": "被阻止 - iOS平台已禁用", + "action-disable-prod-build": "已阻止 - 生产构建已禁用", "action-download-10": "下载进度10%", "action-download-20": "下载进度20%", "action-download-30": "下载进度30%", @@ -119,6 +119,7 @@ "admin-dashboard": "管理员控制面板", "admin-dashboard-construction": "管理员控制面板正在建设中。组件将在下一阶段添加。", "admin-dashboard-description": "全平台统计数据和分析", + "after": "之后", "afternoon": "下午", "alert-2fa-disable": "确认您要禁用 2FA", "alert-2fa-required": "重置密码需要 2FA", @@ -147,7 +148,9 @@ "alert-regenerate-key": "您确定要重新生成此密钥吗", "alert-unknown-error": "未知错误,请参阅开发控制台", "all-apps": "所有应用程序", + "all-operations": "所有操作", "all-organizations": "所有组织", + "all-tables": "所有表格", "allow-dev-build": "允许开发构建", "allow-develoment-bui": "允许开发构建", "allow-device-to-self": "允许设备自我解除关联/关联", @@ -181,6 +184,24 @@ "at-least-one-number": "至少一个数字", "at-least-one-uppercase-letter": "至少一封大写字母", "at-least-two-special-characters": "至少一个特殊字符", + "audit-app_versions-delete": "捆绑包已删除", + "audit-app_versions-insert": "创建的捆绑包", + "audit-app_versions-update": "捆绑包已更新", + "audit-apps-delete": "应用已删除", + "audit-apps-insert": "应用创建完成", + "audit-apps-update": "应用已更新", + "audit-channels-delete": "频道已删除", + "audit-channels-insert": "创建的频道", + "audit-channels-update": "频道已更新", + "audit-log-details": "审计日志详情", + "audit-logs": "审计日志", + "audit-logs-description": "查看对您组织所做更改的历史记录,包括对频道、包和团队成员的修改。", + "audit-org_users-delete": "成员已移除", + "audit-org_users-insert": "成员已添加", + "audit-org_users-update": "会员已更新", + "audit-orgs-delete": "组织已删除", + "audit-orgs-insert": "创建的组织", + "audit-orgs-update": "组织已更新", "available-channels": "可用频道", "available-in-the-san": "在测试环境中可用", "available-versions": "可用的捆绑包", @@ -196,6 +217,7 @@ "bandwidth-usage": "带宽使用情况:", "bandwith-usage": "带宽使用量:", "base": "根据", + "before": "之前", "best-plan": "最佳计划", "bigger-app-size": "更大的应用程序大小", "billed-annually-at": "每年计费于", @@ -314,8 +336,10 @@ "changed-app-name": "更改应用名称成功", "changed-app-retention": "成功更改了应用程序的保留时间", "changed-expose-metadata": "成功更改元数据公开设置", + "changed-fields": "已更改的字段", "changed-name": "API 密钥名称更改成功", "changed-password-suc": "密码修改成功", + "changes": "更改内容", "channel": "渠道", "channel-ab-testing": "启用AB测试", "channel-ab-testing-percentage": "接收辅助版本的用户百分比", @@ -357,6 +381,7 @@ "clear-filters": "清除筛选器", "cli-doc": "CLI文档", "cli-version": "CLI 版本", + "close": "关闭", "commands": "命令", "comment": "评论", "complete-all-fields": "请填写所有字段", @@ -507,6 +532,7 @@ "daily-registrations": "每日注册", "daily-uploads": "每日上传", "dashboard": "仪表板", + "date": "日期", "date-range": "日期范围", "debug-api-description": "使用此curl命令来复制此设备用于检查更新的确切API请求", "debug-api-request": "调试API请求", @@ -548,6 +574,7 @@ "delete-org": "删除组织", "delete-your-account": "删除您的帐户", "deleted": "删除", + "deleted-record": "已删除的记录", "deletion-failed": "删除失败", "demo-email-placeholder": "john.doe@example.com", "demo-external-input-desc": "这展示了从对话框外的组件读取输入值", @@ -579,6 +606,7 @@ "deployments-title": "总计", "deployments-trend": "部署趋势", "detailed-usage-plan": "详细使用方法", + "details": "详细信息", "device": "设备", "device-id": "设备ID", "device-id-placeholder": "00000000-0000-0000-0000-000000000000", @@ -617,6 +645,7 @@ "enter-your-new-passw": "输入您的新密码并确认", "error": "错误", "error-checking-channels": "读取频道时出错", + "error-fetching-audit-logs": "获取审计日志时出错", "error-fetching-builds": "获取构建请求时出错", "error-fetching-deploy-history": "获取部署历史记录时出错", "error-fetching-members": "获取成员时出错", @@ -639,6 +668,8 @@ "fast-forward": "快进", "feel-magic-of-capgo": "感受这种魔力:", "filter-actions": "行动", + "filter-by-operation": "按操作筛选", + "filter-by-table": "按表格筛选", "first-name": "名", "first-name-required": "需要名字", "force-version": "强制版本", @@ -758,11 +789,13 @@ "mfa-invalid-code": "2FA 代码无效,请重试!", "min-update-version": "最小更新版本", "minor": "次要的", + "minutes-short": "{minutes}分钟", "misconfigured": "配置错误", "misconfigured-channels": "某些通道配置错误。\n这些频道的更新将失败!", "missing-email": "缺少电子邮件", "missing-name": "缺少名字", "mo": "MB", + "modified": "修改过的", "modify-org-info": "您可以在此处修改组织的信息。", "module-heading": "模块", "monthly-active": "每月活跃", @@ -782,6 +815,7 @@ "new-name-not-changed": "新名称与旧名称相同", "new-name-to-long": "新名字太长了。\n您只能使用 32 个字符", "new-name-to-short": "apikey 名称太短。\n长度必须至少为 4 个字符", + "new-record": "新记录", "new-users": "新用户", "next": "下一个", "next-run": "下次更新", @@ -800,6 +834,7 @@ "no-device-data": "没有设备数据可用", "no-error-message": "没有可用的错误信息", "no-manifest-bundle": "没有清单", + "no-organization-selected": "未选择组织", "no-permission": "权限不足", "no-permission-ask-super-admin": "权限不足,请让超级管理员不安全地删除此包", "no-public-channel": "该应用没有公共频道,我们无法在没有它的情况下计算部署。", @@ -948,6 +983,7 @@ "reset-password": "重设密码", "reset-spoofed-user": "停止欺骗", "reset-your-password": "重置你的密码", + "resource": "资源", "retention": "自动删除未使用的捆绑包(在x秒后)", "retention-cannot-be-negative": "保留不能是负数", "retention-to-big": "保留不能大于63113903(2年)", @@ -974,6 +1010,7 @@ "search-by-name-or-app-id": "通过名称或AppID搜索", "search-by-name-or-bundle-id": "按名称或捆绑包 ID 搜索", "search-by-name-or-email": "通过姓名或电子邮件搜索", + "search-by-record-id": "按记录ID搜索", "search-by-version": "按版本搜索", "search-channels": "搜索频道", "search-versions": "搜索捆绑包", @@ -1114,6 +1151,7 @@ "version-link-fail": "无法设置捆绑覆盖", "version-linked": "版本链接", "version-name-missing": "版本名称缺失", + "view": "查看", "want-to-unlink": "您想取消链接吗?", "warning-organizations-will-be-deleted": "警告:将删除组织", "warning-organizations-will-be-deleted-message": "您是以下组织中唯一的超级管理员。\n删除您的帐户时,这些组织将被永久删除:", @@ -1125,7 +1163,6 @@ "what-to-do-with-photo-dec": "您想对应用程序照片做什么?", "write-key": "写", "wrong-name-org-del": "您尚未输入组织名称。\n您应该输入:%1", - "minutes-short": "{minutes}分钟", "x-hours-short": "{hours}小时", "yearly": "每年", "yes": "是的", diff --git a/src/components.d.ts b/src/components.d.ts index de17302a3d..cc10191b57 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -18,6 +18,7 @@ declare module 'vue' { AdminTrendChart: typeof import('./components/admin/AdminTrendChart.vue')['default'] AppSetting: typeof import('./components/dashboard/AppSetting.vue')['default'] AppTable: typeof import('./components/tables/AppTable.vue')['default'] + AuditLogTable: typeof import('./components/tables/AuditLogTable.vue')['default'] Banner: typeof import('./components/Banner.vue')['default'] BlurBg: typeof import('./components/BlurBg.vue')['default'] BuildTable: typeof import('./components/tables/BuildTable.vue')['default'] @@ -74,6 +75,7 @@ declare global { const AdminTrendChart: typeof import('./components/admin/AdminTrendChart.vue')['default'] const AppSetting: typeof import('./components/dashboard/AppSetting.vue')['default'] const AppTable: typeof import('./components/tables/AppTable.vue')['default'] + const AuditLogTable: typeof import('./components/tables/AuditLogTable.vue')['default'] const Banner: typeof import('./components/Banner.vue')['default'] const BlurBg: typeof import('./components/BlurBg.vue')['default'] const BuildTable: typeof import('./components/tables/BuildTable.vue')['default'] diff --git a/src/components/tables/AuditLogTable.vue b/src/components/tables/AuditLogTable.vue new file mode 100644 index 0000000000..04e815a818 --- /dev/null +++ b/src/components/tables/AuditLogTable.vue @@ -0,0 +1,636 @@ + + + diff --git a/src/constants/organizationTabs.ts b/src/constants/organizationTabs.ts index e6888911ea..c78a1967ac 100644 --- a/src/constants/organizationTabs.ts +++ b/src/constants/organizationTabs.ts @@ -1,5 +1,6 @@ import type { Tab } from '~/components/comp_def' import IconChart from '~icons/heroicons/chart-bar' +import IconAudit from '~icons/heroicons/clipboard-document-list' import IconPlan from '~icons/heroicons/credit-card' import IconCredits from '~icons/heroicons/currency-dollar' import IconInfo from '~icons/heroicons/information-circle' @@ -8,6 +9,7 @@ import IconUsers from '~icons/heroicons/users' export const organizationTabs: Tab[] = [ { label: 'general', key: '/settings/organization', icon: IconInfo }, { label: 'members', key: '/settings/organization/members', icon: IconUsers }, + { label: 'audit-logs', key: '/settings/organization/auditlogs', icon: IconAudit }, { label: 'plans', key: '/settings/organization/plans', icon: IconPlan }, { label: 'usage', key: '/settings/organization/usage', icon: IconChart }, { label: 'credits', key: '/settings/organization/credits', icon: IconCredits }, diff --git a/src/layouts/settings.vue b/src/layouts/settings.vue index d936976af2..e80a3baa35 100644 --- a/src/layouts/settings.vue +++ b/src/layouts/settings.vue @@ -55,6 +55,17 @@ watchEffect(() => { if (!needsPlans && hasPlans) organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/plans') + // Audit logs - visible only to super_admins + const needsAuditLogs = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin']) + const hasAuditLogs = organizationTabs.value.find(tab => tab.key === '/settings/organization/audit-logs') + if (needsAuditLogs && !hasAuditLogs) { + const base = baseOrgTabs.find(t => t.key === '/settings/organization/audit-logs') + if (base) + organizationTabs.value.push({ ...base }) + } + if (!needsAuditLogs && hasAuditLogs) + organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/audit-logs') + if (!Capacitor.isNativePlatform() && organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin']) && !organizationTabs.value.find(tab => tab.key === '/billing')) { diff --git a/src/pages/settings/organization/AuditLogs.vue b/src/pages/settings/organization/AuditLogs.vue new file mode 100644 index 0000000000..4b8428e63c --- /dev/null +++ b/src/pages/settings/organization/AuditLogs.vue @@ -0,0 +1,49 @@ + + + + + +meta: + layout: settings + diff --git a/src/typed-router.d.ts b/src/typed-router.d.ts index d38546d331..1af908ff63 100644 --- a/src/typed-router.d.ts +++ b/src/typed-router.d.ts @@ -331,6 +331,13 @@ declare module 'vue-router/auto-routes' { Record, | never >, + '/settings/organization/AuditLogs': RouteRecordInfo< + '/settings/organization/AuditLogs', + '/settings/organization/AuditLogs', + Record, + Record, + | never + >, '/settings/organization/Credits': RouteRecordInfo< '/settings/organization/Credits', '/settings/organization/Credits', @@ -650,6 +657,12 @@ declare module 'vue-router/auto-routes' { views: | never } + 'src/pages/settings/organization/AuditLogs.vue': { + routes: + | '/settings/organization/AuditLogs' + views: + | never + } 'src/pages/settings/organization/Credits.vue': { routes: | '/settings/organization/Credits' diff --git a/src/types/supabase.types.ts b/src/types/supabase.types.ts index c094d53b34..5fbc08c371 100644 --- a/src/types/supabase.types.ts +++ b/src/types/supabase.types.ts @@ -238,6 +238,60 @@ export type Database = { }, ] } + audit_logs: { + Row: { + id: number + created_at: string + table_name: string + record_id: string + operation: string + user_id: string | null + org_id: string + old_record: Json | null + new_record: Json | null + changed_fields: string[] | null + } + Insert: { + id?: number + created_at?: string + table_name: string + record_id: string + operation: string + user_id?: string | null + org_id: string + old_record?: Json | null + new_record?: Json | null + changed_fields?: string[] | null + } + Update: { + id?: number + created_at?: string + table_name?: string + record_id?: string + operation?: string + user_id?: string | null + org_id?: string + old_record?: Json | null + new_record?: Json | null + changed_fields?: string[] | null + } + Relationships: [ + { + foreignKeyName: "audit_logs_org_id_fkey" + columns: ["org_id"] + isOneToOne: false + referencedRelation: "orgs" + referencedColumns: ["id"] + }, + { + foreignKeyName: "audit_logs_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } apps: { Row: { app_id: string @@ -444,6 +498,33 @@ export type Database = { }, ] } + cache_entry: { + Row: { + cached_at: string | null + end_date: string | null + id: number | null + org_id: string | null + response: Json | null + start_date: string | null + } + Insert: { + cached_at?: string | null + end_date?: string | null + id?: number | null + org_id?: string | null + response?: Json | null + start_date?: string | null + } + Update: { + cached_at?: string | null + end_date?: string | null + id?: number | null + org_id?: string | null + response?: Json | null + start_date?: string | null + } + Relationships: [] + } capgo_credits_steps: { Row: { created_at: string diff --git a/supabase/functions/_backend/public/organization/audit.ts b/supabase/functions/_backend/public/organization/audit.ts new file mode 100644 index 0000000000..b6beae6347 --- /dev/null +++ b/supabase/functions/_backend/public/organization/audit.ts @@ -0,0 +1,81 @@ +import type { Context } from 'hono' +import type { Database } from '../../utils/supabase.types.ts' +import { z } from 'zod/mini' +import { simpleError } from '../../utils/hono.ts' +import { apikeyHasOrgRight, hasOrgRightApikey, supabaseApikey } from '../../utils/supabase.ts' + +const bodySchema = z.object({ + orgId: z.string(), + tableName: z.optional(z.string()), + operation: z.optional(z.string()), + page: z.optional(z.coerce.number()), + limit: z.optional(z.coerce.number()), +}) + +const auditLogSchema = z.object({ + id: z.number(), + created_at: z.string(), + table_name: z.string(), + record_id: z.string(), + operation: z.string(), + user_id: z.nullable(z.string()), + org_id: z.string(), + old_record: z.unknown(), + new_record: z.unknown(), + changed_fields: z.nullable(z.array(z.string())), +}) + +export async function getAuditLogs(c: Context, bodyRaw: any, apikey: Database['public']['Tables']['apikeys']['Row']): Promise { + const bodyParsed = bodySchema.safeParse(bodyRaw) + if (!bodyParsed.success) { + throw simpleError('invalid_body', 'Invalid body', { error: bodyParsed.error }) + } + const body = bodyParsed.data + + // Validate org access + if (!(await hasOrgRightApikey(c, body.orgId, apikey.user_id, 'read', c.get('capgkey') as string))) { + throw simpleError('invalid_org_id', 'You can\'t access this organization', { org_id: body.orgId }) + } + + if (!apikeyHasOrgRight(apikey, body.orgId)) { + throw simpleError('invalid_org_id', 'You can\'t access this organization', { org_id: body.orgId }) + } + + const limit = Math.min(body.limit ?? 50, 100) + const page = body.page ?? 0 + const from = page * limit + const to = (page + 1) * limit - 1 + + let query = supabaseApikey(c, c.get('capgkey') as string) + .from('audit_logs') + .select('*', { count: 'exact' }) + .eq('org_id', body.orgId) + .order('created_at', { ascending: false }) + .range(from, to) + + // Apply optional filters + if (body.tableName) { + query = query.eq('table_name', body.tableName) + } + if (body.operation) { + query = query.eq('operation', body.operation) + } + + const { data, error, count } = await query + + if (error) { + throw simpleError('cannot_get_audit_logs', 'Cannot get audit logs', { error }) + } + + const dataParsed = z.array(auditLogSchema).safeParse(data) + if (!dataParsed.success) { + throw simpleError('cannot_parse_audit_logs', 'Cannot parse audit logs', { error: dataParsed.error }) + } + + return c.json({ + data: dataParsed.data, + total: count ?? 0, + page, + limit, + }) +} diff --git a/supabase/functions/_backend/public/organization/index.ts b/supabase/functions/_backend/public/organization/index.ts index 0fe4e9f55f..6cf3a75c28 100644 --- a/supabase/functions/_backend/public/organization/index.ts +++ b/supabase/functions/_backend/public/organization/index.ts @@ -1,6 +1,7 @@ import type { Database } from '../../utils/supabase.types.ts' import { getBodyOrQuery, honoFactory } from '../../utils/hono.ts' import { middlewareKey } from '../../utils/hono_middleware.ts' +import { getAuditLogs } from './audit.ts' import { deleteOrg } from './delete.ts' import { get } from './get.ts' import { deleteMember } from './members/delete.ts' @@ -52,3 +53,9 @@ app.delete('/members', middlewareKey(['all', 'write', 'read', 'upload']), async const apikey = c.get('apikey') as Database['public']['Tables']['apikeys']['Row'] return deleteMember(c, body, apikey) }) + +app.get('/audit', middlewareKey(['all', 'write', 'read', 'upload']), async (c) => { + const body = await getBodyOrQuery(c) + const apikey = c.get('apikey') as Database['public']['Tables']['apikeys']['Row'] + return getAuditLogs(c, body, apikey) +}) diff --git a/supabase/functions/_backend/utils/supabase.types.ts b/supabase/functions/_backend/utils/supabase.types.ts index c094d53b34..5fbc08c371 100644 --- a/supabase/functions/_backend/utils/supabase.types.ts +++ b/supabase/functions/_backend/utils/supabase.types.ts @@ -238,6 +238,60 @@ export type Database = { }, ] } + audit_logs: { + Row: { + id: number + created_at: string + table_name: string + record_id: string + operation: string + user_id: string | null + org_id: string + old_record: Json | null + new_record: Json | null + changed_fields: string[] | null + } + Insert: { + id?: number + created_at?: string + table_name: string + record_id: string + operation: string + user_id?: string | null + org_id: string + old_record?: Json | null + new_record?: Json | null + changed_fields?: string[] | null + } + Update: { + id?: number + created_at?: string + table_name?: string + record_id?: string + operation?: string + user_id?: string | null + org_id?: string + old_record?: Json | null + new_record?: Json | null + changed_fields?: string[] | null + } + Relationships: [ + { + foreignKeyName: "audit_logs_org_id_fkey" + columns: ["org_id"] + isOneToOne: false + referencedRelation: "orgs" + referencedColumns: ["id"] + }, + { + foreignKeyName: "audit_logs_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } apps: { Row: { app_id: string @@ -444,6 +498,33 @@ export type Database = { }, ] } + cache_entry: { + Row: { + cached_at: string | null + end_date: string | null + id: number | null + org_id: string | null + response: Json | null + start_date: string | null + } + Insert: { + cached_at?: string | null + end_date?: string | null + id?: number | null + org_id?: string | null + response?: Json | null + start_date?: string | null + } + Update: { + cached_at?: string | null + end_date?: string | null + id?: number | null + org_id?: string | null + response?: Json | null + start_date?: string | null + } + Relationships: [] + } capgo_credits_steps: { Row: { created_at: string diff --git a/supabase/migrations/20250530233128_base.sql b/supabase/migrations/20250530233128_base.sql index 0e6504c7c7..bb66d813ff 100644 --- a/supabase/migrations/20250530233128_base.sql +++ b/supabase/migrations/20250530233128_base.sql @@ -4656,6 +4656,23 @@ WITH ) ); +-- SELECT +CREATE POLICY "Allow member and owner to select" ON "public"."org_users" FOR +SELECT + TO "authenticated", + "anon" USING ( + "public"."is_member_of_org" ( + ( + SELECT + "public"."get_identity_org_allowed" ( + '{read,upload,write,all}'::"public"."key_mode" [], + "org_users"."org_id" + ) AS "get_identity_org_allowed" + ), + "org_id" + ) + ); + -- UPDATE CREATE POLICY "Allow org admin to update" ON "public"."org_users" FOR UPDATE diff --git a/supabase/migrations/20251226125240_audit_log.sql b/supabase/migrations/20251226125240_audit_log.sql new file mode 100644 index 0000000000..3c9253c6b8 --- /dev/null +++ b/supabase/migrations/20251226125240_audit_log.sql @@ -0,0 +1,571 @@ +-- Audit Log Table for tracking CRUD operations +-- Tables tracked: orgs, apps, channels, app_versions, org_users + +-- Create the audit_logs table +CREATE TABLE IF NOT EXISTS "public"."audit_logs" ( + "id" BIGSERIAL PRIMARY KEY, + "created_at" TIMESTAMPTZ DEFAULT now() NOT NULL, + "table_name" TEXT NOT NULL, + "record_id" TEXT NOT NULL, + "operation" TEXT NOT NULL, + "user_id" UUID, + "org_id" UUID NOT NULL, + "old_record" JSONB, + "new_record" JSONB, + "changed_fields" TEXT[] +); + +-- Add comments +COMMENT ON TABLE "public"."audit_logs" IS 'Audit log for tracking changes to orgs, apps, channels, app_versions, and org_users tables'; +COMMENT ON COLUMN "public"."audit_logs"."table_name" IS 'Name of the table that was modified (orgs, apps, channels, app_versions, org_users)'; +COMMENT ON COLUMN "public"."audit_logs"."record_id" IS 'Primary key of the affected record'; +COMMENT ON COLUMN "public"."audit_logs"."operation" IS 'Type of operation: INSERT, UPDATE, or DELETE'; +COMMENT ON COLUMN "public"."audit_logs"."user_id" IS 'User who made the change (from auth.uid() or API key)'; +COMMENT ON COLUMN "public"."audit_logs"."org_id" IS 'Organization context for filtering'; +COMMENT ON COLUMN "public"."audit_logs"."old_record" IS 'Previous state of the record (null for INSERT)'; +COMMENT ON COLUMN "public"."audit_logs"."new_record" IS 'New state of the record (null for DELETE)'; +COMMENT ON COLUMN "public"."audit_logs"."changed_fields" IS 'Array of field names that changed (for UPDATE operations)'; + +-- Add foreign key constraints for referential integrity +ALTER TABLE "public"."audit_logs" + ADD CONSTRAINT audit_logs_org_id_fkey + FOREIGN KEY ("org_id") REFERENCES "public"."orgs"("id") + ON DELETE CASCADE; + +ALTER TABLE "public"."audit_logs" + ADD CONSTRAINT audit_logs_user_id_fkey + FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") + ON DELETE SET NULL; + +-- Create indexes for efficient querying +CREATE INDEX idx_audit_logs_org_id ON "public"."audit_logs"("org_id"); +CREATE INDEX idx_audit_logs_table_name ON "public"."audit_logs"("table_name"); +CREATE INDEX idx_audit_logs_user_id ON "public"."audit_logs"("user_id"); +CREATE INDEX idx_audit_logs_created_at ON "public"."audit_logs"("created_at" DESC); +CREATE INDEX idx_audit_logs_org_created ON "public"."audit_logs"("org_id", "created_at" DESC); +CREATE INDEX idx_audit_logs_operation ON "public"."audit_logs"("operation"); + +-- Create the audit trigger function +CREATE OR REPLACE FUNCTION "public"."audit_log_trigger"() +RETURNS TRIGGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +DECLARE + v_old_record JSONB; + v_new_record JSONB; + v_changed_fields TEXT[]; + v_org_id UUID; + v_record_id TEXT; + v_user_id UUID; + v_key TEXT; + v_org_exists BOOLEAN; +BEGIN + -- Skip audit logging for org DELETE operations + -- When an org is deleted, we can't insert into audit_logs because the org_id + -- foreign key would reference a non-existent org + IF TG_TABLE_NAME = 'orgs' AND TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; + + -- Get current user from auth context or API key + -- Uses get_identity() to support both JWT auth and API key authentication + v_user_id := public.get_identity(); + + -- Skip audit logging if no user is identified + -- We only want to log actions performed by authenticated users + IF v_user_id IS NULL THEN + RETURN COALESCE(NEW, OLD); + END IF; + + -- Convert records to JSONB based on operation type + IF TG_OP = 'DELETE' THEN + v_old_record := to_jsonb(OLD); + v_new_record := NULL; + ELSIF TG_OP = 'INSERT' THEN + v_old_record := NULL; + v_new_record := to_jsonb(NEW); + ELSE -- UPDATE + v_old_record := to_jsonb(OLD); + v_new_record := to_jsonb(NEW); + + -- Calculate changed fields by comparing old and new values + FOR v_key IN SELECT jsonb_object_keys(v_new_record) + LOOP + IF v_old_record->v_key IS DISTINCT FROM v_new_record->v_key THEN + v_changed_fields := array_append(v_changed_fields, v_key); + END IF; + END LOOP; + END IF; + + -- Get org_id and record_id based on table being modified + CASE TG_TABLE_NAME + WHEN 'orgs' THEN + v_org_id := COALESCE(NEW.id, OLD.id); + v_record_id := COALESCE(NEW.id, OLD.id)::TEXT; + WHEN 'apps' THEN + v_org_id := COALESCE(NEW.owner_org, OLD.owner_org); + v_record_id := COALESCE(NEW.app_id, OLD.app_id)::TEXT; + WHEN 'channels' THEN + v_org_id := COALESCE(NEW.owner_org, OLD.owner_org); + v_record_id := COALESCE(NEW.id, OLD.id)::TEXT; + WHEN 'app_versions' THEN + v_org_id := COALESCE(NEW.owner_org, OLD.owner_org); + v_record_id := COALESCE(NEW.id, OLD.id)::TEXT; + WHEN 'org_users' THEN + v_org_id := COALESCE(NEW.org_id, OLD.org_id); + v_record_id := COALESCE(NEW.id, OLD.id)::TEXT; + ELSE + -- Fallback for any other table (shouldn't happen with current triggers) + v_org_id := NULL; + v_record_id := NULL; + END CASE; + + -- Only insert if we have a valid org_id and the org still exists + -- This handles edge cases where related tables are deleted after the org + IF v_org_id IS NOT NULL THEN + -- Check if the org still exists (important for DELETE operations on child tables) + SELECT EXISTS(SELECT 1 FROM public.orgs WHERE id = v_org_id) INTO v_org_exists; + + IF v_org_exists THEN + INSERT INTO "public"."audit_logs" ( + table_name, record_id, operation, user_id, org_id, + old_record, new_record, changed_fields + ) VALUES ( + TG_TABLE_NAME, v_record_id, TG_OP, v_user_id, v_org_id, + v_old_record, v_new_record, v_changed_fields + ); + END IF; + END IF; + + RETURN COALESCE(NEW, OLD); +END; +$$; + +-- Attach triggers to tracked tables + +-- Orgs audit trigger +CREATE TRIGGER audit_orgs_trigger + AFTER INSERT OR UPDATE OR DELETE ON "public"."orgs" + FOR EACH ROW EXECUTE FUNCTION "public"."audit_log_trigger"(); + +-- Channels audit trigger +CREATE TRIGGER audit_channels_trigger + AFTER INSERT OR UPDATE OR DELETE ON "public"."channels" + FOR EACH ROW EXECUTE FUNCTION "public"."audit_log_trigger"(); + +-- App versions audit trigger +CREATE TRIGGER audit_app_versions_trigger + AFTER INSERT OR UPDATE OR DELETE ON "public"."app_versions" + FOR EACH ROW EXECUTE FUNCTION "public"."audit_log_trigger"(); + +-- Org users audit trigger +CREATE TRIGGER audit_org_users_trigger + AFTER INSERT OR UPDATE OR DELETE ON "public"."org_users" + FOR EACH ROW EXECUTE FUNCTION "public"."audit_log_trigger"(); + +-- Apps audit trigger +CREATE TRIGGER audit_apps_trigger + AFTER INSERT OR UPDATE OR DELETE ON "public"."apps" + FOR EACH ROW EXECUTE FUNCTION "public"."audit_log_trigger"(); + +-- Enable Row Level Security +ALTER TABLE "public"."audit_logs" ENABLE ROW LEVEL SECURITY; + +-- RLS Policy: Only super_admins can view audit logs for their organizations +CREATE POLICY "Allow select for auth, api keys (super_admin+)" ON "public"."audit_logs" FOR +SELECT + TO "authenticated", + "anon" USING ( + "public"."check_min_rights" ( + 'super_admin'::"public"."user_min_right", + "public"."get_identity_org_allowed" ( + '{read,upload,write,all}'::"public"."key_mode" [], + "org_id" + ), + "org_id", + NULL::character varying, + NULL::bigint + ) + ); + +-- No INSERT/UPDATE/DELETE policies - only triggers can write to this table + +-- Cleanup function for 90-day retention +CREATE OR REPLACE FUNCTION "public"."cleanup_old_audit_logs"() +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +BEGIN + DELETE FROM "public"."audit_logs" + WHERE created_at < NOW() - INTERVAL '90 days'; +END; +$$; + +-- Update delete_accounts_marked_for_deletion to transfer audit_logs ownership +-- This ensures audit log entries are transferred to another super_admin instead of being orphaned +CREATE OR REPLACE FUNCTION "public"."delete_accounts_marked_for_deletion" () +RETURNS TABLE (deleted_count INTEGER, deleted_user_ids UUID[]) +LANGUAGE "plpgsql" +SECURITY DEFINER +SET search_path = '' +AS $$ +DECLARE + account_record RECORD; + org_record RECORD; + deleted_users UUID[] := ARRAY[]::UUID[]; + total_deleted INTEGER := 0; + other_super_admins_count INTEGER; + replacement_owner_id UUID; +BEGIN + -- Loop through all accounts marked for deletion where removal_date has passed + FOR account_record IN + SELECT "account_id", "removal_date", "removed_data" + FROM "public"."to_delete_accounts" + WHERE "removal_date" < NOW() + LOOP + BEGIN + -- Process each org the user belongs to + FOR org_record IN + SELECT DISTINCT "org_id", "user_right" + FROM "public"."org_users" + WHERE "user_id" = account_record.account_id + LOOP + -- Reset replacement_owner_id for each org + replacement_owner_id := NULL; + + -- Check if user is a super_admin in this org + IF org_record.user_right = 'super_admin'::"public"."user_min_right" THEN + -- Count other super_admins in this org (excluding the user being deleted) + SELECT COUNT(*) INTO other_super_admins_count + FROM "public"."org_users" + WHERE "org_id" = org_record.org_id + AND "user_id" != account_record.account_id + AND "user_right" = 'super_admin'::"public"."user_min_right"; + + IF other_super_admins_count = 0 THEN + -- User is the last super_admin: DELETE all org resources + RAISE NOTICE 'User % is last super_admin of org %. Deleting all org resources.', + account_record.account_id, org_record.org_id; + + -- Delete deploy_history for this org + DELETE FROM "public"."deploy_history" WHERE "owner_org" = org_record.org_id; + + -- Delete channel_devices for this org + DELETE FROM "public"."channel_devices" WHERE "owner_org" = org_record.org_id; + + -- Delete channels for this org + DELETE FROM "public"."channels" WHERE "owner_org" = org_record.org_id; + + -- Delete app_versions for this org + DELETE FROM "public"."app_versions" WHERE "owner_org" = org_record.org_id; + + -- Delete apps for this org + DELETE FROM "public"."apps" WHERE "owner_org" = org_record.org_id; + + -- Delete the org itself since user is last super_admin + -- Note: audit_logs will be cascade deleted with the org + DELETE FROM "public"."orgs" WHERE "id" = org_record.org_id; + + -- Skip ownership transfer since all resources are deleted + CONTINUE; + END IF; + END IF; + + -- If we reach here, we need to transfer ownership (either non-super_admin or non-last super_admin) + -- Find a super_admin to transfer ownership to + SELECT "user_id" INTO replacement_owner_id + FROM "public"."org_users" + WHERE "org_id" = org_record.org_id + AND "user_id" != account_record.account_id + AND "user_right" = 'super_admin'::"public"."user_min_right" + LIMIT 1; + + IF replacement_owner_id IS NOT NULL THEN + RAISE NOTICE 'Transferring ownership from user % to user % in org %', + account_record.account_id, replacement_owner_id, org_record.org_id; + + -- Transfer app ownership + UPDATE "public"."apps" + SET "user_id" = replacement_owner_id, "updated_at" = NOW() + WHERE "user_id" = account_record.account_id AND "owner_org" = org_record.org_id; + + -- Transfer app_versions ownership + UPDATE "public"."app_versions" + SET "user_id" = replacement_owner_id, "updated_at" = NOW() + WHERE "user_id" = account_record.account_id AND "owner_org" = org_record.org_id; + + -- Transfer channels ownership + UPDATE "public"."channels" + SET "created_by" = replacement_owner_id, "updated_at" = NOW() + WHERE "created_by" = account_record.account_id AND "owner_org" = org_record.org_id; + + -- Transfer deploy_history ownership + UPDATE "public"."deploy_history" + SET "created_by" = replacement_owner_id, "updated_at" = NOW() + WHERE "created_by" = account_record.account_id AND "owner_org" = org_record.org_id; + + -- Transfer org ownership if user created it + UPDATE "public"."orgs" + SET "created_by" = replacement_owner_id, "updated_at" = NOW() + WHERE "id" = org_record.org_id AND "created_by" = account_record.account_id; + + -- Transfer audit_logs ownership + UPDATE "public"."audit_logs" + SET "user_id" = replacement_owner_id + WHERE "user_id" = account_record.account_id AND "org_id" = org_record.org_id; + ELSE + RAISE WARNING 'No super_admin found to transfer ownership in org % for user %', + org_record.org_id, account_record.account_id; + END IF; + END LOOP; + + -- Delete from public.users table + DELETE FROM "public"."users" WHERE "id" = account_record.account_id; + + -- Delete from auth.users table + DELETE FROM "auth"."users" WHERE "id" = account_record.account_id; + + -- Remove from to_delete_accounts table + DELETE FROM "public"."to_delete_accounts" WHERE "account_id" = account_record.account_id; + + -- Track the deleted user + deleted_users := "array_append"(deleted_users, account_record.account_id); + total_deleted := total_deleted + 1; + + -- Log the deletion + RAISE NOTICE 'Successfully deleted account: % (removal date: %)', + account_record.account_id, account_record.removal_date; + + EXCEPTION + WHEN OTHERS THEN + -- Log the error but continue with other accounts + RAISE WARNING 'Failed to delete account %: %', account_record.account_id, SQLERRM; + END; + END LOOP; + + -- Return results + deleted_count := total_deleted; + deleted_user_ids := deleted_users; + RETURN NEXT; + + RAISE NOTICE 'Deletion process completed. Total accounts deleted: %', total_deleted; +END; +$$; + +-- Ensure permissions remain the same (only service_role and postgres can execute) +REVOKE ALL ON FUNCTION "public"."delete_accounts_marked_for_deletion" () FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."delete_accounts_marked_for_deletion" () FROM anon; +REVOKE ALL ON FUNCTION "public"."delete_accounts_marked_for_deletion" () FROM authenticated; + +GRANT EXECUTE ON FUNCTION "public"."delete_accounts_marked_for_deletion" () TO postgres; +GRANT EXECUTE ON FUNCTION "public"."delete_accounts_marked_for_deletion" () TO service_role; + +-- Update process_all_cron_tasks to include audit log cleanup at 3 AM UTC +-- Per AGENTS.md, we don't create new cron jobs but add to the existing consolidated function +CREATE OR REPLACE FUNCTION public.process_all_cron_tasks () RETURNS void LANGUAGE plpgsql +SET + search_path = '' AS $$ +DECLARE + current_hour int; + current_minute int; + current_second int; +BEGIN + -- Get current time components in UTC + current_hour := EXTRACT(HOUR FROM now()); + current_minute := EXTRACT(MINUTE FROM now()); + current_second := EXTRACT(SECOND FROM now()); + + -- Every 10 seconds: High-frequency queues (at :00, :10, :20, :30, :40, :50) + IF current_second % 10 = 0 THEN + -- Process high-frequency queues with default batch size (950) + BEGIN + PERFORM public.process_function_queue(ARRAY['on_channel_update', 'on_user_create', 'on_user_update', 'on_version_delete', 'on_version_update', 'on_app_delete', 'on_organization_create', 'on_user_delete', 'on_app_create']); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_function_queue (high-frequency) failed: %', SQLERRM; + END; + + -- Process channel device counts with batch size 1000 + BEGIN + PERFORM public.process_channel_device_counts_queue(1000); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_channel_device_counts_queue failed: %', SQLERRM; + END; + + -- Process manifest bundle counts with batch size 1000 + BEGIN + PERFORM public.process_manifest_bundle_counts_queue(1000); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_manifest_bundle_counts_queue failed: %', SQLERRM; + END; + END IF; + + -- Every minute (at :00 seconds): Per-minute tasks + IF current_second = 0 THEN + BEGIN + PERFORM public.delete_accounts_marked_for_deletion(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'delete_accounts_marked_for_deletion failed: %', SQLERRM; + END; + + -- Process with batch size 10 + BEGIN + PERFORM public.process_function_queue(ARRAY['cron_sync_sub', 'cron_stat_app'], 10); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_function_queue (per-minute) failed: %', SQLERRM; + END; + + -- on_manifest_create uses default batch size + BEGIN + PERFORM public.process_function_queue(ARRAY['on_manifest_create']); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_function_queue (manifest_create) failed: %', SQLERRM; + END; + END IF; + + -- Every 5 minutes (at :00 seconds): Org stats with batch size 10 + IF current_minute % 5 = 0 AND current_second = 0 THEN + BEGIN + PERFORM public.process_function_queue(ARRAY['cron_stat_org'], 10); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_function_queue (cron_stat_org) failed: %', SQLERRM; + END; + END IF; + + -- Every hour (at :00:00): Hourly cleanup + IF current_minute = 0 AND current_second = 0 THEN + BEGIN + PERFORM public.cleanup_frequent_job_details(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'cleanup_frequent_job_details failed: %', SQLERRM; + END; + END IF; + + -- Every 2 hours (at :00:00): Low-frequency queues with default batch size + IF current_hour % 2 = 0 AND current_minute = 0 AND current_second = 0 THEN + BEGIN + PERFORM public.process_function_queue(ARRAY['admin_stats', 'cron_email', 'on_version_create', 'on_organization_delete', 'on_deploy_history_create', 'cron_clear_versions']); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_function_queue (low-frequency) failed: %', SQLERRM; + END; + END IF; + + -- Every 6 hours (at :00:00): Stats jobs + IF current_hour % 6 = 0 AND current_minute = 0 AND current_second = 0 THEN + BEGIN + PERFORM public.process_cron_stats_jobs(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_cron_stats_jobs failed: %', SQLERRM; + END; + END IF; + + -- Daily at 00:00:00 - Midnight tasks + IF current_hour = 0 AND current_minute = 0 AND current_second = 0 THEN + BEGIN + PERFORM public.cleanup_queue_messages(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'cleanup_queue_messages failed: %', SQLERRM; + END; + + BEGIN + PERFORM public.delete_old_deleted_apps(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'delete_old_deleted_apps failed: %', SQLERRM; + END; + + BEGIN + PERFORM public.remove_old_jobs(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'remove_old_jobs failed: %', SQLERRM; + END; + END IF; + + -- Daily at 00:40:00 - Old app version retention + IF current_hour = 0 AND current_minute = 40 AND current_second = 0 THEN + BEGIN + PERFORM public.update_app_versions_retention(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'update_app_versions_retention failed: %', SQLERRM; + END; + END IF; + + -- Daily at 01:01:00 - Admin stats creation + IF current_hour = 1 AND current_minute = 1 AND current_second = 0 THEN + BEGIN + PERFORM public.process_admin_stats(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_admin_stats failed: %', SQLERRM; + END; + END IF; + + -- Daily at 03:00:00 - Free trial, credits, and audit log cleanup + IF current_hour = 3 AND current_minute = 0 AND current_second = 0 THEN + BEGIN + PERFORM public.process_free_trial_expired(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_free_trial_expired failed: %', SQLERRM; + END; + + BEGIN + PERFORM public.expire_usage_credits(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'expire_usage_credits failed: %', SQLERRM; + END; + + -- Cleanup old audit logs (90-day retention) + BEGIN + PERFORM public.cleanup_old_audit_logs(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'cleanup_old_audit_logs failed: %', SQLERRM; + END; + END IF; + + -- Daily at 04:00:00 - Sync sub scheduler + IF current_hour = 4 AND current_minute = 0 AND current_second = 0 THEN + BEGIN + PERFORM public.process_cron_sync_sub_jobs(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_cron_sync_sub_jobs failed: %', SQLERRM; + END; + END IF; + + -- Daily at 12:00:00 - Noon tasks + IF current_hour = 12 AND current_minute = 0 AND current_second = 0 THEN + BEGIN + DELETE FROM cron.job_run_details WHERE end_time < now() - interval '7 days'; + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'cleanup job_run_details failed: %', SQLERRM; + END; + + -- Weekly stats email (every Saturday at noon) + IF EXTRACT(DOW FROM now()) = 6 THEN + BEGIN + PERFORM public.process_stats_email_weekly(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_stats_email_weekly failed: %', SQLERRM; + END; + END IF; + + -- Monthly stats email (1st of month at noon) + IF EXTRACT(DAY FROM now()) = 1 THEN + BEGIN + PERFORM public.process_stats_email_monthly(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_stats_email_monthly failed: %', SQLERRM; + END; + END IF; + + -- Production deploy/install stats email (1st of month at noon) + IF EXTRACT(DAY FROM now()) = 1 THEN + BEGIN + PERFORM public.process_production_deploy_install_stats_email(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_production_deploy_install_stats_email failed: %', SQLERRM; + END; + END IF; + END IF; +END; +$$; diff --git a/supabase/schemas/prod.sql b/supabase/schemas/prod.sql index 1ef8346e09..6929a757eb 100644 --- a/supabase/schemas/prod.sql +++ b/supabase/schemas/prod.sql @@ -194,9 +194,7 @@ CREATE TYPE "public"."stats_action" AS ENUM( 'disableAutoUpdateMetadata', 'disableAutoUpdateUnderNative', 'disableDevBuild', - 'disableProdBuild', 'disableEmulator', - 'disableDevice', 'cannotGetBundle', 'checksum_fail', 'NoChannelOrOverride', @@ -216,7 +214,9 @@ CREATE TYPE "public"."stats_action" AS ENUM( 'download_manifest_checksum_fail', 'download_manifest_brotli_fail', 'backend_refusal', - 'download_0' + 'download_0', + 'disableProdBuild', + 'disableDevice' ); ALTER TYPE "public"."stats_action" OWNER TO "postgres"; @@ -320,22 +320,119 @@ DECLARE v_use numeric; v_balance numeric; v_overage_paid numeric := 0; - v_existing_credits numeric := 0; + v_existing_credits_debited numeric := 0; v_required numeric := 0; v_credits_to_apply numeric := 0; + v_credits_available numeric := 0; + v_latest_event_id uuid; + v_latest_overage_amount numeric; + v_needs_new_record boolean := false; grant_rec public.usage_credit_grants%ROWTYPE; BEGIN + -- Early exit for invalid input IF p_overage_amount IS NULL OR p_overage_amount <= 0 THEN RETURN QUERY SELECT 0::numeric, 0::numeric, 0::numeric, 0::numeric, NULL::bigint, 0::numeric, 0::numeric, NULL::uuid; RETURN; END IF; + -- Calculate credit cost for this overage SELECT * INTO v_calc FROM public.calculate_credit_cost(p_metric, p_overage_amount) LIMIT 1; + -- If no pricing step found, create a single record and exit IF v_calc.credit_step_id IS NULL THEN + -- Check if we already have a record for this cycle with NULL step + SELECT uoe.id, uoe.overage_amount INTO v_latest_event_id, v_latest_overage_amount + FROM public.usage_overage_events uoe + WHERE uoe.org_id = p_org_id + AND uoe.metric = p_metric + AND uoe.credit_step_id IS NULL + AND (uoe.billing_cycle_start IS NOT DISTINCT FROM p_billing_cycle_start::date) + AND (uoe.billing_cycle_end IS NOT DISTINCT FROM p_billing_cycle_end::date) + ORDER BY uoe.created_at DESC + LIMIT 1; + + -- Only create new record if overage amount changed significantly (more than 1% or first record) + IF v_latest_event_id IS NULL OR ABS(v_latest_overage_amount - p_overage_amount) / NULLIF(v_latest_overage_amount, 0) > 0.01 THEN + INSERT INTO public.usage_overage_events ( + org_id, + metric, + overage_amount, + credits_estimated, + credits_debited, + credit_step_id, + billing_cycle_start, + billing_cycle_end, + details + ) + VALUES ( + p_org_id, + p_metric, + p_overage_amount, + 0, + 0, + NULL, + p_billing_cycle_start, + p_billing_cycle_end, + p_details + ) + RETURNING id INTO v_event_id; + ELSE + -- Reuse existing event + v_event_id := v_latest_event_id; + END IF; + + RETURN QUERY SELECT p_overage_amount, 0::numeric, 0::numeric, 0::numeric, NULL::bigint, 0::numeric, p_overage_amount, v_event_id; + RETURN; + END IF; + + v_per_unit := v_calc.credit_cost_per_unit; + v_required := v_calc.credits_required; + + -- Get the most recent event for this cycle + SELECT uoe.id, uoe.overage_amount + INTO v_latest_event_id, v_latest_overage_amount + FROM public.usage_overage_events uoe + WHERE uoe.org_id = p_org_id + AND uoe.metric = p_metric + AND (uoe.billing_cycle_start IS NOT DISTINCT FROM p_billing_cycle_start::date) + AND (uoe.billing_cycle_end IS NOT DISTINCT FROM p_billing_cycle_end::date) + ORDER BY uoe.created_at DESC + LIMIT 1; + + -- Calculate how many credits we can still try to apply + -- Use credits_debited for this since it reflects actual consumption + SELECT COALESCE(SUM(credits_debited), 0) + INTO v_existing_credits_debited + FROM public.usage_overage_events + WHERE org_id = p_org_id + AND metric = p_metric + AND (billing_cycle_start IS NOT DISTINCT FROM p_billing_cycle_start::date) + AND (billing_cycle_end IS NOT DISTINCT FROM p_billing_cycle_end::date); + + v_credits_to_apply := GREATEST(v_required - v_existing_credits_debited, 0); + v_remaining := v_credits_to_apply; + + -- Check if there are any credits available in grants + SELECT COALESCE(SUM(GREATEST(credits_total - credits_consumed, 0)), 0) + INTO v_credits_available + FROM public.usage_credit_grants + WHERE org_id = p_org_id + AND expires_at >= now(); + + -- Determine if we need a new record: + -- 1. No existing record for this cycle (first overage) + -- 2. Overage amount changed significantly (more than 1%) + -- 3. We have NEW credits available AND we need to apply them + v_needs_new_record := v_latest_event_id IS NULL + OR (v_latest_overage_amount IS NOT NULL + AND ABS(v_latest_overage_amount - p_overage_amount) / NULLIF(v_latest_overage_amount, 0) > 0.01) + OR (v_credits_to_apply > 0 AND v_credits_available > 0 AND v_existing_credits_debited = 0); + + -- Only create new record if needed + IF v_needs_new_record THEN INSERT INTO public.usage_overage_events ( org_id, metric, @@ -351,130 +448,97 @@ BEGIN p_org_id, p_metric, p_overage_amount, + v_required, 0, - 0, - NULL, + v_calc.credit_step_id, p_billing_cycle_start, p_billing_cycle_end, p_details ) RETURNING id INTO v_event_id; - RETURN QUERY SELECT p_overage_amount, 0::numeric, 0::numeric, 0::numeric, NULL::bigint, 0::numeric, p_overage_amount, v_event_id; - RETURN; - END IF; + -- Apply credits from available grants if any + IF v_credits_to_apply > 0 THEN + FOR grant_rec IN + SELECT * + FROM public.usage_credit_grants + WHERE org_id = p_org_id + AND expires_at >= now() + AND credits_consumed < credits_total + ORDER BY expires_at ASC, granted_at ASC + FOR UPDATE + LOOP + EXIT WHEN v_remaining <= 0; - v_per_unit := v_calc.credit_cost_per_unit; - v_required := v_calc.credits_required; + v_available := grant_rec.credits_total - grant_rec.credits_consumed; + IF v_available <= 0 THEN + CONTINUE; + END IF; - SELECT COALESCE(SUM(credits_debited), 0) - INTO v_existing_credits - FROM public.usage_overage_events - WHERE org_id = p_org_id - AND metric = p_metric - AND (billing_cycle_start IS NOT DISTINCT FROM p_billing_cycle_start::date) - AND (billing_cycle_end IS NOT DISTINCT FROM p_billing_cycle_end::date); + v_use := LEAST(v_available, v_remaining); + v_remaining := v_remaining - v_use; + v_applied := v_applied + v_use; - v_credits_to_apply := GREATEST(v_required - v_existing_credits, 0); - v_remaining := v_credits_to_apply; + UPDATE public.usage_credit_grants + SET credits_consumed = credits_consumed + v_use + WHERE id = grant_rec.id; - INSERT INTO public.usage_overage_events ( - org_id, - metric, - overage_amount, - credits_estimated, - credits_debited, - credit_step_id, - billing_cycle_start, - billing_cycle_end, - details - ) - VALUES ( - p_org_id, - p_metric, - p_overage_amount, - v_required, - 0, - v_calc.credit_step_id, - p_billing_cycle_start, - p_billing_cycle_end, - p_details - ) - RETURNING id INTO v_event_id; + INSERT INTO public.usage_credit_consumptions ( + grant_id, + org_id, + overage_event_id, + metric, + credits_used + ) + VALUES ( + grant_rec.id, + p_org_id, + v_event_id, + p_metric, + v_use + ); - FOR grant_rec IN - SELECT * - FROM public.usage_credit_grants - WHERE org_id = p_org_id - AND expires_at >= now() - AND credits_consumed < credits_total - ORDER BY expires_at ASC, granted_at ASC - FOR UPDATE - LOOP - EXIT WHEN v_remaining <= 0; + SELECT COALESCE(SUM(GREATEST(credits_total - credits_consumed, 0)), 0) + INTO v_balance + FROM public.usage_credit_grants + WHERE org_id = p_org_id + AND expires_at >= now(); + + INSERT INTO public.usage_credit_transactions ( + org_id, + grant_id, + transaction_type, + amount, + balance_after, + occurred_at, + description, + source_ref + ) + VALUES ( + p_org_id, + grant_rec.id, + 'deduction', + -v_use, + v_balance, + now(), + format('Overage deduction for %s usage', p_metric::text), + jsonb_build_object('overage_event_id', v_event_id, 'metric', p_metric::text) + ); + END LOOP; - v_available := grant_rec.credits_total - grant_rec.credits_consumed; - IF v_available <= 0 THEN - CONTINUE; + -- Update the event with actual credits applied + UPDATE public.usage_overage_events + SET credits_debited = v_applied + WHERE id = v_event_id; END IF; + ELSE + -- Reuse latest event ID, no new record needed + v_event_id := v_latest_event_id; + END IF; - v_use := LEAST(v_available, v_remaining); - v_remaining := v_remaining - v_use; - v_applied := v_applied + v_use; - - UPDATE public.usage_credit_grants - SET credits_consumed = credits_consumed + v_use - WHERE id = grant_rec.id; - - INSERT INTO public.usage_credit_consumptions ( - grant_id, - org_id, - overage_event_id, - metric, - credits_used - ) - VALUES ( - grant_rec.id, - p_org_id, - v_event_id, - p_metric, - v_use - ); - - SELECT COALESCE(SUM(GREATEST(credits_total - credits_consumed, 0)), 0) - INTO v_balance - FROM public.usage_credit_grants - WHERE org_id = p_org_id - AND expires_at >= now(); - - INSERT INTO public.usage_credit_transactions ( - org_id, - grant_id, - transaction_type, - amount, - balance_after, - occurred_at, - description, - source_ref - ) - VALUES ( - p_org_id, - grant_rec.id, - 'deduction', - -v_use, - v_balance, - now(), - format('Overage deduction for %s usage', p_metric::text), - jsonb_build_object('overage_event_id', v_event_id, 'metric', p_metric::text) - ); - END LOOP; - - UPDATE public.usage_overage_events - SET credits_debited = v_applied - WHERE id = v_event_id; - + -- Calculate how much overage is covered by credits IF v_per_unit > 0 THEN - v_overage_paid := LEAST(p_overage_amount, (v_applied + v_existing_credits) / v_per_unit); + v_overage_paid := LEAST(p_overage_amount, (v_applied + v_existing_credits_debited) / v_per_unit); ELSE v_overage_paid := p_overage_amount; END IF; @@ -483,7 +547,7 @@ BEGIN p_overage_amount, v_required, v_applied, - GREATEST(v_required - v_existing_credits - v_applied, 0), + GREATEST(v_required - v_existing_credits_debited - v_applied, 0), v_calc.credit_step_id, v_overage_paid, GREATEST(p_overage_amount - v_overage_paid, 0), @@ -681,12 +745,24 @@ SET "search_path" TO '' AS $$ DECLARE user_right_record RECORD; + org_enforcing_2fa boolean; BEGIN IF user_id IS NULL THEN PERFORM public.pg_log('deny: CHECK_MIN_RIGHTS_NO_UID', jsonb_build_object('org_id', org_id, 'app_id', app_id, 'channel_id', channel_id, 'min_right', min_right::text)); RETURN false; END IF; + -- Check if org has 2FA enforcement enabled + SELECT enforcing_2fa INTO org_enforcing_2fa + FROM public.orgs + WHERE public.orgs.id = check_min_rights.org_id; + + -- If org enforces 2FA and user doesn't have 2FA enabled, deny access + IF org_enforcing_2fa = true AND NOT public.has_2fa_enabled(user_id) THEN + PERFORM public.pg_log('deny: CHECK_MIN_RIGHTS_2FA_ENFORCEMENT', jsonb_build_object('org_id', org_id, 'app_id', app_id, 'channel_id', channel_id, 'min_right', min_right::text, 'user_id', user_id)); + RETURN false; + END IF; + FOR user_right_record IN SELECT org_users.user_right, org_users.app_id, org_users.channel_id FROM public.org_users @@ -713,6 +789,40 @@ ALTER FUNCTION "public"."check_min_rights" ( "channel_id" bigint ) OWNER TO "postgres"; +CREATE OR REPLACE FUNCTION "public"."check_org_members_2fa_enabled" ("org_id" "uuid") RETURNS TABLE ("user_id" "uuid", "2fa_enabled" boolean) LANGUAGE "plpgsql" SECURITY DEFINER +SET + "search_path" TO '' AS $$ +BEGIN + -- Check if org exists + IF NOT EXISTS (SELECT 1 FROM public.orgs WHERE public.orgs.id = check_org_members_2fa_enabled.org_id) THEN + RAISE EXCEPTION 'Organization does not exist'; + END IF; + + -- Check if the current user is a super_admin of the organization + IF NOT ( + public.check_min_rights( + 'super_admin'::public.user_min_right, + (SELECT public.get_identity_org_allowed('{read,upload,write,all}'::public.key_mode[], check_org_members_2fa_enabled.org_id)), + check_org_members_2fa_enabled.org_id, + NULL::character varying, + NULL::bigint + ) + ) THEN + RAISE EXCEPTION 'NO_RIGHTS'; + END IF; + + -- Return list of org members with their 2FA status + RETURN QUERY + SELECT + ou.user_id, + COALESCE(public.has_2fa_enabled(ou.user_id), false) AS "2fa_enabled" + FROM public.org_users ou + WHERE ou.org_id = check_org_members_2fa_enabled.org_id; +END; +$$; + +ALTER FUNCTION "public"."check_org_members_2fa_enabled" ("org_id" "uuid") OWNER TO "postgres"; + CREATE OR REPLACE FUNCTION "public"."check_org_user_privileges" () RETURNS "trigger" LANGUAGE "plpgsql" SET "search_path" TO '' AS $$BEGIN @@ -1572,26 +1682,63 @@ CREATE OR REPLACE FUNCTION "public"."get_app_metrics" ( "fail" bigint, "install" bigint, "uninstall" bigint -) LANGUAGE "plpgsql" STABLE SECURITY DEFINER +) LANGUAGE "plpgsql" SECURITY DEFINER SET "search_path" TO '' AS $$ +DECLARE + cache_entry public.app_metrics_cache%ROWTYPE; + org_exists boolean; BEGIN - RETURN QUERY - WITH DateSeries AS (SELECT generate_series(start_date, end_date, '1 day'::interval)::date AS "date"), - all_apps AS (SELECT apps.app_id FROM public.apps WHERE apps.owner_org = get_app_metrics.org_id - UNION SELECT deleted_apps.app_id FROM public.deleted_apps WHERE deleted_apps.owner_org = get_app_metrics.org_id) - SELECT aa.app_id, ds.date::date, COALESCE(dm.mau, 0) AS mau, COALESCE(dst.storage, 0) AS storage, - COALESCE(db.bandwidth, 0) AS bandwidth, COALESCE(dbt.build_time_unit, 0) AS build_time_unit, - COALESCE(SUM(dv.get)::bigint, 0) AS get, COALESCE(SUM(dv.fail)::bigint, 0) AS fail, - COALESCE(SUM(dv.install)::bigint, 0) AS install, COALESCE(SUM(dv.uninstall)::bigint, 0) AS uninstall - FROM all_apps aa CROSS JOIN DateSeries ds - LEFT JOIN public.daily_mau dm ON aa.app_id = dm.app_id AND ds.date = dm.date - LEFT JOIN public.daily_storage dst ON aa.app_id = dst.app_id AND ds.date = dst.date - LEFT JOIN public.daily_bandwidth db ON aa.app_id = db.app_id AND ds.date = db.date - LEFT JOIN public.daily_build_time dbt ON aa.app_id = dbt.app_id AND ds.date = dbt.date - LEFT JOIN public.daily_version dv ON aa.app_id = dv.app_id AND ds.date = dv.date - GROUP BY aa.app_id, ds.date, dm.mau, dst.storage, db.bandwidth, dbt.build_time_unit - ORDER BY aa.app_id, ds.date; + SELECT EXISTS ( + SELECT 1 FROM public.orgs WHERE id = get_app_metrics.org_id + ) INTO org_exists; + + IF NOT org_exists THEN + RETURN; + END IF; + + SELECT * + INTO cache_entry + FROM public.app_metrics_cache + WHERE app_metrics_cache.org_id = get_app_metrics.org_id; + + IF cache_entry.id IS NULL + OR cache_entry.start_date IS DISTINCT FROM get_app_metrics.start_date + OR cache_entry.end_date IS DISTINCT FROM get_app_metrics.end_date + OR cache_entry.cached_at IS NULL + OR cache_entry.cached_at < (now() - interval '5 minutes') THEN + cache_entry := public.seed_get_app_metrics_caches(get_app_metrics.org_id, get_app_metrics.start_date, get_app_metrics.end_date); + END IF; + + IF cache_entry.response IS NULL THEN + RETURN; + END IF; + + RETURN QUERY + SELECT + metrics.app_id, + metrics.date, + metrics.mau, + metrics.storage, + metrics.bandwidth, + metrics.build_time_unit, + metrics.get, + metrics.fail, + metrics.install, + metrics.uninstall + FROM jsonb_to_recordset(cache_entry.response) AS metrics( + app_id character varying, + date date, + mau bigint, + storage bigint, + bandwidth bigint, + build_time_unit bigint, + get bigint, + fail bigint, + install bigint, + uninstall bigint + ) + ORDER BY metrics.app_id, metrics.date; END; $$; @@ -2417,7 +2564,6 @@ BEGIN FROM public.apps GROUP BY owner_org ), - -- Compute next stats update info for all paying orgs at once paying_orgs_ordered AS ( SELECT o.id, @@ -2431,13 +2577,9 @@ BEGIN OR si.trial_at > now() ) ), - -- Calculate current billing cycle for each org (properly inlined get_cycle_info_org logic) - -- anchor_day = day of month when billing cycle starts (extracted from original subscription_anchor_start) - -- If we're before anchor_day this month, cycle started last month; otherwise cycle started this month billing_cycles AS ( SELECT o.id AS org_id, - -- Calculate cycle_start based on anchor day and current date CASE WHEN COALESCE(si.subscription_anchor_start - date_trunc('MONTH', si.subscription_anchor_start), '0 DAYS'::INTERVAL) > now() - date_trunc('MONTH', now()) @@ -2448,6 +2590,15 @@ BEGIN END AS cycle_start FROM public.orgs o LEFT JOIN public.stripe_info si ON o.customer_id = si.customer_id + ), + -- Calculate 2FA access status for user/org combinations + two_fa_access AS ( + SELECT + o.id AS org_id, + -- should_redact: true if org enforces 2FA and user doesn't have 2FA + (o.enforcing_2fa = true AND NOT public.has_2fa_enabled(userid)) AS should_redact + FROM public.orgs o + JOIN public.org_users ou ON ou.user_id = userid AND o.id = ou.org_id ) SELECT o.id AS gid, @@ -2455,24 +2606,44 @@ BEGIN o.logo, o.name, ou.user_right::varchar AS role, - -- is_paying_org: status = 'succeeded' - (si.status = 'succeeded') AS paying, - -- is_trial_org: days left in trial - GREATEST(COALESCE((si.trial_at::date - now()::date), 0), 0)::integer AS trial_left, - -- is_allowed_action_org (= is_paying_and_good_plan_org): paying with good plan OR in trial - ((si.status = 'succeeded' AND si.is_good_plan = true) OR (si.trial_at::date - now()::date > 0)) AS can_use_more, - -- is_canceled_org: status = 'canceled' - (si.status = 'canceled') AS is_canceled, - -- app_count - COALESCE(ac.cnt, 0) AS app_count, - -- subscription dates (properly calculated current billing cycle) - bc.cycle_start AS subscription_start, - (bc.cycle_start + INTERVAL '1 MONTH') AS subscription_end, - o.management_email, - -- is_org_yearly - COALESCE(si.price_id = p.price_y_id, false) AS is_yearly, + -- Redact sensitive fields if user doesn't have 2FA access + CASE + WHEN tfa.should_redact THEN false + ELSE (si.status = 'succeeded') + END AS paying, + CASE + WHEN tfa.should_redact THEN 0 + ELSE GREATEST(COALESCE((si.trial_at::date - now()::date), 0), 0)::integer + END AS trial_left, + CASE + WHEN tfa.should_redact THEN false + ELSE ((si.status = 'succeeded' AND si.is_good_plan = true) OR (si.trial_at::date - now()::date > 0)) + END AS can_use_more, + CASE + WHEN tfa.should_redact THEN false + ELSE (si.status = 'canceled') + END AS is_canceled, + CASE + WHEN tfa.should_redact THEN 0::bigint + ELSE COALESCE(ac.cnt, 0) + END AS app_count, + CASE + WHEN tfa.should_redact THEN NULL::timestamptz + ELSE bc.cycle_start + END AS subscription_start, + CASE + WHEN tfa.should_redact THEN NULL::timestamptz + ELSE (bc.cycle_start + INTERVAL '1 MONTH') + END AS subscription_end, + CASE + WHEN tfa.should_redact THEN NULL::text + ELSE o.management_email + END AS management_email, + CASE + WHEN tfa.should_redact THEN false + ELSE COALESCE(si.price_id = p.price_y_id, false) + END AS is_yearly, o.stats_updated_at, - -- get_next_stats_update_date (simplified - just add 4 min intervals based on position) CASE WHEN poo.id IS NOT NULL THEN public.get_next_cron_time('0 3 * * *', now()) + make_interval(mins => poo.preceding_count::int * 4) @@ -2483,6 +2654,7 @@ BEGIN ucb.next_expiration AS credit_next_expiration FROM public.orgs o JOIN public.org_users ou ON ou.user_id = userid AND o.id = ou.org_id + JOIN two_fa_access tfa ON tfa.org_id = o.id LEFT JOIN public.stripe_info si ON o.customer_id = si.customer_id LEFT JOIN public.plans p ON si.product_id = p.stripe_id LEFT JOIN app_counts ac ON ac.owner_org = o.id @@ -2494,6 +2666,217 @@ $$; ALTER FUNCTION "public"."get_orgs_v6" ("userid" "uuid") OWNER TO "postgres"; +CREATE OR REPLACE FUNCTION "public"."get_orgs_v7" () RETURNS TABLE ( + "gid" "uuid", + "created_by" "uuid", + "logo" "text", + "name" "text", + "role" character varying, + "paying" boolean, + "trial_left" integer, + "can_use_more" boolean, + "is_canceled" boolean, + "app_count" bigint, + "subscription_start" timestamp with time zone, + "subscription_end" timestamp with time zone, + "management_email" "text", + "is_yearly" boolean, + "stats_updated_at" timestamp without time zone, + "next_stats_update_at" timestamp with time zone, + "credit_available" numeric, + "credit_total" numeric, + "credit_next_expiration" timestamp with time zone, + "enforcing_2fa" boolean, + "2fa_has_access" boolean +) LANGUAGE "plpgsql" SECURITY DEFINER +SET + "search_path" TO '' AS $$ +DECLARE + api_key_text text; + api_key record; + user_id uuid; +BEGIN + SELECT public.get_apikey_header() INTO api_key_text; + user_id := NULL; + + IF api_key_text IS NOT NULL THEN + SELECT * FROM public.apikeys WHERE key = api_key_text INTO api_key; + + IF api_key IS NULL THEN + PERFORM public.pg_log('deny: INVALID_API_KEY', jsonb_build_object('source', 'header')); + RAISE EXCEPTION 'Invalid API key provided'; + END IF; + + user_id := api_key.user_id; + + IF COALESCE(array_length(api_key.limited_to_orgs, 1), 0) > 0 THEN + RETURN QUERY + SELECT orgs.* + FROM public.get_orgs_v7(user_id) AS orgs + WHERE orgs.gid = ANY(api_key.limited_to_orgs::uuid[]); + RETURN; + END IF; + END IF; + + IF user_id IS NULL THEN + SELECT public.get_identity() INTO user_id; + + IF user_id IS NULL THEN + PERFORM public.pg_log('deny: UNAUTHENTICATED', '{}'::jsonb); + RAISE EXCEPTION 'No authentication provided - API key or valid session required'; + END IF; + END IF; + + RETURN QUERY SELECT * FROM public.get_orgs_v7(user_id); +END; +$$; + +ALTER FUNCTION "public"."get_orgs_v7" () OWNER TO "postgres"; + +CREATE OR REPLACE FUNCTION "public"."get_orgs_v7" ("userid" "uuid") RETURNS TABLE ( + "gid" "uuid", + "created_by" "uuid", + "logo" "text", + "name" "text", + "role" character varying, + "paying" boolean, + "trial_left" integer, + "can_use_more" boolean, + "is_canceled" boolean, + "app_count" bigint, + "subscription_start" timestamp with time zone, + "subscription_end" timestamp with time zone, + "management_email" "text", + "is_yearly" boolean, + "stats_updated_at" timestamp without time zone, + "next_stats_update_at" timestamp with time zone, + "credit_available" numeric, + "credit_total" numeric, + "credit_next_expiration" timestamp with time zone, + "enforcing_2fa" boolean, + "2fa_has_access" boolean +) LANGUAGE "plpgsql" STABLE SECURITY DEFINER +SET + "search_path" TO '' AS $$ +BEGIN + RETURN QUERY + WITH app_counts AS ( + SELECT owner_org, COUNT(*) as cnt + FROM public.apps + GROUP BY owner_org + ), + -- Compute next stats update info for all paying orgs at once + paying_orgs_ordered AS ( + SELECT + o.id, + ROW_NUMBER() OVER (ORDER BY o.id ASC) - 1 as preceding_count + FROM public.orgs o + JOIN public.stripe_info si ON o.customer_id = si.customer_id + WHERE ( + (si.status = 'succeeded' + AND (si.canceled_at IS NULL OR si.canceled_at > now()) + AND si.subscription_anchor_end > now()) + OR si.trial_at > now() + ) + ), + -- Calculate current billing cycle for each org + billing_cycles AS ( + SELECT + o.id AS org_id, + CASE + WHEN COALESCE(si.subscription_anchor_start - date_trunc('MONTH', si.subscription_anchor_start), '0 DAYS'::INTERVAL) + > now() - date_trunc('MONTH', now()) + THEN date_trunc('MONTH', now() - INTERVAL '1 MONTH') + + COALESCE(si.subscription_anchor_start - date_trunc('MONTH', si.subscription_anchor_start), '0 DAYS'::INTERVAL) + ELSE date_trunc('MONTH', now()) + + COALESCE(si.subscription_anchor_start - date_trunc('MONTH', si.subscription_anchor_start), '0 DAYS'::INTERVAL) + END AS cycle_start + FROM public.orgs o + LEFT JOIN public.stripe_info si ON o.customer_id = si.customer_id + ), + -- Calculate 2FA access status for user/org combinations + two_fa_access AS ( + SELECT + o.id AS org_id, + o.enforcing_2fa, + -- 2fa_has_access: true if enforcing_2fa is false OR (enforcing_2fa is true AND user has 2FA) + CASE + WHEN o.enforcing_2fa = false THEN true + ELSE public.has_2fa_enabled(userid) + END AS "2fa_has_access", + -- should_redact: true if org enforces 2FA and user doesn't have 2FA + (o.enforcing_2fa = true AND NOT public.has_2fa_enabled(userid)) AS should_redact + FROM public.orgs o + JOIN public.org_users ou ON ou.user_id = userid AND o.id = ou.org_id + ) + SELECT + o.id AS gid, + o.created_by, + o.logo, + o.name, + ou.user_right::varchar AS role, + -- Redact sensitive fields if user doesn't have 2FA access + CASE + WHEN tfa.should_redact THEN false + ELSE (si.status = 'succeeded') + END AS paying, + CASE + WHEN tfa.should_redact THEN 0 + ELSE GREATEST(COALESCE((si.trial_at::date - now()::date), 0), 0)::integer + END AS trial_left, + CASE + WHEN tfa.should_redact THEN false + ELSE ((si.status = 'succeeded' AND si.is_good_plan = true) OR (si.trial_at::date - now()::date > 0)) + END AS can_use_more, + CASE + WHEN tfa.should_redact THEN false + ELSE (si.status = 'canceled') + END AS is_canceled, + CASE + WHEN tfa.should_redact THEN 0::bigint + ELSE COALESCE(ac.cnt, 0) + END AS app_count, + CASE + WHEN tfa.should_redact THEN NULL::timestamptz + ELSE bc.cycle_start + END AS subscription_start, + CASE + WHEN tfa.should_redact THEN NULL::timestamptz + ELSE (bc.cycle_start + INTERVAL '1 MONTH') + END AS subscription_end, + CASE + WHEN tfa.should_redact THEN NULL::text + ELSE o.management_email + END AS management_email, + CASE + WHEN tfa.should_redact THEN false + ELSE COALESCE(si.price_id = p.price_y_id, false) + END AS is_yearly, + o.stats_updated_at, + CASE + WHEN poo.id IS NOT NULL THEN + public.get_next_cron_time('0 3 * * *', now()) + make_interval(mins => poo.preceding_count::int * 4) + ELSE NULL + END AS next_stats_update_at, + COALESCE(ucb.available_credits, 0) AS credit_available, + COALESCE(ucb.total_credits, 0) AS credit_total, + ucb.next_expiration AS credit_next_expiration, + tfa.enforcing_2fa, + tfa."2fa_has_access" + FROM public.orgs o + JOIN public.org_users ou ON ou.user_id = userid AND o.id = ou.org_id + JOIN two_fa_access tfa ON tfa.org_id = o.id + LEFT JOIN public.stripe_info si ON o.customer_id = si.customer_id + LEFT JOIN public.plans p ON si.product_id = p.stripe_id + LEFT JOIN app_counts ac ON ac.owner_org = o.id + LEFT JOIN public.usage_credit_balances ucb ON ucb.org_id = o.id + LEFT JOIN paying_orgs_ordered poo ON poo.id = o.id + LEFT JOIN billing_cycles bc ON bc.org_id = o.id; +END; +$$; + +ALTER FUNCTION "public"."get_orgs_v7" ("userid" "uuid") OWNER TO "postgres"; + CREATE OR REPLACE FUNCTION "public"."get_plan_usage_percent_detailed" ("orgid" "uuid") RETURNS TABLE ( "total_percent" double precision, "mau_percent" double precision, @@ -2968,6 +3351,38 @@ $$; ALTER FUNCTION "public"."get_weekly_stats" ("app_id" character varying) OWNER TO "postgres"; +CREATE OR REPLACE FUNCTION "public"."has_2fa_enabled" () RETURNS boolean LANGUAGE "plpgsql" SECURITY DEFINER +SET + "search_path" TO '' AS $$ +BEGIN + -- Check if the current user has any verified MFA factors + RETURN EXISTS( + SELECT 1 + FROM auth.mfa_factors + WHERE (SELECT auth.uid()) = user_id + AND status = 'verified' + ); +END; +$$; + +ALTER FUNCTION "public"."has_2fa_enabled" () OWNER TO "postgres"; + +CREATE OR REPLACE FUNCTION "public"."has_2fa_enabled" ("user_id" "uuid") RETURNS boolean LANGUAGE "plpgsql" SECURITY DEFINER +SET + "search_path" TO '' AS $$ +BEGIN + -- Check if the specified user has any verified MFA factors + RETURN EXISTS( + SELECT 1 + FROM auth.mfa_factors mfa + WHERE mfa.user_id = has_2fa_enabled.user_id + AND mfa.status = 'verified' + ); +END; +$$; + +ALTER FUNCTION "public"."has_2fa_enabled" ("user_id" "uuid") OWNER TO "postgres"; + CREATE OR REPLACE FUNCTION "public"."has_app_right" ( "appid" character varying, "right" "public"."user_min_right" @@ -3804,6 +4219,12 @@ BEGIN EXCEPTION WHEN OTHERS THEN RAISE WARNING 'cleanup_frequent_job_details failed: %', SQLERRM; END; + + BEGIN + PERFORM public.process_deploy_install_stats_email(); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'process_deploy_install_stats_email failed: %', SQLERRM; + END; END IF; -- Every 2 hours (at :00:00): Low-frequency queues with default batch size @@ -4029,6 +4450,99 @@ $$; ALTER FUNCTION "public"."process_cron_sync_sub_jobs" () OWNER TO "postgres"; +CREATE OR REPLACE FUNCTION "public"."process_deploy_install_stats_email" () RETURNS "void" LANGUAGE "plpgsql" +SET + "search_path" TO '' AS $$ +DECLARE + record RECORD; +BEGIN + FOR record IN + WITH latest AS ( + SELECT DISTINCT ON (dh.app_id, channel_platform) + dh.id, + dh.app_id, + dh.version_id, + dh.deployed_at, + dh.owner_org, + dh.channel_id, + CASE + WHEN c.ios = true AND c.android = false THEN 'ios' + WHEN c.android = true AND c.ios = false THEN 'android' + ELSE 'all' + END AS channel_platform + FROM public.deploy_history dh + JOIN public.channels c ON c.id = dh.channel_id + WHERE c.public = true + AND (c.ios = true OR c.android = true) + ORDER BY dh.app_id, channel_platform, dh.deployed_at DESC NULLS LAST + ), + eligible AS ( + SELECT l.* + FROM latest l + WHERE l.deployed_at IS NOT NULL + AND l.deployed_at <= now() - interval '24 hours' + ), + updated AS ( + UPDATE public.deploy_history dh + SET install_stats_email_sent_at = now() + FROM eligible e + WHERE dh.id = e.id + AND dh.install_stats_email_sent_at IS NULL + RETURNING dh.id, dh.app_id, dh.version_id, dh.deployed_at, dh.owner_org, dh.channel_id + ), + details AS ( + SELECT + u.id, + u.app_id, + u.version_id, + u.deployed_at, + u.owner_org, + u.channel_id, + e.channel_platform, + o.management_email, + c.name AS channel_name, + v.name AS version_name, + a.name AS app_name + FROM updated u + JOIN eligible e ON e.id = u.id + JOIN public.orgs o ON o.id = u.owner_org + JOIN public.channels c ON c.id = u.channel_id + JOIN public.app_versions v ON v.id = u.version_id + JOIN public.apps a ON a.app_id = u.app_id + ) + SELECT + d.* + FROM details d + LOOP + IF record.management_email IS NULL OR record.management_email = '' THEN + CONTINUE; + END IF; + + PERFORM pgmq.send('cron_email', + jsonb_build_object( + 'function_name', 'cron_email', + 'function_type', 'cloudflare', + 'payload', jsonb_build_object( + 'email', record.management_email, + 'appId', record.app_id, + 'type', 'deploy_install_stats', + 'deployId', record.id, + 'versionId', record.version_id, + 'versionName', record.version_name, + 'channelId', record.channel_id, + 'channelName', record.channel_name, + 'platform', record.channel_platform, + 'appName', record.app_name, + 'deployedAt', record.deployed_at + ) + ) + ); + END LOOP; +END; +$$; + +ALTER FUNCTION "public"."process_deploy_install_stats_email" () OWNER TO "postgres"; + CREATE OR REPLACE FUNCTION "public"."process_failed_uploads" () RETURNS "void" LANGUAGE "plpgsql" SET "search_path" TO '' AS $$ @@ -4454,6 +4968,40 @@ $$; ALTER FUNCTION "public"."record_deployment_history" () OWNER TO "postgres"; +CREATE OR REPLACE FUNCTION "public"."reject_access_due_to_2fa" ("org_id" "uuid", "user_id" "uuid") RETURNS boolean LANGUAGE "plpgsql" SECURITY DEFINER +SET + "search_path" TO '' AS $$ +DECLARE + org_enforcing_2fa boolean; +BEGIN + -- Check if org exists + IF NOT EXISTS (SELECT 1 FROM public.orgs WHERE public.orgs.id = reject_access_due_to_2fa.org_id) THEN + RETURN false; + END IF; + + -- Check if org has 2FA enforcement enabled + SELECT enforcing_2fa INTO org_enforcing_2fa + FROM public.orgs + WHERE public.orgs.id = reject_access_due_to_2fa.org_id; + + -- 7.1 If a given org does not enable 2FA enforcement, return false + IF org_enforcing_2fa = false THEN + RETURN false; + END IF; + + -- 7.2 If a given org REQUIRES 2FA, and has_2fa_enabled(user_id) == false, return true + IF org_enforcing_2fa = true AND NOT public.has_2fa_enabled(reject_access_due_to_2fa.user_id) THEN + PERFORM public.pg_log('deny: REJECT_ACCESS_DUE_TO_2FA', jsonb_build_object('org_id', org_id, 'user_id', user_id)); + RETURN true; + END IF; + + -- 7.3 Otherwise, return false + RETURN false; +END; +$$; + +ALTER FUNCTION "public"."reject_access_due_to_2fa" ("org_id" "uuid", "user_id" "uuid") OWNER TO "postgres"; + CREATE OR REPLACE FUNCTION "public"."remove_old_jobs" () RETURNS "void" LANGUAGE "plpgsql" SET "search_path" TO '' AS $$ @@ -5315,12 +5863,12 @@ CREATE TABLE IF NOT EXISTS "public"."channels" ( "android" boolean DEFAULT true NOT NULL, "allow_device_self_set" boolean DEFAULT false NOT NULL, "allow_emulator" boolean DEFAULT true NOT NULL, - "allow_device" boolean DEFAULT true NOT NULL, "allow_dev" boolean DEFAULT true NOT NULL, - "allow_prod" boolean DEFAULT true NOT NULL, "disable_auto_update" "public"."disable_update" DEFAULT 'major'::"public"."disable_update" NOT NULL, "owner_org" "uuid" NOT NULL, - "created_by" "uuid" NOT NULL + "created_by" "uuid" NOT NULL, + "allow_device" boolean DEFAULT true NOT NULL, + "allow_prod" boolean DEFAULT true NOT NULL ); ALTER TABLE ONLY "public"."channels" REPLICA IDENTITY FULL; @@ -5444,7 +5992,8 @@ CREATE TABLE IF NOT EXISTS "public"."deploy_history" ( "version_id" bigint NOT NULL, "deployed_at" timestamp with time zone DEFAULT "now" (), "created_by" "uuid" NOT NULL, - "owner_org" "uuid" NOT NULL + "owner_org" "uuid" NOT NULL, + "install_stats_email_sent_at" timestamp with time zone ); ALTER TABLE "public"."deploy_history" OWNER TO "postgres"; @@ -5549,7 +6098,8 @@ CREATE TABLE IF NOT EXISTS "public"."global_stats" ( "canceled_orgs" integer DEFAULT 0 NOT NULL, "revenue_enterprise" double precision DEFAULT 0 NOT NULL, "plan_enterprise_monthly" integer DEFAULT 0 NOT NULL, - "plan_enterprise_yearly" integer DEFAULT 0 NOT NULL + "plan_enterprise_yearly" integer DEFAULT 0 NOT NULL, + "plan_enterprise" integer DEFAULT 0 ); ALTER TABLE "public"."global_stats" OWNER TO "postgres"; @@ -5655,13 +6205,16 @@ CREATE TABLE IF NOT EXISTS "public"."orgs" ( "management_email" "text" NOT NULL, "customer_id" character varying, "stats_updated_at" timestamp without time zone, - "last_stats_updated_at" timestamp without time zone + "last_stats_updated_at" timestamp without time zone, + "enforcing_2fa" boolean DEFAULT false NOT NULL ); ALTER TABLE ONLY "public"."orgs" REPLICA IDENTITY FULL; ALTER TABLE "public"."orgs" OWNER TO "postgres"; +COMMENT ON COLUMN "public"."orgs"."enforcing_2fa" IS 'When true, all members of this organization must have 2FA enabled to access the organization'; + CREATE TABLE IF NOT EXISTS "public"."plans" ( "created_at" timestamp with time zone DEFAULT "now" () NOT NULL, "updated_at" timestamp with time zone DEFAULT "now" () NOT NULL, @@ -6238,7 +6791,7 @@ ALTER TABLE ONLY "public"."bandwidth_usage" ADD CONSTRAINT "bandwidth_usage_pkey" PRIMARY KEY ("id"); ALTER TABLE ONLY "public"."build_logs" -ADD CONSTRAINT "build_logs_build_id_org_id_key" UNIQUE ("build_id", "org_id"); +ADD CONSTRAINT "build_logs_build_id_org_id_unique" UNIQUE ("build_id", "org_id"); ALTER TABLE ONLY "public"."build_logs" ADD CONSTRAINT "build_logs_pkey" PRIMARY KEY ("id"); @@ -8368,6 +8921,12 @@ GRANT ALL ON FUNCTION "public"."check_min_rights" ( "channel_id" bigint ) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."check_org_members_2fa_enabled" ("org_id" "uuid") TO "anon"; + +GRANT ALL ON FUNCTION "public"."check_org_members_2fa_enabled" ("org_id" "uuid") TO "authenticated"; + +GRANT ALL ON FUNCTION "public"."check_org_members_2fa_enabled" ("org_id" "uuid") TO "service_role"; + GRANT ALL ON FUNCTION "public"."check_org_user_privileges" () TO "anon"; GRANT ALL ON FUNCTION "public"."check_org_user_privileges" () TO "authenticated"; @@ -8833,12 +9392,24 @@ GRANT ALL ON FUNCTION "public"."get_orgs_v6" () TO "authenticated"; GRANT ALL ON FUNCTION "public"."get_orgs_v6" () TO "service_role"; -GRANT ALL ON FUNCTION "public"."get_orgs_v6" ("userid" "uuid") TO "anon"; - -GRANT ALL ON FUNCTION "public"."get_orgs_v6" ("userid" "uuid") TO "authenticated"; +REVOKE ALL ON FUNCTION "public"."get_orgs_v6" ("userid" "uuid") +FROM + PUBLIC; GRANT ALL ON FUNCTION "public"."get_orgs_v6" ("userid" "uuid") TO "service_role"; +GRANT ALL ON FUNCTION "public"."get_orgs_v7" () TO "anon"; + +GRANT ALL ON FUNCTION "public"."get_orgs_v7" () TO "authenticated"; + +GRANT ALL ON FUNCTION "public"."get_orgs_v7" () TO "service_role"; + +REVOKE ALL ON FUNCTION "public"."get_orgs_v7" ("userid" "uuid") +FROM + PUBLIC; + +GRANT ALL ON FUNCTION "public"."get_orgs_v7" ("userid" "uuid") TO "service_role"; + GRANT ALL ON FUNCTION "public"."get_plan_usage_percent_detailed" ("orgid" "uuid") TO "anon"; GRANT ALL ON FUNCTION "public"."get_plan_usage_percent_detailed" ("orgid" "uuid") TO "authenticated"; @@ -8971,6 +9542,18 @@ GRANT ALL ON FUNCTION "public"."get_weekly_stats" ("app_id" character varying) T GRANT ALL ON FUNCTION "public"."get_weekly_stats" ("app_id" character varying) TO "service_role"; +GRANT ALL ON FUNCTION "public"."has_2fa_enabled" () TO "anon"; + +GRANT ALL ON FUNCTION "public"."has_2fa_enabled" () TO "authenticated"; + +GRANT ALL ON FUNCTION "public"."has_2fa_enabled" () TO "service_role"; + +REVOKE ALL ON FUNCTION "public"."has_2fa_enabled" ("user_id" "uuid") +FROM + PUBLIC; + +GRANT ALL ON FUNCTION "public"."has_2fa_enabled" ("user_id" "uuid") TO "service_role"; + GRANT ALL ON FUNCTION "public"."has_app_right" ( "appid" character varying, "right" "public"."user_min_right" @@ -9349,6 +9932,12 @@ FROM GRANT ALL ON FUNCTION "public"."process_cron_sync_sub_jobs" () TO "service_role"; +GRANT ALL ON FUNCTION "public"."process_deploy_install_stats_email" () TO "anon"; + +GRANT ALL ON FUNCTION "public"."process_deploy_install_stats_email" () TO "authenticated"; + +GRANT ALL ON FUNCTION "public"."process_deploy_install_stats_email" () TO "service_role"; + REVOKE ALL ON FUNCTION "public"."process_failed_uploads" () FROM PUBLIC; @@ -9527,6 +10116,12 @@ GRANT ALL ON FUNCTION "public"."record_deployment_history" () TO "authenticated" GRANT ALL ON FUNCTION "public"."record_deployment_history" () TO "service_role"; +REVOKE ALL ON FUNCTION "public"."reject_access_due_to_2fa" ("org_id" "uuid", "user_id" "uuid") +FROM + PUBLIC; + +GRANT ALL ON FUNCTION "public"."reject_access_due_to_2fa" ("org_id" "uuid", "user_id" "uuid") TO "service_role"; + REVOKE ALL ON FUNCTION "public"."remove_old_jobs" () FROM PUBLIC; diff --git a/supabase/tests/29_test_delete_accounts_marked_for_deletion.sql b/supabase/tests/29_test_delete_accounts_marked_for_deletion.sql index a91e47b5d5..133114b9c1 100644 --- a/supabase/tests/29_test_delete_accounts_marked_for_deletion.sql +++ b/supabase/tests/29_test_delete_accounts_marked_for_deletion.sql @@ -1,6 +1,6 @@ BEGIN; -SELECT plan(41); +SELECT plan(47); -- Test helper function to create test users in both auth.users and public.users tables CREATE OR REPLACE FUNCTION create_test_user_for_deletion( @@ -1009,6 +1009,203 @@ DELETE FROM auth.users WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::UUID; +-- Test 10: Audit logs ownership transfer during user deletion +-- Create two users for audit log test +SELECT + create_test_user_for_deletion( + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + 'audit_admin1@test.com' + ); + +SELECT + create_test_user_for_deletion( + 'cccccccc-cccc-cccc-cccc-cccccccccccc'::UUID, + 'audit_admin2@test.com' + ); + +-- Create an org for audit log test +INSERT INTO +public.orgs ( + id, + created_by, + created_at, + updated_at, + name, + management_email +) +VALUES +( + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + now(), + now(), + 'Audit Log Test Org', + 'audit_admin1@test.com' +); + +-- Add both users as super_admins +INSERT INTO +public.org_users (org_id, user_id, user_right) +VALUES +( + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + 'super_admin'::public.USER_MIN_RIGHT +), +( + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + 'cccccccc-cccc-cccc-cccc-cccccccccccc'::UUID, + 'super_admin'::public.USER_MIN_RIGHT +); + +-- Manually insert audit log entries for admin1 +-- (Normally these would be created by triggers, but we insert directly for testing) +INSERT INTO +public.audit_logs (table_name, record_id, operation, user_id, org_id, old_record, new_record) +VALUES +( + 'apps', + 'com.audit.test', + 'INSERT', + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + NULL, + '{"app_id": "com.audit.test"}'::JSONB +), +( + 'channels', + '3001', + 'UPDATE', + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + '{"name": "old_channel"}'::JSONB, + '{"name": "new_channel"}'::JSONB +); + +-- Count audit logs before deletion (includes trigger-created entries from org/org_users inserts) +-- We need to track the count before and compare after +SELECT + ok( + ( + SELECT COUNT(*) + FROM + public.audit_logs + WHERE + user_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID + AND org_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID + ) >= 2, + 'At least two audit log entries exist for admin1 before deletion' + ); + +-- Mark admin1 for deletion +INSERT INTO +public.to_delete_accounts (account_id, removal_date, removed_data) +VALUES +( + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID, + now() - INTERVAL '1 day', + '{"email": "audit_admin1@test.com", "apikeys": []}'::JSONB +); + +-- Run deletion +SELECT + ok( + ( + WITH + deletion_results AS ( + SELECT * + FROM + delete_accounts_marked_for_deletion() + LIMIT + 1 + ) + + SELECT deleted_count + FROM + deletion_results + ) = 1, + 'User with audit logs deleted successfully' + ); + +-- Verify user is deleted +SELECT + ok( + NOT EXISTS ( + SELECT 1 + FROM + public.users + WHERE + id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID + ), + 'Admin1 removed from public.users' + ); + +-- Verify audit logs still exist (not deleted) - should have at least 2 entries +-- Note: triggers may have created additional entries when creating the org/org_users +SELECT + ok( + ( + SELECT COUNT(*) + FROM + public.audit_logs + WHERE + org_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID + ) >= 2, + 'Audit log entries still exist after user deletion' + ); + +-- Verify audit logs that were owned by admin1 are now owned by admin2 +-- The key test is that entries originally created by admin1 are transferred +SELECT + ok( + ( + SELECT COUNT(*) + FROM + public.audit_logs + WHERE + user_id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'::UUID + AND org_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID + AND table_name IN ('apps', 'channels') + AND record_id IN ('com.audit.test', '3001') + ) = 2, + 'Audit log entries ownership transferred to remaining super_admin' + ); + +-- Verify no audit logs owned by admin1 remain (they should have been transferred) +SELECT + ok( + NOT EXISTS ( + SELECT 1 + FROM + public.audit_logs + WHERE + org_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID + AND user_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID + ), + 'No audit log entries remain owned by deleted user' + ); + +-- Clean up audit log test +DELETE FROM public.audit_logs +WHERE + org_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID; + +DELETE FROM public.org_users +WHERE + org_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID; + +DELETE FROM public.orgs +WHERE + id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::UUID; + +DELETE FROM public.users +WHERE + id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'::UUID; + +DELETE FROM auth.users +WHERE + id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'::UUID; + -- Clean up test helper function before permission tests DROP FUNCTION create_test_user_for_deletion(UUID, TEXT); diff --git a/tests/audit-logs.test.ts b/tests/audit-logs.test.ts new file mode 100644 index 0000000000..d78dfa4b02 --- /dev/null +++ b/tests/audit-logs.test.ts @@ -0,0 +1,336 @@ +import { randomUUID } from 'node:crypto' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { z } from 'zod' + +import { BASE_URL, getSupabaseClient, headers, TEST_EMAIL, USER_ID } from './test-utils.ts' + +const ORG_ID = randomUUID() +const globalId = randomUUID() +const name = `Test Audit Organization ${globalId}` +const customerId = `cus_audit_${ORG_ID}` + +// Schema for audit log response +const auditLogSchema = z.object({ + id: z.number(), + created_at: z.string(), + table_name: z.string(), + record_id: z.string(), + operation: z.string(), + user_id: z.nullable(z.string()), + org_id: z.string(), + old_record: z.unknown(), + new_record: z.unknown(), + changed_fields: z.nullable(z.array(z.string())), +}) + +const auditLogsResponseSchema = z.object({ + data: z.array(auditLogSchema), + total: z.number(), + page: z.number(), + limit: z.number(), +}) + +beforeAll(async () => { + // Create stripe_info for this test org + const { error: stripeError } = await getSupabaseClient().from('stripe_info').insert({ + customer_id: customerId, + status: 'succeeded', + product_id: 'prod_LQIregjtNduh4q', + subscription_id: `sub_${globalId}`, + trial_at: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(), + is_good_plan: true, + }) + if (stripeError) + throw stripeError + + // Create test organization (this should trigger an INSERT audit log via the trigger) + const { error } = await getSupabaseClient().from('orgs').insert({ + id: ORG_ID, + name, + management_email: TEST_EMAIL, + created_by: USER_ID, + customer_id: customerId, + }) + if (error) + throw error +}) + +afterAll(async () => { + // Clean up: delete audit logs first (they reference the org) + await getSupabaseClient().from('audit_logs').delete().eq('org_id', ORG_ID) + + // Clean up test organization and stripe_info + await getSupabaseClient().from('orgs').delete().eq('id', ORG_ID) + await getSupabaseClient().from('stripe_info').delete().eq('customer_id', customerId) +}) + +describe('[GET] /organization/audit', () => { + it('get audit logs for organization', async () => { + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + if (safe.success) { + expect(safe.data.page).toBe(0) + expect(safe.data.limit).toBe(50) + expect(Array.isArray(safe.data.data)).toBe(true) + } + }) + + it('get audit logs with pagination', async () => { + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}&page=0&limit=10`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + if (safe.success) { + expect(safe.data.page).toBe(0) + expect(safe.data.limit).toBe(10) + } + }) + + it('get audit logs filtered by table name', async () => { + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}&tableName=orgs`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + if (safe.success) { + // All returned logs should be for the 'orgs' table + for (const log of safe.data.data) { + expect(log.table_name).toBe('orgs') + } + } + }) + + it('get audit logs filtered by operation', async () => { + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}&operation=INSERT`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + if (safe.success) { + // All returned logs should be INSERT operations + for (const log of safe.data.data) { + expect(log.operation).toBe('INSERT') + } + } + }) + + it('get audit logs with combined filters', async () => { + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}&tableName=orgs&operation=INSERT`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + if (safe.success) { + for (const log of safe.data.data) { + expect(log.table_name).toBe('orgs') + expect(log.operation).toBe('INSERT') + } + } + }) + + it('limit is capped at 100', async () => { + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}&limit=200`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + if (safe.success) { + // Server should cap limit at 100 + expect(safe.data.limit).toBe(100) + } + }) + + it('get audit logs with missing orgId returns error', async () => { + const response = await fetch(`${BASE_URL}/organization/audit`, { + headers, + }) + expect(response.status).toBe(400) + const responseData = await response.json() as { error: string } + expect(responseData.error).toBe('invalid_body') + }) + + it('get audit logs with invalid orgId returns error', async () => { + const invalidOrgId = randomUUID() + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${invalidOrgId}`, { + headers, + }) + expect(response.status).toBe(400) + const responseData = await response.json() as { error: string } + expect(responseData.error).toBe('invalid_org_id') + }) +}) + +describe('Audit log triggers', () => { + it('organization UPDATE creates audit log with changed_fields', async () => { + const newName = `Updated Audit Organization ${randomUUID()}` + + // Update the organization + const { error: updateError } = await getSupabaseClient() + .from('orgs') + .update({ name: newName }) + .eq('id', ORG_ID) + expect(updateError).toBeNull() + + // Wait a bit for the trigger to execute + await new Promise(resolve => setTimeout(resolve, 100)) + + // Fetch audit logs for this org + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}&tableName=orgs&operation=UPDATE`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + + if (safe.success && safe.data.data.length > 0) { + const latestUpdate = safe.data.data[0] + expect(latestUpdate.operation).toBe('UPDATE') + expect(latestUpdate.table_name).toBe('orgs') + expect(latestUpdate.record_id).toBe(ORG_ID) + expect(latestUpdate.org_id).toBe(ORG_ID) + // Changed fields should include 'name' and 'updated_at' + expect(Array.isArray(latestUpdate.changed_fields)).toBe(true) + expect(latestUpdate.changed_fields).toContain('name') + // old_record should have the old name + expect(latestUpdate.old_record).toBeTruthy() + // new_record should have the new name + expect(latestUpdate.new_record).toBeTruthy() + if (latestUpdate.new_record && typeof latestUpdate.new_record === 'object') { + expect((latestUpdate.new_record as Record).name).toBe(newName) + } + } + }) + + it('org_users INSERT creates audit log', async () => { + // Get another user to add to the org + const { data: anotherUser, error: userError } = await getSupabaseClient() + .from('users') + .select('id') + .neq('id', USER_ID) + .limit(1) + .single() + + expect(userError).toBeNull() + expect(anotherUser).toBeTruthy() + + if (!anotherUser) { + console.warn('Skipping test: Could not find another user') + return + } + + // Add user to org + const { error: insertError } = await getSupabaseClient() + .from('org_users') + .insert({ + org_id: ORG_ID, + user_id: anotherUser.id, + user_right: 'read', + }) + expect(insertError).toBeNull() + + // Wait a bit for the trigger to execute + await new Promise(resolve => setTimeout(resolve, 100)) + + // Fetch audit logs + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}&tableName=org_users&operation=INSERT`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + + if (safe.success && safe.data.data.length > 0) { + const latestInsert = safe.data.data[0] + expect(latestInsert.operation).toBe('INSERT') + expect(latestInsert.table_name).toBe('org_users') + expect(latestInsert.org_id).toBe(ORG_ID) + expect(latestInsert.old_record).toBeNull() // INSERT has no old record + expect(latestInsert.new_record).toBeTruthy() + } + + // Clean up: delete the org_user + await getSupabaseClient() + .from('org_users') + .delete() + .eq('org_id', ORG_ID) + .eq('user_id', anotherUser.id) + }) + + it('org_users DELETE creates audit log', async () => { + // Get another user to add and then remove + const { data: anotherUser, error: userError } = await getSupabaseClient() + .from('users') + .select('id') + .neq('id', USER_ID) + .limit(1) + .single() + + expect(userError).toBeNull() + expect(anotherUser).toBeTruthy() + + if (!anotherUser) { + console.warn('Skipping test: Could not find another user') + return + } + + // Add user to org + const { error: insertError } = await getSupabaseClient() + .from('org_users') + .insert({ + org_id: ORG_ID, + user_id: anotherUser.id, + user_right: 'read', + }) + expect(insertError).toBeNull() + + // Wait for insert trigger + await new Promise(resolve => setTimeout(resolve, 100)) + + // Delete the org_user + const { error: deleteError } = await getSupabaseClient() + .from('org_users') + .delete() + .eq('org_id', ORG_ID) + .eq('user_id', anotherUser.id) + expect(deleteError).toBeNull() + + // Wait for delete trigger + await new Promise(resolve => setTimeout(resolve, 100)) + + // Fetch audit logs for DELETE + const response = await fetch(`${BASE_URL}/organization/audit?orgId=${ORG_ID}&tableName=org_users&operation=DELETE`, { + headers, + }) + expect(response.status).toBe(200) + const responseData = await response.json() + const safe = auditLogsResponseSchema.safeParse(responseData) + expect(safe.success).toBe(true) + + if (safe.success && safe.data.data.length > 0) { + const latestDelete = safe.data.data[0] + expect(latestDelete.operation).toBe('DELETE') + expect(latestDelete.table_name).toBe('org_users') + expect(latestDelete.org_id).toBe(ORG_ID) + expect(latestDelete.new_record).toBeNull() // DELETE has no new record + expect(latestDelete.old_record).toBeTruthy() + } + }) +})