Compare commits

...
Author SHA1 Message Date
Philip Okugbe 41b9838a8c New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-08-26 15:12:27 +01:00
Philip Okugbe 02d173cff0 New translations translation.json (Chinese Simplified)
[ci skip]
2026-08-26 15:12:25 +01:00
Philip Okugbe 03443012cc New translations translation.json (Ukrainian)
[ci skip]
2026-08-26 15:12:23 +01:00
Philip Okugbe 5e1d169c90 New translations translation.json (Russian)
[ci skip]
2026-08-26 15:12:21 +01:00
Philip Okugbe 279c695b85 New translations translation.json (Dutch)
[ci skip]
2026-08-26 15:12:19 +01:00
Philip Okugbe d077253b17 New translations translation.json (Japanese)
[ci skip]
2026-08-26 15:12:17 +01:00
Philip Okugbe 444ab8c16f New translations translation.json (Italian)
[ci skip]
2026-08-26 15:12:15 +01:00
Philip Okugbe 2170a3e90d New translations translation.json (German)
[ci skip]
2026-08-26 15:12:13 +01:00
Philip Okugbe f439013164 New translations translation.json (French)
[ci skip]
2026-08-26 15:12:11 +01:00
Philip Okugbe ed391e9d39 New translations translation.json (Spanish)
[ci skip]
2026-08-26 15:12:08 +01:00
Philip Okugbe e6096dc161 New translations translation.json (Korean)
[ci skip]
2026-08-26 15:12:06 +01:00
Philip Okugbe 579e22a45e fix: use ACL username and decode credentials in Redis URL parsing (#2434)
* fix: use ACL username and decode credentials in Redis URL parsing

* fix: pass family and TLS options to the collab app module
2026-08-26 14:16:18 +01:00
Philip Okugbe c3d9a19545 New Crowdin updates (#2332) 2026-08-26 14:08:10 +01:00
Philipinho 214a73b6a0 refactor: rename MCP oauth-only setting to enforceMcpOauth 2026-08-26 13:45:28 +01:00
Philipinho 278d318771 sync 2026-08-26 12:56:15 +01:00
Philip Okugbe e56de8eb6c feat(ee): MCP OAuth (#2432)
* feat: mcp oauth

* fix: small refactor

* fix: cleanup consent
2026-08-26 12:55:30 +01:00
Salihu bda802e5df convert only bare links to embeds (#2424) 2026-08-26 12:53:36 +01:00
Philip Okugbe b29e130f26 fix: enhance lightbox (#2429)
* fix: close on click outside; smaller icon size

* fix: make ALT more visible
2026-08-26 02:24:02 +01:00
67 changed files with 2381 additions and 252 deletions
@@ -294,6 +294,7 @@
"Export space": "Bereich exportieren",
"Export {{type}}": "Exportiere {{type}}",
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
"Media": "Medien",
"Align left": "Links ausrichten",
"Align right": "Rechts ausrichten",
"Align center": "Zentrieren",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "Horizontale Trennlinie einfügen",
"Page break": "Seitenumbruch",
"Insert a page break for printing.": "Einen Seitenumbruch zum Drucken einfügen.",
"Footnote": "Fußnote",
"Insert a footnote reference.": "Einen Fußnotenverweis einfügen.",
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.",
"Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
"Upload any audio from your device.": "Laden Sie beliebige Audiodateien von Ihrem Gerät hoch.",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Aktivieren Sie den MCP-Server, damit KI-Assistenten und -Tools mit den Inhalten Ihres Arbeitsbereichs interagieren können.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.",
"MCP Server URL": "MCP-Server-URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "Verwenden Sie Ihren API-Schlüssel zur Authentifizierung. API-Schlüssel können in Ihren Kontoeinstellungen verwaltet werden.",
"Connect AI assistants with your Docmost account via OAuth.": "Verbinde AI-Assistenten über OAuth mit deinem Docmost-Konto.",
"Enforce OAuth": "OAuth erzwingen",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI-Assistenten müssen sich über OAuth mit einem Docmost-Konto verbinden. API-Schlüssel können nicht mit dem MCP-Server verwendet werden.",
"Toggle enforce OAuth for MCP": "OAuth-Erzwingung für MCP umschalten",
"Supported tools": "Unterstützte Tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "In Ihrem Arbeitsbereich ist MCP aktiviert. Verwenden Sie Ihren API-Schlüssel, um KI-Assistenten anzubinden.",
"MCP server URL:": "MCP-Server-URL:",
"Learn more": "Mehr erfahren",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Verwalten Sie API-Schlüssel für alle Nutzer im Arbeitsbereich. Siehe die <anchor>API-Dokumentation</anchor> für Details zur Verwendung.",
@@ -1289,5 +1294,48 @@
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} selected_other": "{{count}} selected",
"Compare": "Vergleichen",
"Compare versions": "Versionen vergleichen",
"Select version from {{date}}": "Version vom {{date}} auswählen",
"Version actions for {{date}}": "Versionsaktionen für {{date}}",
"Comparing {{newer}} and {{older}}": "{{newer}} und {{older}} werden verglichen",
"Exit compare": "Vergleich beenden",
"Search attachments...": "Anhänge suchen...",
"Error loading attachments.": "Fehler beim Laden der Anhänge.",
"No attachments on this page yet.": "Auf dieser Seite gibt es noch keine Anhänge.",
"Uploaded by {{name}}": "Hochgeladen von {{name}}",
"Download {{name}}": "{{name}} herunterladen",
"Access revoked": "Zugriff widerrufen",
"Authorize application": "Anwendung autorisieren",
"{{name}} wants to access {{workspace}}": "{{name}} möchte auf {{workspace}} zugreifen",
"Not you? Switch account": "Nicht du? Konto wechseln",
"This application will be able to:": "Diese Anwendung kann Folgendes:",
"Write": "Schreiben",
"Invalid authorization request": "Ungültige Autorisierungsanfrage",
"Authorize": "Autorisieren",
"Application": "Anwendung",
"Permissions": "Berechtigungen",
"Authorized": "Autorisiert",
"Revoke access": "Zugriff widerrufen",
"Revoke access for {{name}}": "Zugriff für {{name}} widerrufen",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Möchtest du den Zugriff für {{name}} wirklich widerrufen? Die Anwendung kann dann nicht mehr auf dein Konto zugreifen.",
"Something went wrong. Please try again.": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
"Remove {{name}}": "{{name}} entfernen",
"Make sure you trust this application before authorizing it.": "Stelle sicher, dass du dieser Anwendung vertraust, bevor du sie autorisierst.",
"You will be redirected to": "Du wirst weitergeleitet zu",
"View content without making changes.": "Inhalte ansehen, ohne Änderungen vorzunehmen.",
"Create and modify content.": "Inhalte erstellen und bearbeiten.",
"Applications and AI assistants you have authorized to access your account.": "Anwendungen und AI-Assistenten, denen du den Zugriff auf dein Konto autorisiert hast.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "MCP ist für deinen Workspace aktiviert. Verbinde AI-Assistenten über OAuth mit deinem Docmost-Konto.",
"Authorized apps": "Autorisierte Apps",
"No authorized apps yet.": "Noch keine autorisierten Apps.",
"Workspace knowledge only": "Nur Workspace-Wissen",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Beschränke den AI-Chat darauf, nur anhand deiner Workspace-Seiten und hochgeladenen Dateien zu antworten. Es wird kein externes Wissen verwendet.",
"Toggle workspace knowledge only": "Nur Workspace-Wissen umschalten",
"Read-only mode": "Schreibgeschützter Modus",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI-Chat kann Workspace-Inhalte durchsuchen und lesen, aber keine Seiten erstellen oder bearbeiten.",
"Toggle AI Chat read-only mode": "Schreibgeschützten Modus für AI-Chat umschalten",
"Title only": "Nur Titel",
"you": "du"
}
@@ -47,24 +47,24 @@
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.",
"Description": "Description",
"Details": "Details",
"e.g ACME": "e.g ACME",
"e.g ACME Inc": "e.g ACME Inc",
"e.g Developers": "e.g Developers",
"e.g Group for developers": "e.g Group for developers",
"e.g product": "e.g product",
"e.g Product Team": "e.g Product Team",
"e.g Sales": "e.g Sales",
"e.g Space for product team": "e.g Space for product team",
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
"e.g ACME": "e.g. ACME",
"e.g ACME Inc": "e.g. ACME Inc",
"e.g Developers": "e.g. Developers",
"e.g Group for developers": "e.g. Group for developers",
"e.g product": "e.g. product",
"e.g Product Team": "e.g. Product Team",
"e.g Sales": "e.g. Sales",
"e.g Space for product team": "e.g. Space for product team",
"e.g Space for sales team to collaborate": "e.g. Space for sales team to collaborate",
"Edit": "Edit",
"Read": "Read",
"Edit group": "Edit group",
"Email": "Email",
"Enter a strong password": "Enter a strong password",
"Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]",
"enter valid emails addresses": "enter valid emails addresses",
"enter valid emails addresses": "Enter valid email addresses",
"Enter your current password": "Enter your current password",
"enter your full name": "enter your full name",
"enter your full name": "Enter your full name",
"Enter your new password": "Enter your new password",
"Enter your new preferred email": "Enter your new preferred email",
"Enter your password": "Enter your password",
@@ -112,7 +112,7 @@
"Import pages": "Import pages",
"Import pages & space settings": "Import pages & space settings",
"Importing pages": "Importing pages",
"invalid invitation link": "invalid invitation link",
"invalid invitation link": "Invalid invitation link",
"Invitation signup": "Invitation signup",
"Invite by email": "Invite by email",
"Invite members": "Invite members",
@@ -223,7 +223,7 @@
"Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.",
"Sidebar toggle": "Sidebar toggle",
"Comments": "Comments",
"404 page not found": "404 page not found",
"404 page not found": "404! Page not found",
"Sorry, we can't find the page you are looking for.": "Sorry, we can't find the page you are looking for.",
"Take me back to homepage": "Take me back to homepage",
"Forgot password": "Forgot password",
@@ -423,7 +423,7 @@
"Insert current date": "Insert current date",
"Time": "Time",
"Insert current time": "Insert current time",
"Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
"Draw and sketch excalidraw diagrams": "Draw and sketch Excalidraw diagrams",
"Multiple": "Multiple",
"Turn into": "Turn into",
"Text align": "Text align",
@@ -564,7 +564,7 @@
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Make sure to save these codes in a secure place. Your old backup codes are no longer valid.",
"Your new backup codes": "Your new backup codes",
"I've saved my backup codes": "I've saved my backup codes",
"Failed to setup MFA": "Failed to setup MFA",
"Failed to setup MFA": "Failed to set up MFA",
"Setup & Verify": "Setup & Verify",
"Add to authenticator": "Add to authenticator",
"1. Scan this QR code with your authenticator app": "1. Scan this QR code with your authenticator app",
@@ -638,7 +638,7 @@
"Enterprise": "Enterprise",
"Download attachment": "Download attachment",
"Allowed email domains": "Allowed email domains",
"Only users with email addresses from these domains can signup via SSO.": "Only users with email addresses from these domains can signup via SSO.",
"Only users with email addresses from these domains can signup via SSO.": "Only users with email addresses from these domains can sign up via SSO.",
"Enter valid domain names separated by comma or space": "Enter valid domain names separated by comma or space",
"Enforce two-factor authentication": "Enforce two-factor authentication",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Once enforced, all members must enable two-factor authentication to access the workspace.",
@@ -707,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"MCP Server URL": "MCP Server URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "Use your API key for authentication. You can manage API keys in your account settings.",
"Connect AI assistants with your Docmost account via OAuth.": "Connect AI assistants with your Docmost account via OAuth.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Supported tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Your workspace has MCP enabled. Use your API key to connect AI assistants.",
"MCP server URL:": "MCP server URL:",
"Learn more": "Learn more",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.",
@@ -827,7 +829,7 @@
"Choose how this page should stay accurate.": "Choose how this page should stay accurate.",
"Recurring verification": "Recurring verification",
"Verifiers re-confirm this page on a schedule.": "Verifiers re-confirm this page on a schedule.",
"Re-verify on a schedule (e.g every 30 days )": "Re-verify on a schedule (e.g every 30 days )",
"Re-verify on a schedule (e.g every 30 days )": "Re-verify on a schedule (e.g. every 30 days)",
"Page stays editable at all times": "Page stays editable at all times",
"Best for runbooks, FAQs, living documentation": "Best for runbooks, FAQs, living documentation",
"Approval workflow": "Approval workflow",
@@ -903,7 +905,7 @@
"Publish": "Publish",
"Security": "Security",
"Enforce SSO": "Enforce SSO",
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to login with email and password.",
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password.",
"AI-generated content may not be accurate.": "AI-generated content may not be accurate.",
"AI Chat": "AI Chat",
"Analyze for insights": "Analyze for insights",
@@ -1249,7 +1251,7 @@
"Not started": "Not started",
"Number": "Number",
"One month ago": "One month ago",
"One month from now": "One month from now",
"One month from now": "In one month",
"One week ago": "One week ago",
"One week from now": "One week from now",
"Open as page": "Open as page",
@@ -1304,6 +1306,30 @@
"No attachments on this page yet.": "No attachments on this page yet.",
"Uploaded by {{name}}": "Uploaded by {{name}}",
"Download {{name}}": "Download {{name}}",
"Access revoked": "Access revoked",
"Authorize application": "Authorize application",
"{{name}} wants to access {{workspace}}": "{{name}} wants to access {{workspace}}",
"Not you? Switch account": "Not you? Switch account",
"This application will be able to:": "This application will be able to:",
"Write": "Write",
"Invalid authorization request": "Invalid authorization request",
"Authorize": "Authorize",
"Application": "Application",
"Permissions": "Permissions",
"Authorized": "Authorized",
"Revoke access": "Revoke access",
"Revoke access for {{name}}": "Revoke access for {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.",
"Something went wrong. Please try again.": "Something went wrong. Please try again.",
"Remove {{name}}": "Remove {{name}}",
"Make sure you trust this application before authorizing it.": "Make sure you trust this application before authorizing it.",
"You will be redirected to": "You will be redirected to",
"View content without making changes.": "View content without making changes.",
"Create and modify content.": "Create and modify content.",
"Applications and AI assistants you have authorized to access your account.": "Applications and AI assistants you have authorized to access your account.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.",
"Authorized apps": "Authorized apps",
"No authorized apps yet.": "No authorized apps yet.",
"Workspace knowledge only": "Workspace knowledge only",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.",
"Toggle workspace knowledge only": "Toggle workspace knowledge only",
@@ -294,6 +294,7 @@
"Export space": "Exportar espacio",
"Export {{type}}": "Exportar {{type}}",
"File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos",
"Media": "Multimedia",
"Align left": "Alinear a la izquierda",
"Align right": "Alinear a la derecha",
"Align center": "Alinear al centro",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "Insertar regla horizontal",
"Page break": "Salto de página",
"Insert a page break for printing.": "Inserta un salto de página para imprimir.",
"Footnote": "Nota al pie",
"Insert a footnote reference.": "Insertar una referencia de nota al pie.",
"Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.",
"Upload any video from your device.": "Sube cualquier video desde tu dispositivo.",
"Upload any audio from your device.": "Sube cualquier audio desde tu dispositivo.",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Habilite el servidor MCP para permitir que asistentes de IA y herramientas interactúen con el contenido de su espacio de trabajo.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP solo está disponible en la edición empresarial de Docmost. Contacte con sales@docmost.com.",
"MCP Server URL": "URL del servidor MCP",
"Use your API key for authentication. You can manage API keys in your account settings.": "Use su clave API para la autenticación. Puede gestionar las claves API en la configuración de su cuenta.",
"Connect AI assistants with your Docmost account via OAuth.": "Conecta asistentes de IA con tu cuenta de Docmost mediante OAuth.",
"Enforce OAuth": "Exigir OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "Los asistentes de IA deben conectarse con una cuenta de Docmost mediante OAuth. No se pueden usar claves API con el servidor MCP.",
"Toggle enforce OAuth for MCP": "Activar o desactivar la exigencia de OAuth para MCP",
"Supported tools": "Herramientas compatibles",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Su espacio de trabajo tiene MCP habilitado. Use su clave API para conectar asistentes de IA.",
"MCP server URL:": "URL del servidor MCP:",
"Learn more": "Más información",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gestiona las claves de API para todos los usuarios en el espacio de trabajo. Consulta la <anchor>documentación de la API</anchor> para detalles de uso.",
@@ -1186,8 +1191,8 @@
"Default value": "Valor predeterminado",
"Delete property": "Eliminar propiedad",
"Delete view": "Eliminar vista",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Delete {{count}} rows?_one": "¿Eliminar 1 fila?",
"Delete {{count}} rows?_other": "¿Eliminar {{count}} filas?",
"Descending": "Descendente",
"Discard": "Descartar",
"Doesn't contain": "No contiene",
@@ -1286,8 +1291,51 @@
"Value": "Valor",
"View updated for everyone": "Vista actualizada para todos",
"You have unsaved changes. Do you want to discard them?": "Tiene cambios no guardados. ¿Quiere descartarlos?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} rows deleted_one": "1 fila eliminada",
"{{count}} rows deleted_other": "{{count}} filas eliminadas",
"{{count}} selected_one": "1 seleccionado",
"{{count}} selected_other": "{{count}} seleccionadas",
"Compare": "Comparar",
"Compare versions": "Comparar versiones",
"Select version from {{date}}": "Seleccionar la versión del {{date}}",
"Version actions for {{date}}": "Acciones de la versión del {{date}}",
"Comparing {{newer}} and {{older}}": "Comparando {{newer}} y {{older}}",
"Exit compare": "Salir de la comparación",
"Search attachments...": "Buscar archivos adjuntos...",
"Error loading attachments.": "Error al cargar los archivos adjuntos.",
"No attachments on this page yet.": "Todavía no hay archivos adjuntos en esta página.",
"Uploaded by {{name}}": "Subido por {{name}}",
"Download {{name}}": "Descargar {{name}}",
"Access revoked": "Acceso revocado",
"Authorize application": "Autorizar aplicación",
"{{name}} wants to access {{workspace}}": "{{name}} quiere acceder a {{workspace}}",
"Not you? Switch account": "¿No eres tú? Cambiar de cuenta",
"This application will be able to:": "Esta aplicación podrá:",
"Write": "Escribir",
"Invalid authorization request": "Solicitud de autorización no válida",
"Authorize": "Autorizar",
"Application": "Aplicación",
"Permissions": "Permisos",
"Authorized": "Autorizado",
"Revoke access": "Revocar acceso",
"Revoke access for {{name}}": "Revocar acceso para {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "¿Seguro que quieres revocar el acceso de {{name}}? La aplicación ya no podrá acceder a tu cuenta.",
"Something went wrong. Please try again.": "Algo salió mal. Inténtalo de nuevo.",
"Remove {{name}}": "Eliminar {{name}}",
"Make sure you trust this application before authorizing it.": "Asegúrate de confiar en esta aplicación antes de autorizarla.",
"You will be redirected to": "Serás redirigido a",
"View content without making changes.": "Ver contenido sin realizar cambios.",
"Create and modify content.": "Crear y modificar contenido.",
"Applications and AI assistants you have authorized to access your account.": "Aplicaciones y asistentes de IA que has autorizado para acceder a tu cuenta.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "Tu espacio de trabajo tiene MCP habilitado. Conecta asistentes de IA con tu cuenta de Docmost mediante OAuth.",
"Authorized apps": "Aplicaciones autorizadas",
"No authorized apps yet.": "Todavía no hay aplicaciones autorizadas.",
"Workspace knowledge only": "Solo conocimiento del espacio de trabajo",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Restringe AI Chat para que responda solo con información de las páginas y archivos subidos de tu espacio de trabajo. No usará conocimiento externo.",
"Toggle workspace knowledge only": "Alternar solo conocimiento del espacio de trabajo",
"Read-only mode": "Modo de solo lectura",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat puede buscar y leer el contenido del espacio de trabajo, pero no puede crear ni editar páginas.",
"Toggle AI Chat read-only mode": "Alternar modo de solo lectura de AI Chat",
"Title only": "Solo título",
"you": "tú"
}
@@ -294,6 +294,7 @@
"Export space": "Exporter l'espace",
"Export {{type}}": "Exporter {{type}}",
"File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes",
"Media": "Médias",
"Align left": "Aligner à gauche",
"Align right": "Aligner à droite",
"Align center": "Aligner au centre",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "Insérer un séparateur de règle horizontale",
"Page break": "Saut de page",
"Insert a page break for printing.": "Insérer un saut de page pour limpression.",
"Footnote": "Note de bas de page",
"Insert a footnote reference.": "Insérer une référence de note de bas de page.",
"Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.",
"Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.",
"Upload any audio from your device.": "Téléchargez n'importe quel fichier audio depuis votre appareil.",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Activez le serveur MCP pour permettre aux assistants et outils IA d'interagir avec le contenu de votre espace de travail.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP n'est disponible que dans l'édition Entreprise de Docmost. Contactez sales@docmost.com.",
"MCP Server URL": "URL du serveur MCP",
"Use your API key for authentication. You can manage API keys in your account settings.": "Utilisez votre clé API pour l'authentification. Vous pouvez gérer les clés API dans les paramètres de votre compte.",
"Connect AI assistants with your Docmost account via OAuth.": "Connectez des assistants IA à votre compte Docmost via OAuth.",
"Enforce OAuth": "Imposer OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "Les assistants IA doivent se connecter avec un compte Docmost via OAuth. Les clés API ne peuvent pas être utilisées avec le serveur MCP.",
"Toggle enforce OAuth for MCP": "Activer ou désactiver limposition dOAuth pour MCP",
"Supported tools": "Outils pris en charge",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Votre espace de travail a MCP activé. Utilisez votre clé API pour connecter des assistants IA.",
"MCP server URL:": "URL du serveur MCP :",
"Learn more": "En savoir plus",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gérez les clés API pour tous les utilisateurs de l'espace de travail. Consultez la <anchor>documentation API</anchor> pour plus de détails sur l'utilisation.",
@@ -1186,8 +1191,8 @@
"Default value": "Valeur par défaut",
"Delete property": "Supprimer la propriété",
"Delete view": "Supprimer la vue",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Delete {{count}} rows?_one": "Supprimer 1 ligne ?",
"Delete {{count}} rows?_other": "Supprimer {{count}} lignes ?",
"Descending": "Décroissant",
"Discard": "Ignorer",
"Doesn't contain": "Ne contient pas",
@@ -1286,8 +1291,51 @@
"Value": "Valeur",
"View updated for everyone": "Vue mise à jour pour tout le monde",
"You have unsaved changes. Do you want to discard them?": "Vous avez des modifications non enregistrées. Voulez-vous les ignorer ?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} rows deleted_one": "1 ligne supprimée",
"{{count}} rows deleted_other": "{{count}} lignes supprimées",
"{{count}} selected_one": "1 sélectionné(e)",
"{{count}} selected_other": "{{count}} sélectionné(e)s",
"Compare": "Comparer",
"Compare versions": "Comparer les versions",
"Select version from {{date}}": "Sélectionner la version du {{date}}",
"Version actions for {{date}}": "Actions de version pour le {{date}}",
"Comparing {{newer}} and {{older}}": "Comparaison de {{newer}} et {{older}}",
"Exit compare": "Quitter la comparaison",
"Search attachments...": "Rechercher des pièces jointes...",
"Error loading attachments.": "Erreur lors du chargement des pièces jointes.",
"No attachments on this page yet.": "Aucune pièce jointe sur cette page pour le moment.",
"Uploaded by {{name}}": "Téléversé par {{name}}",
"Download {{name}}": "Télécharger {{name}}",
"Access revoked": "Accès révoqué",
"Authorize application": "Autoriser lapplication",
"{{name}} wants to access {{workspace}}": "{{name}} souhaite accéder à {{workspace}}",
"Not you? Switch account": "Ce nest pas vous ? Changer de compte",
"This application will be able to:": "Cette application pourra :",
"Write": "Écriture",
"Invalid authorization request": "Demande dautorisation invalide",
"Authorize": "Autoriser",
"Application": "Application",
"Permissions": "Autorisations",
"Authorized": "Autorisé",
"Revoke access": "Révoquer laccès",
"Revoke access for {{name}}": "Révoquer laccès pour {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Êtes-vous sûr de vouloir révoquer laccès pour {{name}} ? Lapplication ne pourra plus accéder à votre compte.",
"Something went wrong. Please try again.": "Une erreur sest produite. Veuillez réessayer.",
"Remove {{name}}": "Supprimer {{name}}",
"Make sure you trust this application before authorizing it.": "Assurez-vous de faire confiance à cette application avant de lautoriser.",
"You will be redirected to": "Vous serez redirigé vers",
"View content without making changes.": "Afficher le contenu sans apporter de modifications.",
"Create and modify content.": "Créer et modifier du contenu.",
"Applications and AI assistants you have authorized to access your account.": "Applications et assistants IA que vous avez autorisés à accéder à votre compte.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "MCP est activé dans votre espace de travail. Connectez des assistants IA à votre compte Docmost via OAuth.",
"Authorized apps": "Applications autorisées",
"No authorized apps yet.": "Aucune application autorisée pour le moment.",
"Workspace knowledge only": "Connaissances de lespace de travail uniquement",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Limiter AI Chat aux réponses provenant uniquement des pages de votre espace de travail et des fichiers téléversés. Il nutilisera pas de connaissances externes.",
"Toggle workspace knowledge only": "Activer/désactiver les connaissances de lespace de travail uniquement",
"Read-only mode": "Mode lecture seule",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat peut rechercher et lire le contenu de lespace de travail, mais ne peut pas créer ni modifier des pages.",
"Toggle AI Chat read-only mode": "Activer/désactiver le mode lecture seule dAI Chat",
"Title only": "Titre uniquement",
"you": "vous"
}
@@ -205,7 +205,7 @@
"Templates": "Modelli",
"Theme": "Tema",
"To change your email, you have to enter your password and new email.": "Per cambiare la tua email, devi inserire la tua password e la nuova email.",
"Toggle full page width": "Attiva/disattiva larghezza completa della pagina",
"Toggle full page width": "Attiva/disattiva larghezza intera della pagina",
"Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.",
"untitled": "senza titolo",
"Untitled": "Senza titolo",
@@ -294,6 +294,7 @@
"Export space": "Esporta spazio",
"Export {{type}}": "Esporta {{type}}",
"File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}",
"Media": "Contenuti multimediali",
"Align left": "Allinea a sinistra",
"Align right": "Allinea a destra",
"Align center": "Allinea al centro",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "Inserisci divisore di regola orizzontale",
"Page break": "Interruzione di pagina",
"Insert a page break for printing.": "Inserisci un'interruzione di pagina per la stampa.",
"Footnote": "Nota a piè di pagina",
"Insert a footnote reference.": "Inserisci un riferimento a una nota a piè di pagina.",
"Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.",
"Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.",
"Upload any audio from your device.": "Carica qualsiasi audio dal tuo dispositivo.",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Abilita il server MCP per consentire ad assistenti e strumenti IA di interagire con i contenuti del tuo spazio di lavoro.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.",
"MCP Server URL": "URL del server MCP",
"Use your API key for authentication. You can manage API keys in your account settings.": "Usa la tua chiave API per l'autenticazione. Puoi gestire le chiavi API nelle impostazioni del tuo account.",
"Connect AI assistants with your Docmost account via OAuth.": "Connetti gli assistenti AI al tuo account Docmost tramite OAuth.",
"Enforce OAuth": "Rendi obbligatorio OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "Gli assistenti AI devono connettersi con un account Docmost tramite OAuth. Le chiavi API non possono essere utilizzate con il server MCP.",
"Toggle enforce OAuth for MCP": "Attiva/disattiva l'obbligo di OAuth per MCP",
"Supported tools": "Strumenti supportati",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Il tuo spazio di lavoro ha MCP abilitato. Usa la tua chiave API per collegare gli assistenti IA.",
"MCP server URL:": "URL del server MCP:",
"Learn more": "Scopri di più",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gestisci le API key per tutti gli utenti nello spazio di lavoro. Consulta la <anchor>documentazione API</anchor> per i dettagli sull'utilizzo.",
@@ -1186,8 +1191,8 @@
"Default value": "Valore predefinito",
"Delete property": "Elimina proprietà",
"Delete view": "Elimina vista",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Delete {{count}} rows?_one": "Eliminare 1 riga?",
"Delete {{count}} rows?_other": "Eliminare {{count}} righe?",
"Descending": "Decrescente",
"Discard": "Ignora",
"Doesn't contain": "Non contiene",
@@ -1286,8 +1291,51 @@
"Value": "Valore",
"View updated for everyone": "Vista aggiornata per tutti",
"You have unsaved changes. Do you want to discard them?": "Hai modifiche non salvate. Vuoi ignorarle?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} rows deleted_one": "1 riga eliminata",
"{{count}} rows deleted_other": "{{count}} righe eliminate",
"{{count}} selected_one": "1 selezionato",
"{{count}} selected_other": "{{count}} selezionati",
"Compare": "Confronta",
"Compare versions": "Confronta versioni",
"Select version from {{date}}": "Seleziona la versione del {{date}}",
"Version actions for {{date}}": "Azioni della versione del {{date}}",
"Comparing {{newer}} and {{older}}": "Confronto tra {{newer}} e {{older}}",
"Exit compare": "Esci dal confronto",
"Search attachments...": "Cerca allegati...",
"Error loading attachments.": "Errore durante il caricamento degli allegati.",
"No attachments on this page yet.": "Ancora nessun allegato in questa pagina.",
"Uploaded by {{name}}": "Caricato da {{name}}",
"Download {{name}}": "Scarica {{name}}",
"Access revoked": "Accesso revocato",
"Authorize application": "Autorizza applicazione",
"{{name}} wants to access {{workspace}}": "{{name}} vuole accedere a {{workspace}}",
"Not you? Switch account": "Non sei tu? Cambia account",
"This application will be able to:": "Questa applicazione potrà:",
"Write": "Scrivere",
"Invalid authorization request": "Richiesta di autorizzazione non valida",
"Authorize": "Autorizza",
"Application": "Applicazione",
"Permissions": "Autorizzazioni",
"Authorized": "Autorizzato",
"Revoke access": "Revoca accesso",
"Revoke access for {{name}}": "Revoca l'accesso per {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Sei sicuro di voler revocare l'accesso per {{name}}? L'applicazione non potrà più accedere al tuo account.",
"Something went wrong. Please try again.": "Qualcosa è andato storto. Riprova.",
"Remove {{name}}": "Rimuovi {{name}}",
"Make sure you trust this application before authorizing it.": "Assicurati di fidarti di questa applicazione prima di autorizzarla.",
"You will be redirected to": "Verrai reindirizzato a",
"View content without making changes.": "Visualizza il contenuto senza apportare modifiche.",
"Create and modify content.": "Crea e modifica contenuti.",
"Applications and AI assistants you have authorized to access your account.": "Applicazioni e assistenti AI che hai autorizzato ad accedere al tuo account.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "Il tuo workspace ha MCP abilitato. Connetti gli assistenti AI al tuo account Docmost tramite OAuth.",
"Authorized apps": "App autorizzate",
"No authorized apps yet.": "Ancora nessuna app autorizzata.",
"Workspace knowledge only": "Solo conoscenze del workspace",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Limita AI Chat a rispondere solo in base alle pagine del tuo workspace e ai file caricati. Non userà conoscenze esterne.",
"Toggle workspace knowledge only": "Attiva/disattiva solo conoscenze del workspace",
"Read-only mode": "Modalità di sola lettura",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat può cercare e leggere i contenuti del workspace, ma non può creare o modificare pagine.",
"Toggle AI Chat read-only mode": "Attiva/disattiva la modalità di sola lettura di AI Chat",
"Title only": "Solo titolo",
"you": "tu"
}
@@ -294,6 +294,7 @@
"Export space": "エクスポートスペース",
"Export {{type}}": "{{type}}をエクスポート",
"File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています",
"Media": "メディア",
"Align left": "左揃え",
"Align right": "右揃え",
"Align center": "中央揃え",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "区切り線を挿入します",
"Page break": "改ページ",
"Insert a page break for printing.": "印刷用に改ページを挿入します。",
"Footnote": "脚注",
"Insert a footnote reference.": "脚注参照を挿入します。",
"Upload any image from your device.": "デバイスから画像をアップロードします",
"Upload any video from your device.": "デバイスから動画をアップロードします",
"Upload any audio from your device.": "デバイスから音声ファイルをアップロードします。",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "MCP サーバーを有効にして、AI アシスタントやツールがワークスペースのコンテンツとやり取りできるようにします。",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP は Docmost のエンタープライズ版でのみ利用可能です。sales@docmost.com までお問い合わせください。",
"MCP Server URL": "MCP サーバーの URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "認証には API キーを使用してください。API キーはアカウント設定で管理できます。",
"Connect AI assistants with your Docmost account via OAuth.": "OAuth を使用して AI アシスタントを Docmost アカウントに接続します。",
"Enforce OAuth": "OAuth を必須化",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI アシスタントは OAuth を使用して Docmost アカウントに接続する必要があります。MCP サーバーでは API キーは使用できません。",
"Toggle enforce OAuth for MCP": "MCP の OAuth 必須化を切り替え",
"Supported tools": "サポートされているツール",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "このワークスペースでは MCP が有効になっています。AI アシスタントを接続するには API キーを使用してください。",
"MCP server URL:": "MCP サーバーの URL:",
"Learn more": "詳細を見る",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "ワークスペース内のすべてのユーザーのAPIキーを管理します。利用方法の詳細は<anchor>APIドキュメント</anchor>をご覧ください。",
@@ -1186,7 +1191,7 @@
"Default value": "デフォルト値",
"Delete property": "プロパティを削除",
"Delete view": "ビューを削除",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_one": "1行を削除しますか?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Descending": "降順",
"Discard": "破棄",
@@ -1286,8 +1291,51 @@
"Value": "値",
"View updated for everyone": "ビューが全員向けに更新されました",
"You have unsaved changes. Do you want to discard them?": "未保存の変更があります。破棄しますか?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_one": "1行を削除しました",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} selected_one": "1件を選択中",
"{{count}} selected_other": "{{count}} selected",
"Compare": "比較",
"Compare versions": "バージョンを比較",
"Select version from {{date}}": "{{date}} のバージョンを選択",
"Version actions for {{date}}": "{{date}} のバージョンの操作",
"Comparing {{newer}} and {{older}}": "{{newer}} と {{older}} を比較中",
"Exit compare": "比較を終了",
"Search attachments...": "添付ファイルを検索…",
"Error loading attachments.": "添付ファイルの読み込み中にエラーが発生しました。",
"No attachments on this page yet.": "このページにはまだ添付ファイルがありません。",
"Uploaded by {{name}}": "アップロード者: {{name}}",
"Download {{name}}": "{{name}} をダウンロード",
"Access revoked": "アクセスが取り消されました",
"Authorize application": "アプリケーションを認可",
"{{name}} wants to access {{workspace}}": "{{name}} が {{workspace}} へのアクセスを求めています",
"Not you? Switch account": "あなたではありませんか? アカウントを切り替え",
"This application will be able to:": "このアプリケーションで可能なこと:",
"Write": "書き込み",
"Invalid authorization request": "無効な認可リクエストです",
"Authorize": "認可",
"Application": "アプリケーション",
"Permissions": "権限",
"Authorized": "認可済み",
"Revoke access": "アクセスを取り消す",
"Revoke access for {{name}}": "{{name}} のアクセスを取り消す",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "{{name}} のアクセスを取り消してもよろしいですか? このアプリケーションは今後あなたのアカウントにアクセスできなくなります。",
"Something went wrong. Please try again.": "問題が発生しました。もう一度お試しください。",
"Remove {{name}}": "{{name}} を削除",
"Make sure you trust this application before authorizing it.": "認可する前に、このアプリケーションを信頼できることを確認してください。",
"You will be redirected to": "次へリダイレクトされます",
"View content without making changes.": "変更を加えずにコンテンツを表示します。",
"Create and modify content.": "コンテンツを作成および変更します。",
"Applications and AI assistants you have authorized to access your account.": "あなたのアカウントへのアクセスを認可したアプリケーションと AI アシスタント。",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "このワークスペースでは MCP が有効になっています。OAuth を使用して AI アシスタントを Docmost アカウントに接続します。",
"Authorized apps": "認可済みアプリ",
"No authorized apps yet.": "認可済みアプリはまだありません。",
"Workspace knowledge only": "ワークスペースの知識のみ",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "AIチャットの回答を、ワークスペース内のページとアップロードされたファイルのみに制限します。外部の知識は使用されません。",
"Toggle workspace knowledge only": "「ワークスペースの知識のみ」を切り替え",
"Read-only mode": "読み取り専用モード",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AIチャットはワークスペースのコンテンツを検索および閲覧できますが、ページの作成や編集はできません。",
"Toggle AI Chat read-only mode": "AIチャットの読み取り専用モードを切り替え",
"Title only": "タイトルのみ",
"you": "あなた"
}
+140 -92
View File
@@ -2,23 +2,23 @@
"Account": "계정",
"Active": "활성",
"Add": "추가",
"Add group members": "팀에 사용자 추가",
"Add groups": "팀 생성",
"Add group members": "그룹에 멤버 추가",
"Add groups": "그룹 추가",
"Add members": "사용자 추가",
"Add to groups": "에 추가",
"Add to groups": "그룹에 추가",
"Add space members": "Space에 사용자 추가",
"Add to favorites": "즐겨찾기에 추가",
"Admin": "관리자",
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "이 을 삭제하시겠습니까? 해당 팀에 속한 사용자들은이 가진 모든 권한을 잃게 됩니다.",
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "이 그룹을 삭제하시겠습니까? 그룹 멤버는그룹이 가진 모든 권한을 잃게 됩니다.",
"Are you sure you want to delete this page?": "이 페이지를 삭제하시겠습니까?",
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "이 사용자를 에서 제거하시겠습니까? 사용자는 이 이 가진 모든 권한을 잃게 됩니다.",
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "이 사용자를 그룹에서 제거하시겠습니까? 사용자는 이 그룹이 가진 모든 권한을 잃게 됩니다.",
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "이 사용자를 Space에서 제거하시겠습니까? 사용자는 이 Space에 대한 모든 접근 권한을 잃게 됩니다.",
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "이 버전으로 복원하시겠습니까? 저장되지 않은 모든 변경사항이 손실됩니다.",
"Can become members of groups and spaces in workspace": "Workspace 내 팀 및 Space의 사용자가 될 수 있습니다.",
"Can become members of groups and spaces in workspace": "워크스페이스의 그룹 및 스페이스의 멤버가 될 수 있",
"Can create and edit pages in space.": "Space에 페이지를 생성하고 편집할 수 있습니다.",
"Can edit": "편집할 수 있음",
"Can manage workspace": "Workspace를 관리할 수 있음",
"Can manage workspace but cannot delete it": "Workspace를 관리할 수 있지만, 삭제는 불가능.",
"Can manage workspace": "워크스페이스를 관리할 수 있음",
"Can manage workspace but cannot delete it": "워크스페이스를 관리할 수 있지만, 삭제할 수 없음",
"Can view": "볼 수 있음",
"Can view pages in space but not edit.": "Space의 페이지를 볼 수 있지만, 편집은 불가능.",
"Cancel": "취소",
@@ -33,17 +33,17 @@
"Copy as Markdown": "Markdown으로 복사",
"Copy link": "링크 복사",
"Create": "생성",
"Create group": " 생성",
"Create group": "그룹 생성",
"Create page": "페이지 생성",
"Create space": "Space 생성",
"Create workspace": "Workspace 생성",
"Create workspace": "워크스페이스 생성",
"Current password": "현재 비밀번호",
"Dark": "어두운",
"Date": "날짜",
"Delete": "삭제",
"Remove from page": "페이지에서 제거",
"Base options": "베이스 옵션",
"Delete group": " 삭제",
"Delete group": "그룹 삭제",
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "이 페이지를 삭제하시겠습니까? 하위 페이지와 페이지 기록이 모두 삭제됩니다. 이 작업은 되돌릴 수 없습니다.",
"Description": "설명",
"Details": "세부사항",
@@ -58,7 +58,7 @@
"e.g Space for sales team to collaborate": "예: 영업 팀이 협업하는 스페이스",
"Edit": "편집",
"Read": "읽기",
"Edit group": " 편집",
"Edit group": "그룹 편집",
"Email": "이메일",
"Enter a strong password": "강력한 비밀번호를 입력하세요",
"Enter valid email addresses separated by comma or space max_50": "유효한 이메일 주소를 쉼표나 공백으로 구분하여 입력하세요 [최대: 50]",
@@ -103,10 +103,10 @@
"Full page width": "전체 페이지 너비",
"Full width": "전체 너비",
"General": "일반",
"Group": "",
"Group description": " 설명",
"Group name": " 이름",
"Groups": "",
"Group": "그룹",
"Group description": "그룹 설명",
"Group name": "그룹 이름",
"Groups": "그룹",
"Has full access to space settings and pages.": "Space 설정과 페이지에 대한 전체 접근 권한이 있습니다.",
"Home": "홈",
"Import pages": "페이지 가져오기",
@@ -119,14 +119,14 @@
"Invite new members": "새 사용자 초대",
"Invite People": "사용자 초대",
"Invited members who are yet to accept their invitation will appear here.": "초대를 아직 수락하지 않은 초대된 사용자가 여기에 표시됩니다.",
"Invited members will be granted access to spaces the groups can access": "초대된 사용자는 팀이 접근할 수 있는 Space에 대한 접근 권한을 받게 됩니다",
"Join the workspace": "Workspace 참여",
"Invited members will be granted access to spaces the groups can access": "초대된 멤버에게는 그룹이 접근할 수 있는 스페이스에 대한 액세스 권한이 부여됩니다",
"Join the workspace": "워크스페이스 참여",
"Language": "언어",
"Light": "밝은",
"Link copied": "링크 복사됨",
"Login": "로그인",
"Logout": "로그아웃",
"Manage Group": " 관리",
"Manage Group": "그룹 관리",
"Manage members": "사용자 관리",
"member": "사용자",
"Member": "사용자",
@@ -139,7 +139,7 @@
"New email": "새 이메일",
"New page": "새 페이지",
"New password": "새 비밀번호",
"No group found": "그룹을 찾을 수 없습니다",
"No group found": "그룹을 찾을 수 없",
"No page history saved yet.": "아직 저장된 페이지 기록이 없습니다.",
"No pages yet": "아직 페이지가 없습니다",
"No shared pages": "공유된 페이지가 없습니다.",
@@ -148,7 +148,7 @@
"Overview": "개요",
"Owner": "소유자",
"page": "페이지",
"Page deleted successfully": "페이지 삭제 완료",
"Page deleted successfully": "페이지 삭제",
"Page history": "페이지 기록",
"Select version": "버전 선택",
"Highlight changes": "변경 사항 강조",
@@ -156,7 +156,7 @@
"Pages": "페이지",
"pages": "페이지",
"Password": "비밀번호",
"Password changed successfully": "비밀번호 변경 완료",
"Password changed successfully": "비밀번호 변경",
"People": "사용자",
"Pending": "대기 중",
"Please confirm your action": "작업을 확인해 주세요",
@@ -166,15 +166,15 @@
"Recently updated": "최근 업데이트",
"Remove": "제거",
"Remove from favorites": "즐겨찾기에서 제거",
"Remove group member": "팀에서 사용자 제거",
"Remove group member": "그룹 멤버 제거",
"Remove space member": "Space에서 사용자 제거",
"Restore": "복원",
"Role": "역할",
"Save": "저장",
"Search": "검색",
"Search for groups": " 검색",
"Search for groups": "그룹 검색",
"Search for users": "사용자 검색",
"Search for users and groups": "사용자 및 검색",
"Search for users and groups": "사용자 및 그룹 검색",
"Search...": "검색...",
"Select language": "언어 선택",
"Select role": "역할 선택",
@@ -199,8 +199,8 @@
"Search for spaces": "스페이스 검색",
"Start typing to search...": "검색하려면 입력을 시작하세요...",
"Status": "상태",
"Successfully imported": "가져오기에 성공했습니다",
"Successfully restored": "복원에 성공했습니다",
"Successfully imported": "가져오기 완료",
"Successfully restored": "복원 완료",
"System settings": "시스템 설정",
"Templates": "템플릿",
"Theme": "테마",
@@ -209,7 +209,7 @@
"Unable to import pages. Please try again.": "페이지를 가져올 수 없습니다. 다시 시도해주세요.",
"untitled": "제목 없음",
"Untitled": "제목 없음",
"Updated successfully": "성공적으로 업데이트되었습니다",
"Updated successfully": "업데이트 완료",
"User": "사용자",
"Workspace": "워크스페이스",
"Workspace Name": "워크스페이스 이름",
@@ -245,15 +245,15 @@
"Are you sure you want to delete this comment?": "이 댓글을 삭제하시겠습니까?",
"Delete chat": "채팅 삭제",
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "'{{title}}'을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"Comment created successfully": "댓글 생성 완료",
"Comment created successfully": "댓글 생성",
"Error creating comment": "댓글 생성 오류",
"Comment updated successfully": "댓글 업데이트 완료",
"Comment updated successfully": "댓글 업데이트",
"Failed to update comment": "댓글 업데이트 실패",
"Comment deleted successfully": "댓글 삭제 완료",
"Comment deleted successfully": "댓글 삭제",
"Failed to delete comment": "댓글 삭제 실패",
"Comment resolved successfully": "댓글 처리 완료",
"Comment re-opened successfully": "댓글이 성공적으로 다시 열렸습니다",
"Comment unresolved successfully": "댓글 해결이 성공적으로 취소되었습니다",
"Comment resolved successfully": "댓글이 해결로 표시됨",
"Comment re-opened successfully": "댓글이 열린 상태로 표시됨",
"Comment unresolved successfully": "댓글이 미해결로 표시됨",
"Failed to resolve comment": "댓글 처리 실패",
"Resolve comment": "댓글 해결",
"Unresolve comment": "댓글 해결 취소",
@@ -263,19 +263,19 @@
"Are you sure you want to unresolve this comment thread?": "이 댓글 스레드를 미해결로 변경하시겠습니까?",
"Resolved": "해결됨",
"No active comments.": "활성 댓글이 없습니다.",
"Revoke invitation": "초대 취소",
"Revoke": "취소",
"Revoke invitation": "초대 폐기",
"Revoke": "폐기",
"Don't": "하지 않음",
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "이 초대를 취소하시겠습니까? 사용자가 Workspace에 참여할 수 없게 됩니다.",
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "이 초대를 폐기하시겠습니까? 사용자가 워크스페이스에 참여할 수 없게 됩니다.",
"Resend invitation": "초대 재전송",
"Anyone with this link can join this workspace.": "이 링크를 가진 모든 사용자가 이 Workspace에 참여할 수 있습니다.",
"Anyone with this link can join this workspace.": "이 링크를 가진 모든 사용자가 이 워크스페이스에 참여할 수 있습니다.",
"Invite link": "초대 링크",
"Copy": "복사",
"Copy to space": "스페이스로 복사",
"Copied": "복사됨",
"Duplicate": "복제",
"Select a user": "사용자 선택",
"Select a group": " 선택",
"Select a group": "그룹 선택",
"Export all pages and attachments in this space.": "이 Space의 모든 페이지와 첨부파일을 내보냅니다.",
"Delete space": "Space 삭제",
"Are you sure you want to delete this space?": "이 Space을 삭제하시겠습니까?",
@@ -290,10 +290,11 @@
"Export failed:": "내보내기 실패:",
"export error": "내보내기 오류",
"Export page": "페이지 내보내기",
"Export successful": "내보내기 성공",
"Export successful": "내보내기 완료",
"Export space": "Space 내보내기",
"Export {{type}}": "{{type}} 내보내기",
"File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다",
"Media": "미디어",
"Align left": "왼쪽 정렬",
"Align right": "오른쪽 정렬",
"Align center": "가운데 정렬",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "가로 구분선 삽입",
"Page break": "페이지 나누기",
"Insert a page break for printing.": "인쇄용 페이지 나누기를 삽입합니다.",
"Footnote": "각주",
"Insert a footnote reference.": "각주 참조를 삽입합니다.",
"Upload any image from your device.": "기기에서 이미지를 업로드하세요.",
"Upload any video from your device.": "기기에서 비디오를 업로드하세요.",
"Upload any audio from your device.": "기기에서 오디오를 업로드하세요.",
@@ -447,12 +450,12 @@
"Today, {{time}}": "오늘, {{time}}",
"Yesterday, {{time}}": "어제, {{time}}",
"now": "지금",
"Space created successfully": "스페이스가 성공적으로 생성되었습니다",
"Space updated successfully": "스페이스가 성공적으로 업데이트되었습니다",
"Space deleted successfully": "스페이스가 성공적으로 삭제되었습니다",
"Members added successfully": "멤버가 성공적으로 추가되었습니다",
"Member removed successfully": "멤버가 성공적으로 제거되었습니다",
"Member role updated successfully": "멤버 역할이 성공적으로 업데이트되었습니다",
"Space created successfully": "스페이스 생성됨",
"Space updated successfully": "스페이스 업데이트됨",
"Space deleted successfully": "스페이스 삭제됨",
"Members added successfully": "멤버 추가됨",
"Member removed successfully": "멤버 삭제됨",
"Member role updated successfully": "멤버 역할 업데이트됨",
"Created by: <b>{{creatorName}}</b>": "작성자: <b>{{creatorName}}</b>",
"Created at: {{time}}": "작성 시간: {{time}}",
"Edited by {{name}} {{time}}": "{{name}}님이 {{time}}에 편집함",
@@ -465,7 +468,7 @@
"Choose {{format}} file": "{{format}} 파일 선택",
"Reading": "읽기",
"Delete member": "멤버 삭제",
"Member deleted successfully": "멤버가 성공적으로 삭제되었습니다",
"Member deleted successfully": "멤버 삭제됨",
"Are you sure you want to delete this workspace member? This action is irreversible.": "이 워크스페이스 멤버를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"Deactivate member": "멤버 비활성화",
"Activate member": "멤버 활성화",
@@ -498,32 +501,32 @@
"Delete share": "공유 삭제",
"Are you sure you want to delete this shared link?": "이 공유 링크를 삭제하시겠습니까?",
"Publicly shared pages from spaces you are a member of will appear here": "회원으로 속한 스페이스의 공개 공유 페이지가 여기에 표시됩니다",
"Share deleted successfully": "공유가 성공적으로 삭제되었습니다",
"Share deleted successfully": "공유 삭제됨",
"Share not found": "공유를 찾을 수 없습니다",
"Failed to share page": "페이지 공유에 실패했습니다",
"Disable public sharing": "공유 비활성화",
"Prevent members from sharing pages publicly.": "멤버들이 페이지를 공개적으로 공유하지 못하도록 방지하십시오.",
"Toggle public sharing": "공유 전환",
"Toggle space public sharing": "공간 공유 전환",
"Disable public sharing": "공개 공유 비활성화",
"Prevent members from sharing pages publicly.": "멤버 페이지를 공개적으로 공유하지 못하게 합니다.",
"Toggle public sharing": "공개 공유 전환",
"Toggle space public sharing": "스페이스 공개 공유 전환",
"Allow viewers to comment": "뷰어가 댓글을 달 수 있도록 허용",
"Allow viewers to add comments on pages in this space.": "이 공간 내 페이지에 뷰어가 댓글을 추가할 수 있도록 허용합니다.",
"Toggle viewer comments": "뷰어 댓글 전환",
"Public sharing is disabled at the workspace level": "워크스페이스 수준에서 공유가 비활성화되었습니다.",
"Prevent pages in this space from being shared publicly.": "이 공간의 페이지가 공개적으로 공유되지 않도록 방지하십시오.",
"Public sharing is disabled at the workspace level": "공개 공유가 워크스페이스 수준에서 비활성화됨",
"Prevent pages in this space from being shared publicly.": "이 스페이스의 페이지가 공개 공유되지 않도록 합니다.",
"Page permissions": "페이지 권한},{",
"Control who can view and edit individual pages. Available with an enterprise license.": "개별 페이지의 조회 및 편집 권한을 제어합니다. 엔터프라이즈 라이선스에서 이용 가능합니다.",
"Enable public sharing": "공유 활성화",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "공유를 활성화하시겠습니까? 멤버들이 페이지를 공개적으로 공유할 수 있게 됩니다.",
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "정말로 공유를 비활성화하시겠습니까? 이 워크스페이스의 모든 기존 공유 링크가 삭제됩니다.",
"Are you sure you want to enable public sharing for this space?": "이 공간의 공유를 활성화하시겠습니까?",
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "정말로 공유를 비활성화하시겠습니까? 이 공간의 모든 기존 공유 링크가 삭제됩니다.",
"Public sharing is disabled": "공유가 비활성화되었습니다.",
"Public sharing has been disabled at the workspace level.": "워크스페이스 수준에서 공유가 비활성화되었습니다.",
"Public sharing has been disabled for this space.": "이 공간의 공유가 비활성화되었습니다.",
"Enable public sharing": "공개 공유 활성화",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "공개 공유를 활성화하시겠습니까? 멤버 페이지를 공개적으로 공유할 수 있게 됩니다.",
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "공개 공유를 비활성화하시겠습니까? 이 워크스페이스의 기존 공유 링크가 모두 삭제됩니다.",
"Are you sure you want to enable public sharing for this space?": "이 스페이스에 대해 공개 공유를 활성화하시겠습니까?",
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "공개 공유를 비활성화하시겠습니까? 이 스페이스의 기존 공유 링크가 모두 삭제됩니다.",
"Public sharing is disabled": "공개 공유가 비활성화",
"Public sharing has been disabled at the workspace level.": "공개 공유가 워크스페이스 수준에서 비활성화되었습니다.",
"Public sharing has been disabled for this space.": "이 스페이스의 공개 공유가 비활성화되었습니다.",
"Copy page": "페이지 복사",
"Copy page to a different space.": "다른 공간으로 페이지 복사하기.",
"Page copied successfully": "페이지가 성공적으로 복사되었습니다",
"Page duplicated successfully": "페이지가 성공적으로 복제되었습니다",
"Page copied successfully": "페이지 복사됨",
"Page duplicated successfully": "페이지 복제됨",
"Find": "찾기",
"Not found": "찾을 수 없음",
"Previous Match (Shift+Enter)": "이전 일치 항목 (Shift+Enter)",
@@ -539,13 +542,13 @@
"Error": "오류",
"Failed to disable MFA": "MFA 비활성화에 실패했습니다",
"Disable two-factor authentication": "2단계 인증 비활성화",
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "이중 인증을 비활성화하면 계정의 보안이 낮아집니다. 로그인 시 비밀번호만 필요하게 됩니다.",
"Please enter your password to disable two-factor authentication:": "이중 인증 비활성화를 위해 비밀번호를 입력하세요:",
"Two-factor authentication has been enabled": "2단계 인증이 활성화되었습니다",
"Two-factor authentication has been disabled": "2단계 인증이 비활성화되었습니다",
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "2단계 인증을 비활성화하면 계정의 보안이 낮아집니다. 로그인 시 비밀번호만 필요하게 됩니다.",
"Please enter your password to disable two-factor authentication:": "2단계 인증 비활성화를 위해 비밀번호를 입력하세요:",
"Two-factor authentication has been enabled": "2단계 인증이 활성화",
"Two-factor authentication has been disabled": "2단계 인증이 비활성화",
"2-step verification": "2단계 인증",
"Protect your account with an additional verification layer when signing in.": "로그인 시 추가 인증 단계를 통해 계정을 보호하세요.",
"Two-factor authentication is active on your account.": "이중 인증이 계정에 활성화되어 있습니다.",
"Two-factor authentication is active on your account.": "2단계 인증이 계정에 활성화되어 있습니다.",
"Add 2FA method": "2FA 방법 추가",
"Backup codes": "백업 코드",
"Disable": "비활성화",
@@ -575,13 +578,13 @@
"Save your backup codes": "백업 코드를 저장하세요",
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "인증 앱에 대한 접근 권한을 잃은 경우, 이 코드를 사용하여 귀하의 계정에 접근할 수 있습니다. 각 코드는 한 번만 사용할 수 있습니다.",
"Print": "인쇄",
"Two-factor authentication has been set up. Please log in again.": "이중 인증이 설정되었습니다. 다시 로그인해 주세요.",
"Two-Factor authentication required": "2단계 인증 필요합니다",
"Two-factor authentication has been set up. Please log in again.": "2단계 인증이 설정되었습니다. 다시 로그인해 주세요.",
"Two-Factor authentication required": "2단계 인증 필요",
"Your workspace requires two-factor authentication for all users": "이 워크스페이스는 모든 사용자에게 2단계 인증을 요구합니다",
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "워크스페이스 접근을 계속하려면 이중 인증을 설정해야 합니다. 이는 계정에 추가 보안 계층을 추가합니다.",
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "워크스페이스에 계속 액세스하려면 2단계 인증을 설정해야 합니다. 이는 계정의 보안을 한층 강화합니다.",
"Set up two-factor authentication": "2단계 인증 설정",
"Cancel and logout": "취소하고 로그아웃",
"Your workspace requires two-factor authentication. Please set it up to continue.": "워크스페이스에서는 이중 인증이 필요합니다. 계속하려면 설정해 주세요.",
"Your workspace requires two-factor authentication. Please set it up to continue.": "워크스페이스에서는 2단계 인증이 필요합니다. 계속하려면 설정해 주세요.",
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "인증앱에서 얻은 인증 코드를 요구하여 계정의 보안에 추가적인 계층을 추가합니다.",
"Password is required": "비밀번호는 필수입니다",
"Password must be at least 8 characters": "비밀번호는 8자 이상이어야 합니다",
@@ -590,10 +593,10 @@
"Enter the 6-digit code found in your authenticator app": "인증 앱에 표시된 6자리 코드를 입력하세요",
"Need help authenticating?": "인증에 도움이 필요하십니까?",
"MFA QR Code": "MFA QR 코드",
"Account created successfully. Please log in to set up two-factor authentication.": "계정이 성공적으로 생성되었습니다. 이중 인증을 설정하려면 로그인해 주세요.",
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "비밀번호 재설정 성공. 새 비밀번호로 로그인하여 이중 인증을 완료하세요.",
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "비밀번호 재설정 성공. 새 비밀번호로 로그인하여 이중 인증을 설정하세요.",
"Password reset was successful. Please log in with your new password.": "비밀번호 재설정이 성공적으로 완료되었습니다. 새 비밀번호로 로그인하세요.",
"Account created successfully. Please log in to set up two-factor authentication.": "계정 생성됨. 2단계 인증을 설정하려면 로그인해 주세요.",
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "비밀번호 재설정. 새 비밀번호로 로그인하여 2단계 인증을 완료하세요.",
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "비밀번호 재설정. 새 비밀번호로 로그인하여 2단계 인증을 설정하세요.",
"Password reset was successful. Please log in with your new password.": "비밀번호 재설정. 새 비밀번호로 로그인하세요.",
"Two-factor authentication": "2단계 인증",
"Use authenticator app instead": "대신 인증 앱 사용",
"Verify backup code": "백업 코드 확인",
@@ -615,7 +618,7 @@
"Permanently delete": "영구 삭제",
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b>님이 {{time}}에 이 페이지를 휴지통으로 이동했습니다.",
"Page moved to trash": "페이지가 휴지통으로 이동되었습니다",
"Page restored successfully": "페이지가 성공적으로 복원되었습니다",
"Page restored successfully": "페이지 복원됨",
"Deleted by": "삭제한 사람",
"Deleted at": "삭제 시간",
"Preview": "미리보기",
@@ -638,7 +641,7 @@
"Only users with email addresses from these domains can signup via SSO.": "이 도메인의 이메일 주소를 가진 사용자만 SSO를 통해 가입할 수 있습니다.",
"Enter valid domain names separated by comma or space": "쉼표 또는 공백으로 구분된 유효한 도메인 이름을 입력하세요",
"Enforce two-factor authentication": "2단계 인증 강제",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "시행되면 모든 멤버가 작업 공간에 액세스하기 위해 이중 인증을 활성화해야 합니다.",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "시행되면 모든 멤버가 워크스페이스에 액세스하기 위해 2단계 인증을 활성화해야 합니다.",
"Toggle MFA enforcement": "MFA 강제 설정 전환",
"Display name": "표시 이름",
"Allow signup": "가입 허용",
@@ -657,7 +660,7 @@
"Remove image": "이미지 제거",
"Failed to remove image": "이미지 제거 실패",
"Image exceeds 10MB limit.": "이미지가 10MB 용량 제한을 초과합니다.",
"Image removed successfully": "이미지가 성공적으로 제거되었습니다",
"Image removed successfully": "이미지 삭제됨",
"API key": "API 키",
"API keys": "API 키",
"API management": "API 관리",
@@ -669,7 +672,7 @@
"Last use": "최근 사용",
"No API keys found": "API 키를 찾을 수 없습니다",
"No expiration": "유효기간 없음",
"Revoked successfully": "성공적으로 취소되었습니다",
"Revoked successfully": "폐기 완료",
"Select expiration date": "만료일 선택",
"This action cannot be undone. Any applications using this API key will stop working.": "이 작업은 되돌릴 수 없습니다. 이 API 키를 사용하는 모든 응용 프로그램이 작동을 멈출 것입니다.",
"Update": "업데이트",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "AI 어시스턴트와 도구가 워크스페이스 콘텐츠와 상호작용할 수 있도록 MCP 서버를 활성화하세요.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP는 Docmost 엔터프라이즈 에디션에서만 제공됩니다. sales@docmost.com으로 문의하세요.",
"MCP Server URL": "MCP 서버 URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "인증을 위해 API 키를 사용하세요. API 키는 계정 설정에서 관리할 수 있습니다.",
"Connect AI assistants with your Docmost account via OAuth.": "OAuth를 통해 AI 도우미를 Docmost 계정에 연결합니다.",
"Enforce OAuth": "OAuth 강제",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI 도우미는 OAuth를 통해 Docmost 계정에 연결해야 합니다. MCP 서버에서는 API 키를 사용할 수 없습니다.",
"Toggle enforce OAuth for MCP": "MCP에 대해 OAuth 강제 전환",
"Supported tools": "지원되는 도구",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "워크스페이스에 MCP가 활성화되어 있습니다. AI 어시스턴트를 연결하려면 API 키를 사용하세요.",
"MCP server URL:": "MCP 서버 URL:",
"Learn more": "자세히 알아보기",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "워크스페이스의 모든 사용자를 위한 API 키를 관리하세요. 사용 방법은 <anchor>API 문서</anchor>를 참고하세요.",
@@ -939,8 +944,8 @@
"Try again": "다시 시도",
"Untitled chat": "제목 없는 채팅",
"What can I help you with?": "무엇을 도와드릴까요?",
"Are you sure you want to revoke this {{credential}}": "이 {{credential}}을 취소하시겠습니까?",
"Automatically provision users and groups from your identity provider via SCIM.": "SCIM을 통해 ID 공급자에서 사용자와 그룹을 자동으로 프로비저닝합니다.",
"Are you sure you want to revoke this {{credential}}": "이 {{credential}}을(를) 폐기하시겠습니까?",
"Automatically provision users and groups from your identity provider via SCIM.": "SCIM을 통해 ID 공급자로부터 사용자와 그룹을 자동으로 프로비저닝합니다.",
"Configure your identity provider with this URL to provision users and groups.": "사용자와 그룹을 프로비저닝할 수 있도록 이 URL로 ID 공급자를 구성하세요.",
"Create {{credential}}": "{{credential}} 만들기",
"{{credential}} created": "{{credential}} 생성됨",
@@ -953,7 +958,7 @@
"Important": "중요",
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "지금 {{credential}}를 복사해 두세요. 다시는 볼 수 없습니다!",
"Never": "안 함",
"Revoke {{credential}}": "{{credential}} 취소",
"Revoke {{credential}}": "{{credential}} 폐기",
"SCIM endpoint URL": "SCIM 엔드포인트 URL",
"SCIM provisioning": "SCIM 프로비저닝",
"SCIM takes precedence over SSO group sync while enabled.": "SCIM이 활성화되어 있는 동안에는 SSO 그룹 동기화보다 SCIM이 우선 적용됩니다.",
@@ -1186,8 +1191,8 @@
"Default value": "기본값",
"Delete property": "속성 삭제",
"Delete view": "보기 삭제",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Delete {{count}} rows?_one": "1개 행을 삭제하시겠습니까?",
"Delete {{count}} rows?_other": "{{count}}개 행을 삭제하시겠습니까?",
"Descending": "내림차순",
"Discard": "삭제",
"Doesn't contain": "포함하지 않음",
@@ -1286,8 +1291,51 @@
"Value": "값",
"View updated for everyone": "보기가 모두에게 업데이트되었습니다",
"You have unsaved changes. Do you want to discard them?": "저장되지 않은 변경 사항이 있습니다. 버리시겠습니까?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} rows deleted_one": "1개 행 삭제 완료",
"{{count}} rows deleted_other": "{{count}}개 행 삭제 완료",
"{{count}} selected_one": "1개 선택됨",
"{{count}} selected_other": "{{count}}개 선택됨",
"Compare": "비교",
"Compare versions": "버전 비교",
"Select version from {{date}}": "{{date}}의 버전 선택",
"Version actions for {{date}}": "{{date}} 버전 작업",
"Comparing {{newer}} and {{older}}": "{{newer}} 및 {{older}} 비교 중",
"Exit compare": "비교 종료",
"Search attachments...": "첨부파일 검색...",
"Error loading attachments.": "첨부파일을 불러오는 중 오류가 발생했습니다.",
"No attachments on this page yet.": "이 페이지에는 아직 첨부파일이 없습니다.",
"Uploaded by {{name}}": "업로드한 사람: {{name}}",
"Download {{name}}": "{{name}} 다운로드",
"Access revoked": "액세스가 취소되었습니다",
"Authorize application": "애플리케이션 승인",
"{{name}} wants to access {{workspace}}": "{{name}}에서 {{workspace}}에 액세스하려고 합니다",
"Not you? Switch account": "본인이 아니신가요? 계정을 전환하세요",
"This application will be able to:": "이 애플리케이션이 수행할 수 있는 작업:",
"Write": "쓰기",
"Invalid authorization request": "잘못된 승인 요청",
"Authorize": "승인",
"Application": "애플리케이션",
"Permissions": "권한",
"Authorized": "승인됨",
"Revoke access": "액세스 취소",
"Revoke access for {{name}}": "{{name}}의 액세스 취소",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "정말로 {{name}}의 액세스를 취소하시겠습니까? 이 애플리케이션은 더 이상 회원님의 계정에 액세스할 수 없습니다.",
"Something went wrong. Please try again.": "문제가 발생했습니다. 다시 시도해 주세요.",
"Remove {{name}}": "{{name}} 제거",
"Make sure you trust this application before authorizing it.": "이 애플리케이션을 승인하기 전에 신뢰할 수 있는지 확인하세요.",
"You will be redirected to": "다음으로 리디렉션됩니다",
"View content without making changes.": "변경하지 않고 콘텐츠를 봅니다.",
"Create and modify content.": "콘텐츠를 생성하고 수정합니다.",
"Applications and AI assistants you have authorized to access your account.": "회원님의 계정에 액세스하도록 승인한 애플리케이션 및 AI 도우미입니다.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "워크스페이스에서 MCP가 활성화되어 있습니다. OAuth를 통해 AI 도우미를 Docmost 계정에 연결하세요.",
"Authorized apps": "승인된 앱",
"No authorized apps yet.": "아직 승인된 앱이 없습니다.",
"Workspace knowledge only": "워크스페이스 지식만 사용",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "AI 채팅이 워크스페이스 페이지와 업로드된 파일의 내용만 바탕으로 답변하도록 제한합니다. 외부 지식은 사용하지 않습니다.",
"Toggle workspace knowledge only": "워크스페이스 지식만 사용 전환",
"Read-only mode": "읽기 전용 모드",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI 채팅은 워크스페이스 콘텐츠를 검색하고 읽을 수 있지만, 페이지를 생성하거나 편집할 수는 없습니다.",
"Toggle AI Chat read-only mode": "AI 채팅 읽기 전용 모드 전환",
"Title only": "제목만",
"you": "회원님"
}
@@ -294,6 +294,7 @@
"Export space": "Exporteer ruimte",
"Export {{type}}": "Exporteer {{type}}",
"File exceeds the {{limit}} attachment limit": "Bestand overschrijdt de bijlagelimiet van {{limit}}",
"Media": "Media",
"Align left": "Links uitlijnen",
"Align right": "Rechts uitlijnen",
"Align center": "Centreren",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "Horizontale lijn invoegen",
"Page break": "Pagina-einde",
"Insert a page break for printing.": "Voeg een pagina-einde in voor het afdrukken.",
"Footnote": "Voetnoot",
"Insert a footnote reference.": "Voeg een voetnootverwijzing in.",
"Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.",
"Upload any video from your device.": "Upload een video vanaf uw apparaat.",
"Upload any audio from your device.": "Upload een audio vanaf uw apparaat.",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Schakel de MCP-server in zodat AI-assistenten en tools kunnen interageren met de inhoud van uw werkruimte.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP is alleen beschikbaar in de Docmost Enterprise-editie. Neem contact op met sales@docmost.com.",
"MCP Server URL": "MCP-server-URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "Gebruik uw API-sleutel voor authenticatie. U kunt API-sleutels beheren in uw accountinstellingen.",
"Connect AI assistants with your Docmost account via OAuth.": "Verbind AI-assistenten met je Docmost-account via OAuth.",
"Enforce OAuth": "OAuth afdwingen",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI-assistenten moeten verbinding maken met een Docmost-account via OAuth. API-sleutels kunnen niet worden gebruikt met de MCP-server.",
"Toggle enforce OAuth for MCP": "OAuth afdwingen voor MCP in- of uitschakelen",
"Supported tools": "Ondersteunde tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "In uw werkruimte is MCP ingeschakeld. Gebruik uw API-sleutel om AI-assistenten te koppelen.",
"MCP server URL:": "MCP-server-URL:",
"Learn more": "Meer informatie",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Beheer API-sleutels voor alle gebruikers in de werkruimte. Bekijk de <anchor>API-documentatie</anchor> voor gebruiksdetails.",
@@ -1289,5 +1294,48 @@
"{{count}} rows deleted_one": "1 rij verwijderd",
"{{count}} rows deleted_other": "{{count}} rijen verwijderd",
"{{count}} selected_one": "1 geselecteerd",
"{{count}} selected_other": "{{count}} geselecteerd"
"{{count}} selected_other": "{{count}} geselecteerd",
"Compare": "Vergelijken",
"Compare versions": "Versies vergelijken",
"Select version from {{date}}": "Selecteer versie van {{date}}",
"Version actions for {{date}}": "Versieacties voor {{date}}",
"Comparing {{newer}} and {{older}}": "{{newer}} en {{older}} vergelijken",
"Exit compare": "Vergelijken afsluiten",
"Search attachments...": "Bijlagen zoeken...",
"Error loading attachments.": "Fout bij het laden van bijlagen.",
"No attachments on this page yet.": "Er zijn nog geen bijlagen op deze pagina.",
"Uploaded by {{name}}": "Geüpload door {{name}}",
"Download {{name}}": "{{name}} downloaden",
"Access revoked": "Toegang ingetrokken",
"Authorize application": "Applicatie autoriseren",
"{{name}} wants to access {{workspace}}": "{{name}} wil toegang tot {{workspace}}",
"Not you? Switch account": "Ben jij dit niet? Wissel van account",
"This application will be able to:": "Deze applicatie kan het volgende doen:",
"Write": "Schrijven",
"Invalid authorization request": "Ongeldig autorisatieverzoek",
"Authorize": "Autoriseren",
"Application": "Applicatie",
"Permissions": "Machtigingen",
"Authorized": "Geautoriseerd",
"Revoke access": "Toegang intrekken",
"Revoke access for {{name}}": "Toegang voor {{name}} intrekken",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Weet je zeker dat je de toegang voor {{name}} wilt intrekken? De applicatie heeft dan geen toegang meer tot je account.",
"Something went wrong. Please try again.": "Er is iets misgegaan. Probeer het opnieuw.",
"Remove {{name}}": "{{name}} verwijderen",
"Make sure you trust this application before authorizing it.": "Zorg ervoor dat je deze applicatie vertrouwt voordat je haar autoriseert.",
"You will be redirected to": "Je wordt doorgestuurd naar",
"View content without making changes.": "Inhoud bekijken zonder wijzigingen aan te brengen.",
"Create and modify content.": "Inhoud maken en wijzigen.",
"Applications and AI assistants you have authorized to access your account.": "Applicaties en AI-assistenten die je hebt geautoriseerd om toegang te krijgen tot je account.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "MCP is ingeschakeld voor je werkruimte. Verbind AI-assistenten met je Docmost-account via OAuth.",
"Authorized apps": "Geautoriseerde apps",
"No authorized apps yet.": "Nog geen geautoriseerde apps.",
"Workspace knowledge only": "Alleen werkruimtek kennis",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Beperk AI Chat tot het beantwoorden op basis van alleen je werkruimtepagina's en geüploade bestanden. Er wordt geen externe kennis gebruikt.",
"Toggle workspace knowledge only": "Alleen werkruimtekennis in-/uitschakelen",
"Read-only mode": "Alleen-lezenmodus",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat kan werkruimte-inhoud doorzoeken en lezen, maar kan geen pagina's maken of bewerken.",
"Toggle AI Chat read-only mode": "Alleen-lezenmodus voor AI Chat in-/uitschakelen",
"Title only": "Alleen titel",
"you": "jij"
}
@@ -294,6 +294,7 @@
"Export space": "Exportar espaço",
"Export {{type}}": "Exportar para {{type}}",
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
"Media": "Mídia",
"Align left": "Alinhar à esquerda",
"Align right": "Alinhar à direita",
"Align center": "Alinhar ao centro",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "Insira um divisor horizontal",
"Page break": "Quebra de página",
"Insert a page break for printing.": "Insira uma quebra de página para impressão.",
"Footnote": "Nota de rodapé",
"Insert a footnote reference.": "Inserir uma referência de nota de rodapé.",
"Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.",
"Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.",
"Upload any audio from your device.": "Envie qualquer áudio do seu dispositivo.",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Ative o servidor MCP para permitir que assistentes de IA e ferramentas interajam com o conteúdo do seu espaço de trabalho.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "O MCP está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.",
"MCP Server URL": "URL do servidor MCP",
"Use your API key for authentication. You can manage API keys in your account settings.": "Use sua chave de API para autenticação. Você pode gerenciar chaves de API nas configurações da sua conta.",
"Connect AI assistants with your Docmost account via OAuth.": "Conecte assistentes de IA à sua conta do Docmost via OAuth.",
"Enforce OAuth": "Exigir OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "Os assistentes de IA devem se conectar com uma conta do Docmost via OAuth. Chaves de API não podem ser usadas com o servidor MCP.",
"Toggle enforce OAuth for MCP": "Ativar/desativar exigência de OAuth para MCP",
"Supported tools": "Ferramentas compatíveis",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Seu espaço de trabalho tem MCP habilitado. Use sua chave de API para conectar assistentes de IA.",
"MCP server URL:": "URL do servidor MCP:",
"Learn more": "Saiba mais",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gerencie as chaves de API de todos os usuários do workspace. Veja a <anchor>documentação da API</anchor> para detalhes de uso.",
@@ -1289,5 +1294,48 @@
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} selected_other": "{{count}} selected",
"Compare": "Comparar",
"Compare versions": "Comparar versões",
"Select version from {{date}}": "Selecionar versão de {{date}}",
"Version actions for {{date}}": "Ações da versão de {{date}}",
"Comparing {{newer}} and {{older}}": "Comparando {{newer}} e {{older}}",
"Exit compare": "Sair da comparação",
"Search attachments...": "Pesquisar anexos...",
"Error loading attachments.": "Erro ao carregar anexos.",
"No attachments on this page yet.": "Ainda não há anexos nesta página.",
"Uploaded by {{name}}": "Enviado por {{name}}",
"Download {{name}}": "Baixar {{name}}",
"Access revoked": "Acesso revogado",
"Authorize application": "Autorizar aplicativo",
"{{name}} wants to access {{workspace}}": "{{name}} quer acessar {{workspace}}",
"Not you? Switch account": "Não é você? Trocar conta",
"This application will be able to:": "Este aplicativo poderá:",
"Write": "Escrever",
"Invalid authorization request": "Solicitação de autorização inválida",
"Authorize": "Autorizar",
"Application": "Aplicativo",
"Permissions": "Permissões",
"Authorized": "Autorizado",
"Revoke access": "Revogar acesso",
"Revoke access for {{name}}": "Revogar acesso de {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Tem certeza de que deseja revogar o acesso de {{name}}? O aplicativo não poderá mais acessar sua conta.",
"Something went wrong. Please try again.": "Algo deu errado. Tente novamente.",
"Remove {{name}}": "Remover {{name}}",
"Make sure you trust this application before authorizing it.": "Certifique-se de confiar neste aplicativo antes de autorizá-lo.",
"You will be redirected to": "Você será redirecionado para",
"View content without making changes.": "Visualizar conteúdo sem fazer alterações.",
"Create and modify content.": "Criar e modificar conteúdo.",
"Applications and AI assistants you have authorized to access your account.": "Aplicativos e assistentes de IA que você autorizou a acessar sua conta.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "Seu workspace tem o MCP ativado. Conecte assistentes de IA à sua conta do Docmost via OAuth.",
"Authorized apps": "Aplicativos autorizados",
"No authorized apps yet.": "Ainda não há aplicativos autorizados.",
"Workspace knowledge only": "Somente conhecimento do workspace",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Restrinja o AI Chat para responder apenas com base nas páginas do seu workspace e nos arquivos enviados. Ele não usará conhecimento externo.",
"Toggle workspace knowledge only": "Alternar somente conhecimento do workspace",
"Read-only mode": "Modo somente leitura",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "O AI Chat pode pesquisar e ler o conteúdo do workspace, mas não pode criar nem editar páginas.",
"Toggle AI Chat read-only mode": "Alternar modo somente leitura do AI Chat",
"Title only": "Somente título",
"you": "você"
}
@@ -294,6 +294,7 @@
"Export space": "Экспорт пространства",
"Export {{type}}": "Экспорт {{type}}",
"File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}",
"Media": "Медиа",
"Align left": "По левому краю",
"Align right": "По правому краю",
"Align center": "По центру",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "Вставить горизонтальный разделитель",
"Page break": "Разрыв страницы",
"Insert a page break for printing.": "Вставить разрыв страницы для печати.",
"Footnote": "Сноска",
"Insert a footnote reference.": "Вставить ссылку на сноску.",
"Upload any image from your device.": "Загрузить любое изображение с вашего устройства.",
"Upload any video from your device.": "Загрузить любое видео с вашего устройства.",
"Upload any audio from your device.": "Загрузите любой аудиофайл с вашего устройства.",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Включите сервер MCP, чтобы ИИ-ассистенты и инструменты могли взаимодействовать с содержимым вашего рабочего пространства.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP доступен только в корпоративной версии Docmost. Свяжитесь по адресу sales@docmost.com.",
"MCP Server URL": "URL сервера MCP",
"Use your API key for authentication. You can manage API keys in your account settings.": "Используйте ваш API-ключ для аутентификации. Управлять API-ключами можно в настройках аккаунта.",
"Connect AI assistants with your Docmost account via OAuth.": "Подключайте AI-помощников к вашей учетной записи Docmost через OAuth.",
"Enforce OAuth": "Сделать OAuth обязательным",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI-помощники должны подключаться к учетной записи Docmost через OAuth. Ключи API нельзя использовать с MCP-сервером.",
"Toggle enforce OAuth for MCP": "Переключить обязательное использование OAuth для MCP",
"Supported tools": "Поддерживаемые инструменты",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "В вашем рабочем пространстве включён MCP. Используйте свой API-ключ для подключения ИИ-ассистентов.",
"MCP server URL:": "URL сервера MCP:",
"Learn more": "Подробнее",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Управляйте API-ключами для всех пользователей в рабочем пространстве. Смотрите <anchor>документацию по API</anchor> для получения информации об использовании.",
@@ -1186,8 +1191,8 @@
"Default value": "Значение по умолчанию",
"Delete property": "Удалить свойство",
"Delete view": "Удалить представление",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Delete {{count}} rows?_one": "Удалить 1 строку?",
"Delete {{count}} rows?_other": "Удалить {{count}} строк?",
"Descending": "По убыванию",
"Discard": "Отменить",
"Doesn't contain": "Не содержит",
@@ -1286,8 +1291,51 @@
"Value": "Значение",
"View updated for everyone": "Представление обновлено для всех",
"You have unsaved changes. Do you want to discard them?": "У вас есть несохранённые изменения. Хотите их отменить?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} rows deleted_one": "1 строка удалена",
"{{count}} rows deleted_other": "{{count}} строк удалено",
"{{count}} selected_one": "1 выбрано",
"{{count}} selected_other": "{{count}} выбрано",
"Compare": "Сравнить",
"Compare versions": "Сравнить версии",
"Select version from {{date}}": "Выбрать версию от {{date}}",
"Version actions for {{date}}": "Действия с версией от {{date}}",
"Comparing {{newer}} and {{older}}": "Сравнение {{newer}} и {{older}}",
"Exit compare": "Выйти из режима сравнения",
"Search attachments...": "Поиск вложений...",
"Error loading attachments.": "Ошибка при загрузке вложений.",
"No attachments on this page yet.": "На этой странице пока нет вложений.",
"Uploaded by {{name}}": "Загружено пользователем {{name}}",
"Download {{name}}": "Скачать {{name}}",
"Access revoked": "Доступ отозван",
"Authorize application": "Авторизовать приложение",
"{{name}} wants to access {{workspace}}": "{{name}} запрашивает доступ к {{workspace}}",
"Not you? Switch account": "Не вы? Сменить аккаунт",
"This application will be able to:": "Это приложение сможет:",
"Write": "Запись",
"Invalid authorization request": "Недопустимый запрос на авторизацию",
"Authorize": "Авторизовать",
"Application": "Приложение",
"Permissions": "Разрешения",
"Authorized": "Авторизовано",
"Revoke access": "Отозвать доступ",
"Revoke access for {{name}}": "Отозвать доступ для {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Вы уверены, что хотите отозвать доступ для {{name}}? Приложение больше не сможет получить доступ к вашей учетной записи.",
"Something went wrong. Please try again.": "Что-то пошло не так. Пожалуйста, попробуйте еще раз.",
"Remove {{name}}": "Удалить {{name}}",
"Make sure you trust this application before authorizing it.": "Прежде чем авторизовать это приложение, убедитесь, что вы ему доверяете.",
"You will be redirected to": "Вы будете перенаправлены на",
"View content without making changes.": "Просматривать содержимое без внесения изменений.",
"Create and modify content.": "Создавать и изменять содержимое.",
"Applications and AI assistants you have authorized to access your account.": "Приложения и AI-помощники, которым вы разрешили доступ к своей учетной записи.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "В вашем рабочем пространстве включен MCP. Подключайте AI-помощников к вашей учетной записи Docmost через OAuth.",
"Authorized apps": "Авторизованные приложения",
"No authorized apps yet.": "Пока нет авторизованных приложений.",
"Workspace knowledge only": "Только знания рабочего пространства",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Ограничить AI Chat ответами только на основе страниц вашего рабочего пространства и загруженных файлов. Внешние знания использоваться не будут.",
"Toggle workspace knowledge only": "Переключить режим \"Только знания рабочего пространства\"",
"Read-only mode": "Режим только для чтения",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat может искать и читать содержимое рабочего пространства, но не может создавать или редактировать страницы.",
"Toggle AI Chat read-only mode": "Переключить режим только для чтения для AI Chat",
"Title only": "Только заголовок",
"you": "вы"
}
@@ -294,6 +294,7 @@
"Export space": "Експорт простору",
"Export {{type}}": "Експорт {{type}}",
"File exceeds the {{limit}} attachment limit": "Файл перевищує ліміт вкладень {{limit}}",
"Media": "Медіа",
"Align left": "По лівому краю",
"Align right": "По правому краю",
"Align center": "По центру",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "Вставити горизонтальний роздільник",
"Page break": "Розрив сторінки",
"Insert a page break for printing.": "Вставте розрив сторінки для друку.",
"Footnote": "Виноска",
"Insert a footnote reference.": "Вставити посилання на виноску.",
"Upload any image from your device.": "Завантажити будь-яке зображення з вашого пристрою.",
"Upload any video from your device.": "Завантажити будь-яке відео з вашого пристрою.",
"Upload any audio from your device.": "Завантажте будь-який аудіофайл зі свого пристрою.",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Увімкніть MCP‑сервер, щоб дозволити ШІ‑помічникам та інструментам взаємодіяти з вмістом вашого робочого простору.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP доступний лише в корпоративній редакції Docmost. Зверніться до sales@docmost.com.",
"MCP Server URL": "URL сервера MCP",
"Use your API key for authentication. You can manage API keys in your account settings.": "Використовуйте свій API‑ключ для аутентифікації. Ви можете керувати API‑ключами в налаштуваннях облікового запису.",
"Connect AI assistants with your Docmost account via OAuth.": "Підключайте AI-асистентів до свого облікового запису Docmost через OAuth.",
"Enforce OAuth": "Зробити OAuth обов’язковим",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI-асистенти повинні підключатися до облікового запису Docmost через OAuth. Ключі API не можна використовувати з MCP-сервером.",
"Toggle enforce OAuth for MCP": "Увімкнути обов’язковий OAuth для MCP",
"Supported tools": "Підтримувані інструменти",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "У вашому робочому просторі MCP увімкнено. Використайте свій API‑ключ, щоб підключити ШІ‑помічників.",
"MCP server URL:": "URL сервера MCP:",
"Learn more": "Дізнатися більше",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Керуйте ключами API для всіх користувачів у робочому просторі. Перегляньте <anchor>документацію API</anchor> для деталей використання.",
@@ -1289,5 +1294,48 @@
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected"
"{{count}} selected_other": "{{count}} selected",
"Compare": "Порівняти",
"Compare versions": "Порівняти версії",
"Select version from {{date}}": "Виберіть версію від {{date}}",
"Version actions for {{date}}": "Дії з версією від {{date}}",
"Comparing {{newer}} and {{older}}": "Порівняння {{newer}} і {{older}}",
"Exit compare": "Вийти з режиму порівняння",
"Search attachments...": "Пошук вкладень...",
"Error loading attachments.": "Помилка завантаження вкладень.",
"No attachments on this page yet.": "На цій сторінці ще немає вкладень.",
"Uploaded by {{name}}": "Завантажено користувачем {{name}}",
"Download {{name}}": "Завантажити {{name}}",
"Access revoked": "Доступ відкликано",
"Authorize application": "Авторизувати застосунок",
"{{name}} wants to access {{workspace}}": "{{name}} хоче отримати доступ до {{workspace}}",
"Not you? Switch account": "Не ви? Змінити обліковий запис",
"This application will be able to:": "Цей застосунок зможе:",
"Write": "Запис",
"Invalid authorization request": "Недійсний запит на авторизацію",
"Authorize": "Авторизувати",
"Application": "Застосунок",
"Permissions": "Дозволи",
"Authorized": "Авторизовано",
"Revoke access": "Відкликати доступ",
"Revoke access for {{name}}": "Відкликати доступ для {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Ви впевнені, що хочете відкликати доступ для {{name}}? Застосунок більше не зможе отримувати доступ до вашого облікового запису.",
"Something went wrong. Please try again.": "Щось пішло не так. Спробуйте ще раз.",
"Remove {{name}}": "Видалити {{name}}",
"Make sure you trust this application before authorizing it.": "Перш ніж авторизувати цей застосунок, переконайтеся, що ви йому довіряєте.",
"You will be redirected to": "Вас буде перенаправлено на",
"View content without making changes.": "Переглядати вміст без внесення змін.",
"Create and modify content.": "Створювати та змінювати вміст.",
"Applications and AI assistants you have authorized to access your account.": "Застосунки та AI-асистенти, яким ви надали дозвіл на доступ до свого облікового запису.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "У вашому робочому просторі ввімкнено MCP. Підключайте AI-асистентів до свого облікового запису Docmost через OAuth.",
"Authorized apps": "Авторизовані застосунки",
"No authorized apps yet.": "Авторизованих застосунків ще немає.",
"Workspace knowledge only": "Лише знання робочого простору",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Обмежити AI Chat відповідями лише на основі сторінок вашого робочого простору та завантажених файлів. Зовнішні знання не використовуватимуться.",
"Toggle workspace knowledge only": "Перемкнути режим лише знань робочого простору",
"Read-only mode": "Режим лише читання",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat може шукати та читати вміст робочого простору, але не може створювати або редагувати сторінки.",
"Toggle AI Chat read-only mode": "Перемкнути режим лише читання для AI Chat",
"Title only": "Лише заголовок",
"you": "ви"
}
@@ -294,6 +294,7 @@
"Export space": "导出空间",
"Export {{type}}": "导出为 {{type}}",
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
"Media": "媒体",
"Align left": "靠左对齐",
"Align right": "靠右对齐",
"Align center": "居中对齐",
@@ -387,6 +388,8 @@
"Insert horizontal rule divider": "插入水平分割线",
"Page break": "分页符",
"Insert a page break for printing.": "插入一个用于打印的分页符。",
"Footnote": "脚注",
"Insert a footnote reference.": "插入脚注引用。",
"Upload any image from your device.": "从设备上传任何图像",
"Upload any video from your device.": "从设备上传任何视频",
"Upload any audio from your device.": "从您的设备上传任意音频文件。",
@@ -704,9 +707,11 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "启用 MCP 服务器以允许 AI 助手和工具与您的工作区内容交互。",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP 仅在 Docmost 企业版中提供。请联系 sales@docmost.com。",
"MCP Server URL": "MCP 服务器 URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "使用您的 API 密钥进行身份验证。您可以在账户设置中管理 API 密钥。",
"Connect AI assistants with your Docmost account via OAuth.": "通过 OAuth 将 AI 助手连接到你的 Docmost 账户。",
"Enforce OAuth": "强制使用 OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI 助手必须通过 OAuth 使用 Docmost 账户连接。MCP 服务器不能使用 API 密钥。",
"Toggle enforce OAuth for MCP": "切换 MCP 的强制使用 OAuth 设置",
"Supported tools": "支持的工具",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "您的工作区已启用 MCP。使用您的 API 密钥连接 AI 助手。",
"MCP server URL:": "MCP 服务器 URL",
"Learn more": "了解更多",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "为工作区内所有用户管理 API 密钥。有关使用详情,请查阅<anchor>API 文档</anchor>。",
@@ -1289,5 +1294,48 @@
"{{count}} rows deleted_one": "已删除 1 行",
"{{count}} rows deleted_other": "已删除 {{count}} 行",
"{{count}} selected_one": "已选择 1 项",
"{{count}} selected_other": "已选择 {{count}} 项"
"{{count}} selected_other": "已选择 {{count}} 项",
"Compare": "比较",
"Compare versions": "比较版本",
"Select version from {{date}}": "选择 {{date}} 的版本",
"Version actions for {{date}}": "{{date}} 的版本操作",
"Comparing {{newer}} and {{older}}": "正在比较 {{newer}} 和 {{older}}",
"Exit compare": "退出比较",
"Search attachments...": "搜索附件……",
"Error loading attachments.": "加载附件时出错。",
"No attachments on this page yet.": "此页面上还没有附件。",
"Uploaded by {{name}}": "由 {{name}} 上传",
"Download {{name}}": "下载 {{name}}",
"Access revoked": "访问权限已撤销",
"Authorize application": "授权应用程序",
"{{name}} wants to access {{workspace}}": "{{name}} 想要访问 {{workspace}}",
"Not you? Switch account": "不是你?切换账户",
"This application will be able to:": "此应用程序将能够:",
"Write": "写入",
"Invalid authorization request": "无效的授权请求",
"Authorize": "授权",
"Application": "应用程序",
"Permissions": "权限",
"Authorized": "已授权",
"Revoke access": "撤销访问权限",
"Revoke access for {{name}}": "撤销 {{name}} 的访问权限",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "你确定要撤销 {{name}} 的访问权限吗?该应用程序将无法再访问你的账户。",
"Something went wrong. Please try again.": "出了点问题。请重试。",
"Remove {{name}}": "移除 {{name}}",
"Make sure you trust this application before authorizing it.": "在授权之前,请确保你信任此应用程序。",
"You will be redirected to": "你将被重定向到",
"View content without making changes.": "查看内容而不进行更改。",
"Create and modify content.": "创建和修改内容。",
"Applications and AI assistants you have authorized to access your account.": "你已授权访问你账户的应用程序和 AI 助手。",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "你的工作区已启用 MCP。通过 OAuth 将 AI 助手连接到你的 Docmost 账户。",
"Authorized apps": "已授权的应用",
"No authorized apps yet.": "尚无已授权的应用。",
"Workspace knowledge only": "仅限工作区知识",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "将 AI 聊天限制为仅根据你的工作区页面和已上传文件进行回答。它不会使用外部知识。",
"Toggle workspace knowledge only": "切换“仅限工作区知识”",
"Read-only mode": "只读模式",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI 聊天可以搜索和读取工作区内容,但不能创建或编辑页面。",
"Toggle AI Chat read-only mode": "切换 AI 聊天只读模式",
"Title only": "仅标题",
"you": "你"
}
+7
View File
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
import { useTrackOrigin } from "@/hooks/use-track-origin";
const SetupWorkspace = lazy(() => import("@/pages/auth/setup-workspace.tsx"));
const LoginPage = lazy(() => import("@/pages/auth/login"));
const Home = lazy(() => import("@/pages/dashboard/home"));
@@ -73,6 +74,7 @@ const FavoritesPage = lazy(() => import("@/pages/favorites/favorites-page"));
const AiChat = lazy(() => import("@/ee/ai-chat/pages/ai-chat.tsx"));
const VerifyEmail = lazy(() => import("@/ee/pages/verify-email.tsx"));
const LabelPage = lazy(() => import("@/pages/label/label-page"));
const OAuthConsent = lazy(() => import("@/ee/oauth/pages/oauth-consent.tsx"));
export default function App() {
const { t } = useTranslation();
@@ -95,6 +97,7 @@ export default function App() {
<Route path={"/password-reset"} element={<PasswordReset />} />
<Route path={"/login/mfa"} element={<MfaChallengePage />} />
<Route path={"/login/mfa/setup"} element={<MfaSetupRequiredPage />} />
<Route path={"/oauth/consent"} element={<OAuthConsent />} />
{!isCloud() && (
<Route path={"/setup/register"} element={<SetupWorkspace />} />
@@ -148,6 +151,10 @@ export default function App() {
element={<AccountPreferences />}
/>
<Route path={"account/api-keys"} element={<UserApiKeys />} />
<Route
path={"account/api-keys/authorized-apps"}
element={<UserApiKeys />}
/>
<Route path={"workspace"} element={<WorkspaceSettings />} />
<Route path={"members"} element={<WorkspaceMembers />} />
<Route path={"api-keys"} element={<WorkspaceApiKeys />} />
@@ -1,5 +1,6 @@
import {
Anchor,
Badge,
Group,
List,
Text,
@@ -107,11 +108,11 @@ export default function McpSettings() {
</CopyButton>
</Group>
<Text size="sm" c="dimmed" mt="xs">
{t(
"Use your API key for authentication. You can manage API keys in your account settings.",
)}
{t("Connect AI assistants with your Docmost account via OAuth.")}
</Text>
<McpEnforceOauthSetting />
<div>
<Text size="sm" fw={500} mt="md" mb={4}>
{t("Supported tools")}
@@ -154,3 +155,56 @@ export default function McpSettings() {
</Stack>
);
}
function McpEnforceOauthSetting() {
const { t } = useTranslation();
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const [checked, setChecked] = useState(workspace?.settings?.ai?.enforceMcpOauth);
const hasAccess = useHasFeature(Feature.MCP_CONTROLS);
const upgradeLabel = useUpgradeLabel();
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked;
try {
const updatedWorkspace = await updateWorkspace({ enforceMcpOauth: value });
setChecked(value);
setWorkspace(updatedWorkspace);
} catch (err) {
notifications.show({
message: err?.response?.data?.message,
color: "red",
});
}
};
return (
<Group justify="space-between" wrap="nowrap" gap="xl" mt="md">
<div>
<Group gap="xs" align="center">
<Text size="sm" fw={500}>
{t("Enforce OAuth")}
</Text>
{!hasAccess && (
<Badge variant="light" size="sm" radius="sm">
{t("Enterprise")}
</Badge>
)}
</Group>
<Text size="sm" c="dimmed">
{t(
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.",
)}
</Text>
</div>
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
<Switch
defaultChecked={checked}
onChange={handleChange}
disabled={!hasAccess}
aria-label={t("Toggle enforce OAuth for MCP")}
/>
</Tooltip>
</Group>
);
}
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Anchor, Alert, Button, Group, Space, Text } from "@mantine/core";
import { Anchor, Alert, Button, Group, Space, Tabs, Text } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import { Trans, useTranslation } from "react-i18next";
import SettingsTitle from "@/components/settings/settings-title";
@@ -17,6 +17,8 @@ import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import useUserRole from "@/hooks/use-user-role.tsx";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
import { useLocation, useNavigate } from "react-router-dom";
import { AuthorizedAppsPanel } from "@/ee/oauth/components/authorized-apps-panel.tsx";
export default function UserApiKeys() {
const { t } = useTranslation();
@@ -29,6 +31,11 @@ export default function UserApiKeys() {
const { data, isLoading } = useGetApiKeysQuery({ cursor });
const [workspace] = useAtom(workspaceAtom);
const { isAdmin } = useUserRole();
const location = useLocation();
const navigate = useNavigate();
const activeTab = location.pathname.endsWith("/authorized-apps")
? "authorized-apps"
: "api-keys";
const mcpEnabled = workspace?.settings?.ai?.mcp === true;
const restrictToAdmins = workspace?.settings?.api?.restrictToAdmins === true;
const canCreate = !restrictToAdmins || isAdmin;
@@ -47,26 +54,27 @@ export default function UserApiKeys() {
setRevokeModalOpened(true);
};
const handleTabChange = (value: string | null) => {
navigate(
value === "authorized-apps"
? "/settings/account/api-keys/authorized-apps"
: "/settings/account/api-keys",
);
};
return (
<>
<DocumentTitle title={t("API keys")} />
<DocumentTitle
title={activeTab === "authorized-apps" ? t("Authorized apps") : t("API keys")}
/>
<SettingsTitle title={t("API keys")} />
<Text size="sm" c="dimmed" mb="md">
<Trans
i18nKey="View the <anchor>API documentation</anchor> for usage details."
components={{
anchor: <Anchor href="https://docmost.com/api-docs" target="_blank" size="sm" />,
}}
/>
</Text>
{mcpEnabled && canCreate && (
{mcpEnabled && (
<Alert variant="light" color="blue" mb="md" p="sm" icon={<IconInfoCircle />}>
<Text size="sm">
{t(
"Your workspace has MCP enabled. Use your API key to connect AI assistants.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.",
)}{" "}
<Anchor
href="https://docmost.com/docs/user-guide/mcp"
@@ -85,37 +93,70 @@ export default function UserApiKeys() {
</Alert>
)}
{canCreate ? (
<Group justify="flex-end" mb="md">
<Button onClick={() => setCreateModalOpened(true)}>
{t("Create API Key")}
</Button>
</Group>
) : restrictToAdmins ? (
<Alert variant="light" color="yellow" mb="md" p="sm" icon={<IconInfoCircle />}>
<Text size="sm">
{t("API key creation is restricted to admins by your workspace administrator.")}
<Tabs color="dark" value={activeTab} onChange={handleTabChange}>
<Tabs.List>
<Tabs.Tab fw={500} value="api-keys">
{t("API keys")}
</Tabs.Tab>
<Tabs.Tab fw={500} value="authorized-apps">
{t("Authorized apps")}
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="api-keys" pt="md">
<Group justify="space-between" align="center" mb="md">
<Text size="sm" c="dimmed">
<Trans
i18nKey="View the <anchor>API documentation</anchor> for usage details."
components={{
anchor: <Anchor href="https://docmost.com/api-docs" target="_blank" size="sm" />,
}}
/>
</Text>
</Alert>
) : null}
<ApiKeyTable
apiKeys={data?.items || []}
isLoading={isLoading}
onUpdate={handleUpdate}
onRevoke={handleRevoke}
/>
{canCreate && (
<Button
onClick={() => setCreateModalOpened(true)}
style={{ flexShrink: 0 }}
>
{t("Create API Key")}
</Button>
)}
</Group>
<Space h="md" />
{!canCreate && restrictToAdmins && (
<Alert variant="light" color="yellow" mb="md" p="sm" icon={<IconInfoCircle />}>
<Text size="sm">
{t("API key creation is restricted to admins by your workspace administrator.")}
</Text>
</Alert>
)}
{data?.items.length > 0 && (
<Paginate
hasPrevPage={data?.meta?.hasPrevPage}
hasNextPage={data?.meta?.hasNextPage}
onNext={() => goNext(data?.meta?.nextCursor)}
onPrev={goPrev}
<ApiKeyTable
apiKeys={data?.items || []}
isLoading={isLoading}
onUpdate={handleUpdate}
onRevoke={handleRevoke}
/>
)}
<Space h="md" />
{data?.items.length > 0 && (
<Paginate
hasPrevPage={data?.meta?.hasPrevPage}
hasNextPage={data?.meta?.hasNextPage}
onNext={() => goNext(data?.meta?.nextCursor)}
onPrev={goPrev}
/>
)}
</Tabs.Panel>
<Tabs.Panel value="authorized-apps" pt="md">
<AuthorizedAppsPanel />
</Tabs.Panel>
</Tabs>
<CreateApiKeyModal
opened={createModalOpened}
+2
View File
@@ -22,5 +22,7 @@ export const Feature = {
PERSONAL_SPACES: 'spaces:personal',
DOCX_EXPORT: 'export:docx',
BASES: 'bases',
OAUTH: 'oauth',
AI_CONTROLS: 'ai:controls',
MCP_CONTROLS: 'mcp:controls',
} as const;
@@ -0,0 +1,19 @@
import { Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { OAuthGrantsTable } from "@/ee/oauth/components/oauth-grants-table";
import { useOAuthGrantsQuery } from "@/ee/oauth/queries/oauth-query";
export function AuthorizedAppsPanel() {
const { t } = useTranslation();
const { data, isLoading } = useOAuthGrantsQuery();
return (
<>
<Text size="sm" c="dimmed" mb="md">
{t("Applications and AI assistants you have authorized to access your account.")}
</Text>
<OAuthGrantsTable grants={data || []} isLoading={isLoading} />
</>
);
}
@@ -0,0 +1,178 @@
import { ActionIcon, Badge, Group, Skeleton, Table, Text, Tooltip } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { useTranslation } from "react-i18next";
import NoTableResults from "@/components/common/no-table-results";
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
import { IOAuthGrant } from "@/ee/oauth/types/oauth.types";
import { useRevokeOAuthGrantMutation } from "@/ee/oauth/queries/oauth-query";
// Callback hosts identify the app; the full URL belongs on the consent screen.
function callbackHosts(redirectUris: string[] = []): string[] {
const hosts = redirectUris.map((uri) => {
try {
const url = new URL(uri);
return url.protocol === "http:" || url.protocol === "https:"
? url.host
: url.protocol;
} catch {
return uri;
}
});
return Array.from(new Set(hosts));
}
type OAuthGrantsTableProps = {
grants: IOAuthGrant[];
isLoading?: boolean;
};
function TableSkeleton() {
return (
<>
{Array.from({ length: 3 }).map((_, i) => (
<Table.Tr key={i}>
<Table.Td>
<Skeleton height={14} width={140} />
</Table.Td>
<Table.Td>
<Group gap={4}>
<Skeleton height={20} width={50} />
<Skeleton height={20} width={55} />
</Group>
</Table.Td>
<Table.Td>
<Skeleton height={14} width={90} />
</Table.Td>
<Table.Td>
<Skeleton height={14} width={90} />
</Table.Td>
<Table.Td>
<Skeleton height={28} width={28} circle />
</Table.Td>
</Table.Tr>
))}
</>
);
}
export function OAuthGrantsTable({ grants, isLoading }: OAuthGrantsTableProps) {
const { t } = useTranslation();
const locale = useDateFnsLocale();
const revokeMutation = useRevokeOAuthGrantMutation();
const scopeMeta: Record<string, { color: string; label: string }> = {
read: { color: "blue", label: t("Read") },
write: { color: "orange", label: t("Write") },
};
const formatDate = (date: string | null) => {
if (!date) return t("Never");
return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
};
const openRevokeModal = (grant: IOAuthGrant) =>
modals.openConfirmModal({
title: t("Revoke access"),
centered: true,
children: (
<Text size="sm">
{t(
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.",
{ name: grant.clientName },
)}
</Text>
),
labels: { confirm: t("Revoke access"), cancel: t("Cancel") },
confirmProps: { color: "red" },
onConfirm: () => revokeMutation.mutate(grant.id),
});
return (
<Table.ScrollContainer minWidth={600}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Application")}</Table.Th>
<Table.Th>{t("Permissions")}</Table.Th>
<Table.Th>{t("Authorized")}</Table.Th>
<Table.Th>{t("Last used")}</Table.Th>
<Table.Th aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{isLoading ? (
<TableSkeleton />
) : grants.length > 0 ? (
grants.map((grant) => (
<Table.Tr key={grant.id}>
<Table.Td>
<Text fz="sm" fw={500}>
{grant.clientName}
</Text>
<Text
fz="xs"
c="dimmed"
title={grant.redirectUris?.join("\n")}
style={{ overflowWrap: "anywhere" }}
>
{callbackHosts(grant.redirectUris).join(", ")}
</Text>
</Table.Td>
<Table.Td>
<Group gap={4}>
{grant.scopes.map((scope) => {
const meta = scopeMeta[scope];
if (!meta) return null;
return (
<Badge
key={scope}
variant="light"
color={meta.color}
size="sm"
>
{meta.label}
</Badge>
);
})}
</Group>
</Table.Td>
<Table.Td>
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
{formatDate(grant.createdAt)}
</Text>
</Table.Td>
<Table.Td>
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
{formatDate(grant.lastUsedAt)}
</Text>
</Table.Td>
<Table.Td>
<Tooltip label={t("Revoke access")}>
<ActionIcon
variant="subtle"
color="red"
aria-label={t("Revoke access for {{name}}", {
name: grant.clientName,
})}
onClick={() => openRevokeModal(grant)}
>
<IconTrash size={16} />
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
))
) : (
<NoTableResults colSpan={5} text={t("No authorized apps yet.")} />
)}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
);
}
@@ -0,0 +1,333 @@
import React, { useEffect, useMemo, useState } from "react";
import {
Alert,
Anchor,
Box,
Button,
Center,
Checkbox,
Container,
Divider,
Group,
Loader,
Paper,
Stack,
Text,
ThemeIcon,
Title,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import {
IconAlertTriangle,
IconEye,
IconPencil,
} from "@tabler/icons-react";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { RESET } from "jotai/utils";
import { useTranslation } from "react-i18next";
import { useLocation, useSearchParams } from "react-router-dom";
import { AuthLayout } from "@/features/auth/components/auth-layout.tsx";
import classes from "@/features/auth/components/auth.module.css";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
import { UserInfo } from "@/components/common/user-info.tsx";
import useCurrentUser from "@/features/user/hooks/use-current-user";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { logout } from "@/features/auth/services/auth-service";
import { ICurrentUser } from "@/features/user/types/user.types";
import APP_ROUTE from "@/lib/app-route.ts";
import {
approveOAuthAuthorization,
getOAuthAuthorizeInfo,
} from "@/ee/oauth/services/oauth-service";
import {
IApproveAuthorizationPayload,
IAuthorizeParams,
IOAuthAuthorizeInfo,
} from "@/ee/oauth/types/oauth.types";
function loginRedirectUrl(pathname: string, search: string): string {
return `${APP_ROUTE.AUTH.LOGIN}?redirect=${encodeURIComponent(pathname + search)}`;
}
function errorStatus(error: any): number | undefined {
return error?.response?.status;
}
function errorText(error: any): string | undefined {
const data = error?.response?.data;
if (typeof data?.error_description === "string") return data.error_description;
if (Array.isArray(data?.message)) return data.message.join(", ");
return data?.message;
}
const OAUTH_PARAM_NAMES = [
"response_type",
"client_id",
"redirect_uri",
"state",
"code_challenge",
"code_challenge_method",
"scope",
"resource",
];
function pickOAuthParams(params: IAuthorizeParams): IAuthorizeParams {
const picked: IAuthorizeParams = {};
for (const name of OAUTH_PARAM_NAMES) {
if (params[name] !== undefined) {
picked[name] = params[name];
}
}
return picked;
}
export default function OAuthConsent() {
const { t } = useTranslation();
const location = useLocation();
const [searchParams] = useSearchParams();
const params = useMemo<IAuthorizeParams>(
() => Object.fromEntries(searchParams.entries()),
[searchParams],
);
const currentUserQuery = useCurrentUser();
const infoQuery = useQuery({
queryKey: ["oauth-authorize-info", params],
queryFn: () => getOAuthAuthorizeInfo(params),
});
const isUnauthenticated =
errorStatus(currentUserQuery.error) === 401 ||
errorStatus(infoQuery.error) === 401;
useEffect(() => {
if (isUnauthenticated) {
window.location.replace(
loginRedirectUrl(location.pathname, location.search),
);
}
}, [isUnauthenticated, location.pathname, location.search]);
const isLoading =
isUnauthenticated || currentUserQuery.isLoading || infoQuery.isLoading;
return (
<AuthLayout>
<DocumentTitle title={t("Authorize application")} />
<Container size={460} className={classes.container}>
<Box p="xl">
{isLoading ? (
<Center mih={200}>
<Loader />
</Center>
) : infoQuery.data && currentUserQuery.data ? (
<ConsentCard
info={infoQuery.data}
currentUser={currentUserQuery.data}
params={params}
/>
) : (
<InvalidRequestCard
description={errorText(infoQuery.error ?? currentUserQuery.error)}
/>
)}
</Box>
</Container>
</AuthLayout>
);
}
function InvalidRequestCard({ description }: { description?: string }) {
const { t } = useTranslation();
return (
<Stack align="center" gap="sm">
<ThemeIcon size={48} radius="xl" variant="light" color="red">
<IconAlertTriangle size={26} stroke={1.5} />
</ThemeIcon>
<Title order={3} ta="center" fw={600}>
{t("Invalid authorization request")}
</Title>
{description && (
<Text size="sm" c="dimmed" ta="center">
{description}
</Text>
)}
</Stack>
);
}
type ConsentCardProps = {
info: IOAuthAuthorizeInfo;
currentUser: ICurrentUser;
params: IAuthorizeParams;
};
function ConsentCard({ info, currentUser, params }: ConsentCardProps) {
const { t } = useTranslation();
const location = useLocation();
const [, setCurrentUser] = useAtom(currentUserAtom);
const [approvedScopes, setApprovedScopes] = useState<string[]>(
info.scopes.filter((scope) => scope === "read" || scope === "write"),
);
const [submitting, setSubmitting] = useState<"approve" | "deny" | null>(null);
const scopeRows = [
{
scope: "read",
icon: <IconEye size={16} stroke={1.5} />,
label: t("Read"),
description: t("View data in your workspace without making changes."),
},
{
scope: "write",
icon: <IconPencil size={16} stroke={1.5} />,
label: t("Write"),
description: t("Create and modify data in your workspace."),
},
];
function toggleScope(scope: string, checked: boolean) {
setApprovedScopes((prev) =>
checked ? [...prev, scope] : prev.filter((item) => item !== scope),
);
}
async function submitDecision(approved: boolean) {
setSubmitting(approved ? "approve" : "deny");
const oauthParams = pickOAuthParams(params);
const payload: IApproveAuthorizationPayload = approved
? { ...oauthParams, approved: true, approvedScopes }
: { ...oauthParams, approved: false };
try {
const res = await approveOAuthAuthorization(payload);
window.location.replace(res.redirectUrl);
} catch (err) {
if (errorStatus(err) === 401) {
window.location.replace(
loginRedirectUrl(location.pathname, location.search),
);
return;
}
setSubmitting(null);
notifications.show({
message: errorText(err) || t("Something went wrong. Please try again."),
color: "red",
});
}
}
async function switchAccount() {
if (submitting !== null) {
return;
}
setCurrentUser(RESET);
try {
await logout();
} finally {
window.location.replace(
loginRedirectUrl(location.pathname, location.search),
);
}
}
return (
<Stack gap="lg">
<Title order={3} ta="center" fw={600}>
{t("{{name}} wants to access {{workspace}}", {
name: info.clientName,
workspace: currentUser.workspace.name,
})}
</Title>
<Paper withBorder radius="md" p="sm">
<Group justify="space-between" wrap="nowrap">
<UserInfo user={currentUser.user} />
<Anchor
component="button"
type="button"
size="xs"
c="dimmed"
disabled={submitting !== null}
onClick={switchAccount}
style={{ whiteSpace: "nowrap" }}
>
{t("Not you? Switch account")}
</Anchor>
</Group>
</Paper>
<Divider />
<Stack gap="sm">
<Text size="sm" fw={500}>
{t("This application will be able to:")}
</Text>
{scopeRows.map((row) => {
const requested = info.scopes.includes(row.scope);
return (
<Checkbox
key={row.scope}
size="sm"
checked={approvedScopes.includes(row.scope)}
disabled={!requested || submitting !== null}
onChange={(event) =>
toggleScope(row.scope, event.currentTarget.checked)
}
label={
<Group gap={6} wrap="nowrap">
{row.icon}
<Text size="sm" fw={500}>
{row.label}
</Text>
</Group>
}
description={row.description}
/>
);
})}
</Stack>
<div>
<Text size="xs" c="dimmed">
{t("You will be redirected to")}
</Text>
<Text size="xs" ff="monospace" style={{ overflowWrap: "anywhere" }}>
{info.redirectUri}
</Text>
</div>
{!info.verified && (
<Alert
variant="light"
color="yellow"
py="xs"
icon={<IconAlertTriangle size={16} />}
>
{t("Make sure you trust this application before authorizing it.")}
</Alert>
)}
<Group grow>
<Button
variant="default"
onClick={() => submitDecision(false)}
loading={submitting === "deny"}
disabled={submitting === "approve"}
>
{t("Cancel")}
</Button>
<Button
onClick={() => submitDecision(true)}
loading={submitting === "approve"}
disabled={approvedScopes.length === 0 || submitting === "deny"}
>
{t("Authorize")}
</Button>
</Group>
</Stack>
);
}
@@ -0,0 +1,45 @@
import {
useMutation,
useQuery,
useQueryClient,
UseQueryResult,
} from "@tanstack/react-query";
import {
getOAuthGrants,
revokeOAuthGrant,
} from "@/ee/oauth/services/oauth-service";
import { IOAuthGrant } from "@/ee/oauth/types/oauth.types";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
export function useOAuthGrantsQuery(): UseQueryResult<IOAuthGrant[], Error> {
return useQuery({
queryKey: ["oauth-grants"],
queryFn: () => getOAuthGrants(),
staleTime: 0,
gcTime: 0,
});
}
export function useRevokeOAuthGrantMutation() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation<void, Error, string>({
mutationFn: (grantId) => revokeOAuthGrant(grantId),
onSuccess: () => {
notifications.show({ message: t("Access revoked") });
queryClient.invalidateQueries({
predicate: (item) =>
["oauth-grants"].includes(item.queryKey[0] as string),
});
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
message: errorMessage || t("Something went wrong. Please try again."),
color: "red",
});
},
});
}
@@ -0,0 +1,36 @@
import api from "@/lib/api-client";
import {
IApproveAuthorizationPayload,
IAuthorizeParams,
IOAuthAuthorizeInfo,
IOAuthGrant,
} from "@/ee/oauth/types/oauth.types";
export async function getOAuthAuthorizeInfo(
params: IAuthorizeParams,
): Promise<IOAuthAuthorizeInfo> {
const req = await api.post<IOAuthAuthorizeInfo>(
"/oauth/authorize-info",
params,
);
return req.data;
}
export async function approveOAuthAuthorization(
payload: IApproveAuthorizationPayload,
): Promise<{ redirectUrl: string }> {
const req = await api.post<{ redirectUrl: string }>(
"/oauth/authorize",
payload,
);
return req.data;
}
export async function getOAuthGrants(): Promise<IOAuthGrant[]> {
const req = await api.post<IOAuthGrant[]>("/oauth/grants", {});
return req.data;
}
export async function revokeOAuthGrant(grantId: string): Promise<void> {
await api.post("/oauth/grants/revoke", { grantId });
}
@@ -0,0 +1,24 @@
export interface IOAuthAuthorizeInfo {
clientName: string;
redirectUri: string;
scopes: string[];
clientCreatedAt: string;
verified: boolean;
}
export interface IOAuthGrant {
id: string;
clientName: string;
redirectUris: string[];
scopes: string[];
createdAt: string;
lastUsedAt: string | null;
}
export type IAuthorizeParams = Record<string, string>;
export type IApproveAuthorizationPayload = {
[param: string]: unknown;
approved: boolean;
approvedScopes?: string[];
};
@@ -4,8 +4,6 @@ import Lightbox, { type Slide } from "yet-another-react-lightbox";
import type { LightboxRequest } from "@/features/editor/atoms/editor-atoms";
import { getFileUrl } from "@/lib/config.ts";
import "yet-another-react-lightbox/styles.css";
import "yet-another-react-lightbox/plugins/captions.css";
import Captions from "yet-another-react-lightbox/plugins/captions";
import Download from "yet-another-react-lightbox/plugins/download";
import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen";
import Video from "yet-another-react-lightbox/plugins/video";
@@ -53,13 +51,11 @@ function getFilename(src: string) {
function getMedia(rawSrc: string, type?: string, alt?: string): Slide {
const src = getFileUrl(rawSrc);
const filename = getFilename(rawSrc);
const caption = alt || filename;
if (type === "video") {
return {
type: "video",
sources: [{ src, type: getVideoMimeType(rawSrc) }],
title: caption,
download: { url: src, filename },
};
} else {
@@ -67,7 +63,6 @@ function getMedia(rawSrc: string, type?: string, alt?: string): Slide {
type: "image",
src,
alt: alt || undefined,
title: caption,
download: { url: src, filename },
};
}
@@ -124,6 +119,11 @@ export default function LightboxView({
const [pageSlides, setPageSlides] = useState<Slide[]>([]);
const [loadedMediaKey, setLoadedMediaKey] = useState<string | null>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
if (!open) setIsFullscreen(false);
}, [open]);
useEffect(() => {
if (!open) return;
@@ -161,9 +161,21 @@ export default function LightboxView({
close={onClose}
index={index}
slides={slides}
plugins={[Captions, Download, Fullscreen, Video, Zoom]}
styles={{ container: { backgroundColor: "rgba(0, 0, 0, 0.8)" } }}
captions={{ descriptionTextAlign: "center" }}
plugins={[Download, Fullscreen, Video, Zoom]}
styles={{
container: { backgroundColor: "rgba(0, 0, 0, 0.8)" },
icon: { width: 24, height: 24 },
toolbar: {
margin: 8,
borderRadius: 8,
backgroundColor: "rgba(0, 0, 0, 0.5)",
},
}}
controller={{ closeOnBackdropClick: !isFullscreen }}
on={{
enterFullscreen: () => setIsFullscreen(true),
exitFullscreen: () => setIsFullscreen(false),
}}
video={{ controls: true, playsInline: true }}
zoom={{
scrollToZoom: true,
@@ -38,6 +38,60 @@
line-height: var(--mantine-line-height-md);
}
.media-alt-badge {
position: absolute;
right: 8px;
bottom: 8px;
z-index: 1;
padding: 2px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
cursor: default;
user-select: none;
background-color: var(--mantine-color-body);
@mixin light {
border: 1px solid var(--mantine-color-gray-3);
color: var(--mantine-color-gray-7);
}
@mixin dark {
border: 1px solid var(--mantine-color-dark-4);
color: var(--mantine-color-dark-1);
}
@media print {
display: none;
}
&::after {
content: attr(data-alt);
display: none;
position: absolute;
right: 0;
bottom: calc(100% + 6px);
width: max-content;
max-width: 320px;
padding: 6px 10px;
border-radius: var(--mantine-radius-default);
font-size: var(--mantine-font-size-sm);
font-weight: 400;
letter-spacing: normal;
line-height: var(--mantine-line-height-sm);
white-space: pre-wrap;
overflow-wrap: break-word;
text-align: start;
background-color: var(--mantine-color-gray-9);
color: var(--mantine-color-white);
}
&:hover::after {
display: block;
}
}
.media-pulse {
animation: media-pulse 1.2s ease-in-out infinite;
@@ -27,6 +27,7 @@ export interface IWorkspace {
mcpEnabled?: boolean;
aiChatReadOnly?: boolean;
aiChatWorkspaceKnowledgeOnly?: boolean;
enforceMcpOauth?: boolean;
trashRetentionDays?: number;
restrictApiToAdmins?: boolean;
allowMemberTemplates?: boolean;
@@ -52,6 +53,7 @@ export interface IWorkspaceAiSettings {
search?: boolean;
generative?: boolean;
mcp?: boolean;
enforceMcpOauth?: boolean;
chat?: boolean;
chatReadOnly?: boolean;
chatWorkspaceKnowledgeOnly?: boolean;
+2
View File
@@ -76,6 +76,8 @@ function redirectToLogin() {
APP_ROUTE.AUTH.MFA_CHALLENGE,
APP_ROUTE.AUTH.MFA_SETUP_REQUIRED,
"/invites",
// the oauth consent page redirects to login itself, preserving its query string
"/oauth/consent",
];
if (!exemptPaths.some((path) => window.location.pathname.startsWith(path))) {
const redirectTo = window.location.pathname;
+1
View File
@@ -43,6 +43,7 @@
"@fastify/cookie": "11.1.2",
"@fastify/multipart": "10.1.1",
"@fastify/static": "10.1.3",
"@jmondi/oauth2-server": "^4.3.7",
"@keyv/redis": "5.1.6",
"@langchain/core": "1.1.46",
"@langchain/textsplitters": "1.0.1",
@@ -63,6 +63,7 @@ export class CollaborationGateway {
redis: new RedisClient({
host: this.redisConfig.host,
port: this.redisConfig.port,
username: this.redisConfig.username,
password: this.redisConfig.password,
db: this.redisConfig.db,
family: this.redisConfig.family,
@@ -14,7 +14,8 @@ import { RedisModule } from '@nestjs-labs/nestjs-ioredis';
import { RedisConfigService } from '../../integrations/redis/redis-config.service';
import { CaslModule } from '../../core/casl/casl.module';
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';
import KeyvRedis, { defaultReconnectStrategy } from '@keyv/redis';
import { parseRedisUrl } from '../../common/helpers';
@Module({
imports: [
@@ -33,10 +34,20 @@ import KeyvRedis from '@keyv/redis';
isGlobal: true,
useFactory: async (environmentService: EnvironmentService) => {
const redisUrl = environmentService.getRedisUrl();
const { family, tls } = parseRedisUrl(redisUrl);
return {
ttl: 5 * 1000,
stores: [new KeyvRedis(redisUrl)],
stores: [
new KeyvRedis({
url: redisUrl,
socket: {
family,
reconnectStrategy: defaultReconnectStrategy,
...tls,
},
}),
],
};
},
inject: [EnvironmentService],
@@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common';
export const OAUTH_SCOPE_KEY = 'oauthScope';
export type OAuthRouteScope = 'read' | 'write';
export const OAuthScope = (scope: OAuthRouteScope) =>
SetMetadata(OAUTH_SCOPE_KEY, scope);
@@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
export const REQUIRE_SESSION_AUTH_KEY = 'requireSessionAuth';
export const RequireSessionAuth = () =>
SetMetadata(REQUIRE_SESSION_AUTH_KEY, true);
@@ -23,6 +23,11 @@ export const AuditEvent = {
API_KEY_UPDATED: 'api_key.updated',
API_KEY_DELETED: 'api_key.deleted',
// OAuth
OAUTH_CLIENT_REGISTERED: 'oauth_client.registered',
OAUTH_GRANT_CREATED: 'oauth_grant.created',
OAUTH_GRANT_REVOKED: 'oauth_grant.revoked',
// SCIM Tokens
SCIM_TOKEN_CREATED: 'scim_token.created',
SCIM_TOKEN_UPDATED: 'scim_token.updated',
@@ -124,6 +129,8 @@ export const AuditResource = {
COMMENT: 'comment',
SHARE: 'share',
API_KEY: 'api_key',
OAUTH_CLIENT: 'oauth_client',
OAUTH_GRANT: 'oauth_grant',
SCIM_TOKEN: 'scim_token',
SSO_PROVIDER: 'sso_provider',
WORKSPACE_INVITATION: 'workspace_invitation',
@@ -12,6 +12,8 @@ export enum EventName {
SPACE_UPDATED = 'space.updated',
SPACE_DELETED = 'space.deleted',
USER_PASSWORD_RESET = 'user.password.reset',
WORKSPACE_CREATED = 'workspace.created',
WORKSPACE_UPDATED = 'workspace.updated',
WORKSPACE_DELETED = 'workspace.deleted',
+2
View File
@@ -23,7 +23,9 @@ export const Feature = {
PERSONAL_SPACES: 'spaces:personal',
DOCX_EXPORT: 'export:docx',
BASES: 'bases',
OAUTH: 'oauth',
AI_CONTROLS: 'ai:controls',
MCP_CONTROLS: 'mcp:controls',
} as const;
export type FeatureKey = (typeof Feature)[keyof typeof Feature];
@@ -0,0 +1,186 @@
import {
ExecutionContext,
ForbiddenException,
UnauthorizedException,
} from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
import { OAUTH_SCOPE_KEY } from '../decorators/oauth-scope.decorator';
import { REQUIRE_SESSION_AUTH_KEY } from '../decorators/require-session-auth.decorator';
import { JwtType } from '../../core/auth/dto/jwt-payload';
const handlerSentinel = () => 'handler';
const classSentinel = class Controller {};
function createCtx(): ExecutionContext {
return {
getHandler: () => handlerSentinel,
getClass: () => classSentinel,
} as any;
}
function createGuard(scopeMetadata?: unknown, requireSession?: boolean) {
const reflector = {
getAllAndOverride: jest.fn((key: string) =>
key === REQUIRE_SESSION_AUTH_KEY ? requireSession : scopeMetadata,
),
} as any;
const environmentService = {
isCloud: jest.fn().mockReturnValue(false),
} as any;
const guard = new JwtAuthGuard(reflector, environmentService);
return { guard, reflector };
}
function oauthUser(scopes: string[]) {
return {
user: { id: 'user_1' },
workspace: { id: 'ws_1' },
oauth: { grantId: 'grant_1', scopes },
};
}
describe('JwtAuthGuard.handleRequest', () => {
it('rethrows the strategy error', () => {
const { guard } = createGuard();
const err = new UnauthorizedException('bad token');
expect(() => guard.handleRequest(err, null, null, createCtx())).toThrow(err);
});
it('throws UnauthorizedException when there is no user', () => {
const { guard } = createGuard();
expect(() => guard.handleRequest(null, null, null, createCtx())).toThrow(
UnauthorizedException,
);
});
it('returns a non-oauth user untouched without consulting scope metadata', () => {
const { guard, reflector } = createGuard();
const user = { user: { id: 'user_1' }, workspace: { id: 'ws_1' } };
expect(guard.handleRequest(null, user, null, createCtx())).toBe(user);
expect(reflector.getAllAndOverride).not.toHaveBeenCalledWith(
OAUTH_SCOPE_KEY,
expect.anything(),
);
});
it('forbids an oauth user on a route without scope metadata', () => {
const { guard, reflector } = createGuard(undefined);
expect(() =>
guard.handleRequest(null, oauthUser(['read', 'write']), null, createCtx()),
).toThrow(ForbiddenException);
expect(reflector.getAllAndOverride).toHaveBeenCalledWith(OAUTH_SCOPE_KEY, [
handlerSentinel,
classSentinel,
]);
});
it('passes read scope on a read route', () => {
const { guard } = createGuard('read');
const user = oauthUser(['read']);
expect(guard.handleRequest(null, user, null, createCtx())).toBe(user);
});
it('forbids read scope on a write route with insufficient_scope', () => {
const { guard } = createGuard('write');
expect(() =>
guard.handleRequest(null, oauthUser(['read']), null, createCtx()),
).toThrow('insufficient_scope');
});
it('passes write scope on a read route', () => {
const { guard } = createGuard('read');
const user = oauthUser(['write']);
expect(guard.handleRequest(null, user, null, createCtx())).toBe(user);
});
it('passes write scope on a write route', () => {
const { guard } = createGuard('write');
const user = oauthUser(['write']);
expect(guard.handleRequest(null, user, null, createCtx())).toBe(user);
});
describe('session-only routes', () => {
const sessionUser = {
user: { id: 'user_1' },
workspace: { id: 'ws_1' },
authType: JwtType.ACCESS,
};
it('allows a signed-in session', () => {
const { guard } = createGuard(undefined, true);
expect(guard.handleRequest(null, sessionUser, null, createCtx())).toBe(
sessionUser,
);
});
it('forbids an api key', () => {
const { guard } = createGuard(undefined, true);
const apiKeyUser = {
user: { id: 'user_1' },
workspace: { id: 'ws_1' },
authType: JwtType.API_KEY,
};
expect(() =>
guard.handleRequest(null, apiKeyUser, null, createCtx()),
).toThrow('This action requires an interactive user session');
});
it('forbids an oauth token even when it carries write scope', () => {
const { guard } = createGuard('write', true);
const user = { ...oauthUser(['write']), authType: JwtType.OAUTH_ACCESS };
expect(() => guard.handleRequest(null, user, null, createCtx())).toThrow(
'This action requires an interactive user session',
);
});
it('leaves api keys working on routes without the marker', () => {
const { guard } = createGuard(undefined, undefined);
const apiKeyUser = {
user: { id: 'user_1' },
workspace: { id: 'ws_1' },
authType: JwtType.API_KEY,
};
expect(guard.handleRequest(null, apiKeyUser, null, createCtx())).toBe(
apiKeyUser,
);
});
});
it('lets handler metadata override class metadata', () => {
const metadataByTarget = new Map<unknown, string>([
[handlerSentinel, 'write'],
[classSentinel, 'read'],
]);
const reflector = {
getAllAndOverride: jest.fn((key: string, targets: unknown[]) => {
if (key === REQUIRE_SESSION_AUTH_KEY) {
return undefined;
}
for (const target of targets) {
if (metadataByTarget.has(target)) {
return metadataByTarget.get(target);
}
}
return undefined;
}),
} as any;
const environmentService = { isCloud: jest.fn().mockReturnValue(false) } as any;
const guard = new JwtAuthGuard(reflector, environmentService);
expect(() =>
guard.handleRequest(null, oauthUser(['read']), null, createCtx()),
).toThrow('insufficient_scope');
});
});
@@ -1,16 +1,26 @@
import {
ExecutionContext,
ForbiddenException,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
import {
OAUTH_SCOPE_KEY,
OAuthRouteScope,
} from '../decorators/oauth-scope.decorator';
import { REQUIRE_SESSION_AUTH_KEY } from '../decorators/require-session-auth.decorator';
import { JwtType } from '../../core/auth/dto/jwt-payload';
import { Reflector } from '@nestjs/core';
import { EnvironmentService } from '../../integrations/environment/environment.service';
import { addDays } from 'date-fns';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
private logger = new Logger('JwtAuthGuard');
constructor(
private reflector: Reflector,
private environmentService: EnvironmentService,
@@ -36,6 +46,39 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
throw err || new UnauthorizedException();
}
const requiresSession = this.reflector.getAllAndOverride<boolean>(
REQUIRE_SESSION_AUTH_KEY,
[ctx.getHandler(), ctx.getClass()],
);
if (requiresSession && user.authType !== JwtType.ACCESS) {
this.logger.debug(
`session-only endpoint ${ctx.getClass()?.name}.${ctx.getHandler()?.name} refused authType ${user.authType}`,
);
throw new ForbiddenException(
'This action requires an interactive user session',
);
}
if (user.oauth) {
const required = this.reflector.getAllAndOverride<
OAuthRouteScope | undefined
>(OAUTH_SCOPE_KEY, [ctx.getHandler(), ctx.getClass()]);
if (!required) {
this.logger.warn(
`oauth scope check: no @OAuthScope metadata on ${ctx.getClass()?.name}.${ctx.getHandler()?.name}`,
);
throw new ForbiddenException('OAuth tokens cannot access this endpoint');
}
const scopes: string[] = user.oauth.scopes ?? [];
const satisfied =
required === 'read'
? scopes.includes('read') || scopes.includes('write')
: scopes.includes('write');
if (!satisfied) {
throw new ForbiddenException('insufficient_scope');
}
}
this.setJoinedWorkspacesCookie(user, ctx);
return user;
}
@@ -0,0 +1,66 @@
import {
resolveFrameHeader,
resolveFrameHeadersForPath,
SecurityHeader,
} from './security-headers';
describe('resolveFrameHeader', () => {
it('denies framing with X-Frame-Options when embedding is off', () => {
expect(resolveFrameHeader(false, [])).toEqual({
name: 'X-Frame-Options',
value: 'SAMEORIGIN',
});
});
it('returns null when embedding is on but no origins are configured', () => {
expect(resolveFrameHeader(true, [])).toBeNull();
});
it('emits a frame-ancestors CSP for the allowed origins', () => {
expect(resolveFrameHeader(true, ['https://a.example', 'https://b.example']))
.toEqual({
name: 'Content-Security-Policy',
value: "frame-ancestors 'self' https://a.example https://b.example",
});
});
});
describe('resolveFrameHeadersForPath', () => {
const configured: SecurityHeader = {
name: 'Content-Security-Policy',
value: "frame-ancestors 'self' https://a.example",
};
it.each(['/oauth/consent', '/oauth/consent/nested'])(
'force-denies %s regardless of configured header',
(path) => {
expect(resolveFrameHeadersForPath(path, configured)).toEqual([
{ name: 'X-Frame-Options', value: 'DENY' },
{ name: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
]);
},
);
it('force-denies consent even when the global header is absent', () => {
expect(resolveFrameHeadersForPath('/oauth/consent', null)).toEqual([
{ name: 'X-Frame-Options', value: 'DENY' },
{ name: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
]);
});
it('does not match an unrelated path that merely contains the prefix', () => {
expect(
resolveFrameHeadersForPath('/oauth/consenting-adults', configured),
).toEqual([configured]);
});
it('passes the configured header through for other paths', () => {
expect(resolveFrameHeadersForPath('/home', configured)).toEqual([
configured,
]);
});
it('returns nothing for other paths when no header is configured', () => {
expect(resolveFrameHeadersForPath('/home', null)).toEqual([]);
});
});
@@ -17,3 +17,19 @@ export function resolveFrameHeader(
value: `frame-ancestors 'self' ${allowedOrigins.join(' ')}`,
};
}
// Deny OAuth consent in iframe
export const OAUTH_CONSENT_PATH = '/oauth/consent';
export function resolveFrameHeadersForPath(
path: string,
configuredHeader: SecurityHeader | null,
): SecurityHeader[] {
if (path === OAUTH_CONSENT_PATH || path.startsWith(`${OAUTH_CONSENT_PATH}/`)) {
return [
{ name: 'X-Frame-Options', value: 'DENY' },
{ name: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
];
}
return configuredHeader ? [configuredHeader] : [];
}
+19 -2
View File
@@ -28,6 +28,7 @@ export type RedisConfig = {
host: string;
port: number;
db: number;
username?: string;
password?: string;
family?: number;
tls?: { rejectUnauthorized?: boolean };
@@ -36,7 +37,15 @@ export type RedisConfig = {
export function parseRedisUrl(redisUrl: string): RedisConfig {
// format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6][&rejectUnauthorized=false]
const url = new URL(redisUrl);
const { hostname, port, password, pathname, protocol, searchParams } = url;
const {
hostname,
port,
username,
password,
pathname,
protocol,
searchParams,
} = url;
const portInt = port ? parseInt(port, 10) : 6379;
let db: number = 0;
@@ -62,7 +71,15 @@ export function parseRedisUrl(redisUrl: string): RedisConfig {
: {}
: undefined;
return { host: hostname, port: portInt, password: password || undefined, db, family, tls };
return {
host: hostname,
port: portInt,
username: username ? decodeURIComponent(username) : undefined,
password: password ? decodeURIComponent(password) : undefined,
db,
family,
tls,
};
}
export function createRetryStrategy() {
@@ -24,6 +24,7 @@ import * as bytes from 'bytes';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { OAuthScope } from '../../common/decorators/oauth-scope.decorator';
import { Attachment, User, Workspace } from '@docmost/db/types/entity.types';
import { StorageService } from '../../integrations/storage/storage.service';
import {
@@ -175,6 +176,7 @@ export class AttachmentController {
@UseGuards(JwtAuthGuard)
@Get('/files/:fileId/:fileName')
@OAuthScope('read')
async getFile(
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
@@ -427,6 +429,7 @@ export class AttachmentController {
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('pages/attachments')
@OAuthScope('read')
async getPageAttachments(
@Body() dto: PageIdDto,
@Body() pagination: PaginationOptions,
+2 -2
View File
@@ -12,7 +12,7 @@ import {
} from '@nestjs/common';
import { SkipThrottle, ThrottlerGuard } from '@nestjs/throttler';
import {
AI_CHAT_THROTTLER,
ALL_NAMED_THROTTLERS_SKIPPED,
AUTH_THROTTLER,
} from '../../integrations/throttle/throttler-names';
import { LoginDto } from './dto/login.dto';
@@ -38,7 +38,7 @@ import {
IAuditService,
} from '../../integrations/audit/audit.service';
@SkipThrottle({ [AI_CHAT_THROTTLER]: true })
@SkipThrottle({ ...ALL_NAMED_THROTTLERS_SKIPPED, [AUTH_THROTTLER]: false })
@UseGuards(ThrottlerGuard)
@Controller('auth')
export class AuthController {
@@ -7,6 +7,7 @@ export enum JwtType {
API_KEY = 'api_key',
PDF_RENDER = 'pdf_render',
PDF_EXPORT_DOWNLOAD = 'pdf_export_download',
OAUTH_ACCESS = 'oauth_access',
}
export type JwtPayload = {
sub: string;
@@ -59,3 +60,14 @@ export type JwtPdfExportDownloadPayload = {
workspaceId: string;
type: 'pdf_export_download';
};
export type JwtOAuthPayload = {
sub: string;
workspaceId: string;
grantId: string;
scope: string;
aud: string;
iss: string;
jti: string;
type: JwtType.OAUTH_ACCESS;
};
@@ -2,6 +2,7 @@ import {
BadRequestException,
Inject,
Injectable,
Logger,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
@@ -40,9 +41,13 @@ import {
IAuditService,
} from '../../../integrations/audit/audit.service';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { EventName } from '../../../common/events/event.contants';
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(
private signupService: SignupService,
private tokenService: TokenService,
@@ -53,6 +58,7 @@ export class AuthService {
private mailService: MailService,
private domainService: DomainService,
private environmentService: EnvironmentService,
private eventEmitter: EventEmitter2,
@InjectKysely() private readonly db: KyselyDB,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
) {}
@@ -261,6 +267,19 @@ export class AuthService {
await this.userSessionRepo.deleteByUserId(user.id, workspace.id);
// A failed revocation must not block the reset itself; log loudly instead.
try {
await this.eventEmitter.emitAsync(EventName.USER_PASSWORD_RESET, {
userId: user.id,
workspaceId: workspace.id,
});
} catch (err) {
this.logger.error(
`failed to revoke oauth grants for user ${user.id} after password reset`,
err,
);
}
this.auditService.setActorId(user.id);
this.auditService.log({
event: AuditEvent.USER_PASSWORD_RESET,
@@ -2,7 +2,12 @@ import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { JwtApiKeyPayload, JwtPayload, JwtType } from '../dto/jwt-payload';
import {
JwtApiKeyPayload,
JwtOAuthPayload,
JwtPayload,
JwtType,
} from '../dto/jwt-payload';
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
import { UserRepo } from '@docmost/db/repos/user/user.repo';
import { UserSessionRepo } from '@docmost/db/repos/session/user-session.repo';
@@ -33,7 +38,10 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
});
}
async validate(req: any, payload: JwtPayload | JwtApiKeyPayload) {
async validate(
req: any,
payload: JwtPayload | JwtApiKeyPayload | JwtOAuthPayload,
) {
if (!payload.workspaceId) {
throw new UnauthorizedException();
}
@@ -42,8 +50,21 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
throw new UnauthorizedException('Workspace does not match');
}
// authType lets guards tell an interactive session from a programmatic credential.
if (payload.type === JwtType.API_KEY) {
return this.validateApiKey(req, payload as JwtApiKeyPayload);
const authResult = await this.validateApiKey(
req,
payload as JwtApiKeyPayload,
);
return { ...authResult, authType: JwtType.API_KEY };
}
if (payload.type === JwtType.OAUTH_ACCESS) {
const authResult = await this.validateOAuthToken(
req,
payload as JwtOAuthPayload,
);
return { ...authResult, authType: JwtType.OAUTH_ACCESS };
}
if (payload.type !== JwtType.ACCESS) {
@@ -71,7 +92,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
this.sessionActivityService.trackActivity(sessionId, payload.sub, payload.workspaceId);
}
return { user, workspace };
return { user, workspace, authType: JwtType.ACCESS };
}
private async validateApiKey(req: any, payload: JwtApiKeyPayload) {
@@ -99,4 +120,36 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
throw new UnauthorizedException('Enterprise API Key module missing');
}
private async validateOAuthToken(req: any, payload: JwtOAuthPayload) {
let OAuthStrategyModule: any;
let isOAuthModuleReady = false;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
OAuthStrategyModule = require('./../../../ee/oauth/services/oauth-strategy.service');
isOAuthModuleReady = true;
} catch (err) {
this.logger.debug(
'OAuth module requested but enterprise module not bundled in this build',
);
isOAuthModuleReady = false;
}
if (isOAuthModuleReady) {
const OAuthStrategyService = this.moduleRef.get(
OAuthStrategyModule.OAuthStrategyService,
{
strict: false,
},
);
return OAuthStrategyService.validateOAuthToken(payload, {
workspaceId: req.raw.workspaceId,
host: req.raw.headers?.host ?? req.headers?.host,
});
}
throw new UnauthorizedException('Enterprise OAuth module missing');
}
}
@@ -16,6 +16,7 @@ import { PageIdDto, CommentIdDto } from './dto/comments.input';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { OAuthScope } from '../../common/decorators/oauth-scope.decorator';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { User, Workspace } from '@docmost/db/types/entity.types';
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
@@ -48,6 +49,7 @@ export class CommentController {
@HttpCode(HttpStatus.OK)
@Post('create')
@OAuthScope('write')
async create(
@Body() createCommentDto: CreateCommentDto,
@AuthUser() user: User,
@@ -84,6 +86,7 @@ export class CommentController {
@HttpCode(HttpStatus.OK)
@Post('/')
@OAuthScope('read')
async findPageComments(
@Body() input: PageIdDto,
@Body()
@@ -125,6 +128,7 @@ export class CommentController {
@HttpCode(HttpStatus.OK)
@Post('update')
@OAuthScope('write')
async update(@Body() dto: UpdateCommentDto, @AuthUser() user: User, @AuthWorkspace() workspace: Workspace) {
const comment = await this.commentRepo.findById(dto.commentId, {
includeCreator: true,
@@ -26,6 +26,7 @@ import { PageHistoryService } from './services/page-history.service';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { OAuthScope } from '../../common/decorators/oauth-scope.decorator';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { Page, User, Workspace } from '@docmost/db/types/entity.types';
import { SidebarPageDto } from './dto/sidebar-page.dto';
@@ -69,6 +70,7 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('/info')
@OAuthScope('read')
async getPage(@Body() dto: PageInfoDto, @AuthUser() user: User) {
const page = await this.pageRepo.findById(dto.pageId, {
includeSpace: true,
@@ -199,6 +201,7 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('create')
@OAuthScope('write')
async create(
@Body() createPageDto: CreatePageDto,
@AuthUser() user: User,
@@ -269,6 +272,7 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('update')
@OAuthScope('write')
async update(@Body() updatePageDto: UpdatePageDto, @AuthUser() user: User) {
const page = await this.pageRepo.findById(updatePageDto.pageId);
@@ -413,6 +417,7 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('recent')
@OAuthScope('read')
async getRecentPages(
@Body() recentPageDto: RecentPageDto,
@Body() pagination: PaginationOptions,
@@ -527,6 +532,7 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('/sidebar-pages')
@OAuthScope('read')
async getSidebarPages(
@Body() dto: SidebarPageDto,
@Body() pagination: PaginationOptions,
@@ -569,6 +575,7 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('move-to-space')
@OAuthScope('write')
async movePageToSpace(
@Body() dto: MovePageToSpaceDto,
@AuthUser() user: User,
@@ -622,6 +629,7 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('duplicate')
@OAuthScope('write')
async duplicatePage(@Body() dto: DuplicatePageDto, @AuthUser() user: User) {
const copiedPage = await this.pageRepo.findById(dto.pageId);
if (!copiedPage) {
@@ -706,6 +714,7 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('move')
@OAuthScope('write')
async movePage(@Body() dto: MovePageDto, @AuthUser() user: User) {
const movedPage = await this.pageRepo.findById(dto.pageId);
if (!movedPage) {
@@ -17,6 +17,7 @@ import {
} from './dto/search.dto';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { OAuthScope } from '../../common/decorators/oauth-scope.decorator';
import { User, Workspace } from '@docmost/db/types/entity.types';
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
import {
@@ -42,6 +43,7 @@ export class SearchController {
@HttpCode(HttpStatus.OK)
@Post()
@OAuthScope('read')
async pageSearch(
@Body() searchDto: SearchDTO,
@AuthUser() user: User,
@@ -75,6 +77,7 @@ export class SearchController {
@HttpCode(HttpStatus.OK)
@Post('suggest')
@OAuthScope('read')
async searchSuggestions(
@Body() dto: SearchSuggestionDTO,
@AuthUser() user: User,
@@ -13,6 +13,7 @@ import { SpaceService } from './services/space.service';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { OAuthScope } from '../../common/decorators/oauth-scope.decorator';
import { SpaceIdDto } from './dto/space-id.dto';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { SpaceMemberService } from './services/space-member.service';
@@ -48,6 +49,7 @@ export class SpaceController {
@HttpCode(HttpStatus.OK)
@Post('/')
@OAuthScope('read')
async getWorkspaceSpaces(
@Body()
pagination: PaginationOptions,
@@ -92,6 +94,7 @@ export class SpaceController {
@HttpCode(HttpStatus.OK)
@Post('info')
@OAuthScope('read')
async getSpaceInfo(
@Body() spaceIdDto: SpaceIdDto,
@AuthUser() user: User,
@@ -129,6 +132,7 @@ export class SpaceController {
@HttpCode(HttpStatus.OK)
@Post('create')
@OAuthScope('write')
createSpace(
@Body() createSpaceDto: CreateSpaceDto,
@AuthUser() user: User,
@@ -145,6 +149,7 @@ export class SpaceController {
@HttpCode(HttpStatus.OK)
@Post('update')
@OAuthScope('write')
async updateSpace(
@Body() updateSpaceDto: UpdateSpaceDto,
@AuthUser() user: User,
@@ -10,6 +10,7 @@ import { UserService } from './user.service';
import { UpdateUserDto } from './dto/update-user.dto';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { OAuthScope } from '../../common/decorators/oauth-scope.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import { User, Workspace } from '@docmost/db/types/entity.types';
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
@@ -24,6 +25,7 @@ export class UserController {
@HttpCode(HttpStatus.OK)
@Post('me')
@OAuthScope('read')
async getUserInfo(
@AuthUser() authUser: User,
@AuthWorkspace() workspace: Workspace,
@@ -24,6 +24,7 @@ import {
RevokeInviteDto,
} from '../dto/invitation.dto';
import { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard';
import { OAuthScope } from '../../../common/decorators/oauth-scope.decorator';
import { User, Workspace } from '@docmost/db/types/entity.types';
import WorkspaceAbilityFactory from '../../casl/abilities/workspace-ability.factory';
import {
@@ -58,6 +59,7 @@ export class WorkspaceController {
@HttpCode(HttpStatus.OK)
@Post('/info')
@OAuthScope('read')
async getWorkspace(@AuthWorkspace() workspace: Workspace) {
return this.workspaceService.getWorkspaceInfo(workspace.id);
}
@@ -113,6 +115,7 @@ export class WorkspaceController {
@HttpCode(HttpStatus.OK)
@Post('members')
@OAuthScope('read')
async getWorkspaceMembers(
@Body()
pagination: PaginationOptions,
@@ -76,4 +76,8 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
@IsOptional()
@IsBoolean()
aiChatWorkspaceKnowledgeOnly: boolean;
@IsOptional()
@IsBoolean()
enforceMcpOauth: boolean;
}
@@ -336,7 +336,8 @@ export class WorkspaceService {
typeof updateWorkspaceDto.isScimEnabled !== 'undefined' ||
typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined' ||
typeof updateWorkspaceDto.aiChatReadOnly !== 'undefined' ||
typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined'
typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined' ||
typeof updateWorkspaceDto.enforceMcpOauth !== 'undefined'
) {
const ws = await this.db
.selectFrom('workspaces')
@@ -391,6 +392,18 @@ export class WorkspaceService {
}
}
if (typeof updateWorkspaceDto.enforceMcpOauth !== 'undefined') {
if (
!this.licenseCheckService.hasFeature(
ws.licenseKey,
Feature.MCP_CONTROLS,
ws.plan,
)
) {
throw new ForbiddenException('This feature requires a valid license');
}
}
if (
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
@@ -561,6 +574,20 @@ export class WorkspaceService {
);
}
if (typeof updateWorkspaceDto.enforceMcpOauth !== 'undefined') {
const prev = settingsBefore?.ai?.enforceMcpOauth ?? false;
if (prev !== updateWorkspaceDto.enforceMcpOauth) {
before.enforceMcpOauth = prev;
after.enforceMcpOauth = updateWorkspaceDto.enforceMcpOauth;
}
await this.workspaceRepo.updateAiSettings(
workspaceId,
'enforceMcpOauth',
updateWorkspaceDto.enforceMcpOauth,
trx,
);
}
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
const prev = settingsBefore?.spaces?.allowPersonal ?? false;
if (prev !== updateWorkspaceDto.allowPersonalSpaces) {
@@ -600,6 +627,7 @@ export class WorkspaceService {
delete updateWorkspaceDto.defaultPageEditMode;
delete updateWorkspaceDto.aiChatReadOnly;
delete updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly;
delete updateWorkspaceDto.enforceMcpOauth;
await this.workspaceRepo.updateWorkspace(
updateWorkspaceDto,
@@ -0,0 +1,88 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('oauth_clients')
.addColumn('id', 'uuid', (col) => col.primaryKey().defaultTo(sql`gen_uuid_v7()`))
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('redirect_uris', 'jsonb', (col) => col.notNull())
.addColumn('client_uri', 'text')
.addColumn('logo_uri', 'text')
.addColumn('grant_types', 'jsonb', (col) => col.notNull())
.addColumn('scopes', 'jsonb', (col) => col.notNull())
.addColumn('token_endpoint_auth_method', 'text', (col) => col.notNull().defaultTo('none'))
.addColumn('secret_hash', 'text')
.addColumn('is_dynamic', 'boolean', (col) => col.notNull().defaultTo(true))
.addColumn('workspace_id', 'uuid', (col) => col.notNull().references('workspaces.id').onDelete('cascade'))
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
.addColumn('updated_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
.addColumn('deleted_at', 'timestamptz')
.execute();
await db.schema.createIndex('oauth_clients_workspace_id_idx').on('oauth_clients').column('workspace_id').execute();
await db.schema
.createTable('oauth_authorization_codes')
.addColumn('id', 'uuid', (col) => col.primaryKey().defaultTo(sql`gen_uuid_v7()`))
.addColumn('code_hash', 'text', (col) => col.notNull().unique())
.addColumn('client_id', 'uuid', (col) => col.notNull().references('oauth_clients.id').onDelete('cascade'))
.addColumn('user_id', 'uuid', (col) => col.notNull().references('users.id').onDelete('cascade'))
.addColumn('workspace_id', 'uuid', (col) => col.notNull().references('workspaces.id').onDelete('cascade'))
.addColumn('scopes', 'jsonb', (col) => col.notNull())
.addColumn('redirect_uri', 'text', (col) => col.notNull())
.addColumn('code_challenge', 'text')
.addColumn('code_challenge_method', 'text')
.addColumn('expires_at', 'timestamptz', (col) => col.notNull())
.addColumn('consumed_at', 'timestamptz')
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
.execute();
await db.schema.createIndex('oauth_authorization_codes_expires_at_idx').on('oauth_authorization_codes').column('expires_at').execute();
await db.schema.createIndex('oauth_authorization_codes_consumed_at_idx').on('oauth_authorization_codes').column('consumed_at').execute();
await db.schema.createIndex('oauth_authorization_codes_client_id_idx').on('oauth_authorization_codes').column('client_id').execute();
await db.schema.createIndex('oauth_authorization_codes_user_id_idx').on('oauth_authorization_codes').column('user_id').execute();
await db.schema.createIndex('oauth_authorization_codes_workspace_id_idx').on('oauth_authorization_codes').column('workspace_id').execute();
await db.schema
.createTable('oauth_grants')
.addColumn('id', 'uuid', (col) => col.primaryKey().defaultTo(sql`gen_uuid_v7()`))
.addColumn('user_id', 'uuid', (col) => col.notNull().references('users.id').onDelete('cascade'))
.addColumn('client_id', 'uuid', (col) => col.notNull().references('oauth_clients.id').onDelete('cascade'))
.addColumn('workspace_id', 'uuid', (col) => col.notNull().references('workspaces.id').onDelete('cascade'))
.addColumn('scopes', 'jsonb', (col) => col.notNull())
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
.addColumn('updated_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
.addColumn('last_used_at', 'timestamptz')
.addColumn('revoked_at', 'timestamptz')
.addUniqueConstraint('oauth_grants_user_client_unique', ['user_id', 'client_id'])
.execute();
// The user_id/client_id unique constraint cannot serve client-side FK lookups.
await db.schema.createIndex('oauth_grants_client_id_idx').on('oauth_grants').column('client_id').execute();
await db.schema
.createTable('oauth_tokens')
.addColumn('id', 'uuid', (col) => col.primaryKey().defaultTo(sql`gen_uuid_v7()`))
.addColumn('grant_id', 'uuid', (col) => col.notNull().references('oauth_grants.id').onDelete('cascade'))
.addColumn('workspace_id', 'uuid', (col) => col.notNull().references('workspaces.id').onDelete('cascade'))
.addColumn('access_token_jti', 'text', (col) => col.notNull())
.addColumn('refresh_token_hash', 'text', (col) => col.unique())
.addColumn('scopes', 'jsonb', (col) => col.notNull())
.addColumn('access_expires_at', 'timestamptz', (col) => col.notNull())
.addColumn('refresh_expires_at', 'timestamptz')
.addColumn('revoked_at', 'timestamptz')
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
.execute();
await db.schema.createIndex('oauth_tokens_grant_id_idx').on('oauth_tokens').column('grant_id').execute();
await db.schema.createIndex('oauth_tokens_access_token_jti_idx').on('oauth_tokens').columns(['workspace_id', 'access_token_jti']).execute();
// One index per branch of the cleanup sweep's OR so it can bitmap-or them.
await db.schema.createIndex('oauth_tokens_access_expires_at_idx').on('oauth_tokens').column('access_expires_at').execute();
await db.schema.createIndex('oauth_tokens_refresh_expires_at_idx').on('oauth_tokens').column('refresh_expires_at').execute();
await db.schema.createIndex('oauth_tokens_revoked_at_idx').on('oauth_tokens').column('revoked_at').execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('oauth_tokens').execute();
await db.schema.dropTable('oauth_grants').execute();
await db.schema.dropTable('oauth_authorization_codes').execute();
await db.schema.dropTable('oauth_clients').execute();
}
+61
View File
@@ -636,6 +636,63 @@ export interface UserSessions {
createdAt: Generated<Timestamp>;
}
export interface OauthAuthorizationCodes {
clientId: string;
codeChallenge: string | null;
codeChallengeMethod: string | null;
codeHash: string;
consumedAt: Timestamp | null;
createdAt: Generated<Timestamp>;
expiresAt: Timestamp;
id: Generated<string>;
redirectUri: string;
scopes: Json;
userId: string;
workspaceId: string;
}
export interface OauthClients {
clientUri: string | null;
createdAt: Generated<Timestamp>;
deletedAt: Timestamp | null;
grantTypes: Json;
id: Generated<string>;
isDynamic: Generated<boolean>;
logoUri: string | null;
name: string;
redirectUris: Json;
scopes: Json;
secretHash: string | null;
tokenEndpointAuthMethod: Generated<string>;
updatedAt: Generated<Timestamp>;
workspaceId: string;
}
export interface OauthGrants {
clientId: string;
createdAt: Generated<Timestamp>;
id: Generated<string>;
lastUsedAt: Timestamp | null;
revokedAt: Timestamp | null;
scopes: Json;
updatedAt: Generated<Timestamp>;
userId: string;
workspaceId: string;
}
export interface OauthTokens {
accessExpiresAt: Timestamp;
accessTokenJti: string;
createdAt: Generated<Timestamp>;
grantId: string;
id: Generated<string>;
refreshExpiresAt: Timestamp | null;
refreshTokenHash: string | null;
revokedAt: Timestamp | null;
scopes: Json;
workspaceId: string;
}
export interface DB {
aiChats: AiChats;
aiChatMessages: AiChatMessages;
@@ -677,4 +734,8 @@ export interface DB {
watchers: Watchers;
workspaceInvitations: WorkspaceInvitations;
workspaces: Workspaces;
oauthAuthorizationCodes: OauthAuthorizationCodes;
oauthClients: OauthClients;
oauthGrants: OauthGrants;
oauthTokens: OauthTokens;
}
@@ -40,6 +40,10 @@ import {
Watchers,
Audit as _Audit,
Templates,
OauthClients,
OauthAuthorizationCodes,
OauthGrants,
OauthTokens,
} from './db';
import { PageEmbeddings } from '@docmost/db/types/embeddings.types';
@@ -184,6 +188,13 @@ export type ScimToken = Selectable<ScimTokens>;
export type InsertableScimToken = Insertable<ScimTokens>;
export type UpdatableScimToken = Updateable<Omit<ScimTokens, 'id'>>;
// OAuth
export type OAuthClient = Selectable<OauthClients>;
export type InsertableOAuthClient = Insertable<OauthClients>;
export type OAuthGrant = Selectable<OauthGrants>;
export type OAuthAuthorizationCode = Selectable<OauthAuthorizationCodes>;
export type OAuthToken = Selectable<OauthTokens>;
// Page Embedding
export type PageEmbedding = Selectable<PageEmbeddings>;
export type InsertablePageEmbedding = Insertable<PageEmbeddings>;
@@ -97,6 +97,15 @@ export function xwikiFormatter($: CheerioAPI, $root: Cheerio<any>) {
}
}
function isBareLink($el: Cheerio<any>): boolean {
const href = $el.attr("href")?.trim();
const text = $el.text().trim();
if(!text || !href) return false
return text === href;
}
export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
normalizeTableColumnWidths($, $root);
@@ -104,7 +113,9 @@ export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
const $el = $(el);
const url = $el.attr('href')!;
const { provider } = getEmbedUrlAndProvider(url);
if (provider === 'iframe') return;
if (provider === 'iframe' || !isBareLink($el)) {
return;
}
const embed = `<div data-type=\"embed\" data-src=\"${url}\" data-provider=\"${provider}\" data-align=\"center\" data-width=\"640\" data-height=\"480\"></div>`;
$el.replaceWith(embed);
@@ -15,6 +15,7 @@ import { GeneralQueueProcessor } from './processors/general-queue.processor';
connection: {
host: redisConfig.host,
port: redisConfig.port,
username: redisConfig.username,
password: redisConfig.password,
db: redisConfig.db,
family: redisConfig.family,
@@ -16,6 +16,7 @@ export class RedisConfigService implements RedisOptionsFactory {
config: {
host: redisConfig.host,
port: redisConfig.port,
username: redisConfig.username,
password: redisConfig.password,
db: redisConfig.db,
family: redisConfig.family,
@@ -4,7 +4,13 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'
import { EnvironmentService } from '../environment/environment.service';
import { EnvironmentModule } from '../environment/environment.module';
import { createRetryStrategy, parseRedisUrl } from '../../common/helpers';
import { AUTH_THROTTLER, AI_CHAT_THROTTLER } from './throttler-names';
import {
AUTH_THROTTLER,
AI_CHAT_THROTTLER,
OAUTH_REGISTER_THROTTLER,
OAUTH_TOKEN_THROTTLER,
OAUTH_AUTHORIZE_THROTTLER,
} from './throttler-names';
import Redis from 'ioredis';
@Module({
@@ -18,12 +24,16 @@ import Redis from 'ioredis';
throttlers: [
{ name: AUTH_THROTTLER, ttl: 60_000, limit: 10 },
{ name: AI_CHAT_THROTTLER, ttl: 60_000, limit: 25 },
{ name: OAUTH_REGISTER_THROTTLER, ttl: 3_600_000, limit: 10 },
{ name: OAUTH_TOKEN_THROTTLER, ttl: 60_000, limit: 60 },
{ name: OAUTH_AUTHORIZE_THROTTLER, ttl: 60_000, limit: 30 },
],
errorMessage: 'Too many requests',
storage: new ThrottlerStorageRedisService(
new Redis({
host: redisConfig.host,
port: redisConfig.port,
username: redisConfig.username,
password: redisConfig.password,
db: redisConfig.db,
family: redisConfig.family,
@@ -1,2 +1,14 @@
export const AUTH_THROTTLER = 'auth';
export const AI_CHAT_THROTTLER = 'ai-chat';
export const OAUTH_REGISTER_THROTTLER = 'oauth-register';
export const OAUTH_TOKEN_THROTTLER = 'oauth-token';
export const OAUTH_AUTHORIZE_THROTTLER = 'oauth-authorize';
// Every named throttler must appear here; spread it in @SkipThrottle and re-enable per name with false.
export const ALL_NAMED_THROTTLERS_SKIPPED: Record<string, boolean> = {
[AUTH_THROTTLER]: true,
[AI_CHAT_THROTTLER]: true,
[OAUTH_REGISTER_THROTTLER]: true,
[OAUTH_TOKEN_THROTTLER]: true,
[OAUTH_AUTHORIZE_THROTTLER]: true,
};
+30 -18
View File
@@ -13,7 +13,10 @@ import fastifyCookie from '@fastify/cookie';
import fastifyIp from 'fastify-ip';
import { InternalLogFilter } from './common/logger/internal-log-filter';
import { EnvironmentService } from './integrations/environment/environment.service';
import { resolveFrameHeader } from './common/helpers';
import {
resolveFrameHeader,
resolveFrameHeadersForPath,
} from './common/helpers';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
@@ -39,7 +42,14 @@ async function bootstrap() {
app.useLogger(app.get(PinoLogger));
app.setGlobalPrefix('api', {
exclude: ['robots.txt', 'share/:shareId/p/:pageSlug', 'mcp'],
exclude: [
'robots.txt',
'share/:shareId/p/:pageSlug',
'mcp',
'.well-known/oauth-authorization-server',
'.well-known/oauth-protected-resource',
'.well-known/oauth-protected-resource/mcp',
],
});
const reflector = app.get(Reflector);
@@ -57,22 +67,24 @@ async function bootstrap() {
environmentService.isIframeEmbedAllowed(),
environmentService.getIframeAllowedOrigins(),
);
if (frameHeader) {
// Skipped routes:
// /api/files/ - attachment controller sets its own CSP we'd overwrite
// /share/ 0 public share pages are safe to embed
const frameHeaderSkippedPrefixes = ['/api/files/', '/share/'];
app
.getHttpAdapter()
.getInstance()
.addHook('onSend', (req, reply, payload, done) => {
if (frameHeaderSkippedPrefixes.some((p) => req.url.startsWith(p))) {
return done(null, payload);
}
reply.header(frameHeader.name, frameHeader.value);
done(null, payload);
});
}
// Skipped routes:
// /api/files/ - attachment controller sets its own CSP we'd overwrite
// /share/ - public share pages are safe to embed
const frameHeaderSkippedPrefixes = ['/api/files/', '/share/'];
app
.getHttpAdapter()
.getInstance()
.addHook('onSend', (req, reply, payload, done) => {
if (frameHeaderSkippedPrefixes.some((p) => req.url.startsWith(p))) {
return done(null, payload);
}
const path = req.url.split('?')[0];
// Force-denies the oauth consent screen even when the global frame header is absent.
for (const header of resolveFrameHeadersForPath(path, frameHeader)) {
reply.header(header.name, header.value);
}
done(null, payload);
});
app
.getHttpAdapter()
+5 -1
View File
@@ -2,7 +2,7 @@ import { Node, mergeAttributes } from "@tiptap/core";
import { ResizableNodeView } from "./resizable-nodeview";
import type { ResizableNodeViewDirection } from "./resizable-nodeview";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { normalizeFileUrl } from "./media-utils";
import { normalizeFileUrl, syncAltBadge } from "./media-utils";
export type DrawioResizeOptions = {
enabled: boolean;
@@ -293,6 +293,8 @@ export const Drawio = Node.create<DrawioOptions>({
const container = nodeView.dom as HTMLElement;
applyAlignment(container, align);
syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt);
currentNode = updatedNode;
return true;
},
@@ -310,6 +312,8 @@ export const Drawio = Node.create<DrawioOptions>({
const dom = nodeView.dom as HTMLElement;
syncAltBadge(nodeView.wrapper, node.attrs.alt);
applyAlignment(dom, node.attrs.align || "center");
// Handle percentage width backward compat
+5 -1
View File
@@ -2,7 +2,7 @@ import { Node, mergeAttributes } from "@tiptap/core";
import { ResizableNodeView } from "./resizable-nodeview";
import type { ResizableNodeViewDirection } from "./resizable-nodeview";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { normalizeFileUrl } from "./media-utils";
import { normalizeFileUrl, syncAltBadge } from "./media-utils";
export type ExcalidrawResizeOptions = {
enabled: boolean;
@@ -293,6 +293,8 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
const container = nodeView.dom as HTMLElement;
applyAlignment(container, align);
syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt);
currentNode = updatedNode;
return true;
},
@@ -310,6 +312,8 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
const dom = nodeView.dom as HTMLElement;
syncAltBadge(nodeView.wrapper, node.attrs.alt);
applyAlignment(dom, node.attrs.align || "center");
// Handle percentage width backward compat
+5 -1
View File
@@ -7,7 +7,7 @@ import {
} from "@tiptap/core";
import { ResizableNodeView } from "../resizable-nodeview";
import type { ResizableNodeViewDirection } from "../resizable-nodeview";
import { normalizeFileUrl } from "../media-utils";
import { normalizeFileUrl, syncAltBadge } from "../media-utils";
export type ImageResizeOptions = {
enabled: boolean;
@@ -316,6 +316,8 @@ export const TiptapImage = Image.extend<ImageOptions>({
const container = nodeView.dom as HTMLElement;
applyAlignment(container, align);
syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt);
currentNode = updatedNode;
return true;
},
@@ -333,6 +335,8 @@ export const TiptapImage = Image.extend<ImageOptions>({
const dom = nodeView.dom as HTMLElement;
syncAltBadge(nodeView.wrapper, node.attrs.alt);
// Apply initial alignment
applyAlignment(dom, node.attrs.align || "center");
@@ -7,6 +7,27 @@ export function normalizeFileUrl(src: string): string {
return src || "";
}
export function syncAltBadge(wrapper: HTMLElement, alt: unknown): void {
const existing = wrapper.querySelector<HTMLElement>(
":scope > .media-alt-badge",
);
if (typeof alt !== "string" || !alt.trim()) {
existing?.remove();
return;
}
const badge = existing ?? document.createElement("span");
badge.dataset.alt = alt;
if (!existing) {
badge.className = "media-alt-badge";
badge.textContent = "ALT";
badge.setAttribute("aria-hidden", "true");
wrapper.appendChild(badge);
}
}
export type UploadFn = (
file: File,
editor: Editor,
+15
View File
@@ -527,6 +527,9 @@ importers:
'@fastify/static':
specifier: 10.1.3
version: 10.1.3
'@jmondi/oauth2-server':
specifier: ^4.3.7
version: 4.3.7
'@keyv/redis':
specifier: 5.1.6
version: 5.1.6(keyv@5.6.0)
@@ -2525,6 +2528,10 @@ packages:
resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jmondi/oauth2-server@4.3.7':
resolution: {integrity: sha512-Mo0HHIB/TVRETVSxzvmyclfNPn0HnvSyrLoqvrtlk0z3oWMUr1CFRc7msDNQm6vRJIbRGnCbNKuAwXiNaS6xfw==}
engines: {node: '>=16'}
'@joplin/turndown-plugin-gfm@1.0.64':
resolution: {integrity: sha512-8GJ7f9OenE3zkSVII5B6qzIkvgF7C/a20gaASEjM6jWPLPJFFQ2nQ3Ou/kXH1mPUTs9dC9VYs8QXVPvZabKXBQ==}
@@ -4994,6 +5001,7 @@ packages:
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@@ -6322,6 +6330,7 @@ packages:
eslint@9.28.0:
resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
peerDependencies:
jiti: '*'
@@ -12411,6 +12420,12 @@ snapshots:
'@types/yargs': 17.0.35
chalk: 4.1.2
'@jmondi/oauth2-server@4.3.7':
dependencies:
jsonwebtoken: 9.0.3
ms: 2.1.3
uri-js: 4.4.1
'@joplin/turndown-plugin-gfm@1.0.64': {}
'@joplin/turndown@4.0.82(supports-color@7.2.0)':