Compare commits

...

40 Commits

Author SHA1 Message Date
641c340b15 added prettier 2020-08-11 18:39:42 +05:30
af4181185c added formatter plugin 2020-08-11 17:30:48 +05:30
ced84a05a6 Updated migration script v3
now sets unix permissions to 600 for the generated password file
2020-08-11 15:05:40 +05:30
474d014f09 frontend dependencies security updates 2020-08-08 23:27:11 +05:30
a5346f6970 Bumped spring boot minor version to 2.1.16 2020-08-06 01:32:14 +05:30
85b831e05c added prettier config 2020-08-05 14:38:22 +05:30
911ea52b96 Removed obsolete html file 2020-08-05 14:38:15 +05:30
385fe25dab Removed css files
Removed datatables cssfile
Removed sb admin css file
Load sb admin css file via cdn
2020-08-04 22:31:43 +05:30
8ceadfa28a Added new user button handler in admin 2020-07-31 12:45:48 +05:30
42f8978d06 Updated security config 2020-07-31 12:43:17 +05:30
2a2844afcc Removed deprecated css file 2020-07-31 01:40:59 +05:30
943171f35a Added scrollbar styling 2020-07-31 01:21:51 +05:30
0037f07559 Improved admin user rest API semantics 2020-07-31 00:35:41 +05:30
40cc234b83 Added delete user handler in admin #13 2020-07-30 23:22:57 +05:30
dce2e3ece3 Added preview image to readme 2020-07-30 13:35:35 +05:30
24ae4f99ec Updated gruntfile 2020-07-30 13:35:35 +05:30
852ecb4993 Added buttons and selection mechanism 2020-07-30 13:35:35 +05:30
b5373fd286 Updated package.json watch admin script 2020-07-30 13:35:35 +05:30
9ebc9f6188 updated readme 2020-07-30 13:35:35 +05:30
f7db51db63 Added property metadata 2020-07-30 13:35:35 +05:30
b3e92aec86 Removed unused section in homepage 2020-07-30 13:35:35 +05:30
7ab40e7fd3 Updated readme 2020-07-30 13:35:35 +05:30
561f9dc60f Add license image to readme 2020-07-30 13:35:35 +05:30
c0cfd538db added license image 2020-07-30 13:35:35 +05:30
d86cb08338 Changed home page size 2020-07-30 13:35:35 +05:30
60cdc31b3f Updated footer design 2020-07-30 13:35:35 +05:30
6a7f291e5c Added git info plugin and footer 2020-07-26 20:02:13 +05:30
b9ad037f60 added spinners to admin home 2020-07-26 20:02:13 +05:30
e443ea3fd4 removed log setLevel in admin 2020-07-26 20:02:13 +05:30
3eb617f272 added chat worker prod bundle 2020-07-26 20:02:13 +05:30
ecfcecb53c added dedent to fix js banner formatting 2020-07-26 20:02:13 +05:30
795ca1b682 Added admin bundle to grunt build 2020-07-24 14:14:02 +05:30
9119fe57f3 Added basic admin user profile page 2020-07-24 13:14:43 +05:30
e569420ca5 added paginated users table 2020-07-24 13:14:43 +05:30
b6c4cb1575 added api endpoint for displaying user in admin 2020-07-24 13:14:43 +05:30
85a8f41da0 simplified jqeury code 2020-07-24 13:14:43 +05:30
05ba6fbacc Added stats to admin UI #11 2020-07-24 13:14:43 +05:30
77bff3924c Added stats endpoint 2020-07-24 13:14:43 +05:30
4e40f0bdd8 Added basic statistics service #11 2020-07-24 13:14:43 +05:30
b725458a8a [backend] Removed obsolete user service method 2020-07-24 13:14:43 +05:30
103 changed files with 2578 additions and 23935 deletions

3
.gitignore vendored
View File

@ -29,6 +29,7 @@ build/
### VS Code ###
.vscode/
.cache/
node_modules
bundle.js
@ -36,6 +37,8 @@ bundle.min.js
adminBundle.js
adminBundle.min.js
worker.js
chatWorker.js
chatWorker.min.js
node
src/main/javascript/node/
dist

51
.prettierignore Normal file
View File

@ -0,0 +1,51 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/
.cache/
node_modules
bundle.js
bundle.min.js
adminBundle.js
adminBundle.min.js
worker.js
chatWorker.js
chatWorker.min.js
node
src/main/javascript/node/
dist
out
yarn-error.log
gen-password.txt
**/static/**
License.md

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"useTabs": false,
"printWidth": 80
}

View File

@ -1,97 +1,140 @@
module.exports = function (grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
terser: {
options: {
// Task-specific options go here.
},
build: {
// Target-specific file lists and/or options go here.
src: 'src/main/resources/static/js/bundle.js',
dest: 'src/main/resources/static/js/bundle.min.js'
},
chat_worker: {
src: 'src/main/resources/static/js/worker.js',
dest: 'src/main/resources/static/js/worker.js'
},
},
banner: `
const dedent = require("dedent-js");
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
terser: {
options: {
// Task-specific options go here.
},
build: {
// Target-specific file lists and/or options go here.
src: "src/main/resources/static/js/bundle.js",
dest: "src/main/resources/static/js/bundle.min.js",
},
chat_worker: {
src: "src/main/resources/static/js/chatWorker.js",
dest: "src/main/resources/static/js/chatWorker.min.js",
},
admin_bundle: {
src: "src/main/resources/static/js/adminBundle.js",
dest: "src/main/resources/static/js/adminBundle.min.js",
},
},
banner: dedent(`
/*
* -----------------------------------------
* @date <%= grunt.template.today("yyyy-mm-dd") %>
* @project Chatto
* @author nova
* @license GPL
* -----------------------------------------
`,
usebanner: {
dist: {
options: {
position: 'top',
banner: '/*! <%= banner %> */ '
},
files: {
src: ['src/main/resources/static/js/bundle.min.js', 'src/main/resources/static/js/worker.js']
}
}
*/
`),
usebanner: {
dist: {
options: {
position: "top",
banner: "<%= banner %>",
},
browserify: {
chat_worker_dev: {
src: 'src/main/frontend/workers/encryption-worker/main.ts',
dest: 'src/main/resources/static/js/worker.js',
options: {
browserifyOptions: {
debug: true
},
}
files: {
src: [
"src/main/resources/static/js/bundle.min.js",
"src/main/resources/static/js/adminBundle.min.js",
"src/main/resources/static/js/chatWorker.min.js",
],
},
},
},
browserify: {
chat_worker_dev: {
src: "src/main/frontend/workers/encryption-worker/main.ts",
dest: "src/main/resources/static/js/chatWorker.js",
options: {
browserifyOptions: {
debug: true,
},
},
},
dev: {
src: "src/main/frontend/chat/main.ts",
dest: "src/main/resources/static/js/bundle.js",
options: {
browserifyOptions: {
debug: true,
},
},
},
prod: {
src: "src/main/frontend/chat/main.ts",
dest: "src/main/resources/static/js/bundle.js",
banner: '/*! Chat.js <%= grunt.template.today("yyyy-mm-dd") %> */ ',
options: {
browserifyOptions: {
debug: false,
},
},
},
chat_worker_prod: {
src: "src/main/frontend/workers/encryption-worker/main.ts",
dest: "src/main/resources/static/js/chatWorker.js",
},
admin_bundle_dev: {
src: "src/main/frontend/admin/main.ts",
dest: "src/main/resources/static/js/adminBundle.js",
options: {
browserifyOptions: {
debug: true,
},
},
},
admin_bundle_prod: {
src: "src/main/frontend/admin/main.ts",
dest: "src/main/resources/static/js/adminBundle.js",
banner: '/*! Chat.js <%= grunt.template.today("yyyy-mm-dd") %> */ ',
options: {
browserifyOptions: {
debug: false,
},
},
},
options: {
plugin: [
[
"tsify",
{
target: "ES6",
noImplicitAny: true,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
},
dev: {
src: 'src/main/frontend/chat/main.ts',
dest: 'src/main/resources/static/js/bundle.js',
options: {
browserifyOptions: {
debug: true
},
}
},
prod: {
src: 'src/main/frontend/chat/main.ts',
dest: 'src/main/resources/static/js/bundle.js',
banner: '/*! Chat.js <%= grunt.template.today("yyyy-mm-dd") %> */ ',
options: {
browserifyOptions: {
debug: false
},
}
},
chat_worker_prod: {
src: 'src/main/frontend/workers/encryption-worker/main.ts',
dest: 'src/main/resources/static/js/worker.min.js',
},
options: {
plugin: [
['tsify', { target: 'ES6', noImplicitAny: true, esModuleInterop: true, allowSyntheticDefaultImports: true }], // register plugin by name
],
], // register plugin by name
],
browserifyOptions: {},
},
},
});
browserifyOptions: {},
}
}
});
// Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks("grunt-terser");
// Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks('grunt-terser');
// // Default task(s).
// grunt.registerTask('default', ['uglify']);
// // Default task(s).
// grunt.registerTask('default', ['uglify']);
grunt.loadNpmTasks("grunt-browserify");
grunt.loadNpmTasks("grunt-banner");
grunt.loadNpmTasks('grunt-browserify')
grunt.loadNpmTasks('grunt-banner');
grunt.registerTask('default', ['browserify:dev', 'browserify:chat_worker_dev'])
grunt.registerTask('prod', ["browserify:prod", 'browserify:chat_worker_dev', "terser", 'usebanner'])
};
grunt.registerTask("default", [
"browserify:dev",
"browserify:chat_worker_dev",
"browserify:admin_bundle_dev",
]);
grunt.registerTask("prod", [
"browserify:prod",
"browserify:chat_worker_prod",
"browserify:admin_bundle_prod",
"terser",
"usebanner",
]);
};

View File

@ -1,8 +1,16 @@
# Chatto
A self hosted chat application with end-to-end encrypted messaging.
![Chatto Preview](chatto-preview-main.png)
Documentation available [here](https://git.arcusiridis.com/nova/Chatto/wiki)
The backend is written in Java, and uses Spring Boot. The app itself provides a front end written with thymeleaf templates. The chat AJAX logic is written in TypeScript without any JS framework. The app exposes a JSON API that the inbuilt front end uses to provide chat functionality. However, this API can also be used by a separate client app to provide the same functions.
A WIP client made in ScalaFX is available [here](https://git.arcusiridis.com/nova/Chatto-Desktop-ScalaFX). It is a complete rewrite in Scala of the previous client, which was also written using Java and Spring Boot.
A WIP client made in ScalaFX is available [here](https://git.arcusiridis.com/nova/Chatto-Desktop-ScalaFX). It is a complete rewrite in Scala of the previous client, which was also written using Java and Spring Boot.
## License
Chatto is [Free Software](https://www.gnu.org/philosophy/free-sw.en.html), licensed under AGPLv3.
![AGPLv3](agpl-v3-logo.png)

BIN
agpl-v3-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
chatto-preview-main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

View File

@ -1,6 +1,9 @@
{
"dependencies": {
"@types/bootbox": "^5.2.0",
"@types/datatables.net": "^1.10.19",
"@types/datatables.net-buttons": "^1.4.3",
"@types/datatables.net-select": "^1.2.6",
"@types/dompurify": "^2.0.0",
"@types/jquery": "^3.3.31",
"@types/markdown-it": "^0.0.9",
@ -10,6 +13,7 @@
"bootbox": "^5.4.0",
"builder-pattern": "^1.2.3",
"chart.js": "^2.9.3",
"dedent-js": "^1.0.1",
"dompurify": "^2.0.7",
"fuse.js": "^3.4.6",
"global": "^4.4.0",
@ -30,6 +34,7 @@
"grunt-banner": "^0.6.0",
"grunt-browserify": "^5.3.0",
"grunt-terser": "^1.0.0",
"prettier": "2.0.5",
"tsify": "^4.0.1",
"typescript": "^3.7.4",
"uglify-js": "^3.7.5"
@ -56,6 +61,6 @@
},
"scripts": {
"watch": "watchify src/main/frontend/chat/main.ts -p [ tsify --target ES6 --noImplicitAny ] --debug -o src/main/resources/static/js/bundle.js",
"watch-admin": "watchify src/main/frontend/admin/main.ts -p [ tsify --target ES6 --noImplicitAny ] --debug -o src/main/resources/static/js/adminBundle.js"
"watch:admin": "watchify src/main/frontend/admin/main.ts -p [ tsify --target ES6 --noImplicitAny ] --debug -o src/main/resources/static/js/adminBundle.js"
}
}
}

44
pom.xml
View File

@ -4,7 +4,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<version>2.1.16.RELEASE</version>
<relativePath />
</parent>
<groupId>org.ros</groupId>
@ -103,6 +103,11 @@
<artifactId>flyway-core</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.spencerwi</groupId>
<artifactId>Either.java</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
@ -192,6 +197,43 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>4.0.1</version>
<executions>
<execution>
<id>get-the-git-infos</id>
<goals>
<goal>revision</goal>
</goals>
</execution>
<execution>
<id>validate-the-git-infos</id>
<goals>
<goal>validateRevision</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<excludeProperties>
<excludeProperty>git.user.*</excludeProperty>
</excludeProperties>
</configuration>
</plugin>
<plugin>
<groupId>net.revelc.code.formatter</groupId>
<artifactId>formatter-maven-plugin</artifactId>
<version>2.12.0</version>
<configuration>
<configFile>${project.basedir}/eclipse-formatter.xml</configFile>
<compilerSource>11</compilerSource>
<compilerCompliance>11</compilerCompliance>
<compilerTargetPlatform>11</compilerTargetPlatform>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -2,11 +2,26 @@ import { changePassphrase } from "./pages/user/ChangePassphrase";
import { EncryptionServiceFactory } from "../common/service/EncryptionServiceFactory";
import log from "loglevel";
import { AlertifyNotificationService } from "../common/service/AlertifyNotificationService";
import { stats } from "./pages/Home";
import { Credentials } from "../common/global/Credentials";
import { getOtherUsers } from "../common/ajax/Users";
import { viewUsers } from "./pages/user/ViewUsers";
log.setLevel("TRACE");
// log.setLevel("TRACE");
const es = EncryptionServiceFactory.getEncryptionService();
const ns = new AlertifyNotificationService();
const authToken = Credentials.authToken;
$("#changePassphraseForm").on("submit", (event) => {
event.preventDefault();
changePassphrase(es, ns);
changePassphrase(es, ns, authToken);
});
const pathMatcher = (path: string) => window.location.pathname == path;
if (pathMatcher("/admin")) {
stats();
}
if (pathMatcher("/admin/users")) {
// $("#usersTableBody").html("");
viewUsers(authToken);
}

View File

@ -0,0 +1,18 @@
import { getStats } from "../../common/ajax/Messages";
import { Credentials } from "../../common/global/Credentials";
import log from "loglevel";
export async function stats(): Promise<void> {
const stats = await getStats(Credentials.authToken);
setTimeout(() => {
$(".admin-stats-spinner").hide();
$("#totalMessages").text(stats.totalMessages).removeAttr("hidden");
$("#totalUsers").text(stats.totalUsers).removeAttr("hidden");
$("#totalOnlineUsers").text(stats.totalOnlineUsers).removeAttr("hidden");
$("#numMessagesToday").text(stats.numMessagesToday).removeAttr("hidden");
}, 1000);
log.debug(stats);
}

View File

@ -7,15 +7,16 @@ import { Credentials } from "../../../common/global/Credentials";
import { MessageCipher } from "../../../common/entity/MessageCipher";
import log from "loglevel";
import { NotificationService } from "../../../common/service/NotificationService";
import { isPassphraseValid } from "../../../common/util/passphrase";
import { isPassphraseValid } from "../../../common/util/Passphrase";
import {
getAllMessages,
sendReencryptedMessages,
} from "../../../common/ajax/messages";
} from "../../../common/ajax/Messages";
export async function changePassphrase(
es: EncryptionService,
ns: NotificationService
ns: NotificationService,
authToken: string
): Promise<void> {
// $("#changePassphraseForm").val();
@ -26,7 +27,7 @@ export async function changePassphrase(
const passphraseOld: string = $("#passphraseOld").val() as string;
const passphraseNew: string = $("#passphraseNew").val() as string;
const valid = await isPassphraseValid(passphraseOld, user, es);
const valid = await isPassphraseValid(passphraseOld, user, es, authToken);
if (!valid) {
log.error("Please check your passphrase");

View File

@ -0,0 +1,75 @@
import moment from "moment";
import { capitalize } from "../../../common/util/Util";
import { AdminUserDTO } from "../../../common/dto/AdminUserDTO";
import log from "loglevel";
import { createApiHeaders } from "../../../common/ajax/util";
export async function viewUsers(authToken: string) {
// const users = await getOtherUsers(authToken);
const usersTable = $("#usersTable").DataTable({
ajax: {
url: "/api/admin/get/users",
headers: {
"X-AUTH-TOKEN": authToken,
},
dataSrc: "",
},
columns: [
{ data: "id" },
{ data: "userName" },
{
data: "role",
render: (data: string) => {
return capitalize(data.replace("_", " ").toLowerCase());
},
},
{
data: "joinDate",
render: (data: string, type) => {
return type === "sort"
? data
: moment.utc(data).format("DD/MM/YY").toString();
},
},
],
buttons: [
{
extend: "selectedSingle",
text: "User Profile",
action: (e, dt, button, config) => {
const username = (dt.row({ selected: true }).data() as AdminUserDTO)
.userName;
window.location.assign(`/admin/users/${username}`);
},
},
{
extend: "selectedSingle",
text: "Delete User",
action: (e, dt, button, config) => {
const selectedRow = dt.row({ selected: true });
const username = (selectedRow.data() as AdminUserDTO).userName;
fetch(`/api/admin/delete/users/${username}`, {
headers: createApiHeaders(authToken),
method: "DELETE",
}).then((resp) => {
if (resp.status === 200) {
selectedRow.remove();
log.info("User deleted successfully");
dt.draw();
}
});
},
},
{
text: "New User",
action: () => {
window.location.assign("/admin/users?mode=new");
},
},
],
dom: "Blfrtip",
lengthMenu: [2, 10, 25, 50, 75, 100],
select: true,
});
}

View File

@ -4,27 +4,21 @@ import { ReencryptionDTO } from "../dto/ReencryptionDTO";
import { ChatMessageDTO } from "../dto/ChatMessageDTO";
import { JsonAPI } from "../../chat/singleton/JsonAPI";
import * as log from "loglevel";
import { StatsDTO } from "../dto/StatsDTO";
import { createApiHeaders } from "./util";
export async function getAllMessages(user: string, authToken: string) {
let headers = new Headers();
// headers.append('Accept','application/json')
// headers.append('Content-Type', 'application/json');
headers.append("X-AUTH-TOKEN", authToken);
let response = await fetch(`${Routes.Admin.getAllMessagesURL}${user}`, {
let response = await fetch(`${Routes.Admin.getAllMessages}${user}`, {
method: "GET",
headers: headers,
headers: createApiHeaders(authToken),
});
return response.json() as Promise<ReencryptionDTO[]>;
}
async function getAllRegularUsers(authToken: string) {
let headers = new Headers();
// headers.append('Accept','application/json')
// headers.append('Content-Type', 'application/json');
headers.append("X-AUTH-TOKEN", authToken);
let response = await fetch(`${Routes.Admin.getAllRegularUsersURL}`, {
let response = await fetch(`${Routes.Admin.getOtherUsers}`, {
method: "GET",
headers: headers,
headers: createApiHeaders(authToken),
});
let data = (await response.json()) as string[];
return data;
@ -40,7 +34,7 @@ export async function sendReencryptedMessages(
// headers.append('Accept','application/json')
headers.append("Content-Type", "application/json");
headers.append("X-AUTH-TOKEN", authToken);
fetch(Routes.Admin.reencryptURL, {
fetch(Routes.Admin.reencrypt, {
method: "POST",
headers: headers,
body: JSON.stringify(rencryptionDTOs),
@ -49,19 +43,14 @@ export async function sendReencryptedMessages(
export async function getOneMessage(
toUser: string,
page: number
page: number,
authToken: string
): Promise<ChatMessageDTO[]> {
const headers = new Headers();
if (JsonAPI.authToken == null) {
log.error("authToken null");
return [];
}
headers.append("X-AUTH-TOKEN", JsonAPI.authToken);
const url = Sprintf(JsonAPI.CHAT_MESSAGE_PAGE_GET, toUser, page, 1);
log.debug(url);
const response = await fetch(url, {
method: "GET",
headers: headers,
headers: createApiHeaders(authToken),
});
log.debug(response.clone());
// if (fetchErrorHandler(response.clone(), this._notificationService)) {
@ -80,3 +69,11 @@ export async function getOneMessage(
const data2 = func(data);
return data2;
}
export async function getStats(authToken: string) {
const response = await fetch("/api/stats/", {
headers: createApiHeaders(authToken),
method: "GET",
});
return (await response.json()) as StatsDTO;
}

View File

@ -0,0 +1,15 @@
import { Routes } from "../routes/Routes";
import { createApiHeaders } from "./util";
import { AdminUserDTO } from "../dto/AdminUserDTO";
export async function getOtherUsers(
authToken: string
): Promise<AdminUserDTO[]> {
const response = await fetch(Routes.Admin.getOtherUsers, {
method: "GET",
headers: createApiHeaders(authToken),
});
let data = (await response.json()) as AdminUserDTO[];
return data;
}

View File

@ -0,0 +1,9 @@
export function createApiHeaders(authToken: string): Headers {
const headers = new Headers();
// headers.append("Content-Type", "application/json");
headers.append("X-AUTH-TOKEN", authToken);
return headers;
}

View File

@ -0,0 +1,6 @@
export interface AdminUserDTO {
id: number;
userName: string;
role: string;
joinDate: string;
}

View File

@ -0,0 +1,6 @@
export interface StatsDTO {
totalMessages: number;
totalOnlineUsers: number;
totalUsers: number;
numMessagesToday: number;
}

View File

@ -1,5 +1,5 @@
export namespace Admin {
export const getAllMessagesURL = `/api/admin/get/messages/`; //hostAddress set in thymeleaf backend
export const reencryptURL = `/api/admin/post/re-encrypt`;
export const getAllRegularUsersURL = `/api/admin/get/users`;
export const getAllMessages = `/api/admin/get/messages/`; //hostAddress set in thymeleaf backend
export const reencrypt = `/api/admin/post/re-encrypt`;
export const getOtherUsers = `/api/admin/get/users`;
}

View File

@ -0,0 +1,3 @@
export namespace Stats {
export const rootStats = "/api/stats/"
}

View File

@ -4,9 +4,13 @@ import { SJCLEncryptionService } from "./SJCLEncryptionService";
import PromiseWorker from "promise-worker";
export class EncryptionServiceFactory {
private static readonly _worker = new Worker('/js/worker.js');
private static readonly _promiseWorker = new PromiseWorker(EncryptionServiceFactory._worker);
public static getEncryptionService(): EncryptionService {
return new SJCLEncryptionService(this._promiseWorker)
}
}
private static readonly _worker = new Worker(
localStorage.getItem("CHAT_WORKER_BUNDLE") || "chat-worker-error.js"
);
private static readonly _promiseWorker = new PromiseWorker(
EncryptionServiceFactory._worker
);
public static getEncryptionService(): EncryptionService {
return new SJCLEncryptionService(this._promiseWorker);
}
}

View File

@ -1,14 +1,15 @@
import { EncryptionService } from "../service/EncryptionService";
import { ChatMessageDTO } from "../dto/ChatMessageDTO";
import * as log from "loglevel";
import { getOneMessage } from "../ajax/messages";
import { getOneMessage } from "../ajax/Messages";
export async function isPassphraseValid(
passphrase: string,
userName: string,
es: EncryptionService
es: EncryptionService,
authToken: string
): Promise<boolean> {
const messages: ChatMessageDTO[] = await getOneMessage(userName, 0);
const messages: ChatMessageDTO[] = await getOneMessage(userName, 0, authToken);
if (messages.length === 0) return true;
try {
es.decrypt(passphrase, messages[0].messageCipher);

View File

@ -0,0 +1,16 @@
/**
* Capitalizes first letters of words in string.
* @param {string} str String to be modified
* @param {boolean=false} lower Whether all other letters should be lowercased
* @return {string}
* @usage
* capitalize('fix this string'); // -> 'Fix This String'
* capitalize('javaSCrIPT'); // -> 'JavaSCrIPT'
* capitalize('javaSCrIPT', true); // -> 'Javascript'
*/
export function capitalize(str: string, lower = false): string {
return (lower ? str.toLowerCase() : str).replace(
/(?:^|\s|["'([{])+\S/g,
(match) => match.toUpperCase()
);
}

View File

@ -1,15 +1,34 @@
{"properties": [{
"name": "chatto.token.timeout-duration",
"type": "java.lang.String",
"description": "The duration for auth token validity. Token expires after this period of inactivity"
},
{
"name": "chatto.frontend.log-level",
"type": "java.lang.String",
"description": "The log level for the frontend JS application"
},
{
"name": "chatto.frontend.chat-page-size",
"type": "java.lang.Integer",
"description": "The pagination size for the chat area"
}]}
"properties" : [
{
"name" : "chatto.token.timeout-duration",
"type" : "java.lang.String",
"description" : "The duration for auth token validity. Token expires after this period of inactivity"
},
{
"name" : "chatto.frontend.log-level",
"type" : "java.lang.String",
"description" : "The log level for the frontend JS application"
},
{
"name" : "chatto.frontend.chat-page-size",
"type" : "java.lang.Integer",
"description" : "The pagination size for the chat area"
},
{
"name" : "chat-worker-bundle",
"type" : "java.lang.String",
"description" : "Name of the chat worker js bundle"
},
{
"name" : "chat-bundle",
"type" : "java.lang.String",
"description" : "Name of the chatjs bundle"
},
{
"name" : "admin-bundle",
"type" : "java.lang.String",
"description" : "Name of the admin js bundle"
}
]
}

View File

@ -2,6 +2,9 @@ package db.migration;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.security.SecureRandom;
import java.sql.PreparedStatement;
@ -12,50 +15,75 @@ import org.springframework.security.crypto.password.PasswordEncoder;
public class V3__add_default_admin extends BaseJavaMigration {
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private final SecureRandom random = new SecureRandom();
private final SecureRandom random = new SecureRandom();
/** different dictionaries used */
private final String ALPHA_CAPS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private final String ALPHA = "abcdefghijklmnopqrstuvwxyz";
private final String NUMERIC = "0123456789";
private final String SPECIAL_CHARS = "!@#$%^&*_=+-/";
/** different dictionaries used */
private final String ALPHA_CAPS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private final String ALPHA = "abcdefghijklmnopqrstuvwxyz";
private final String NUMERIC = "0123456789";
private final String SPECIAL_CHARS = "!@#$%^&*_=+-/";
/**
* Method will generate random string based on the parameters
*
* @param len the length of the random string
* @param dic the dictionary used to generate the password
* @return the random password
*/
public String generatePassword(int len, String dic) {
String result = "";
for (int i = 0; i < len; i++) {
int index = random.nextInt(dic.length());
result += dic.charAt(index);
}
return result;
}
/**
* Method will generate random string based on the parameters
*
* @param len
* the length of the random string
* @param dic
* the dictionary used to generate the password
* @return the random password
*/
public String generatePassword(int len, String dic) {
String result = "";
for (int i = 0; i < len; i++) {
int index = random.nextInt(dic.length());
result += dic.charAt(index);
}
return result;
}
@Override
public void migrate(final Context context) throws Exception {
try (final PreparedStatement ps = context.getConnection()
.prepareStatement("insert into users (user_id, name, password) values (0,?,?)")) {
final String generatedPassword = generatePassword(60, ALPHA_CAPS + ALPHA + SPECIAL_CHARS);
final BufferedWriter bw = new BufferedWriter(new FileWriter("gen-password.txt"));
bw.write(generatedPassword);
bw.write("\nPlease delete this file");
bw.close();
ps.setString(1, "admin");
ps.setString(2, passwordEncoder.encode(generatedPassword));
ps.execute();
}
@Override
public void migrate(final Context context) throws Exception {
try (final PreparedStatement ps = context.getConnection()
.prepareStatement(
"insert into users (user_id, name, password) values (0,?,?)")) {
final String generatedPassword = generatePassword(60,
ALPHA_CAPS + ALPHA + SPECIAL_CHARS);
final BufferedWriter bw = new BufferedWriter(
new FileWriter("gen-password.txt"));
try (final PreparedStatement ps = context.getConnection()
.prepareStatement("insert into users_roles (user_id, role_id) values (1,0)")) {
ps.execute();
}
}
bw.write(generatedPassword);
bw.write("\nPlease delete this file");
bw.close();
final var perms = Files
.getPosixFilePermissions(Paths.get("gen-password.txt"));
// add owners permission
perms.add(PosixFilePermission.OWNER_READ);
perms.add(PosixFilePermission.OWNER_WRITE);
perms.remove(PosixFilePermission.OWNER_EXECUTE);
// add group permissions
perms.remove(PosixFilePermission.GROUP_READ);
perms.remove(PosixFilePermission.GROUP_WRITE);
perms.remove(PosixFilePermission.GROUP_EXECUTE);
// add others permissions
perms.remove(PosixFilePermission.OTHERS_READ);
perms.remove(PosixFilePermission.OTHERS_WRITE);
perms.remove(PosixFilePermission.OTHERS_EXECUTE);
Files.setPosixFilePermissions(Paths.get("gen-password.txt"), perms);
ps.setString(1, "admin");
ps.setString(2, passwordEncoder.encode(generatedPassword));
ps.execute();
}
try (final PreparedStatement ps = context.getConnection()
.prepareStatement(
"insert into users_roles (user_id, role_id) values (1,0)")) {
ps.execute();
}
}
}

View File

@ -28,7 +28,8 @@ public class BeanConfigurations {
@Bean
public MessageSource messageSource() {
final ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:/messages,file:./config/messages");
messageSource
.setBasenames("classpath:/messages,file:./config/messages");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(5);
@ -45,10 +46,4 @@ public class BeanConfigurations {
return keyBasedPersistenceTokenService;
}
// @Bean
// public Connection connection() throws SQLException
// {
// return DriverManager.getConnection(url, userName, password);
// }
}

View File

@ -17,8 +17,8 @@ public class ChattoApplication extends SpringBootServletInitializer {
}
@Configuration
@Profile("development")
@ComponentScan(lazyInit = true)
static class LocalConfig {
}
@Profile("development")
@ComponentScan(lazyInit = true)
static class LocalConfig {
}
}

View File

@ -12,19 +12,21 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationEn
import org.springframework.stereotype.Component;
@Component
public final class RESTAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
public final class RESTAuthenticationEntryPoint
extends BasicAuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.println("HTTP ApplicationStatus 401 - " + authEx.getMessage());
}
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response, AuthenticationException authEx)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.println("HTTP ApplicationStatus 401 - " + authEx.getMessage());
}
@Override
public void afterPropertiesSet() throws Exception {
setRealmName("Chatto");
super.afterPropertiesSet();
}
@Override
public void afterPropertiesSet() throws Exception {
setRealmName("Chatto");
super.afterPropertiesSet();
}
}

View File

@ -6,7 +6,8 @@ import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
protected SpringApplicationBuilder configure(
SpringApplicationBuilder application) {
return application.sources(ChattoApplication.class);
}

View File

@ -5,16 +5,14 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
//@EnableWebMvc
// @EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("*")
.allowedMethods("POST","GET","OPTIONS")
.allowedHeaders("*")
.allowCredentials(false).maxAge(3600);
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**").allowedOrigins("*")
.allowedMethods("POST", "GET", "OPTIONS").allowedHeaders("*")
.allowCredentials(false).maxAge(3600);
}
}
}

View File

@ -20,14 +20,17 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
private final MyUserDetailsService myUserDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
private final PasswordEncoder passwordEncoder;
@Override
@Bean
@ -37,7 +40,7 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(myUserDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
@ -50,80 +53,76 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Configuration
@Order(1)
@RequiredArgsConstructor
public static class ApiWebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private CustomBasicAuthenticationFilter customBasicAuthFilter;
private final RESTAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private TokenAuthenticationFilter tokenFilter;
private final CustomBasicAuthenticationFilter customBasicAuthFilter;
@Autowired
private final TokenAuthenticationFilter tokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().disable().exceptionHandling()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// .cors().and()
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// .cors().and()
.antMatcher("/api/**").authorizeRequests()
.antMatchers("/api/chat/**").hasAnyRole("USER", "ADMIN", "SUPER_USER")
.antMatchers("/api/chat/**")
.hasAnyRole("USER", "ADMIN", "SUPER_USER")
.antMatchers("/api/admin/**")
.hasAnyRole("ADMIN", "SUPER_USER")
.antMatchers("/api/demo/**").hasRole("SUPER_USER")
// .antMatchers("/perform-login").permitAll()
.anyRequest()
// .hasAnyRole("USER", "ADMIN", "SUPER_USER")
.authenticated()
.and().httpBasic().authenticationEntryPoint(authenticationEntryPoint)
;
http.addFilterBefore(tokenFilter, BasicAuthenticationFilter.class);
// Creating token when basic authentication is successful and the same token can
// be used to authenticate for further requests
// final CustomBasicAuthenticationFilter customBasicAuthFilter = new CustomBasicAuthenticationFilter(
// authenticationManagerBean());
http.addFilter(customBasicAuthFilter);
.anyRequest().authenticated().and().httpBasic()
.authenticationEntryPoint(authenticationEntryPoint).and()
.addFilterBefore(tokenFilter,
BasicAuthenticationFilter.class)
// Creating token when basic authentication is successful
// and the
// same token can
// be used to authenticate for further requests
.addFilter(customBasicAuthFilter);
}
}
@Configuration
@Order(2)
@RequiredArgsConstructor
public static class FormWebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private UserSessionLoggingLoginSuccessHandler loginSuccessHandler;
private final UserSessionLoggingLoginSuccessHandler loginSuccessHandler;
@Autowired
private UserSessionLoggingLogoutSuccessHandler logoutSuccessHandler;
private final UserSessionLoggingLogoutSuccessHandler logoutSuccessHandler;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and()
protected void configure(final HttpSecurity httpSecurity)
throws Exception {
httpSecurity.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and()
.authorizeRequests()
// .antMatchers(HttpMethod.POST, "/api/**").permitAll()
.antMatchers("/", "perform_login", "/logout**", "/favicon.ico", "/login*", "/registration",
"/perform_registration", "/css/**", "/js/**", "/img/**")
.permitAll()
// .antMatchers("/","/api**","/api/**","/login*","/registration","/perform_registration","/css/**", "/js/**", "/images/**").permitAll()
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN", "SUPER_USER").antMatchers("/admin/**")
.hasAnyRole("ADMIN", "SUPER_USER")
// .and()
// .antMatcher("/api/**")
// .authorizeRequests()
.antMatchers("/", "perform_login", "/logout**",
"/favicon.ico", "/login*", "/registration",
"/perform_registration", "/css/**", "/js/**",
"/img/**")
.permitAll().antMatchers("/user/**")
.hasAnyRole("USER", "ADMIN", "SUPER_USER")
.antMatchers("/admin/**").hasAnyRole("ADMIN", "SUPER_USER")
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll().loginProcessingUrl("/perform_login")
.formLogin().loginPage("/login").permitAll()
.loginProcessingUrl("/perform_login")
.successHandler(loginSuccessHandler).and().logout()
.logoutSuccessHandler(logoutSuccessHandler)
;
.logoutSuccessHandler(logoutSuccessHandler);
}
// }
}
}

View File

@ -4,7 +4,10 @@ import java.awt.image.BufferedImage;
interface CaptchaBehaviour {
public BufferedImage generateCaptcha();
public BufferedImage generateCaptcha(String captchaText);
public String getRandomChars(int size);
public String getRandomChars();
}

View File

@ -6,9 +6,10 @@ import lombok.Builder;
/*Class for providing your own captcha generator*/
@Builder
public class ManualCaptchaBehaviour implements CaptchaBehaviour{
public class ManualCaptchaBehaviour implements CaptchaBehaviour {
private final int length;
private final String style;
@Override
public BufferedImage generateCaptcha() {
// TODO Auto-generated method stub
@ -33,5 +34,4 @@ public class ManualCaptchaBehaviour implements CaptchaBehaviour{
return null;
}
}

View File

@ -10,174 +10,183 @@ import java.io.IOException;
import java.util.Random;
/**
* This class represents a simple captcha consisting
* of an image {@code png} and its text value.
* Comic Neue Bold Font.
* Capital english letters {@code ONLY}.
* This class represents a simple captcha consisting of an image {@code png} and
* its text value. Comic Neue Bold Font. Capital english letters {@code ONLY}.
*
* @since 1.3
* @author Gennadiy Golovin
*/
public final class SimpleCaptcha {
private BufferedImage imagePng;
private char[] text;
private BufferedImage imagePng;
private char[] text;
/**
* Initializes a newly created default object
* consisting of 8 capital english letters.
*/
public SimpleCaptcha() {
this.text = getRandomChars();
/**
* Initializes a newly created default object consisting of 8 capital
* english letters.
*/
public SimpleCaptcha() {
this.text = getRandomChars();
try {
generateCaptcha();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
generateCaptcha();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Initializes a newly created object, which length
* depends on the passed {@code int} parameter,
* which {@code MUST} be greater than 0.
* If the condition is not met, initializes a newly
* created default object consisting of 8 symbols.
*
* @param length the quantity of symbols, that the
* captcha consists of, greater than 0.
*/
public SimpleCaptcha(int length) {
if (length < 1) {
this.text = getRandomChars();
} else {
this.text = getRandomChars(length);
}
/**
* Initializes a newly created object, which length depends on the passed
* {@code int} parameter, which {@code MUST} be greater than 0. If the
* condition is not met, initializes a newly created default object
* consisting of 8 symbols.
*
* @param length
* the quantity of symbols, that the captcha consists of, greater
* than 0.
*/
public SimpleCaptcha(int length) {
if (length < 1) {
this.text = getRandomChars();
} else {
this.text = getRandomChars(length);
}
try {
generateCaptcha();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
generateCaptcha();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Initializes a newly created object based on the passed
* {@link String} parameter, consisting of capital english
* letters. If the condition is not met, initializes a newly
* created default object consisting of 8 capital english letters.
*
* @param text the text string with the value of the captcha,
* length greater than 0.
*/
public SimpleCaptcha(String text) {
if (text == null || text.equals("")) {
this.text = getRandomChars();
} else {
this.text = text.toCharArray();
}
/**
* Initializes a newly created object based on the passed {@link String}
* parameter, consisting of capital english letters. If the condition is not
* met, initializes a newly created default object consisting of 8 capital
* english letters.
*
* @param text
* the text string with the value of the captcha, length greater
* than 0.
*/
public SimpleCaptcha(String text) {
if (text == null || text.equals("")) {
this.text = getRandomChars();
} else {
this.text = text.toCharArray();
}
try {
generateCaptcha();
} catch (IOException e) {
this.text = getRandomChars();
try {
generateCaptcha();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
try {
generateCaptcha();
} catch (IOException e) {
this.text = getRandomChars();
try {
generateCaptcha();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
/**
* Returns the picture with captcha
*
* @return {@link BufferedImage}
*/
public BufferedImage getImagePng() {
return imagePng;
}
/**
* Returns the picture with captcha
*
* @return {@link BufferedImage}
*/
public BufferedImage getImagePng() {
return imagePng;
}
/**
* Returns the text value of the captcha
*
* @return {@link String}
*/
public String getText() {
return String.valueOf(text);
}
/**
* Returns the text value of the captcha
*
* @return {@link String}
*/
public String getText() {
return String.valueOf(text);
}
//////// //////// //////// //////// //////// //////// //////// ////////
//////// //////// //////// //////// //////// //////// //////// ////////
private char[] getRandomChars() {
return getRandomChars(8);
}
private char[] getRandomChars() {
return getRandomChars(8);
}
private char[] getRandomChars(int quantity) {
private char[] getRandomChars(int quantity) {
char[] randomString = new char[quantity];
char[] randomString = new char[quantity];
Random random = new Random();
Random random = new Random();
int capitalLetter;
int capitalLetter;
for (int i = 0; i < quantity; i++) {
capitalLetter = 65 + random.nextInt(26);
randomString[i] = (char) capitalLetter;
}
for (int i = 0; i < quantity; i++) {
capitalLetter = 65 + random.nextInt(26);
randomString[i] = (char) capitalLetter;
}
return randomString;
}
return randomString;
}
private void generateCaptcha() throws IOException {
int charsQuantity = this.text.length;
BufferedImage[] images = new BufferedImage[charsQuantity];
private void generateCaptcha() throws IOException {
int charsQuantity = this.text.length;
BufferedImage[] images = new BufferedImage[charsQuantity];
for (int i = 0; i < charsQuantity; i++) {
images[i] = ImageIO.read(SimpleCaptcha.class.getResourceAsStream("/pictures/" + this.text[i] + ".png"));
if (i % 2 == 0) {
images[i] = rotateImage(images[i], 25);
} else {
images[i] = rotateImage(images[i], -20);
}
}
for (int i = 0; i < charsQuantity; i++) {
images[i] = ImageIO.read(SimpleCaptcha.class
.getResourceAsStream("/pictures/" + this.text[i] + ".png"));
if (i % 2 == 0) {
images[i] = rotateImage(images[i], 25);
} else {
images[i] = rotateImage(images[i], -20);
}
}
int imageSize = 30;
int rotatedImageSize = (int) Math.sqrt(imageSize * imageSize * 2);
int imageSize = 30;
int rotatedImageSize = (int) Math.sqrt(imageSize * imageSize * 2);
BufferedImage captchaImg = new BufferedImage(rotatedImageSize * (charsQuantity - 1) / 10 * 6 + rotatedImageSize, rotatedImageSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2d = captchaImg.createGraphics();
graphics2d.setBackground(Color.WHITE);
graphics2d.clearRect(0, 0, captchaImg.getWidth(), captchaImg.getHeight());
for (int i = 0; i < charsQuantity; i++) {
captchaImg.getGraphics().drawImage(images[i], rotatedImageSize * i / 10 * 6, 0, null);
}
graphics2d.dispose();
this.imagePng = captchaImg;
}
BufferedImage captchaImg = new BufferedImage(
rotatedImageSize * (charsQuantity - 1) / 10 * 6
+ rotatedImageSize,
rotatedImageSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2d = captchaImg.createGraphics();
graphics2d.setBackground(Color.WHITE);
graphics2d.clearRect(0, 0, captchaImg.getWidth(),
captchaImg.getHeight());
for (int i = 0; i < charsQuantity; i++) {
captchaImg.getGraphics().drawImage(images[i],
rotatedImageSize * i / 10 * 6, 0, null);
}
graphics2d.dispose();
this.imagePng = captchaImg;
}
private BufferedImage rotateImage(BufferedImage buffImage, double angle) {
private BufferedImage rotateImage(BufferedImage buffImage, double angle) {
double radian = Math.toRadians(angle);
double sin = Math.abs(Math.sin(radian));
double cos = Math.abs(Math.cos(radian));
double radian = Math.toRadians(angle);
double sin = Math.abs(Math.sin(radian));
double cos = Math.abs(Math.cos(radian));
int width = buffImage.getWidth();
int height = buffImage.getHeight();
int width = buffImage.getWidth();
int height = buffImage.getHeight();
int nWidth = (int) Math.floor((double) width * cos + (double) height * sin);
int nHeight = (int) Math.floor((double) height * cos + (double) width * sin);
int nWidth = (int) Math
.floor((double) width * cos + (double) height * sin);
int nHeight = (int) Math
.floor((double) height * cos + (double) width * sin);
BufferedImage rotatedImage = new BufferedImage(nWidth, nHeight, BufferedImage.TYPE_INT_ARGB);
BufferedImage rotatedImage = new BufferedImage(nWidth, nHeight,
BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = rotatedImage.createGraphics();
Graphics2D graphics = rotatedImage.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
graphics.translate((nWidth - width) / 2, (nHeight - height) / 2);
graphics.rotate(radian, (double) (width / 2), (double) (height / 2));
graphics.drawImage(buffImage, 0, 0,null);
graphics.dispose();
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
graphics.translate((nWidth - width) / 2, (nHeight - height) / 2);
graphics.rotate(radian, (double) (width / 2), (double) (height / 2));
graphics.drawImage(buffImage, 0, 0, null);
graphics.dispose();
return rotatedImage;
}
return rotatedImage;
}
}

View File

@ -9,30 +9,30 @@ public class SimpleCaptchaBehavior implements CaptchaBehaviour {
SimpleCaptcha simpleCaptcha = new SimpleCaptcha();
return simpleCaptcha.getImagePng();
}
@Override
public BufferedImage generateCaptcha(String captchaText) {
SimpleCaptcha simpleCaptcha = new SimpleCaptcha(captchaText);
return simpleCaptcha.getImagePng();
}
public String getRandomChars() {
return getRandomChars(8);
}
public String getRandomChars(int quantity)
{
return getRandomChars(8);
}
public String getRandomChars(int quantity) {
char[] randomString = new char[quantity];
Random random = new Random();
Random random = new Random();
int capitalLetter;
int capitalLetter;
for (int i = 0; i < quantity; i++) {
capitalLetter = 65 + random.nextInt(26);
randomString[i] = (char) capitalLetter;
}
for (int i = 0; i < quantity; i++) {
capitalLetter = 65 + random.nextInt(26);
randomString[i] = (char) capitalLetter;
}
return new String(randomString);
return new String(randomString);
}
}

View File

@ -7,34 +7,35 @@ import lombok.Builder;
@Builder
public class WebCaptcha {
private final CaptchaBehaviour captchaBehaviour;
public BufferedImage generateCaptcha() {
return captchaBehaviour.generateCaptcha();
}
}
public BufferedImage generateCaptcha(String captchaText) {
return captchaBehaviour.generateCaptcha(captchaText);
}
public String getRandomChars() {
return captchaBehaviour.getRandomChars();
}
}
public String getRandomChars(int quantity) {
return captchaBehaviour.getRandomChars(quantity);
}
public String getRandomChars() {
return captchaBehaviour.getRandomChars();
}
public String getRandomChars(int quantity) {
return captchaBehaviour.getRandomChars(quantity);
}
}
// WebCaptcha webCaptcha = WebCaptcha.builder().captchaBehaviour(new SimpleCaptchaBehavior()).build();
// webCaptcha.generateCaptcha();
// WebCaptcha webCaptcha = WebCaptcha.builder().captchaBehaviour(new
// SimpleCaptchaBehavior()).build();
// webCaptcha.generateCaptcha();
//
// // @formatter:off
// webCaptcha = WebCaptcha.builder()
// .captchaBehaviour(
// ManualCaptchaBehaviour.builder()
// .length(8)
// .style("black")
// .build()
// ).build();
//
// // @formatter:on
// // @formatter:off
// webCaptcha = WebCaptcha.builder()
// .captchaBehaviour(
// ManualCaptchaBehaviour.builder()
// .length(8)
// .style("black")
// .build()
// ).build();
//
// // @formatter:on

View File

@ -0,0 +1,22 @@
package org.ros.chatto.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import lombok.Getter;
@Component
@PropertySource(value = "classpath:git.properties")
@Getter
public class BuildInfo {
private final String buildVersion;
private final String branchName;
public BuildInfo(@Value("${git.build.version") String buildVersion,
@Value("${git.branch") String branchName) {
this.buildVersion = buildVersion;
this.branchName = branchName;
}
}

View File

@ -6,11 +6,14 @@ import org.ehcache.event.CacheEventListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomCacheEventLogger implements CacheEventListener<Object, Object> {
public class CustomCacheEventLogger
implements CacheEventListener<Object, Object> {
@Override
public void onEvent(CacheEvent<? extends Object, ? extends Object> cacheEvent) {
log.debug("custom Caching event {} key = {} old {} new {} ", cacheEvent.getType(), cacheEvent.getKey(),
public void onEvent(
CacheEvent<? extends Object, ? extends Object> cacheEvent) {
log.debug("custom Caching event {} key = {} old {} new {} ",
cacheEvent.getType(), cacheEvent.getKey(),
cacheEvent.getOldValue(), cacheEvent.getNewValue());
}
}

View File

@ -1,6 +1,5 @@
package org.ros.chatto.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

View File

@ -1,16 +1,25 @@
package org.ros.chatto.controller;
import java.util.Optional;
import org.ros.chatto.model.ChatUser;
import org.ros.chatto.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Controller
@RequestMapping("/admin")
@Slf4j
@RequiredArgsConstructor
public class AdminController {
@ -27,4 +36,25 @@ public class AdminController {
model.addAttribute("userNames", userService.getAllRegularUsers());
return "admin/change-passphrase";
}
@GetMapping("/users")
public String usersPage(@RequestParam Optional<String> mode) {
if (mode.isPresent()) {
switch (mode.get()) {
case "new":
return "admin/new-user";
}
}
return "admin/users";
}
@GetMapping("/users/{userName}")
public String userProfile(@PathVariable String userName) {
Optional<ChatUser> maybeUser = userService.getUser(userName);
if (!maybeUser.isPresent()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"Requested user does not exist");
}
return "admin/user-profile";
}
}

View File

@ -2,33 +2,42 @@ package org.ros.chatto.controller;
import java.security.Principal;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import javax.validation.Valid;
import com.spencerwi.either.Result;
import org.ros.chatto.dto.AdminUserDTO;
import org.ros.chatto.dto.ChatMessageDTO;
import org.ros.chatto.dto.ReencryptionDTO;
import org.ros.chatto.service.AdminService;
import org.ros.chatto.service.ChatService;
import org.ros.chatto.service.UserDTOSpec;
import org.ros.chatto.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/api/admin")
@RequiredArgsConstructor
public class AdminRESTController {
@Autowired
private ChatService chatService;
private final ChatService chatService;
@Autowired
private UserService userService;
private final UserService userService;
private final AdminService adminService;
@PostMapping(value = "/post/re-encrypt", consumes = { "application/json" })
public ResponseEntity<ReencryptionDTO> reencryptMessages(
@ -56,13 +65,33 @@ public class AdminRESTController {
return chatMessageDTOs;
}
@GetMapping("/regular-users")
public List<String> getAllRegularUsers() {
return userService.getAllRegularUsers();
@GetMapping("/get/users")
public List<AdminUserDTO> getUsers(Principal principal,
@RequestParam Optional<String> select) {
if (select.isPresent()) {
switch (select.get()) {
case "all":
return adminService.getUsers("", UserDTOSpec.ALL_USERS);
case "regular":
return adminService.getUsers("", UserDTOSpec.REGULAR_USERS);
}
}
return adminService.getUsers(principal.getName(),
UserDTOSpec.OTHER_USERS);
}
@GetMapping("/get/users")
public List<String> getAllOtherUsers(Principal principal) {
return userService.findAllOtherUsers(principal.getName());
@DeleteMapping("/delete/users/{userName}")
public ResponseEntity<?> deleteUser(@PathVariable String userName) {
Result<Void> res = adminService.deleteUser(userName);
if (res.isOk()) {
return ResponseEntity.ok()
.body("Deleted User with name - " + userName);
} else {
return ResponseEntity.badRequest().body(
"An error occured while trying to delete user with name - "
+ userName);
}
}
}

View File

@ -89,8 +89,8 @@ public class ChatMessageController {
@PathVariable final String userName,
@PathVariable final Instant lastMessageTime,
final Principal principal) {
final List<ChatMessageDTO> chatMessageDTOs = chatService.getNewMessages(
principal.getName(), userName, lastMessageTime);
final List<ChatMessageDTO> chatMessageDTOs = chatService
.getNewMessages(principal.getName(), userName, lastMessageTime);
return chatMessageDTOs;
}

View File

@ -0,0 +1,14 @@
package org.ros.chatto.controller;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}

View File

@ -45,7 +45,7 @@ public class Home {
|| r.getAuthority().equals("ROLE_SUPER_USER"));
log.trace("Is admin? " + isAdmin);
// model.addAttribute("activeUsers",
// userService.getOtherActiveUsers(principal.getName()));
// userService.getOtherActiveUsers(principal.getName()));
return "chat";
}
}

View File

@ -0,0 +1,29 @@
package org.ros.chatto.controller;
import java.time.Instant;
import org.ros.chatto.dto.StatsDTO;
import org.ros.chatto.service.StatisticsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/api/stats")
@RequiredArgsConstructor
public class StatisticsController {
private final StatisticsService statisticsService;
@GetMapping
public StatsDTO rootStats() {
return StatsDTO.builder()
.totalMessages(statisticsService.totalMessage())
.totalOnlineUsers(statisticsService.totalUsersOnline())
.numMessagesToday(
statisticsService.messagesOnDay(Instant.now()))
.totalUsers(statisticsService.totalUsers()).build();
}
}

View File

@ -6,9 +6,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping
public String viewUserProfile() {
return "user/home";
}
@RequestMapping
public String viewUserProfile() {
return "user/home";
}
}

View File

@ -0,0 +1,17 @@
package org.ros.chatto.dto;
import java.time.Instant;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class AdminUserDTO {
private int id;
private String userName;
private String role;
private Instant joinDate;
}

View File

@ -1,6 +1,5 @@
package org.ros.chatto.dto;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
@ -12,10 +11,10 @@ import lombok.Data;
public class ChatMessageDTO {
@NotBlank(message = "Username should not be blank")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "Username must be alphanumeric")
@Size(max=15)
@Size(max = 15)
private String toUser;
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "Username must be alphanumeric")
@Size(max=15)
@Size(max = 15)
private String fromUser;
private MessageCipherDTO messageCipher;
private Instant messageTime;

View File

@ -12,7 +12,9 @@ import lombok.Data;
@Data
public class MessageCipherDTO {
@Pattern(regexp = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$") // regex for base64
@Pattern(regexp = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$") // regex
// for
// base64
@NotBlank
private String iv;
@Max(1)

View File

@ -0,0 +1,21 @@
package org.ros.chatto.dto;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class StatsDTO {
@Builder.Default
private long totalUsers = 0;
@Builder.Default
private long totalOnlineUsers = 0;
@Builder.Default
private long totalMessages = 0;
@Builder.Default
private long numMessagesToday = 0;
}

View File

@ -16,11 +16,13 @@ public class UserRegistrationDTO {
@Transient
@Size(min = 4, max = 75, message = "Password must be between 4 and 75 characters")
@NotBlank(message = "Password should not be blank")
// @Pattern(regexp = "^.*(?=.{6,})(?=.*d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*? ]).*$", message = "Invalid password format")
// @Pattern(regexp =
// "^.*(?=.{6,})(?=.*d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*? ]).*$", message
// = "Invalid password format")
private String password;
private Long captchaID;
private String captchaText;
private String captchaInput;
}

View File

@ -7,15 +7,14 @@ import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class ErrorModel{
public class ErrorModel {
@JsonProperty("field_name")
private String fieldName;
private String fieldName;
@JsonProperty("rejected_value")
private Object rejectedValue;
private Object rejectedValue;
@JsonProperty("error_message")
private String messageError;
private String messageError;
}

View File

@ -15,6 +15,6 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
public class ErrorResponse {
@JsonProperty("errors")
private List<ErrorModel> errorMessage;
private List<ErrorModel> errorMessage;
}

View File

@ -4,5 +4,6 @@ import org.springframework.cache.annotation.CacheEvict;
public class TokenCacheUtil {
@CacheEvict(value = "userTokenCache", key = "#cacheKey")
public static void evictSingleTokenValue(String cacheKey) {}
public static void evictSingleTokenValue(String cacheKey) {
}
}

View File

@ -26,7 +26,8 @@ public class UserLoggingSessionListener implements HttpSessionBindingListener {
@Override
public void valueBound(HttpSessionBindingEvent event) {
UserLoggingSessionListener user = (UserLoggingSessionListener) event.getValue();
UserLoggingSessionListener user = (UserLoggingSessionListener) event
.getValue();
log.debug("Incrementing session count for user {}", user.getUsername());
@ -38,7 +39,8 @@ public class UserLoggingSessionListener implements HttpSessionBindingListener {
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
UserLoggingSessionListener user = (UserLoggingSessionListener) event.getValue();
UserLoggingSessionListener user = (UserLoggingSessionListener) event
.getValue();
log.debug("Decrementing session count for user {}", user.getUsername());

View File

@ -11,17 +11,19 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
import org.springframework.stereotype.Component;
@Component("myAuthenticationSuccessHandler")
public class UserSessionLoggingLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException {
HttpSession session = request.getSession(false);
if (session != null) {
UserLoggingSessionListener user = new UserLoggingSessionListener(authentication.getName());
session.setAttribute("user", user);
}
response.sendRedirect("/chat");
}
public class UserSessionLoggingLoginSuccessHandler
implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException {
HttpSession session = request.getSession(false);
if (session != null) {
UserLoggingSessionListener user = new UserLoggingSessionListener(
authentication.getName());
session.setAttribute("user", user);
}
response.sendRedirect("/chat");
}
}

View File

@ -12,15 +12,16 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
import org.springframework.stereotype.Component;
@Component("myLogoutSuccessHandler")
public class UserSessionLoggingLogoutSuccessHandler implements LogoutSuccessHandler{
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
HttpSession session = request.getSession();
if (session != null){
session.removeAttribute("user");
}
response.sendRedirect("/login?logout");
}
public class UserSessionLoggingLogoutSuccessHandler
implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
HttpSession session = request.getSession();
if (session != null) {
session.removeAttribute("user");
}
response.sendRedirect("/login?logout");
}
}

View File

@ -24,18 +24,18 @@ public class ChatMessage {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "m_id")
private Long messageID;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "from_user")
private ChatUser fromUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "to_user")
private ChatUser toUser;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "message")
private MessageCipher messageCipher;
private Instant messageTime;
private Instant messageTime;
}

View File

@ -1,6 +1,6 @@
package org.ros.chatto.model;
import java.util.Date;
import java.time.Instant;
import java.util.Set;
import javax.persistence.CascadeType;
@ -12,8 +12,6 @@ import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@ -43,9 +41,9 @@ public class ChatUser {
@Column(name = "name")
private String userName;
String password;
@Temporal(TemporalType.TIMESTAMP)
private Date joinDate;
private String password;
private Instant joinDate;
@ToString.Exclude
@EqualsAndHashCode.Exclude
@ -53,11 +51,4 @@ public class ChatUser {
CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH })
@JsonBackReference
private Set<UserRole> userRoles;
// @ToString.Exclude
// @EqualsAndHashCode.Exclude
// @OneToMany(mappedBy = "fromUser", cascade = { CascadeType.PERSIST,
// CascadeType.MERGE, CascadeType.DETACH,
// CascadeType.REFRESH })
// private List<ChatMessage> chatMessages;
}

View File

@ -19,7 +19,7 @@ this is what the json will look like*/
@Entity
@Table(name = "message_ciphers")
@EntityListeners(AuditingEntityListener.class)
//@JsonIgnoreProperties(value = { "id"}, allowGetters = false)
// @JsonIgnoreProperties(value = { "id"}, allowGetters = false)
public class MessageCipher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@ -16,7 +16,6 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Entity
@Table(name = "roles")
@Data
@ -24,14 +23,14 @@ public class Role {
@Id
@Column(name = "role_id")
private int roleID;
@Column(name = "role_name")
private String name;
private String name;
private String description;
@OneToMany(mappedBy = "role", cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH,
CascadeType.REFRESH })
@OneToMany(mappedBy = "role", cascade = { CascadeType.PERSIST,
CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH })
@JsonBackReference
@ToString.Exclude
@EqualsAndHashCode.Exclude

View File

@ -20,13 +20,13 @@ public class UserSession {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private ChatUser user;
private boolean online;
private int numSessions;
private Instant timeStamp;
}

View File

@ -0,0 +1,25 @@
package org.ros.chatto.repository;
import java.util.List;
import org.ros.chatto.dto.AdminUserDTO;
import org.ros.chatto.model.ChatUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface AdminUserRepository extends JpaRepository<ChatUser, Long> {
@Query("select new org.ros.chatto.dto.AdminUserDTO(u.userID, u.userName, ur.role.name, u.joinDate )"
+ " from ChatUser u join u.userRoles ur where u.userName != ?1")
public List<AdminUserDTO> getOtherUsers(String principal);
@Query("select new org.ros.chatto.dto.AdminUserDTO(u.userID, u.userName, ur.role.name, u.joinDate )"
+ " from ChatUser u join u.userRoles ur ")
public List<AdminUserDTO> getAllUsers();
@Query("select new org.ros.chatto.dto.AdminUserDTO(u.userID, u.userName, ur.role.name, u.joinDate )"
+ " from ChatUser u join u.userRoles ur where ur.role.roleID = 2")
public List<AdminUserDTO> getRegularUsers();
}

View File

@ -10,7 +10,8 @@ import org.springframework.stereotype.Repository;
import java.time.Instant;
@Repository
public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> {
public interface ChatMessageRepository
extends JpaRepository<ChatMessage, Long> {
@Query("select m from ChatMessage m join fetch m.messageCipher mc join fetch m.toUser tu join fetch m.fromUser fu "
+ "where (tu.userName = ?1 or tu.userName = ?2) and "
+ "(fu.userName = ?1 or fu.userName = ?2) order by m.messageTime asc")
@ -19,10 +20,18 @@ public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long>
@Query("select m from ChatMessage m join fetch m.messageCipher mc join fetch m.toUser tu join fetch m.fromUser fu "
+ "where (tu.userName = ?1 or tu.userName = ?2) and "
+ "(fu.userName = ?1 or fu.userName = ?2) and (m.messageTime > ?3) order by m.messageTime asc")
public List<ChatMessage> getNewMessages(String fromUser, String toUser, Instant lastMessageTime);
public List<ChatMessage> getNewMessages(String fromUser, String toUser,
Instant lastMessageTime);
@Query("select m from ChatMessage m join fetch m.messageCipher mc join fetch m.toUser tu join fetch m.fromUser fu "
+ "where (tu.userName = ?1 or tu.userName = ?2) and "
+ "(fu.userName = ?1 or fu.userName = ?2) order by m.messageTime desc")
public List<ChatMessage> getAllMessages(String fromUser, String toUser, PageRequest pageRequest);
public List<ChatMessage> getAllMessages(String fromUser, String toUser,
PageRequest pageRequest);
@Query("select count(c) from ChatMessage c")
public Long totalMessages();
@Query("select count(c) from ChatMessage c where c.messageTime=?1")
public Long messagesOnDate(Instant date);
}

View File

@ -1,9 +1,10 @@
//package org.ros.chatto.repository;
// package org.ros.chatto.repository;
//
//import org.springframework.data.jpa.repository.JpaRepository;
//import org.springframework.stereotype.Repository;
// import org.springframework.data.jpa.repository.JpaRepository;
// import org.springframework.stereotype.Repository;
//
//@Repository
//public interface DBInitializerRepostory extends JpaRepository<Integer, Integer>{
//
//}
// @Repository
// public interface DBInitializerRepostory extends JpaRepository<Integer,
// Integer>{
//
// }

View File

@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MessageCipherRepository extends JpaRepository<MessageCipher, Long>{
public interface MessageCipherRepository
extends JpaRepository<MessageCipher, Long> {
}

View File

@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long>{
public interface RoleRepository extends JpaRepository<Role, Long> {
@Query("select r from Role r where r.name = ?1")
public Role findByName(String roleName);
}

View File

@ -3,6 +3,7 @@ package org.ros.chatto.repository;
import java.util.List;
import java.util.Optional;
import org.ros.chatto.dto.AdminUserDTO;
import org.ros.chatto.model.ChatUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
@ -10,12 +11,12 @@ import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<ChatUser, Long> {
@Query("select cu from ChatUser cu where cu.userName = ?1")
public Optional<ChatUser> findByUserName(String userName);
@Query("select cu from ChatUser cu join fetch cu.userRoles where cu.userName = ?1")
public ChatUser findByUserNameWithRole(String userName);
public Optional<ChatUser> findByUserName(String userName);
@Query("select cu.userName from ChatUser cu where cu.userName != ?1")
public List<String> findAllOtherUserNames(String userName);
@Query("select count(u) from ChatUser u")
public Long totalUsers();
}

View File

@ -4,8 +4,8 @@ import java.util.List;
public interface UserRepositoryCustom {
// @Query("select s from Article s where s.author like ?1 and s.title = ?2")
// List<Article> findByAuthorAndTitle(String author, String title);
// @Query("select u from ChatUser u")
// @Query("select s from Article s where s.author like ?1 and s.title = ?2")
// List<Article> findByAuthorAndTitle(String author, String title);
// @Query("select u from ChatUser u")
public List<String> getAllUserNames(String s);
}

View File

@ -14,30 +14,34 @@ import org.ros.chatto.model.ChatUser;
import org.ros.chatto.repository.UserRepositoryCustom;
@Service
class UserRepositoryCustomImpl implements UserRepositoryCustom{
class UserRepositoryCustomImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<String> getAllUserNames(String userName) {
List<String> userNamesList = null;
// Session session = null;
// Session session = null;
try {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<String> criteriaQuery = criteriaBuilder.createQuery(String.class);
CriteriaBuilder criteriaBuilder = entityManager
.getCriteriaBuilder();
CriteriaQuery<String> criteriaQuery = criteriaBuilder
.createQuery(String.class);
Root<ChatUser> root = criteriaQuery.from(ChatUser.class);
criteriaQuery.select(root.get("userName"));
criteriaQuery.where(criteriaBuilder.notEqual(root.get("userName"), userName));
userNamesList = entityManager.createQuery(criteriaQuery).getResultList();
// for(String un: userNamesList)
// {
// System.out.println(un);
// }
criteriaQuery.where(
criteriaBuilder.notEqual(root.get("userName"), userName));
userNamesList = entityManager.createQuery(criteriaQuery)
.getResultList();
// for(String un: userNamesList)
// {
// System.out.println(un);
// }
} catch (Exception e) {
e.printStackTrace();
}
return userNamesList;
}
}

View File

@ -8,10 +8,14 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface UserSessionRepository extends JpaRepository<UserSession, Long> {
public interface UserSessionRepository
extends JpaRepository<UserSession, Long> {
@Query("select us from UserSession us join fetch us.user where us.user.userName = ?1 ")
public UserSession findByUserName(String userName);
@Query("select us from UserSession us join fetch us.user")
public List<UserSession> findAllUserSessions();
@Query("select count(us) from UserSession us where us.online=true")
public Long totalOnlineUsers();
}

View File

@ -16,20 +16,29 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
import org.springframework.stereotype.Component;
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
private static final SimpleGrantedAuthority SUPER_USER_AUTHORITY = new SimpleGrantedAuthority("ROLE_SUPER_USER");
private static final SimpleGrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority("ROLE_ADMIN");
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public class AuthenticationSuccessHandlerImpl
implements AuthenticationSuccessHandler {
private static final SimpleGrantedAuthority SUPER_USER_AUTHORITY = new SimpleGrantedAuthority(
"ROLE_SUPER_USER");
private static final SimpleGrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority(
"ROLE_ADMIN");
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.contains(ADMIN_AUTHORITY) || authorities.contains(SUPER_USER_AUTHORITY)) {
redirectStrategy.sendRedirect(httpServletRequest, httpServletResponse, "/admin");
} else {
redirectStrategy.sendRedirect(httpServletRequest, httpServletResponse, "/user");
}
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication)
throws IOException, ServletException {
Collection<? extends GrantedAuthority> authorities = authentication
.getAuthorities();
if (authorities.contains(ADMIN_AUTHORITY)
|| authorities.contains(SUPER_USER_AUTHORITY)) {
redirectStrategy.sendRedirect(httpServletRequest,
httpServletResponse, "/admin");
} else {
redirectStrategy.sendRedirect(httpServletRequest,
httpServletResponse, "/user");
}
}
}

View File

@ -1,5 +1,6 @@
package org.ros.chatto.security;
import java.util.Optional;
import java.util.Set;
import org.ros.chatto.model.ChatUser;
@ -26,17 +27,19 @@ public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(final String username) {
log.trace("User Details - loading with username: {}", username);
ChatUser user = userService.getUserWithRole(username);
Optional<ChatUser> user = userService.getUser(username);
if (user == null) {
if (!user.isPresent()) {
log.warn("Request for unknown user {}", username);
throw new UsernameNotFoundException(username);
}
Set<UserRole> userRoles = user.getUserRoles();
ChatUser user2 = user.get();
return User.withUsername(user.getUserName())
.password(user.getPassword())
Set<UserRole> userRoles = user2.getUserRoles();
return User.withUsername(user2.getUserName())
.password(user2.getPassword())
.roles(userRoles.stream().map(ur -> ur.getRole().getName())
.toArray(size -> new String[size]))
.build();

View File

@ -0,0 +1,46 @@
package org.ros.chatto.service;
import java.util.List;
import java.util.Optional;
import com.spencerwi.either.Result;
import org.ros.chatto.dto.AdminUserDTO;
import org.ros.chatto.model.ChatUser;
import org.ros.chatto.repository.AdminUserRepository;
import org.ros.chatto.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class AdminService {
private final UserRepository userRepository;
private final AdminUserRepository adminUserRepository;
public List<AdminUserDTO> getUsers(String principal, UserDTOSpec spec) {
switch (spec) {
case OTHER_USERS:
return adminUserRepository.getOtherUsers(principal);
case ALL_USERS:
return adminUserRepository.getAllUsers();
case REGULAR_USERS:
return adminUserRepository.getRegularUsers();
default:
throw new UnsupportedOperationException();
}
}
public Result<Void> deleteUser(String userName) {
Optional<ChatUser> user = userRepository.findByUserName(userName);
return Result.attempt(() -> user.get()).map((u) -> {
userRepository.delete(u);
return (Void) null;
});
}
}

View File

@ -9,18 +9,17 @@ import org.springframework.stereotype.Service;
@Service
public class CaptchaService {
private final WebCaptcha webCaptcha;
public CaptchaService() {
webCaptcha = WebCaptcha.builder().captchaBehaviour(new SimpleCaptchaBehavior()).build();
webCaptcha = WebCaptcha.builder()
.captchaBehaviour(new SimpleCaptchaBehavior()).build();
}
public BufferedImage createCaptchaImage(final String captchaText)
{
public BufferedImage createCaptchaImage(final String captchaText) {
return webCaptcha.generateCaptcha(captchaText);
}
public String getRandomText()
{
public String getRandomText() {
return webCaptcha.getRandomChars();
}
}

View File

@ -46,13 +46,15 @@ public class DBInitializerService {
});
}
private void resetAllUserSessions(final Connection connection) throws SQLException {
private void resetAllUserSessions(final Connection connection)
throws SQLException {
final PreparedStatement preparedStatement = connection
.prepareStatement(dbInitializerConfig.getResetSessionsQuery());
preparedStatement.executeUpdate();
}
private void clearAllTokens(final Connection connection) throws SQLException {
private void clearAllTokens(final Connection connection)
throws SQLException {
final PreparedStatement preparedStatement = connection
.prepareStatement(dbInitializerConfig.getClearTokensQuery());
preparedStatement.executeUpdate();

View File

@ -0,0 +1,36 @@
package org.ros.chatto.service;
import java.time.Instant;
import org.ros.chatto.repository.ChatMessageRepository;
import org.ros.chatto.repository.UserRepository;
import org.ros.chatto.repository.UserSessionRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class StatisticsService {
private final ChatMessageRepository chatMessageRepository;
private final UserRepository userRepo;
private final UserSessionRepository userSessionRepo;
public Long totalMessage() {
return chatMessageRepository.totalMessages();
}
public Long totalUsers() {
return userRepo.totalUsers();
}
public Long totalUsersOnline() {
return userSessionRepo.totalOnlineUsers();
}
public Long messagesOnDay(Instant date) {
return chatMessageRepository.messagesOnDate(date);
}
}

View File

@ -0,0 +1,5 @@
package org.ros.chatto.service;
public enum UserDTOSpec {
ALL_USERS, OTHER_USERS, REGULAR_USERS
}

View File

@ -25,9 +25,8 @@ public interface UserService {
public List<ActiveUserDTO> getOtherActiveUsers(String userName);
public ChatUser getUserWithRole(String userName);
public void incrementUserSession(String userName);
public UserSession decrementUserSession(String userName);
}

View File

@ -139,12 +139,6 @@ public class UserServiceImpl implements UserService {
}
}
@Override
@Transactional(readOnly = true)
public ChatUser getUserWithRole(final String userName) {
return userRepository.findByUserNameWithRole(userName);
}
@Override
public void incrementUserSession(final String userName) {
final Optional<ChatUser> chatUser = getUser(userName);
@ -200,4 +194,14 @@ public class UserServiceImpl implements UserService {
return user.getUserRoles().stream().map(ur -> ur.getRole())
.collect(Collectors.toSet());
}
public void deleteUser(String userName) {
//
Optional<ChatUser> user = userRepository.findByUserName(userName);
user.ifPresentOrElse(u -> {
userRepository.delete(u);
}, () -> {
log.error("User with name {} not found for deletion", userName);
});
}
}

View File

@ -6,4 +6,6 @@ logging.level.org.hibernate.type.descriptor.sql.BasicBinder=INFO
spring.http.log-request-details=false
logging.level.org.springframework.cache = INFO
chat-bundle=bundle.min.js
admin-bundle=adminBundle.min.js
chat-worker-bundle=chatWorker.min.js
chatto.frontend.log-level=INFO

View File

@ -3,7 +3,6 @@
#spring.datasource.url = jdbc:mysql://localhost:3306/chatto_db?useSSL=false
#spring.datasource.username = chatto_user
#spring.datasource.password = password
chatto.datasource.username=chatto_user
chatto.datasource.password=password
chatto.datasource.database-name=chatto_db2
@ -29,6 +28,8 @@ spring.cache.jcache.config=classpath:ehcache.xml
logging.level.org.springframework.cache=DEBUG
chatto.token.timeout-duration=30
chat-bundle=bundle.js
admin-bundle=adminBundle.js
chat-worker-bundle=chatWorker.js
# spring.devtools.add-properties=false
chatto.frontend.log-level=TRACE
chatto.frontend.chat-page-size=9

View File

@ -1,33 +1,33 @@
.text-primary-dark {
/* color: #67b2fd; */
color: #7accff;
/* color: #67b2fd; */
color: #7accff;
}
.border-left-primary-dark {
border-left: 0.25rem solid #7accff !important;
border-left: 0.25rem solid #7accff !important;
}
.sidebar .nav-item .collapse .collapse-inner .collapse-item,
.sidebar .nav-item .collapsing .collapse-inner .collapse-item {
padding: 0.5rem 1rem;
margin: 0 0.5rem;
display: block;
color: #fff;
text-decoration: none;
border-radius: 0.35rem;
white-space: nowrap;
padding: 0.5rem 1rem;
margin: 0 0.5rem;
display: block;
color: #fff;
text-decoration: none;
border-radius: 0.35rem;
white-space: nowrap;
}
.sidebar-dark .nav-item .nav-link i {
color: rgba(255, 255, 255, 0.7);
color: rgba(255, 255, 255, 0.7);
}
.sidebar-dark .nav-item .nav-link {
color: rgba(255, 255, 255, 0.95);
color: rgba(255, 255, 255, 0.95);
}
.sidebar-dark .nav-item .nav-link[data-toggle="collapse"]::after {
color: rgba(255, 255, 255, 0.8);
color: rgba(255, 255, 255, 0.8);
}
.sidebar .nav-item .collapse .collapse-inner .collapse-item:hover,
@ -40,7 +40,7 @@
} */
.text-info {
color: #5bd6e8 !important;
color: #5bd6e8 !important;
}
/* :root {
@ -48,14 +48,18 @@
} */
.sidebar-dark hr.sidebar-divider {
border-top: 1px solid rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(255, 255, 255, 0.5);
}
body {
background: #333;
color: #fff;
background: #333;
color: #fff;
}
#wrapper #content-wrapper {
background-color: #333;
}
background-color: #333;
}
table.table-bordered.dataTable {
border-right-width: 1px;
}

View File

@ -0,0 +1,8 @@
label,
#usersTable_info,
.dataTables_paginate,
.paginate_button,
#usersTable_next,
#usersTable_previous {
color: white !important;
}

View File

@ -25,8 +25,9 @@ input[type="radio"]:focus + label {
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
min-height: 900px;
height: auto;
min-height: 600px;
height: 95vh;
/* height: auto; */
}
#home-section .home-inner {
@ -44,7 +45,8 @@ input[type="radio"]:focus + label {
top: 0;
left: 0;
width: 100%;
min-height: 900px;
min-height: 600px;
height: 95vh;
background: rgba(0, 0, 0, 0.7);
}
@ -99,3 +101,24 @@ textarea {
width: 90%;
} */
}
html {
--scrollbarBG: hsla(0, 0%, 100%, 0.1);
--thumbBG: #90a4ae;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webpack App</title>
</head>
<body>
<script type="text/javascript" src="/bundle.js"></script></body>
</html>

View File

@ -8,14 +8,9 @@
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js"
data-template-prefix="../" defer="defer" th:if="false"></script>
<!-- <script th:src="@{/js/admin.js}" src="../../static/js/admin.js" defer="defer"></script> -->
<script th:src="@{/js/adminBundle.js}" src="../../static/js/adminBundle.js" defer="defer"></script>
<script th:src="@{/js/sb-admin-2.js}" src="../../static/js/sb-admin-2.js" defer="defer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.4.1/jquery.easing.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.js"></script>
<link th:href="@{/css/sb-admin-2.css}" href="../../static/css/sb-admin-2.css" rel="stylesheet">
<link th:href="@{/css/admin-custom.css}" href="../../static/css/admin-custom.css" rel="stylesheet">
<th:block th:include="fragments/admin :: headFragment"></th:block>
</head>
<!-- TODO
Make user admin / remove user from admin

View File

@ -6,14 +6,10 @@
<title id="pageTitle">Admin Home</title>
</div>
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js" data-template-prefix="../" defer="defer" th:if="false"></script>
<script th:src="@{js/admin.js}" src="../../static/js/admin.js" defer="defer"></script>
<script th:src="@{js/sb-admin-2.js}" src="../../static/js/sb-admin-2.js" defer="defer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.4.1/jquery.easing.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.js"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js"
data-template-prefix="../" defer="defer" th:if="false"></script>
<link th:href="@{/css/sb-admin-2.css}" href="../../static/css/sb-admin-2.css" rel="stylesheet">
<link th:href="@{/css/admin-custom.css}" href="../../static/css/admin-custom.css" rel="stylesheet">
<th:block th:include="fragments/admin :: headFragment"></th:block>
</head>
<!-- TODO
Make user admin / remove user from admin
@ -21,13 +17,13 @@
Delete Messages
-->
<!-- <div th:include="fragments/admin :: admin-sidebar"></div> -->
<!-- <div th:include="fragments/admin :: admin-sideb ar"></div> -->
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<th:block th:include="fragments/admin :: sidebar-fragment"></th:block>
@ -36,7 +32,7 @@
<!-- Main Content -->
<div id="content">
<div th:include="fragments/admin :: topbar-fragment"></div>
<!-- Begin Page Content -->
@ -45,7 +41,8 @@
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-light">Dashboard</h1>
<a href="#" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm"><i class="fas fa-download fa-sm text-white-50"></i> Generate Report</a>
<a href="#" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm"><i
class="fas fa-download fa-sm text-white-50"></i> Generate Report</a>
</div>
<!-- Content Row -->
@ -53,15 +50,21 @@
<!-- Earnings (Monthly) Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card text-light border border-dark bg-dark border-left-primary-dark shadow h-100 py-2">
<div
class="card text-light border border-dark bg-dark border-left-primary-dark shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary-dark text-uppercase mb-1">Earnings (Monthly)</div>
<div class="h5 mb-0 font-weight-bold ">$40,000</div>
<div
class="text-xs font-weight-bold text-primary-dark text-uppercase mb-1">
Total Users</div>
<div class="h5 mb-0 font-weight-bold " id="totalUsers" hidden>0</div>
<div
class="spinner-grow spinner-grow-lg text-info admin-stats-spinner admin-spinner">
</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar fa-2x text-gray-300"></i>
<i class="fas fa-users fa-2x text-gray-300"></i>
</div>
</div>
</div>
@ -70,15 +73,21 @@
<!-- Earnings (Monthly) Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card text-light border border-dark bg-dark border-left-success shadow h-100 py-2">
<div
class="card text-light border border-dark bg-dark border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Earnings (Annual)</div>
<div class="h5 mb-0 font-weight-bold text-light">$215,000</div>
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Total Messages</div>
<div class="h5 mb-0 font-weight-bold text-light" id="totalMessages" hidden>0
</div>
<div
class="spinner-grow spinner-grow-lg text-info admin-stats-spinner admin-spinner">
</div>
</div>
<div class="col-auto">
<i class="fas fa-dollar-sign fa-2x text-gray-300"></i>
<i class="fas fa-comments fa-2x text-gray-300"></i>
</div>
</div>
</div>
@ -91,20 +100,28 @@
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Tasks</div>
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Users
Online
</div>
<div class="row no-gutters align-items-center">
<div class="col-auto">
<div class="h5 mb-0 mr-3 font-weight-bold text-white">50%</div>
</div>
<div class="col">
<div class="progress progress-sm mr-2">
<div class="progress-bar bg-info" role="progressbar" style="width: 50%" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
<div class="h5 mb-0 mr-3 font-weight-bold text-white"
id="totalOnlineUsers" hidden>0</div>
<div
class="spinner-grow spinner-grow-lg text-info admin-stats-spinner admin-spinner">
</div>
</div>
<!-- <div class="col">
<div class="progress progress-sm mr-2">
<div class="progress-bar bg-info" role="progressbar"
style="width: 50%" aria-valuenow="50" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div> -->
</div>
</div>
<div class="col-auto">
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
<i class="fas fa-user fa-2x text-gray-300"></i>
</div>
</div>
</div>
@ -113,15 +130,21 @@
<!-- Pending Requests Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card text-light border border-dark bg-dark border-left-warning shadow h-100 py-2">
<div
class="card text-light border border-dark bg-dark border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Pending Requests</div>
<div class="h5 mb-0 font-weight-bold text-white">18</div>
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Messages Today</div>
<div class="h5 mb-0 font-weight-bold text-white" id="numMessagesToday"
hidden>0</div>
<div
class="spinner-grow spinner-grow-lg text-info admin-stats-spinner admin-spinner">
</div>
</div>
<div class="col-auto">
<i class="fas fa-comments fa-2x text-gray-300"></i>
<i class="fas fa-comment fa-2x text-gray-300"></i>
</div>
</div>
</div>
@ -137,13 +160,16 @@
<div class="col-xl-8 col-lg-7">
<div class="card bg-dark border border-dark text-white shadow mb-4">
<!-- Card Header - Dropdown -->
<div class="card-header bg-secondary border border-secondary py-3 d-flex flex-row align-items-center justify-content-between">
<div
class="card-header bg-secondary border border-secondary py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-white">Earnings Overview</h6>
<div class="dropdown no-arrow">
<a class="dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="dropdown-toggle" href="#" role="button" id="dropdownMenuLink"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v fa-sm fa-fw text-white"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in" aria-labelledby="dropdownMenuLink">
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"
aria-labelledby="dropdownMenuLink">
<div class="dropdown-header">Dropdown Header:</div>
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
@ -165,13 +191,16 @@
<div class="col-xl-4 col-lg-5">
<div class="card bg-dark border border-dark text-white shadow mb-4">
<!-- Card Header - Dropdown -->
<div class="card-header bg-secondary border border-secondary py-3 d-flex flex-row align-items-center justify-content-between">
<div
class="card-header bg-secondary border border-secondary py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-white">Revenue Sources</h6>
<div class="dropdown no-arrow">
<a class="dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="dropdown-toggle" href="#" role="button" id="dropdownMenuLink"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v fa-sm fa-fw text-white"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in" aria-labelledby="dropdownMenuLink">
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"
aria-labelledby="dropdownMenuLink">
<div class="dropdown-header">Dropdown Header:</div>
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
@ -187,14 +216,14 @@
</div>
<div class="mt-4 text-center small">
<span class="mr-2">
<i class="fas fa-circle text-primary"></i> Direct
</span>
<i class="fas fa-circle text-primary"></i> Direct
</span>
<span class="mr-2">
<i class="fas fa-circle text-success"></i> Social
</span>
<i class="fas fa-circle text-success"></i> Social
</span>
<span class="mr-2">
<i class="fas fa-circle text-info"></i> Referral
</span>
<i class="fas fa-circle text-info"></i> Referral
</span>
</div>
</div>
</div>
@ -213,25 +242,35 @@
<h6 class="m-0 font-weight-bold text-white">Projects</h6>
</div>
<div class="card-body">
<h4 class="small font-weight-bold">Server Migration <span class="float-right">20%</span></h4>
<h4 class="small font-weight-bold">Server Migration <span
class="float-right">20%</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-danger" role="progressbar" style="width: 20%" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-danger" role="progressbar" style="width: 20%"
aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Sales Tracking <span class="float-right">40%</span></h4>
<h4 class="small font-weight-bold">Sales Tracking <span
class="float-right">40%</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-warning" role="progressbar" style="width: 40%" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-warning" role="progressbar" style="width: 40%"
aria-valuenow="40" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Customer Database <span class="float-right">60%</span></h4>
<h4 class="small font-weight-bold">Customer Database <span
class="float-right">60%</span></h4>
<div class="progress mb-4">
<div class="progress-bar" role="progressbar" style="width: 60%" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar" role="progressbar" style="width: 60%"
aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Payout Details <span class="float-right">80%</span></h4>
<h4 class="small font-weight-bold">Payout Details <span
class="float-right">80%</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-info" role="progressbar" style="width: 80%" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-info" role="progressbar" style="width: 80%"
aria-valuenow="80" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Account Setup <span class="float-right">Complete!</span></h4>
<h4 class="small font-weight-bold">Account Setup <span
class="float-right">Complete!</span></h4>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-success" role="progressbar" style="width: 100%"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
@ -299,11 +338,17 @@
</div>
<div class="card-body">
<div class="text-center">
<img class="img-fluid px-3 px-sm-4 mt-3 mb-4" style="width: 25rem;" th:src="@{/img/undraw_posting_photo.svg}" src="../../static/img/undraw_posting_photo.svg" alt="">
<img class="img-fluid px-3 px-sm-4 mt-3 mb-4" style="width: 25rem;"
th:src="@{/img/undraw_posting_photo.svg}"
src="../../static/img/undraw_posting_photo.svg" alt="">
</div>
<p>Add some quality, svg illustrations to your project courtesy of <a target="_blank" rel="nofollow" href="https://undraw.co/">unDraw</a>, a constantly updated collection of beautiful svg images that you can use completely
<p>Add some quality, svg illustrations to your project courtesy of <a
target="_blank" rel="nofollow" href="https://undraw.co/">unDraw</a>, a
constantly updated collection of beautiful svg images that you can use
completely
free and without attribution!</p>
<a target="_blank" rel="nofollow" href="https://undraw.co/">Browse Illustrations on unDraw &rarr;</a>
<a target="_blank" rel="nofollow" href="https://undraw.co/">Browse Illustrations on
unDraw &rarr;</a>
</div>
</div>
@ -313,8 +358,11 @@
<h6 class="m-0 font-weight-bold text-white">Development Approach</h6>
</div>
<div class="card-body">
<p>SB Admin 2 makes extensive use of Bootstrap 4 utility classes in order to reduce CSS bloat and poor page performance. Custom CSS classes are used to create custom components and custom utility classes.</p>
<p class="mb-0">Before working with this theme, you should become familiar with the Bootstrap framework, especially the utility classes.</p>
<p>SB Admin 2 makes extensive use of Bootstrap 4 utility classes in order to reduce
CSS bloat and poor page performance. Custom CSS classes are used to create
custom components and custom utility classes.</p>
<p class="mb-0">Before working with this theme, you should become familiar with the
Bootstrap framework, especially the utility classes.</p>
</div>
</div>
@ -336,7 +384,9 @@
<!-- End of Page Wrapper -->
<th:block th:include="fragments/admin :: modal"></th:block>
<div th:if="false"><th:block th:include="admin :: modal"></th:block></div>
<div th:if="false">
<th:block th:include="admin :: modal"></th:block>
</div>
<!-- Page level custom scripts -->
<script th:src="@{/js/demo/chart-area-demo.js}" src="../../static/js/demo/chart-area-demo.js"></script>

View File

@ -1,110 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<div th:replace="fragments/head :: headFragment">
<title id="pageTitle">Admin Home</title>
</div>
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js" data-template-prefix="../" defer="defer" th:if="false"></script>
<script th:src="@{js/admin.js}" src="../../static/js/admin.js" defer="defer"></script>
<link th:href="@{/css/master.css}" href="../../static/css/master.css" rel="stylesheet" th:if="false">
<link th:href="@{/css/colors.css}" href="../../static/css/colors.css" rel="stylesheet" th:if="false">
</head>
<!-- TODO
Make user admin / remove user from admin
Change E2E passphrase
Delete Messages
-->
<body>
<div th:include="fragments/navbar :: navbarFragment"></div>
<header>
<div class="container bg-primary">
<div class="row">
<div class="col-sm py-5">
<h1 class="display-4 text-center">Admin Page</h1>
<p class="alert-danger px-2">Warning: these settings can be dangerous..</p>
</div>
</div>
<div class="row">
<div class="col-sm col-md-4">
<h4>Make User an Admin</h4>
<form id="addUserToAdminForm">
<div class="form-group">
<label for="addUserToDropDown">Select User:</label>
<select class="form-control custom-select" size="4" id="addUserToAdminDropDown">
<option th:each="userName : ${userNames}"
th:value="${userName}"
th:text="#{${userName}}">
Wireframe
</option>
</select>
</div>
<div class="form-group">
<button class="btn btn-danger form-control">Make admin</button>
</div>
</form>
</div>
<div class="col-sm col-md-4">
<h4>Change passphrases</h4>
<form id="changePassphraseForm">
<div class="form-group">
<label for="changePassphraseDropDown">Select User:</label>
<select class="form-control" id="changePassphraseDropDown">
<option th:each="userName : ${userNames}"
th:value="${userName}"
th:text="#{${userName}}">
Wireframe
</option>
</select>
</div>
<div class="form-group">
<label for="passphraseOld">Passphrase Old</label>
<input type="password" id="passphraseOld" class="form-control">
</div>
<div class="form-group">
<label for="passphraseNew">Passphrase New</label>
<input type="password" id="passphraseNew" class="form-control">
</div>
<div class="form-group">
<button class="btn btn-danger form-control">Change Passphrase</button>
</div>
</form>
</div>
</div>
<div class="row">
<!-- <div class="col-sm"></div> -->
<div class="col-sm d-lg-block">
<div class="d-flex justify-content-center">
<div class="py-5">
<h4 class="p-2 text-center">Logout</h4>
<form action="#" th:action="@{/logout}" method="POST">
<!-- <input type="submit" value="logout"> -->
<!-- <input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}" /> -->
<div class="form-group">
<button class="btn btn-secondary form-control">Logout</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</header>
</body>
</html>

View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<div th:replace="fragments/head :: headFragment">
<title id="pageTitle" th:text="${userName}">New User</title>
</div>
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js"
data-template-prefix="../" defer="defer" th:if="false"></script>
<th:block th:include="fragments/admin :: headFragment"></th:block>
</head>
<!-- TODO
Make user admin / remove user from admin
Change E2E passphrase
Delete Messages
-->
<!-- <div th:include="fragments/admin :: admin-sidebar"></div> -->
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<div th:include="fragments/admin :: sidebar-fragment"></div>
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column" style="background-color: #333;">
<!-- Main Content -->
<div id="content">
<div th:include="fragments/admin :: topbar-fragment"></div>
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-light">Dashboard</h1>
</div>
<!-- Content Row -->
<div class="row">
<div class="col-lg-4 mb-4 offset-lg-4">
<!-- Approach -->
<div class="card bg-dark border border-dark text-white shadow mb-4">
<div class="card-header bg-secondary border border-secondary py-3">
<h6 class="m-0 font-weight-bold text-white">New User</h6>
</div>
<div class="card-body">
page under construction
</div>
</div>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<div th:include="fragments/admin :: footer"></div>
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->
<th:block th:include="fragments/admin :: modal"></th:block>
<div th:if="false">
<th:block th:include="admin :: modal"></th:block>
</div>
</body>
</html>

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<div th:replace="fragments/head :: headFragment">
<title id="pageTitle" th:text="${userName}">User Profile</title>
</div>
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js"
data-template-prefix="../" defer="defer" th:if="false"></script>
<th:block th:include="fragments/admin :: headFragment"></th:block>
</head>
<!-- TODO
Make user admin / remove user from admin
Change E2E passphrase
Delete Messages
-->
<!-- <div th:include="fragments/admin :: admin-sidebar"></div> -->
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<div th:include="fragments/admin :: sidebar-fragment"></div>
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column" style="background-color: #333;">
<!-- Main Content -->
<div id="content">
<div th:include="fragments/admin :: topbar-fragment"></div>
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-light">Dashboard</h1>
<a href="#" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm"><i
class="fas fa-download fa-sm text-white-50"></i> Generate Report</a>
</div>
<!-- Content Row -->
<div class="row">
<div class="col-lg-4 mb-4 offset-lg-4">
<!-- Approach -->
<div class="card bg-dark border border-dark text-white shadow mb-4">
<div class="card-header bg-secondary border border-secondary py-3">
<h6 class="m-0 font-weight-bold text-white">Development Approach</h6>
</div>
<div class="card-body">
<div th:text="${userName}">John</div>
</div>
</div>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<div th:include="fragments/admin :: footer"></div>
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->
<th:block th:include="fragments/admin :: modal"></th:block>
<div th:if="false">
<th:block th:include="admin :: modal"></th:block>
</div>
</body>
</html>

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<div th:replace="fragments/head :: headFragment">
<title id="pageTitle">Users</title>
</div>
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js"
data-template-prefix="../" defer="defer" th:if="false"></script>
<th:block th:include="fragments/admin :: headFragment"></th:block>
<style>
/* .sidebar {
height: 100%;
} */
</style>
</head>
<!-- TODO
Make user admin / remove user from admin
Change E2E passphrase
Delete Messages
-->
<!-- <div th:include="fragments/admin :: admin-sidebar"></div> -->
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<div th:include="fragments/admin :: sidebar-fragment"></div>
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column" style="background-color: #333;">
<!-- Main Content -->
<div id="content">
<div th:include="fragments/admin :: topbar-fragment"></div>
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- Page Heading -->
<!-- Content Row -->
<div class="row">
<div class="col">
<!-- Approach -->
<div class="card bg-dark border border-dark text-white shadow mb-4">
<div class="card-header bg-secondary border border-secondary py-3">
<h6 class="m-0 font-weight-bold text-white">Users</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table data-toggle="table" class="table table-bordered text-white" width="100%"
cellspacing="0" id="usersTable">
<thead>
<tr>
<th>User ID</th>
<th>User Name</th>
<th>Role</th>
<th>Registration Date</th>
<!-- <th>Action</th> -->
</tr>
</thead>
<tbody id="usersTableBody">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<div th:include="fragments/admin :: footer"></div>
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->
<th:block th:include="fragments/admin :: modal"></th:block>
<div th:if="false">
<th:block th:include="admin :: modal"></th:block>
</div>
<template id="users-table-template">
<tr>
<td>{{id}}</td>
<td>{{userName}}</td>
<td>{{role}}</td>
<td>{{joinDate}}</td>
<td><button class="btn btn-info"><i class="fas fa-ellipsis-h"></i></button></td>
</tr>
</template>
</body>
</html>

View File

@ -13,6 +13,8 @@
</div>
<script src="./../../javascript/bundle.js" th:src="@{'/js/' + ${@environment.getProperty('chat-bundle')}}"
defer></script>
<script src="./../../javascript/bundle.js" th:src="@{'/js/' + ${@environment.getProperty('chat-worker-bundle')}}"
defer></script>
<link rel="stylesheet" th:href="@{/css/chat.css}" href="../../resources/static/css/chat.css">
</head>
@ -128,6 +130,8 @@
</div>
</div>
</div>
<th:block th:include="fragments/head :: footerFragment"></th:block>
<template id="msg_container_template">
<div class="d-flex justify-content-start mb-4">
<div class="img_cont_msg">

View File

@ -1,18 +0,0 @@
<html>
<head xmlns:th="http://www.thymeleaf.org">
<title></title>
<!--
<link th:href="@{/css/things.css}" rel="stylesheet"></link>
JS
<script th:src="@{/js/things.js}" type="text/javascript"></script> -->
<script th:src="@{js/scljs.js}" type="text/javascript"></script>
<script th:src="@{js/my_Crypto.js}" type="text/javascript"></script>
</head>
<body>
<div id="crypt"></div>
</body>
</html>

View File

@ -7,11 +7,84 @@
<title id="pageTitle">Navbar Fragment</title>
</div> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" th:if="false"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.bundle.js" th:if="false"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.css" th:if="false" rel="stylesheet" type="text/css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.bundle.js"
th:if="false"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.css" th:if="false"
rel="stylesheet" type="text/css">
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js" defer="defer" th:if="false"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css" th:if="false">
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js" defer="defer"
th:if="false"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css"
th:if="false">
<th:block th:fragment="headFragment">
<!-- <script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous" defer></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"
defer></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"
defer></script>
<!-- <script src="https://unpkg.com/bootstrap-table@1.17.1/dist/bootstrap-table.min.js" defer></script> -->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/jquery.dataTables.min.js"
integrity="sha512-BkpSL20WETFylMrcirBahHfSnY++H2O1W+UnEEO4yNIl+jI2+zowyoGJpbtk6bx97fBXf++WJHSSK2MV4ghPcg=="
crossorigin="anonymous"></script> -->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/dataTables.bootstrap.min.js"
integrity="sha512-F0E+jKGaUC90odiinxkfeS3zm9uUT1/lpusNtgXboaMdA3QFMUez0pBmAeXGXtGxoGZg3bLmrkSkbK1quua4/Q=="
crossorigin="anonymous"></script> -->
<!-- <link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/css/dataTables.bootstrap4.min.css"
integrity="sha512-PT0RvABaDhDQugEbpNMwgYBCnGCiTZMh9yOzUsJHDgl/dMhD9yjHAwoumnUk3JydV3QTcIkNDuN40CJxik5+WQ=="
crossorigin="anonymous" /> -->
<!-- <script th:src="@{/js/admin.js}" src="../../static/js/admin.js" defer="defer"></script> -->
<script th:src="@{/js/sb-admin-2.js}" src="../../static/js/sb-admin-2.js" defer="defer"></script>
<link href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" rel="stylesheet">
<link href="http://fonts.googleapis.com/css?family=Roboto:400,700,300" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/startbootstrap-sb-admin-2/4.0.7/css/sb-admin-2.min.css" integrity="sha512-FXgL8f6gtCYx8PjODtilf5GCHlgTDdIVZKRcUT/smwfum7hr4M1ytewqTtNd9LK4/CzbW4czU6Tr3f3Xey6lRg==" crossorigin="anonymous" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!-- <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.17.1/dist/bootstrap-table.min.css"> -->
<!-- <link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/css/jquery.dataTables.min.css"
integrity="sha512-1k7mWiTNoyx2XtmI96o+hdjP8nn0f3Z2N4oF/9ZZRgijyV4omsKOXEnqL1gKQNPy2MTSP9rIEWGcH/CInulptA=="
crossorigin="anonymous" /> -->
<!-- <link rel="stylesheet" th:href="@{/css/admin-table.css}" href="../../../resources/static/css/admin-table.css"> -->
<link rel="stylesheet" type="text/css"
href="https://cdn.datatables.net/v/bs4/jszip-2.5.0/dt-1.10.21/b-1.6.2/b-html5-1.6.2/datatables.min.css" />
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
<script type="text/javascript"
src="https://cdn.datatables.net/v/bs4/jszip-2.5.0/dt-1.10.21/b-1.6.2/b-html5-1.6.2/datatables.min.js"></script>
<script src="https://cdn.datatables.net/select/1.3.1/js/dataTables.select.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/select/1.3.1/css/select.dataTables.min.css">
<link th:href="@{/css/admin-custom.css}" href="../../static/css/admin-custom.css" rel="stylesheet">
<script th:src="@{'/js/' + ${@environment.getProperty('admin-bundle')}}" src="../../static/js/adminBundle.js"
defer="defer"></script>
</th:block>
<link href="../../static/css/sb-admin-2.css" rel="stylesheet" th:if="false">
<link href="../../static/css/admin-custom.css" rel="stylesheet" th:if="false">
@ -27,7 +100,8 @@
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" th:href="@{/admin}" href="../admin/home.html">
<a class="sidebar-brand d-flex align-items-center justify-content-center" th:href="@{/admin}"
href="../admin/home.html">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-laugh-wink"></i>
</div>
@ -54,15 +128,17 @@
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseTwo">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseOne"
aria-expanded="true" aria-controls="collapseTwo">
<i class="fas fa-fw fa-cog"></i>
<span>Users</span>
</a>
<div id="collapseOne" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
<div class="bg-dark py-2 collapse-inner rounded">
<h6 class="collapse-header">Settings:</h6>
<a class="collapse-item" th:href="@{/admin/change-passphrase}" href="../admin/change-passphrase.html">Change Passphrase</a>
<a class="collapse-item" href="cards.html">Cards</a>
<a class="collapse-item" th:href="@{/admin/change-passphrase}"
href="../admin/change-passphrase.html">Change Passphrase</a>
<a class="collapse-item" th:href="@{/admin/users}">View users</a>
</div>
</div>
</li>
@ -78,7 +154,8 @@
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="true" aria-controls="collapseTwo">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseTwo"
aria-expanded="true" aria-controls="collapseTwo">
<i class="fas fa-fw fa-cog"></i>
<span>Components</span>
</a>
@ -93,11 +170,13 @@
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseUtilities" aria-expanded="true" aria-controls="collapseUtilities">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseUtilities"
aria-expanded="true" aria-controls="collapseUtilities">
<i class="fas fa-fw fa-wrench"></i>
<span>Utilities</span>
</a>
<div id="collapseUtilities" class="collapse" aria-labelledby="headingUtilities" data-parent="#accordionSidebar">
<div id="collapseUtilities" class="collapse" aria-labelledby="headingUtilities"
data-parent="#accordionSidebar">
<div class="bg-dark py-2 collapse-inner rounded">
<h6 class="collapse-header">Custom Utilities:</h6>
<a class="collapse-item" href="utilities-color.html">Colors</a>
@ -118,11 +197,13 @@
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages" aria-expanded="true" aria-controls="collapsePages">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages"
aria-expanded="true" aria-controls="collapsePages">
<i class="fas fa-fw fa-folder"></i>
<span>Pages</span>
</a>
<div id="collapsePages" class="collapse" aria-labelledby="headingPages" data-parent="#accordionSidebar">
<div id="collapsePages" class="collapse" aria-labelledby="headingPages"
data-parent="#accordionSidebar">
<div class="bg-dark py-2 collapse-inner rounded">
<h6 class="collapse-header">Login Screens:</h6>
<a class="collapse-item" href="login.html">Login</a>
@ -174,17 +255,19 @@
<!-- Sidebar Toggle (Topbar) -->
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<i class="fa fa-bars"></i>
</button>
<!-- Topbar Search -->
<form class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
<form
class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
<div class="input-group">
<input type="text" class="form-control bg-light border-0 small" placeholder="Search for..." aria-label="Search" aria-describedby="basic-addon2">
<input type="text" class="form-control bg-light border-0 small"
placeholder="Search for..." aria-label="Search" aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
@ -195,18 +278,22 @@
<!-- Nav Item - Search Dropdown (Visible Only XS) -->
<li class="nav-item dropdown no-arrow d-sm-none">
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-search fa-fw"></i>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in" aria-labelledby="searchDropdown">
<div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in"
aria-labelledby="searchDropdown">
<form class="form-inline mr-auto w-100 navbar-search">
<div class="input-group">
<input type="text" class="form-control bg-light border-0 small" placeholder="Search for..." aria-label="Search" aria-describedby="basic-addon2">
<input type="text" class="form-control bg-light border-0 small"
placeholder="Search for..." aria-label="Search"
aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
@ -221,13 +308,15 @@
<!-- Nav Item - Alerts -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-bell fa-fw"></i>
<!-- Counter - Alerts -->
<span class="badge badge-danger badge-counter">3+</span>
</a>
<!-- Dropdown - Alerts -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="alertsDropdown">
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="alertsDropdown">
<h6 class="dropdown-header">
Alerts Center
</h6>
@ -239,7 +328,8 @@
</div>
<div>
<div class="small text-gray-500">December 12, 2019</div>
<span class="font-weight-bold">A new monthly report is ready to download!</span>
<span class="font-weight-bold">A new monthly report is ready to
download!</span>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
@ -264,63 +354,76 @@
Spending Alert: We've noticed unusually high spending for your account.
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Show All
Alerts</a>
</div>
</li>
<!-- Nav Item - Messages -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-envelope fa-fw"></i>
<!-- Counter - Messages -->
<span class="badge badge-danger badge-counter">7</span>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="messagesDropdown">
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="messagesDropdown">
<h6 class="dropdown-header">
Message Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/fn_BT9fwg_E/60x60" alt="">
<img class="rounded-circle"
src="https://source.unsplash.com/fn_BT9fwg_E/60x60" alt="">
<div class="status-indicator bg-success"></div>
</div>
<div class="font-weight-bold">
<div class="text-truncate">Hi there! I am wondering if you can help me with a problem I've been having.</div>
<div class="text-truncate">Hi there! I am wondering if you can help me with
a problem I've been having.</div>
<div class="small text-gray-500">Emily Fowler · 58m</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/AU4VPcFN4LE/60x60" alt="">
<img class="rounded-circle"
src="https://source.unsplash.com/AU4VPcFN4LE/60x60" alt="">
<div class="status-indicator"></div>
</div>
<div>
<div class="text-truncate">I have the photos that you ordered last month, how would you like them sent to you?</div>
<div class="text-truncate">I have the photos that you ordered last month,
how would you like them sent to you?</div>
<div class="small text-gray-500">Jae Chun · 1d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/CS2uCrpNzJY/60x60" alt="">
<img class="rounded-circle"
src="https://source.unsplash.com/CS2uCrpNzJY/60x60" alt="">
<div class="status-indicator bg-warning"></div>
</div>
<div>
<div class="text-truncate">Last month's report looks great, I am very happy with the progress so far, keep up the good work!</div>
<div class="text-truncate">Last month's report looks great, I am very happy
with the progress so far, keep up the good work!</div>
<div class="small text-gray-500">Morgan Alvarez · 2d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/Mv9hjnEUHR4/60x60" alt="">
<img class="rounded-circle"
src="https://source.unsplash.com/Mv9hjnEUHR4/60x60" alt="">
<div class="status-indicator bg-success"></div>
</div>
<div>
<div class="text-truncate">Am I a good boy? The reason I ask is because someone told me that people say this to all dogs, even if they aren't good...</div>
<div class="text-truncate">Am I a good boy? The reason I ask is because
someone told me that people say this to all dogs, even if they aren't
good...</div>
<div class="small text-gray-500">Chicken the Dog · 2w</div>
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Read More
Messages</a>
</div>
</li>
@ -328,7 +431,8 @@
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-white small" th:text="${#authentication.name}">Valerie Luna</span>
<img class="img-profile rounded-circle" src="https://source.unsplash.com/QAB-WJcbgJk/60x60">
</a>

View File

@ -64,6 +64,9 @@
let loglevel = /*[[${@environment.getProperty('chatto.frontend.log-level')}]]*/ "DEBUG";
window.log.setLevel(loglevel)
let chatWorkerBundle = /*[[${@environment.getProperty('chat-worker-bundle')}]]*/ "chatWorker.js";
localStorage.setItem('CHAT_WORKER_BUNDLE', "/js/" + chatWorkerBundle)
let chatPageSize = /*[[${@environment.getProperty('chatto.frontend.chat-page-size')}]]*/ 9;
localStorage.setItem("CHAT_PAGE_SIZE", chatPageSize);
</script>
@ -75,6 +78,27 @@
<body>
<th:block th:fragment="footerFragment">
<!-- Footer -->
<footer class="footer text-small py-2 bg-dark">
<div class="container-fluid">
<div class="row justify-content-between">
<div class="col-md-4 text-center text-md-left ">Chatto Version:
<strong th:text="${@environment.getProperty('git.build.version')}">None
</strong>
</div>
<div class="col-md-4 text-center text-md-right">Branch <strong
th:text="${@environment.getProperty('git.branch')}">
None</strong>
</div>
</div>
</div>
</footer>
<!-- Footer -->
</th:block>
</body>
</html>

View File

@ -6,10 +6,13 @@
<meta charset="UTF-8">
<title id="pageTitle">Navbar Fragment</title>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.15.0/esm/popper.js" type="module" th:if="false"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.css" th:if="false" rel="stylesheet" type="text/css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.15.0/esm/popper.js" type="module"
th:if="false"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.css" th:if="false"
rel="stylesheet" type="text/css">
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js" defer="defer" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js" defer="defer"
th:if="false"></script>
</head>
<body>
@ -18,47 +21,6 @@
<nav class="navbar navbar-expand-sm bg-dark navbar-dark fixed-top">
<div class="container">
<a href="home.html" th:href="@{/}" class="navbar-brand">Chatto</a>
<!-- <div class="navbar-header">
<button class="navbar-toggler" data-toggle="collapse" data-target="#navbarCollapse">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a href="home.html" th:href="@{/}" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="user/home.html" sec:authorize="isFullyAuthenticated()" th:href="@{/user}" class="nav-link">User Area</a>
</li>
<li class="nav-item">
<a th:href="chat" href="chat.html" class="nav-link">Chat</a>
</li>
<li class="nav-item">
<a th:href="login" sec:authorize="!isFullyAuthenticated()" href="login.html" class="nav-link">Login</a>
</li>
<li class="nav-item">
<a th:href="registration" sec:authorize="!isFullyAuthenticated()" href="registration.html" class="nav-link">Register</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">Contact</a>
</li>
<li class="nav-item">
<a href="#" sec:authorize="isFullyAuthenticated()" th:href="@{/admin}" class="nav-link">
Admin Area
</a>
</li>
<li class="nav-item">
<a href="#" sec:authorize="isFullyAuthenticated()" th:text="${#authentication.name}" class="nav-link text-white font-weight-bold">
nova
</a>
</li>
</ul>
</div> -->
<th:block th:include="fragments/navbar :: navbarContent"></th:block>
</div>
</nav>
@ -66,42 +28,46 @@
</th:block>
<th:block th:fragment="navbarContent">
<div class="navbar-header">
<button class="navbar-toggler" data-toggle="collapse" data-target="#navbarCollapse">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a href="home.html" th:href="@{/}" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="user/home.html" sec:authorize="isFullyAuthenticated()" th:href="@{/user}" class="nav-link">User Area</a>
</li>
<li class="nav-item">
<a th:href="chat" href="chat.html" class="nav-link">Chat</a>
</li>
<li class="nav-item">
<a th:href="login" sec:authorize="!isFullyAuthenticated()" href="login.html" class="nav-link">Login</a>
</li>
<li class="nav-item">
<a th:href="registration" sec:authorize="!isFullyAuthenticated()" href="registration.html" class="nav-link">Register</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">Contact</a>
</li>
<li class="nav-item">
<a href="#" sec:authorize="hasRole('ROLE_ADMIN') || hasRole('ROLE_SUPER_USER')" th:href="@{/admin}" class="nav-link">
Admin Area
</a>
</li>
<li class="nav-item">
<a href="#" sec:authorize="isFullyAuthenticated()" th:text="${#authentication.name}" class="nav-link text-white font-weight-bold">
<div class="navbar-header">
<button class="navbar-toggler" data-toggle="collapse" data-target="#navbarCollapse">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a href="home.html" th:href="@{/}" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="user/home.html" sec:authorize="isFullyAuthenticated()" th:href="@{/user}"
class="nav-link">User Area</a>
</li>
<li class="nav-item">
<a th:href="@{/chat}" href="chat.html" class="nav-link">Chat</a>
</li>
<li class="nav-item">
<a th:href="login" sec:authorize="!isFullyAuthenticated()" href="login.html"
class="nav-link">Login</a>
</li>
<li class="nav-item">
<a th:href="registration" sec:authorize="!isFullyAuthenticated()" href="registration.html"
class="nav-link">Register</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">Contact</a>
</li>
<li class="nav-item">
<a href="#" sec:authorize="hasRole('ROLE_ADMIN') || hasRole('ROLE_SUPER_USER')" th:href="@{/admin}"
class="nav-link">
Admin Area
</a>
</li>
<li class="nav-item">
<a href="#" sec:authorize="isFullyAuthenticated()" th:text="${#authentication.name}" class="nav-link text-white font-weight-bold">
nova
</a>
</li>

View File

@ -12,7 +12,8 @@
<script src="https://code.jquery.com/jquery-2.1.4.min.js" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js" defer="defer" th:if="false"></script>
<script src="http://blackpeppersoftware.github.io/thymeleaf-fragment.js/thymeleaf-fragment.js" defer="defer"
th:if="false"></script>
</link>
@ -93,19 +94,7 @@
</div>
</div>
</header>
<section id="my-section">
<div class="container">
<div class="row">
<div class="col text-center py-5">
<h1 class="display-4">
<p class="lead">Lorem ipsum dolor sit, amet consectetur adipisicing elit. Aliquid illum ea accusamus animi voluptate. Quam temporibus aperiam, similique in labore sint quasi harum. Praesentium enim iste dicta quaerat perspiciatis eos.</p>
</h1>
<a href="#" class="btn btn-secondary">Find out more</a>
</div>
</div>
</div>
</section>
<th:block th:include="fragments/head :: footerFragment"></th:block>
</body>
</html>

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