Merge pull request #87 from liosha84/84-create-json-dump

84 create json dump
This commit is contained in:
liosha84
2025-08-18 12:28:00 +03:00
committed by GitHub
83 changed files with 2931 additions and 464 deletions
+10
View File
@@ -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}
+3
View File
@@ -0,0 +1,3 @@
{"id": 1, "name": "ROLE_USER"}
{"id": 2, "name": "ROLE_MODERATOR"}
{"id": 3, "name": "ROLE_ADMIN"}
File diff suppressed because one or more lines are too long
+5
View File
@@ -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}
+4
View File
@@ -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"}
+5
View File
@@ -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"
@@ -1,4 +1,4 @@
<mat-divider></mat-divider>
<!-- html -->
<nav aria-label="Breadcrumb" class="breadcrumb">
@let breadcrumbs = breadcrumbs$ | async;
@@ -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;
}
@@ -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);
@@ -0,0 +1,17 @@
<app-vertical-nav
[routes]="routes"
[basePath]="basePath"
(collapsedChange)="navCollapsedChanged($event)">
</app-vertical-nav>
<div class="app-breadcrumb" [class.collapsed]="isCollapsed">
<app-breadcrumbs></app-breadcrumbs>
</div>
<div class="app-container" [class.collapsed]="isCollapsed">
<div class="page-component">
<router-outlet />
</div>
</div>
@@ -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;
}
@@ -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<SideBarAdminComponent>;
describe('InnerLayoutComponent', () => {
let component: InnerLayoutComponent;
let fixture: ComponentFixture<InnerLayoutComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SideBarAdminComponent]
imports: [InnerLayoutComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SideBarAdminComponent);
fixture = TestBed.createComponent(InnerLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -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;
}
}
@@ -0,0 +1,25 @@
<div class="nav-container" [class.collapsed]="!opened()">
<div class="nav-header">
<button mat-icon-button (click)="toggle()" aria-label="Toggle sidebar">
<mat-icon>{{ opened() ? 'chevron_left' : 'chevron_right' }}</mat-icon>
</button>
</div>
<mat-nav-list class="nav-list">
@for (item of (items || navItems()); track item) {
<a
mat-list-item
[routerLink]="item.path"
[routerLinkActive]="'active'"
[routerLinkActiveOptions]="{ exact: item.exact !== false }"
[matTooltip]="opened() ? '' : item.title"
matTooltipPosition="right"
>
<mat-icon matListItemIcon>{{ item.icon }}</mat-icon>
<span matListItemTitle class="label">{{ item.title }}</span>
</a>
}
</mat-nav-list>
</div>
@@ -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;
}
@@ -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<VerticalNavComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VerticalNavComponent]
})
.compileComponents();
fixture = TestBed.createComponent(VerticalNavComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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<NavItem[]>([]);
readonly hasItems = computed(() => (this.items?.length ?? this.navItems().length) > 0);
@Output() collapsedChange = new EventEmitter<boolean>();
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;
}
}
@@ -0,0 +1,7 @@
import { Table } from './table';
describe('Table', () => {
it('should create an instance', () => {
expect(new Table()).toBeTruthy();
});
});
+4
View File
@@ -0,0 +1,4 @@
export class Table {
schema?: string;
table?: string;
}
@@ -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<any> {
return this.http.put(`${this.baseUrl}/users/${fileName}`, data);
getUsers(): Observable<User[]> {
return this.http.get<User[]>(`${this.baseUrl}/users`);
}
getAllRoles(): Observable<Role[]> {
return this.http.get<Role[]>(`${this.baseUrl}/roles`);
}
updateUserRoles(id: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${id}`, data);
}
/*
getUserTables(schema?: string): Observable<Table[]> {
let params = new HttpParams();
if (schema) {
params = params.set('schema', schema);
}
return this.http.get<Table[]>(`${this.baseUrl}/admin/json-dump/get-tables`, { params });
}
*/
}
@@ -1,11 +1,16 @@
<app-side-bar-admin
class="pc-sidebar"
>
</app-side-bar-admin>
<div class="pc-container">
<app-breadcrumbs></app-breadcrumbs>
<router-outlet />
<app-vertical-nav
[routes]="adminRoutes"
[basePath]="basePath"
(collapsedChange)="navCollapsedChanged($event)">
</app-vertical-nav>
<div class="app-breadcrumb" [class.collapsed]="isCollapsed">
<app-breadcrumbs></app-breadcrumbs>
</div>
<div class="app-container" [class.collapsed]="isCollapsed">
<div class="page-component">
<router-outlet />
</div>
</div>
@@ -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;
}
@@ -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>(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;
}
}
@@ -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}
}
]
}
@@ -0,0 +1,88 @@
<div class="json-dump-container" *ngIf="!loading; else loadingTpl">
<h2>JSON Dump Management</h2>
<!-- Entity-specific Export/Import -->
<!-- <div class="entity-actions" *ngFor="let entity of entityTypes">
<h3>{{ entity | titlecase }}</h3>
<button mat-raised-button color="primary" (click)="exportEntity(entity)">
<mat-icon>download</mat-icon> Export {{ entity }}
</button>
<label mat-raised-button color="accent">
<mat-icon>upload</mat-icon> Import {{ entity }}
<input type="file" hidden accept=".json" (change)="onFileSelected($event, entity)">
</label>
<button mat-icon-button (click)="restore(entity)" matTooltip="Restore from latest file">
<mat-icon>restore_from_trash</mat-icon>
</button>
<label mat-i color="accent">
<mat-icon>upload</mat-icon> Import {{ entity }}
<input type="file" hidden accept=".json" (change)="onFileSelected($event, entity)">
</label>
</div>-->
<mat-toolbar>
@for (entity of entityTypes; track entity) {
<mat-toolbar-row>
<mat-label>{{ entity | titlecase }}</mat-label>
<button mat-icon-button (click)="exportEntity(entity)" matTooltip="Export {{ entity }}">
<mat-icon>download</mat-icon>
</button>
<button mat-icon-button (click)="fileInput.click()" matTooltip=" Import {{ entity }}">
<mat-icon>restore_from_trash</mat-icon>
</button>
<button mat-icon-button (click)="restore(entity)" matTooltip="Restore from latest file">
<mat-icon>upload</mat-icon>
</button>
<input
type="file"
#fileInput
accept=".json"
(change)="onFileSelected($event, entity)"
hidden
/>
</mat-toolbar-row>
}
</mat-toolbar>
<!-- Files Table -->
<h3>Available JSON Files</h3>
<mat-table [dataSource]="files">
<ng-container matColumnDef="fileName">
<mat-header-cell *matHeaderCellDef> File Name </mat-header-cell>
<mat-cell *matCellDef="let file">{{ file.fileName }}</mat-cell>
</ng-container>
<ng-container matColumnDef="size">
<mat-header-cell *matHeaderCellDef> Size </mat-header-cell>
<mat-cell *matCellDef="let file">{{ jsonDumpService.formatFileSize(file.size) }}</mat-cell>
</ng-container>
<ng-container matColumnDef="lastModified">
<mat-header-cell *matHeaderCellDef> Last Modified </mat-header-cell>
<mat-cell *matCellDef="let file">{{ jsonDumpService.formatDate(file.lastModified) }}</mat-cell>
</ng-container>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
<mat-cell *matCellDef="let file">
<button mat-icon-button (click)="downloadFile(file.fileName)" matTooltip="Download">
<mat-icon>download</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteFile(file.fileName)" matTooltip="Delete">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="['fileName', 'size', 'lastModified', 'actions']"></mat-header-row>
<mat-row *matRowDef="let row; columns: ['fileName', 'size', 'lastModified', 'actions']"></mat-row>
</mat-table>
<!-- Archives -->
<h3>Archives</h3>
<mat-list>
<mat-list-item *ngFor="let archive of archives">{{ archive }}</mat-list-item>
</mat-list>
</div>
<ng-template #loadingTpl>
<mat-spinner></mat-spinner>
</ng-template>
@@ -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;
}
@@ -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<JsonDumpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [JsonDumpComponent]
})
.compileComponents();
fixture = TestBed.createComponent(JsonDumpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,146 @@
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';
import {MatToolbar, MatToolbarRow} from '@angular/material/toolbar';
import {MatLabel} from '@angular/material/input';
@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,
MatToolbar,
MatToolbarRow,
MatLabel
],
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 });
}
restore(fileName: string) {
}
}
@@ -0,0 +1,7 @@
<p>data-base.component works!</p>
<ul>
@for (table of tables; track table){
<li>{{ table.schema }}.{{ table.table }}</li>
}
</ul>
@@ -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<DataBaseComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DataBaseComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DataBaseComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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)
// });
}
}
@@ -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();
});
});
@@ -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<T = any> {
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}/file`, formData);
}
// ==================== EXPORT METHODS ====================
/**
* Export all entities (users, tutorials, roles)
*/
exportAll(): Observable<ExportResponse> {
return this.http.post<ExportResponse>(`${this.baseUrl}/export/all`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Export users only
*/
exportUsers(): Observable<ExportResponse> {
return this.http.post<ExportResponse>(`${this.baseUrl}/export/users`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Export tutorials only
*/
exportTutorials(): Observable<ExportResponse> {
return this.http.post<ExportResponse>(`${this.baseUrl}/export/tutorials`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Export roles only
*/
exportRoles(): Observable<ExportResponse> {
return this.http.post<ExportResponse>(`${this.baseUrl}/export/roles`, {})
.pipe(
catchError(this.handleError)
);
}
// ==================== IMPORT METHODS ====================
/**
* Import users from latest JSON file
*/
importUsers(): Observable<ImportResponse> {
return this.http.post<ImportResponse>(`${this.baseUrl}/import/users`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Import tutorials from latest JSON file
*/
importTutorials(): Observable<ImportResponse> {
return this.http.post<ImportResponse>(`${this.baseUrl}/import/tutorials`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Import roles from latest JSON file
*/
importRoles(): Observable<ImportResponse> {
return this.http.post<ImportResponse>(`${this.baseUrl}/import/roles`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Import users from uploaded file
*/
importUsersFromFile(file: File): Observable<ImportResponse> {
const formData = new FormData();
formData.append('file', file);
return this.http.post<ImportResponse>(`${this.baseUrl}/import/users/file`, formData)
.pipe(
catchError(this.handleError)
);
}
/**
* Import tutorials from uploaded file
*/
importTutorialsFromFile(file: File): Observable<ImportResponse> {
const formData = new FormData();
formData.append('file', file);
return this.http.post<ImportResponse>(`${this.baseUrl}/import/tutorials/file`, formData)
.pipe(
catchError(this.handleError)
);
}
// ==================== ARCHIVE METHODS ====================
/**
* Get list of all archived files
*/
getArchivedFiles(): Observable<ArchivesResponse> {
return this.http.get<ArchivesResponse>(`${this.baseUrl}/archives`)
.pipe(
catchError(this.handleError)
);
}
archiveOldFiles(): Observable<ApiResponse> {
return this.http.post<ApiResponse>(`${this.baseUrl}/archives/archive-old-files`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Extract archived file
*/
extractArchive(fileName: string): Observable<ExtractResponse> {
return this.http.post<ExtractResponse>(`${this.baseUrl}/archives/${fileName}/extract`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Download archived file
*/
downloadArchive(fileName: string): Observable<Blob> {
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<FilesResponse> {
return this.http.get<FilesResponse>(`${this.baseUrl}/files`)
.pipe(
catchError(this.handleError)
);
}
/**
* Download JSON file
*/
downloadFile(fileName: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}/files/${fileName}/download`, {
responseType: 'blob'
}).pipe(
catchError(this.handleError)
);
}
/**
* Delete JSON file
*/
deleteFile(fileName: string): Observable<ApiResponse> {
return this.http.delete<ApiResponse>(`${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<never> => {
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));
};
}
@@ -25,3 +25,5 @@
</mat-tab-group>
</mat-card-content>
</mat-card>
<mat-divider></mat-divider>
<app-json-dump></app-json-dump>
@@ -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'
@@ -1,35 +0,0 @@
<mat-nav-list>
<a mat-list-item routerLink="admin-welcome">
<span class="entry">
<mat-icon>house</mat-icon>
@if (!isCollapsed) {
<span >Dashboard</span>
}
</span>
</a>
<a mat-list-item routerLink="users">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Users</span>
}
</span>
</a>
<a mat-list-item routerLink="settings">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Settings</span>
}
</span>
</a>
<a mat-list-item routerLink="system">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >System</span>
}
</span>
</a>
</mat-nav-list>
@@ -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);
}
@@ -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;
}
@@ -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(<string>element.username?.toUpperCase());
}
}
export const UserColumns = [
@@ -1,60 +1,63 @@
<mat-toolbar class="fixed-top">
<button mat-button routerLink="">
{{title}}
</button>
<button mat-raised-button routerLink="./ai/generate-image">
Generate Image
</button>
<button mat-raised-button routerLink="./tutorials/tutorials-all">
Tutorials
</button>
<span class="menu-spacer"></span>
@if (!isLoggedIn) {
<button matMiniFab matTooltip="Sign Up" aria-label="Sign Up" (click)="openSignupDialog('100ms', '5ms')">
<mat-icon>person_add</mat-icon>
<!-- <mat-toolbar-row>-->
<button mat-button routerLink="">
{{title}}
</button>
<button mat-raised-button routerLink="./ai/generate-image">
Generate Image
</button>
<button mat-raised-button routerLink="./tutorials/tutorials-all">
Tutorials
</button>
<button matMiniFab matTooltip="Login" aria-label="Login" (click)="openLoginDialog('100ms', '5ms')" >
<mat-icon>login</mat-icon>
</button>
}@else {
<button matButton="elevated" aria-label="Account" [matMenuTriggerFor]="menu">
<mat-icon>account_circle</mat-icon>
<mat-label>{{username}}</mat-label>
</button>
<mat-menu #menu="matMenu">
@if (showAdminBoard) {
<button mat-menu-item routerLink="admin/admin-welcome">
<mat-icon>admin_panel_settings</mat-icon>
Admin Panel
</button>
}
@if (showUserBoard) {
<button mat-menu-item routerLink="user/user-welcome">
<mat-icon>space_dashboard</mat-icon>
User tools
</button>
}
@if (showModeratorBoard) {
<button mat-menu-item routerLink="moderator/moderator-welcome">
<mat-icon>space_dashboard</mat-icon>
Moderator board
</button>
}
<span class="menu-spacer"></span>
<button mat-menu-item routerLink="profile">
<mat-icon>person</mat-icon>
Profile
@if (!isLoggedIn) {
<button matMiniFab matTooltip="Sign Up" aria-label="Sign Up" (click)="openSignupDialog('100ms', '5ms')">
<mat-icon>person_add</mat-icon>
</button>
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
Logout
<button matMiniFab matTooltip="Login" aria-label="Login" (click)="openLoginDialog('100ms', '5ms')" >
<mat-icon>login</mat-icon>
</button>
</mat-menu>
}
}@else {
<button matButton="elevated" aria-label="Account" [matMenuTriggerFor]="menu">
<mat-icon>account_circle</mat-icon>
<mat-label>{{username}}</mat-label>
</button>
<mat-menu #menu="matMenu">
@if (showAdminBoard) {
<button mat-menu-item routerLink="admin/admin-welcome">
<mat-icon>admin_panel_settings</mat-icon>
Admin Panel
</button>
}
@if (showUserBoard) {
<button mat-menu-item routerLink="user/user-welcome">
<mat-icon>space_dashboard</mat-icon>
User tools
</button>
}
@if (showModeratorBoard) {
<button mat-menu-item routerLink="moderator/moderator-welcome">
<mat-icon>space_dashboard</mat-icon>
Moderator board
</button>
}
<button mat-menu-item routerLink="profile">
<mat-icon>person</mat-icon>
Profile
</button>
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
Logout
</button>
</mat-menu>
}
<!-- </mat-toolbar-row>-->
</mat-toolbar>
@@ -30,6 +30,7 @@ mat-toolbar{
margin-top: 65px;
height: 100%;
min-height: fit-content;
width: 100%;
}
.component{
@@ -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',
@@ -1,5 +1,2 @@
<app-side-bar-moderator class="pc-sidebar" ></app-side-bar-moderator>
<div class="pc-container">
<app-breadcrumbs></app-breadcrumbs>
<router-outlet></router-outlet>
</div>
<app-inner-layout [moduleRoutes]="moderatorRouts" [basePath]="basePath"></app-inner-layout>
@@ -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;
}
@@ -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';
}
@@ -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);
@@ -1,10 +1,2 @@
<app-side-bar-user
class="pc-sidebar"
>
</app-side-bar-user>
<div class="pc-container">
<app-breadcrumbs></app-breadcrumbs>
<router-outlet></router-outlet>
</div>
<app-inner-layout [moduleRoutes]="userRouts" [basePath]="basePath"></app-inner-layout>
@@ -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';
}
@@ -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);
@@ -24,8 +24,6 @@ export class UserService {
return this.http.get(API_URL + '/mod', { responseType: 'text' });
}
getAdminBoard(): Observable<User[]> {
return this.http.get<User[]>(API_URL);
}
}
+2 -2
View File
@@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family= Jersey 20 Charted:wght@300;400;500&display=swap">
<!--<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family= Jersey 20 Charted:wght@300;400;500&display=swap">-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<!-- Google tag (gtag.js) -->
+2
View File
@@ -7,6 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<!--<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family= Jersey 20 Charted:wght@300;400;500&display=swap">-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
+23 -12
View File
@@ -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';
@@ -49,20 +49,31 @@ 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;
}
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);
@@ -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;
}
}
@@ -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;
}
}
@@ -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<RoleDto> roles; // typically you don't expose password in DTOs
public UserDto() {
}
public UserDto(Long id, String username, String email, String password, Set<RoleDto> 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<RoleDto> getRoles() {
return roles;
}
public void setRoles(Set<RoleDto> 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;
}
}
@@ -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);
// }
}
}
@@ -0,0 +1,342 @@
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.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.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@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<Map<String, Object>> exportAll() {
Map<String, Object> 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<Map<String, Object>> exportUsers() {
return performExport(() -> _jsonDumpService.exportAllUsersToJson(), "users");
}
@PostMapping("/export/tutorials")
public ResponseEntity<Map<String, Object>> exportTutorials() {
return performExport(() -> _jsonDumpService.exportAllTutorialsToJson(), "tutorials");
}
@PostMapping("/export/roles")
public ResponseEntity<Map<String, Object>> exportRoles() {
return performExport(() -> _jsonDumpService.exportAllRolesToJson(), "roles");
}
// ==================== IMPORT ENDPOINTS ====================
@PostMapping("/restore/users")
public ResponseEntity<Map<String, Object>> restoreUsers() {
return performImport(() -> _jsonDumpService.importUsers(), "users");
}
@PostMapping("/restore/tutorials")
public ResponseEntity<Map<String, Object>> restoreTutorials() {
return performImport(() -> _jsonDumpService.importTutorials(), "tutorials");
}
@PostMapping("/restore/roles")
public ResponseEntity<Map<String, Object>> restoreRoles() {
return performImport(() -> _jsonDumpService.importRoles(), "roles");
}
@PostMapping("/import/users/file")
public ResponseEntity<Map<String, Object>> importUsersFromFile(@RequestParam("file") MultipartFile file) {
return importFromFile(file, (path) -> _jsonDumpService.importUsers(path), "users");
}
@PostMapping("/import/tutorials/file")
public ResponseEntity<Map<String, Object>> importTutorialsFromFile(@RequestParam("file") MultipartFile file) {
return importFromFile(file, (path) -> _jsonDumpService.importTutorials(path), "tutorials");
}
// ==================== ARCHIVE MANAGEMENT ====================
@PostMapping("/archives/archive-old-files")
public ResponseEntity<Map<String, Object>> archiveOldFiles() {
return performArchiveOldFiles(() -> _jsonDumpService.archiveOldFiles());
}
@GetMapping("/archives")
public ResponseEntity<Map<String, Object>> getArchivedFiles() {
Map<String, Object> response = new HashMap<>();
try {
List<String> 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<Map<String, Object>> extractArchive(@PathVariable String fileName) {
Map<String, Object> 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<Resource> 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<Map<String, Object>> listFiles() {
Map<String, Object> response = new HashMap<>();
try {
List<Map<String, Object>> 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<Resource> 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<Map<String, Object>> deleteFile(@PathVariable String fileName) {
Map<String, Object> 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<Map<String, Object>> performExport(ExportOperation operation, String entityType) {
Map<String, Object> 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<Map<String, Object>> performImport(ImportOperation operation, String entityType) {
Map<String, Object> 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<Map<String, Object>> importFromFile(MultipartFile file, FileImportOperation operation, String entityType) {
Map<String, Object> 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<Map<String, Object>> performArchiveOldFiles(ArchiveOperation operation) {
Map<String, Object> 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;
}
}
@@ -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() {
@@ -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.initData();
}
}
@@ -0,0 +1,202 @@
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.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;
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);
protected ObjectMapper _mapper;
@Autowired
private ResourceLoader _resources;
@Autowired
private RoleRepository _roleRepository;
@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
public void importRoles(Path jsonFile) throws Exception {
_logger.info("Importing roles from {}", jsonFile.toAbsolutePath());
List<RoleDto> items = readList(jsonFile, new TypeReference<List<RoleDto>>() {});
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<ERole, Role> rolesByName = _roleRepository.findAll().stream()
.collect(Collectors.toMap(Role::getName, r -> r));
List<UserDto> items = readList(jsonFile, new TypeReference<List<UserDto>>() {});
for (UserDto dto : items) {
if(!_userRepository.existsByUsername(dto.getUsername())) {
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<Role> 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);
_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", dto.getUsername());
}
}
}
protected <T> List<T> readList(Path file, TypeReference<List<T>> type) throws Exception {
String json = Files.readString(file);
return _mapper.readValue(json, type);
}
public void initData() {
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: {}", rolesFile.getFileName());
}
Path usersFile = _initDataPath.resolve("users.json");
if (usersFile.toFile().exists()) {
importUsers(usersFile);
} else {
_logger.warn("Users import file not found: {}", usersFile.getFileName());
}
} catch (Exception e) {
_logger.error("Error importing data", e);
throw new RuntimeException("Failed to import data", e);
}
}
}
@@ -0,0 +1,460 @@
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 UserRepository _userRepository;
@Autowired
private RoleRepository _roleRepository;
@Autowired
private 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() {
}
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<Tutorial> tutorials = _tutorialRepository.findAll();
List<TutorialDto> tutorialDtos = tutorials.stream().map(
TutorialDto::fromEntity
).toList();
exportToJson(tutorialDtos, "tutorials");
}
public void exportAllRolesToJson() throws Exception {
List<Role> roles = _roleRepository.findAll();
exportToJson(roles, "roles");
}
@Transactional
public void exportAllUsersToJson() throws Exception {
List<User> users = _userRepository.findAll();
List<UserDto> userDtos = users.stream().map(
user-> UserDto.fromEntity(user)
).toList();
exportToJson(userDtos, "users");
}
private Optional<Path> getLatestJsonFile(String prefix) throws IOException {
Optional<Path> 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<Long, User> usersById = _userRepository.findAll().stream()
.collect(Collectors.toMap(User::getId, u -> u));
List<TutorialDto> items = super.readList(jsonFile, new TypeReference<List<TutorialDto>>() {});
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());
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<User> 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());
}
} 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);
}
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());
}
}
@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<Map<String, Object>> getJsonDumpInfo() throws IOException {
List<Map<String, Object>> infoList = new ArrayList<>();
if (!Files.exists(_root)) {
Map<String, Object> 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<String, Object> 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<String> 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<String, Object> deleteJsonDumpFile(String fileName) throws IOException {
Path filePath = _root.resolve(fileName);
Map<String, Object> 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;
}
}
@@ -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();
// }
}
}
@@ -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;
}
@@ -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 + "]";
@@ -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
@@ -11,4 +11,7 @@ import java.util.Optional;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(ERole name);
boolean existsByName(ERole name);
}
@@ -17,4 +17,6 @@ public interface TutorialRepository extends JpaRepository<Tutorial, Long> {
List<Tutorial> findBytobepublished(boolean tobepublished);
List<Tutorial> findByIdAndTobepublished(Long id,boolean tobepublished);
List<Tutorial> findByTitleContaining(String title);
List<Tutorial> findByTitle(String title);
}
@@ -51,7 +51,6 @@ public class WebSecurityConfig implements WebMvcConfigurer {// extends WebSecuri
private AuthEntryPointJwt unauthorizedHandler;
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
@@ -121,8 +120,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()
);
@@ -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
@@ -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
);
+78 -6
View File
@@ -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;
@@ -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)
);
@@ -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)
);
@@ -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);
@@ -1,2 +0,0 @@
ALTER TABLE IF EXISTS public.tutorials
ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY;
@@ -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 );
@@ -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;
@@ -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
)
@@ -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";
+10
View File
@@ -0,0 +1,10 @@
[ {
"id" : 1,
"name" : "ROLE_USER"
}, {
"id" : 2,
"name" : "ROLE_MODERATOR"
}, {
"id" : 3,
"name" : "ROLE_ADMIN"
} ]
+34
View File
@@ -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"
} ]
} ]