Merge pull request #48 from liosha84/47-add-moderator-board-with-all-tutorials-list-edit-preview-delete-functionality

47 add moderator board with all tutorials list edit preview delete functionality
This commit is contained in:
liosha84
2025-07-28 13:53:17 +03:00
committed by GitHub
117 changed files with 2507 additions and 296 deletions
+36
View File
@@ -108,6 +108,42 @@ docker-compose -f src/Docker/docker-compose.yml up -d
```
- For connection from pgadmin use
hostename: **host.docker.internal**
# How to run the application in Docker with HTTPS support
To run the application in Docker with HTTPS support, follow these steps:
1. **Cerbot**
- Run this command below to get certificate and store it to volume certs.
- Expanded(readable) command example:
docker run -d \
--name certbot \
-v certs:/etc/letsencrypt \
-v certs-data:/var/lib/letsencrypt \
-p 80:80 \
-p 443:443 \
certbot/certbot \
certonly --standalone --preferred-challenges http --email youremail@gmail.com -d yourdomain.com --agree-tos
- Command to run certbot with specific domain and email:
```bash
docker run -d --name certbot -v certs:/etc/letsencrypt -v certs-data:/var/lib/letsencrypt -p 8081:80 -p 8443:443 certbot/certbot certonly --standalone --preferred-challenges http --email liosha84@gmail.com -d jambotron.run.place --agree-tos
```
- Tip: You can generate certificate to local machine used next command:
```bash
docker run -it --rm -p 8081:80 --name certbot -v "C:\certbot\etc\letsencrypt:/etc/letsencrypt" -v "C:\certbot\var\lib\letsencrypt:/var/lib/letsencrypt" certbot/certbot certonly --standalone -d jambotron.run.place
```
certificate files will be stored in `C:\certbot\etc\letsencrypt` and `C:\certbot\var\lib\letsencrypt` directories.
- If you have same trouble before running certbot, run this project
`Helper_projects/spring-boot-https-main` insted of jambotron project.
- TODO: Add cron job to renew certificate automatically.
### Reference Documentation
For further reference, please consider the following sections:
@@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
@@ -0,0 +1,10 @@
FROM openjdk:20 as build
WORKDIR /app
COPY . ./
RUN microdnf install findutils
RUN ./gradlew build -x test
FROM openjdk:20-jdk-slim
WORKDIR /app
COPY --from=build /app/build/libs/spring-boot-https-0.0.1.jar .
CMD ["java", "-jar", "spring-boot-https-0.0.1.jar"]
@@ -0,0 +1 @@
https://dev.to/beksultandev/how-to-add-https-support-to-your-spring-boot-app-2h53
@@ -0,0 +1,29 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
}
group = 'dev.beksultan'
version = '0.0.1'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('bootBuildImage') {
builder = 'paketobuildpacks/builder-jammy-base:latest'
}
tasks.named('test') {
useJUnitPlatform()
}
@@ -0,0 +1,17 @@
services:
spring-boot-https:
container_name: spring-boot-https
image: beksultancs/spring-boot-https:1
restart: unless-stopped
ports:
- "8081:80"
- "8443:443"
volumes:
- certs:/certs
environment:
- SERVER_PORT=443
- FULLCHAINPEM=/certs/live/jambotron.run.place/fullchain.pem
- PRIVKEYPEM=/certs/live/jambotron.run.place/privkey.pem
volumes:
certs:
external: true
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+249
View File
@@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+92
View File
@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1 @@
rootProject.name = 'spring-boot-https'
@@ -0,0 +1,20 @@
package https;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class SpringBootHttpsApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootHttpsApplication.class, args);
}
@GetMapping("/hello")
public String hello() {
return "Hello World! 🚀";
}
}
@@ -0,0 +1,3 @@
server.ssl.enabled=true
server.ssl.certificate=${FULLCHAINPEM}
server.ssl.certificate-private-key=${PRIVKEYPEM}
@@ -0,0 +1,5 @@
server:
ssl:
enabled: true
certificate: ${FULLCHAINPEM}
certificate-private-key: ${PRIVKEYPEM}
@@ -0,0 +1,13 @@
package https;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringBootHttpsApplicationTests {
@Test
void contextLoads() {
}
}
+49 -1
View File
@@ -15,6 +15,7 @@ java {
}
repositories {
mavenCentral()
}
@@ -37,6 +38,9 @@ dependencies {
implementation 'org.springframework.ai:spring-ai-zhipuai-spring-boot-starter:1.0.0-M6'
//implementation 'org.springframework.ai:spring-ai-starter-model-zhipuai'
//implementation 'org.springframework.ai:spring-ai-zhipuai-spring-boot-starter'
//implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT")
@@ -94,8 +98,52 @@ apply plugin: 'java'
}
}*/
tasks.register('buildAngular_dev', Exec) {
//dependsOn deleteStaticFolder
workingDir './jambotron-ui' // Path to your Angular project (e.g., './angular-app')
executable 'npm.cmd' // Use 'npm' for Unix-like systems or 'npm.cmd' for Windows
// Your Angular build command
args = ['run', 'build_dev']//do not forget build_dev used port 8080 for connect to backend rest api
}
tasks.register('buildAngular', Exec) {
//dependsOn deleteStaticFolder
workingDir './jambotron-ui' // Path to your Angular project (e.g., './angular-app')
executable 'npm.cmd' // Use 'npm' for Unix-like systems or 'npm.cmd' for Windows
// Your Angular build command
args = ['run', 'build']//do not forget build_dev used port 8080 for connect to backend rest api
}
tasks.register('deleteStaticFolder', Delete) {
dependsOn buildAngular
def dirName = "src/main/resources/static"
file(dirName).list().each {
f ->
delete "${dirName}/${f}"
}
}
tasks.register('deleteStaticFolder_dev', Delete) {
dependsOn buildAngular_dev
def dirName = "src/main/resources/static"
file(dirName).list().each {
f ->
delete "${dirName}/${f}"
}
}
tasks.register('copyAngularBuild', Copy) {
//dependsOn buildAngular
dependsOn deleteStaticFolder
from 'jambotron-ui/dist/jambotron-ui' // Path to your Angular dist folder
into "src/main/resources/static" // Path INSIDE the WAR
}
tasks.register('copyAngularBuild_dev', Copy) {
dependsOn deleteStaticFolder_dev
from 'jambotron-ui/dist/jambotron-ui' // Path to your Angular dist folder
into "src/main/resources/static" // Path INSIDE the WAR
}
Binary file not shown.
+7 -1
View File
@@ -20,7 +20,6 @@
"outputPath": "dist/jambotron-ui",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
@@ -48,6 +47,12 @@
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [
{
"type": "initial",
@@ -78,6 +83,7 @@
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "jambotron-ui:build:production"
+2
View File
@@ -4,8 +4,10 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"start_prod": "ng serve --configuration production",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"build_dev":"ng build --configuration development",
"test": "ng test",
"serve:ssr:jambotron-ui": "node dist/jambotron-ui/server/server.mjs"
},
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AdminModuleService } from './admin-module.service';
describe('AdminModuleService', () => {
let service: AdminModuleService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AdminModuleService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient} from '@angular/common/http';
import {User} from '../models/user.model';
import {GlobalConstants} from '../global-constants';
@Injectable({
providedIn: 'root'
})
export class AdminModuleService {
baseUrl = GlobalConstants.API_URL;
constructor(private http: HttpClient) {
}
updateUserRoles(id: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${id}`, data);
}
}
@@ -1,28 +1,13 @@
<!--<p>admin.component works!</p>-->
<app-navigation
class="pc-sidebar"
[ngClass]="{
'navbar-collapsed': navCollapsed,
'mob-open': navCollapsedMob
}"
(NavCollapse)="this.navCollapsed = !this.navCollapsed"
/>
<app-nav-bar (NavCollapsedMob)="navMobClick()" (NavCollapse)="this.navCollapsed = !this.navCollapsed"/>
<app-side-bar-admin
class="pc-sidebar"
>
</app-side-bar-admin>
<div class="pc-container">
<div class="coded-wrapper">
<div class="coded-content">
<div class="coded-inner-content">
<app-breadcrumb />
<div class="main-body">
<div class="page-wrapper">
<router-outlet />
</div>
</div>
</div>
</div>
</div>
<div class="pc-menu-overlay" (click)="closeMenu()" (keydown)="handleKeyDown($event)" tabindex="0"></div>
</div>
@@ -1,22 +1,16 @@
.example-container {
width: 500px;
height: 300px;
border: 1px solid rgba(0, 0, 0, 0.5);
}
.example-sidenav-content {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
.example-sidenav {
padding: 20px;
}
//---------------
.pc-sidebar{
top: 120px;
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;
}
@@ -9,6 +9,7 @@ import {MatButton} from '@angular/material/button';
import { ScrollPanelModule } from 'primeng/scrollpanel';
import {MenuItem} from 'primeng/api';
import {PanelMenu} from 'primeng/panelmenu';
import {SideBarAdminComponent} from '../side-bar-admin.component/side-bar-admin.component';
@Component({
selector: 'app-admin.component',
imports: [
@@ -24,6 +25,7 @@ import {PanelMenu} from 'primeng/panelmenu';
RouterLink,
PanelMenu,
NgStyle,
SideBarAdminComponent,
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
@@ -8,7 +8,7 @@ const ADMIN_ROUTES: Routes = [
children: [
{
path: 'welcome',
path: 'admin-welcome',
loadComponent: () => import('../admin-module/admin-welcome.component/admin-welcome.component').then((c) => c.AdminWelcomeComponent),
},
{
@@ -17,9 +17,12 @@ const ADMIN_ROUTES: Routes = [
},
{
path: 'system',
loadComponent: () => import('../admin-module/system.component/system.component').then((c) => c.SystemComponent)
}
},
{
path: 'users',
loadComponent: () => import('../admin-module/users.component/users.component').then((c) => c.UsersComponent)
}
]
}
];
@@ -0,0 +1,35 @@
<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>
@@ -0,0 +1,14 @@
.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);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SideBarAdminComponent } from './side-bar-admin.component';
describe('SideBarAdminComponent', () => {
let component: SideBarAdminComponent;
let fixture: ComponentFixture<SideBarAdminComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SideBarAdminComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SideBarAdminComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
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;
}
@@ -0,0 +1,56 @@
<table mat-table [dataSource]="users">
@for (column of columnsSchema; track column){
<ng-container [matColumnDef]="column.key">
<th mat-header-cell *matHeaderCellDef>
{{column.label}}
</th>
<td mat-cell *matCellDef="let element">
@switch (column.type) {
@case ('list') {
<mat-form-field class="example-chip-list">
<mat-chip-grid #chipGrid aria-label="Role selection">
@for (role of element.roles; track $index) {
<mat-chip-row (removed)="remove(role,element)">
{{role.name}}
<mat-icon
matChipRemove
[attr.aria-label]="'remove ' + role.name"
>
cancel
</mat-icon>
</mat-chip-row>
}
</mat-chip-grid>
<input
name="currentRole"
placeholder="Add role..."
[matChipInputFor]="chipGrid"
[matAutocomplete]="auto"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[formControl]="rolesControl"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="add($event,element)"
(input)="change($event,filteredRoles(element.roles))"
/>
<mat-autocomplete [formControl]="ac"
autoActiveFirstOption
#auto
(optionSelected)="selected(element,$event); ">
@for (role of filteredRoles(element.roles); track role) {
<mat-option [value]="role">{{role.name}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
}
@default {
{{ element[column.key] }}
}
}
</td>
</ng-container>
}
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
@@ -0,0 +1,3 @@
.example-chip-list {
width: 100%;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component';
describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UsersComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UsersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,185 @@
import {ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, model} from '@angular/core';
import {UserService} from '../../services/user.service';
import {RolesService} from '../../services/roles.service';
import {Role} from '../../models/role';
import {User} from '../../models/user.model';
import {
MatCell,
MatCellDef, MatColumnDef,
MatHeaderCell, MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef, MatTable
} from '@angular/material/table';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {
MatAutocomplete,
MatAutocompleteSelectedEvent,
MatAutocompleteTrigger,
MatOption
} from '@angular/material/autocomplete';
import {
MatChip,
MatChipGrid,
MatChipInput,
MatChipInputEvent,
MatChipRow,
MatChipsModule
} from '@angular/material/chips';
import {MatIcon} from '@angular/material/icon';
import {COMMA, ENTER} from '@angular/cdk/keycodes';
import {MatFormField} from '@angular/material/input';
import {MatSelect, MatSelectTrigger} from '@angular/material/select';
import {AdminModuleService} from '../admin-module.service';
@Component({
selector: 'app-users.component',
imports: [
MatCell,
MatCellDef,
MatHeaderCell,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
MatTable,
MatColumnDef,
MatHeaderCellDef,
FormsModule,
ReactiveFormsModule,
MatChipGrid,
MatChipRow,
MatIcon,
MatAutocompleteTrigger,
MatChipInput,
MatAutocomplete,
MatOption,
MatFormField,
MatSelect,
MatSelectTrigger,
MatChip,
MatChipsModule
],
templateUrl: './users.component.html',
styleUrl: './users.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class UsersComponent {
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
allRoles:Role[] = [];
users: User[] = [];
displayedColumns: string[] = UserColumns.map((col) => col.key)
columnsSchema: any = UserColumns
readonly currentRole = model('');
rolesControl = new FormControl([]);
protected ac = new FormControl('');
constructor(private userService: UserService,
private roleService: RolesService,
private adminService:AdminModuleService) {
this.getAllRoles();
this.getUsers();
}
getAllRoles():any{
this.roleService.getAllRoles().subscribe(
(data : any) => {
this.allRoles = data;
console.log(data);
},
(err : any)=> {
this.allRoles = JSON.parse(err.error).message;
}
);
}
getUsers(){
this.userService.getAdminBoard().subscribe(
(data : any) => {
this.users = data;
console.log(data);
},
(err : any)=> {
console.log(JSON.parse(err.error).message);
}
);
}
filteredRoles(roles : Role[]):any {
return this.allRoles.filter(
(r:Role) => !roles.some((item) => item.id === r.id),
);
}
change($event: Event, roles: Role[]) {
roles.filter(
(r:Role) => !roles.some((item) => item.name?.toLowerCase() === r.name?.toLowerCase()),
)
}
selected(user: User, $event: MatAutocompleteSelectedEvent) {
user.roles.push($event.option.value);
this.saveUserRoles(user);
this.currentRole.set('');
$event.option.deselect();
}
add($event: MatChipInputEvent, user:User) {
/*const value = ($event.value || '').trim();
const role = this.allRoles.find(value1 => value1.name === value);
if(role){
user.roles.push(value);
this.saveUserRoles(user);
}*/
// Clear the input value
this.currentRole.set('');
}
remove(role: any, user: User) {
const updatedRoles: Role[] = user.roles.filter(
(r: Role) => r.id !== role.id // Simple comparison with the role to remove
);
if (updatedRoles && updatedRoles.length >= 1) {
user.roles = updatedRoles;
this.saveUserRoles(user);
}
}
saveUserRoles(user:User){
this.adminService.updateUserRoles(user.id, user).subscribe(
(data:any) => {
console.log(data);
},
(err:any) => {
console.log(err);
}
)
}
}
export const UserColumns = [
{
key: 'username',
type: 'text',
label: 'Name',
required: true,
},
{
key: 'email',
type: 'text',
label: 'Email',
},
{
key: 'roles',
type: 'list',
label: 'Roles',
required: true,
}
];
+1 -1
View File
@@ -17,7 +17,7 @@ export const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(routes)],//, { useHash: true }
imports: [RouterModule.forRoot(routes, { useHash: true}) ],
exports: [RouterModule],
})
export class AppRoutingModule {}
@@ -80,7 +80,6 @@ export class BoardAdminComponent implements OnInit {
return this.allRoles.filter(
(r:Role) => !roles.some((item) => item.id === r.id),
);
}
// private _filter(value: string): string[] {
@@ -111,7 +110,7 @@ export class BoardAdminComponent implements OnInit {
// this.announcer.announce(`Removed ${fruit}`);
// return [...fruits];
}
}
@@ -0,0 +1,7 @@
import { GlobalConstants } from './global-constants';
describe('GlobalConstants', () => {
it('should create an instance', () => {
expect(new GlobalConstants()).toBeTruthy();
});
});
+15
View File
@@ -0,0 +1,15 @@
import {environment} from '../environments/environment';
export class GlobalConstants {
public static readonly API_URL = (() => {
// ... calculate the value and return it
if(environment.production) {
return `${environment.host_name}/api`;
}else {
return `${environment.host_name}:${environment.port}/api`;
}
})();
}
@@ -27,7 +27,7 @@
</button>
<mat-menu #menu="matMenu">
@if (showAdminBoard) {
<button mat-menu-item routerLink="admin/welcome">
<button mat-menu-item routerLink="admin/admin-welcome">
<mat-icon>admin_panel_settings</mat-icon>
Admin Panel
</button>
@@ -39,6 +39,12 @@
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>
@@ -29,7 +29,7 @@ import { environment } from '../../../environments/environment';
MatToolbar,
MatButton,
RouterLink,
IconDirective,
RouterOutlet,
CommonModule,
MatIcon,
@@ -38,8 +38,7 @@ import { environment } from '../../../environments/environment';
MatMenu,
MatMenuItem,
MatTooltip,
MatLabel,
MatFabButton
MatLabel
],
schemas: [ CUSTOM_ELEMENTS_SCHEMA ],
@@ -186,7 +185,9 @@ export class MainComponent implements OnInit{
this.refreshToolbar();
dialogLoginRef.close()
this.router.navigate(['main/user/user-welcome']);
this.router.navigate(['main/tutorials']);
},
err => {
dialogLoginRef.componentInstance.openLoginFailedSnackBar(err.error.message);
@@ -232,8 +233,7 @@ export class MainComponent implements OnInit{
);
// do something here with the data
dialogloginSubscription.unsubscribe();
dialogSubmitSubscription.unsubscribe();
});
}
}
@@ -23,6 +23,11 @@ const MAIN_ROUTES: Routes =[
loadChildren: () =>
import('../user-module/user.module').then((m) => m.UserModule),
},
{
path: 'moderator',
loadChildren: () =>
import('../moderator-module/moderator.module').then((m) => m.ModeratorModule),
},
{
path: 'home',
component: HomeComponent
+1 -35
View File
@@ -6,42 +6,8 @@ export class Tutorial {
published?: boolean;
created?: Date;
modified?: Date;
tobepublished?: boolean;
isEdit?: boolean;
}
export const TutorialColumns = [
{
key: 'isSelected',
type: 'isSelected',
label: '',
},
{
key: 'title',
type: 'text',
label: 'Title',
required: true,
},
{
key: 'published',
type: 'boolean',
label: 'Is Published',
},
{
key: 'created',
type: 'datetime',
label: 'Created Date',
required: true,
},
{
key: 'modified',
type: 'datetime',
label: 'Modified Date',
required: true
},
{
key: 'isEdit',
type: 'isEdit',
label: '',
}
];
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ModeratorApiService } from './moderator-api.service';
describe('ModeratorApiService', () => {
let service: ModeratorApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ModeratorApiService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Observable} from 'rxjs';
import {Tutorial} from '../models/tutorial.model';
import {GlobalConstants} from '../global-constants';
@Injectable({
providedIn: 'root'
})
export class ModeratorApiService {
baseUrl = `${GlobalConstants.API_URL}/moderator`;
constructor(private http: HttpClient) {
}
getBePublishedTutorials(): Observable<Tutorial[]> {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${id}`);
}
publish(id: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${id}`, data);
}
}
@@ -0,0 +1 @@
<p>moderator-welcome.component works!</p>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModeratorWelcomeComponent } from './moderator-welcome.component';
describe('ModeratorWelcomeComponent', () => {
let component: ModeratorWelcomeComponent;
let fixture: ComponentFixture<ModeratorWelcomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModeratorWelcomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ModeratorWelcomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-moderator-welcome.component',
imports: [],
templateUrl: './moderator-welcome.component.html',
styleUrl: './moderator-welcome.component.scss'
})
export class ModeratorWelcomeComponent {
}
@@ -0,0 +1,4 @@
<app-side-bar-moderator class="pc-sidebar" ></app-side-bar-moderator>
<div class="pc-container">
<router-outlet></router-outlet>
</div>
@@ -0,0 +1,13 @@
//---------------
.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;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModeratorComponent } from './moderator.component';
describe('ModeratorComponent', () => {
let component: ModeratorComponent;
let fixture: ComponentFixture<ModeratorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModeratorComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ModeratorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import {RouterOutlet} from '@angular/router';
import {SideBarModeratorComponent} from '../side-bar-moderator.component/side-bar-moderator.component';
@Component({
selector: 'app-moderator.component',
imports: [
RouterOutlet,
SideBarModeratorComponent,
],
templateUrl: './moderator.component.html',
styleUrl: './moderator.component.scss'
})
export class ModeratorComponent {
}
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {moderatorRouting} from './moderator.routing';
import {ModeratorComponent} from './moderator.component/moderator.component';
@NgModule({
declarations: [],
imports: [
moderatorRouting,
ModeratorComponent,
CommonModule
]
})
export class ModeratorModule { }
@@ -0,0 +1,7 @@
import { ModeratorRouting } from './moderator.routing';
describe('ModeratorRouting', () => {
it('should create an instance', () => {
expect(new ModeratorRouting()).toBeTruthy();
});
});
@@ -0,0 +1,27 @@
import {RouterModule, Routes} from '@angular/router';
import {UserComponent} from '../user-module/user.component/user.component';
import {ModeratorComponent} from './moderator.component/moderator.component';
const MODERATOR_ROUTES: Routes = [
{
path: '',
component: ModeratorComponent,
children: [
{
path: 'moderator-welcome',
loadComponent: () => import('../moderator-module/moderator-welcome.component/moderator-welcome.component').then((c) => c.ModeratorWelcomeComponent),
},
{
path: 'tutorials-list',
loadComponent: () => import('../moderator-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent)
},
{
path: 'tutorial-preview',
loadComponent: () => import('../moderator-module/tutorial-preview.component/tutorial-preview.component').then((c) => c.TutorialPreviewComponent)
}
]
}
];
export const moderatorRouting = RouterModule.forChild(MODERATOR_ROUTES);
@@ -0,0 +1,18 @@
<mat-nav-list>
<a mat-list-item routerLink="moderator-welcome">
<span class="entry">
<mat-icon>house</mat-icon>
@if (!isCollapsed) {
<span >Dashboard</span>
}
</span>
</a>
<a mat-list-item routerLink="tutorials-list">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Tutorials</span>
}
</span>
</a>
</mat-nav-list>
@@ -0,0 +1,14 @@
.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);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SideBarModeratorComponent } from './side-bar-moderator.component';
describe('SideBarModeratorComponent', () => {
let component: SideBarModeratorComponent;
let fixture: ComponentFixture<SideBarModeratorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SideBarModeratorComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SideBarModeratorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {MatIcon} from '@angular/material/icon';
import {MatListItem, MatNavList} from '@angular/material/list';
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-side-bar-moderator',
imports: [
MatIcon,
MatListItem,
MatNavList,
RouterLink
],
templateUrl: './side-bar-moderator.component.html',
styleUrl: './side-bar-moderator.component.scss',
schemas:[CUSTOM_ELEMENTS_SCHEMA]
})
export class SideBarModeratorComponent {
isCollapsed = false;
}
@@ -0,0 +1,43 @@
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title>Edit tutorial</mat-card-title>
</mat-card-header>
<mat-card-content >
<mat-form-field class="example-full-width">
<mat-label>Title</mat-label>
<input matInput required
[(ngModel)]="tutorial.title">
</mat-form-field>
<mat-tab-group>
<mat-tab label="Preview">
<markdown class="preview" [data]="tutorial.description"></markdown>
</mat-tab>
<mat-tab label="Edit">
<textarea class="variable-textarea" [(ngModel)]="tutorial.description"></textarea>
<markdown class="variable-binding" [data]="tutorial.description"></markdown>
</mat-tab>
<mat-tab label="Example">
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
<markdown class="variable-binding" [data]="markdown"></markdown>
</mat-tab>
</mat-tab-group>
</mat-card-content>
<mat-card-actions>
<button matButton (click)="publishTutorial()">Publicate</button>
@if (hasError){
<mat-error>
{{errorMessage}}
</mat-error>
}
@if(submitted) {
<h4>Tutorial was submitted successfully!</h4>
<button matButton routerLink="../tutorials-list">Back to list</button>
}
</mat-card-actions>
</mat-card>
@@ -0,0 +1,59 @@
.submit-form {
max-width: 400px;
margin: auto;
}
mat-card{
//margin: 20px;
}
mat-card-title{
color: cyan;
}
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
.variable-binding,
.variable-textarea {
width: 49%;
}
.variable-textarea {
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0,0,0,.07);
min-height: 420px;
padding: 8px;
transition: all 300ms ease-out;
}
.variable-textarea:hover {
box-shadow: 0 6px 12px 3px rgba(0,0,0,.09),
0 2px 3px 1px rgba(0,0,0,.06);
}
.variable-binding {
display: block;
float: right;
}
.preview {
/* display: block;
float: right;*/
}
.mat-mdc-card-outlined {
background-color: var(--mat-card-outlined-container-color, var(--mat-sys-surface));
border-radius:0;
border-width: var(--mat-card-outlined-outline-width, 1px);
border-color: var(--mat-card-outlined-outline-color, var(--mat-sys-outline-variant));
box-shadow: var(--mat-card-outlined-container-elevation, var(--mat-sys-level0));
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TutorialPreviewComponent } from './tutorial-preview.component';
describe('TutorialPreviewComponent', () => {
let component: TutorialPreviewComponent;
let fixture: ComponentFixture<TutorialPreviewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TutorialPreviewComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TutorialPreviewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,94 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {FormsModule} from "@angular/forms";
import {MarkdownComponent} from "ngx-markdown";
import {MatButton} from "@angular/material/button";
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from "@angular/material/card";
import {MatError, MatFormField, MatInput, MatLabel} from "@angular/material/input";
import {MatTab, MatTabGroup} from "@angular/material/tabs";
import {ActivatedRoute, RouterLink} from "@angular/router";
import {Tutorial} from '../../models/tutorial.model';
import {UserApiService} from '../../user-module/user-api.service';
import {ModeratorApiService} from '../moderator-api.service';
@Component({
selector: 'app-tutorial-preview.component',
imports: [
FormsModule,
MarkdownComponent,
MatButton,
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatError,
MatFormField,
MatInput,
MatLabel,
MatTab,
MatTabGroup,
RouterLink,
MatError,
MatFormField
],
templateUrl: './tutorial-preview.component.html',
styleUrl: './tutorial-preview.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class TutorialPreviewComponent {
tutorial: Tutorial = new Tutorial();
submitted = false;
hasError = false;
errorMessage = '';
markdown = `## Markdown __rulez__!
---
### Syntax highlight
\`\`\`typescript
const language = 'typescript';
\`\`\`
### Lists
1. Ordered list
2. Another bullet point
- Unordered list
- Another unordered bullet
### Blockquote
> Blockquote to the max`;
private id: string | null | undefined;
constructor(private moderatorApiService: ModeratorApiService, private route: ActivatedRoute) {
this.route.queryParams
.subscribe(params => {
console.log(params);
this.id = params['id'];
console.log(this.id);
});
this.moderatorApiService.getTutorial(this.id).subscribe(
data=>{
this.tutorial = data;
}
);
}
publishTutorial(): void {
this.tutorial.published = true;
this.moderatorApiService.publish(this.id, this.tutorial)
.subscribe(
response => {
console.log(response);
this.submitted = true;
this.hasError = false;
},
error => {
console.log(error);
this.errorMessage = error.error.message;
this.hasError = true;
});
}
}
@@ -0,0 +1,44 @@
<table mat-table [dataSource]="dataSource">
@for (column of columnsSchema; track column){
<ng-container [matColumnDef]="column.key">
<th mat-header-cell *matHeaderCellDef>
{{column.label}}
</th>
<td mat-cell *matCellDef="let element">
@switch (column.type) {
@case('isEdit') {
<div class="btn-edit" >
<button mat-button routerLink='../tutorial-preview' [queryParams]="{id:element.id}" (click)="element.isEdit = !element.isEdit">
Preview
</button>
</div>
}
@case ('boolean') {
<mat-slide-toggle
class="example-margin"
[checked]="element[column.key]"
(change)="publish(element, $event.checked)"
>
</mat-slide-toggle>
}
@case ('datetime') {
{{ element[column.key] | date: 'medium' }}
}
@default {
{{ element[column.key] }}
}
}
</td>
</ng-container>
}
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TutorialsListComponent } from './tutorials-list.component';
describe('TutorialsListComponent', () => {
let component: TutorialsListComponent;
let fixture: ComponentFixture<TutorialsListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TutorialsListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TutorialsListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,144 @@
import { Component } from '@angular/core';
import {DatePipe} from "@angular/common";
import {MatButton} from "@angular/material/button";
import {
MatCell,
MatCellDef, MatColumnDef,
MatHeaderCell, MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef, MatTable, MatTableDataSource
} from "@angular/material/table";
import {MatCheckbox} from "@angular/material/checkbox";
import {MatSlideToggle} from "@angular/material/slide-toggle";
import {Router, RouterLink} from "@angular/router";
import {Tutorial} from '../../models/tutorial.model';
import {EventData} from '../../_shared/event.class';
import {ModeratorApiService} from '../moderator-api.service';
import {TokenStorageService} from '../../services/token-storage.service';
import {EventBusService} from '../../_shared/event-bus.service';
import {AuthService} from '../../services/auth.service';
import {HttpHeaders} from '@angular/common/http';
@Component({
selector: 'app-tutorials-list.component',
imports: [
DatePipe,
MatButton,
MatCell,
MatCellDef,
MatHeaderCell,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
MatSlideToggle,
MatTable,
RouterLink,
MatColumnDef,
MatHeaderCellDef
],
templateUrl: './tutorials-list.component.html',
styleUrl: './tutorials-list.component.scss'
})
export class TutorialsListComponent {
displayedColumns: string[] = TutorialColumns.map((col) => col.key)
columnsSchema: any = TutorialColumns
dataSource = new MatTableDataSource<Tutorial>()
constructor(private moderatorApiService: ModeratorApiService,
private storageService: TokenStorageService,
private eventBusService: EventBusService,
private authService: AuthService,
private router: Router
) {
this.getTutorials();
}
getTutorials(): void {
this.moderatorApiService.getBePublishedTutorials()
.subscribe(( data:Tutorial[] ) => {
this.dataSource.data = data;
console.log(data);
},
error => {
console.log(error);
if (
(
error.status === 401
)
&& this.storageService.isLoggedIn()
) {
this.eventBusService.emit(new EventData('logout', null));
}
});
}
publish(element: any, checked: boolean){
element.published = checked;
this.moderatorApiService.publish(element.id, element).subscribe(
response => {
console.log(response);
},
error => {
console.log(error);
});
}
logout(): void {
this.authService.logout().subscribe({
next: res => {
console.log(res);
this.storageService.clean();
//window.location.reload();
this.router.navigate(['main/generate-image']).then(() => {
//window.location.reload();
})
},
error: err => {
console.log(err);
}
});
}
}
export const TutorialColumns = [
{
key: 'title',
type: 'text',
label: 'Title',
required: true,
},
{
key: 'published',
type: 'boolean',
label: 'Is Published',
},
{
key: 'created',
type: 'datetime',
label: 'Created Date',
required: true,
},
{
key: 'modified',
type: 'datetime',
label: 'Modified Date',
required: true
},
{
key: 'isEdit',
type: 'isEdit',
label: '',
}
];
@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import {GlobalConstants} from '../global-constants';
const AUTH_API = 'http://localhost:8080/api/auth/';
const AUTH_API = GlobalConstants.API_URL + '/auth/';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
@@ -15,6 +16,7 @@ export class AuthService {
constructor(private http: HttpClient) { }
login(username: string, password: string): Observable<any> {
console.log(AUTH_API + 'signin');
return this.http.post(AUTH_API + 'signin', {
username,
password
@@ -2,8 +2,9 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Role } from '../models/role';
import {GlobalConstants} from '../global-constants';
const API_URL = 'http://localhost:8080/api/roles';
const API_URL = `${GlobalConstants.API_URL}/roles`;
@Injectable({
@@ -4,13 +4,14 @@ import {Observable} from 'rxjs';
import {Tutorial} from '../models/tutorial.model';
import {Bean} from '../models/Bean';
import {NameValueItem} from '../models/name-value-item';
import {GlobalConstants} from '../global-constants';
@Injectable({
providedIn: 'root'
})
export class SystemService {
baseUrl : string = 'http://localhost:8080/api/system';
baseUrl : string = `${GlobalConstants.API_URL}/system`;
constructor(private http: HttpClient) { }
getDataSourceProperties(): Observable<NameValueItem[]>{
@@ -2,9 +2,9 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Tutorial } from '../models/tutorial.model';
import {text} from 'node:stream/consumers';
import {GlobalConstants} from '../global-constants';
const baseUrl = 'http://localhost:8080/api/public/tutorials';
const baseUrl = `${GlobalConstants.API_URL}/public/tutorials`;
@Injectable({
providedIn: 'root'
@@ -2,9 +2,9 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {User} from "../models/user.model";
import {Tutorial} from "../models/tutorial.model";
import {GlobalConstants} from '../global-constants';
const API_URL = 'http://localhost:8080/api/users';
const API_URL = `${GlobalConstants.API_URL}/users`;
@Injectable({
providedIn: 'root'
@@ -4,6 +4,7 @@ import {Tutorial} from '../models/tutorial.model';
import {HttpClient} from '@angular/common/http';
import {Image} from '../models/image';
import {SpinnerService} from './spinner.service';
import {GlobalConstants} from '../global-constants';
@@ -13,7 +14,7 @@ import {SpinnerService} from './spinner.service';
export class ZhipuaiImageService {
baseUrl : string = 'http://localhost:8080/api/zhipuai';
baseUrl : string = `${GlobalConstants.API_URL}/public/zhipuai`;
constructor(private http: HttpClient,public spinnerService: SpinnerService) {
@@ -0,0 +1,19 @@
<mat-nav-list>
<a mat-list-item routerLink="user-welcome">
<span class="entry">
<mat-icon>house</mat-icon>
@if (!isCollapsed) {
<span >Dashboard</span>
}
</span>
</a>
<a mat-list-item routerLink="tutorials-list">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Tutorials</span>
}
</span>
</a>
</mat-nav-list>
@@ -0,0 +1,14 @@
.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);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SideBarUserComponent } from './side-bar-user.component';
describe('SideBarUserComponent', () => {
let component: SideBarUserComponent;
let fixture: ComponentFixture<SideBarUserComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SideBarUserComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SideBarUserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
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-user',
imports: [
MatIcon,
MatListItem,
MatNavList,
RouterLink
],
templateUrl: './side-bar-user.component.html',
styleUrl: './side-bar-user.component.scss',
schemas:[CUSTOM_ELEMENTS_SCHEMA]
})
export class SideBarUserComponent {
isCollapsed = false;
}
@@ -4,7 +4,7 @@
}
mat-card{
margin: 20px;
//margin: 20px;
}
mat-card-title{
color: cyan;
@@ -50,3 +50,10 @@ mat-card-title{
float: right;*/
}
.mat-mdc-card-outlined {
background-color: var(--mat-card-outlined-container-color, var(--mat-sys-surface));
border-radius:0;
border-width: var(--mat-card-outlined-outline-width, 1px);
border-color: var(--mat-card-outlined-outline-color, var(--mat-sys-outline-variant));
box-shadow: var(--mat-card-outlined-container-elevation, var(--mat-sys-level0));
}
@@ -4,7 +4,6 @@ import {MatButton} from '@angular/material/button';
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from '@angular/material/card';
import {MatError, MatFormField, MatInput, MatLabel} from '@angular/material/input';
import {MatTab, MatTabGroup} from '@angular/material/tabs';
import {NgIf} from '@angular/common';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Tutorial} from '../../models/tutorial.model';
import {UserApiService} from '../user-api.service';
@@ -6,9 +6,6 @@
>
Remove Rows
</button>
<!--<button class="button-add-row" mat-button (click)="addRow()">
Add Row
</button>-->
<button mat-raised-button routerLink="../tutorial-add">Add tutorial</button>
</article>
@@ -56,7 +53,9 @@
<mat-slide-toggle
class="example-margin"
[checked]="element[column.key]"
[disabled]="true">
[disabled]="column.key !== 'tobepublished'"
(change)="publish(element, $event.checked)"
>
</mat-slide-toggle>
}
@@ -1,6 +1,6 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, OnInit} from '@angular/core';
import {MatList, MatListItem} from '@angular/material/list';
import {Tutorial, TutorialColumns} from '../../models/tutorial.model';
import {Tutorial} from '../../models/tutorial.model';
import {TutorialService} from '../../services/tutorial.service';
import {UserApiService} from '../user-api.service';
import {MatButton} from '@angular/material/button';
@@ -23,22 +23,15 @@ import {
MatTable,
MatTableDataSource
} from '@angular/material/table';
import {MatFormField, MatInput} from '@angular/material/input';
import {DatePipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault} from '@angular/common';
import {DatePipe} from '@angular/common';
import {MatCheckbox} from '@angular/material/checkbox';
import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from '@angular/material/datepicker';
import {MatSlideToggle} from '@angular/material/slide-toggle';
@Component({
selector: 'app-tutorials-list.component',
imports: [
MatList,
MatListItem,
MatLine,
MatButton,
MatIcon,
MatOption,
RouterLink,
ReactiveFormsModule,
MatTable,
@@ -47,22 +40,12 @@ import {MatSlideToggle} from '@angular/material/slide-toggle';
MatRowDef,
MatRow,
FormsModule,
MatInput,
MatFormField,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
NgSwitch,
NgSwitchCase,
NgSwitchDefault,
MatCellDef,
MatCheckbox,
MatDatepickerInput,
MatDatepickerToggle,
MatDatepicker,
DatePipe,
NgIf,
NgForOf,
MatCell,
MatSlideToggle
],
@@ -76,7 +59,7 @@ import {MatSlideToggle} from '@angular/material/slide-toggle';
displayedColumns: string[] = TutorialColumns.map((col) => col.key)
columnsSchema: any = TutorialColumns
dataSource = new MatTableDataSource<Tutorial>()
valid: any = {}
constructor(private userApiService: UserApiService,
private storageService: TokenStorageService,
@@ -154,22 +137,16 @@ import {MatSlideToggle} from '@angular/material/slide-toggle';
})
}
editRow(row: Tutorial) {
/*if (row.id === 0) {
this.userService.addUser(row).subscribe((newUser: User) => {
row.id = newUser.id
row.isEdit = false
})
} else {
this.userService.updateUser(row).subscribe(() => (row.isEdit = false))
}*/
}
publish(element: any, checked: boolean){
element.tobepublished = checked;
this.userApiService.update(element.id, element).subscribe(
response => {
console.log(response);
},
error => {
console.log(error);
});
disableSubmit(id: number) {
if (this.valid[id]) {
return Object.values(this.valid[id]).some((item) => item === false)
}
return false
}
logout(): void {
@@ -191,3 +168,47 @@ import {MatSlideToggle} from '@angular/material/slide-toggle';
}
}
export const TutorialColumns = [
{
key: 'isSelected',
type: 'isSelected',
label: '',
},
{
key: 'title',
type: 'text',
label: 'Title',
required: true,
},
{
key: 'published',
type: 'boolean',
label: 'Is Published',
},
{
key: 'created',
type: 'datetime',
label: 'Created Date',
required: true,
},
{
key: 'modified',
type: 'datetime',
label: 'Modified Date',
required: true
},
{
key: 'tobepublished',
type: 'boolean',
label: 'To be published',
required: true
},
{
key: 'isEdit',
type: 'isEdit',
label: '',
}
];
@@ -2,23 +2,21 @@ import { Injectable } from '@angular/core';
import {forkJoin, Observable} from 'rxjs';
import {Tutorial} from '../models/tutorial.model';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {GlobalConstants} from '../global-constants';
@Injectable({
providedIn: 'root'
})
export class UserApiService {
baseUrl = 'http://localhost:8080/api/user';
baseUrl = `${GlobalConstants.API_URL}/user`;
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' } )
};
constructor(private http: HttpClient) {
}
getUserAllTutorials(): Observable<Tutorial[]> {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`, this.httpOptions);
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
@@ -1,34 +1,9 @@
<app-side-bar
<app-side-bar-user
class="pc-sidebar"
>
</app-side-bar>
</app-side-bar-user>
<div class="pc-container">
<router-outlet></router-outlet>
</div>
<!--<mat-toolbar color="primary">-->
<!-- <button mat-icon-button aria-label="Menu icon" (click)="toggleMenu()">-->
<!-- <mat-icon>menu</mat-icon>-->
<!-- </button>-->
<!-- <h1>Responsive Material Sidenavigation</h1>-->
<!--</mat-toolbar>-->
<!--<mat-sidenav-container autosize="true">-->
<!-- <mat-sidenav [ngClass]="!isCollapsed ? 'expanded' : ''" [mode]="isMobile ? 'over' : 'side'" [opened]="isMobile ? 'false' : 'true'">-->
<!-- <mat-nav-list>-->
<!-- <a mat-list-item>-->
<!-- <span class="entry">-->
<!-- <mat-icon>house</mat-icon>-->
<!-- @if (!isCollapsed) {-->
<!-- <span >Dashboard</span>-->
<!-- }-->
<!-- </span>-->
<!-- </a>-->
<!-- </mat-nav-list>-->
<!-- </mat-sidenav>-->
<!-- <mat-sidenav-content>-->
<!-- Content-->
<!-- <router-outlet></router-outlet>-->
<!-- </mat-sidenav-content>-->
<!--</mat-sidenav-container>-->
@@ -1,49 +1,10 @@
mat-toolbar{
position:fixed;
top:0;
z-index: 2;
}
mat-sidenav-container {
height:100%;
}
// Move the content down so that it won't be hidden by the toolbar
mat-sidenav {
padding-top: 3.5rem;
transition: width 0.3s ease;
@media screen and (min-width: 600px) {
padding-top: 4rem;
}
.entry{
display: flex;
align-items: center;
gap: 1rem;
padding:0.75rem;
}
}
// Move the content down so that it won't be hidden by the toolbar
mat-sidenav-content{
padding-top: 3.5rem;
@media screen and (min-width: 600px) {
padding-top: 4rem;
}
}
.expanded {
width: 250px;
}
//---------------
.pc-sidebar{
top: 65px;
overflow-y: auto;
background-color: rgba(153, 153, 153, 0.16);
backdrop-filter: blur(8px);
}
.pc-container{
top: 0px;
@@ -8,6 +8,7 @@ import {MatIcon} from '@angular/material/icon';
import {BreakpointObserver} from '@angular/cdk/layout';
import {MatToolbar} from '@angular/material/toolbar';
import {MatIconButton} from '@angular/material/button';
import {SideBarUserComponent} from '../side-bar-user.component/side-bar-user.component';
@Component({
selector: 'app-user.component',
@@ -21,7 +22,8 @@ import {MatIconButton} from '@angular/material/button';
MatListItem,
MatIcon,
MatToolbar,
MatIconButton
MatIconButton,
SideBarUserComponent
],
templateUrl: './user.component.html',
styleUrl: './user.component.scss',
@@ -1,3 +1,7 @@
export const environment = {
default_page: 'main/generate-image'
default_page: 'main/generate-image',
port: 8080,
fromWeb: false,
production: false,
host_name: 'http://localhost',
};
@@ -0,0 +1,7 @@
export const environment = {
default_page: 'main/generate-image',
port: 8081,
production: true,
fromWeb: true,
host_name: 'https://jambotron.run.place'
};
+5 -1
View File
@@ -1,3 +1,7 @@
export const environment = {
default_page: 'main/generate-image'
default_page: 'main/generate-image',
port: 8080,
production: true,
fromWeb: false,
host_name: 'http://localhost'
};
+9 -10
View File
@@ -1,15 +1,14 @@
#FROM openjdk:24 AS BUILD_IMAGE
#ENV APP_HOME=/jambotron
#RUN mkdir -p $APP_HOME/src/main/java
#WORKDIR $APP_HOME
#COPY ./build.gradle ./gradlew ./gradlew.bat $APP_HOME/
#COPY gradle $APP_HOME/gradle
#COPY ./src/ $APP_HOME/src/
#RUN ./gradlew clean build
#WORKDIR /jambotron/
#COPY . ./
#RUN microdnf install findutils
#RUN ./gradlew build -x test
FROM openjdk:24
WORKDIR /jambotron/
COPY './build/libs/jambotron-0.0.1-SNAPSHOT.jar' '/app/jambotron.jar'
#COPY --from=BUILD_IMAGE '/jambotron/build/libs/jambotron-0.0.1-SNAPSHOT.jar' '/app/jambotron.jar'
EXPOSE 8080
CMD ["java","-jar","/app/jambotron.jar"]
#COPY --from=BUILD_IMAGE /jambotron/build/libs/jambotron-0.0.1-SNAPSHOT.jar .
#EXPOSE 8080
CMD ["java","-jar","/app/jambotron.jar"]
#CMD ["java","-jar","jambotron-0.0.1-SNAPSHOT.jar"]
+29 -7
View File
@@ -6,19 +6,38 @@ services:
context: ../../
dockerfile: ./src/Docker/Dockerfile
ports:
- "8080:8080"
# - "8081:80"
- "8443:443"
depends_on:
- postgres_jambotron
volumes:
- certs:/certs
# env_file: "webapp.env"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://ep-red-breeze-a2cxhq5f.eu-central-1.pg.koyeb.app:5432/jambotronDB
SPRING_DATASOURCE_USERNAME: koyeb-adm
SPRING_DATASOURCE_PASSWORD: npg_HfFEUA7bay1i
SERVER_PORT: 443
FULLCHAINPEM: /certs/live/jambotron.run.place/fullchain.pem
PRIVKEYPEM: /certs/live/jambotron.run.place/privkey.pem
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres_jambotron:5432/jambotronDB
SPRING_DATASOURCE_USERNAME: admin
SPRING_DATASOURCE_PASSWORD: postgrespw
SPRING_FLYWAY_BASELINE-ON-MIGRATE: "true"
SPRING_FLYWAY_VALIDATE-ON-MIGRATE: "true"
SPRING_FLYWAY_USER: koyeb-adm
SPRING_FLYWAY_PASSWORD: npg_HfFEUA7bay1i
SPRING_FLYWAY_URL: jdbc:postgresql://ep-red-breeze-a2cxhq5f.eu-central-1.pg.koyeb.app:5432/jambotronDB
SPRING_FLYWAY_USER: admin
SPRING_FLYWAY_PASSWORD: postgrespw
SPRING_FLYWAY_URL: jdbc:postgresql://postgres_jambotron:5432/jambotronDB
# SPRING_DATASOURCE_URL: jdbc:postgresql://ep-red-breeze-a2cxhq5f.eu-central-1.pg.koyeb.app:5432/jambotronDB
# SPRING_DATASOURCE_USERNAME: koyeb-adm
# SPRING_DATASOURCE_PASSWORD: npg_HfFEUA7bay1i
# SPRING_FLYWAY_BASELINE-ON-MIGRATE: "true"
# SPRING_FLYWAY_VALIDATE-ON-MIGRATE: "true"
#
# SPRING_FLYWAY_USER: koyeb-adm
# SPRING_FLYWAY_PASSWORD: npg_HfFEUA7bay1i
# SPRING_FLYWAY_URL: jdbc:postgresql://ep-red-breeze-a2cxhq5f.eu-central-1.pg.koyeb.app:5432/jambotronDB
# SPRING_DATASOURCE_PASSWORD: /run/secrets/db_password
# secrets:
@@ -41,3 +60,6 @@ services:
#secrets:
# db_password:
# file: db_password.txt
volumes:
certs:
external: true
+3
View File
@@ -0,0 +1,3 @@
SERVER_PORT=443
FULLCHAINPEM=/certs/live/jambotron.run.place/fullchain.pem
PRIVKEYPEM=/certs/live/jambotron.run.place/privkey.pem
@@ -1,5 +1,6 @@
package com.jambotronGroup.jambotron;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
@@ -7,6 +8,7 @@ import org.slf4j.event.Level;
import org.slf4j.helpers.BasicMarker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
import java.util.Iterator;
@@ -19,9 +21,9 @@ public class JambotronApplication {
SpringApplication.run(JambotronApplication.class, args);
logger.error("Application Run.");
logger.debug("Application Run.");
logger.info("Application Run.");
logger.error("Application Run. This is an error message.");
logger.debug("Application Run. This is a debug message.");
logger.info("Application Run. This is an info message.");
}
}
@@ -12,13 +12,13 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
@CrossOrigin(origins = "http://localhost:4200, https://pony-sincere-chimp.ngrok-free.app/",
maxAge = 3600,
allowCredentials="true",
allowedHeaders = {"Content-Type", "Authorization", "X-Requested-With"}
)
//@CrossOrigin(origins = "http://localhost:4200, https://pony-sincere-chimp.ngrok-free.app/",
// maxAge = 3600,
// allowCredentials="true",
// allowedHeaders = {"Content-Type", "Authorization", "X-Requested-With"}
//)
@RestController
@RequestMapping("/api/zhipuai")
@RequestMapping("/api/public/zhipuai")
public class ImageController {
@Autowired
@@ -32,7 +32,7 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
//@CrossOrigin(origins = "*", maxAge = 3600)
@CrossOrigin(origins = "http://localhost:4200,https://a576-5-248-149-207.ngrok-free.app, https://pony-sincere-chimp.ngrok-free.app", maxAge = 3600, allowCredentials="true")
//@CrossOrigin(origins = "http://localhost:4200,https://a576-5-248-149-207.ngrok-free.app, https://pony-sincere-chimp.ngrok-free.app", maxAge = 3600, allowCredentials="true")
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@@ -20,7 +20,7 @@ import java.time.LocalDateTime;
import java.util.*;
//@CrossOrigin(origins = "http://localhost:4200", maxAge = 3600, allowCredentials="true")
//@CrossOrigin(origins = "http://localhost:4200,http://www.jambotron.run.place", maxAge = 3600, allowCredentials="true")
@RestController
@RequestMapping("/api")
public class TutorialController {
@@ -66,30 +66,6 @@ public class TutorialController {
try {
List<Tutorial> tutorials = new ArrayList<Tutorial>();
/* Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String name = authenticationFacade.getAuthentication().getName();
User user = userRepository.findById(((UserDetailsImpl)userDetails).getId()).get();
if (user.getRoles().stream().anyMatch(role -> role.getName().name().equals("ROLE_ADMIN"))) {*/
/*
if (title == null)
tutorialRepository.findAll().forEach(tutorials::add);
else
tutorialRepository.findByTitleContaining(title).forEach(tutorials::add);
*/
/* } else {
// If the user is not an admin, filter tutorials by user
tutorialRepository.findByUserId(user.getId()).forEach(tutorials::add);
if (tutorials.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(tutorials, HttpStatus.OK);
}*/
//all published tutorials without authentication
tutorialRepository.findByPublished(true).forEach(tutorials::add);
if (tutorials.isEmpty()) {
@@ -102,17 +78,44 @@ public class TutorialController {
}
}
@GetMapping("/moderator/tutorials")
public ResponseEntity<List<Tutorial>> getBePublishedTutorials(@RequestParam(required = false) String title) {
try {
List<Tutorial> tutorials = new ArrayList<Tutorial>();
tutorialRepository.findBytobepublished(true).forEach(tutorials::add);
if (tutorials.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(tutorials, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("user/tutorial-add")
public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// not work from white IP port 80 to docker container port 8080 or 8081
//UserDetails userDetails = authenticationFacade.getUserDetails();
User user = userRepository.findById(((UserDetailsImpl)userDetails).getId()).get();
User user = authenticationFacade.getUser();
try {
Tutorial newTutorial =new Tutorial(
tutorial.getTitle(),
tutorial.getDescription(),
false,
false,
user,
Timestamp.valueOf(LocalDateTime.now()),
Timestamp.valueOf(LocalDateTime.now())
);
Tutorial _tutorial = tutorialRepository
.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), false, user, Timestamp.valueOf(LocalDateTime.now()), Timestamp.valueOf(LocalDateTime.now())));
.save(newTutorial);
return new ResponseEntity<>(_tutorial, HttpStatus.CREATED);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
@@ -131,6 +134,18 @@ public class TutorialController {
}
}
@GetMapping("moderator/tutorial-get/{id}")
public ResponseEntity<Tutorial> getBePublishedTutorial(@PathVariable("id") long id) {
Optional<Tutorial> tutorialData = tutorialRepository.findById(id);
if (tutorialData.isPresent()) {
return new ResponseEntity<Tutorial>(tutorialData.get(), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@PutMapping("user/tutorial-update/{id}")
public ResponseEntity<?> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
Optional<Tutorial> tutorialData = tutorialRepository.findById(id);
@@ -140,6 +155,31 @@ public class TutorialController {
servTutorial.setTitle(tutorial.getTitle());
servTutorial.setDescription(tutorial.getDescription());
servTutorial.setPublished(tutorial.isPublished());
servTutorial.setTobepublished(tutorial.isTobepublished());
try {
servTutorial = tutorialRepository.save(servTutorial);
} catch (Exception e) {
map.put("status", 0);
map.put("message", e.getMessage());
return new ResponseEntity<>(map,HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(servTutorial, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@PutMapping("moderator/tutorial-update/{id}")
public ResponseEntity<?> moderatorUpdateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
Optional<Tutorial> tutorialData = tutorialRepository.findById(id);
Map<String, Object> map = new LinkedHashMap<String, Object>();
if (tutorialData.isPresent()) {
Tutorial servTutorial = tutorialData.get();
servTutorial.setTitle(tutorial.getTitle());
servTutorial.setDescription(tutorial.getDescription());
servTutorial.setPublished(tutorial.isPublished());
servTutorial.setTobepublished(tutorial.isTobepublished());
try {
servTutorial = tutorialRepository.save(servTutorial);
} catch (Exception e) {
@@ -1,6 +1,7 @@
package com.jambotronGroup.jambotron.controllers;
import com.jambotronGroup.jambotron.model.Tutorial;
import com.jambotronGroup.jambotron.model.User;
import com.jambotronGroup.jambotron.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
@@ -8,8 +9,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
@CrossOrigin(origins = "${app.origin}", maxAge = 3600, allowCredentials="true")
@RestController
@@ -35,4 +35,31 @@ public class UsersController {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PutMapping("users/{id}")
public ResponseEntity<?> updateUserRoles(@PathVariable("id") long id, @RequestBody User user) {
Optional<User> userData = userRepository.findById(id);
Map<String, Object> map = new LinkedHashMap<String, Object>();
if (userData.isPresent()) {
User servUser = userData.get();
if(user.getRoles().size() > 0)
servUser.setRoles(user.getRoles());
else {
map.put("status", 0);
map.put("message", "User must have at least one role");
return new ResponseEntity<>(map, HttpStatus.BAD_REQUEST);
}
try {
servUser = userRepository.save(servUser);
} catch (Exception e) {
map.put("status", 0);
map.put("message", e.getMessage());
return new ResponseEntity<>(map,HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(servUser, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}
@@ -2,6 +2,7 @@ package com.jambotronGroup.jambotron.model;
import jakarta.persistence.*;
import java.sql.Timestamp;
@Entity
@@ -21,6 +22,9 @@ public class Tutorial {
@Column(name = "published")
private boolean published;
@Column(name = "tobepublished")
private boolean tobepublished;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "userID", nullable = false)
private User user;
@@ -37,10 +41,11 @@ public class Tutorial {
}
public Tutorial(String title, String description, boolean published, User user, java.sql.Timestamp created, java.sql.Timestamp modified) {
public Tutorial(String title, String description, boolean published, boolean tobepublished, User user, Timestamp created, Timestamp modified) {
this.title = title;
this.description = description;
this.published = published;
this.tobepublished = tobepublished;
this.user = user;
this.created = created;
this.modified = modified;
@@ -74,6 +79,14 @@ public class Tutorial {
this.published = isPublished;
}
public boolean isTobepublished() {
return tobepublished;
}
public void setTobepublished(boolean tobepublished) {
this.tobepublished = tobepublished;
}
public void setCreated(java.sql.Timestamp created) {
this.created = created;
}
@@ -19,6 +19,7 @@ import java.util.Set;
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "username")
@@ -14,5 +14,7 @@ public interface TutorialRepository extends JpaRepository<Tutorial, Long> {
List<Tutorial> findByUserId(Long userId);
List<Tutorial> findByUserIdAndTitle(Long userId, String title);
List<Tutorial> findByPublished(boolean published);
List<Tutorial> findBytobepublished(boolean tobepublished);
List<Tutorial> findByIdAndTobepublished(Long id,boolean tobepublished);
List<Tutorial> findByTitleContaining(String title);
}
@@ -1,29 +1,76 @@
package com.jambotronGroup.jambotron.security;
import com.jambotronGroup.jambotron.controllers.AuthController;
import com.jambotronGroup.jambotron.model.User;
import com.jambotronGroup.jambotron.repository.UserRepository;
import com.jambotronGroup.jambotron.security.services.UserDetailsImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class AuthenticationFacade implements IAuthenticationFacade {
private static final Logger logger = LoggerFactory.getLogger(AuthenticationFacade.class);
@Autowired
UserRepository userRepository;
@Override
public Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
//Deprecated method, use getUser() instead
@Override
public UserDetailsImpl getUserDetails() {
logger.warn("Retrieving user details from the security context");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
if (authentication == null || !authentication.isAuthenticated()) {
throw new IllegalStateException("No authenticated user found");
}
if (!(authentication.getPrincipal() instanceof UserDetails)) {
throw new IllegalStateException("Authentication principal is not an instance of UserDetails");
}
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
if (!(userDetails instanceof UserDetailsImpl)) {
throw new IllegalStateException("UserDetails is not an instance of UserDetailsImpl");
}
return (UserDetailsImpl) userDetails;
}
@Override
public com.jambotronGroup.jambotron.model.User getUser() {
User returnValue = null;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication.getPrincipal() instanceof UserDetails)) {
logger.info("Authentication principal is not an instance of UserDetails, returning null");
logger.info(authentication.toString());
Optional<User> user = userRepository.findByUsername(authentication.getPrincipal().toString());
returnValue = user.get();
}else{
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
UserDetailsImpl userDetailsImpl = (UserDetailsImpl) userDetails;
returnValue = userRepository.findById(userDetailsImpl.getId()).get();
}
return returnValue;
}
}
@@ -1,5 +1,6 @@
package com.jambotronGroup.jambotron.security;
import com.jambotronGroup.jambotron.model.User;
import com.jambotronGroup.jambotron.security.services.UserDetailsImpl;
import org.springframework.security.core.Authentication;
@@ -7,4 +8,6 @@ public interface IAuthenticationFacade {
Authentication getAuthentication();
UserDetailsImpl getUserDetails();
User getUser();
}

Some files were not shown because too many files have changed in this diff Show More