Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4e50547d43 | ||
bebea782c7 | |||
75f4b6a178 | |||
c1a2e77e4d | |||
|
5af0868021 | ||
4410a17fcf | |||
535cca10bd | |||
7cb753ed2e | |||
|
2224416988 | ||
00c99afbcf | |||
|
cbf4b0b9a3 | ||
|
2096215195 | ||
|
8315a23855 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/frontend/node_modules/
|
||||
/build
|
||||
/.idea
|
15
build.gradle
15
build.gradle
@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.0.2'
|
||||
id 'org.springframework.boot' version '2.6.3'
|
||||
id 'io.spring.dependency-management' version '1.1.0'
|
||||
}
|
||||
|
||||
@ -14,7 +14,20 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'com.h2database:h2:2.1.210'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'org.springframework.boot:spring-boot-devtools'
|
||||
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
|
||||
implementation 'org.webjars:bootstrap:5.1.3'
|
||||
implementation 'org.webjars:jquery:3.6.0'
|
||||
implementation 'org.webjars:font-awesome:6.1.0'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
implementation 'com.auth0:java-jwt:4.4.0'
|
||||
implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.5'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
implementation 'org.hibernate.validator:hibernate-validator'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
|
17
frontend/README.md
Normal file
17
frontend/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
frontend/babel.config.js
Normal file
5
frontend/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Калькулятор</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
||||
<div class="m-auto dflex flex-row align-items-center">
|
||||
<div class="col-sm-3 my-1">
|
||||
<input type="number" class="form-control" id="num1">
|
||||
</div>
|
||||
<div class="col-sm-3 my-1">
|
||||
<select class="form-control" id="operation">
|
||||
<option value="sum" selected>+</option>
|
||||
<option value="sub">-</option>
|
||||
<option value="mul">*</option>
|
||||
<option value="div">/</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-3 my-1">
|
||||
<input type="number" class="form-control" id="num2">
|
||||
</div>
|
||||
<div class="col-sm-3 my-1">
|
||||
<button type="button" class="form-control btn btn-light" id="get-result" onclick="calculate()">=</button>
|
||||
</div>
|
||||
<div class="col-sm-3 my-1">
|
||||
<p class="h5" id="result"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function calculate() {
|
||||
const num1 = document.getElementById("num1").value
|
||||
const num2 = document.getElementById("num2").value
|
||||
document.getElementById("result").innerHTML = await (await fetch(`http://127.0.0.1:8080/${document.getElementById("operation").value}?num1=${num1}&num2=${num2}`)).text()
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const http = require('http')
|
||||
|
||||
const requestListener = async function (req, res) {
|
||||
res.writeHead(200);
|
||||
fs.readFile('index.html', (err, data) => {
|
||||
res.end(data);
|
||||
});
|
||||
};
|
||||
|
||||
const server = http.createServer(requestListener)
|
||||
server.listen(5050)
|
19
frontend/jsconfig.json
Normal file
19
frontend/jsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
9
frontend/node_modules/fs/README.md
generated
vendored
9
frontend/node_modules/fs/README.md
generated
vendored
@ -1,9 +0,0 @@
|
||||
# Security holding package
|
||||
|
||||
This package name is not currently in use, but was formerly occupied
|
||||
by another package. To avoid malicious use, npm is hanging on to the
|
||||
package name, but loosely, and we'll probably give it to you if you
|
||||
want it.
|
||||
|
||||
You may adopt this package by contacting support@npmjs.com and
|
||||
requesting the name.
|
46
frontend/node_modules/fs/package.json
generated
vendored
46
frontend/node_modules/fs/package.json
generated
vendored
@ -1,46 +0,0 @@
|
||||
{
|
||||
"_from": "fs",
|
||||
"_id": "fs@0.0.1-security",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==",
|
||||
"_location": "/fs",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "fs",
|
||||
"name": "fs",
|
||||
"escapedName": "fs",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
|
||||
"_shasum": "8a7bd37186b6dddf3813f23858b57ecaaf5e41d4",
|
||||
"_spec": "fs",
|
||||
"_where": "C:\\Users\\user\\Desktop\\something\\frontend",
|
||||
"author": "",
|
||||
"bugs": {
|
||||
"url": "https://github.com/npm/security-holder/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"description": "This package name is not currently in use, but was formerly occupied by another package. To avoid malicious use, npm is hanging on to the package name, but loosely, and we'll probably give it to you if you want it.",
|
||||
"homepage": "https://github.com/npm/security-holder#readme",
|
||||
"keywords": [],
|
||||
"license": "ISC",
|
||||
"main": "index.js",
|
||||
"name": "fs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/npm/security-holder.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"version": "0.0.1-security"
|
||||
}
|
9
frontend/node_modules/http/README.md
generated
vendored
9
frontend/node_modules/http/README.md
generated
vendored
@ -1,9 +0,0 @@
|
||||
# Security holding package
|
||||
|
||||
This package name is not currently in use, but was formerly occupied
|
||||
by another package. To avoid malicious use, npm is hanging on to the
|
||||
package name, but loosely, and we'll probably give it to you if you
|
||||
want it.
|
||||
|
||||
You may adopt this package by contacting support@npmjs.com and
|
||||
requesting the name.
|
39
frontend/node_modules/http/package.json
generated
vendored
39
frontend/node_modules/http/package.json
generated
vendored
@ -1,39 +0,0 @@
|
||||
{
|
||||
"_from": "http",
|
||||
"_id": "http@0.0.1-security",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==",
|
||||
"_location": "/http",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "http",
|
||||
"name": "http",
|
||||
"escapedName": "http",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz",
|
||||
"_shasum": "3aac09129d12dc2747bbce4157afde20ad1f7995",
|
||||
"_spec": "http",
|
||||
"_where": "C:\\Users\\user\\Desktop\\something\\frontend",
|
||||
"bugs": {
|
||||
"url": "https://github.com/npm/security-holder/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"description": "security holding package",
|
||||
"homepage": "https://github.com/npm/security-holder#readme",
|
||||
"name": "http",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/npm/security-holder.git"
|
||||
},
|
||||
"version": "0.0.1-security"
|
||||
}
|
17567
frontend/package-lock.json
generated
17567
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,29 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"name": "front",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node index.js"
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs": "0.0.1-security",
|
||||
"http": "0.0.1-security"
|
||||
}
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"axios": "^1.3.4",
|
||||
"core-js": "^3.8.3",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
|
19
frontend/public/index.html
Normal file
19
frontend/public/index.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.slim.min.js" integrity="sha256-tG5mcZUtJsZvyKAxYLVXrmjKBVLd6VpVccqz/r4ypFE=" crossorigin="anonymous"></script>
|
||||
<title>Социальная сеть</title>
|
||||
</head>
|
||||
<body class="container">
|
||||
<script src="https://unpkg.com/@popperjs/core@2"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
77
frontend/src/App.vue
Normal file
77
frontend/src/App.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="nav row">
|
||||
<div class="nav-left col-10">
|
||||
<router-link to="/customers" v-bind:class="'button primary ' + this.$route.path.startsWith('/customers')? 'clear' : ''">Профили</router-link>
|
||||
<router-link to="/feed" v-bind:class="'button primary ' + this.$route.path.startsWith('/feed')? 'clear' : ''">Посты</router-link>
|
||||
<router-link v-if="role === 'ADMIN'" to="/admin" v-bind:class="'button primary ' + this.$route.path.startsWith('/admin')? 'clear' : ''">Администрирование</router-link>
|
||||
</div>
|
||||
<div class="nav-right col-2">
|
||||
<router-link v-if="this.token_value == null" to="/login" v-bind:class="'button primary ' + this.$route.path.startsWith('/login')? 'clear' : ''">Войти</router-link>
|
||||
<button v-if="this.token_value != null" v-on:click="logout()" v-bind:class="'button primary ' + this.$route.path.startsWith('/signup')? 'clear' : ''">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
token_value: localStorage.getItem("token"),
|
||||
role: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
this.token = null
|
||||
this.role = ''
|
||||
localStorage.clear()
|
||||
this.$router.push('login')
|
||||
},
|
||||
async actualRole() {
|
||||
let response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/role/" + localStorage.getItem("token"),
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.role = await response.text()
|
||||
localStorage.setItem("role", this.role)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
token: {
|
||||
get: function() {
|
||||
return this.token_value
|
||||
},
|
||||
set: function(value) {
|
||||
this.token_value = value
|
||||
localStorage.setItem("token", value)
|
||||
localStorage.setItem("role", this.role)
|
||||
}
|
||||
}
|
||||
},
|
||||
async beforeMount() {
|
||||
if (localStorage.getItem("token") === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.actualRole()
|
||||
|
||||
const component = this
|
||||
|
||||
document.addEventListener('token_changed', async function() {
|
||||
component.token = localStorage.getItem("token")
|
||||
await component.actualRole()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="">
|
||||
|
||||
</style>
|
223
frontend/src/components/Admin.vue
Normal file
223
frontend/src/components/Admin.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row mb-5">
|
||||
<p class='is-center h2'>Профили</p>
|
||||
</div>
|
||||
<div class="row mb-5">
|
||||
<div class="col"></div>
|
||||
<div class="col-10 is-center">
|
||||
<button class="button primary" data-bs-toggle="modal" data-bs-target="#customerCreate">
|
||||
Добавить нового пользователя
|
||||
</button>
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
|
||||
<p class='h3 is-center row mb-5'>Список профилей</p>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row card mb-3">
|
||||
<div class="row">
|
||||
<div class="col-3 is-left h3 fw-bold">ID</div>
|
||||
<div class="col-3 is-center h3 fw-bold">Никнейм</div>
|
||||
<div class="col-3 is-right h3 fw-bold">Пароль</div>
|
||||
<div class="col-2"></div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="customer in customers" class="row card mb-3">
|
||||
<div class="row">
|
||||
<div class="col-3 is-left h3">{{ customer.id }}</div>
|
||||
<router-link :to="{name: 'Customers', params: {'id': customer.id}}" class="col-3 is-center h3">{{ customer.username }}</router-link>
|
||||
<div class="col-3 is-right h3">
|
||||
<span style="text-overflow: ellipsis; overflow: hidden; max-width: 10ch; white-space: nowrap">
|
||||
{{ customer.password }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button style="max-width: 66px; max-height: 38px;" v-on:click="prepareEditModal(customer)" class="button primary outline is-right" data-bs-toggle="modal" data-bs-target="#customerEdit">
|
||||
<i class="fa fa-pencil" aria-hidden="true">
|
||||
</i>
|
||||
</button>
|
||||
|
||||
<div class="col-1 is-right">
|
||||
<button class="button dark outline is-right" v-on:click="deleteCustomer(customer)" style="max-width: 66px; max-height: 38px;">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="customerCreate" tabindex="-1" role="dialog" aria-labelledby="customerCreateLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="customerCreateLabel">Создать профиль</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p>Логин</p>
|
||||
<textarea name="username" v-model="newCustomer.username" cols="30" rows="1"></textarea>
|
||||
<p>Пароль</p>
|
||||
<textarea name="password" v-model="newCustomer.password" id="passwordTextC" cols="30" rows="1"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button type="button" v-on:click="createCustomer()" class="btn btn-primary" data-bs-dismiss="modal">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="customerEdit" tabindex="-1" role="dialog" aria-labelledby="customerEditLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content" id="edit-customer-form">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="customerEditLabel">Редактировать профиль</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p>Логин</p>
|
||||
<textarea name="username" v-model="editedCustomer.username" cols="30" rows="1"></textarea>
|
||||
<p>Пароль</p>
|
||||
<textarea name="password" v-model="editedCustomer.password" cols="30" rows="1"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button type="button" v-on:click="editCustomer()" class="btn btn-primary" data-bs-dismiss="modal">Изменить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
editedCustomer: {
|
||||
id: -1,
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
newCustomer: {
|
||||
id: -1,
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
customers: [],
|
||||
currentCustomerId: -1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateCustomers() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.customers = await response.json()
|
||||
},
|
||||
async createCustomer() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"username": this.newCustomer.username,
|
||||
"password": this.newCustomer.password
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await this.updateCustomers()
|
||||
},
|
||||
async editCustomer() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/" + this.editedCustomer.id,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"username": this.editedCustomer.username,
|
||||
"password": this.editedCustomer.password
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (this.currentCustomerId == this.editedCustomer.id) {
|
||||
localStorage.clear()
|
||||
document.dispatchEvent(new Event("token_changed"))
|
||||
this.$router.replace("login")
|
||||
}
|
||||
|
||||
await this.updateCustomers()
|
||||
},
|
||||
async deleteCustomer(customer) {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/" + customer.id,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (this.currentCustomerId == this.editedCustomer.id) {
|
||||
localStorage.clear()
|
||||
document.dispatchEvent(new Event("token_changed"))
|
||||
this.$router.replace("login")
|
||||
}
|
||||
|
||||
await this.updateCustomers()
|
||||
},
|
||||
async prepareEditModal(customer) {
|
||||
this.editedCustomer.username = customer.username
|
||||
this.editedCustomer.id = customer.id
|
||||
},
|
||||
async getCurrentCustomer() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/me",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.currentCustomerId = (await response.json())['id']
|
||||
}
|
||||
},
|
||||
async beforeMount() {
|
||||
if (localStorage.getItem("role") !== "ADMIN") {
|
||||
this.$router.replace("login")
|
||||
}
|
||||
if (!localStorage.getItem("token") === null) {
|
||||
this.$router.replace("login")
|
||||
}
|
||||
|
||||
await this.getCurrentCustomer()
|
||||
|
||||
await this.updateCustomers()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
165
frontend/src/components/Customers.vue
Normal file
165
frontend/src/components/Customers.vue
Normal file
@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row mb-5">
|
||||
<p class='is-center h2'>Профили</p>
|
||||
</div>
|
||||
|
||||
<p class='h3 is-center row mb-5'>Список профилей</p>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div v-for="customer in customers" class="row card mb-3">
|
||||
<div class="row">
|
||||
<div class="col is-left h3">ID: {{ customer['id'] }}</div>
|
||||
<div class="col is-right h3">Никнейм: {{ customer['username'] }}</div>
|
||||
</div>
|
||||
<p class="row">Комментарии:</p>
|
||||
<div class="row" v-if="customer['comments'].length !== 0">
|
||||
<div class="col">
|
||||
<div v-for="comment in customer['comments']" class="row is-left card mb-3">
|
||||
<div class="row is-left h4">"{{ comment['content'] }}" - к посту "{{ comment['postTitle'] }}" от пользователя {{ comment['postAuthor'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="row">Нет комментариев</p>
|
||||
<p class="row">Посты: </p>
|
||||
<div class="row" v-if="customer['posts'].length !== 0">
|
||||
<div class="col">
|
||||
<div v-for="post in customer['posts']" class="row is-left card mb-3">
|
||||
<div class="row is-center h1">{{ post['title'] }}</div>
|
||||
<div class="row is-left h3">{{ post['content'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="row">Нет постов</p>
|
||||
<div class="row" v-if="currentCustomerId === customer['id']">
|
||||
<button v-on:click="deleteCustomer(customer)" class="button dark outline col">Удалить</button>
|
||||
<button v-on:click="prepareEditModal(customer)" class="col button primary outline" data-bs-toggle="modal" data-bs-target="#customerEdit">Редактировать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="customerEdit" tabindex="-1" role="dialog" aria-labelledby="customerEditLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content" id="edit-customer-form">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="customerEditLabel">Редактировать профиль</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p>Логин</p>
|
||||
<textarea name="username" id="usernameTextE" cols="30" rows="1"></textarea>
|
||||
<p>Пароль</p>
|
||||
<textarea name="password" id="passwordTextE" cols="30" rows="1"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button v-on:click="editCustomer()" type="button" class="btn btn-primary">Изменить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
customers: [],
|
||||
currentCustomerId: -1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateCustomers() {
|
||||
if (!this.$route.params.id) {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
this.customers = await response.json()
|
||||
}
|
||||
else {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/" + this.$route.params.id,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
this.customers = [await response.json()]
|
||||
}
|
||||
},
|
||||
async getCurrentCustomer() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/me",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.currentCustomerId = (await response.json())['id']
|
||||
},
|
||||
prepareEditModal(customer) {
|
||||
$("#usernameTextE").val(customer['username'])
|
||||
},
|
||||
async deleteCustomer() {
|
||||
await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/" + this.currentCustomerId,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
localStorage.clear()
|
||||
this.$router.push("login")
|
||||
},
|
||||
async editCustomer() {
|
||||
await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/" + this.currentCustomerId,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"username": $("#usernameTextE").val(),
|
||||
"password": $("#passwordTextE").val()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
localStorage.clear()
|
||||
|
||||
this.$router.replace("login")
|
||||
}
|
||||
},
|
||||
async beforeMount() {
|
||||
if (localStorage.getItem("token") == null) {
|
||||
this.$router.push("login")
|
||||
}
|
||||
|
||||
await Promise.all([this.updateCustomers(), this.getCurrentCustomer()])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="">
|
||||
|
||||
</style>
|
338
frontend/src/components/Feed.vue
Normal file
338
frontend/src/components/Feed.vue
Normal file
@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row mb-5">
|
||||
<p class='is-center h2'>Посты</p>
|
||||
</div>
|
||||
<form class="row mb-5" @submit.prevent="searchPosts">
|
||||
<input type="search" v-model="searchQuery" class="col-10">
|
||||
<button class="button primary col" type="submit">Найти</button>
|
||||
</form>
|
||||
<div class="row mb-5" v-if="currentCustomerId > 0">
|
||||
<div class="col"></div>
|
||||
<form class="col-10" @submit.prevent="createPost">
|
||||
<input name="customerId" type="text" style="display: none;" :value="currentCustomerId">
|
||||
<div class="row mb-4 is-center">
|
||||
<p class="col-2 is-left mb-2">Заголовок:</p>
|
||||
<input name="title" type="text" class="col-6" v-model="newPost.title" />
|
||||
</div>
|
||||
<div class="row mb-4 is-center">
|
||||
<p class="col-2 is-left mb-2">Текст:</p>
|
||||
<textarea name="content" class="col-6" v-model="newPost.content"></textarea>
|
||||
</div>
|
||||
<div class="row is-center">
|
||||
<button type='submit' class="button primary col-8">
|
||||
Опубликовать
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
<div v-if="posts.length > 0" class="row">
|
||||
<div class="col">
|
||||
<div class="row mb-5 card" v-for="post in posts" :key="post.id">
|
||||
<div class="col">
|
||||
<div class="row h3">
|
||||
<div class="col is-left">
|
||||
<p>Автор: <a :href="'/customers/' + post.customerId" class="text-primary">{{ post.customerName }}</a></p>
|
||||
</div>
|
||||
<div class="col is-right">
|
||||
<p>Дата: <span class="text-primary">{{ post.createDate }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<span class="h1">{{ post.title }}</span>
|
||||
</div>
|
||||
<div class="row text-center mb-5">
|
||||
<span class="h3">{{ post.content }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p class="row h2 is-center mb-3">Комментарии</p>
|
||||
<div v-if="post.comments.length > 0" class="row text-left mb-5 card" v-for="comment in post.comments" :key="comment.id">
|
||||
<div class="row mb-1">
|
||||
<div class="col is-left">
|
||||
<span class="h2 text-primary">{{ comment.customerName }}</span>
|
||||
</div>
|
||||
<div class="col is-right">
|
||||
<span class="h2 text-primary">{{ comment.createDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<span class="h3">{{ comment.content }}</span>
|
||||
</div>
|
||||
<div v-if="currentCustomerId === comment.customerId" class="row">
|
||||
<button @click="prepareCommentEditModal(comment)" class="button primary outline col" data-bs-toggle="modal" data-bs-target="#commentEdit">Изменить комментарий</button>
|
||||
<form @submit.prevent="deleteComment(comment.id)" class="col">
|
||||
<button type="submit" class="button error is-full-width">Удалить комментарий</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="h3 row is-center mb-5">Пусто</p>
|
||||
<form class="row" v-if="currentCustomerId !== -1" @submit.prevent="addComment(post.id)">
|
||||
<input name="content" type="text" class="col-9" v-model="newComment.content"/>
|
||||
<input name="customerId" type="text" style="display: none;" :value="currentCustomerId">
|
||||
<input name="postId" type="text" style="display: none;" :value="post.id">
|
||||
<button type="submit" class="button col-3 secondary outline">Комментировать</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="currentCustomerId === post.customerId">
|
||||
<form class="col" @submit.prevent="deletePost(post.id)">
|
||||
<button type="submit" class="is-full-width button dark outline">Удалить пост</button>
|
||||
</form>
|
||||
<button @click="preparePostEditModal(post)" class="col button primary outline" data-bs-toggle="modal" data-bs-target="#postEdit">Изменить пост</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="row text-center is-center">
|
||||
Нет постов
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for editing a post -->
|
||||
<div class="modal fade" id="postEdit" tabindex="-1" role="dialog" aria-labelledby="postEditLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="postEditLabel">Изменить пост</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="updatePost">
|
||||
<input type="hidden" name="postId" v-model="editedPost.id">
|
||||
<div class="form-group">
|
||||
<label for="postEditTitle">Заголовок</label>
|
||||
<input type="text" class="form-control" id="postEditTitle" v-model="editedPost.title">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="postEditContent">Содержание</label>
|
||||
<textarea class="form-control" id="postEditContent" rows="5" v-model="editedPost.content"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Сохранить изменения</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for editing a comment -->
|
||||
<div class="modal fade" id="commentEdit" tabindex="-1" role="dialog" aria-labelledby="commentEditLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="commentEditLabel">Изменить комментарий</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="updateComment">
|
||||
<input type="hidden" name="commentId" v-model="editedComment.id">
|
||||
<div class="form-group">
|
||||
<label for="commentEditContent">Содержание</label>
|
||||
<textarea class="form-control" id="commentEditContent" rows="5" v-model="editedComment.content"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Сохранить изменения</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
posts: [],
|
||||
currentCustomerId: -1,
|
||||
searchQuery: '',
|
||||
newPost: {
|
||||
title: '',
|
||||
content: ''
|
||||
},
|
||||
newComment: {
|
||||
content: ''
|
||||
},
|
||||
editedPost: {
|
||||
id: -1,
|
||||
title: '',
|
||||
content: ''
|
||||
},
|
||||
editedComment: {
|
||||
id: -1,
|
||||
content: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updatePosts() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/post",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.posts = await response.json()
|
||||
},
|
||||
async searchPosts() {
|
||||
if (this.searchQuery) {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/post/search?query=" + this.searchQuery,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.posts = await response.json()
|
||||
} else {
|
||||
await this.updatePosts()
|
||||
}
|
||||
},
|
||||
async createPost() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/post",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"customerId": this.currentCustomerId,
|
||||
"title": this.newPost.title,
|
||||
"content": this.newPost.content
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await this.updatePosts()
|
||||
},
|
||||
async updatePost() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/post/" + this.editedPost.id,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"title": this.editedPost.title,
|
||||
"content": this.editedPost.content,
|
||||
"customerId": this.currentCustomerId
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await this.updatePosts()
|
||||
},
|
||||
async deletePost(postId) {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/post/" + postId,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await this.updatePosts()
|
||||
},
|
||||
async addComment(postId) {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/comment",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"postId": postId,
|
||||
"content": this.newComment.content,
|
||||
"customerId": this.currentCustomerId
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await this.updatePosts()
|
||||
},
|
||||
async updateComment() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/comment/" + this.editedComment.id,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"content": this.editedComment.content
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await this.updatePosts()
|
||||
},
|
||||
async deleteComment(commentId) {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/comment/" + commentId,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await this.updatePosts()
|
||||
},
|
||||
prepareCommentEditModal(comment) {
|
||||
this.editedComment.id = comment.id
|
||||
this.editedComment.content = comment.content
|
||||
},
|
||||
preparePostEditModal(post) {
|
||||
this.editedPost.id = post.id
|
||||
this.editedPost.title = post.title
|
||||
this.editedPost.content = post.content
|
||||
},
|
||||
async getCurrentCustomer() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/me",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + localStorage.getItem("token")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.currentCustomerId = (await response.json()).id
|
||||
}
|
||||
},
|
||||
async beforeMount() {
|
||||
if (localStorage.getItem("token") == null) {
|
||||
this.$router.push("login")
|
||||
}
|
||||
|
||||
await Promise.all([this.updatePosts(), this.getCurrentCustomer()])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
64
frontend/src/components/Login.vue
Normal file
64
frontend/src/components/Login.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<div class="col-6">
|
||||
<div class="row mb-3">
|
||||
<label>Имя пользователя: <input type="text" id="username" required /></label>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label>Пароль: <input type="password" id="password" required /></label>
|
||||
</div>
|
||||
<div class="row mb-3 alert alert-danger" v-if="error !== null" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-4"></div>
|
||||
<button v-on:click="login()" type="button" class="button primary col" id="enter">Войти</button>
|
||||
<div class="col-4"></div>
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<router-link to="/signup" class="button primary outline">Регистрация</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
error: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
let response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer/jwt/login",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"username": $("#username").val(),
|
||||
"password": $("#password").val()
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status !== 200) {
|
||||
this.error = await response.text()
|
||||
} else {
|
||||
localStorage.setItem("token", await response.text())
|
||||
document.dispatchEvent(new Event("token_changed"))
|
||||
this.$router.push("feed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
69
frontend/src/components/Signup.vue
Normal file
69
frontend/src/components/Signup.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<div class="col-6">
|
||||
<div class="row">
|
||||
<label>Имя пользователя: <input type="text" id="username" required /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Пароль: <input v-on:change="check()" type="password" id="password" required /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Пароль повторно: <input v-on:change="check()" type="password" id="confirm-password" required /></label>
|
||||
</div>
|
||||
<div class="row mb-3 alert alert-danger" v-if="error !== null" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-4"></div>
|
||||
<button v-on:click="register()" class="button primary col" id="enter" disabled>Регистрация</button>
|
||||
<div class="col-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
error: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
check() {
|
||||
if ($("#password").val() === $("#confirm-password").val()) {
|
||||
$("#enter").removeAttr("disabled")
|
||||
} else {
|
||||
$("#enter").attr("disabled", "disabled")
|
||||
}
|
||||
},
|
||||
async register() {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/1.0/customer",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"username": $("#username").val(),
|
||||
"password": $("#password").val()
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status !== 200) {
|
||||
this.error = await response.text()
|
||||
} else {
|
||||
this.$router.push("login")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
45
frontend/src/main.js
Normal file
45
frontend/src/main.js
Normal file
@ -0,0 +1,45 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App'
|
||||
import { createRouter, createWebHistory } from "vue-router"
|
||||
|
||||
import Customers from './components/Customers'
|
||||
import Feed from './components/Feed'
|
||||
import Login from "@/components/Login.vue";
|
||||
import Signup from "@/components/Signup.vue";
|
||||
import Admin from "@/components/Admin.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/customers/:id?',
|
||||
name: "Customers",
|
||||
component: Customers
|
||||
},
|
||||
{
|
||||
path: '/feed',
|
||||
name: "Feed",
|
||||
component: Feed
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: "Login",
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
name: "Signup",
|
||||
component: Signup
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: "Admin",
|
||||
component: Admin
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
routes,
|
||||
history: createWebHistory()
|
||||
})
|
||||
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
4
frontend/vue.config.js
Normal file
4
frontend/vue.config.js
Normal file
@ -0,0 +1,4 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true
|
||||
})
|
58
src/main/java/np/something/DTO/AlbumDto.java
Normal file
58
src/main/java/np/something/DTO/AlbumDto.java
Normal file
@ -0,0 +1,58 @@
|
||||
package np.something.DTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import np.something.model.Album;
|
||||
import np.something.model.Photo;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
public class AlbumDto {
|
||||
private Long id;
|
||||
@Size(min=3, max=32, message="Album's name's size must be from 3 to 32 letters")
|
||||
private String name;
|
||||
private Long ownerId;
|
||||
private String ownerName;
|
||||
private List<PhotoDto> photos;
|
||||
|
||||
|
||||
public AlbumDto() {
|
||||
}
|
||||
|
||||
public AlbumDto(Album album) {
|
||||
this.id = album.getId();
|
||||
this.name = album.getName();
|
||||
this.ownerId = album.getOwner().getId();
|
||||
this.ownerName = album.getOwner().getUsername();
|
||||
this.photos = album.getPhotos().stream().map(PhotoDto::new).toList();
|
||||
}
|
||||
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
public Long getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
public String getOwnerName() {
|
||||
return ownerName;
|
||||
}
|
||||
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
public List<PhotoDto> getPhotos() {
|
||||
return photos;
|
||||
}
|
||||
}
|
56
src/main/java/np/something/DTO/CustomerDto.java
Normal file
56
src/main/java/np/something/DTO/CustomerDto.java
Normal file
@ -0,0 +1,56 @@
|
||||
package np.something.DTO;
|
||||
|
||||
import java.util.List;
|
||||
import np.something.model.Customer;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
public class CustomerDto {
|
||||
private Long id;
|
||||
@Size(min=3, max=32, message="Username string length must be from 3 to 32 letters")
|
||||
private String username;
|
||||
private List<AlbumDto> ownedAlbums;
|
||||
private List<AlbumDto> albums;
|
||||
|
||||
public CustomerDto(Customer customer) {
|
||||
this.id = customer.getId();
|
||||
this.username = customer.getUsername();
|
||||
this.ownedAlbums = customer.getOwnedAlbums().stream().map(AlbumDto::new).toList();
|
||||
this.albums = customer.getAlbums().stream().map(AlbumDto::new).toList();
|
||||
}
|
||||
|
||||
public CustomerDto() {
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public List<AlbumDto> getOwnedAlbums() {
|
||||
return ownedAlbums;
|
||||
}
|
||||
|
||||
public void setOwnedAlbums(List<AlbumDto> ownedAlbums) {
|
||||
this.ownedAlbums = ownedAlbums;
|
||||
}
|
||||
|
||||
public List<AlbumDto> getAlbums() {
|
||||
return albums;
|
||||
}
|
||||
|
||||
public void setAlbums(List<AlbumDto> albums) {
|
||||
this.albums = albums;
|
||||
}
|
||||
}
|
37
src/main/java/np/something/DTO/PhotoDto.java
Normal file
37
src/main/java/np/something/DTO/PhotoDto.java
Normal file
@ -0,0 +1,37 @@
|
||||
package np.something.DTO;
|
||||
|
||||
import np.something.model.Photo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PhotoDto {
|
||||
private Long id;
|
||||
private List<TagDto> tags;
|
||||
private Long albumId;
|
||||
|
||||
public PhotoDto() {
|
||||
|
||||
}
|
||||
|
||||
public PhotoDto(Photo photo) {
|
||||
this.id = photo.getId();
|
||||
this.tags = photo.getTags().stream().map(TagDto::new).toList();
|
||||
this.albumId = photo.getAlbum().getId();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public List<TagDto> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void setTags(List<TagDto> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
35
src/main/java/np/something/DTO/TagDto.java
Normal file
35
src/main/java/np/something/DTO/TagDto.java
Normal file
@ -0,0 +1,35 @@
|
||||
package np.something.DTO;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import np.something.model.Tag;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
import java.util.List;
|
||||
|
||||
public class TagDto {
|
||||
private Long id;
|
||||
@Size(min=3, max=32, message="Tag's name must be from 3 to 32 letters")
|
||||
private String name;
|
||||
|
||||
public TagDto() {
|
||||
|
||||
}
|
||||
|
||||
public TagDto(Tag tag) {
|
||||
this.id = tag.getId();
|
||||
this.name = tag.getName();
|
||||
}
|
||||
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
29
src/main/java/np/something/DTO/UserSignupDto.java
Normal file
29
src/main/java/np/something/DTO/UserSignupDto.java
Normal file
@ -0,0 +1,29 @@
|
||||
package np.something.DTO;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
public class UserSignupDto {
|
||||
@NotBlank
|
||||
@Size(min = 3, max = 64)
|
||||
private String username;
|
||||
@NotBlank
|
||||
@Size(min = 6, max = 64)
|
||||
private String password;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package np.something.Exceptions;
|
||||
|
||||
public class AlbumNotFoundException extends RuntimeException {
|
||||
public AlbumNotFoundException(Long id) {
|
||||
super(String.format("Album with id [%s] is not found", id));
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package np.something.Exceptions;
|
||||
|
||||
public class CustomerNotFoundException extends RuntimeException {
|
||||
public CustomerNotFoundException(Long id) {
|
||||
super(String.format("Customer with id [%s] is not found", id));
|
||||
}
|
||||
}
|
@ -1,11 +1,31 @@
|
||||
package np.something;
|
||||
|
||||
import np.something.security.SecurityConfiguration;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
WebMvcConfigurer.super.addViewControllers(registry);
|
||||
registry.addViewController("login");
|
||||
registry.addViewController("/notFound").setViewName("forward:/");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> containerCustomizer() {
|
||||
return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notFound"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**").allowedMethods("*");
|
||||
|
@ -1,41 +0,0 @@
|
||||
package np.something.controllers;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class MathController {
|
||||
@GetMapping("/sum")
|
||||
public int sum(
|
||||
@RequestParam(value = "num1", defaultValue = "0") int num1,
|
||||
@RequestParam(value = "num2", defaultValue = "0") int num2
|
||||
) {
|
||||
|
||||
return num1 + num2;
|
||||
}
|
||||
|
||||
@GetMapping("/sub")
|
||||
public int sub(
|
||||
@RequestParam(value = "num1", defaultValue = "0") int num1,
|
||||
@RequestParam(value = "num2", defaultValue = "0") int num2
|
||||
) {
|
||||
return num1 - num2;
|
||||
}
|
||||
|
||||
@GetMapping("/mul")
|
||||
public int mul(
|
||||
@RequestParam(value = "num1", defaultValue = "0") int num1,
|
||||
@RequestParam(value = "num2", defaultValue = "0") int num2
|
||||
) {
|
||||
return num1 * num2;
|
||||
}
|
||||
|
||||
@GetMapping("/div")
|
||||
public int div(
|
||||
@RequestParam(value = "num1", defaultValue = "0") int num1,
|
||||
@RequestParam(value = "num2", defaultValue = "0") int num2
|
||||
) {
|
||||
return num1 / num2;
|
||||
}
|
||||
}
|
94
src/main/java/np/something/model/Album.java
Normal file
94
src/main/java/np/something/model/Album.java
Normal file
@ -0,0 +1,94 @@
|
||||
package np.something.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.util.*;
|
||||
|
||||
@Entity
|
||||
public class Album {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Column
|
||||
@Size(min=3, max=32, message="Album's name's size must be from 3 to 32 letters")
|
||||
private String name;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "owner_fk")
|
||||
private Customer owner;
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(name = "albums_customers",
|
||||
joinColumns = {
|
||||
@JoinColumn(name = "album_id", referencedColumnName = "id",
|
||||
nullable = false, updatable = false)},
|
||||
inverseJoinColumns = {
|
||||
@JoinColumn(name = "customer_id", referencedColumnName = "id",
|
||||
nullable = false, updatable = false)})
|
||||
private Set<Customer> customers;
|
||||
|
||||
@OneToMany(mappedBy = "album", fetch = FetchType.EAGER)
|
||||
private List<Photo> photos = new ArrayList<>();
|
||||
|
||||
public Album(String name, Customer owner) {
|
||||
this.name = name;
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public Album() {
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Customer getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public void setOwner(Customer owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public Set<Customer> getCustomers() {
|
||||
return customers;
|
||||
}
|
||||
|
||||
public void setCustomers(Set<Customer> customers) {
|
||||
this.customers = customers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Album album = (Album) o;
|
||||
return id.equals(album.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public List<Photo> getPhotos() {
|
||||
return photos;
|
||||
}
|
||||
|
||||
public void setPhotos(List<Photo> photos) {
|
||||
this.photos = photos;
|
||||
}
|
||||
}
|
99
src/main/java/np/something/model/Customer.java
Normal file
99
src/main/java/np/something/model/Customer.java
Normal file
@ -0,0 +1,99 @@
|
||||
package np.something.model;
|
||||
|
||||
import org.h2.engine.User;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
@Column
|
||||
@Size(min=3, max=32, message="Username string length must be from 3 to 32 letters")
|
||||
private String username;
|
||||
@Column
|
||||
@Size(min=8, max=1024, message="Password string length must be from 8 to 1024 letters")
|
||||
private String password;
|
||||
|
||||
@ManyToMany(mappedBy = "customers", fetch = FetchType.EAGER)
|
||||
private Set<Album> albums = new HashSet<>();
|
||||
|
||||
@OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
|
||||
private List<Album> ownedAlbums = new ArrayList<>();
|
||||
|
||||
private UserRole role;
|
||||
|
||||
public Customer() {
|
||||
|
||||
}
|
||||
|
||||
public Customer(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.role = UserRole.USER;
|
||||
}
|
||||
|
||||
public Customer(String username, String password, UserRole role) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public UserRole getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null || getClass() != obj.getClass()) return false;
|
||||
Customer customer = (Customer) obj;
|
||||
return Objects.equals(id, customer.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
public Set<Album> getAlbums() {
|
||||
return albums;
|
||||
}
|
||||
|
||||
public void setAlbums(Set<Album> albums) {
|
||||
this.albums = albums;
|
||||
}
|
||||
|
||||
public List<Album> getOwnedAlbums() {
|
||||
return ownedAlbums;
|
||||
}
|
||||
|
||||
public void setOwnedAlbums(List<Album> ownedAlbums) {
|
||||
this.ownedAlbums = ownedAlbums;
|
||||
}
|
||||
}
|
70
src/main/java/np/something/model/Photo.java
Normal file
70
src/main/java/np/something/model/Photo.java
Normal file
@ -0,0 +1,70 @@
|
||||
package np.something.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
public class Photo {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "album_fk")
|
||||
private Album album;
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(name = "photos_tags",
|
||||
joinColumns = {
|
||||
@JoinColumn(name = "photo_id", referencedColumnName = "id",
|
||||
nullable = false, updatable = false)},
|
||||
inverseJoinColumns = {
|
||||
@JoinColumn(name = "tag_id", referencedColumnName = "id",
|
||||
nullable = false, updatable = false)})
|
||||
private Set<Tag> tags;
|
||||
|
||||
public Photo(Album album) {
|
||||
this.album = album;
|
||||
}
|
||||
|
||||
public Photo() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Photo photo = (Photo) o;
|
||||
return id.equals(photo.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
public Album getAlbum() {
|
||||
return album;
|
||||
}
|
||||
|
||||
public void setAlbum(Album album) {
|
||||
this.album = album;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Set<Tag> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void setTags(Set<Tag> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
64
src/main/java/np/something/model/Tag.java
Normal file
64
src/main/java/np/something/model/Tag.java
Normal file
@ -0,0 +1,64 @@
|
||||
package np.something.model;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
public class Tag {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Column
|
||||
@Size(min=3, max=32, message="Tag's name must be from 3 to 32 letters")
|
||||
private String name;
|
||||
|
||||
@ManyToMany(mappedBy = "tags")
|
||||
List<Photo> photos;
|
||||
|
||||
public Tag() {
|
||||
}
|
||||
|
||||
public Tag(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public List<Photo> getPhotos() {
|
||||
return photos;
|
||||
}
|
||||
|
||||
public void setPhotos(List<Photo> photos) {
|
||||
this.photos = photos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Tag tag = (Tag) o;
|
||||
return id.equals(tag.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
20
src/main/java/np/something/model/UserRole.java
Normal file
20
src/main/java/np/something/model/UserRole.java
Normal file
@ -0,0 +1,20 @@
|
||||
package np.something.model;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
public enum UserRole implements GrantedAuthority {
|
||||
ADMIN,
|
||||
USER;
|
||||
|
||||
private static final String PREFIX = "ROLE_";
|
||||
|
||||
@Override
|
||||
public String getAuthority() {
|
||||
return PREFIX + this.name();
|
||||
}
|
||||
|
||||
public static final class AsString {
|
||||
public static final String ADMIN = PREFIX + "ADMIN";
|
||||
public static final String USER = PREFIX + "USER";
|
||||
}
|
||||
}
|
132
src/main/java/np/something/mvc/AlbumMVC.java
Normal file
132
src/main/java/np/something/mvc/AlbumMVC.java
Normal file
@ -0,0 +1,132 @@
|
||||
package np.something.mvc;
|
||||
|
||||
import np.something.DTO.AlbumDto;
|
||||
import np.something.model.Album;
|
||||
import np.something.model.Customer;
|
||||
import np.something.services.AlbumService;
|
||||
import np.something.services.CustomerService;
|
||||
import np.something.services.PhotoService;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.Objects;
|
||||
|
||||
@Controller
|
||||
@RequestMapping(value = {"/albums"})
|
||||
public class AlbumMVC {
|
||||
private final AlbumService albumService;
|
||||
private final CustomerService customerService;
|
||||
private final PhotoService photoService;
|
||||
|
||||
public AlbumMVC(AlbumService albumService, CustomerService customerService, PhotoService photoService) {
|
||||
this.albumService = albumService;
|
||||
this.customerService = customerService;
|
||||
this.photoService = photoService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String albums(Model model) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
model.addAttribute("currentCustomer", customer);
|
||||
model.addAttribute("albums", customer.getAlbums());
|
||||
model.addAttribute("ownerAlbums", customer.getOwnedAlbums());
|
||||
|
||||
return "/albums";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String album(Model model, @PathVariable Long id) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
Album album = albumService.find(id);
|
||||
model.addAttribute("currentCustomer", customer);
|
||||
model.addAttribute("customers", customerService.findAllCustomers().stream().filter(x -> !Objects.equals(x.getId(), album.getOwner().getId()) && !album.getCustomers().contains(x)).toList());
|
||||
model.addAttribute("album", album);
|
||||
model.addAttribute("photos", album.getPhotos());
|
||||
|
||||
return "/album";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createAlbum(@ModelAttribute @Valid AlbumDto albumDto) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
albumService.add(albumDto.getName(), customer);
|
||||
|
||||
return "redirect:/albums";
|
||||
}
|
||||
|
||||
@PostMapping("/edit/{id}")
|
||||
public String editAlbum(@PathVariable Long id, @ModelAttribute @Valid AlbumDto albumDto) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
if (Objects.equals(albumService.find(id).getOwner().getId(), customer.getId())) {
|
||||
albumService.update(id, albumDto.getName());
|
||||
}
|
||||
|
||||
return "redirect:/albums/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/delete/{id}")
|
||||
public String deleteAlbum(@PathVariable Long id) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
if (Objects.equals(albumService.find(id).getOwner().getId(), customer.getId())) {
|
||||
albumService.delete(id);
|
||||
}
|
||||
|
||||
return "redirect:/albums";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/add/photo")
|
||||
public String addPhoto(@PathVariable Long id) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
if (Objects.equals(albumService.find(id).getOwner().getId(), customer.getId())) {
|
||||
albumService.addPhoto(id);
|
||||
}
|
||||
|
||||
return "redirect:/albums/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/add/customer/{cid}")
|
||||
public String addCustomer(@PathVariable Long id, @PathVariable Long cid) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
if (Objects.equals(albumService.find(id).getOwner().getId(), customer.getId())) {
|
||||
albumService.addCustomer(id, cid);
|
||||
}
|
||||
|
||||
return "redirect:/albums/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/remove/customer/{cid}")
|
||||
public String removeCustomer(@PathVariable Long id, @PathVariable Long cid) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
if (Objects.equals(albumService.find(id).getOwner().getId(), customer.getId())) {
|
||||
albumService.removeCustomer(id, cid);
|
||||
}
|
||||
|
||||
return "redirect:/albums/" + id;
|
||||
}
|
||||
}
|
94
src/main/java/np/something/mvc/PhotoMVC.java
Normal file
94
src/main/java/np/something/mvc/PhotoMVC.java
Normal file
@ -0,0 +1,94 @@
|
||||
package np.something.mvc;
|
||||
|
||||
import np.something.model.Album;
|
||||
import np.something.model.Customer;
|
||||
import np.something.model.Photo;
|
||||
import np.something.services.CustomerService;
|
||||
import np.something.services.PhotoService;
|
||||
import np.something.services.TagService;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.Objects;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/photos")
|
||||
public class PhotoMVC {
|
||||
private final PhotoService photoService;
|
||||
private final TagService tagService;
|
||||
|
||||
private final CustomerService customerService;
|
||||
|
||||
public PhotoMVC(PhotoService photoService, TagService tagService, CustomerService customerService) {
|
||||
this.photoService = photoService;
|
||||
this.tagService = tagService;
|
||||
this.customerService = customerService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String photo(Model model, @PathVariable Long id) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
Photo photo = photoService.getPhotoById(id);
|
||||
|
||||
model.addAttribute("currentCustomer", customer);
|
||||
model.addAttribute("photo", photo);
|
||||
model.addAttribute("tags", tagService.findAll().stream().filter(x -> !photo.getTags().contains(x)).toList());
|
||||
|
||||
return "/photo";
|
||||
}
|
||||
|
||||
@PostMapping("/delete/{id}")
|
||||
public String deletePhoto(@PathVariable Long id) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
Photo photo = photoService.getPhotoById(id);
|
||||
Album album = photo.getAlbum();
|
||||
|
||||
if (Objects.equals(album.getOwner().getId(), customer.getId())) {
|
||||
photoService.delete(id);
|
||||
}
|
||||
|
||||
return "redirect:/albums/" + album.getId();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/add/tag/{tag_id}")
|
||||
public String addTag(@PathVariable Long id, @PathVariable Long tag_id) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
Photo photo = photoService.getPhotoById(id);
|
||||
Album album = photo.getAlbum();
|
||||
|
||||
if (Objects.equals(album.getOwner().getId(), customer.getId())) {
|
||||
photoService.addTag(id, tag_id);
|
||||
}
|
||||
|
||||
return "redirect:/photos/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/remove/tag/{tag_id}")
|
||||
public String removeTag(@PathVariable Long id, @PathVariable Long tag_id) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
Photo photo = photoService.getPhotoById(id);
|
||||
Album album = photo.getAlbum();
|
||||
|
||||
if (Objects.equals(album.getOwner().getId(), customer.getId())) {
|
||||
photoService.removeTag(id, tag_id);
|
||||
}
|
||||
|
||||
return "redirect:/photos/" + id;
|
||||
}
|
||||
}
|
79
src/main/java/np/something/mvc/TagMVC.java
Normal file
79
src/main/java/np/something/mvc/TagMVC.java
Normal file
@ -0,0 +1,79 @@
|
||||
package np.something.mvc;
|
||||
|
||||
import np.something.DTO.TagDto;
|
||||
import np.something.model.Customer;
|
||||
import np.something.model.Tag;
|
||||
import np.something.model.UserRole;
|
||||
import np.something.services.CustomerService;
|
||||
import np.something.services.PhotoService;
|
||||
import np.something.services.TagService;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/tags")
|
||||
public class TagMVC {
|
||||
private final TagService tagService;
|
||||
private final CustomerService customerService;
|
||||
private final PhotoService photoService;
|
||||
|
||||
public TagMVC(TagService tagService, CustomerService customerService, PhotoService photoService) {
|
||||
this.tagService = tagService;
|
||||
this.customerService = customerService;
|
||||
this.photoService = photoService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String tags(Model model, @RequestParam(required = false) String search) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
model.addAttribute("currentCustomer", customer);
|
||||
if (search == null || search.isBlank()) {
|
||||
model.addAttribute("tags", tagService.findAll());
|
||||
} else {
|
||||
model.addAttribute("tags", tagService.tagsByTagPrefix(search));
|
||||
}
|
||||
|
||||
return "/tags";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String tag(Model model, @PathVariable Long id) {
|
||||
Customer customer = customerService.findByUsername(
|
||||
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
|
||||
);
|
||||
|
||||
Tag tag = tagService.find(id);
|
||||
|
||||
model.addAttribute("currentCustomer", customer);
|
||||
model.addAttribute("tag", tag);
|
||||
model.addAttribute("photos", tagService.customersPhotosByTag(customer, id));
|
||||
|
||||
return "/tag";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Secured({UserRole.AsString.ADMIN})
|
||||
public String createTag(@ModelAttribute @Valid TagDto tagDto) {
|
||||
tagService.add(tagDto.getName());
|
||||
|
||||
return "redirect:/tags";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}")
|
||||
@Secured({UserRole.AsString.ADMIN})
|
||||
public String editTag(@PathVariable Long id, @ModelAttribute @Valid TagDto tagDto) {
|
||||
tagService.update(id, tagDto.getName());
|
||||
|
||||
return "redirect:/tags";
|
||||
}
|
||||
}
|
53
src/main/java/np/something/mvc/UserSignUp.java
Normal file
53
src/main/java/np/something/mvc/UserSignUp.java
Normal file
@ -0,0 +1,53 @@
|
||||
package np.something.mvc;
|
||||
|
||||
import np.something.DTO.UserSignupDto;
|
||||
import np.something.model.Customer;
|
||||
import np.something.services.CustomerService;
|
||||
import np.something.util.validation.ValidationException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
|
||||
@Controller
|
||||
@RequestMapping(UserSignUp.SIGNUP_URL)
|
||||
public class UserSignUp {
|
||||
public static final String SIGNUP_URL = "/signup";
|
||||
|
||||
private final CustomerService customerService;
|
||||
|
||||
public UserSignUp(CustomerService customerService) {
|
||||
this.customerService = customerService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String showSignupForm(Model model) {
|
||||
model.addAttribute("userDto", new UserSignupDto());
|
||||
return "signup";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String signup(@ModelAttribute("userDto") @Valid UserSignupDto userSignupDto,
|
||||
BindingResult bindingResult, HttpServletRequest request,
|
||||
Model model) {
|
||||
model.addAttribute("request", request);
|
||||
if (bindingResult.hasErrors()) {
|
||||
model.addAttribute("errors", bindingResult.getAllErrors());
|
||||
return "signup";
|
||||
}
|
||||
try {
|
||||
final Customer customer = customerService.addCustomer(
|
||||
userSignupDto.getUsername(), userSignupDto.getPassword());
|
||||
return "redirect:/login?created=" + customer.getUsername();
|
||||
} catch (ValidationException e) {
|
||||
model.addAttribute("errors", e.getMessage());
|
||||
return "signup";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package np.something.repositories;
|
||||
|
||||
import np.something.model.Album;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AlbumRepository extends JpaRepository<Album, Long> {
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package np.something.repositories;
|
||||
|
||||
import np.something.model.Customer;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface CustomerRepository extends JpaRepository<Customer, Long> {
|
||||
Customer findByUsername(String username);
|
||||
}
|
12
src/main/java/np/something/repositories/PhotoRepository.java
Normal file
12
src/main/java/np/something/repositories/PhotoRepository.java
Normal file
@ -0,0 +1,12 @@
|
||||
package np.something.repositories;
|
||||
|
||||
import np.something.model.Photo;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PhotoRepository extends JpaRepository<Photo, Long> {
|
||||
|
||||
}
|
17
src/main/java/np/something/repositories/TagRepository.java
Normal file
17
src/main/java/np/something/repositories/TagRepository.java
Normal file
@ -0,0 +1,17 @@
|
||||
package np.something.repositories;
|
||||
|
||||
import np.something.model.Photo;
|
||||
import np.something.model.Tag;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TagRepository extends JpaRepository<Tag, Long> {
|
||||
@Query("SELECT t FROM Tag t WHERE LOWER(t.name) LIKE LOWER(CONCAT(:tagPrefix, '%'))")
|
||||
List<Tag> findTagsByTagPrefix(@Param("tagPrefix") String tagPrefix);
|
||||
|
||||
@Query("SELECT p FROM Photo p JOIN p.tags t WHERE (:tagIds) IN t.id GROUP BY p HAVING COUNT(DISTINCT t) >= :tagCount")
|
||||
List<Photo> findPhotosByTags(@Param("tagIds") List<Long> tagIds, @Param("tagCount") long tagCount);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package np.something.security;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
@Configuration
|
||||
public class PasswordEncoderConfiguration {
|
||||
@Bean
|
||||
public PasswordEncoder createPasswordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package np.something.security;
|
||||
|
||||
import np.something.model.UserRole;
|
||||
import np.something.mvc.UserSignUp;
|
||||
import np.something.services.CustomerService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(securedEnabled = true)
|
||||
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
|
||||
private static final String LOGIN_URL = "/login";
|
||||
private final CustomerService customerService;
|
||||
|
||||
public SecurityConfiguration(CustomerService customerService) {
|
||||
this.customerService = customerService;
|
||||
createAdminOnStartup();
|
||||
}
|
||||
|
||||
private void createAdminOnStartup() {
|
||||
final String admin = "admin";
|
||||
if (customerService.findByUsername(admin) == null) {
|
||||
log.info("Admin user successfully created");
|
||||
customerService.addCustomer(admin, admin + admin, UserRole.ADMIN);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http.exceptionHandling().authenticationEntryPoint(delegatingEntryPoint());
|
||||
http.headers().frameOptions().sameOrigin().and()
|
||||
.cors().and()
|
||||
.csrf().disable()
|
||||
.authorizeRequests()
|
||||
.antMatchers(UserSignUp.SIGNUP_URL).permitAll()
|
||||
.antMatchers(HttpMethod.GET, LOGIN_URL).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin().defaultSuccessUrl("/albums", true)
|
||||
.loginPage(LOGIN_URL).permitAll()
|
||||
.and()
|
||||
.logout().permitAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth.userDetailsService(customerService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(WebSecurity web) {
|
||||
web.ignoring()
|
||||
.antMatchers("/css/**")
|
||||
.antMatchers("/js/**")
|
||||
.antMatchers("/templates/**")
|
||||
.antMatchers("/webjars/**")
|
||||
.antMatchers("/swagger-resources/**")
|
||||
.antMatchers("/v3/api-docs/**");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationEntryPoint delegatingEntryPoint() {
|
||||
final LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap();
|
||||
map.put(new AntPathRequestMatcher("/"), new LoginUrlAuthenticationEntryPoint(LOGIN_URL));
|
||||
map.put(new AntPathRequestMatcher("/api/1.0/**"), new Http403ForbiddenEntryPoint());
|
||||
|
||||
final DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(map);
|
||||
entryPoint.setDefaultEntryPoint(new LoginUrlAuthenticationEntryPoint(LOGIN_URL));
|
||||
|
||||
return entryPoint;
|
||||
}
|
||||
}
|
85
src/main/java/np/something/services/AlbumService.java
Normal file
85
src/main/java/np/something/services/AlbumService.java
Normal file
@ -0,0 +1,85 @@
|
||||
package np.something.services;
|
||||
|
||||
import np.something.model.Album;
|
||||
import np.something.model.Customer;
|
||||
import np.something.model.Photo;
|
||||
import np.something.repositories.AlbumRepository;
|
||||
import np.something.repositories.CustomerRepository;
|
||||
import np.something.repositories.PhotoRepository;
|
||||
import np.something.util.validation.ValidatorUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AlbumService {
|
||||
private final AlbumRepository albumRepository;
|
||||
private final CustomerRepository customerRepository;
|
||||
private final PhotoRepository photoRepository;
|
||||
private final ValidatorUtil validatorUtil;
|
||||
|
||||
public AlbumService(AlbumRepository albumRepository, CustomerRepository customerRepository, PhotoRepository photoRepository, ValidatorUtil validatorUtil) {
|
||||
this.albumRepository = albumRepository;
|
||||
this.customerRepository = customerRepository;
|
||||
this.photoRepository = photoRepository;
|
||||
this.validatorUtil = validatorUtil;
|
||||
}
|
||||
|
||||
public Album find(Long id) {
|
||||
return albumRepository.findById(id).get();
|
||||
}
|
||||
|
||||
public List<Album> findAll() {
|
||||
return albumRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Album add(String name, Customer owner) {
|
||||
Album album = new Album(name, owner);
|
||||
validatorUtil.validate(album);
|
||||
return albumRepository.save(album);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Album update(Long id, String name) {
|
||||
Album album = albumRepository.findById(id).get();
|
||||
album.setName(name);
|
||||
validatorUtil.validate(album);
|
||||
albumRepository.save(album);
|
||||
return album;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
albumRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addPhoto(Long id) {
|
||||
Album album = albumRepository.findById(id).get();
|
||||
album.getPhotos().add(photoRepository.save(new Photo(album)));
|
||||
albumRepository.save(album);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void removePhoto(Long photo_id) {
|
||||
photoRepository.deleteById(photo_id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addCustomer(Long album_id, Long customer_id) {
|
||||
Album album = albumRepository.findById(album_id).get();
|
||||
Customer customer = customerRepository.findById(customer_id).get();
|
||||
album.getCustomers().add(customer);
|
||||
albumRepository.save(album);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void removeCustomer(Long album_id, Long customer_id) {
|
||||
Album album = albumRepository.findById(album_id).get();
|
||||
Customer customer = customerRepository.findById(customer_id).get();
|
||||
album.getCustomers().remove(customer);
|
||||
albumRepository.save(album);
|
||||
}
|
||||
}
|
93
src/main/java/np/something/services/CustomerService.java
Normal file
93
src/main/java/np/something/services/CustomerService.java
Normal file
@ -0,0 +1,93 @@
|
||||
package np.something.services;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
|
||||
import np.something.DTO.CustomerDto;
|
||||
import np.something.Exceptions.CustomerNotFoundException;
|
||||
import np.something.model.Album;
|
||||
import np.something.model.Customer;
|
||||
import np.something.model.UserRole;
|
||||
import np.something.repositories.CustomerRepository;
|
||||
import np.something.util.validation.ValidatorUtil;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class CustomerService implements UserDetailsService {
|
||||
private final CustomerRepository customerRepository;
|
||||
private final ValidatorUtil validatorUtil;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public CustomerService(CustomerRepository customerRepository,
|
||||
ValidatorUtil validatorUtil, PasswordEncoder passwordEncoder) {
|
||||
this.customerRepository = customerRepository;
|
||||
this.validatorUtil = validatorUtil;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Customer findCustomer(Long id) {
|
||||
return customerRepository.findById(id).orElseThrow(() -> new CustomerNotFoundException(id));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<Customer> findAllCustomers() {
|
||||
return customerRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Customer addCustomer(String username, String password) {
|
||||
Customer customer = new Customer(username, passwordEncoder.encode(password));
|
||||
validatorUtil.validate(customer);
|
||||
customer.getAlbums().add(new Album("Стандартный", customer));
|
||||
return customerRepository.save(customer);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Customer addCustomer(String username, String password, UserRole role) {
|
||||
Customer customer = new Customer(username, passwordEncoder.encode(password), role);
|
||||
validatorUtil.validate(customer);
|
||||
return customerRepository.save(customer);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Customer updateCustomer(Long id, String username, String password) {
|
||||
Customer customer = findCustomer(id);
|
||||
customer.setUsername(username);
|
||||
customer.setPassword(passwordEncoder.encode(password));
|
||||
validatorUtil.validate(customer);
|
||||
return customerRepository.save(customer);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Customer deleteCustomer(Long id) {
|
||||
Customer customer = findCustomer(id);
|
||||
customerRepository.delete(customer);
|
||||
return customer;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteAllCustomers() {
|
||||
customerRepository.deleteAll();
|
||||
}
|
||||
|
||||
public Customer findByUsername(String username) {
|
||||
return customerRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
final Customer customerEntity = findByUsername(username);
|
||||
if (customerEntity == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
customerEntity.getUsername(), customerEntity.getPassword(), Collections.singleton(customerEntity.getRole()));
|
||||
}
|
||||
}
|
52
src/main/java/np/something/services/PhotoService.java
Normal file
52
src/main/java/np/something/services/PhotoService.java
Normal file
@ -0,0 +1,52 @@
|
||||
package np.something.services;
|
||||
|
||||
import np.something.model.Photo;
|
||||
import np.something.model.Tag;
|
||||
import np.something.repositories.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class PhotoService {
|
||||
|
||||
private final PhotoRepository photoRepository;
|
||||
private final TagRepository tagRepository;
|
||||
|
||||
@Autowired
|
||||
public PhotoService(PhotoRepository photoRepository, TagRepository tagRepository) {
|
||||
this.photoRepository = photoRepository;
|
||||
this.tagRepository = tagRepository;
|
||||
}
|
||||
|
||||
public List<Photo> getAllPhotos() {
|
||||
return photoRepository.findAll();
|
||||
}
|
||||
|
||||
public Photo getPhotoById(Long id) {
|
||||
return photoRepository.findById(id).get();
|
||||
}
|
||||
|
||||
public Photo add() {
|
||||
return photoRepository.save(new Photo());
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
photoRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public void addTag(Long photo_id, Long tag_id) {
|
||||
Photo photo = photoRepository.findById(photo_id).get();
|
||||
Tag tag = tagRepository.findById(tag_id).get();
|
||||
photo.getTags().add(tag);
|
||||
photoRepository.save(photo);
|
||||
}
|
||||
|
||||
public void removeTag(Long photo_id, Long tag_id) {
|
||||
Photo photo = photoRepository.findById(photo_id).get();
|
||||
Tag tag = tagRepository.findById(tag_id).get();
|
||||
photo.getTags().remove(tag);
|
||||
photoRepository.save(photo);
|
||||
}
|
||||
}
|
71
src/main/java/np/something/services/TagService.java
Normal file
71
src/main/java/np/something/services/TagService.java
Normal file
@ -0,0 +1,71 @@
|
||||
package np.something.services;
|
||||
|
||||
import np.something.model.Album;
|
||||
import np.something.model.Customer;
|
||||
import np.something.model.Photo;
|
||||
import np.something.model.Tag;
|
||||
import np.something.repositories.AlbumRepository;
|
||||
import np.something.repositories.CustomerRepository;
|
||||
import np.something.repositories.PhotoRepository;
|
||||
import np.something.repositories.TagRepository;
|
||||
import np.something.util.validation.ValidatorUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Service
|
||||
public class TagService {
|
||||
private final AlbumRepository albumRepository;
|
||||
private final CustomerRepository customerRepository;
|
||||
private final PhotoRepository photoRepository;
|
||||
private final TagRepository tagRepository;
|
||||
private final ValidatorUtil validatorUtil;
|
||||
|
||||
public TagService(AlbumRepository albumRepository, CustomerRepository customerRepository, PhotoRepository photoRepository, TagRepository tagRepository, ValidatorUtil validatorUtil) {
|
||||
this.albumRepository = albumRepository;
|
||||
this.customerRepository = customerRepository;
|
||||
this.photoRepository = photoRepository;
|
||||
this.tagRepository = tagRepository;
|
||||
this.validatorUtil = validatorUtil;
|
||||
}
|
||||
|
||||
public Tag find(Long id) {
|
||||
return tagRepository.findById(id).get();
|
||||
}
|
||||
|
||||
public List<Tag> findAll() {
|
||||
return tagRepository.findAll();
|
||||
}
|
||||
|
||||
public Tag add(String name) {
|
||||
Tag tag = new Tag(name);
|
||||
validatorUtil.validate(tag);
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
public Tag update(Long id, String name) {
|
||||
Tag tag = tagRepository.findById(id).get();
|
||||
tag.setName(name);
|
||||
validatorUtil.validate(tag);
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
tagRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public List<Tag> tagsByTagPrefix(String prefix) {
|
||||
return tagRepository.findTagsByTagPrefix(prefix);
|
||||
}
|
||||
|
||||
public List<Photo> photosByTags(List<Long> ids) {
|
||||
return tagRepository.findPhotosByTags(ids, ids.size());
|
||||
}
|
||||
|
||||
public List<Photo> customersPhotosByTag(Customer customer, Long tag_id) {
|
||||
var albums = Stream.concat(customer.getAlbums().stream(), customer.getOwnedAlbums().stream());
|
||||
List<Photo> photos = albums.flatMap(x -> x.getPhotos().stream()).toList();
|
||||
return photosByTags(List.of(tag_id)).stream().filter(photos::contains).toList();
|
||||
}
|
||||
}
|
39
src/main/java/np/something/util/error/AdviceController.java
Normal file
39
src/main/java/np/something/util/error/AdviceController.java
Normal file
@ -0,0 +1,39 @@
|
||||
package np.something.util.error;
|
||||
|
||||
import np.something.Exceptions.CustomerNotFoundException;
|
||||
import np.something.util.validation.ValidationException;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ControllerAdvice(annotations = RestController.class)
|
||||
public class AdviceController {
|
||||
@ExceptionHandler({
|
||||
CustomerNotFoundException.class,
|
||||
ValidationException.class
|
||||
})
|
||||
public ResponseEntity<Object> handleException(Throwable e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Object> handleBindException(MethodArgumentNotValidException e) {
|
||||
final ValidationException validationException = new ValidationException(
|
||||
e.getBindingResult().getAllErrors().stream()
|
||||
.map(DefaultMessageSourceResolvable::getDefaultMessage)
|
||||
.collect(Collectors.toSet()));
|
||||
return handleException(validationException);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Object> handleUnknownException(Throwable e) {
|
||||
e.printStackTrace();
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package np.something.util.validation;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class ValidationException extends RuntimeException {
|
||||
public ValidationException(Set<String> errors) {
|
||||
super(String.join("\n", errors));
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package np.something.util.validation;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.ValidatorFactory;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class ValidatorUtil {
|
||||
private final Validator validator;
|
||||
|
||||
public ValidatorUtil() {
|
||||
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
|
||||
this.validator = factory.getValidator();
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void validate(T object) {
|
||||
final Set<ConstraintViolation<T>> errors = validator.validate(object);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new ValidationException(errors.stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1,13 @@
|
||||
|
||||
spring.main.banner-mode=off
|
||||
#server.port=8080
|
||||
spring.datasource.url=jdbc:h2:file:./data
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=password
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.h2.console.enabled=true
|
||||
spring.h2.console.settings.trace=false
|
||||
spring.h2.console.settings.web-allow-others=false
|
||||
jwt.dev-token=my-secret-jwt
|
||||
jwt.dev=true
|
||||
|
58
src/main/resources/templates/album.html
Normal file
58
src/main/resources/templates/album.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<form class="col card mb-5" method="POST" th:action="@{/albums/edit/{id}(id=${album.getId()})}">
|
||||
<div class="row mb-3">
|
||||
<label>Название альбома: <input name="name" pattern=".+{3,32}" required></label>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<button type="submit" class="button primary">Изменить</button>
|
||||
</div>
|
||||
</form>
|
||||
<form id="customerform" class="col" th:action="@{/albums/{id}/add/customer/(id=${album.getId()})}" method="POST" th:onsubmit="|document.getElementById('customerform').action += document.getElementById('select').value|">
|
||||
<select class="row mb-3" id="select">
|
||||
<option th:each="c : ${customers}" th:value="${c.getId()}" th:text="${c.getUsername()}"></option>
|
||||
</select>
|
||||
<div class="row mb-3">
|
||||
<button type="submit" class="button primary">Добавить пользователя</button>
|
||||
</div>
|
||||
</form>
|
||||
<form id="customeform" class="col" th:action="@{/albums/{id}/remove/customer/(id=${album.getId()})}" method="POST" th:onsubmit="|document.getElementById('customeform').action += document.getElementById('selectt').value|">
|
||||
<select class="row mb-3" id="selectt">
|
||||
<option th:each="c : ${album.getCustomers()}" th:value="${c.getId()}" th:text="${c.getUsername()}"></option>
|
||||
</select>
|
||||
<div class="row mb-3">
|
||||
<button type="submit" class="button primary">Убрать пользователя</button>
|
||||
</div>
|
||||
</form>
|
||||
<form class="col card mb-5" method="POST" th:action="@{/albums/{id}/add/photo(id=${album.getId()})}">
|
||||
<div class="row mb-3">
|
||||
<button type="submit" class="button primary">Добавить фотографию</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
<div class="row card mb-5" th:each="photo : ${photos}">
|
||||
<div class="col">
|
||||
<div class="row is-center"><img style="max-height: 300px; max-width: 300px" src="https://cdn.theatlantic.com/thumbor/viW9N1IQLbCrJ0HMtPRvXPXShkU=/0x131:2555x1568/976x549/media/img/mt/2017/06/shutterstock_319985324/original.jpg"></div>
|
||||
<div class="row is-center mb-2 h1" th:text="${'Фотография №' + photo.getId()}"></div>
|
||||
<div class="row is-center">
|
||||
<div class="col"><a class="button primary" th:href="@{/photos/{id}(id=${photo.getId()})}">Открыть</a></div>
|
||||
<form class="col" th:action="@{/photos/delete/{id}(id=${photo.getId()})}" method="POST"><button type="submit" class="button primary">Удалить</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
44
src/main/resources/templates/albums.html
Normal file
44
src/main/resources/templates/albums.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<form class="col card mb-5" method="POST">
|
||||
<div class="row mb-3">
|
||||
<label>Название альбома: <input name="name" pattern=".+{3,32}" required></label>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<button type="submit" class="button primary">Создать новый альбом</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
<div class="row card mb-5" th:each="album : ${ownerAlbums}">
|
||||
<div class="col">
|
||||
<div class="row is-center mb-2 h1" th:text="${album.getName()}"></div>
|
||||
<div class="row is-center">
|
||||
<div class="col"><a class="button primary" th:href="@{/albums/{id}(id=${album.getId()})}">Открыть</a></div>
|
||||
<form class="col" th:action="@{/albums/delete/{id}(id=${album.getId()})}" method="POST"><button type="submit" class="button primary">Удалить</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row card mb-5" th:each="album : ${albums}">
|
||||
<div class="col">
|
||||
<div class="row is-center mb-2 h1" th:text="${album.getName()}"></div>
|
||||
<div class="row is-center">
|
||||
<div class="col"><a class="button primary" th:href="@{/albums/{id}(id=${album.getId()})}">Открыть</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
32
src/main/resources/templates/default.html
Normal file
32
src/main/resources/templates/default.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<title>Фотографии</title>
|
||||
</head>
|
||||
<body class="container">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
<div>
|
||||
<div class="nav row" th:with="activeLink=${#request.requestURI}">
|
||||
<div class="nav-left col-10">
|
||||
<a href="/albums" class="button primary" th:classappend="${#strings.startsWith(activeLink, '/customers')} ? 'clear' : ''">Альбомы</a>
|
||||
<a href="/tags" class="button primary" th:classappend="${#strings.startsWith(activeLink, '/customers')} ? 'clear' : ''">Тэги</a>
|
||||
</div>
|
||||
<div class="nav-right col-2">
|
||||
<a sec:authorize="!isAuthenticated()" href="/login" class="button primary" th:classappend="${#strings.startsWith(activeLink, '/login')} ? 'clear' : ''">Войти</a>
|
||||
<a sec:authorize="isAuthenticated()" href="/logout" class="button primary" th:classappend="${#strings.startsWith(activeLink, '/logout')} ? 'clear' : ''">Выйти</a>
|
||||
</div>
|
||||
</div>
|
||||
<div layout:fragment="content">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
13
src/main/resources/templates/error.html
Normal file
13
src/main/resources/templates/error.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{default}">
|
||||
<body>
|
||||
<div class="container" layout:fragment="content">
|
||||
<div class="alert alert-danger">
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
<a href="/albums" class="is-center">На главную</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
31
src/main/resources/templates/login.html
Normal file
31
src/main/resources/templates/login.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<form class="col-6" action="/login" method="post">
|
||||
<div class="row">
|
||||
<label>Логин: <input type="text" name="username" pattern=".+{3, 32}" required /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Пароль: <input type="password" name="password" id="password" pattern=".+{8, 32}" required /></label>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-4"></div>
|
||||
<button type="submit" class="button primary col" id="enter">Войти</button>
|
||||
<div class="col-4"></div>
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<a class="button primary outline" href="/signup">Регистрация</a>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
37
src/main/resources/templates/photo.html
Normal file
37
src/main/resources/templates/photo.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row card">
|
||||
<form id="tagform" class="col" th:action="@{/photos/{id}/add/tag/(id=${photo.getId()})}" method="POST" th:onsubmit="|document.getElementById('tagform').action += document.getElementById('select').value|">
|
||||
<select name="name" class="row" id="select">
|
||||
<option th:each="tag : ${tags}" th:value="${tag.getId()}" th:text="${tag.getName()}"></option>
|
||||
</select>
|
||||
<button class="row" type="submit">Добавить</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row card">
|
||||
<div class="col">
|
||||
<div class="row mb-2 is-center"><img style="max-height: 300px; max-width: 300px" src="https://cdn.theatlantic.com/thumbor/viW9N1IQLbCrJ0HMtPRvXPXShkU=/0x131:2555x1568/976x549/media/img/mt/2017/06/shutterstock_319985324/original.jpg"></div>
|
||||
<div class="row is-center mb-2 h1" th:text="${'Фотография №' + photo.getId()}"></div>
|
||||
<div class="row">
|
||||
<div class="col card" th:each="tag : ${photo.getTags()}">
|
||||
<span th:text="${tag.getName()}"></span>
|
||||
<form method="POST" th:action="@{/photos/{id}/remove/tag/{tag_id}(id=${photo.getId()}, tag_id=${tag.getId()})}">
|
||||
<button type="submit" class="button primary">Удалить тэг</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
44
src/main/resources/templates/signup.html
Normal file
44
src/main/resources/templates/signup.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<form class="col-6" action="/signup" method="post">
|
||||
<div class="row">
|
||||
<label>Логин: <input type="text" name="username" required /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Пароль: <input type="password" name="password" id="password" th:onchange="|check()|" required /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Пароль повторно: <input type="password" id="confirm-password" th:onchange="|check()|" required /></label>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-4"></div>
|
||||
<button type="submit" class="button primary col" id="enter" disabled>Регистрация</button>
|
||||
<div class="col-4"></div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
<script th:inline="javascript">
|
||||
function check() {
|
||||
const password = document.getElementById("password")
|
||||
const confpassword = document.getElementById("confirm-password")
|
||||
const button = document.getElementById("enter")
|
||||
|
||||
if (password.value !== confpassword.value) {
|
||||
button.setAttribute('disabled', 'disabled')
|
||||
} else {
|
||||
button.removeAttribute('disabled')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
36
src/main/resources/templates/tag.html
Normal file
36
src/main/resources/templates/tag.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<form class="col card mb-5" method="POST" th:action="@{/tags/edit/{id}(id=${tag.getId()})}">
|
||||
<div class="row mb-3">
|
||||
<label>Название тэга: <input name="name" pattern=".+{3,32}" required></label>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<button type="submit" class="button primary">Изменить</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
<div class="row card mb-5" th:each="photo : ${photos}">
|
||||
<div class="col">
|
||||
<div class="row is-center"><img style="max-height: 300px; max-width: 300px" src="https://cdn.theatlantic.com/thumbor/viW9N1IQLbCrJ0HMtPRvXPXShkU=/0x131:2555x1568/976x549/media/img/mt/2017/06/shutterstock_319985324/original.jpg"></div>
|
||||
<div class="row is-center mb-2 h1" th:text="${'Фотография №' + photo.getId()}"></div>
|
||||
<div class="row is-center">
|
||||
<div class="col"><a class="button primary" th:href="@{/photos/{id}(id=${photo.getId()})}">Открыть</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
42
src/main/resources/templates/tags.html
Normal file
42
src/main/resources/templates/tags.html
Normal file
@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<form class="col card mb-5" method="POST">
|
||||
<div class="row mb-3">
|
||||
<label>Название тэга: <input name="name" pattern=".+{3,32}" required></label>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<button type="submit" class="button primary">Создать новый тэг</button>
|
||||
</div>
|
||||
</form>
|
||||
<form class="col card mb-5" method="GET">
|
||||
<div class="row mb-3">
|
||||
<label>Поиск по тэгам: <input name="search" pattern=".+{3,32}"></label>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<button type="submit" class="button primary">Искать</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row card mb-5" th:each="tag : ${tags}">
|
||||
<div class="col">
|
||||
<div class="row is-center mb-2 h1" th:text="${tag.getName()}"></div>
|
||||
<div class="row is-center">
|
||||
<div class="col"><a class="button primary" th:href="@{/tags/{id}(id=${tag.getId()})}">Открыть</a></div>
|
||||
<form class="col" th:action="@{/tags/delete/{id}(id=${tag.getId()})}" method="POST"><button type="submit" class="button primary">Удалить</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
@ -1,13 +0,0 @@
|
||||
package np.something;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class SomethingApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
6
src/test/resources/application.properties
Normal file
6
src/test/resources/application.properties
Normal file
@ -0,0 +1,6 @@
|
||||
spring.datasource.url=jdbc:h2:mem:testdb
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=password
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
Loading…
Reference in New Issue
Block a user