@@ -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}
|
||||
@@ -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
@@ -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}
|
||||
@@ -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"}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+88
@@ -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>
|
||||
+27
@@ -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;
|
||||
}
|
||||
+23
@@ -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();
|
||||
});
|
||||
});
|
||||
+146
@@ -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>
|
||||
+23
@@ -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'
|
||||
|
||||
-35
@@ -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>
|
||||
|
||||
-14
@@ -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);
|
||||
}
|
||||
-20
@@ -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',
|
||||
|
||||
+2
-5
@@ -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>
|
||||
|
||||
-13
@@ -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;
|
||||
}
|
||||
|
||||
+5
-4
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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) -->
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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";
|
||||
@@ -0,0 +1,10 @@
|
||||
[ {
|
||||
"id" : 1,
|
||||
"name" : "ROLE_USER"
|
||||
}, {
|
||||
"id" : 2,
|
||||
"name" : "ROLE_MODERATOR"
|
||||
}, {
|
||||
"id" : 3,
|
||||
"name" : "ROLE_ADMIN"
|
||||
} ]
|
||||
@@ -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"
|
||||
} ]
|
||||
} ]
|
||||
Reference in New Issue
Block a user