From b5e99b01160f21bacebab9dfe999aadc0935e8fd Mon Sep 17 00:00:00 2001 From: liosha84 <138026690+liosha84@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:50:16 +0300 Subject: [PATCH 1/3] remake init data json dump --- backups/flyway_schema_history.ndjson | 10 + backups/roles.ndjson | 3 + backups/tutorials.ndjson | 4 + backups/user_roles.ndjson | 5 + backups/users.ndjson | 4 + .../breadcrumbs.component.html | 2 +- .../breadcrumbs.component.scss | 2 + .../breadcrumbs.component.ts | 8 +- .../inner-layout.component.html | 17 + .../inner-layout.component.scss | 36 ++ .../inner-layout.component.spec.ts} | 12 +- .../inner-layout.component.ts | 39 ++ .../vertical-nav.component.html | 25 + .../vertical-nav.component.scss | 68 +++ .../vertical-nav.component.spec.ts | 23 + .../vertical-nav.component.ts | 111 +++++ jambotron-ui/src/app/models/table.spec.ts | 7 + jambotron-ui/src/app/models/table.ts | 4 + .../admin-module/admin-module.service.ts | 28 +- .../admin.component/admin.component.html | 25 +- .../admin.component/admin.component.scss | 40 +- .../admin.component/admin.component.ts | 72 +-- .../app/modules/admin-module/admin.routing.ts | 19 +- .../json-dump.component.html | 59 +++ .../json-dump.component.scss | 27 ++ .../json-dump.component.spec.ts | 23 + .../json-dump.component.ts | 135 ++++++ .../data-base.component.html | 7 + .../data-base.component.scss | 0 .../data-base.component.spec.ts | 23 + .../data-base.component.ts | 25 + .../admin-module/json-dump.service.spec.ts | 16 + .../modules/admin-module/json-dump.service.ts | 449 +++++++++++++++++ .../settings.component.html | 2 + .../settings.component/settings.component.ts | 4 +- .../side-bar-admin.component.html | 35 -- .../side-bar-admin.component.scss | 14 - .../side-bar-admin.component.ts | 20 - .../users.component/users.component.ts | 12 +- .../main.component/main.component.html | 105 ++-- .../main.component/main.component.scss | 1 + .../app/modules/main-module/main.routing.ts | 5 +- .../moderator.component.html | 7 +- .../moderator.component.scss | 13 - .../moderator.component.ts | 9 +- .../moderator-module/moderator.routing.ts | 12 +- .../user.component/user.component.html | 10 +- .../user.component/user.component.ts | 35 +- .../app/modules/user-module/user.routing.ts | 18 +- .../src/app/services/spinner.service.ts | 4 +- jambotron-ui/src/app/services/user.service.ts | 4 +- jambotron-ui/src/index.html | 4 +- jambotron-ui/src/index_dev.html | 2 + jambotron-ui/src/styles.scss | 6 +- .../jambotron/DTOs/RoleDto.java | 48 ++ .../jambotron/DTOs/TutorialDto.java | 145 ++++++ .../jambotron/DTOs/UserDto.java | 102 ++++ .../jambotron/JambotronApplication.java | 13 + .../controllers/JsonDumpController.java | 347 +++++++++++++ .../fileUpload/FilesStorageServiceImpl.java | 1 + .../initData/InitDataConfiguration.java | 21 + .../jambotron/initData/InitDataService.java | 148 ++++++ .../jambotron/jsonDump/JsonDumpService.java | 456 ++++++++++++++++++ .../JsonDumpServiceConfiguration.java | 34 ++ .../jambotronGroup/jambotron/model/Role.java | 6 +- .../jambotron/model/Tutorial.java | 14 +- .../jambotronGroup/jambotron/model/User.java | 2 +- .../jambotron/repository/RoleRepository.java | 3 + .../jambotron/security/WebSecurityConfig.java | 7 +- src/main/resources/application-dev.properties | 12 + .../db/migration/V10__refresh_token.sql | 12 - src/main/resources/db/migration/V1__Init.sql | 84 +++- src/main/resources/db/migration/V2__init.sql | 14 - src/main/resources/db/migration/V3__init.sql | 25 - .../resources/db/migration/V4__Init_data.sql | 20 - src/main/resources/db/migration/V5__Fixes.sql | 2 - .../db/migration/V6__fix_tutorial_id.sql | 6 - .../V7__add_column_userid_to_tutorials.sql | 27 -- .../db/migration/V8__tutorial_remake.sql | 19 - .../db/migration/V9__title_image.sql | 9 - src/main/resources/import/roles.json | 10 + src/main/resources/import/users.json | 34 ++ 82 files changed, 2814 insertions(+), 457 deletions(-) create mode 100644 backups/flyway_schema_history.ndjson create mode 100644 backups/roles.ndjson create mode 100644 backups/tutorials.ndjson create mode 100644 backups/user_roles.ndjson create mode 100644 backups/users.ndjson create mode 100644 jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.html create mode 100644 jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.scss rename jambotron-ui/src/app/{modules/admin-module/side-bar-admin.component/side-bar-admin.component.spec.ts => components/inner-layout.component/inner-layout.component.spec.ts} (51%) create mode 100644 jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.ts create mode 100644 jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.html create mode 100644 jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.scss create mode 100644 jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.spec.ts create mode 100644 jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.ts create mode 100644 jambotron-ui/src/app/models/table.spec.ts create mode 100644 jambotron-ui/src/app/models/table.ts create mode 100644 jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.html create mode 100644 jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.scss create mode 100644 jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.spec.ts create mode 100644 jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.ts create mode 100644 jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.html create mode 100644 jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.scss create mode 100644 jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.spec.ts create mode 100644 jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.ts create mode 100644 jambotron-ui/src/app/modules/admin-module/json-dump.service.spec.ts create mode 100644 jambotron-ui/src/app/modules/admin-module/json-dump.service.ts delete mode 100644 jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.html delete mode 100644 jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.scss delete mode 100644 jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.ts create mode 100644 src/main/java/com/jambotronGroup/jambotron/DTOs/RoleDto.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/DTOs/TutorialDto.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/DTOs/UserDto.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/initData/InitDataConfiguration.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpServiceConfiguration.java delete mode 100644 src/main/resources/db/migration/V10__refresh_token.sql delete mode 100644 src/main/resources/db/migration/V2__init.sql delete mode 100644 src/main/resources/db/migration/V3__init.sql delete mode 100644 src/main/resources/db/migration/V4__Init_data.sql delete mode 100644 src/main/resources/db/migration/V5__Fixes.sql delete mode 100644 src/main/resources/db/migration/V6__fix_tutorial_id.sql delete mode 100644 src/main/resources/db/migration/V7__add_column_userid_to_tutorials.sql delete mode 100644 src/main/resources/db/migration/V8__tutorial_remake.sql delete mode 100644 src/main/resources/db/migration/V9__title_image.sql create mode 100644 src/main/resources/import/roles.json create mode 100644 src/main/resources/import/users.json diff --git a/backups/flyway_schema_history.ndjson b/backups/flyway_schema_history.ndjson new file mode 100644 index 0000000..5caf3c8 --- /dev/null +++ b/backups/flyway_schema_history.ndjson @@ -0,0 +1,10 @@ +{"type": "SQL", "script": "V1__Init.sql", "success": true, "version": "1", "checksum": 1015436223, "description": "Init", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.278963", "execution_time": 23, "installed_rank": 1} +{"type": "SQL", "script": "V2__init.sql", "success": true, "version": "2", "checksum": 1439164107, "description": "init", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.364728", "execution_time": 21, "installed_rank": 2} +{"type": "SQL", "script": "V3__init.sql", "success": true, "version": "3", "checksum": 1126077872, "description": "init", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.430577", "execution_time": 16, "installed_rank": 3} +{"type": "SQL", "script": "V4__Init_data.sql", "success": true, "version": "4", "checksum": 1082838296, "description": "Init data", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.480949", "execution_time": 22, "installed_rank": 4} +{"type": "SQL", "script": "V5__Fixes.sql", "success": true, "version": "5", "checksum": 1011882594, "description": "Fixes", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.54043", "execution_time": 10, "installed_rank": 5} +{"type": "SQL", "script": "V6__fix_tutorial_id.sql", "success": true, "version": "6", "checksum": 2025331495, "description": "fix tutorial id", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.577613", "execution_time": 16, "installed_rank": 6} +{"type": "SQL", "script": "V7__add_column_userid_to_tutorials.sql", "success": true, "version": "7", "checksum": -1581139665, "description": "add column userid to tutorials", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.624337", "execution_time": 11, "installed_rank": 7} +{"type": "SQL", "script": "V8__tutorial_remake.sql", "success": true, "version": "8", "checksum": 2000720842, "description": "tutorial remake", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.657116", "execution_time": 11, "installed_rank": 8} +{"type": "SQL", "script": "V9__title_image.sql", "success": true, "version": "9", "checksum": 499316035, "description": "title image", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.689202", "execution_time": 13, "installed_rank": 9} +{"type": "SQL", "script": "V10__refresh_token.sql", "success": true, "version": "10", "checksum": -284916251, "description": "refresh token", "installed_by": "admin", "installed_on": "2025-08-11T18:05:52.724264", "execution_time": 9, "installed_rank": 10} diff --git a/backups/roles.ndjson b/backups/roles.ndjson new file mode 100644 index 0000000..02dd18d --- /dev/null +++ b/backups/roles.ndjson @@ -0,0 +1,3 @@ +{"id": 1, "name": "ROLE_USER"} +{"id": 2, "name": "ROLE_MODERATOR"} +{"id": 3, "name": "ROLE_ADMIN"} diff --git a/backups/tutorials.ndjson b/backups/tutorials.ndjson new file mode 100644 index 0000000..5a6d213 --- /dev/null +++ b/backups/tutorials.ndjson @@ -0,0 +1,4 @@ +{"id": 1, "body": "## Markdown __rulez__!\n---\n\n### Syntax highlight\n```typescript\nconst language = 'typescript';\n```\n\n### Lists\n1. Ordered list\n2. Another bullet point\n - Unordered list\n - Another unordered bullet\n\n### Blockquote\n> Blockquote to the max", "title": "Test 1", "userid": 1, "created": "2025-08-11T18:07:54.037686+03:00", "modified": "2025-08-11T18:07:54.037686+03:00", "published": true, "titleimage": "http://localhost:8082/files/Tutorial_user-image-1-07a460cf07aa41d1b7f2f9d2b9789c0b.png", "description": null, "tobepublished": true} +{"id": 3, "body": "## Markdown __rulez__!\n---\n\n### Syntax highlight\n```typescript\nconst language = 'typescript';\n```\n\n### Lists\n1. Ordered list\n2. Another bullet point\n - Unordered list\n - Another unordered bullet\n\n### Blockquote\n> Blockquote to the max", "title": "Test 2", "userid": 1, "created": "2025-08-11T18:08:33.839282+03:00", "modified": "2025-08-11T18:08:33.839403+03:00", "published": true, "titleimage": "http://localhost:8082/files/Tutorial_user-image-1-c4281de414ae44ba8e39e03f06c8f803.png", "description": null, "tobepublished": true} +{"id": 4, "body": "## Markdown __rulez__!\n---\n\n### Syntax highlight\n```typescript\nconst language = 'typescript';\n```\n\n### Lists\n1. Ordered list\n2. Another bullet point\n - Unordered list\n - Another unordered bullet\n\n### Blockquote\n> Blockquote to the max", "title": "Test 3", "userid": 1, "created": "2025-08-11T18:08:59.02719+03:00", "modified": "2025-08-11T18:08:59.02719+03:00", "published": true, "titleimage": "http://localhost:8082/files/Tutorial_user-image-1-0a55158e36d34fe8842ceac520bd6032.png", "description": null, "tobepublished": true} +{"id": 6, "body": "# How to run the application\n\nTo run the application, follow these steps:\n\n1. **Build the Project**:\n - Ensure you have Java 24, Gradle, and Node.js installed.\n - Run the following command to build the project:\n ```bash\n ./gradlew build\n ```\n\n 2. **Prepare Frontend Resources**:\n - Build the Angular frontend by navigating to the `jambotron-ui` directory and running\n ```bash\n cd jambotron-ui\n ```\n - next:\n ```bash\n npm install\n ```\n - next:\n ```bash\n npm run build\n ```\n - This will generate the frontend files in the `jambotron-ui/dist/jambotron-ui/browser` directory.\n\n\n3. **Copy Frontend Resources**:\n - Use the Gradle task to copy the frontend resources to the `jumbotron/src/main/resources/public` directory:\n ```bash\n cd ..\n ```\n - next:\n ```bash\n ./gradlew copyResources\n ```\n then\n4. **Create postgres server and database**:\n - Create a PostgreSQL database \n - named `jambotronDB` with the \n - user `admin` and \n - password `postgrespw`.\n \n - You can use a PostgreSQL client or run the following command in your terminal:\n ```sql\n CREATE DATABASE jambotronDB;\n CREATE USER admin WITH PASSWORD 'postgrespw';\n GRANT ALL PRIVILEGES ON DATABASE jambotronDB TO admin;\n ```\n - If you are using Docker, you can run PostgreSQL using the following command:\n - port 5433 is used.\n ```bash\n docker run --name postgresDB -p 5433:5432 -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=postgrespw -e POSTGRES_DB=jambotronDB -d postgres\n ```\n - if you are wont to use pgadmin\n ```bash\n docker run --name pgadmin -e \"PGADMIN_DEFAULT_EMAIL=name@example.com\" -e \"PGADMIN_DEFAULT_PASSWORD=admin\" -p 5050:80 -d dpage/pgadmin4\n ```\n then\n - create a network for the containers to communicate:\n ```bash\n docker network create --driver bridge pgnetwork\n ```\n - Show list of networks:\n ```bash\n docker network ls\n ```\n - Connect pgadmin to the network:\n ```bash\n docker network connect pgnetwork pgadmin\n ```\n - Connect postgresDB to the network (postgresDB must by running):\n ```bash\n docker network connect pgnetwork postgresDB\n ```\n - Inspect the network to ensure both containers are connected:\n ```bash\n docker network inspect pgnetwork\n ```\n \n\n\n5. **Run the Application**:\n - Start the Spring Boot application:\n ```bash\n ./gradlew bootRun\n ```\n\n6. **Access the Application**:\n - Open your browser and navigate to `http://localhost:8080`.\n\nMake sure all dependencies and configurations are correctly set up before running these steps.\n\n\n\n\n# How to run the application in Docker\n\nTo run the application in Docker, follow these steps:\n\n- build jar file\n```bash\n./gradlew bootJar\n ```\n- Run docker compose to start the application and PostgreSQL:\n```bash\ndocker-compose -f src/Docker/docker-compose.yml up -d \n```\n- For connection from pgadmin use \nhostename: **host.docker.internal**\n\n# How to run the application in Docker with HTTPS support\n\nTo run the application in Docker with HTTPS support, follow these steps:\n\n1. **Cerbot**\n \n - Run this command below to get certificate and store it to volume certs.\n\n - Expanded(readable) command example:\n\n docker run -d \\\n --name certbot \\\n -v certs:/etc/letsencrypt \\\n -v certs-data:/var/lib/letsencrypt \\\n -p 80:80 \\\n -p 443:443 \\\n certbot/certbot \\\n certonly --standalone --preferred-challenges http --email youremail@gmail.com -d yourdomain.com --agree-tos\n\n - Command to run certbot with specific domain and email:\n ```bash\n docker run -d --name certbot -v certs:/etc/letsencrypt -v certs-data:/var/lib/letsencrypt -p 8081:80 -p 8443:443 certbot/certbot certonly --standalone --preferred-challenges http --email liosha84@gmail.com -d jambotron.run.place --agree-tos\n ```\n - Tip: You can generate certificate to local machine used next command:\n ```bash\n docker run -it --rm -p 8081:80 --name certbot -v \"C:\\certbot\\etc\\letsencrypt:/etc/letsencrypt\" -v \"C:\\certbot\\var\\lib\\letsencrypt:/var/lib/letsencrypt\" certbot/certbot certonly --standalone -d jambotron.run.place \n ```\n certificate files will be stored in `C:\\certbot\\etc\\letsencrypt` and `C:\\certbot\\var\\lib\\letsencrypt` directories.\n\n - If you have same trouble before running certbot, run this project\n `Helper_projects/spring-boot-https-main` insted of jambotron project.\n \n\n - TODO: Add cron job to renew certificate automatically.\n\n### Reference Documentation\nFor further reference, please consider the following sections:\n\n* [Official Gradle documentation](https://docs.gradle.org)\n* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.0/gradle-plugin)\n* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.0/gradle-plugin/packaging-oci-image.html)\n\n### Additional Links\nThese additional references should also help you:\n\n* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle)\n\n\n\nFor docker-compose db connect with pgAdmin used\nhost.docker.internal", "title": "Test 4", "userid": 1, "created": "2025-08-11T18:10:12.111675+03:00", "modified": "2025-08-11T18:10:12.111675+03:00", "published": true, "titleimage": "http://localhost:8082/files/Tutorial_9a17dfe0-9d45-4e4f-bddc-d514b5774529.png", "description": null, "tobepublished": true} diff --git a/backups/user_roles.ndjson b/backups/user_roles.ndjson new file mode 100644 index 0000000..13e4b82 --- /dev/null +++ b/backups/user_roles.ndjson @@ -0,0 +1,5 @@ +{"role_id": 1, "user_id": 3} +{"role_id": 2, "user_id": 2} +{"role_id": 3, "user_id": 1} +{"role_id": 1, "user_id": 1} +{"role_id": 2, "user_id": 1} diff --git a/backups/users.ndjson b/backups/users.ndjson new file mode 100644 index 0000000..876ad35 --- /dev/null +++ b/backups/users.ndjson @@ -0,0 +1,4 @@ +{"id": 1, "email": "liosha84@gmail.com", "password": "$2a$10$qyoKXYSukha6XCjorTzoweF4Os1pwmwyzbaSsb3RCVB0LK6WLQKPC", "username": "Admin"} +{"id": 2, "email": "Moderator@gmail.com", "password": "$2a$10$zLo5th8Xbfq.MM7y/dCRQu3Ud7HHyAwm.7.yS08ytJtkHMKrbOJlu", "username": "Moderator"} +{"id": 3, "email": "user@gmail.com", "password": "$2a$10$2EcwYffteBF3GhVaf5qPB.I7XiHepDEauU5D4fx9fpBXMTZI/QnnC", "username": "User"} +{"id": 4, "email": "user1@gmail.com", "password": "$2a$10$/lditoDdNNkwjr.jL0KUveGGSzF1ZA6Nhj4lC/w8CkuOGWTj8pOY6", "username": "user1"} diff --git a/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.html b/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.html index 6753bf6..7f153ac 100644 --- a/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.html +++ b/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.html @@ -1,4 +1,4 @@ - + @let breadcrumbs = breadcrumbs$ | async; diff --git a/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.scss b/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.scss index 6eb5043..48deb9b 100644 --- a/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.scss +++ b/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.scss @@ -3,6 +3,7 @@ align-items: center; gap: 4px; margin-bottom: inherit; + padding-left: 25px; } .breadcrumb .sep { font-size: 16px; @@ -12,4 +13,5 @@ min-width: auto; padding: 0 4px; text-transform: none; + color:cyan; } diff --git a/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.ts b/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.ts index 5b5c840..ed631c3 100644 --- a/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.ts +++ b/jambotron-ui/src/app/components/breadcrumbs.component/breadcrumbs.component.ts @@ -1,7 +1,7 @@ import {Component, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from '@angular/core'; import {ActivatedRoute, NavigationEnd, Router, RouterLink} from '@angular/router'; import {MatButton} from '@angular/material/button'; -import {AsyncPipe, NgForOf, NgIf} from '@angular/common'; +import {AsyncPipe} from '@angular/common'; import {MatIcon} from '@angular/material/icon'; import {Observable, startWith} from 'rxjs'; import {filter, map} from 'rxjs/operators'; @@ -15,9 +15,9 @@ type Breadcrumb = { label: string; url: string }; RouterLink, MatIcon, MatButton, - NgIf, + AsyncPipe, - NgForOf, + MatDivider ], templateUrl: './breadcrumbs.component.html', @@ -47,7 +47,7 @@ export class BreadcrumbsComponent { const routeURL = child.snapshot.url.map(s => s.path).join('/'); if (routeURL) url += `/${routeURL}`; - const label = child.snapshot.data['breadcrumb'] as string | undefined; + const label = child.snapshot.data['title'] as string | undefined; if (label) crumbs.push({ label, url }); return this.build(child, url, crumbs); diff --git a/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.html b/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.html new file mode 100644 index 0000000..6719ef8 --- /dev/null +++ b/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.scss b/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.scss new file mode 100644 index 0000000..3680de9 --- /dev/null +++ b/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.scss @@ -0,0 +1,36 @@ + +.app-container{ + top: -12px; + padding-left: 5px; + padding-right: 5px; + margin-left: 240px; + padding-top: 1px; + position: relative; + &.collapsed{ + margin-left: 64px; + } +} +.app-breadcrumb{ + position: fixed; + left: 240px; + width: 100%; + background-color: rgba(153, 153, 153, 0.16); + backdrop-filter: blur(8px); + z-index: 100; + &.collapsed{ + left: 64px; + } +} + +.page-component{ + height: 100%; + margin-top: 65px; + margin-bottom: 50px; + margin-left: 10px; + margin-right: 10px; + background: aliceblue; + position: relative; + padding: 20px; +} + + diff --git a/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.spec.ts b/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.spec.ts similarity index 51% rename from jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.spec.ts rename to jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.spec.ts index b6f9f21..821bedc 100644 --- a/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.spec.ts +++ b/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SideBarAdminComponent } from './side-bar-admin.component'; +import { InnerLayoutComponent } from './inner-layout.component'; -describe('SideBarAdminComponent', () => { - let component: SideBarAdminComponent; - let fixture: ComponentFixture; +describe('InnerLayoutComponent', () => { + let component: InnerLayoutComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SideBarAdminComponent] + imports: [InnerLayoutComponent] }) .compileComponents(); - fixture = TestBed.createComponent(SideBarAdminComponent); + fixture = TestBed.createComponent(InnerLayoutComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.ts b/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.ts new file mode 100644 index 0000000..6f7e417 --- /dev/null +++ b/jambotron-ui/src/app/components/inner-layout.component/inner-layout.component.ts @@ -0,0 +1,39 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {BreadcrumbsComponent} from "../breadcrumbs.component/breadcrumbs.component"; +import {Route, RouterOutlet} from "@angular/router"; +import {VerticalNavComponent} from "../vertical-nav.component/vertical-nav.component"; +import {ADMIN_ROUTES} from '../../modules/admin-module/admin.routing'; + +@Component({ + selector: 'app-inner-layout', + imports: [ + BreadcrumbsComponent, + RouterOutlet, + VerticalNavComponent + ], + templateUrl: './inner-layout.component.html', + styleUrl: './inner-layout.component.scss' +}) +export class InnerLayoutComponent implements OnInit{ + + + //adminRoutes = (ADMIN_ROUTES[0]?.children ?? []).filter(r => r.path && r.data?.['nav']); + //adminRoutes = (ADMIN_ROUTES[0]?.children ?? []).filter(r => r.path && r.data?.['nav']); + routes:Route[] = []; + + @Input() moduleRoutes:Route[] = []; + + // If your admin module is mounted at '/admin' in the app routes, keep this as '/admin'. + // If it's at root, set this to ''. + @Input() basePath = ''; + + isCollapsed = false; + + ngOnInit(): void { + this.routes = (this.moduleRoutes[0]?.children ?? []).filter(r => r.path && r.data?.['nav']); + } + + navCollapsedChanged($event: boolean) { + this.isCollapsed = $event; + } +} diff --git a/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.html b/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.html new file mode 100644 index 0000000..903e8a4 --- /dev/null +++ b/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.html @@ -0,0 +1,25 @@ + + + + {{ opened() ? 'chevron_left' : 'chevron_right' }} + + + + + @for (item of (items || navItems()); track item) { + + {{ item.icon }} + {{ item.title }} + + } + + + diff --git a/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.scss b/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.scss new file mode 100644 index 0000000..31242e5 --- /dev/null +++ b/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.scss @@ -0,0 +1,68 @@ +.nav-container { + width: 240px; + height: 100%; + display: flex; + flex-direction: column; + border-right: 1px solid var(--mat-sys-outline-variant, rgba(0,0,0,0.12)); + background-color: rgba(153,153,153,0.16); + backdrop-filter: blur(8px); + position: fixed; + z-index: 100; + &.collapsed { + width: 64px; + + .label { + display: none; + } + } +} + +.nav-header { + display: flex; + justify-content: flex-end; + +// border-bottom: 1px solid var(--mat-sys-outline-variant, rgba(0,0,0,0.12)); + padding-bottom: 1px; +} + + +a.mat-mdc-list-item { + height: 44px; + border-radius: 8px; + margin: 4px 4px; + color: cyan; + &.active { + background: color-mix(in oklab, var(--mat-sys-primary, #3f51b5) 16%, transparent); + } +} + +.nav-list{ + //background-color: #f5f7fb; /* your color */ + background-color: rgba(153,153,153,0.16); + backdrop-filter: blur(8px); +} +.nav-list .mdc-list-item { + background-color: rgba(153,153,153,0.16); + backdrop-filter: blur(8px); +} +.nav-list .mdc-list-item:hover, +.nav-list .mdc-list-item:focus { + background-color: rgba(153,153,153,0.16); + backdrop-filter: blur(8px); +} +.nav-list .mdc-list-item--selected { + background-color: rgba(153, 153, 153, 0.50); + backdrop-filter: blur(8px); +} + +/* Primary and secondary text */ +.nav-list .mdc-list-item__primary-text, +.nav-list .mdc-list-item__secondary-text { + color: cyan; +} + +/* Optional: icons inside list items */ +.nav-list .mat-icon { + color: cyan; +} + diff --git a/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.spec.ts b/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.spec.ts new file mode 100644 index 0000000..55aa881 --- /dev/null +++ b/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VerticalNavComponent } from './vertical-nav.component'; + +describe('VerticalNavComponent', () => { + let component: VerticalNavComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VerticalNavComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(VerticalNavComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.ts b/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.ts new file mode 100644 index 0000000..49cb2ca --- /dev/null +++ b/jambotron-ui/src/app/components/vertical-nav.component/vertical-nav.component.ts @@ -0,0 +1,111 @@ +import { + Component, + computed, + CUSTOM_ELEMENTS_SCHEMA, + EventEmitter, + inject, + Input, + OnInit, + Output, + signal +} from '@angular/core'; +import {Route, Router, RouterLink, RouterLinkActive} from '@angular/router'; +import {MatListItem, MatListItemIcon, MatListItemTitle, MatNavList} from '@angular/material/list'; +import {NgForOf} from '@angular/common'; +import {MatIconButton} from '@angular/material/button'; +import {MatTooltip} from '@angular/material/tooltip'; +import {MatIcon} from '@angular/material/icon'; +import {adminRouting} from '../../modules/admin-module/admin.routing'; + +export interface NavItem { + path: string; // absolute path like /dashboard + title: string; // label to show + icon?: string; // material icon name + exact?: boolean; // whether to match exactly +} + + +@Component({ + selector: 'app-vertical-nav', + imports: [ + MatTooltip, + MatIcon, + MatListItemIcon, + MatListItemTitle, + RouterLinkActive, + RouterLink, + MatListItem, + MatNavList, + NgForOf, + MatIconButton, + + ], + templateUrl: './vertical-nav.component.html', + styleUrl: './vertical-nav.component.scss', + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class VerticalNavComponent implements OnInit{ + private router = inject(Router); + + // Optional: if you want to pass items directly instead of deriving from routes + @Input() items: NavItem[] | null = null; + + // Optional: pass a specific Route[] (e.g., ADMIN_ROUTES). If not provided, uses router.config. + @Input() routes: Route[] = []; + + // Optional: prepend a base path (e.g., '/admin') when using feature routes. + @Input() basePath = ''; + + // Sidebar state (expanded/collapsed) + opened = signal(true); + + navItems = signal([]); + readonly hasItems = computed(() => (this.items?.length ?? this.navItems().length) > 0); + + @Output() collapsedChange = new EventEmitter(); + + + constructor() { + + } + + ngOnInit(): void { + + this.navItems.set(this.deriveFromRoutes(this.routes, this.basePath)); + } + + toggle(): void { + this.opened.update(v => !v); + this.collapsedChange.emit(!this.opened()); + } + + private deriveFromRoutes(routes: Route[], base: string): NavItem[] { + // Flatten one level; you can make this recursive if needed + const items: NavItem[] = []; + for (const r of routes) { + if (!r || r.path === '**') continue; + + const data = (r as any).data || {}; + const seg = r.path || ''; + const nextBase = [base, seg].filter(Boolean).join('/'); + + /* // Recurse into children if present (common in feature modules) + if (r.children && r.children.length) { + items.push(...this.deriveFromRoutes(r.children, nextBase)); + } +*/ + if (data.nav === false) continue; + + const title = data.title as string | undefined; + if (!title) continue; + + const icon = data.icon as string | undefined; + const full = '/' + nextBase.replace(/\/+/g, '/'); + items.push({ path: full, title, icon, exact: true }); + } + + return items; + + } + +} diff --git a/jambotron-ui/src/app/models/table.spec.ts b/jambotron-ui/src/app/models/table.spec.ts new file mode 100644 index 0000000..634a5a3 --- /dev/null +++ b/jambotron-ui/src/app/models/table.spec.ts @@ -0,0 +1,7 @@ +import { Table } from './table'; + +describe('Table', () => { + it('should create an instance', () => { + expect(new Table()).toBeTruthy(); + }); +}); diff --git a/jambotron-ui/src/app/models/table.ts b/jambotron-ui/src/app/models/table.ts new file mode 100644 index 0000000..1c25c8b --- /dev/null +++ b/jambotron-ui/src/app/models/table.ts @@ -0,0 +1,4 @@ +export class Table { + schema?: string; + table?: string; +} diff --git a/jambotron-ui/src/app/modules/admin-module/admin-module.service.ts b/jambotron-ui/src/app/modules/admin-module/admin-module.service.ts index 70c8954..97b89fc 100644 --- a/jambotron-ui/src/app/modules/admin-module/admin-module.service.ts +++ b/jambotron-ui/src/app/modules/admin-module/admin-module.service.ts @@ -1,8 +1,10 @@ import { Injectable } from '@angular/core'; import {Observable} from 'rxjs'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; import {User} from '../../models/user.model'; import {GlobalConstants} from '../../global-constants'; +import {Table} from '../../models/table'; +import {Role} from '../../models/role'; @Injectable({ providedIn: 'root' @@ -13,8 +15,26 @@ export class AdminModuleService { constructor(private http: HttpClient) { } - - updateUserRoles(fileName: any,data: User): Observable { - return this.http.put(`${this.baseUrl}/users/${fileName}`, data); + getUsers(): Observable { + return this.http.get(`${this.baseUrl}/users`); } + + getAllRoles(): Observable { + return this.http.get(`${this.baseUrl}/roles`); + } + + updateUserRoles(id: any,data: User): Observable { + return this.http.put(`${this.baseUrl}/users/${id}`, data); + } +/* + + getUserTables(schema?: string): Observable { + let params = new HttpParams(); + if (schema) { + params = params.set('schema', schema); + } + return this.http.get(`${this.baseUrl}/admin/json-dump/get-tables`, { params }); + } +*/ + } diff --git a/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.html b/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.html index 6d634bf..a14df3e 100644 --- a/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.html +++ b/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.html @@ -1,11 +1,16 @@ - - - - - + + + + + + + + + + + - - - + diff --git a/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.scss b/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.scss index 9a8a04f..3680de9 100644 --- a/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.scss +++ b/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.scss @@ -1,16 +1,36 @@ -//--------------- -.pc-sidebar{ - top: 65px; - overflow-y: auto; - background-color: rgba(153, 153, 153, 0.16); - backdrop-filter: blur(8px); -} -.pc-container{ - top: 0px; +.app-container{ + top: -12px; padding-left: 5px; padding-right: 5px; + margin-left: 240px; + padding-top: 1px; + position: relative; + &.collapsed{ + margin-left: 64px; + } +} +.app-breadcrumb{ + position: fixed; + left: 240px; + width: 100%; + background-color: rgba(153, 153, 153, 0.16); + backdrop-filter: blur(8px); + z-index: 100; + &.collapsed{ + left: 64px; + } +} + +.page-component{ + height: 100%; + margin-top: 65px; + margin-bottom: 50px; + margin-left: 10px; + margin-right: 10px; + background: aliceblue; + position: relative; + padding: 20px; } - diff --git a/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.ts b/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.ts index afcddb0..514c3dd 100644 --- a/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.ts +++ b/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.ts @@ -1,69 +1,31 @@ -import {Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit} from '@angular/core'; -import {Router, RouterOutlet} from "@angular/router"; -import {DOCUMENT} from '@angular/common'; -import {SideBarAdminComponent} from '../side-bar-admin.component/side-bar-admin.component'; +import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {RouterOutlet} from "@angular/router"; import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component'; +import {VerticalNavComponent} from '../../../components/vertical-nav.component/vertical-nav.component'; +import {ADMIN_ROUTES} from '../admin.routing'; @Component({ selector: 'app-admin.component', imports: [ RouterOutlet, - SideBarAdminComponent, - BreadcrumbsComponent + BreadcrumbsComponent, + VerticalNavComponent ], schemas:[CUSTOM_ELEMENTS_SCHEMA], templateUrl: './admin.component.html', styleUrl: './admin.component.scss' }) -export class AdminComponent implements OnInit { - private document = inject(DOCUMENT); - height = 0; +export class AdminComponent{ - constructor(private router: Router) { - this.height = 0 + //adminRoutes = (ADMIN_ROUTES[0]?.children ?? []).filter(r => r.path && r.data?.['nav']); + adminRoutes = (ADMIN_ROUTES[0]?.children ?? []).filter(r => r.path && r.data?.['nav']); + // If your admin module is mounted at '/admin' in the app routes, keep this as '/admin'. + // If it's at root, set this to ''. + basePath = '/main/admin'; + + isCollapsed = false; + + navCollapsedChanged($event: boolean) { + this.isCollapsed = $event; } - - // public props - navCollapsed: boolean = false; - navCollapsedMob: boolean = false; - - // public method - navMobClick() { - if (this.navCollapsedMob && !document.querySelector('app-navigation.pc-sidebar')?.classList.contains('mob-open')) { - this.navCollapsedMob = !this.navCollapsedMob; - setTimeout(() => { - this.navCollapsedMob = !this.navCollapsedMob; - }, 100); - } else { - this.navCollapsedMob = !this.navCollapsedMob; - } - if (document.querySelector('app-navigation.pc-sidebar')?.classList.contains('navbar-collapsed')) { - document.querySelector('app-navigation.pc-sidebar')?.classList.remove('navbar-collapsed'); - } - } - - handleKeyDown(event: KeyboardEvent): void { - if (event.key === 'Escape') { - this.closeMenu(); - } - } - - closeMenu() { - if (document.querySelector('app-navigation.pc-sidebar')?.classList.contains('mob-open')) { - document.querySelector('app-navigation.pc-sidebar')?.classList.remove('mob-open'); - } - } - - ngOnInit() { - - if(this.document.defaultView !== null || this.document.defaultView !== undefined) { - // @ts-ignore - this.height = this.document.defaultView.innerHeight; - } - else - this.height = 0; - - - } - } diff --git a/jambotron-ui/src/app/modules/admin-module/admin.routing.ts b/jambotron-ui/src/app/modules/admin-module/admin.routing.ts index 01daa89..2b9dfb8 100644 --- a/jambotron-ui/src/app/modules/admin-module/admin.routing.ts +++ b/jambotron-ui/src/app/modules/admin-module/admin.routing.ts @@ -1,32 +1,37 @@ import {RouterModule, Routes} from '@angular/router'; import {AdminComponent} from './admin.component/admin.component'; -const ADMIN_ROUTES: Routes = [ +export const ADMIN_ROUTES: Routes = [ { path: '', - component: AdminComponent, - data:{breadcrumb: 'Admin'}, + loadComponent: () => import('../admin-module/admin.component/admin.component').then((c) => c.AdminComponent), + data:{title: 'Admin', icon:'', nav:true}, children: [ { path: '', pathMatch: 'full', redirectTo: 'admin-welcome' }, { path: 'admin-welcome', loadComponent: () => import('../admin-module/admin-welcome.component/admin-welcome.component').then((c) => c.AdminWelcomeComponent), - data:{breadcrumb: 'Welcome'} + data:{title: 'Welcome', icon:'dashboard', nav:true} }, { path: 'settings', loadComponent: () => import('../admin-module/settings.component/settings.component').then((c) => c.SettingsComponent), - data:{breadcrumb: 'Settings'} + data:{title: 'Settings', icon:'settings', nav:true} }, { path: 'system', loadComponent: () => import('../admin-module/system.component/system.component').then((c) => c.SystemComponent), - data:{breadcrumb: 'System'} + data:{title: 'System', icon:'component_exchange', nav:true} }, { path: 'users', loadComponent: () => import('../admin-module/users.component/users.component').then((c) => c.UsersComponent), - data:{breadcrumb: 'Users'} + data:{title: 'Users', icon:'', nav:true} + }, + { + path:"data-base", + loadComponent: () => import('../admin-module/data-base.component/data-base.component').then((c) => c.DataBaseComponent), + data:{title: 'Data', icon:'data_table', nav:true} } ] } diff --git a/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.html b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.html new file mode 100644 index 0000000..86111ea --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.html @@ -0,0 +1,59 @@ + + JSON Dump Management + + + + {{ entity | titlecase }} + + download Export {{ entity }} + + + upload Import {{ entity }} + + + + + + Available JSON Files + + + File Name + {{ file.fileName }} + + + + Size + {{ jsonDumpService.formatFileSize(file.size) }} + + + + Last Modified + {{ jsonDumpService.formatDate(file.lastModified) }} + + + + Actions + + + download + + + delete + + + + + + + + + + Archives + + {{ archive }} + + + + + + diff --git a/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.scss b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.scss new file mode 100644 index 0000000..8619e47 --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.scss @@ -0,0 +1,27 @@ +.json-dump-container { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 16px; +} + +.export-buttons, +.import-section { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.actions { + margin-bottom: 16px; + + button { + margin-right: 8px; + } +} +table { + width: 100%; +} +mat-table { + margin-bottom: 24px; +} diff --git a/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.spec.ts b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.spec.ts new file mode 100644 index 0000000..06d6308 --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { JsonDumpComponent } from './json-dump.component'; + +describe('JsonDumpComponent', () => { + let component: JsonDumpComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [JsonDumpComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(JsonDumpComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.ts b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.ts new file mode 100644 index 0000000..11a1d0c --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.ts @@ -0,0 +1,135 @@ +import {Component, CUSTOM_ELEMENTS_SCHEMA, OnInit} from '@angular/core'; + +import { MatSnackBar } from '@angular/material/snack-bar'; +import {MatList, MatListItem} from '@angular/material/list'; +import { + MatCell, MatCellDef, + MatColumnDef, + MatHeaderCell, MatHeaderCellDef, + MatHeaderRow, + MatHeaderRowDef, + MatRow, + MatRowDef, MatTable +} from '@angular/material/table'; +import {MatIconButton} from '@angular/material/button'; +import {NgForOf, NgIf, TitleCasePipe} from '@angular/common'; +import {MatProgressSpinner} from '@angular/material/progress-spinner'; +import {MatIcon} from '@angular/material/icon'; +import {MatTooltip} from '@angular/material/tooltip'; +import {ArchivesResponse, FileInfo, FilesResponse, JsonDumpService} from '../../json-dump.service'; + +@Component({ + selector: 'app-json-dump', + templateUrl: './json-dump.component.html', + imports: [ + MatProgressSpinner, + MatListItem, + MatList, + MatHeaderRow, + MatRow, + MatHeaderRowDef, + MatRowDef, + MatIcon, + MatHeaderCell, + MatCell, + MatIconButton, + MatTooltip, + MatColumnDef, + MatHeaderCellDef, + MatCellDef, + MatTable, + TitleCasePipe, + NgIf, + NgForOf + ], + styleUrls: ['./json-dump.component.scss'], + schemas:[CUSTOM_ELEMENTS_SCHEMA] +}) +export class JsonDumpComponent implements OnInit { + files: FileInfo[] = []; + archives: string[] = []; + loading = false; + + entityTypes = ['users', 'roles', 'tutorials']; + + constructor( + public jsonDumpService: JsonDumpService, + private snackBar: MatSnackBar + ) {} + + ngOnInit(): void { + this.loadFiles(); + this.loadArchives(); + } + + loadFiles(): void { + this.loading = true; + this.jsonDumpService.getFiles().subscribe({ + next: (res: FilesResponse) => { + this.files = res.files || []; + this.loading = false; + }, + error: (err: { message: any; }) => { + this.loading = false; + this.showMessage(`Error loading files: ${err.message}`); + } + }); + } + + loadArchives(): void { + this.jsonDumpService.getArchivedFiles().subscribe({ + next: (res: ArchivesResponse) => { + this.archives = res.archives || []; + }, + error: (err: { message: any; }) => { + this.showMessage(`Error loading archives: ${err.message}`); + } + }); + } + + exportEntity(entity: string): void { + this.jsonDumpService.exportEntity(entity).subscribe({ + next: () => { + this.showMessage(`Exported ${entity} successfully`); + this.loadFiles(); + }, + error: (err: { message: any; }) => this.showMessage(`Export ${entity} failed: ${err.message}`) + }); + } + + importEntity(entity: string, file: File): void { + this.jsonDumpService.importEntity(entity, file).subscribe({ + next: () => this.showMessage(`Imported ${entity} successfully`), + error: (err: { message: any; }) => this.showMessage(`Import ${entity} failed: ${err.message}`) + }); + } + + onFileSelected(event: Event, entity: string): void { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + this.importEntity(entity, input.files[0]); + input.value = ''; // reset input + } + } + + deleteFile(fileName: string): void { + this.jsonDumpService.deleteFile(fileName).subscribe({ + next: () => { + this.showMessage(`Deleted file: ${fileName}`); + this.loadFiles(); + }, + error: (err: { message: any; }) => this.showMessage(`Delete failed: ${err.message}`) + }); + } + + downloadFile(fileName: string): void { + this.jsonDumpService.downloadFile(fileName).subscribe({ + next: (blob: any) => this.jsonDumpService.downloadBlob(blob, fileName), + error: (err: { message: any; }) => this.showMessage(`Download failed: ${err.message}`) + }); + } + + private showMessage(msg: string): void { + this.snackBar.open(msg, 'Close', { duration: 3000 }); + } +} diff --git a/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.html b/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.html new file mode 100644 index 0000000..f16f125 --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.html @@ -0,0 +1,7 @@ +data-base.component works! + + @for (table of tables; track table){ + {{ table.schema }}.{{ table.table }} + } + + diff --git a/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.scss b/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.spec.ts b/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.spec.ts new file mode 100644 index 0000000..93b2304 --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataBaseComponent } from './data-base.component'; + +describe('DataBaseComponent', () => { + let component: DataBaseComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DataBaseComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DataBaseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.ts b/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.ts new file mode 100644 index 0000000..cb57610 --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/data-base.component/data-base.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import {Table} from '../../../models/table'; +import {AdminModuleService} from '../admin-module.service'; + +@Component({ + selector: 'app-data-base.component', + imports: [], + templateUrl: './data-base.component.html', + styleUrl: './data-base.component.scss' +}) +export class DataBaseComponent { + tables: Table[] = []; + + constructor(private adminService:AdminModuleService) { + this.getTables(); + } + + getTables(){ + // this.adminService.getUserTables(/* optionally: 'public' */).subscribe({ + // next: data => (this.tables = data), + // error: err => console.error('Failed to load tables', err) + // }); + + } +} diff --git a/jambotron-ui/src/app/modules/admin-module/json-dump.service.spec.ts b/jambotron-ui/src/app/modules/admin-module/json-dump.service.spec.ts new file mode 100644 index 0000000..afbd120 --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/json-dump.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { JsonDumpService } from './json-dump.service'; + +describe('JsonDumpService', () => { + let service: JsonDumpService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(JsonDumpService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/jambotron-ui/src/app/modules/admin-module/json-dump.service.ts b/jambotron-ui/src/app/modules/admin-module/json-dump.service.ts new file mode 100644 index 0000000..10db41f --- /dev/null +++ b/jambotron-ui/src/app/modules/admin-module/json-dump.service.ts @@ -0,0 +1,449 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import {GlobalConstants} from '../../global-constants'; + +// Interfaces +export interface ApiResponse { + success: boolean; + message: string; + timestamp?: string; + data?: T; +} + +export interface ExportResponse extends ApiResponse { + timestamp: string; +} + +export interface ImportResponse extends ApiResponse { + timestamp: string; + fileName?: string; +} + +export interface FileInfo { + fileName: string; + size: number; + lastModified: string; + //path: string; +} + +export interface FilesResponse extends ApiResponse { + files: FileInfo[]; + count: number; +} + +export interface ArchivesResponse extends ApiResponse { + archives: string[]; + count: number; +} + +export interface ExtractResponse extends ApiResponse { + fileName: string; + targetDirectory: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class JsonDumpService { + private readonly baseUrl = `${GlobalConstants.API_URL}/admin/json-dump`; + + constructor(private http: HttpClient) {} + + exportEntity(entity: string) { + return this.http.post<{message: string}>(`${this.baseUrl}/export/${entity}`, {}); + } + + importEntity(entity: string, file: File) { + const formData = new FormData(); + formData.append('file', file); + return this.http.post(`${this.baseUrl}/import/${entity}`, formData); + } + + // ==================== EXPORT METHODS ==================== + + /** + * Export all entities (users, tutorials, roles) + */ + exportAll(): Observable { + return this.http.post(`${this.baseUrl}/export/all`, {}) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Export users only + */ + exportUsers(): Observable { + return this.http.post(`${this.baseUrl}/export/users`, {}) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Export tutorials only + */ + exportTutorials(): Observable { + return this.http.post(`${this.baseUrl}/export/tutorials`, {}) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Export roles only + */ + exportRoles(): Observable { + return this.http.post(`${this.baseUrl}/export/roles`, {}) + .pipe( + catchError(this.handleError) + ); + } + + // ==================== IMPORT METHODS ==================== + + /** + * Import users from latest JSON file + */ + importUsers(): Observable { + return this.http.post(`${this.baseUrl}/import/users`, {}) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Import tutorials from latest JSON file + */ + importTutorials(): Observable { + return this.http.post(`${this.baseUrl}/import/tutorials`, {}) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Import roles from latest JSON file + */ + importRoles(): Observable { + return this.http.post(`${this.baseUrl}/import/roles`, {}) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Import users from uploaded file + */ + importUsersFromFile(file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + + return this.http.post(`${this.baseUrl}/import/users/file`, formData) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Import tutorials from uploaded file + */ + importTutorialsFromFile(file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + + return this.http.post(`${this.baseUrl}/import/tutorials/file`, formData) + .pipe( + catchError(this.handleError) + ); + } + + // ==================== ARCHIVE METHODS ==================== + + /** + * Get list of all archived files + */ + getArchivedFiles(): Observable { + return this.http.get(`${this.baseUrl}/archives`) + .pipe( + catchError(this.handleError) + ); + } + + archiveOldFiles(): Observable { + return this.http.post(`${this.baseUrl}/archives/archive-old-files`, {}) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Extract archived file + */ + extractArchive(fileName: string): Observable { + return this.http.post(`${this.baseUrl}/archives/${fileName}/extract`, {}) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Download archived file + */ + downloadArchive(fileName: string): Observable { + return this.http.get(`${this.baseUrl}/archives/${fileName}/download`, { + responseType: 'blob' + }).pipe( + catchError(this.handleError) + ); + } + + // ==================== FILE MANAGEMENT METHODS ==================== + + /** + * Get list of all JSON files with details + */ + getFiles(): Observable { + return this.http.get(`${this.baseUrl}/files`) + .pipe( + catchError(this.handleError) + ); + } + + /** + * Download JSON file + */ + downloadFile(fileName: string): Observable { + return this.http.get(`${this.baseUrl}/files/${fileName}/download`, { + responseType: 'blob' + }).pipe( + catchError(this.handleError) + ); + } + + /** + * Delete JSON file + */ + deleteFile(fileName: string): Observable { + return this.http.delete(`${this.baseUrl}/files/${fileName}`) + .pipe( + catchError(this.handleError) + ); + } + + // ==================== UTILITY METHODS ==================== + + /** + * Download blob as file + */ + downloadBlob(blob: Blob, fileName: string): void { + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } + + /** + * Format file size for display + */ + formatFileSize(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + /** + * Format date for display + */ + formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleString(); + } + + /** + * Validate JSON file + */ + isValidJsonFile(file: File): boolean { + return file.type === 'application/json' || file.name.toLowerCase().endsWith('.json'); + } + + /** + * Get entity type from file name + */ + getEntityTypeFromFileName(fileName: string): string { + if (fileName.includes('users_')) return 'users'; + if (fileName.includes('tutorials_')) return 'tutorials'; + if (fileName.includes('roles_')) return 'roles'; + return 'unknown'; + } + + /** + * Check if file is recent (less than 7 days old) + */ + isRecentFile(lastModified: string): boolean { + const fileDate = new Date(lastModified); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + return fileDate > sevenDaysAgo; + } + + // ==================== BATCH OPERATIONS ==================== + + /** + * Export all entities and return combined status + */ + // exportAllSeparately(): Observable<{ users: ExportResponse; tutorials: ExportResponse; roles: ExportResponse }> { + // const users$ = this.exportUsers(); + // const tutorials$ = this.exportTutorials(); + // const roles$ = this.exportRoles(); + // + // return new Observable(observer => { + // const results: any = {}; + // let completed = 0; + // + // const checkCompletion = () => { + // if (completed === 3) { + // observer.next( + // value:{users:users$,tutorial:tutorials$,roles:roles$}, + // ); + // observer.complete(); + // } + // }; + // + // users$.subscribe({ + // next: (result) => { + // results.users = result; + // completed++; + // checkCompletion(); + // }, + // error: (error) => { + // results.users = { success: false, message: error.message, timestamp: new Date().toISOString() }; + // completed++; + // checkCompletion(); + // } + // }); + // + // tutorials$.subscribe({ + // next: (result) => { + // results.tutorials = result; + // completed++; + // checkCompletion(); + // }, + // error: (error) => { + // results.tutorials = { success: false, message: error.message, timestamp: new Date().toISOString() }; + // completed++; + // checkCompletion(); + // } + // }); + // + // roles$.subscribe({ + // next: (result) => { + // results.roles = result; + // completed++; + // checkCompletion(); + // }, + // error: (error) => { + // results.roles = { success: false, message: error.message, timestamp: new Date().toISOString() }; + // completed++; + // checkCompletion(); + // } + // }); + // }); + // } + + /** + * Import all entities from latest files + */ + // importAll(): Observable<{ users: ImportResponse; tutorials: ImportResponse; roles: ImportResponse }> { + // const users$ = this.importUsers(); + // const tutorials$ = this.importTutorials(); + // const roles$ = this.importRoles(); + // + // return new Observable(observer => { + // const results: any = {}; + // let completed = 0; + // + // const checkCompletion = () => { + // if (completed === 3) { + // observer.next( + // value:{user:} + // ); + // observer.complete(); + // } + // }; + // + // users$.subscribe({ + // next: (result) => { + // results.users = result; + // completed++; + // checkCompletion(); + // }, + // error: (error) => { + // results.users = { success: false, message: error.message, timestamp: new Date().toISOString() }; + // completed++; + // checkCompletion(); + // } + // }); + // + // tutorials$.subscribe({ + // next: (result) => { + // results.tutorials = result; + // completed++; + // checkCompletion(); + // }, + // error: (error) => { + // results.tutorials = { success: false, message: error.message, timestamp: new Date().toISOString() }; + // completed++; + // checkCompletion(); + // } + // }); + // + // roles$.subscribe({ + // next: (result) => { + // results.roles = result; + // completed++; + // checkCompletion(); + // }, + // error: (error) => { + // results.roles = { success: false, message: error.message, timestamp: new Date().toISOString() }; + // completed++; + // checkCompletion(); + // } + // }); + // }); + // } + + // ==================== ERROR HANDLING ==================== + + private handleError = (error: HttpErrorResponse): Observable => { + let errorMessage = 'An unknown error occurred'; + + if (error.error instanceof ErrorEvent) { + // Client-side error + errorMessage = error.error.message; + } else { + // Server-side error + if (error.error && error.error.message) { + errorMessage = error.error.message; + } else if (error.message) { + errorMessage = error.message; + } else { + errorMessage = `Server returned code ${error.status}: ${error.statusText}`; + } + } + + console.error('JsonDumpService Error:', errorMessage, error); + return throwError(() => new Error(errorMessage)); + }; +} diff --git a/jambotron-ui/src/app/modules/admin-module/settings.component/settings.component.html b/jambotron-ui/src/app/modules/admin-module/settings.component/settings.component.html index 26b0cd1..b3ea4d7 100644 --- a/jambotron-ui/src/app/modules/admin-module/settings.component/settings.component.html +++ b/jambotron-ui/src/app/modules/admin-module/settings.component/settings.component.html @@ -25,3 +25,5 @@ + + diff --git a/jambotron-ui/src/app/modules/admin-module/settings.component/settings.component.ts b/jambotron-ui/src/app/modules/admin-module/settings.component/settings.component.ts index b728cab..19b7c3e 100644 --- a/jambotron-ui/src/app/modules/admin-module/settings.component/settings.component.ts +++ b/jambotron-ui/src/app/modules/admin-module/settings.component/settings.component.ts @@ -13,6 +13,7 @@ import { } from '@angular/material/table'; import {SystemService} from '../../../services/system.service'; import {NameValueItem} from '../../../models/name-value-item'; +import {JsonDumpComponent} from '../components/json-dump.component/json-dump.component'; @Component({ selector: 'app-settings.component', @@ -32,7 +33,8 @@ import {NameValueItem} from '../../../models/name-value-item'; MatHeaderRow, MatHeaderRowDef, MatRow, - MatRowDef + MatRowDef, + JsonDumpComponent ], templateUrl: './settings.component.html', styleUrl: './settings.component.scss' diff --git a/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.html b/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.html deleted file mode 100644 index 3d40615..0000000 --- a/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - house - @if (!isCollapsed) { - Dashboard - } - - - - - newspaper - @if (!isCollapsed) { - Users - } - - - - - newspaper - @if (!isCollapsed) { - Settings - } - - - - - newspaper - @if (!isCollapsed) { - System - } - - - - diff --git a/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.scss b/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.scss deleted file mode 100644 index 62a5b67..0000000 --- a/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -.entry{ - display: flex; - align-items: center; - gap: 1rem; - padding:0.75rem; - color: rgba(24, 255, 255, 0.96); - -} - -a.mdc-list-item -{ - - background-color: rgba(24,255,255,0.04); -} diff --git a/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.ts b/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.ts deleted file mode 100644 index 0940b6f..0000000 --- a/jambotron-ui/src/app/modules/admin-module/side-bar-admin.component/side-bar-admin.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {MatListItem, MatNavList} from '@angular/material/list'; -import {RouterLink} from '@angular/router'; -import {MatIcon} from '@angular/material/icon'; - -@Component({ - selector: 'app-side-bar-admin', - imports: [ - MatIcon, - MatListItem, - MatNavList, - RouterLink - ], - templateUrl: './side-bar-admin.component.html', - styleUrl: './side-bar-admin.component.scss', - schemas:[CUSTOM_ELEMENTS_SCHEMA] -}) -export class SideBarAdminComponent { - isCollapsed = false; -} diff --git a/jambotron-ui/src/app/modules/admin-module/users.component/users.component.ts b/jambotron-ui/src/app/modules/admin-module/users.component/users.component.ts index b7ebcb4..1011b80 100644 --- a/jambotron-ui/src/app/modules/admin-module/users.component/users.component.ts +++ b/jambotron-ui/src/app/modules/admin-module/users.component/users.component.ts @@ -53,15 +53,15 @@ export class UsersComponent { displayedColumns: string[] = UserColumns.map((col) => col.key) columnsSchema: any = UserColumns - constructor(private userService: UserService, - private roleService: RolesService, + constructor( + private adminService:AdminModuleService) { this.getAllRoles(); this.getUsers(); } getAllRoles():any{ - this.roleService.getAllRoles().subscribe( + this.adminService.getAllRoles().subscribe( (data : any) => { this.allRoles = data; console.log(data); @@ -73,7 +73,7 @@ export class UsersComponent { } getUsers(){ - this.userService.getAdminBoard().subscribe( + this.adminService.getUsers().subscribe( (data : any) => { this.users = data; console.log(data); @@ -84,7 +84,7 @@ export class UsersComponent { ); } - saveUserRoles(user:User){ + saveUserRoles(user:User){ this.adminService.updateUserRoles(user.id, user).subscribe( (data:any) => { console.log(data); @@ -114,6 +114,8 @@ export class UsersComponent { isDisabled(element:User, role: Role) { return role.name?.includes(element.username?.toUpperCase()); } + + } export const UserColumns = [ diff --git a/jambotron-ui/src/app/modules/main-module/main.component/main.component.html b/jambotron-ui/src/app/modules/main-module/main.component/main.component.html index d4c3725..054f7ed 100644 --- a/jambotron-ui/src/app/modules/main-module/main.component/main.component.html +++ b/jambotron-ui/src/app/modules/main-module/main.component/main.component.html @@ -1,60 +1,63 @@ - - {{title}} - - - Generate Image - - - Tutorials - - - - - @if (!isLoggedIn) { - - person_add + + + {{title}} + + + Generate Image + + + Tutorials - - login - - }@else { - - account_circle - {{username}} - - - @if (showAdminBoard) { - - admin_panel_settings - Admin Panel - - } - @if (showUserBoard) { - - space_dashboard - User tools - - } - @if (showModeratorBoard) { - - space_dashboard - Moderator board - - } + - - person - Profile + @if (!isLoggedIn) { + + person_add - - - logout - Logout + + login - - } + }@else { + + account_circle + {{username}} + + + @if (showAdminBoard) { + + admin_panel_settings + Admin Panel + + } + + @if (showUserBoard) { + + space_dashboard + User tools + + } + @if (showModeratorBoard) { + + space_dashboard + Moderator board + + } + + + person + Profile + + + + logout + Logout + + + } + + diff --git a/jambotron-ui/src/app/modules/main-module/main.component/main.component.scss b/jambotron-ui/src/app/modules/main-module/main.component/main.component.scss index 893af8a..3f6da6d 100644 --- a/jambotron-ui/src/app/modules/main-module/main.component/main.component.scss +++ b/jambotron-ui/src/app/modules/main-module/main.component/main.component.scss @@ -30,6 +30,7 @@ mat-toolbar{ margin-top: 65px; height: 100%; min-height: fit-content; + width: 100%; } .component{ diff --git a/jambotron-ui/src/app/modules/main-module/main.routing.ts b/jambotron-ui/src/app/modules/main-module/main.routing.ts index c80ef9b..5872280 100644 --- a/jambotron-ui/src/app/modules/main-module/main.routing.ts +++ b/jambotron-ui/src/app/modules/main-module/main.routing.ts @@ -1,10 +1,10 @@ import {RouterModule, Routes} from '@angular/router'; import {MainComponent} from './main.component/main.component'; -const MAIN_ROUTES: Routes =[ +export const MAIN_ROUTES: Routes =[ { path: '', - component: MainComponent, + loadComponent: () => import('./main.component/main.component').then((c) => c.MainComponent), children: [ { path: 'tutorials', @@ -21,6 +21,7 @@ const MAIN_ROUTES: Routes =[ path: 'admin', loadChildren: () => import('../admin-module/admin.module').then((m) => m.AdminModule), + data:{nav:true} }, { path: 'user', diff --git a/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.html b/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.html index 726bafa..479132c 100644 --- a/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.html +++ b/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.html @@ -1,5 +1,2 @@ - - - - - + + diff --git a/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.scss b/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.scss index 016384e..e69de29 100644 --- a/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.scss +++ b/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.scss @@ -1,13 +0,0 @@ -//--------------- -.pc-sidebar{ - top: 65px; - overflow-y: auto; - background-color: rgba(153, 153, 153, 0.16); - - backdrop-filter: blur(8px); -} -.pc-container{ - top: 0px; - padding-left: 5px; - padding-right: 5px; -} diff --git a/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.ts b/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.ts index 4f663e2..c83efaa 100644 --- a/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.ts +++ b/jambotron-ui/src/app/modules/moderator-module/moderator.component/moderator.component.ts @@ -2,20 +2,21 @@ import { Component } from '@angular/core'; import {RouterOutlet} from '@angular/router'; import {SideBarModeratorComponent} from '../side-bar-moderator.component/side-bar-moderator.component'; import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component'; +import {InnerLayoutComponent} from '../../../components/inner-layout.component/inner-layout.component'; +import {MODERATOR_ROUTS} from '../moderator.routing'; @Component({ selector: 'app-moderator.component', imports: [ - RouterOutlet, - SideBarModeratorComponent, - BreadcrumbsComponent, + InnerLayoutComponent, ], templateUrl: './moderator.component.html', styleUrl: './moderator.component.scss' }) export class ModeratorComponent { - + moderatorRouts = MODERATOR_ROUTS; + basePath = '/main/moderator'; } diff --git a/jambotron-ui/src/app/modules/moderator-module/moderator.routing.ts b/jambotron-ui/src/app/modules/moderator-module/moderator.routing.ts index dd22003..073b7f9 100644 --- a/jambotron-ui/src/app/modules/moderator-module/moderator.routing.ts +++ b/jambotron-ui/src/app/modules/moderator-module/moderator.routing.ts @@ -2,30 +2,30 @@ import {RouterModule, Routes} from '@angular/router'; import {ModeratorComponent} from './moderator.component/moderator.component'; -const MODERATOR_ROUTES: Routes = [ +export const MODERATOR_ROUTS: Routes = [ { path: '', component: ModeratorComponent, - data:{breadcrumb: 'Moderator'}, + data:{title: 'Moderator'}, children: [ { path: '', pathMatch: 'full', redirectTo: 'moderator-welcome' }, // default redirect { path: 'moderator-welcome', loadComponent: () => import('../moderator-module/moderator-welcome.component/moderator-welcome.component').then((c) => c.ModeratorWelcomeComponent), - data:{breadcrumb: 'Welcome'} + data:{title: 'Welcome', icon:'dashboard', nav:true} }, { path: 'tutorials-list', loadComponent: () => import('../moderator-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent), - data:{breadcrumb: 'Tutorials List'} + data:{title: 'Tutorials List', icon:'list', nav:true} }, { path: 'tutorial-preview', loadComponent: () => import('../moderator-module/tutorial-preview.component/tutorial-preview.component').then((c) => c.TutorialPreviewComponent), - data:{breadcrumb: 'Tutorial Preview'} + data:{title: 'Tutorial Preview'} } ] } ]; -export const moderatorRouting = RouterModule.forChild(MODERATOR_ROUTES); +export const moderatorRouting = RouterModule.forChild(MODERATOR_ROUTS); diff --git a/jambotron-ui/src/app/modules/user-module/user.component/user.component.html b/jambotron-ui/src/app/modules/user-module/user.component/user.component.html index 85123bf..04c4e93 100644 --- a/jambotron-ui/src/app/modules/user-module/user.component/user.component.html +++ b/jambotron-ui/src/app/modules/user-module/user.component/user.component.html @@ -1,10 +1,2 @@ - - - - - - - + diff --git a/jambotron-ui/src/app/modules/user-module/user.component/user.component.ts b/jambotron-ui/src/app/modules/user-module/user.component/user.component.ts index 54c1832..28471e5 100644 --- a/jambotron-ui/src/app/modules/user-module/user.component/user.component.ts +++ b/jambotron-ui/src/app/modules/user-module/user.component/user.component.ts @@ -4,43 +4,24 @@ import {MatSidenav} from '@angular/material/sidenav'; import {BreakpointObserver} from '@angular/cdk/layout'; import {SideBarUserComponent} from '../side-bar-user.component/side-bar-user.component'; import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component'; +import {InnerLayoutComponent} from '../../../components/inner-layout.component/inner-layout.component'; +import {MODERATOR_ROUTS} from '../../moderator-module/moderator.routing'; +import {USER_ROUTS} from '../user.routing'; @Component({ selector: 'app-user.component', imports: [ RouterOutlet, SideBarUserComponent, - BreadcrumbsComponent + BreadcrumbsComponent, + InnerLayoutComponent ], templateUrl: './user.component.html', styleUrl: './user.component.scss', schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class UserComponent implements OnInit { - @ViewChild(MatSidenav) - sidenav!: MatSidenav; - isMobile= true; - isCollapsed = true; +export class UserComponent { - - constructor(private observer: BreakpointObserver) { - - } - - ngOnInit(): void { - this.observer.observe(['(max-width: 800px)']).subscribe((screenSize) => { - this.isMobile = screenSize.matches; - }) - - - } - toggleMenu() { - if(this.isMobile){ - this.sidenav.toggle(); - this.isCollapsed = false; - } else { - this.sidenav.open(); - this.isCollapsed = !this.isCollapsed; - } - } + userRouts = USER_ROUTS; + basePath = '/main/user'; } diff --git a/jambotron-ui/src/app/modules/user-module/user.routing.ts b/jambotron-ui/src/app/modules/user-module/user.routing.ts index 2b18ea7..9d1a946 100644 --- a/jambotron-ui/src/app/modules/user-module/user.routing.ts +++ b/jambotron-ui/src/app/modules/user-module/user.routing.ts @@ -2,47 +2,47 @@ import {RouterModule, Routes} from '@angular/router'; import {UserComponent} from './user.component/user.component'; -const USER_ROUTES: Routes = [ +export const USER_ROUTS: Routes = [ { path: '', component: UserComponent, - data:{breadcrumb: 'User'}, + data:{title: 'User'}, children: [ { path: '', pathMatch: 'full', redirectTo: 'user-welcome' }, // default redirect { path: 'user-welcome', loadComponent: () => import('../user-module/user-welcome.component/user-welcome.component').then((c) => c.UserWelcomeComponent), - data:{breadcrumb: 'Welcome'} + data:{title: 'Welcome', icon:'dashboard', nav:true} }, { path: 'tutorials-list', loadComponent: () => import('../user-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent), - data:{breadcrumb: 'Tutorials List'} + data:{title: 'Tutorials List', icon:'list', nav:true} }, { path: 'tutorial-add', loadComponent: () => import('../user-module/tutorial-add.component/tutorial-add.component').then((c) => c.TutorialAddComponent), - data:{breadcrumb: 'Add Tutorial'} + data:{title: 'Add Tutorial'} }, { path: 'tutorial-edit', loadComponent: () => import('../user-module/tutorial-edit.component/tutorial-edit.component').then((c) => c.TutorialEditComponent), - data:{breadcrumb: 'Edit Tutorial'} + data:{title: 'Edit Tutorial'} }, { path: 'ai-models', loadComponent: () => import('../user-module/ai-models.component/ai-models.component').then((c) => c.AiModelsComponent), - data:{breadcrumb: 'AI'} + data:{title: 'AI'} }, { path: 'images', loadComponent: () => import('../user-module/images.component/images.component').then((c) => c.ImagesComponent), - data:{breadcrumb: 'Images'} + data:{title: 'Images', icon: 'imagesmode', nav:true} } ] } ]; -export const userRouting = RouterModule.forChild(USER_ROUTES); +export const userRouting = RouterModule.forChild(USER_ROUTS); diff --git a/jambotron-ui/src/app/services/spinner.service.ts b/jambotron-ui/src/app/services/spinner.service.ts index c4fa063..5c7aef5 100644 --- a/jambotron-ui/src/app/services/spinner.service.ts +++ b/jambotron-ui/src/app/services/spinner.service.ts @@ -10,10 +10,10 @@ export class SpinnerService { } show() { - this.visibility.next(true); + this.visibility.next(false); } hide() { - this.visibility.next(false); + this.visibility.next(true); } } diff --git a/jambotron-ui/src/app/services/user.service.ts b/jambotron-ui/src/app/services/user.service.ts index f6b1764..9fe6423 100644 --- a/jambotron-ui/src/app/services/user.service.ts +++ b/jambotron-ui/src/app/services/user.service.ts @@ -24,8 +24,6 @@ export class UserService { return this.http.get(API_URL + '/mod', { responseType: 'text' }); } - getAdminBoard(): Observable { - return this.http.get(API_URL); - } + } diff --git a/jambotron-ui/src/index.html b/jambotron-ui/src/index.html index aa574b3..e054db8 100644 --- a/jambotron-ui/src/index.html +++ b/jambotron-ui/src/index.html @@ -7,8 +7,8 @@ - + diff --git a/jambotron-ui/src/index_dev.html b/jambotron-ui/src/index_dev.html index 944af06..3781ee0 100644 --- a/jambotron-ui/src/index_dev.html +++ b/jambotron-ui/src/index_dev.html @@ -7,6 +7,8 @@ + diff --git a/jambotron-ui/src/styles.scss b/jambotron-ui/src/styles.scss index c22d8a1..32972ed 100644 --- a/jambotron-ui/src/styles.scss +++ b/jambotron-ui/src/styles.scss @@ -49,11 +49,11 @@ body { } .pc-container{ top: 0; - padding-left: 5px; - padding-right: 5px; + padding-left: 20px; + padding-right: 20px; position: relative; - margin-left: 260px; + margin-left: 240px; min-height: calc(100vh - 113px); background: #fafafbb3; } diff --git a/src/main/java/com/jambotronGroup/jambotron/DTOs/RoleDto.java b/src/main/java/com/jambotronGroup/jambotron/DTOs/RoleDto.java new file mode 100644 index 0000000..fdc0e17 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/DTOs/RoleDto.java @@ -0,0 +1,48 @@ +package com.jambotronGroup.jambotron.DTOs; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.jambotronGroup.jambotron.model.ERole; +import com.jambotronGroup.jambotron.model.Role; + +public class RoleDto { + @JsonIgnore + private Long id; + private ERole name; + + public RoleDto() { + } + + public RoleDto(Long id, ERole name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ERole getName() { + return name; + } + + public void setName(ERole name) { + this.name = name; + } + + // Mapping helpers + public static RoleDto fromEntity(Role role) { + if (role == null) return null; + return new RoleDto(role.getId(), role.getName()); + } + + public Role toEntity() { + Role role = new Role(); + role.setId(this.id); + role.setName(this.name); + return role; + } +} diff --git a/src/main/java/com/jambotronGroup/jambotron/DTOs/TutorialDto.java b/src/main/java/com/jambotronGroup/jambotron/DTOs/TutorialDto.java new file mode 100644 index 0000000..166bfb0 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/DTOs/TutorialDto.java @@ -0,0 +1,145 @@ +package com.jambotronGroup.jambotron.DTOs; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.jambotronGroup.jambotron.model.Tutorial; + +import java.sql.Timestamp; + +public class TutorialDto { + @JsonIgnore + private Long id; + private String title; + private String description; + private Boolean published; + + private boolean tobepublished; + + private Long userID; + + private java.sql.Timestamp created; + + private java.sql.Timestamp modified; + + private String titleimage; + + private String body; + + public TutorialDto() { + } + + public TutorialDto(Long id, String title, String description, Boolean published, boolean tobepublished, Long userID, Timestamp created, Timestamp modified, String titleimage, String body) { + this.id = id; + this.title = title; + this.description = description; + this.published = published; + this.tobepublished = tobepublished; + this.userID = userID; + this.created = created; + this.modified = modified; + this.titleimage = titleimage; + this.body = body; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getPublished() { + return published; + } + + public void setPublished(Boolean published) { + this.published = published; + } + + public boolean isTobepublished() { + return tobepublished; + } + public void setTobepublished(boolean tobepublished) { + this.tobepublished = tobepublished; + } + public Long getUserID() { + return userID; + } + public void setUserID(Long userID) { + this.userID = userID; + } + public Timestamp getCreated() { + return created; + } + public void setCreated(Timestamp created) { + this.created = created; + } + public Timestamp getModified() { + return modified; + } + public void setModified(Timestamp modified) { + this.modified = modified; + } + public String getTitleimage() { + return titleimage; + } + public void setTitleimage(String titleimage) { + this.titleimage = titleimage; + } + public String getBody() { + return body; + } + public void setBody(String body) { + this.body = body; + } + + // Mapping helpers + public static TutorialDto fromEntity(Tutorial tutorial) { + if (tutorial == null) return null; + return new TutorialDto( + tutorial.getId(), + tutorial.getTitle(), + tutorial.getDescription(), + tutorial.isPublished(), + tutorial.isTobepublished(), + tutorial.getUser().getId(), + tutorial.getCreated(), + tutorial.getModified(), + tutorial.getTitleimage(), + tutorial.getBody() + ); + } + + public Tutorial toEntity() { + Tutorial tutorial = new Tutorial(); + tutorial.setId(this.id); + tutorial.setTitle(this.title); + tutorial.setDescription(this.description); + if (this.published != null) { + tutorial.setPublished(this.published); + } + tutorial.setCreated(this.created); + tutorial.setModified(this.modified); + tutorial.setTobepublished(this.tobepublished); + tutorial.setTitleimage(this.titleimage); + tutorial.setBody(this.body); + + return tutorial; + } +} diff --git a/src/main/java/com/jambotronGroup/jambotron/DTOs/UserDto.java b/src/main/java/com/jambotronGroup/jambotron/DTOs/UserDto.java new file mode 100644 index 0000000..de04d49 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/DTOs/UserDto.java @@ -0,0 +1,102 @@ +package com.jambotronGroup.jambotron.DTOs; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.jambotronGroup.jambotron.model.User; + +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class UserDto { + @JsonIgnore + private Long id; + private String username; + private String email; + + private String password; // typically you don't expose password in DTOs + + private Set roles; // typically you don't expose password in DTOs + + + public UserDto() { + } + + public UserDto(Long id, String username, String email, String password, Set roles) { + this.id = id; + this.username = username; + this.email = email; + this.password = password; + this.roles = roles; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + // Mapping helpers + public static UserDto fromEntity(User user) { + if (user == null) return null; + return new UserDto( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getPassword(), + user.getRoles() == null ? null : + user.getRoles().stream() + .filter(Objects::nonNull) + .map(RoleDto::fromEntity) + .collect(Collectors.toSet()) + ); + } + + public User toEntity() { + User user = new User(); + user.setId(this.id); + user.setUsername(this.username); + user.setEmail(this.email); + user.setPassword(this.password); // Assuming password is already hashed + if (this.roles != null) { + user.setRoles(this.roles.stream() + .filter(Objects::nonNull) + .map(RoleDto::toEntity) + .collect(Collectors.toSet())); + } + return user; + } +} diff --git a/src/main/java/com/jambotronGroup/jambotron/JambotronApplication.java b/src/main/java/com/jambotronGroup/jambotron/JambotronApplication.java index 3c64e86..7cfd134 100644 --- a/src/main/java/com/jambotronGroup/jambotron/JambotronApplication.java +++ b/src/main/java/com/jambotronGroup/jambotron/JambotronApplication.java @@ -2,9 +2,11 @@ package com.jambotronGroup.jambotron; import com.jambotronGroup.jambotron.fileUpload.FilesStorageService; + import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -14,6 +16,9 @@ public class JambotronApplication implements CommandLineRunner { @Resource FilesStorageService storageService; +// @Autowired +// UserExportService userExportService; + private static final Logger logger = LoggerFactory.getLogger(JambotronApplication.class); public static void main(String[] args) { @@ -29,6 +34,14 @@ public class JambotronApplication implements CommandLineRunner { public void run(String... arg) throws Exception { // storageService.deleteAll(); storageService.init(); + +// try { +// userExportService.exportAllRolesToJson(); +// userExportService.exportAllUsersToJson(); +// +// } catch (Exception e) { +// logger.error("Error exporting users or roles to JSON", e); +// } } } diff --git a/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java b/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java new file mode 100644 index 0000000..3d75495 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java @@ -0,0 +1,347 @@ +package com.jambotronGroup.jambotron.controllers; + +import com.jambotronGroup.jambotron.jsonDump.JsonDumpService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +@RestController +@RequestMapping("/api/admin/json-dump") +@PreAuthorize("hasRole('ADMIN')") +public class JsonDumpController { + + private static final Logger _logger = LoggerFactory.getLogger(JsonDumpController.class); + + @Autowired + private JsonDumpService _jsonDumpService; + + // ==================== EXPORT ENDPOINTS ==================== + + @PostMapping("/export/all") + public ResponseEntity> exportAll() { + Map response = new HashMap<>(); + try { + _jsonDumpService.exportAllRolesToJson(); + _jsonDumpService.exportAllUsersToJson(); + _jsonDumpService.exportAllTutorialsToJson(); + + response.put("success", true); + response.put("message", "All entities exported successfully"); + response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + + _logger.info("Manual export of all entities completed successfully"); + return ResponseEntity.ok(response); + + } catch (Exception e) { + _logger.error("Failed to export all entities", e); + response.put("success", false); + response.put("message", "Export failed: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + @PostMapping("/export/users") + public ResponseEntity> exportUsers() { + return performExport(() -> _jsonDumpService.exportAllUsersToJson(), "users"); + } + + @PostMapping("/export/tutorials") + public ResponseEntity> exportTutorials() { + return performExport(() -> _jsonDumpService.exportAllTutorialsToJson(), "tutorials"); + } + + @PostMapping("/export/roles") + public ResponseEntity> exportRoles() { + return performExport(() -> _jsonDumpService.exportAllRolesToJson(), "roles"); + } + + // ==================== IMPORT ENDPOINTS ==================== + + @PostMapping("/import/users") + public ResponseEntity> importUsers() { + return performImport(() -> _jsonDumpService.importUsers(), "users"); + } + + @PostMapping("/import/tutorials") + public ResponseEntity> importTutorials() { + return performImport(() -> _jsonDumpService.importTutorials(), "tutorials"); + } + + @PostMapping("/import/roles") + public ResponseEntity> importRoles() { + return performImport(() -> _jsonDumpService.importRoles(), "roles"); + } + + @PostMapping("/import/users/file") + public ResponseEntity> importUsersFromFile(@RequestParam("file") MultipartFile file) { + return importFromFile(file, (path) -> _jsonDumpService.importUsers(path), "users"); + } + + @PostMapping("/import/tutorials/file") + public ResponseEntity> importTutorialsFromFile(@RequestParam("file") MultipartFile file) { + return importFromFile(file, (path) -> _jsonDumpService.importTutorials(path), "tutorials"); + } + + // ==================== ARCHIVE MANAGEMENT ==================== + + @PostMapping("/archives/archive-old-files") + public ResponseEntity> archiveOldFiles() { + return performArchiveOldFiles(() -> _jsonDumpService.archiveOldFiles()); + } + @GetMapping("/archives") + public ResponseEntity> getArchivedFiles() { + Map response = new HashMap<>(); + try { + List archivedFiles = _jsonDumpService.getArchivedFiles(); + response.put("success", true); + response.put("archives", archivedFiles); + response.put("count", archivedFiles.size()); + + return ResponseEntity.ok(response); + + } catch (IOException e) { + _logger.error("Failed to list archived files", e); + response.put("success", false); + response.put("message", "Failed to list archives: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + @PostMapping("/archives/{fileName}/extract") + public ResponseEntity> extractArchive(@PathVariable String fileName) { + Map response = new HashMap<>(); + try { + + _jsonDumpService.extractArchivedFile(fileName); + + response.put("success", true); + response.put("message", "Archive extracted successfully"); + response.put("fileName", fileName); + //response.put("targetDirectory", targetDir.toAbsolutePath().toString()); + + _logger.info("Archive {} extracted successfully", fileName); + return ResponseEntity.ok(response); + + } catch (Exception e) { + _logger.error("Failed to extract archive: {}", fileName, e); + response.put("success", false); + response.put("message", "Failed to extract archive: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + @GetMapping("/archives/{fileName}/download") + public ResponseEntity downloadArchive(@PathVariable String fileName) { + try { + + + Resource resource = _jsonDumpService.getArchivedFileResource(fileName); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentLength(Files.size(resource.getFile().toPath())) + .body(resource); + + } catch (IOException e) { + _logger.error("Failed to download archive: {}", fileName, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + // ==================== FILE MANAGEMENT ==================== + + @GetMapping("/files") + public ResponseEntity> listFiles() { + Map response = new HashMap<>(); + try { + + List> files = _jsonDumpService.getJsonDumpInfo(); + + response.put("success", true); + response.put("files", files); + response.put("count", files.size()); + + return ResponseEntity.ok(response); + + } catch (IOException e) { + _logger.error("Failed to list files", e); + response.put("success", false); + response.put("message", "Failed to list files: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + @GetMapping("/files/{fileName}/download") + public ResponseEntity downloadFile(@PathVariable String fileName) { + try { + + Resource resource = _jsonDumpService.getJsonDumpResource(fileName); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"") + .contentType(MediaType.APPLICATION_JSON) + .contentLength(Files.size(resource.getFile().toPath())) + .body(resource); + + } catch (IOException e) { + _logger.error("Failed to download file: {}", fileName, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @DeleteMapping("/files/{fileName}") + public ResponseEntity> deleteFile(@PathVariable String fileName) { + Map response = new HashMap<>(); + try { + response = _jsonDumpService.deleteJsonDumpFile(fileName); + _logger.info("File {} deleted successfully", fileName); + return ResponseEntity.ok(response); + + } catch (IOException e) { + _logger.error("Failed to delete file: {}", fileName, e); + response.put("success", false); + response.put("message", "Failed to delete file: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + // ==================== UTILITY METHODS ==================== + + private ResponseEntity> performExport(ExportOperation operation, String entityType) { + Map response = new HashMap<>(); + try { + operation.execute(); + response.put("success", true); + response.put("message", entityType + " exported successfully"); + response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + + _logger.info("Manual export of {} completed successfully", entityType); + return ResponseEntity.ok(response); + + } catch (Exception e) { + _logger.error("Failed to export {}", entityType, e); + response.put("success", false); + response.put("message", "Export failed: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + private ResponseEntity> performImport(ImportOperation operation, String entityType) { + Map response = new HashMap<>(); + try { + operation.execute(); + response.put("success", true); + response.put("message", entityType + " imported successfully"); + response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + + _logger.info("Manual import of {} completed successfully", entityType); + return ResponseEntity.ok(response); + + } catch (Exception e) { + _logger.error("Failed to import {}", entityType, e); + response.put("success", false); + response.put("message", "Import failed: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + private ResponseEntity> importFromFile(MultipartFile file, FileImportOperation operation, String entityType) { + Map response = new HashMap<>(); + try { + if (file.isEmpty() || !file.getOriginalFilename().endsWith(".json")) { + response.put("success", false); + response.put("message", "Please upload a valid JSON file"); + return ResponseEntity.badRequest().body(response); + } + + // Save uploaded file temporarily + Path tempFile = Files.createTempFile("import_", ".json"); + file.transferTo(tempFile.toFile()); + + try { + operation.execute(tempFile); + response.put("success", true); + response.put("message", entityType + " imported successfully from uploaded file"); + response.put("fileName", file.getOriginalFilename()); + response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + + _logger.info("Manual import of {} from file {} completed successfully", entityType, file.getOriginalFilename()); + return ResponseEntity.ok(response); + + } finally { + // Clean up temp file + Files.deleteIfExists(tempFile); + } + + } catch (Exception e) { + _logger.error("Failed to import {} from file", entityType, e); + response.put("success", false); + response.put("message", "Import failed: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + private ResponseEntity> performArchiveOldFiles(ArchiveOperation operation) { + Map response = new HashMap<>(); + try { + operation.execute(); + response.put("success", true); + response.put("message", "Archive old files successfully"); + response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + + _logger.info("Archive old files successfully"); + return ResponseEntity.ok(response); + + } catch (Exception e) { + _logger.error("Failed Archive old files.", e); + response.put("success", false); + response.put("message", "Failed Archive old files: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + + @FunctionalInterface + private interface ArchiveOperation { + void execute() throws Exception; + } + + // Functional interfaces for operations + @FunctionalInterface + private interface ExportOperation { + void execute() throws Exception; + } + + @FunctionalInterface + private interface ImportOperation { + void execute() throws Exception; + } + + @FunctionalInterface + private interface FileImportOperation { + void execute(Path filePath) throws Exception; + } +} \ No newline at end of file diff --git a/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageServiceImpl.java b/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageServiceImpl.java index 2697396..3ee23c1 100644 --- a/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageServiceImpl.java +++ b/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageServiceImpl.java @@ -42,6 +42,7 @@ public class FilesStorageServiceImpl implements FilesStorageService { private final Path root = Paths.get("/jambotron_data/uploads/user-images/"); private final Path rootPublic = Paths.get("/jambotron_data/uploads/public-images/"); + // private final Path rootInitData = Pa @Override public void init() { diff --git a/src/main/java/com/jambotronGroup/jambotron/initData/InitDataConfiguration.java b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataConfiguration.java new file mode 100644 index 0000000..93d2fb1 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataConfiguration.java @@ -0,0 +1,21 @@ +package com.jambotronGroup.jambotron.initData; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +@Configuration +//@ConditionalOnProperty(name = "app.init.data.enabled", havingValue = "true", matchIfMissing = true) +public class InitDataConfiguration { + + @Autowired + private InitDataService _initDataService; + + @PostConstruct + public void init() { + + _initDataService.importData(); + } + +} diff --git a/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java new file mode 100644 index 0000000..73ee140 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java @@ -0,0 +1,148 @@ +package com.jambotronGroup.jambotron.initData; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jambotronGroup.jambotron.DTOs.RoleDto; +import com.jambotronGroup.jambotron.DTOs.UserDto; +import com.jambotronGroup.jambotron.model.ERole; +import com.jambotronGroup.jambotron.model.Role; +import com.jambotronGroup.jambotron.model.User; +import com.jambotronGroup.jambotron.repository.RoleRepository; +import com.jambotronGroup.jambotron.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@Primary +public class InitDataService { + + private static final Logger _logger = LoggerFactory.getLogger(InitDataService.class); + + private ObjectMapper _mapper; + + @Autowired + private ResourceLoader _resources; + @Autowired + private RoleRepository _roleRepository; + + @Autowired + private UserRepository _userRepository; + + public InitDataService(){ + _mapper = new ObjectMapper(); + } + + @Transactional + public void importRoles(Path jsonFile) throws Exception { + _logger.info("Importing roles from {}", jsonFile.toAbsolutePath()); + List items = readList(jsonFile, new TypeReference>() {}); + for (RoleDto dto : items) { + Role roleEntity = dto.toEntity(); + // If a role with same name exists, reuse its ID to upsert +// _roleRepository.findByName(entity.getName()) +// .ifPresent(existing -> entity.setId(existing.getId())); + + if(!_roleRepository.existsByName(roleEntity.getName())) { + _roleRepository.save(roleEntity); + _logger.info("Saving new role: {}", roleEntity.getName()); + } else { + _logger.info("Role {} already exists, SKIPPING import", roleEntity.getName()); + } + } + _logger.info("Imported {} roles", items.stream() + .map(RoleDto::getName).toList() + .stream().map(ERole::toString).collect(Collectors.joining(", "))); + } + + @Transactional + public void importUsers(Path jsonFile) throws Exception { + _logger.info("Importing users from {}", jsonFile.toAbsolutePath()); + // Preload roles map by name for quick lookup + Map rolesByName = _roleRepository.findAll().stream() + .collect(Collectors.toMap(Role::getName, r -> r)); + + List items = readList(jsonFile, new TypeReference>() {}); + for (UserDto dto : items) { + User user = new User(); + if (dto.getId() != null) user.setId(dto.getId()); + user.setUsername(dto.getUsername()); + user.setEmail(dto.getEmail()); + // Expecting already-hashed values; do not import plain text secrets + user.setPassword(dto.getPassword()); + + // Resolve roles by enum name + Set roles = new HashSet<>(); + if (dto.getRoles() != null) { + for (RoleDto roleDto : dto.getRoles()) { + ERole erole = ERole.valueOf(roleDto.getName().name()); + Role role = rolesByName.get(erole); + if (role == null) { + // Optionally create missing roles on the fly + role = new Role(); + role.setName(erole); + role = _roleRepository.save(role); + rolesByName.put(erole, role); + } + roles.add(role); + } + } + user.setRoles(roles); + + if(!_userRepository.existsByUsername(user.getUsername())) { + _userRepository.save(user); + + _logger.info("Imported user: {} with roles: {}", user.getUsername(), user.getRoles().stream() + .map(Role::getName).collect(Collectors.toList()).stream().map(ERole::toString) + .collect(Collectors.joining(", "))); + } else { + _logger.warn("User with username {} already exists, SKIPPING import", user.getUsername()); + _logger.info("Imported user: {} with roles: {}", user.getUsername(), user.getRoles().stream() + .map(Role::getName).collect(Collectors.toList()).stream().map(ERole::toString) + .collect(Collectors.joining(", "))); + } + } + } + + protected List readList(Path file, TypeReference> type) throws Exception { + String json = Files.readString(file); + return _mapper.readValue(json, type); + } + + public void importData() { + try { + Resource resource = _resources.getResource("classpath:import/roles.json"); + if (resource.exists()) { + + importRoles(resource.getFile().toPath()); + } else { + + _logger.warn("Roles import file not found: {}", resource.getFilename()); + } + + resource = _resources.getResource("classpath:import/users.json"); + if (resource.exists()) { + importUsers(resource.getFile().toPath()); + } else { + + _logger.warn("Users import file not found: {}", resource.getFilename()); + } + } catch (Exception e) { + _logger.error("Error importing data", e); + throw new RuntimeException("Failed to import data", e); + } + } +} diff --git a/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java b/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java new file mode 100644 index 0000000..0c39dbf --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java @@ -0,0 +1,456 @@ +package com.jambotronGroup.jambotron.jsonDump; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jambotronGroup.jambotron.DTOs.TutorialDto; +import com.jambotronGroup.jambotron.DTOs.UserDto; +import com.jambotronGroup.jambotron.controllers.AuthController; +import com.jambotronGroup.jambotron.initData.InitDataService; +import com.jambotronGroup.jambotron.model.ERole; +import com.jambotronGroup.jambotron.model.Role; +import com.jambotronGroup.jambotron.model.Tutorial; +import com.jambotronGroup.jambotron.model.User; +import com.jambotronGroup.jambotron.repository.RoleRepository; +import com.jambotronGroup.jambotron.repository.TutorialRepository; +import com.jambotronGroup.jambotron.repository.UserRepository; +import jakarta.persistence.EntityManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +@Service +public class JsonDumpService extends InitDataService { + + private static final Logger _logger = LoggerFactory.getLogger(JsonDumpService.class); + private final Path _root = Paths.get("/jambotron_data/jsonDump/"); + private final Path _archivePath = _root.resolve("archive"); + + private final Path _extractedPath = _root.resolve("extracted"); + + private final ObjectMapper _mapper; + + @Autowired + private final UserRepository _userRepository; + + @Autowired + private final RoleRepository _roleRepository; + + @Autowired + private final TutorialRepository _tutorialRepository; + + @Value("${app.json.dump.cleanup.enabled:true}") + private boolean _cleanupEnabled; + + @Value("${app.json.dump.cleanup.retention.weeks:4}") + private int _retentionWeeks; + + @Value("${app.json.dump.archive.enabled:true}") + private boolean _archiveEnabled; + + public JsonDumpService(ObjectMapper mapper, UserRepository userRepository, RoleRepository roleRepository, TutorialRepository tutorialRepository) { + _mapper = mapper; + _userRepository = userRepository; + _roleRepository = roleRepository; + _tutorialRepository = tutorialRepository; + } + + private static String getFileNameWithTimestamp(String prefix) { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")); + return String.format("%s_%s.json", prefix, timestamp); + } + + private void exportToJson(List> entities, String entitiesName) throws Exception { + + // Create filename with timestamp + String fileNameWithTimestamp = JsonDumpService.getFileNameWithTimestamp(entitiesName); + + // Ensure export directory exists + Files.createDirectories(_root); + + // Write entities to JSON file + Path filePath = _root.resolve(fileNameWithTimestamp); + _mapper.writerWithDefaultPrettyPrinter().writeValue(filePath.toFile(), entities); + + _logger.info("Exported {} to {}", entitiesName, filePath.toAbsolutePath()); + } + + public void exportAllTutorialsToJson() throws Exception { + List tutorials = _tutorialRepository.findAll(); + + List tutorialDtos = tutorials.stream().map( + TutorialDto::fromEntity + ).toList(); + exportToJson(tutorialDtos, "tutorials"); + } + + public void exportAllRolesToJson() throws Exception { + List roles = _roleRepository.findAll(); + + exportToJson(roles, "roles"); + } + + @Transactional + public void exportAllUsersToJson() throws Exception { + List users = _userRepository.findAll(); + + List userDtos = users.stream().map( + user-> UserDto.fromEntity(user) + ).toList(); + + exportToJson(userDtos, "users"); + } + + private Optional getLatestJsonFile(String prefix) throws IOException { + Optional latestFile = Files.list(_root) + .filter(p -> p.getFileName().toString().matches(prefix + "_.*\\.json")) + .max(Comparator.comparing(p -> p.toFile().lastModified())); + return latestFile.isPresent() ? latestFile : Optional.empty(); + } + + public void importRoles() throws Exception { + _logger.info("Importing roles from latest JSON file in {}", _root.toAbsolutePath()); + + Path jsonFile = getLatestJsonFile("roles").get(); + + if (!Files.exists(jsonFile)) { + throw new IllegalArgumentException("Roles JSON file not found: " + jsonFile.toAbsolutePath()); + } + importRoles(jsonFile); + } + + public void importUsers() throws Exception { + _logger.info("Importing users from latest JSON file in {}", _root.toAbsolutePath()); + + Path jsonFile = getLatestJsonFile("users").get(); + + if (!Files.exists(jsonFile)) { + throw new IllegalArgumentException("Users JSON file not found: " + jsonFile.toAbsolutePath()); + } + importUsers(jsonFile); + } + + public void importTutorials() throws Exception { + _logger.info("Importing tutorials from latest JSON file in {}", _root.toAbsolutePath()); + + Path jsonFile = getLatestJsonFile("tutorials").get(); + + if (!Files.exists(jsonFile)) { + throw new IllegalArgumentException("Tutorials JSON file not found: " + jsonFile.toAbsolutePath()); + } + importTutorials(jsonFile); + } + + @Transactional + public void importTutorials(Path jsonFile) throws Exception { + Map usersById = _userRepository.findAll().stream() + .collect(Collectors.toMap(User::getId, u -> u)); + List items = super.readList(jsonFile, new TypeReference>() {}); + + for (TutorialDto dto : items) { + Tutorial t = new Tutorial(); + t.setId(dto.getId()); + t.setTitle(dto.getTitle()); + t.setDescription(dto.getDescription()); + t.setPublished(dto.getPublished()); + t.setTobepublished(dto.isTobepublished()); + t.setCreated(dto.getCreated()); + t.setModified(dto.getModified()); + t.setTitleimage(dto.getTitleimage()); + t.setBody(dto.getBody()); + + if (!usersById.containsKey(dto.getUserID())) { + _logger.warn(String.format("Missing or unknown userId [%s] for tutorial: [%s]", dto.getUserID(), dto.getTitle())); + Optional admin = _userRepository.findAll() + .stream().filter( + user -> user.getRoles() + .stream().anyMatch( + role -> role.getName().equals(ERole.ROLE_ADMIN) + ) + ).findFirst(); + if (admin.isPresent()) { + _logger.info("Assigning tutorial [%s] to admin user [%s]".formatted(dto.getTitle(), admin.get().getUsername())); + t.setUser(admin.get()); + } else { + throw new IllegalArgumentException("Missing user with ROLE_ADMIN to assign tutorial: " + dto.getTitle()); + } + } + User user = usersById.get(dto.getUserID()); + //user = _entityManager.merge(user); // Ensure user is managed by EntityManager + //user.setRoles(null); // because we don't want to load roles to user object + t.setUser(user); + + if(_tutorialRepository.findByUserIdAndTitle(t.getUser().getId(), t.getTitle()).size()> 0) { + _logger.warn("Tutorial with title [{}] already exists for user [{}], SKIPPING import", t.getTitle(), t.getUser().getUsername()); + continue; + } + _tutorialRepository.saveAndFlush(t); + //_tutorialRepository.save(t); + _logger.info("Imported tutorial: {} with user: {}", t.getTitle(), t.getUser().getUsername()); + } + } + + + @Scheduled(cron = "${app.json.dump.schedule.cron:0 0 2 * * SUN}") // Configurable schedule, default: Sunday 2 AM + public void exportEntities() { + try { + _logger.info("--------------Starting weekly entity export...-------------------"); + + exportAllRolesToJson(); + exportAllUsersToJson(); + exportAllTutorialsToJson(); + + _logger.info("--------------Weekly entity export completed successfully.--------"); + + + // Optional: Clean up old exports (configurable) + if (_cleanupEnabled) { + if( _archiveEnabled) { + archiveOldFiles(_retentionWeeks); + } else { + cleanupOldFiles(); + } + + } + + } catch (Exception e) { + _logger.error("Failed to export entities", e); + + } + } + public void cleanupOldFiles() throws IOException { + Path exportDir = _root; + if (!Files.exists(exportDir)) return; + + LocalDateTime cutoffDate = LocalDateTime.now().minusWeeks(_retentionWeeks); + + Files.list(exportDir) + .filter(path -> path.toString().endsWith(".json")) + .filter(path -> { + try { + FileTime lastModified = Files.getLastModifiedTime(path); + return lastModified.toInstant().isBefore(cutoffDate.atZone(ZoneId.systemDefault()).toInstant()); + } catch (IOException e) { + return false; + } + }) + .forEach(path -> { + try { + Files.delete(path); + _logger.info("Deleted old export file: {}", path); + + } catch (IOException e) { + _logger.warn("Failed to delete old export file: {}", path, e); + + } + }); + } + + + public void archiveOldFiles() throws IOException { + this.archiveOldFiles(0); + } + + private void archiveOldFiles(int retentionWeeks) throws IOException { + Path exportDir = _root; + Path archiveDir = _archivePath; + + if (!Files.exists(exportDir)) return; + + // Create archive directory if it doesn't exist + Files.createDirectories(archiveDir); + + LocalDateTime cutoffDate = LocalDateTime.now().minusWeeks(retentionWeeks); + + Files.list(exportDir) + .filter(path -> path.toString().endsWith(".json")) + //.filter(path -> path.getFileName().toString().startsWith("entities_")) + .filter(path -> { + try { + FileTime lastModified = Files.getLastModifiedTime(path); + return lastModified.toInstant().isBefore(cutoffDate.atZone(ZoneId.systemDefault()).toInstant()); + } catch (IOException e) { + return false; + } + }) + .forEach(path -> { + try { + // Create compressed archive + String fileName = path.getFileName().toString(); + String zipFileName = fileName.replace(".json", ".zip"); + Path zipPath = archiveDir.resolve(zipFileName); + + compressFile(path, zipPath); + + // Delete original after successful compression + Files.delete(path); + _logger.info("Archived and deleted old export file: {} -> {}", path, zipPath); + + + } catch (IOException e) { + _logger.error("Failed to archive export file: {}", path, e); + + } + }); + } + + + + private void compressFile(Path sourceFile, Path zipFile) throws IOException { + try (FileOutputStream fos = new FileOutputStream(zipFile.toFile()); + ZipOutputStream zos = new ZipOutputStream(fos); + FileInputStream fis = new FileInputStream(sourceFile.toFile())) { + + ZipEntry zipEntry = new ZipEntry(sourceFile.getFileName().toString()); + zos.putNextEntry(zipEntry); + + byte[] buffer = new byte[1024]; + int length; + while ((length = fis.read(buffer)) > 0) { + zos.write(buffer, 0, length); + } + + zos.closeEntry(); + } + } + + public void extractArchivedFile(String zipFileName) throws IOException { + Path targetDir = _extractedPath; + extractArchivedFile(zipFileName, targetDir); + } + + // Method to extract archived file when needed + public void extractArchivedFile(String zipFileName, Path targetDir) throws IOException { + Path zipPath = _archivePath.resolve(zipFileName); + + if (!Files.exists(zipPath)) { + throw new FileNotFoundException("Archive file not found: " + zipFileName); + } + + Files.createDirectories(targetDir); + + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipPath.toFile()))) { + ZipEntry entry = zis.getNextEntry(); + + while (entry != null) { + Path filePath = targetDir.resolve(entry.getName()); + + try (FileOutputStream fos = new FileOutputStream(filePath.toFile())) { + byte[] buffer = new byte[1024]; + int length; + while ((length = zis.read(buffer)) > 0) { + fos.write(buffer, 0, length); + } + } + + entry = zis.getNextEntry(); + } + } + + _logger.info("Extracted archive {} to {}", zipFileName, targetDir.toAbsolutePath()); + + } + + public Resource getArchivedFileResource(String zipFileName) throws IOException { + Path zipPath = _archivePath.resolve(zipFileName); + if (!Files.exists(zipPath)) { + throw new FileNotFoundException("Archive file not found: " + zipFileName); + } + return new org.springframework.core.io.FileSystemResource(zipPath.toFile()); + } + + public List> getJsonDumpInfo() throws IOException { + List> infoList = new ArrayList<>(); + + if (!Files.exists(_root)) { + Map info = new HashMap<>(); + info.put("success", true); + info.put("files", List.of()); + info.put("count", 0); + infoList.add(info); + return infoList; + } + + // Get all JSON files in the root directory + Files.list(_root) + .filter(path -> path.toString().endsWith(".json")) + .forEach(path -> { + Map info = new HashMap<>(); + info.put("fileName", path.getFileName().toString()); + try { + info.put("lastModified", Files.getLastModifiedTime(path).toInstant().toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { + info.put("size", Files.size(path)); + } catch (IOException e) { + throw new RuntimeException(e); + } + infoList.add(info); + }); + + return infoList; + } + + public Resource getJsonDumpResource(String fileName) throws IOException { + Path filePath = _root.resolve(fileName); + if (!Files.exists(filePath)) { + throw new FileNotFoundException("JSON dump file not found: " + fileName); + } + return new org.springframework.core.io.FileSystemResource(filePath.toFile()); + } + + // Method to list archived files + public List getArchivedFiles() throws IOException { + Path archiveDir = _archivePath; + + if (!Files.exists(archiveDir)) { + return new ArrayList<>(); + } + + return Files.list(archiveDir) + .filter(path -> path.toString().endsWith(".zip")) + .map(path -> path.getFileName().toString()) + .sorted() + .collect(Collectors.toList()); + } + + public Map deleteJsonDumpFile(String fileName) throws IOException { + Path filePath = _root.resolve(fileName); + Map response = new HashMap<>(); + + if (!Files.exists(filePath)) { + response.put("success", false); + response.put("message", "File not found: " + fileName); + return response; + } + + Files.delete(filePath); + response.put("success", true); + response.put("message", "File deleted successfully: " + fileName); + return response; + } +} diff --git a/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpServiceConfiguration.java b/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpServiceConfiguration.java new file mode 100644 index 0000000..4863004 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpServiceConfiguration.java @@ -0,0 +1,34 @@ +package com.jambotronGroup.jambotron.jsonDump; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JsonDumpServiceConfiguration { + + @Autowired + private JsonDumpService _jsonDumpService; + + @PostConstruct + public void onInit(){ + // This method will be called after the Spring context is initialized + // and will trigger the export of users and roles to JSON files. +// try{ +// _jsonDumpService.exportAllRolesToJson(); +// _jsonDumpService.exportAllUsersToJson(); +// _jsonDumpService.exportAllTutorialsToJson(); +// } catch (Exception e) { +// // Handle any exceptions that may occur during the export process +// e.printStackTrace(); +// } +// try { +// _jsonDumpService.importRoles(); +// _jsonDumpService.importUsers(); +// _jsonDumpService.importTutorials(); +// }catch (Exception e) { +// // Handle any exceptions that may occur during the import process +// e.printStackTrace(); +// } + } +} diff --git a/src/main/java/com/jambotronGroup/jambotron/model/Role.java b/src/main/java/com/jambotronGroup/jambotron/model/Role.java index 0505277..a1d601b 100644 --- a/src/main/java/com/jambotronGroup/jambotron/model/Role.java +++ b/src/main/java/com/jambotronGroup/jambotron/model/Role.java @@ -9,7 +9,7 @@ import jakarta.persistence.*; public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; + private Long id; @Enumerated(EnumType.STRING) @Column(length = 20) @@ -23,11 +23,11 @@ public class Role { this.name = name; } - public Integer getId() { + public Long getId() { return id; } - public void setId(Integer id) { + public void setId(Long id) { this.id = id; } diff --git a/src/main/java/com/jambotronGroup/jambotron/model/Tutorial.java b/src/main/java/com/jambotronGroup/jambotron/model/Tutorial.java index d969305..4fed431 100644 --- a/src/main/java/com/jambotronGroup/jambotron/model/Tutorial.java +++ b/src/main/java/com/jambotronGroup/jambotron/model/Tutorial.java @@ -13,7 +13,7 @@ public class Tutorial { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; + private Long id; @Column(name = "title") private String title; @@ -27,7 +27,7 @@ public class Tutorial { @Column(name = "tobepublished") private boolean tobepublished; - @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + @ManyToOne(fetch = FetchType.EAGER)//, cascade = CascadeType.PERSIST) @JoinColumn(name = "userID", nullable = false) private User user; @@ -59,10 +59,14 @@ public class Tutorial { this.body = body; } - public long getId() { + public Long getId() { return id; } + public void setId(Long id) { + this.id = id; + } + public String getTitle() { return title; } @@ -115,6 +119,10 @@ public class Tutorial { return user; } + public void setUser(User user) { + this.user = user; + } + @Override public String toString() { return "Tutorial [id=" + id + ", title=" + title + ", desc=" + description + ", published=" + published + "]"; diff --git a/src/main/java/com/jambotronGroup/jambotron/model/User.java b/src/main/java/com/jambotronGroup/jambotron/model/User.java index ea22f3c..87defb7 100644 --- a/src/main/java/com/jambotronGroup/jambotron/model/User.java +++ b/src/main/java/com/jambotronGroup/jambotron/model/User.java @@ -20,7 +20,7 @@ public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") - private long id; + private Long id; @Column(name = "username") @NotBlank diff --git a/src/main/java/com/jambotronGroup/jambotron/repository/RoleRepository.java b/src/main/java/com/jambotronGroup/jambotron/repository/RoleRepository.java index 44d4bd5..260230c 100644 --- a/src/main/java/com/jambotronGroup/jambotron/repository/RoleRepository.java +++ b/src/main/java/com/jambotronGroup/jambotron/repository/RoleRepository.java @@ -11,4 +11,7 @@ import java.util.Optional; @Repository public interface RoleRepository extends JpaRepository { Optional findByName(ERole name); + + boolean existsByName(ERole name); } + diff --git a/src/main/java/com/jambotronGroup/jambotron/security/WebSecurityConfig.java b/src/main/java/com/jambotronGroup/jambotron/security/WebSecurityConfig.java index 08bda7f..985aa58 100644 --- a/src/main/java/com/jambotronGroup/jambotron/security/WebSecurityConfig.java +++ b/src/main/java/com/jambotronGroup/jambotron/security/WebSecurityConfig.java @@ -1,5 +1,6 @@ package com.jambotronGroup.jambotron.security; +import com.jambotronGroup.jambotron.initData.InitDataService; import com.jambotronGroup.jambotron.security.jwt.AuthEntryPointJwt; import com.jambotronGroup.jambotron.security.jwt.AuthTokenFilter; import com.jambotronGroup.jambotron.security.services.UserDetailsServiceImpl; @@ -51,7 +52,6 @@ public class WebSecurityConfig implements WebMvcConfigurer {// extends WebSecuri private AuthEntryPointJwt unauthorizedHandler; - @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); @@ -121,8 +121,11 @@ public class WebSecurityConfig implements WebMvcConfigurer {// extends WebSecuri // Access permitted for specific roles .requestMatchers("/api/user/**").hasRole("USER") .requestMatchers("/api/moderator/**").hasRole("MODERATOR") - .requestMatchers("/api/admin/**").hasRole("ADMIN") + //.requestMatchers("/api/admin/**").hasRole("ADMIN") + .requestMatchers("/api/admin/**").permitAll() + .requestMatchers("/api/admin/json-dump/**").permitAll() + .requestMatchers("/api/admin/json-dump/import/**").permitAll() .anyRequest().authenticated() ); diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 479a5d5..1ab7f5a 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -16,6 +16,18 @@ spring.flyway.url=jdbc:postgresql://localhost:5433/jambotronDB spring.flyway.user=admin spring.flyway.password=postgrespw +#spring.datasource.url= jdbc:postgresql://localhost:5433/db_for_import +#spring.datasource.username= admin +#spring.datasource.password= postgrespw +# +#spring.flyway.enabled=false +##spring.flyway.baseline-on-migrate=true +##spring.flyway.validate-on-migrate=true +## +##spring.flyway.url=jdbc:postgresql://localhost:5433/db_for_import +##spring.flyway.user=admin +##spring.flyway.password=postgrespw + #============jpa===================== spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect diff --git a/src/main/resources/db/migration/V10__refresh_token.sql b/src/main/resources/db/migration/V10__refresh_token.sql deleted file mode 100644 index 440b8ba..0000000 --- a/src/main/resources/db/migration/V10__refresh_token.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE IF NOT EXISTS public.refreshtoken -( - id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), - user_id bigint NOT NULL, - token character varying COLLATE pg_catalog."default" NOT NULL, - expiry_date timestamp with time zone, - CONSTRAINT refreshtoken_pkey PRIMARY KEY (id), - CONSTRAINT "FK_refreshtoken_users" FOREIGN KEY (user_id) - REFERENCES public.users (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION -); diff --git a/src/main/resources/db/migration/V1__Init.sql b/src/main/resources/db/migration/V1__Init.sql index 9132d24..b179350 100644 --- a/src/main/resources/db/migration/V1__Init.sql +++ b/src/main/resources/db/migration/V1__Init.sql @@ -1,9 +1,81 @@ +-- This script was generated by the ERD tool in pgAdmin 4. +-- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps. +BEGIN; - - -CREATE TABLE IF NOT EXISTS roles +CREATE TABLE IF NOT EXISTS public.refreshtoken ( - id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - name character varying(20) - + id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), + user_id bigint NOT NULL, + token character varying COLLATE pg_catalog."default" NOT NULL, + expiry_date timestamp with time zone, + CONSTRAINT refreshtoken_pkey PRIMARY KEY (id) ); + +CREATE TABLE IF NOT EXISTS public.roles +( + id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), + name character varying(20) COLLATE pg_catalog."default", + CONSTRAINT roles_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS public.tutorials +( + id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), + published boolean, + tobepublished boolean, + title character varying(255) COLLATE pg_catalog."default", + userid bigint, + created timestamp with time zone, + modified timestamp with time zone, + description character varying(255) COLLATE pg_catalog."default", + titleimage text COLLATE pg_catalog."default", + body text COLLATE pg_catalog."default", + CONSTRAINT tutorials_title_key UNIQUE (title) +); + +CREATE TABLE IF NOT EXISTS public.user_roles +( + user_id bigint NOT NULL, + role_id integer NOT NULL, + CONSTRAINT user_roles_pkey PRIMARY KEY (user_id, role_id) +); + +CREATE TABLE IF NOT EXISTS public.users +( + id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), + email character varying(50) COLLATE pg_catalog."default", + password character varying(120) COLLATE pg_catalog."default", + username character varying(20) COLLATE pg_catalog."default", + CONSTRAINT users_pkey PRIMARY KEY (id), + CONSTRAINT uk6dotkott2kjsp8vw4d0m25fb7 UNIQUE (email), + CONSTRAINT ukr43af9ap4edm43mmtq01oddj6 UNIQUE (username) +); + +ALTER TABLE IF EXISTS public.refreshtoken + ADD CONSTRAINT "FK_refreshtoken_users" FOREIGN KEY (user_id) + REFERENCES public.users (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + + +ALTER TABLE IF EXISTS public.tutorials + ADD CONSTRAINT "tutorial_user_FK" FOREIGN KEY (userid) + REFERENCES public.users (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + + +ALTER TABLE IF EXISTS public.user_roles + ADD CONSTRAINT fkh8ciramu9cc9q3qcqiv4ue8a6 FOREIGN KEY (role_id) + REFERENCES public.roles (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + + +ALTER TABLE IF EXISTS public.user_roles + ADD CONSTRAINT fkhfh9dx7w3ubf1co1vdev94g3f FOREIGN KEY (user_id) + REFERENCES public.users (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +END; \ No newline at end of file diff --git a/src/main/resources/db/migration/V2__init.sql b/src/main/resources/db/migration/V2__init.sql deleted file mode 100644 index 4a88861..0000000 --- a/src/main/resources/db/migration/V2__init.sql +++ /dev/null @@ -1,14 +0,0 @@ - - -CREATE TABLE IF NOT EXISTS users -( - id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - email character varying(50) COLLATE pg_catalog."default", - password character varying(120) COLLATE pg_catalog."default", - username character varying(20) COLLATE pg_catalog."default", - - CONSTRAINT uk6dotkott2kjsp8vw4d0m25fb7 UNIQUE (email), - CONSTRAINT ukr43af9ap4edm43mmtq01oddj6 UNIQUE (username) -); - - diff --git a/src/main/resources/db/migration/V3__init.sql b/src/main/resources/db/migration/V3__init.sql deleted file mode 100644 index a4adedb..0000000 --- a/src/main/resources/db/migration/V3__init.sql +++ /dev/null @@ -1,25 +0,0 @@ - -CREATE TABLE IF NOT EXISTS public.user_roles -( - user_id bigint NOT NULL, - role_id integer NOT NULL, - CONSTRAINT user_roles_pkey PRIMARY KEY (user_id, role_id), - CONSTRAINT fkh8ciramu9cc9q3qcqiv4ue8a6 FOREIGN KEY (role_id) - REFERENCES public.roles (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION, - CONSTRAINT fkhfh9dx7w3ubf1co1vdev94g3f FOREIGN KEY (user_id) - REFERENCES public.users (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION -); - - -CREATE TABLE IF NOT EXISTS public.tutorials -( - id bigint NOT NULL, - description character varying(255) COLLATE pg_catalog."default", - published boolean, - title character varying(255) COLLATE pg_catalog."default", - CONSTRAINT tutorials_pkey PRIMARY KEY (id) -); \ No newline at end of file diff --git a/src/main/resources/db/migration/V4__Init_data.sql b/src/main/resources/db/migration/V4__Init_data.sql deleted file mode 100644 index b2f1629..0000000 --- a/src/main/resources/db/migration/V4__Init_data.sql +++ /dev/null @@ -1,20 +0,0 @@ - -INSERT INTO roles (id, name) OVERRIDING SYSTEM VALUE VALUES (1, 'ROLE_USER'); -INSERT INTO roles (id, name) OVERRIDING SYSTEM VALUE VALUES (2, 'ROLE_MODERATOR'); -INSERT INTO roles (id, name) OVERRIDING SYSTEM VALUE VALUES (3, 'ROLE_ADMIN'); - - -INSERT INTO users (id, email, password, username) -OVERRIDING SYSTEM VALUE -VALUES (1, 'liosha84@gmail.com', '$2a$10$qyoKXYSukha6XCjorTzoweF4Os1pwmwyzbaSsb3RCVB0LK6WLQKPC', 'Admin'); -INSERT INTO users (id, email, password, username) -OVERRIDING SYSTEM VALUE -VALUES (2, 'Moderator@gmail.com', '$2a$10$zLo5th8Xbfq.MM7y/dCRQu3Ud7HHyAwm.7.yS08ytJtkHMKrbOJlu', 'Moderator'); -INSERT INTO users (id, email, password, username) -OVERRIDING SYSTEM VALUE -VALUES (3, 'user@gmail.com', '$2a$10$2EcwYffteBF3GhVaf5qPB.I7XiHepDEauU5D4fx9fpBXMTZI/QnnC', 'User'); - - -INSERT INTO user_roles (user_id, role_id) OVERRIDING SYSTEM VALUE VALUES (3, 1); -INSERT INTO user_roles (user_id, role_id) OVERRIDING SYSTEM VALUE VALUES (1, 3); -INSERT INTO user_roles (user_id, role_id) OVERRIDING SYSTEM VALUE VALUES (2, 2); \ No newline at end of file diff --git a/src/main/resources/db/migration/V5__Fixes.sql b/src/main/resources/db/migration/V5__Fixes.sql deleted file mode 100644 index 8c1492e..0000000 --- a/src/main/resources/db/migration/V5__Fixes.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE IF EXISTS public.tutorials - ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY; \ No newline at end of file diff --git a/src/main/resources/db/migration/V6__fix_tutorial_id.sql b/src/main/resources/db/migration/V6__fix_tutorial_id.sql deleted file mode 100644 index 108fee1..0000000 --- a/src/main/resources/db/migration/V6__fix_tutorial_id.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Column: public.tutorials. - - ALTER TABLE IF EXISTS public.tutorials DROP COLUMN IF EXISTS id; - -ALTER TABLE IF EXISTS public.tutorials - ADD COLUMN id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ); \ No newline at end of file diff --git a/src/main/resources/db/migration/V7__add_column_userid_to_tutorials.sql b/src/main/resources/db/migration/V7__add_column_userid_to_tutorials.sql deleted file mode 100644 index e579d78..0000000 --- a/src/main/resources/db/migration/V7__add_column_userid_to_tutorials.sql +++ /dev/null @@ -1,27 +0,0 @@ -DROP TABLE IF EXISTS public.tutorials; - -CREATE TABLE IF NOT EXISTS public.tutorials -( - description character varying(255) COLLATE pg_catalog."default", - published boolean, - title character varying(255) COLLATE pg_catalog."default", - id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), - userid bigint, - CONSTRAINT "tutorial_user_FK" FOREIGN KEY (userid) - REFERENCES public.users (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION - NOT VALID -) - -TABLESPACE pg_default; - - --- Index: fki_tutorial_user_FK - --- DROP INDEX IF EXISTS public."fki_tutorial_user_FK"; - -CREATE INDEX IF NOT EXISTS "fki_tutorial_user_FK" - ON public.tutorials USING btree - (userid ASC NULLS LAST) - TABLESPACE pg_default; \ No newline at end of file diff --git a/src/main/resources/db/migration/V8__tutorial_remake.sql b/src/main/resources/db/migration/V8__tutorial_remake.sql deleted file mode 100644 index 7d58047..0000000 --- a/src/main/resources/db/migration/V8__tutorial_remake.sql +++ /dev/null @@ -1,19 +0,0 @@ -DROP TABLE tutorials; - -CREATE TABLE tutorials -( - id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), - published boolean, - tobepublished boolean, - title character varying(255) COLLATE pg_catalog."default", - userid bigint, - created timestamp with time zone, - modified timestamp with time zone, - description character varying(255) COLLATE pg_catalog."default", - CONSTRAINT tutorials_title_key UNIQUE (title), - CONSTRAINT "tutorial_user_FK" FOREIGN KEY (userid) - REFERENCES public.users (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION -) - diff --git a/src/main/resources/db/migration/V9__title_image.sql b/src/main/resources/db/migration/V9__title_image.sql deleted file mode 100644 index 862d17f..0000000 --- a/src/main/resources/db/migration/V9__title_image.sql +++ /dev/null @@ -1,9 +0,0 @@ -ALTER TABLE IF EXISTS public.tutorials DROP COLUMN IF EXISTS titleimage; - -ALTER TABLE IF EXISTS public.tutorials - ADD COLUMN titleimage text COLLATE pg_catalog."default"; - -ALTER TABLE IF EXISTS public.tutorials DROP COLUMN IF EXISTS body; - -ALTER TABLE IF EXISTS public.tutorials - ADD COLUMN body text COLLATE pg_catalog."default"; \ No newline at end of file diff --git a/src/main/resources/import/roles.json b/src/main/resources/import/roles.json new file mode 100644 index 0000000..412c284 --- /dev/null +++ b/src/main/resources/import/roles.json @@ -0,0 +1,10 @@ +[ { + "id" : 1, + "name" : "ROLE_USER" +}, { + "id" : 2, + "name" : "ROLE_MODERATOR" +}, { + "id" : 3, + "name" : "ROLE_ADMIN" +} ] \ No newline at end of file diff --git a/src/main/resources/import/users.json b/src/main/resources/import/users.json new file mode 100644 index 0000000..683ba55 --- /dev/null +++ b/src/main/resources/import/users.json @@ -0,0 +1,34 @@ +[ { + "id" : 1, + "username" : "Admin", + "email" : "liosha84@gmail.com", + "password" : "$2a$10$qyoKXYSukha6XCjorTzoweF4Os1pwmwyzbaSsb3RCVB0LK6WLQKPC", + "roles" : [ { + "id" : 1, + "name" : "ROLE_USER" + }, { + "id" : 2, + "name" : "ROLE_MODERATOR" + }, { + "id" : 3, + "name" : "ROLE_ADMIN" + } ] +}, { + "id" : 2, + "username" : "Moderator", + "email" : "Moderator@gmail.com", + "password" : "$2a$10$zLo5th8Xbfq.MM7y/dCRQu3Ud7HHyAwm.7.yS08ytJtkHMKrbOJlu", + "roles" : [ { + "id" : 2, + "name" : "ROLE_MODERATOR" + } ] +}, { + "id" : 3, + "username" : "User", + "email" : "user@gmail.com", + "password" : "$2a$10$2EcwYffteBF3GhVaf5qPB.I7XiHepDEauU5D4fx9fpBXMTZI/QnnC", + "roles" : [ { + "id" : 1, + "name" : "ROLE_USER" + } ] +} ] \ No newline at end of file From 0885a460c4506d3a37fc82ae79b32b3340d170f5 Mon Sep 17 00:00:00 2001 From: liosha84 <138026690+liosha84@users.noreply.github.com> Date: Fri, 15 Aug 2025 02:14:54 +0300 Subject: [PATCH 2/3] remake init data json dump Small bug fix --- .../src/app/services/spinner.service.ts | 4 +- .../controllers/JsonDumpController.java | 15 ++-- .../initData/InitDataConfiguration.java | 2 +- .../jambotron/initData/InitDataService.java | 77 ++++++++++++++++--- .../jambotron/jsonDump/JsonDumpService.java | 15 ++-- .../jambotron/security/WebSecurityConfig.java | 1 - .../resources/{import => initData}/roles.json | 0 .../resources/{import => initData}/users.json | 0 8 files changed, 80 insertions(+), 34 deletions(-) rename src/main/resources/{import => initData}/roles.json (100%) rename src/main/resources/{import => initData}/users.json (100%) diff --git a/jambotron-ui/src/app/services/spinner.service.ts b/jambotron-ui/src/app/services/spinner.service.ts index 5c7aef5..c4fa063 100644 --- a/jambotron-ui/src/app/services/spinner.service.ts +++ b/jambotron-ui/src/app/services/spinner.service.ts @@ -10,10 +10,10 @@ export class SpinnerService { } show() { - this.visibility.next(false); + this.visibility.next(true); } hide() { - this.visibility.next(true); + this.visibility.next(false); } } diff --git a/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java b/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java index 3d75495..6dab15f 100644 --- a/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java +++ b/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java @@ -4,7 +4,6 @@ import com.jambotronGroup.jambotron.jsonDump.JsonDumpService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -17,15 +16,11 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; - -import static java.util.stream.Collectors.toList; @RestController @RequestMapping("/api/admin/json-dump") @@ -79,27 +74,27 @@ public class JsonDumpController { // ==================== IMPORT ENDPOINTS ==================== - @PostMapping("/import/users") + @PostMapping("/initData/users") public ResponseEntity> importUsers() { return performImport(() -> _jsonDumpService.importUsers(), "users"); } - @PostMapping("/import/tutorials") + @PostMapping("/initData/tutorials") public ResponseEntity> importTutorials() { return performImport(() -> _jsonDumpService.importTutorials(), "tutorials"); } - @PostMapping("/import/roles") + @PostMapping("/initData/roles") public ResponseEntity> importRoles() { return performImport(() -> _jsonDumpService.importRoles(), "roles"); } - @PostMapping("/import/users/file") + @PostMapping("/initData/users/file") public ResponseEntity> importUsersFromFile(@RequestParam("file") MultipartFile file) { return importFromFile(file, (path) -> _jsonDumpService.importUsers(path), "users"); } - @PostMapping("/import/tutorials/file") + @PostMapping("/initData/tutorials/file") public ResponseEntity> importTutorialsFromFile(@RequestParam("file") MultipartFile file) { return importFromFile(file, (path) -> _jsonDumpService.importTutorials(path), "tutorials"); } diff --git a/src/main/java/com/jambotronGroup/jambotron/initData/InitDataConfiguration.java b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataConfiguration.java index 93d2fb1..1c6b836 100644 --- a/src/main/java/com/jambotronGroup/jambotron/initData/InitDataConfiguration.java +++ b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataConfiguration.java @@ -15,7 +15,7 @@ public class InitDataConfiguration { @PostConstruct public void init() { - _initDataService.importData(); + _initDataService.initData(); } } diff --git a/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java index 73ee140..4f8ac94 100644 --- a/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java +++ b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java @@ -13,11 +13,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; @@ -32,7 +34,7 @@ public class InitDataService { private static final Logger _logger = LoggerFactory.getLogger(InitDataService.class); - private ObjectMapper _mapper; + protected ObjectMapper _mapper; @Autowired private ResourceLoader _resources; @@ -42,8 +44,53 @@ public class InitDataService { @Autowired private UserRepository _userRepository; + + private final Path _initDataPath = Path.of("/jambotron-data/initData/"); + public InitDataService(){ + _mapper = new ObjectMapper(); + + + } + + private void extractInitData(){ + _logger.info("Extracting init data to {}", _initDataPath.toAbsolutePath()); + if(!_initDataPath.toFile().exists()) { + _logger.info("Creating init data directory at {}", _initDataPath.toAbsolutePath()); + try { + Files.createDirectories(_initDataPath); + } catch (Exception e) { + _logger.error("Failed to create init data directory", e); + } + } + extractJsonFile("roles.json"); + extractJsonFile("users.json"); + } + + private void extractJsonFile(String fileName) { + try { + ClassPathResource resource = new ClassPathResource("initData/"+ fileName); + + + Path targetPath = _initDataPath.resolve(fileName); + if (!Files.exists(targetPath)) { + try (InputStream inputStream = resource.getInputStream()) { + Files.copy(resource.getInputStream(), targetPath); + } + + _logger.info("Copied {} to {}", fileName, targetPath.toAbsolutePath()); + } else { + try (InputStream inputStream = resource.getInputStream()) { + Files.copy(resource.getInputStream(), targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + + _logger.warn("{} already exists, REPLACE", targetPath.toAbsolutePath()); + } + + } catch (Exception e) { + _logger.error("Failed to extract JSON file: {}", fileName, e); + } } @Transactional @@ -122,23 +169,31 @@ public class InitDataService { return _mapper.readValue(json, type); } - public void importData() { - try { - Resource resource = _resources.getResource("classpath:import/roles.json"); - if (resource.exists()) { + public void initData() { - importRoles(resource.getFile().toPath()); + extractInitData(); + + _logger.info("Initializing data import from {}", _initDataPath.toAbsolutePath()); + + try { + + Path rolesFile = _initDataPath.resolve("roles.json"); + + if (rolesFile.toFile().exists()) { + + importRoles(rolesFile); } else { - _logger.warn("Roles import file not found: {}", resource.getFilename()); + _logger.warn("Roles import file not found: {}", rolesFile.getFileName()); } - resource = _resources.getResource("classpath:import/users.json"); - if (resource.exists()) { - importUsers(resource.getFile().toPath()); + Path usersFile = _initDataPath.resolve("users.json"); + + if (usersFile.toFile().exists()) { + importUsers(usersFile); } else { - _logger.warn("Users import file not found: {}", resource.getFilename()); + _logger.warn("Users import file not found: {}", usersFile.getFileName()); } } catch (Exception e) { _logger.error("Error importing data", e); diff --git a/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java b/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java index 0c39dbf..58fcc55 100644 --- a/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java +++ b/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java @@ -50,16 +50,16 @@ public class JsonDumpService extends InitDataService { private final Path _extractedPath = _root.resolve("extracted"); - private final ObjectMapper _mapper; + //private final ObjectMapper _mapper; @Autowired - private final UserRepository _userRepository; + private UserRepository _userRepository; @Autowired - private final RoleRepository _roleRepository; + private RoleRepository _roleRepository; @Autowired - private final TutorialRepository _tutorialRepository; + private TutorialRepository _tutorialRepository; @Value("${app.json.dump.cleanup.enabled:true}") private boolean _cleanupEnabled; @@ -70,11 +70,8 @@ public class JsonDumpService extends InitDataService { @Value("${app.json.dump.archive.enabled:true}") private boolean _archiveEnabled; - public JsonDumpService(ObjectMapper mapper, UserRepository userRepository, RoleRepository roleRepository, TutorialRepository tutorialRepository) { - _mapper = mapper; - _userRepository = userRepository; - _roleRepository = roleRepository; - _tutorialRepository = tutorialRepository; + public JsonDumpService() { + } private static String getFileNameWithTimestamp(String prefix) { diff --git a/src/main/java/com/jambotronGroup/jambotron/security/WebSecurityConfig.java b/src/main/java/com/jambotronGroup/jambotron/security/WebSecurityConfig.java index 985aa58..11de5f7 100644 --- a/src/main/java/com/jambotronGroup/jambotron/security/WebSecurityConfig.java +++ b/src/main/java/com/jambotronGroup/jambotron/security/WebSecurityConfig.java @@ -1,6 +1,5 @@ package com.jambotronGroup.jambotron.security; -import com.jambotronGroup.jambotron.initData.InitDataService; import com.jambotronGroup.jambotron.security.jwt.AuthEntryPointJwt; import com.jambotronGroup.jambotron.security.jwt.AuthTokenFilter; import com.jambotronGroup.jambotron.security.services.UserDetailsServiceImpl; diff --git a/src/main/resources/import/roles.json b/src/main/resources/initData/roles.json similarity index 100% rename from src/main/resources/import/roles.json rename to src/main/resources/initData/roles.json diff --git a/src/main/resources/import/users.json b/src/main/resources/initData/users.json similarity index 100% rename from src/main/resources/import/users.json rename to src/main/resources/initData/users.json From a05e9a31fcece79e3b2cd4815bada295c2233fef Mon Sep 17 00:00:00 2001 From: liosha84 <138026690+liosha84@users.noreply.github.com> Date: Mon, 18 Aug 2025 12:26:34 +0300 Subject: [PATCH 3/3] remake init data json dump Small bug fix --- build.gradle | 5 ++ .../json-dump.component.html | 35 +++++++++++-- .../json-dump.component.ts | 15 +++++- .../modules/admin-module/json-dump.service.ts | 2 +- jambotron-ui/src/styles.scss | 29 +++++++---- .../controllers/JsonDumpController.java | 16 +++--- .../jambotron/initData/InitDataService.java | 51 +++++++++---------- .../jambotron/jsonDump/JsonDumpService.java | 21 +++++--- .../repository/TutorialRepository.java | 2 + 9 files changed, 120 insertions(+), 56 deletions(-) diff --git a/build.gradle b/build.gradle index abbfed1..b5d91fe 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,10 @@ dependencies { implementation 'org.springframework.ai:spring-ai-zhipuai-spring-boot-starter:1.0.0-M6' implementation 'org.springframework:spring-mock:2.0.8' + + implementation 'com.google.guava:guava:33.4.8-jre' + implementation 'io.micrometer:micrometer-core:1.12.0' + implementation 'org.springframework.boot:spring-boot-starter-actuator' } apply plugin: 'io.spring.dependency-management' @@ -49,6 +53,7 @@ apply plugin: 'io.spring.dependency-management' apply plugin: 'java' + tasks.register("bootRun_Dev") { group = "_jambotron_build" description = "Runs the Spring Boot application with the dev profile" diff --git a/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.html b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.html index 86111ea..ffa49c7 100644 --- a/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.html +++ b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.html @@ -2,7 +2,7 @@ JSON Dump Management - + + + @for (entity of entityTypes; track entity) { + + {{ entity | titlecase }} + + download + + + restore_from_trash + + + upload + + + + } + Available JSON Files diff --git a/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.ts b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.ts index 11a1d0c..0cce18d 100644 --- a/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.ts +++ b/jambotron-ui/src/app/modules/admin-module/components/json-dump.component/json-dump.component.ts @@ -17,6 +17,8 @@ import {MatProgressSpinner} from '@angular/material/progress-spinner'; import {MatIcon} from '@angular/material/icon'; import {MatTooltip} from '@angular/material/tooltip'; import {ArchivesResponse, FileInfo, FilesResponse, JsonDumpService} from '../../json-dump.service'; +import {MatToolbar, MatToolbarRow} from '@angular/material/toolbar'; +import {MatLabel} from '@angular/material/input'; @Component({ selector: 'app-json-dump', @@ -40,7 +42,10 @@ import {ArchivesResponse, FileInfo, FilesResponse, JsonDumpService} from '../../ MatTable, TitleCasePipe, NgIf, - NgForOf + NgForOf, + MatToolbar, + MatToolbarRow, + MatLabel ], styleUrls: ['./json-dump.component.scss'], schemas:[CUSTOM_ELEMENTS_SCHEMA] @@ -99,7 +104,9 @@ export class JsonDumpComponent implements OnInit { importEntity(entity: string, file: File): void { this.jsonDumpService.importEntity(entity, file).subscribe({ - next: () => this.showMessage(`Imported ${entity} successfully`), + next: () => { + this.showMessage(`Imported ${entity} successfully`) + }, error: (err: { message: any; }) => this.showMessage(`Import ${entity} failed: ${err.message}`) }); } @@ -132,4 +139,8 @@ export class JsonDumpComponent implements OnInit { private showMessage(msg: string): void { this.snackBar.open(msg, 'Close', { duration: 3000 }); } + + restore(fileName: string) { + + } } diff --git a/jambotron-ui/src/app/modules/admin-module/json-dump.service.ts b/jambotron-ui/src/app/modules/admin-module/json-dump.service.ts index 10db41f..82cbaec 100644 --- a/jambotron-ui/src/app/modules/admin-module/json-dump.service.ts +++ b/jambotron-ui/src/app/modules/admin-module/json-dump.service.ts @@ -58,7 +58,7 @@ export class JsonDumpService { importEntity(entity: string, file: File) { const formData = new FormData(); formData.append('file', file); - return this.http.post(`${this.baseUrl}/import/${entity}`, formData); + return this.http.post(`${this.baseUrl}/import/${entity}/file`, formData); } // ==================== EXPORT METHODS ==================== diff --git a/jambotron-ui/src/styles.scss b/jambotron-ui/src/styles.scss index 32972ed..79ff90e 100644 --- a/jambotron-ui/src/styles.scss +++ b/jambotron-ui/src/styles.scss @@ -1,4 +1,4 @@ -@use '@angular/material' as mat; +@use 'node_modules/@angular/material' as mat; //markdown styles @import 'bootstrap/dist/css/bootstrap.min.css'; @@ -58,11 +58,22 @@ body { background: #fafafbb3; } -html { - color-scheme: light dark; - @include mat.theme(( - color: mat.$azure-palette, - typography: Jersey 20 Charted, - density: 0 - )); -} +//// Include the common styles for Angular Material +//@include mat.core(); +// +//// Define your custom palette +//$my-primary: mat.define-palette(mat.$blue-palette, 700); +//$my-accent: mat.define-palette(mat.$orange-palette, A200, A100, A400); +//$my-warn: mat.define-palette(mat.$red-palette); +// +//// Create the theme +//$my-theme: mat.define-light-theme(( +// color: ( +// primary: $my-primary, +// accent: $my-accent, +// warn: $my-warn, +// ) +//)); +// +//// Include theme styles for core and each component used in your app +//@include mat.all-component-themes($my-theme); diff --git a/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java b/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java index 6dab15f..6e4855b 100644 --- a/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java +++ b/src/main/java/com/jambotronGroup/jambotron/controllers/JsonDumpController.java @@ -74,27 +74,27 @@ public class JsonDumpController { // ==================== IMPORT ENDPOINTS ==================== - @PostMapping("/initData/users") - public ResponseEntity> importUsers() { + @PostMapping("/restore/users") + public ResponseEntity> restoreUsers() { return performImport(() -> _jsonDumpService.importUsers(), "users"); } - @PostMapping("/initData/tutorials") - public ResponseEntity> importTutorials() { + @PostMapping("/restore/tutorials") + public ResponseEntity> restoreTutorials() { return performImport(() -> _jsonDumpService.importTutorials(), "tutorials"); } - @PostMapping("/initData/roles") - public ResponseEntity> importRoles() { + @PostMapping("/restore/roles") + public ResponseEntity> restoreRoles() { return performImport(() -> _jsonDumpService.importRoles(), "roles"); } - @PostMapping("/initData/users/file") + @PostMapping("/import/users/file") public ResponseEntity> importUsersFromFile(@RequestParam("file") MultipartFile file) { return importFromFile(file, (path) -> _jsonDumpService.importUsers(path), "users"); } - @PostMapping("/initData/tutorials/file") + @PostMapping("/import/tutorials/file") public ResponseEntity> importTutorialsFromFile(@RequestParam("file") MultipartFile file) { return importFromFile(file, (path) -> _jsonDumpService.importTutorials(path), "tutorials"); } diff --git a/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java index 4f8ac94..0cdc641 100644 --- a/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java +++ b/src/main/java/com/jambotronGroup/jambotron/initData/InitDataService.java @@ -124,42 +124,41 @@ public class InitDataService { List items = readList(jsonFile, new TypeReference>() {}); for (UserDto dto : items) { - User user = new User(); - if (dto.getId() != null) user.setId(dto.getId()); - user.setUsername(dto.getUsername()); - user.setEmail(dto.getEmail()); - // Expecting already-hashed values; do not import plain text secrets - user.setPassword(dto.getPassword()); + if(!_userRepository.existsByUsername(dto.getUsername())) { - // Resolve roles by enum name - Set roles = new HashSet<>(); - if (dto.getRoles() != null) { - for (RoleDto roleDto : dto.getRoles()) { - ERole erole = ERole.valueOf(roleDto.getName().name()); - Role role = rolesByName.get(erole); - if (role == null) { - // Optionally create missing roles on the fly - role = new Role(); - role.setName(erole); - role = _roleRepository.save(role); - rolesByName.put(erole, role); + User user = new User(); + if (dto.getId() != null) user.setId(dto.getId()); + user.setUsername(dto.getUsername()); + user.setEmail(dto.getEmail()); + // Expecting already-hashed values; do not import plain text secrets + user.setPassword(dto.getPassword()); + + // Resolve roles by enum name + Set roles = new HashSet<>(); + if (dto.getRoles() != null) { + for (RoleDto roleDto : dto.getRoles()) { + ERole erole = ERole.valueOf(roleDto.getName().name()); + Role role = rolesByName.get(erole); + if (role == null) { + // Optionally create missing roles on the fly + role = new Role(); + role.setName(erole); + role = _roleRepository.save(role); + rolesByName.put(erole, role); + } + roles.add(role); } - roles.add(role); } - } - user.setRoles(roles); + user.setRoles(roles); - if(!_userRepository.existsByUsername(user.getUsername())) { _userRepository.save(user); _logger.info("Imported user: {} with roles: {}", user.getUsername(), user.getRoles().stream() .map(Role::getName).collect(Collectors.toList()).stream().map(ERole::toString) .collect(Collectors.joining(", "))); } else { - _logger.warn("User with username {} already exists, SKIPPING import", user.getUsername()); - _logger.info("Imported user: {} with roles: {}", user.getUsername(), user.getRoles().stream() - .map(Role::getName).collect(Collectors.toList()).stream().map(ERole::toString) - .collect(Collectors.joining(", "))); + _logger.warn("User with username {} already exists, SKIPPING import", dto.getUsername()); + } } } diff --git a/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java b/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java index 58fcc55..7650aca 100644 --- a/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java +++ b/src/main/java/com/jambotronGroup/jambotron/jsonDump/JsonDumpService.java @@ -167,6 +167,12 @@ public class JsonDumpService extends InitDataService { List items = super.readList(jsonFile, new TypeReference>() {}); for (TutorialDto dto : items) { + + if(_tutorialRepository.findByUserIdAndTitle(dto.getUserID(), dto.getTitle()).size()> 0) { + _logger.warn("Tutorial with title [{}] already exists for userID [{}], SKIPPING import", dto.getTitle(), dto.getUserID()); + continue; + } + Tutorial t = new Tutorial(); t.setId(dto.getId()); t.setTitle(dto.getTitle()); @@ -193,16 +199,17 @@ public class JsonDumpService extends InitDataService { } else { throw new IllegalArgumentException("Missing user with ROLE_ADMIN to assign tutorial: " + dto.getTitle()); } + } else { + User user = usersById.get(dto.getUserID()); + //user = _entityManager.merge(user); // Ensure user is managed by EntityManager + //user.setRoles(null); // because we don't want to load roles to user object + t.setUser(user); } - User user = usersById.get(dto.getUserID()); - //user = _entityManager.merge(user); // Ensure user is managed by EntityManager - //user.setRoles(null); // because we don't want to load roles to user object - t.setUser(user); - if(_tutorialRepository.findByUserIdAndTitle(t.getUser().getId(), t.getTitle()).size()> 0) { - _logger.warn("Tutorial with title [{}] already exists for user [{}], SKIPPING import", t.getTitle(), t.getUser().getUsername()); - continue; + if(_tutorialRepository.findByTitle(t.getTitle()).size() > 0) { + t.setTitle(t.getTitle() + " (imported) " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); } + _tutorialRepository.saveAndFlush(t); //_tutorialRepository.save(t); _logger.info("Imported tutorial: {} with user: {}", t.getTitle(), t.getUser().getUsername()); diff --git a/src/main/java/com/jambotronGroup/jambotron/repository/TutorialRepository.java b/src/main/java/com/jambotronGroup/jambotron/repository/TutorialRepository.java index 0709372..fd0672e 100644 --- a/src/main/java/com/jambotronGroup/jambotron/repository/TutorialRepository.java +++ b/src/main/java/com/jambotronGroup/jambotron/repository/TutorialRepository.java @@ -17,4 +17,6 @@ public interface TutorialRepository extends JpaRepository { List findBytobepublished(boolean tobepublished); List findByIdAndTobepublished(Long id,boolean tobepublished); List findByTitleContaining(String title); + + List findByTitle(String title); } \ No newline at end of file
data-base.component works!