diff --git a/README.md b/README.md index 3bfe9f0..c797c37 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ This template equips you with a foundational React application integrated with A - **Authentication**: Setup with Amazon Cognito for secure user authentication with email login. - More info on how to setup and configuration option: https://docs.amplify.aws/react/build-a-backend/auth/set-up-auth/ - **Storage**: Configured with multiple S3 buckets and granular access controls. The sample is configured with - - Default storage bucket with public, admin, and private access paths - - Secondary storage bucket with separate backup paths. + - Default `frauden-bucket` storage bucket with `doctrina`, `medios`, `jurisprudencia`, and `legislacion` access paths for authenticated readers and admin read/write/delete access. + - Secondary `frauden-expedientes` storage bucket with `publico`, `confidencial`, and owner-scoped `privado/{entity_id}` access paths, including delete permissions wherever write access is granted. - More info on how to setup : https://docs.amplify.aws/react/build-a-backend/storage/set-up-storage/#building-your-storage-backend - **UI Components**: Pre-integrated Amplify UI React components including: - Authenticator for sign-in/sign-up flows diff --git a/amplify/auth/resource.ts b/amplify/auth/resource.ts index 8bbd191..e10a2e5 100644 --- a/amplify/auth/resource.ts +++ b/amplify/auth/resource.ts @@ -8,5 +8,5 @@ export const auth = defineAuth({ loginWith: { email: true, }, - groups: ['admin'] + groups: ['admin', 'eliminadores'], }); diff --git a/amplify/backend.ts b/amplify/backend.ts index 032938d..0c49596 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -2,12 +2,21 @@ import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource'; import { storage, secondaryStorage } from './storage/resource'; - /** * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more */ -defineBackend({ +const backend = defineBackend({ auth, - storage, - secondaryStorage + storage, + secondaryStorage, }); + +// Do not override cfnBucket.bucketName here. S3 bucket names are globally unique, +// and Amplify's generated physical names avoid collisions during deployments. + +const { cfnUserPool } = backend.auth.resources.cfnResources; + +cfnUserPool.adminCreateUserConfig = { + ...cfnUserPool.adminCreateUserConfig, + allowAdminCreateUserOnly: true, +}; diff --git a/amplify/storage/resource.ts b/amplify/storage/resource.ts index 62d6340..a31485c 100644 --- a/amplify/storage/resource.ts +++ b/amplify/storage/resource.ts @@ -1,39 +1,46 @@ import { defineStorage } from '@aws-amplify/backend'; export const storage = defineStorage({ - name: 'myStorageBucket', + name: 'frauden', isDefault: true, - access: (allow) => ({ - 'public/*': [ - allow.guest.to(['read', 'write']), - allow.authenticated.to(['read', 'write', 'delete']), + access: (allow) => ({ + 'doctrina/*': [ + allow.authenticated.to(['read']), + allow.groups(['admin']).to(['read', 'write']), + allow.groups(['eliminadores']).to(['delete']), ], - 'admin/*': [ - allow.groups(['admin']).to(['read', 'write', 'delete']), - allow.authenticated.to(['read']) + 'medios/*': [ + allow.authenticated.to(['read']), + allow.groups(['admin']).to(['read', 'write']), + allow.groups(['eliminadores']).to(['delete']), ], - 'private/{entity_id}/*': [ - allow.entity('identity').to(['read', 'write', 'delete']) - ] - }) + 'jurisprudencia/*': [ + allow.authenticated.to(['read']), + allow.groups(['admin']).to(['read', 'write']), + allow.groups(['eliminadores']).to(['delete']), + ], + 'legislacion/*': [ + allow.authenticated.to(['read']), + allow.groups(['admin']).to(['read', 'write']), + allow.groups(['eliminadores']).to(['delete']), + ], + }), }); export const secondaryStorage = defineStorage({ - name: 'mySecondaryStorageBucket', - access: (allow) => ({ - 'backup_public/*': [ - allow.guest.to(['read', 'write']), - allow.authenticated.to(['read', 'write', 'delete']), + name: 'frauden-expedientes', + access: (allow) => ({ + 'publico/*': [ + allow.authenticated.to(['read', 'write']), + allow.groups(['eliminadores']).to(['delete']), + ], + 'confidencial/*': [ + allow.groups(['admin']).to(['read', 'write']), + allow.groups(['eliminadores']).to(['delete']), ], - 'backup_admin/*': [ - allow.groups(['admin']).to(['read', 'write', 'delete']), - allow.authenticated.to(['read']) + 'privado/{entity_id}/*': [ + allow.entity('identity').to(['read', 'write']), + allow.groups(['eliminadores']).to(['delete']), ], - 'backup_private/{entity_id}/*': [ - allow.entity('identity').to(['read', 'write', 'delete']) - ] - }) + }), }); - - - diff --git a/index.html b/index.html index 1a96360..a929ef2 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,9 @@ - + - Amplify Storage Browser Sample + Frauden | Gestor documental
diff --git a/src/App.css b/src/App.css index 5bbce99..605fc59 100644 --- a/src/App.css +++ b/src/App.css @@ -1,11 +1,341 @@ +:root { + color-scheme: light; + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + --frauden-navy: #101936; + --frauden-navy-soft: #1f2d5f; + --frauden-blue: #26366f; + --frauden-silver: #c9ccd1; + --frauden-silver-dark: #8e939b; + --frauden-ink: #171b26; + --frauden-muted: #667085; + --frauden-surface: rgba(255, 255, 255, 0.9); + --frauden-border: rgba(16, 25, 54, 0.12); + --frauden-shadow: 0 24px 70px rgba(16, 25, 54, 0.14); + + --amplify-colors-brand-primary-10: #eef1f8; + --amplify-colors-brand-primary-20: #d8ddeb; + --amplify-colors-brand-primary-40: #9fa9cd; + --amplify-colors-brand-primary-60: #53639b; + --amplify-colors-brand-primary-80: var(--frauden-blue); + --amplify-colors-brand-primary-90: var(--frauden-navy-soft); + --amplify-colors-brand-primary-100: var(--frauden-navy); + --amplify-colors-font-primary: var(--frauden-ink); + --amplify-colors-font-secondary: var(--frauden-muted); + --amplify-colors-border-primary: var(--frauden-border); + --amplify-radii-small: 0.65rem; + --amplify-radii-medium: 0.95rem; + --amplify-radii-large: 1.4rem; +} + +* { + box-sizing: border-box; +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + color: var(--frauden-ink); + background: + radial-gradient(circle at top left, rgba(201, 204, 209, 0.48), transparent 34rem), + linear-gradient(135deg, #f8fafc 0%, #eef1f6 47%, #fdfdfd 100%); +} + +body::before { + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + content: ''; + background: + linear-gradient(115deg, transparent 0 62%, rgba(16, 25, 54, 0.08) 62% 100%), + radial-gradient(circle at 86% 10%, rgba(38, 54, 111, 0.14), transparent 20rem); +} + #root { + width: 100%; + min-height: 100vh; +} + +.app-shell { + width: min(1440px, 100%); + min-height: 100vh; margin: 0 auto; - padding: 2rem; - text-align: center; + padding: 1.5rem; +} + +.site-header { + display: flex; + gap: 1.5rem; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + background: rgba(255, 255, 255, 0.82); + border: 1px solid var(--frauden-border); + border-radius: 1.5rem; + box-shadow: 0 18px 50px rgba(16, 25, 54, 0.1); + backdrop-filter: blur(18px); +} + +.brand { + display: inline-flex; + align-items: center; + min-width: 0; +} + +.brand-logo { + display: block; + width: clamp(14rem, 34vw, 25rem); + max-height: 5.75rem; + object-fit: contain; +} + +.header-actions { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; + align-items: center; + justify-content: flex-end; +} + +.user-card { + min-width: min(15rem, 100%); + padding: 0.75rem 1rem; + text-align: right; + background: linear-gradient(135deg, rgba(16, 25, 54, 0.06), rgba(201, 204, 209, 0.18)); + border: 1px solid rgba(16, 25, 54, 0.08); + border-radius: 1rem; +} + +.user-card strong { + display: block; + overflow: hidden; + color: var(--frauden-navy); + text-overflow: ellipsis; + white-space: nowrap; +} + +.eyebrow { + display: inline-flex; + margin-bottom: 0.35rem; + color: var(--frauden-silver-dark); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.sign-out-button.amplify-button { + min-height: 3rem; + padding-inline: 1.25rem; + font-weight: 700; + background: linear-gradient(135deg, var(--frauden-navy), var(--frauden-blue)); + border: 0; + box-shadow: 0 14px 30px rgba(16, 25, 54, 0.25); +} + +.hero-card { + position: relative; + overflow: hidden; + margin: 1rem 0; + padding: clamp(1rem, 2.5vw, 2.25rem) clamp(1.25rem, 4vw, 3.75rem); + color: white; + background: + linear-gradient(135deg, rgba(16, 25, 54, 0.94), rgba(31, 45, 95, 0.88)), + linear-gradient(90deg, rgba(201, 204, 209, 0.22), transparent); + border-radius: 2rem; + box-shadow: var(--frauden-shadow); +} + +.hero-card::after { + position: absolute; + top: -38%; + right: -10%; + width: 27rem; + height: 27rem; + content: ''; + background: radial-gradient(circle, rgba(255, 255, 255, 0.2), transparent 62%); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 999px; +} + +.hero-card > div { + position: relative; + z-index: 1; + width: 100%; +} + +.hero-card .eyebrow { + color: var(--frauden-silver); +} + +.hero-card h1 { + width: 100%; + max-width: none; + margin: 0; + font-size: clamp(2rem, 5vw, 4.75rem); + line-height: 0.96; + letter-spacing: -0.06em; +} + +.hero-card p { + max-width: 43rem; + margin: 1.1rem 0 0; + color: rgba(255, 255, 255, 0.78); + font-size: clamp(1rem, 2vw, 1.2rem); + line-height: 1.7; } -.header { +.knowledge-sync-panel { display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; justify-content: space-between; + margin-bottom: 1rem; + padding: 1rem 1.1rem; + background: linear-gradient(135deg, rgba(16, 25, 54, 0.06), rgba(201, 204, 209, 0.18)); + border: 1px solid rgba(16, 25, 54, 0.08); + border-radius: 1.25rem; +} + +.knowledge-sync-panel p { + margin: 0; + color: var(--frauden-silver-dark); + line-height: 1.5; +} + +.knowledge-sync-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; align-items: center; -} \ No newline at end of file + justify-content: flex-end; +} + +.knowledge-sync-button.amplify-button { + min-height: 3rem; + padding-inline: 1.25rem; + font-weight: 700; + box-shadow: 0 14px 30px rgba(16, 25, 54, 0.18); +} + +.knowledge-sync-status { + max-width: 28rem; + padding: 0.7rem 0.85rem; + font-size: 0.92rem; + font-weight: 700; + border-radius: 0.85rem; +} + +.knowledge-sync-status--error { + color: #8a1f11; + background: #fff0ed; + border: 1px solid #f3b3a8; +} + +.knowledge-sync-status--info { + color: var(--frauden-navy); + background: #eef4ff; + border: 1px solid #c7d8f8; +} + +.knowledge-sync-status--success { + color: #115f35; + background: #edfff5; + border: 1px solid #a8e2c0; +} + +.storage-card { + padding: clamp(0.85rem, 2vw, 1.4rem); + background: var(--frauden-surface); + border: 1px solid var(--frauden-border); + border-radius: 2rem; + box-shadow: var(--frauden-shadow); + backdrop-filter: blur(18px); +} + +.storage-card :is(.amplify-button, button) { + border-radius: 0.85rem; +} + +.storage-card :is(.amplify-button--primary, [data-variation='primary']) { + background: linear-gradient(135deg, var(--frauden-navy), var(--frauden-blue)); + border-color: transparent; +} + +.storage-card :is(table, .amplify-table) { + overflow: hidden; + border-radius: 1rem; +} + +.storage-card :is(th, .amplify-table__th) { + color: var(--frauden-navy); + background: #f4f6f9; +} + +.storage-card [data-testid='LOCATIONS_VIEW'] :is(.amplify-storage-browser__table, .storage-browser__table) + :is(th, td):nth-child(2) { + display: none; +} + +.amplify-authenticator { + min-height: 100vh; + background: + radial-gradient(circle at 18% 10%, rgba(201, 204, 209, 0.5), transparent 22rem), + linear-gradient(135deg, #f8fafc, #eef1f6); +} + +.amplify-authenticator [data-amplify-router] { + overflow: hidden; + border: 1px solid var(--frauden-border); + border-radius: 1.5rem; + box-shadow: var(--frauden-shadow); +} + +@media (max-width: 760px) { + .app-shell { + padding: 0.85rem; + } + + .site-header, + .header-actions { + align-items: stretch; + } + + .site-header { + flex-direction: column; + } + + .brand-logo { + width: min(100%, 22rem); + } + + .header-actions, + .sign-out-button.amplify-button { + width: 100%; + } + + .user-card { + flex: 1; + text-align: left; + } +} + +.auth-brand { + display: flex; + justify-content: center; + padding: 2rem 2rem 0.5rem; +} + +.auth-brand-logo { + width: min(20rem, 82vw); + height: auto; +} diff --git a/src/App.tsx b/src/App.tsx index e2f45be..5873c0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,378 @@ import { + componentsDefault, createAmplifyAuthAdapter, createStorageBrowser, } from '@aws-amplify/ui-react-storage/browser'; +import '@aws-amplify/ui-react/styles.css'; import '@aws-amplify/ui-react-storage/styles.css'; import './App.css'; - +import { useState } from 'react'; import config from '../amplify_outputs.json'; import { Amplify } from 'aws-amplify'; -import { Authenticator, Button } from '@aws-amplify/ui-react'; +import { fetchAuthSession } from 'aws-amplify/auth'; +import { I18n } from 'aws-amplify/utils'; +import { Authenticator, Button, translations } from '@aws-amplify/ui-react'; +import fraudenLogo from './assets/frauden-logo.svg'; + Amplify.configure(config); +I18n.putVocabularies(translations); +I18n.setLanguage('es'); const { StorageBrowser } = createStorageBrowser({ config: createAmplifyAuthAdapter(), + components: componentsDefault, }); +type StorageBrowserDisplayText = NonNullable[0]['displayText']>; + +type KnowledgeSyncStatus = { + message: string; + type: 'error' | 'info' | 'success'; +} | null; + +const syncKnowledgeEndpoint = import.meta.env.VITE_SYNC_KNOWLEDGE_LAMBDA_URL?.trim(); +const syncKnowledgeTooltip = + 'Cuando agregues nuevos documentos o elimines debes sincronizar la base de conocimiento para cargar la nueva información'; + +const authenticatorComponents = { + Header() { + return ( +
+ Frauden +
+ ); + }, +}; + +function KnowledgeSyncButton() { + const [isSyncing, setIsSyncing] = useState(false); + const [syncStatus, setSyncStatus] = useState(null); + + const handleKnowledgeSync = async () => { + if (!syncKnowledgeEndpoint) { + setSyncStatus({ + type: 'error', + message: + 'Configura VITE_SYNC_KNOWLEDGE_LAMBDA_URL con la URL de la Lambda para sincronizar.', + }); + return; + } + + setIsSyncing(true); + setSyncStatus({ type: 'info', message: 'Solicitando sincronización de conocimiento...' }); + + try { + const session = await fetchAuthSession(); + const idToken = session.tokens?.idToken?.toString(); + + if (!idToken) { + throw new Error('No se encontró una sesión autenticada para invocar la Lambda.'); + } + + const response = await fetch(syncKnowledgeEndpoint, { + method: 'POST', + headers: { + Authorization: `Bearer ${idToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ action: 'syncKnowledgeBase' }), + }); + + if (!response.ok) { + const errorMessage = await response.text(); + throw new Error( + errorMessage || `La Lambda respondió con estado ${response.status}. Intenta nuevamente.` + ); + } + + setSyncStatus({ + type: 'success', + message: 'Sincronización de conocimiento iniciada correctamente.', + }); + } catch (error) { + setSyncStatus({ + type: 'error', + message: + error instanceof Error + ? error.message + : 'No se pudo iniciar la sincronización de conocimiento.', + }); + } finally { + setIsSyncing(false); + } + }; + + return ( +
+
+ Base de conocimiento +
+
+ + {syncStatus ? ( +
+ {syncStatus.message} +
+ ) : null} +
+
+ ); +} + +const storageBrowserDisplayText: StorageBrowserDisplayText = { + LocationsView: { + title: 'Inicio', + searchPlaceholder: 'Filtrar carpetas y archivos', + searchSubmitLabel: 'Buscar', + searchClearLabel: 'Limpiar búsqueda', + loadingIndicatorLabel: 'Cargando', + tableColumnBucketHeader: 'Bucket', + tableColumnFolderHeader: 'Carpeta', + tableColumnPermissionsHeader: 'Permisos', + tableColumnActionsHeader: 'Acciones', + getPermissionName: (permissions) => { + const canRead = permissions.includes('get') || permissions.includes('list'); + const canWrite = permissions.includes('write') || permissions.includes('delete'); + + if (canRead && canWrite) return 'Lectura/Escritura'; + if (canRead) return 'Lectura'; + if (canWrite) return 'Escritura'; + + return permissions.join('/'); + }, + getDownloadLabel: (fileName) => `Descargar ${fileName}`, + getListLocationsResultMessage: (data) => { + const { isLoading, items, hasExhaustedSearch, hasError = false, message } = data ?? {}; + + if (isLoading) return undefined; + if (hasError) { + return { + type: 'error', + content: message ?? 'Ocurrió un error al cargar las ubicaciones.', + }; + } + if (items?.length === 0 && !hasExhaustedSearch) { + return { type: 'info', content: 'No hay carpetas ni archivos.' }; + } + if (hasExhaustedSearch) { + return { + type: 'info', + content: 'Se muestran resultados de hasta los primeros 10,000 elementos.', + }; + } + + return undefined; + }, + }, + LocationDetailView: { + loadingIndicatorLabel: 'Cargando', + searchPlaceholder: 'Buscar en la carpeta actual', + searchSubmitLabel: 'Buscar', + searchClearLabel: 'Limpiar búsqueda', + searchSubfoldersToggleLabel: 'Incluir subcarpetas', + selectFileLabel: 'Seleccionar archivo', + selectAllFilesLabel: 'Seleccionar todos los archivos', + tableColumnLastModifiedHeader: 'Última modificación', + tableColumnNameHeader: 'Nombre', + tableColumnSizeHeader: 'Tamaño', + tableColumnTypeHeader: 'Tipo', + getTitle: ({ current, key }) => key || current?.bucket || '', + getActionListItemLabel: (key = '') => { + const labels: Record = { + Copy: 'Copiar', + Delete: 'Eliminar', + 'Create folder': 'Crear carpeta', + Upload: 'Subir', + }; + + return labels[key] ?? key; + }, + getListItemsResultMessage: (data) => { + const { items, hasExhaustedSearch, hasError = false, message, isLoading } = data ?? {}; + + if (isLoading) return undefined; + if (hasError) { + return { + type: 'error', + content: message ?? 'Ocurrió un error al cargar los elementos.', + }; + } + if (!items?.length && hasExhaustedSearch) { + return { + type: 'info', + content: 'No se encontraron resultados en los primeros 10,000 elementos.', + }; + } + if (!items?.length) return { type: 'info', content: 'No hay archivos.' }; + if (hasExhaustedSearch) { + return { + type: 'info', + content: 'Se muestran resultados de hasta los primeros 10,000 elementos.', + }; + } + + return undefined; + }, + }, + UploadView: { + title: 'Subir', + actionStartLabel: 'Subir', + actionCancelLabel: 'Cancelar', + actionExitLabel: 'Salir', + addFilesLabel: 'Agregar archivos', + addFolderLabel: 'Agregar carpeta', + overwriteToggleLabel: 'Sobrescribir archivos existentes', + statusDisplayCanceledLabel: 'Cancelado', + statusDisplayCompletedLabel: 'Completado', + statusDisplayFailedLabel: 'Fallido', + statusDisplayInProgressLabel: 'En progreso', + statusDisplayOverwritePreventedLabel: 'Sobrescritura evitada', + statusDisplayQueuedLabel: 'Sin iniciar', + statusDisplayTotalLabel: 'Total', + tableColumnFolderHeader: 'Carpeta', + tableColumnNameHeader: 'Nombre', + tableColumnTypeHeader: 'Tipo', + tableColumnSizeHeader: 'Tamaño', + tableColumnStatusHeader: 'Estado', + tableColumnProgressHeader: 'Progreso', + getActionCompleteMessage: () => ({ + content: 'Proceso de carga finalizado.', + type: 'success', + }), + }, + DeleteView: { + title: 'Eliminar', + actionStartLabel: 'Eliminar', + actionCancelLabel: 'Cancelar', + actionExitLabel: 'Salir', + statusDisplayCanceledLabel: 'Cancelado', + statusDisplayCompletedLabel: 'Completado', + statusDisplayFailedLabel: 'Fallido', + statusDisplayInProgressLabel: 'En progreso', + statusDisplayQueuedLabel: 'Sin iniciar', + statusDisplayTotalLabel: 'Total', + tableColumnFolderHeader: 'Carpeta', + tableColumnNameHeader: 'Nombre', + tableColumnTypeHeader: 'Tipo', + tableColumnSizeHeader: 'Tamaño', + tableColumnStatusHeader: 'Estado', + getActionCompleteMessage: () => ({ + content: 'Proceso de eliminación finalizado.', + type: 'success', + }), + }, + CopyView: { + title: 'Copiar', + actionStartLabel: 'Copiar', + actionCancelLabel: 'Cancelar', + actionExitLabel: 'Salir', + actionDestinationLabel: 'Destino de la copia', + loadingIndicatorLabel: 'Cargando' as 'Loading', + overwriteWarningMessage: + 'Los archivos copiados sobrescribirán archivos existentes en el destino seleccionado.', + searchPlaceholder: 'Buscar carpetas', + searchSubmitLabel: 'Buscar', + searchClearLabel: 'Limpiar búsqueda', + statusDisplayCanceledLabel: 'Cancelado', + statusDisplayCompletedLabel: 'Completado', + statusDisplayFailedLabel: 'Fallido', + statusDisplayInProgressLabel: 'En progreso', + statusDisplayQueuedLabel: 'Sin iniciar', + statusDisplayTotalLabel: 'Total', + tableColumnFolderHeader: 'Carpeta', + tableColumnNameHeader: 'Nombre', + tableColumnTypeHeader: 'Tipo', + tableColumnSizeHeader: 'Tamaño', + tableColumnStatusHeader: 'Estado', + tableColumnProgressHeader: 'Progreso', + getListFoldersResultsMessage: ({ folders, query, hasError, message, hasExhaustedSearch }) => { + if (!folders?.length) { + return { + content: query + ? `No se encontraron carpetas que coincidan con "${query}".` + : 'No se encontraron subcarpetas en la carpeta seleccionada.', + type: 'info', + }; + } + if ((message && query) || hasError) + return { content: 'Error al cargar carpetas.', type: 'error' }; + if (hasExhaustedSearch) { + return { + content: 'Se muestran resultados de hasta los primeros 10,000 elementos.', + type: 'info', + }; + } + + return undefined; + }, + getActionCompleteMessage: () => ({ + content: 'Proceso de copia finalizado.', + type: 'success', + }), + }, + CreateFolderView: { + title: 'Crear carpeta', + actionStartLabel: 'Crear carpeta', + actionCancelLabel: 'Cancelar', + actionExitLabel: 'Salir', + actionDestinationLabel: 'Destino', + folderNameLabel: 'Nombre de la carpeta', + folderNamePlaceholder: 'No puede contener "/" ni empezar o terminar con "."', + getValidationMessage: () => 'El nombre no puede contener "/" ni empezar o terminar con "."', + getActionCompleteMessage: () => ({ + content: 'Carpeta creada.', + type: 'success', + }), + }, +}; + function App() { return ( - + {({ signOut, user }) => ( - <> -
-

{`Hello ${user?.username}`}

- -
- - +
+
+ + Frauden + +
+
+ Sesión activa + {user?.username ?? 'Usuario'} +
+ +
+
+ +
+
+ Panel seguro +

Gestor documental de Frauden

+

+ carga archivos y organiza de forma segura y sencilla el archivo documental de + Frauden. +

+
+
+ +
+ + +
+
)}
); diff --git a/src/assets/frauden-logo.svg b/src/assets/frauden-logo.svg new file mode 100644 index 0000000..1a7d2c9 --- /dev/null +++ b/src/assets/frauden-logo.svg @@ -0,0 +1,24 @@ + + Frauden + Logo de Frauden con texto plateado y azul marino + + + + + + + + + + + + + + + + + FRAUD + EN + + Fraude empresarial y en los negocios +