diff --git a/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts b/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts index f4ce1e867..74a4d97c6 100644 --- a/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts +++ b/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts @@ -5,6 +5,8 @@ import { BaseType } from '../../../common/data-injection.tokens.js'; import { GetTableRowsDs } from '../application/data-structures/get-table-rows.ds.js'; import { IExportCSVFromTable } from './table-use-cases.interface.js'; import { Messages } from '../../../exceptions/text/messages.js'; +import { LogOperationTypeEnum, OperationResultStatusEnum } from '../../../enums/index.js'; +import { TableLogsService } from '../../table-logs/table-logs.service.js'; import { findFilteringFieldsUtil, parseFilteringFieldsFromBodyData } from '../utils/find-filtering-fields.util.js'; import { findOrderingFieldUtil } from '../utils/find-ordering-field.util.js'; import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; @@ -26,6 +28,7 @@ export class ExportCSVFromTableUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private tableLogsService: TableLogsService, ) { super(); } @@ -47,6 +50,8 @@ export class ExportCSVFromTableUseCase throw new NonAvailableInFreePlanException(Messages.CONNECTION_IS_FROZEN); } + let operationResult = OperationResultStatusEnum.unknown; + try { const dao = getDataAccessObject(connection); @@ -103,6 +108,8 @@ export class ExportCSVFromTableUseCase filteringFields, ); + operationResult = OperationResultStatusEnum.successfully; + //todo: rework as streams when node oracle driver will support it correctly //todo: agent return data as array of table rows, not as stream, because we cant //todo: transfer data as a stream from clint to server @@ -119,6 +126,7 @@ export class ExportCSVFromTableUseCase } return new StreamableFile(rowsStream.pipe(csv.stringify({ header: true }))); } catch (error) { + operationResult = OperationResultStatusEnum.unsuccessfully; if (error instanceof HttpException) { throw error; } @@ -136,6 +144,15 @@ export class ExportCSVFromTableUseCase }, HttpStatus.INTERNAL_SERVER_ERROR, ); + } finally { + const logRecord = { + table_name: tableName, + userId: userId, + connection: connection, + operationType: LogOperationTypeEnum.exportRows, + operationStatusResult: operationResult, + }; + await this.tableLogsService.crateAndSaveNewLogUtil(logRecord); } } } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index d7bf6121c..a775781e8 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -86,6 +86,20 @@ const routes: Routes = [ loadComponent: () => import('./components/company/company.component').then((m) => m.CompanyComponent), canActivate: [AuthGuard], }, + { + path: 'branding', + pathMatch: 'full', + loadComponent: () => import('./components/branding/branding.component').then((m) => m.BrandingComponent), + canActivate: [AuthGuard], + title: 'Branding | Rocketadmin', + }, + { + path: 'api-keys', + pathMatch: 'full', + loadComponent: () => import('./components/api-keys/api-keys.component').then((m) => m.ApiKeysComponent), + canActivate: [AuthGuard], + title: 'API Keys | Rocketadmin', + }, { path: 'secrets', pathMatch: 'full', @@ -94,10 +108,11 @@ const routes: Routes = [ title: 'Secrets | Rocketadmin', }, { - path: 'sso/:company-id', + path: 'saml', pathMatch: 'full', loadComponent: () => import('./components/sso/sso.component').then((m) => m.SsoComponent), canActivate: [AuthGuard], + title: 'SAML SSO | Rocketadmin', }, { path: 'change-password', diff --git a/frontend/src/app/components/api-keys/api-keys.component.css b/frontend/src/app/components/api-keys/api-keys.component.css new file mode 100644 index 000000000..bd83623bb --- /dev/null +++ b/frontend/src/app/components/api-keys/api-keys.component.css @@ -0,0 +1,200 @@ +.profile-layout { + display: flex; + height: calc(100vh - 64px); +} + +.profile-main { + flex: 1; + overflow-y: auto; +} + +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; +} + +.api-keys-page { + margin: var(--top-margin) auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 800px; +} + +@media (width <= 600px) { + .api-keys-page { + padding: 0 16px; + margin: 1.5em auto; + } +} + +.api-keys-description { + color: rgba(0, 0, 0, 0.64); + margin-top: 8px; + margin-bottom: 32px; +} + +@media (prefers-color-scheme: dark) { + .api-keys-description { + color: rgba(255, 255, 255, 0.7); + } +} + +.api-keys-content { + display: flex; + flex-direction: column; + gap: 24px; +} + +.api-key-form { + display: flex; + align-items: flex-start; + gap: 16px; +} + +@media (width <= 600px) { + .api-key-form { + flex-direction: column; + align-items: stretch; + } +} + +.api-key-form__input { + flex: 1; +} + +@media (width <= 600px) { + .api-key-form__input { + width: 100%; + } +} + +.api-key-form__generate-button { + margin-top: 4px; +} + +@media (width <= 600px) { + .api-key-form__generate-button { + margin-top: 0; + } +} + +.api-key-value { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 16px; + background-color: rgba(76, 175, 80, 0.08); + border-radius: 8px; + border: 1px solid rgba(76, 175, 80, 0.24); +} + +@media (prefers-color-scheme: dark) { + .api-key-value { + background-color: rgba(76, 175, 80, 0.12); + border-color: rgba(76, 175, 80, 0.32); + } +} + +.api-key-value__input { + flex: 1; +} + +.api-key-value__copy-button { + margin-top: 4px; +} + +.api-keys-list { + margin-top: 16px; +} + +.api-keys-list__heading { + margin-bottom: 16px; + font-weight: 500; +} + +.api-keys-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 48px 24px; + text-align: center; + background-color: rgba(0, 0, 0, 0.02); + border-radius: 8px; +} + +@media (prefers-color-scheme: dark) { + .api-keys-empty { + background-color: rgba(255, 255, 255, 0.05); + } +} + +.api-keys-empty__icon { + font-size: 48px; + width: 48px; + height: 48px; + color: rgba(0, 0, 0, 0.26); + margin-bottom: 16px; +} + +@media (prefers-color-scheme: dark) { + .api-keys-empty__icon { + color: rgba(255, 255, 255, 0.3); + } +} + +.api-keys-empty p { + margin: 0; + color: rgba(0, 0, 0, 0.54); +} + +@media (prefers-color-scheme: dark) { + .api-keys-empty p { + color: rgba(255, 255, 255, 0.54); + } +} + +.api-keys-items { + padding: 0; +} + +.api-key-list-item { + border-radius: 8px; + margin-bottom: 8px; +} + +.api-key-list-item:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +@media (prefers-color-scheme: dark) { + .api-key-list-item:hover { + background-color: rgba(255, 255, 255, 0.08); + } +} + +.api-key-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.api-key-item__info { + display: flex; + align-items: center; + gap: 12px; +} + +.api-key-item__icon { + color: rgba(0, 0, 0, 0.54); +} + +@media (prefers-color-scheme: dark) { + .api-key-item__icon { + color: rgba(255, 255, 255, 0.54); + } +} + +.api-key-item__title { + font-weight: 500; +} diff --git a/frontend/src/app/components/api-keys/api-keys.component.html b/frontend/src/app/components/api-keys/api-keys.component.html new file mode 100644 index 000000000..e72126991 --- /dev/null +++ b/frontend/src/app/components/api-keys/api-keys.component.html @@ -0,0 +1,81 @@ +
+ Generate and manage API keys to access Rocketadmin programmatically. +
+ +You don't have any API keys yet.
+