Labwork06 React is done?
This commit is contained in:
parent
9d4f79f1ae
commit
af9ea10b78
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -16,6 +16,9 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'com.h2database:h2:2.1.214'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
implementation 'com.auth0:java-jwt:4.4.0'
|
||||
implementation 'junit:junit:4.13.2'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'org.springframework.boot:spring-boot-devtools'
|
||||
|
BIN
data.mv.db
BIN
data.mv.db
Binary file not shown.
@ -13,11 +13,11 @@ export default function App() {
|
||||
<div>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path='/movie/:customerId' element={<MainPage/>} />
|
||||
<Route path='/genre/:customerId' element={<SearchPage/>} />
|
||||
<Route path='/library/:customerId' element={<LibPage/>} />
|
||||
<Route path='/movie/:token' element={<MainPage/>} />
|
||||
<Route path='/genre/:token' element={<SearchPage/>} />
|
||||
<Route path='/library/:token' element={<LibPage/>} />
|
||||
<Route path='*' element={<RegPage/>} />
|
||||
<Route path='/movie/:customerId/:movieId' element={<FilmInfo/>} />
|
||||
<Route path='/movie/:token/:movieId' element={<FilmInfo/>} />
|
||||
<Route path='/login' element={<Loginpage/>} />
|
||||
</Routes>
|
||||
</Router>
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
@ -1,13 +1,14 @@
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
const host = "http://localhost:8080";
|
||||
const host = "http://localhost:8080/api/1.0";
|
||||
|
||||
export async function registerUser(fullName,password)
|
||||
export async function registerUser(fullName,password,role)
|
||||
{
|
||||
console.log(fullName);
|
||||
console.log(password);
|
||||
const response = await axios.post(`${host}/customer?fullName=${fullName}&password=${password}`);
|
||||
console.log(role);
|
||||
const response = await axios.post(`${host}/customer?fullName=${fullName}&password=${password}&role=${role}`);
|
||||
|
||||
if (response.status === 200) {
|
||||
const customerDTO = response.data;
|
||||
@ -16,6 +17,17 @@ export async function registerUser(fullName,password)
|
||||
|
||||
}
|
||||
|
||||
export async function loginUser(username,password)
|
||||
{
|
||||
|
||||
const response = await axios.post(`${host}/customer/login`,JSON.stringify({"username": username,"password": password}));
|
||||
|
||||
if (response.status === 200) {
|
||||
const token = response.data;
|
||||
console.log(token);
|
||||
return token;}
|
||||
}
|
||||
|
||||
export async function getMovies()
|
||||
{
|
||||
const response = await axios.get(`${host}/movie`);
|
||||
@ -33,17 +45,17 @@ export async function getMovie(movieId)
|
||||
const movieDTO = response.data;
|
||||
return movieDTO;}
|
||||
}
|
||||
export async function getCustomerMovies(customerId)
|
||||
export async function getCustomerMovies(token)
|
||||
{
|
||||
const response = await axios.get(`${host}/customer/movies/${customerId}`);
|
||||
const response = await axios.get(`${host}/customer/movies?token=${token}`);
|
||||
if (response.status === 200) {
|
||||
const customerMovies = response.data;
|
||||
return customerMovies;}
|
||||
}
|
||||
|
||||
export async function acquireMovie(movieId,customerId)
|
||||
export async function acquireMovie(movieId,token)
|
||||
{
|
||||
const response = await axios.post(`${host}/movie/${customerId}/${movieId}`);
|
||||
const response = await axios.post(`${host}/movie/customer/movies/add/${movieId}?token=${token}`);
|
||||
if (response.status === 200) {
|
||||
const movieDTO= response.data;
|
||||
return movieDTO;
|
||||
@ -101,7 +113,12 @@ export async function updateMovie(movieId, modalData)
|
||||
const response = await axios.put(`${host}/movie/${movieId}?title=${modalData["title"]}&length=${modalData["length"]}&score=${modalData["score"]}`);
|
||||
}
|
||||
|
||||
export async function removeCustomerMovie(movieId,customerId)
|
||||
export async function deleteMovie(movieId,token)
|
||||
{
|
||||
const response = await axios.delete(`${host}/movie/${customerId}/${movieId}`);
|
||||
const response = await axios.delete(`${host}/movie/${movieId}?token=${token}`);
|
||||
}
|
||||
|
||||
export async function removeCustomerMovie(movieId,token)
|
||||
{
|
||||
const response = await axios.delete(`${host}/movie/customer/movies/delete/${movieId}?token=${token}`);
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<footer class="flex-item6">
|
||||
<div class="text-center text-light p-2" style="font-size: 30px; font-weight: bold;">
|
||||
@2022 Copyright: BLSJY.com
|
||||
</div>
|
||||
</footer>
|
10
frontend/src/Components/Footer.js
Normal file
10
frontend/src/Components/Footer.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="flex-item6">
|
||||
<div className="text-center text-light p-2" style={{fontSize: "30px", fontWeight: "bold"}}>@2022 Copyright: BLSJY.com</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
export default Footer;
|
@ -1,24 +0,0 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" th:href="@{/movie/{customerId}(customerId=${customerId})}">
|
||||
Main Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" th:href="@{/genre/{customerId}(customerId=${customerId})}">
|
||||
Search Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" th:href="@{/library/{customerId}(customerId=${customerId})}">
|
||||
Library Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" th:href="@{/customer}">
|
||||
Registration Page
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
28
frontend/src/Components/Navbar.js
Normal file
28
frontend/src/Components/Navbar.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function Navbar({ customerId }) {
|
||||
return (
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link"><Link style={{color: "white", textDecoration: "none"}} to={`/movie/${customerId}`}>Main Page</Link></a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link"><Link style={{color: "white", textDecoration: "none"}} to={`/genre/${customerId}`}>Search Page</Link></a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link"><Link style={{color: "white", textDecoration: "none"}} to={`/library/${customerId}`}>Library Page</Link></a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link"><Link style={{color: "white", textDecoration: "none"}} to="/customer">Registration Page</Link></a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navbar;
|
@ -12,17 +12,19 @@ function Librarypage()
|
||||
const { customerId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const token = params.get('token');
|
||||
fetchMovies();
|
||||
}, []);
|
||||
|
||||
const fetchMovies = async () => {
|
||||
const moviesData = await getCustomerMovies(customerId);
|
||||
const moviesData = await getCustomerMovies(token);
|
||||
console.log(moviesData);
|
||||
setMovies(moviesData);
|
||||
};
|
||||
|
||||
const handleDeleteMovie = async (movieId) => {
|
||||
await removeCustomerMovie(movieId,customerId);
|
||||
await removeCustomerMovie(movieId,token);
|
||||
fetchMovies();
|
||||
}
|
||||
return(
|
||||
|
@ -1,54 +1,41 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import gradientImg from '../Assets/background.png';
|
||||
import { getCustomers } from '../Components/DataService';
|
||||
import { fillRepos } from '../Components/DataService';
|
||||
import {loginUser} from '../Components/DataService';
|
||||
function Loginpage() {
|
||||
|
||||
const [users, setUsers] = useState([]);
|
||||
const [selectedUser, setSelectedUser] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const fetchUsers = async () => {
|
||||
fillRepos();
|
||||
const usersData = await getCustomers();
|
||||
setUsers(usersData);
|
||||
|
||||
}
|
||||
|
||||
const handleUserChange = (event) => {
|
||||
setSelectedUser(event.target.value);
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
|
||||
if (selectedUser || users.length === 1) {
|
||||
const userId = selectedUser || users[0].id;
|
||||
|
||||
const handleLogin = () => {
|
||||
|
||||
const userId = loginUser(document.getElementById("username").value,document.getElementById("password").value)
|
||||
fillRepos();
|
||||
navigate(`/movie/${userId}`);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
|
||||
<div className="flex-container" style={{flexDirection:"column",display: "flex",backgroundImage: `url(${gradientImg})`}}>
|
||||
<div className="flex-container min-vh-100" style={{flexDirection: "column", display: "flex", alignItems: "center", justifyContent: "center", gap: "20px"}}>
|
||||
<h1 htmlFor="userSelect" style={{color: "white"}}>Select User:</h1>
|
||||
<select id="userSelect" value={selectedUser} onChange={handleUserChange} style={{width: "100px"}}>
|
||||
{
|
||||
users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{`${user.username}`}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
<button style={{marginTop: "50px"}}onClick={handleLogin}>Login</button>
|
||||
<form onSubmit={handleLogin}>
|
||||
<div className="mb-3">
|
||||
<input type="text" name="username" id="username" className="form-control"
|
||||
placeholder="Login" required autoFocus />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<input type="password" name="password" id="password" className="form-control"
|
||||
placeholder="Password" required />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-success button-fixed">Login</button>
|
||||
<a className="btn btn-primary button-fixed" href="/">Registration</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@ import gradientImg from '../Assets/background.png';
|
||||
import Footer from '../Components/Footer';
|
||||
import { getMovies } from '../Components/DataService';
|
||||
import { acquireMovie } from '../Components/DataService';
|
||||
import { deleteMovie } from '../Components/DataService';
|
||||
import { useParams} from 'react-router-dom';
|
||||
import Navbar from '../Components/Navbar';
|
||||
import Film from '../Components/Film';
|
||||
@ -11,6 +12,8 @@ function MainPage() {
|
||||
const { customerId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const token = params.get('token');
|
||||
fetchMovies();
|
||||
}, []);
|
||||
|
||||
@ -21,13 +24,17 @@ function MainPage() {
|
||||
};
|
||||
|
||||
const handleAcquireMovie = async (movieId) => {
|
||||
await acquireMovie(movieId,customerId);
|
||||
await acquireMovie(movieId,token);
|
||||
const updatedMovies = movies.map((movie) => {
|
||||
return movie;
|
||||
});
|
||||
|
||||
setMovies(updatedMovies);
|
||||
};
|
||||
|
||||
const handleDeleteMovie = async (movieId) => {
|
||||
await deleteMovie(movieId,token);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
|
@ -9,7 +9,8 @@ function Regpage()
|
||||
|
||||
const [inputUsername, setUsername] = useState('');
|
||||
const [inputPassword,setPassword] = useState('');
|
||||
|
||||
const [inputRole,setRole] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
@ -19,16 +20,23 @@ function Regpage()
|
||||
}
|
||||
if(id === "inputPassword"){
|
||||
setPassword(value);
|
||||
}
|
||||
}
|
||||
if(id === "inputRole"){
|
||||
setRole(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const customerDTO = await registerUser(inputUsername, inputPassword);
|
||||
if(inputRole === '')
|
||||
{
|
||||
setRole("USER");
|
||||
}
|
||||
const customerDTO = await registerUser(inputUsername, inputPassword,inputRole);
|
||||
setUsername ('');
|
||||
setPassword ('');
|
||||
setRole ('');
|
||||
navigate(`/login`);
|
||||
|
||||
|
||||
@ -48,14 +56,21 @@ function Regpage()
|
||||
<label style={{color: "#320D3E",fontSize: "32px",fontWeight: "bold"}}>USERNAME</label>
|
||||
</section>
|
||||
<section className="flex-itemR3" style={{display: "flex", width: "320px", paddingLeft: "30px"}}>
|
||||
<input className="form-control" id="inputUsername" type="string" value={inputUsername} onChange = {(e) => handleInputChange(e)} placeholder="Enter username"/>
|
||||
<input className="form-control" id="inputUsername" type="string" value={inputUsername} onChange = {(e) => handleInputChange(e)} placeholder="Enter username" required autoFocus maxLength={64}/>
|
||||
</section>
|
||||
<section className="flex-itemR6" style={{color: "#320D3E",fontSize: "35px",fontWeight: "bold", paddingLeft: "30px",paddingTop: "10px"}}>
|
||||
<label style={{color: "#320D3E",fontSize: "32px",fontWeight: "bold"}}>PASSWORD</label>
|
||||
</section>
|
||||
<section className="flex-itemR7" style={{display: "flex", width: "320px", paddingLeft: "30px"}}>
|
||||
<input className="form-control" id="inputPassword" type="password" value={inputPassword} onChange = {(e) => handleInputChange(e)} placeholder="Enter password"/>
|
||||
</section>
|
||||
<input className="form-control" id="inputPassword" type="password" value={inputPassword} onChange = {(e) => handleInputChange(e)} placeholder="Enter password" required minlength={6} maxlength={64}/>
|
||||
</section>
|
||||
<section class="flex-itemR8" style={{display: "flex", width: "320px", paddingLeft: "30px"}}>
|
||||
<select id="inputRole" required onChange = {(e) => handleInputChange(e)}>
|
||||
<option value="nothing"></option>
|
||||
<option value="USER">USER</option>
|
||||
<option value="ADMIN">ADMIN</option>
|
||||
</select>
|
||||
</section>
|
||||
<button className="btn btn-primary" type="submit" id="register" style={{fontSize: "20px", marginLeft: "30px", marginTop: "15px", width: "150px", backgroundColor: "#320D3E", color:"white", fontWeight: "bold"}} onClick={(e)=>handleSubmit(e)} >Register</button>
|
||||
<a href="/login">Sign in</a>
|
||||
</form>
|
||||
|
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div><span th:text="${error}"></span></div>
|
||||
<a href="/">Return to registration</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,85 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="flex-container" style="flex-direction: column; display: flex; background-image: url('background.png');">
|
||||
<div class="flex-container min-vh-100" style="flex-direction: column; display: flex;">
|
||||
<main style="display: flex; flex: 1; gap: 100px;">
|
||||
<div class="flex-container" style="display: inline-flex; flex: 1; flex-wrap: wrap; gap: 40px;">
|
||||
<div class="flex-item1 align-self-center" style="flex: 1;">
|
||||
<img src="https://www.seekpng.com/png/detail/8-84931_question-mark-question-mark-white-background.png" width="400px" height="600px"/>
|
||||
</div>
|
||||
<div class="flex-container align-self-center" style="flex: 3;">
|
||||
<div class="flex-item1 text-light" style="font-size: 50px;"><a th:text="${movie.title}"></a></div>
|
||||
<div class="flex-item3 text-light" style="font-size: 35px;">Average score: <span th:text="${movie.score}"></span></div>
|
||||
<div class="flex-item3 text-light" style="font-size: 35px;">Length: <span th:text="${movie.length}"></span></div>
|
||||
<div class="flex-item3 text-light" style="font-size: 35px;">Genre: <span th:text="${movie.genreName}"></span></div>
|
||||
<button class="primary" onclick="handleModalOpen()">Edit Movie</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div id="modal" class="modal" th:classappend="${showModal} ? 'show' : ''">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="handleModalClose()">×</span>
|
||||
<h2>Edit Movie</h2>
|
||||
<div class="form-group">
|
||||
<label>Title:</label>
|
||||
<input type="text" class="form-control" name="title" th:value="${modalData.title}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Score:</label>
|
||||
<input type="text" class="form-control" name="score" th:value="${modalData.score}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Length:</label>
|
||||
<input type="text" class="form-control" name="length" th:value="${modalData.length}"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="handleModalClose()">Close</button>
|
||||
<button class="btn btn-primary" onclick="handleModalSubmit()">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
<script>
|
||||
|
||||
function handleModalOpen() {
|
||||
var modal = document.getElementById("modal");
|
||||
modal.classList.add("show");
|
||||
}
|
||||
|
||||
function handleModalClose() {
|
||||
var modal = document.getElementById("modal");
|
||||
modal.classList.remove("show");
|
||||
|
||||
titleInput.value = "";
|
||||
scoreInput.value = "";
|
||||
lengthInput.value = "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleModalSubmit() {
|
||||
var titleInput = document.getElementsByName("title")[0];
|
||||
var scoreInput = document.getElementsByName("score")[0];
|
||||
var lengthInput = document.getElementsByName("length")[0];
|
||||
|
||||
var modalData = {
|
||||
title: titleInput.value,
|
||||
score: scoreInput.value,
|
||||
length: lengthInput.value
|
||||
};
|
||||
|
||||
const movieId = "${param.movieId}";
|
||||
modal.classList.remove("show");
|
||||
window.location.href = `/movies/movie/update/${movieId}?title=${modalData["title"]}&length=${modalData["length"]}&score=${modalData["score"]}`
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
|
||||
|
||||
<div class="flex-container min-vh-100" style="display: flex; margin-left: 20px; margin-top: 50px">
|
||||
|
||||
<th:block th:if="${movies.size() > 0}">
|
||||
<div style="flex-direction: row; display: flex; flex-wrap: wrap; gap: 30px">
|
||||
<th:block th:each="movie : ${movies}">
|
||||
<div>
|
||||
<h3 th:text="${movie.title}"></h3>
|
||||
<form th:action="@{/movies/{customerId}/delete/{movieId}(customerId=${customerId}, movieId=${movie.id})}" method="post">
|
||||
<button type="submit">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</th:block>
|
||||
<h1 th:unless="${movies.size() > 0}" style="color: white">No movies available</h1>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
<script>
|
||||
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,32 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="flex-container" style="flex-direction: column; display: flex; ">
|
||||
<div class="flex-container min-vh-100" style="flex-direction: column; display: flex; align-items: center; justify-content: center; gap: 20px;">
|
||||
<h1 for="userSelect" style="color: white;">Select User:</h1>
|
||||
<select id="userSelect" onchange="handleUserChange(event)" style="width: 100px;">
|
||||
<option th:each="customer : ${customers}" th:value="${customer.id}" th:text="${customer.username}"></option>
|
||||
</select>
|
||||
<button style="margin-top: 50px;" onclick="handleLogin()">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
<script>
|
||||
|
||||
function handleUserChange(event) {
|
||||
var selectedUserId = event.target.value;
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
var selectedUserId = document.getElementById("userSelect").value;
|
||||
document.cookie = "userID=" + selectedUserId;
|
||||
window.location.href = "/genre/fill";
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,38 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
|
||||
|
||||
<div class="flex-container min-vh-100" style="flex-direction: row; display: flex; margin-left: 20px; margin-top: 50px;">
|
||||
<div th:if="${movies.length > 0}" style="flex-direction: row; display: flex; flex-wrap: wrap; justify-content: space-between;">
|
||||
<div th:each="movie : ${movies}" style="margin-bottom: 20px;">
|
||||
<div>
|
||||
<h3><a th:href="@{/movies/movie/{movieId}, movieId=${movie.id}}"
|
||||
th:text="${movie.title}"></a></h3>
|
||||
<button type="button" th:attr="data-movie-id=${movie.id}" onclick="handleAcquireMovie(event)">Acquire</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 th:unless="${movies.length > 0}" style="color: white;">No movies available</h1>
|
||||
</div>
|
||||
|
||||
<div th:replace="fragments/footer :: footer"></div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
<script th:inline="javascript">
|
||||
|
||||
function handleAcquireMovie(movieId) {
|
||||
var movieId = event.target.getAttribute("data-movie-id");
|
||||
var customerID = /*[[${userId}]]*/ null;
|
||||
var url = "/movies/" + customerId + "/" + movieId;
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="flex-container" style="flex-direction: column; display: flex;">
|
||||
<div class="flex-container min-vh-100" style="flex-direction: column; display: flex;">
|
||||
<main style="display: flex; flex: 1;">
|
||||
<aside class="flex-item2" style="flex: 2;"></aside>
|
||||
<div class="flex-item3 align-self-center" style="flex: 3;"><img src="cover.png" alt="cover" width="100%" height="600px"/></div>
|
||||
<section class="flex-container align-self-center" style="height: 660px; flex: 4; background-color: #ffd79d; flex-direction: column; display: flex;">
|
||||
<form action="#" th:action="@{/customer}" method="post">
|
||||
<section class="flex-itemR1" style="color: #320D3E; font-size: 50px; font-weight: bold; padding-left: 30px; padding-top: 10px;">SIGN UP</section>
|
||||
<section class="flex-itemR2" style="color: #320D3E; font-size: 35px; font-weight: bold; padding-left: 30px; padding-top: 10px;">
|
||||
<label style="color: #320D3E; font-size: 32px; font-weight: bold;">USERNAME</label>
|
||||
</section>
|
||||
<section class="flex-itemR3" style="display: flex; width: 320px; padding-left: 30px;">
|
||||
<input class="form-control" id="inputUsername" type="string" name="fullName" placeholder="Enter username" th:value="${inputUsername}"/>
|
||||
</section>
|
||||
<section class="flex-itemR6" style="color: #320D3E; font-size: 35px; font-weight: bold; padding-left: 30px; padding-top: 10px;">
|
||||
<label style="color: #320D3E; font-size: 32px; font-weight: bold;">PASSWORD</label>
|
||||
</section>
|
||||
<section class="flex-itemR7" style="display: flex; width: 320px; padding-left: 30px;">
|
||||
<input class="form-control" id="inputPassword" type="password" name="password" placeholder="Enter password" th:value="${inputPassword}"/>
|
||||
</section>
|
||||
<button class="btn btn-primary" type="submit" id="register" style="font-size: 20px; margin-left: 30px; margin-top: 15px; width: 150px; background-color: #320D3E; color: white; font-weight: bold;">Register</button>
|
||||
<a href="/login">Sign in</a>
|
||||
</form>
|
||||
</section>
|
||||
<aside class="flex-item5" style="flex: 2;"></aside>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,70 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="flex-container" style="flex-direction: column; display: flex;">
|
||||
<div class="flex-container min-vh-100" style="flex-direction: column; display: flex; flex: 1; align-items: center; justify-content: flex-start; margin-top: 20px">
|
||||
<div>
|
||||
<select id="genre-select">
|
||||
<option value="all">All Genres</option>
|
||||
<option th:each="genre : ${genres}" th:value="${genre.name}" th:text="${genre.name}"></option>
|
||||
</select>
|
||||
<button type="button" onclick="handleSearch()">Search</button>
|
||||
</div>
|
||||
<div class="flex-container min-vh-100" style="flex-direction: row; display: flex; margin-left: 20px; margin-top: 50px">
|
||||
<div style="flex-direction: row; display: flex; flex-wrap: wrap; gap: 30px">
|
||||
<div th:if="${movies.length > 0}" th:each="movie : ${movies}" style="margin-bottom: 20px;">
|
||||
<div>
|
||||
<h3><a th:href="@{/movies/movie/{movieId}, movieId=${movie.id}}"
|
||||
th:text="${movie.title}"></a></h3>
|
||||
</div>
|
||||
</div>
|
||||
<h1 th:if="${movies.length == 0}" style="color: white">No movies found</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<th:block layout:fragment="scripts">
|
||||
<script>
|
||||
function handleSearch() {
|
||||
const genreSelect = document.getElementById("genre-select");
|
||||
const selectedGenre = genreSelect.value;
|
||||
|
||||
fetch(`/movies/${selectedGenre}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Update the movies array in the Thymeleaf model
|
||||
const moviesContainer = document.querySelector(".flex-container.min-vh-100 > div:last-child");
|
||||
moviesContainer.innerHTML = ""; // Clear previous movie elements
|
||||
|
||||
if (data.length > 0) {
|
||||
data.forEach(movie => {
|
||||
const movieElement = document.createElement("div");
|
||||
movieElement.innerHTML = `
|
||||
<div>
|
||||
<h3><a th:href="@{/movies/movie/{movieId}, movieId=${movie.id}}"
|
||||
th:text="${movie.title}"></a></h3>
|
||||
</div>
|
||||
`;
|
||||
moviesContainer.appendChild(movieElement);
|
||||
});
|
||||
} else {
|
||||
const noMoviesElement = document.createElement("h1");
|
||||
noMoviesElement.style.color = "white";
|
||||
noMoviesElement.textContent = "No movies found";
|
||||
moviesContainer.appendChild(noMoviesElement);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching movies:", error);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
|
||||
<div style="background-image: url(background.png);">
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/movies" th:classappend="${#strings.equals(activeLink, '/movies')} ? 'active' : ''">
|
||||
Main Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/genre" th:classappend="${#strings.equals(activeLink, '/genre')} ? 'active' : ''">
|
||||
Search Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" th:href="@{/customer/movies/{customerId}(customerId=${param.customerId})}">
|
||||
Library Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/" th:classappend="${#strings.equals(activeLink, '/')} ? 'active' : ''">
|
||||
Registration Page
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div layout:fragment="content">
|
||||
|
||||
</div>
|
||||
<footer class="flex-item6">
|
||||
<div class="text-center text-light p-2" style="font-size: 30px; font-weight: bold;">
|
||||
@2022 Copyright: BLSJY.com
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
@ -1,16 +1,18 @@
|
||||
package ru.ulstu.is.sbapp.Customer.Controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.CustomerRole;
|
||||
import ru.ulstu.is.sbapp.Customer.Service.CustomerService;
|
||||
import ru.ulstu.is.sbapp.Movie.Controller.MovieDTO;
|
||||
import ru.ulstu.is.sbapp.WebConfiguration;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(WebConfiguration.REST_API + "/customer")
|
||||
@ControllerAdvice(annotations = RestController.class)
|
||||
public class CustomerController {
|
||||
public static final String URL_LOGIN = "/login";
|
||||
private final CustomerService customerService;
|
||||
|
||||
public CustomerController(CustomerService customerService)
|
||||
@ -18,6 +20,12 @@ public class CustomerController {
|
||||
this.customerService = customerService;
|
||||
}
|
||||
|
||||
|
||||
@PostMapping(URL_LOGIN)
|
||||
public String login(@RequestBody @Valid CustomerDTO customerDTO) {
|
||||
return customerService.loginAndGetToken(customerDTO);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public CustomerDTO getCustomer(@PathVariable Long id) {
|
||||
return new CustomerDTO(customerService.findCustomer(id));
|
||||
@ -29,8 +37,8 @@ public class CustomerController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public CustomerDTO createCustomer(@RequestParam("fullName") String fullName, @RequestParam("password") String password ) {
|
||||
return new CustomerDTO(customerService.addCustomer(fullName,password));
|
||||
public CustomerDTO createCustomer(@RequestParam("fullName") String fullName, @RequestParam("password") String password, @RequestParam("role") String role) {
|
||||
return new CustomerDTO(customerService.createUser(fullName,password,CustomerRole.valueOf(role)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@ -43,9 +51,12 @@ public class CustomerController {
|
||||
return new CustomerDTO(customerService.deleteCustomer(id));
|
||||
}
|
||||
|
||||
@GetMapping("/movies/{customerId}")
|
||||
public List<MovieDTO> getCustomerMovies(@PathVariable("customerId") Long customerId) {
|
||||
return customerService.findCustomerMovies(customerId).stream()
|
||||
@GetMapping("/movies")
|
||||
public List<MovieDTO> getCustomerMovies(@RequestParam("token") String token) {
|
||||
|
||||
String username = customerService.loadUserByToken(token).getUsername();
|
||||
|
||||
return customerService.findCustomerMovies(customerService.findByLogin(username).getId()).stream()
|
||||
.map(MovieDTO::new)
|
||||
.toList();
|
||||
}
|
||||
|
@ -2,19 +2,26 @@ package ru.ulstu.is.sbapp.Customer.Controller;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.Customer;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.CustomerRole;
|
||||
import ru.ulstu.is.sbapp.Movie.Controller.MovieDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CustomerDTO {
|
||||
private final long id;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final List<MovieDTO> movies;
|
||||
public long id;
|
||||
public String username;
|
||||
public String password;
|
||||
public CustomerRole role;
|
||||
public List<MovieDTO> movies;
|
||||
|
||||
public CustomerDTO() {
|
||||
|
||||
}
|
||||
public CustomerDTO(Customer customer) {
|
||||
this.id = customer.getId();
|
||||
this.username = customer.getUsername();
|
||||
this.password = customer.getPassword();
|
||||
this.role = customer.getRole();
|
||||
this.movies = customer.getMovies().stream().map(MovieDTO::new).toList();
|
||||
}
|
||||
|
||||
@ -31,6 +38,10 @@ public class CustomerDTO {
|
||||
return password;
|
||||
}
|
||||
|
||||
public CustomerRole getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public List<MovieDTO> getMovies() {
|
||||
return movies;
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package ru.ulstu.is.sbapp.Customer.Exception;
|
||||
|
||||
public class CustomerExistsException extends RuntimeException{
|
||||
public CustomerExistsException(String login) {
|
||||
super(String.format("Customer '%s' already exists", login));
|
||||
}
|
||||
}
|
@ -4,4 +4,7 @@ public class CustomerNotFoundException extends RuntimeException{
|
||||
public CustomerNotFoundException(Long id){
|
||||
super(String.format("Customer with id [%s] is not found", id));
|
||||
}
|
||||
public CustomerNotFoundException(String username) {
|
||||
super(String.format("Customer is not found '%s'", username));
|
||||
}
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
package ru.ulstu.is.sbapp.Customer.MVC;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.sbapp.Customer.Controller.CustomerDTO;
|
||||
import ru.ulstu.is.sbapp.Customer.Service.CustomerService;
|
||||
import ru.ulstu.is.sbapp.Movie.Controller.MovieDTO;
|
||||
import ru.ulstu.is.sbapp.Utilities.CookiesManagement;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/customer")
|
||||
public class CustomerMVC {
|
||||
private final CustomerService customerService;
|
||||
private final CookiesManagement cookiesManagement;
|
||||
|
||||
public CustomerMVC(CustomerService customerService)
|
||||
{
|
||||
this.customerService = customerService;
|
||||
this.cookiesManagement = new CookiesManagement();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String getCustomer(@PathVariable Long id, Model model) {
|
||||
model.addAttribute("customer",new CustomerDTO(customerService.findCustomer(id)));
|
||||
return "customer-details";
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String getCustomers(Model model) {
|
||||
model.addAttribute("customers", customerService.findAllCustomers().stream().map(CustomerDTO::new).toList());
|
||||
return "Login";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createCustomer(@RequestParam("fullName") String fullName, @RequestParam("password") String password ) {
|
||||
customerService.addCustomer(fullName,password);
|
||||
return "redirect:/customer";
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public String updateCustomer(@PathVariable Long id, @RequestParam("fullName") String fullName) {
|
||||
return "redirect:/customer/" + new CustomerDTO(customerService.updateCustomer(id,fullName)).getID();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public String deleteCustomer(@PathVariable Long id) {
|
||||
customerService.deleteCustomer(id);
|
||||
return "redirect:/customer";
|
||||
}
|
||||
|
||||
@GetMapping("/movies")
|
||||
public String getCustomerMovies(HttpServletRequest request, Model model) {
|
||||
|
||||
Long userId = Long.parseLong(cookiesManagement.GetUserID(request));
|
||||
model.addAttribute("movies", customerService.findCustomerMovies(userId).stream()
|
||||
.map(MovieDTO::new)
|
||||
.toList());
|
||||
model.addAttribute("customerId",userId);
|
||||
return "Librarypage";
|
||||
}
|
||||
|
||||
}
|
@ -8,6 +8,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
@Entity
|
||||
public class Customer
|
||||
@ -15,12 +16,16 @@ public class Customer
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
@Column
|
||||
@Column(nullable = false, unique = true, length = 64)
|
||||
@NotBlank(message = "Username can't be null or empty")
|
||||
@Size(min = 3, max = 64)
|
||||
private String username;
|
||||
@Column
|
||||
@Column(nullable = false, length = 64)
|
||||
@NotBlank(message = "Password can't be empty")
|
||||
@Size(min = 6, max = 64)
|
||||
private String password;
|
||||
|
||||
private CustomerRole role;
|
||||
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
|
||||
private List<Movie> movies;
|
||||
|
||||
@ -29,14 +34,15 @@ public class Customer
|
||||
{
|
||||
|
||||
}
|
||||
public Customer(String username,String password)
|
||||
public Customer(String username,String password,CustomerRole role)
|
||||
{
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
|
||||
this.role = role;
|
||||
this.movies = new ArrayList<>();
|
||||
}
|
||||
|
||||
|
||||
public Long getId()
|
||||
{
|
||||
return id;
|
||||
@ -48,6 +54,10 @@ public class Customer
|
||||
return username;
|
||||
}
|
||||
|
||||
public CustomerRole getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setUsername(String username)
|
||||
{
|
||||
this.username = username;
|
||||
|
@ -0,0 +1,20 @@
|
||||
package ru.ulstu.is.sbapp.Customer.Model;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
public enum CustomerRole 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";
|
||||
}
|
||||
}
|
@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.Customer;
|
||||
|
||||
public interface CustomerRepository extends JpaRepository<Customer, Long> {
|
||||
Customer findOneByUsernameIgnoreCase(String login);
|
||||
}
|
||||
|
@ -1,45 +1,88 @@
|
||||
package ru.ulstu.is.sbapp.Customer.Service;
|
||||
|
||||
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 org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import ru.ulstu.is.sbapp.Customer.Controller.CustomerDTO;
|
||||
import ru.ulstu.is.sbapp.Customer.Exception.CustomerExistsException;
|
||||
import ru.ulstu.is.sbapp.Customer.Exception.CustomerNotFoundException;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.Customer;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.CustomerRole;
|
||||
import ru.ulstu.is.sbapp.Customer.Repository.CustomerRepository;
|
||||
import ru.ulstu.is.sbapp.Movie.Model.Movie;
|
||||
import ru.ulstu.is.sbapp.Utilities.validation.ValidationException;
|
||||
import ru.ulstu.is.sbapp.Utilities.validation.ValidatorUtil;
|
||||
import javax.persistence.EntityNotFoundException;
|
||||
import ru.ulstu.is.sbapp.jwt.JwtException;
|
||||
import ru.ulstu.is.sbapp.jwt.JwtProvider;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class CustomerService
|
||||
public class CustomerService implements UserDetailsService
|
||||
{
|
||||
private final CustomerRepository customerRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final ValidatorUtil validatorUtil;
|
||||
private final JwtProvider jwtProvider;
|
||||
|
||||
public CustomerService(CustomerRepository customerRepository, ValidatorUtil validatorUtil) {
|
||||
public CustomerService(CustomerRepository customerRepository, PasswordEncoder passwordEncoder,
|
||||
ValidatorUtil validatorUtil,
|
||||
JwtProvider jwtProvider) {
|
||||
this.customerRepository = customerRepository;
|
||||
this.validatorUtil = validatorUtil;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtProvider = jwtProvider;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Customer addCustomer(String fullName,String password)
|
||||
{
|
||||
if(!StringUtils.hasText(fullName))
|
||||
{
|
||||
throw new IllegalArgumentException("Customer's name or surname is missing");
|
||||
}
|
||||
|
||||
if(!StringUtils.hasText(password))
|
||||
{
|
||||
throw new IllegalArgumentException("Customer's name or surname is missing");
|
||||
}
|
||||
public Customer findByLogin(String login) {
|
||||
return customerRepository.findOneByUsernameIgnoreCase(login);
|
||||
}
|
||||
|
||||
final Customer customer = new Customer(fullName,password);
|
||||
validatorUtil.validate(customer);
|
||||
return customerRepository.save(customer);
|
||||
public Customer createUser(String login, String password, CustomerRole role) {
|
||||
if (findByLogin(login) != null) {
|
||||
throw new CustomerExistsException(login);
|
||||
}
|
||||
final Customer user = new Customer(login, passwordEncoder.encode(password), role);
|
||||
validatorUtil.validate(user);
|
||||
return customerRepository.save(user);
|
||||
}
|
||||
|
||||
public String loginAndGetToken(CustomerDTO customerDTO) {
|
||||
final Customer customer = findByLogin(customerDTO.getUsername());
|
||||
if (customer == null) {
|
||||
throw new CustomerNotFoundException(customerDTO.getUsername());
|
||||
}
|
||||
if (!passwordEncoder.matches(customerDTO.getPassword(), customer.getPassword())) {
|
||||
throw new CustomerNotFoundException(customer.getUsername());
|
||||
}
|
||||
return jwtProvider.generateToken(customer.getUsername());
|
||||
}
|
||||
|
||||
public UserDetails loadUserByToken(String token) throws UsernameNotFoundException {
|
||||
if (!jwtProvider.isTokenValid(token)) {
|
||||
throw new JwtException("Bad token");
|
||||
}
|
||||
final String userLogin = jwtProvider.getLoginFromToken(token)
|
||||
.orElseThrow(() -> new JwtException("Token is not contain Login"));
|
||||
return loadUserByUsername(userLogin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
final Customer userEntity = findByLogin(username);
|
||||
if (userEntity == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
userEntity.getUsername(), userEntity.getPassword(), Collections.singleton(userEntity.getRole()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
@ -1,56 +0,0 @@
|
||||
package ru.ulstu.is.sbapp.Genre.MVC;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.sbapp.Genre.Controller.GenreDTO;
|
||||
import ru.ulstu.is.sbapp.Genre.Service.GenreService;
|
||||
import java.util.List;
|
||||
@Controller
|
||||
@RequestMapping("/genre")
|
||||
public class GenreMVC {
|
||||
private final GenreService genreService;
|
||||
|
||||
public GenreMVC(GenreService genreService) {
|
||||
this.genreService = genreService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String getGenre(@PathVariable Long id, Model model) {
|
||||
model.addAttribute("genre",new GenreDTO(genreService.findGenre(id)));
|
||||
return "genre-details";
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String getGenres(Model model) {
|
||||
model.addAttribute("genres",genreService.findAllGenres().stream()
|
||||
.map(GenreDTO::new)
|
||||
.toList());
|
||||
return "Searchpage";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createGenre(@RequestParam("name") String name) {
|
||||
|
||||
return "redirect:/genre/" + new GenreDTO(genreService.addGenre(name)).getID();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public String updateGenre(@PathVariable Long id,
|
||||
@RequestParam("name") String name) {
|
||||
return "redirect:/genre/" + new GenreDTO(genreService.updateGenre(id,name)).getID();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public String deleteGenre(@PathVariable Long id) {
|
||||
|
||||
genreService.deleteGenre(id);
|
||||
return "redirect:/genre";
|
||||
}
|
||||
|
||||
@GetMapping("/fill")
|
||||
public String insertGenres() {
|
||||
genreService.fillRepo();
|
||||
return "redirect:/movies/fill";
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package ru.ulstu.is.sbapp.Movie.Controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.Customer;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.CustomerRole;
|
||||
import ru.ulstu.is.sbapp.Customer.Service.CustomerService;
|
||||
import ru.ulstu.is.sbapp.Movie.Service.MovieService;
|
||||
import ru.ulstu.is.sbapp.WebConfiguration;
|
||||
|
||||
@ -10,9 +13,11 @@ import java.util.List;
|
||||
@ControllerAdvice(annotations = RestController.class)
|
||||
public class MovieController {
|
||||
private final MovieService movieService;
|
||||
private final CustomerService customerService;
|
||||
|
||||
public MovieController(MovieService movieService) {
|
||||
public MovieController(MovieService movieService, CustomerService customerService) {
|
||||
this.movieService = movieService;
|
||||
this.customerService = customerService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@ -47,19 +52,26 @@ public class MovieController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public MovieDTO deleteMovie(@PathVariable("id") Long id)
|
||||
{
|
||||
public MovieDTO deleteMovie(@PathVariable("id") Long id,@RequestParam("token") String token) throws IllegalAccessException {
|
||||
String username = customerService.loadUserByToken(token).getUsername();
|
||||
Customer customer = customerService.findByLogin(username);
|
||||
if(customer.getRole() != CustomerRole.ADMIN)
|
||||
{
|
||||
throw new IllegalAccessException("You don't have an access to do it");
|
||||
}
|
||||
return new MovieDTO(movieService.deleteMovie(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{customerId}/{id}")
|
||||
public MovieDTO assignMovie(@PathVariable("customerId") Long customerId, @PathVariable("id") Long id)
|
||||
@PostMapping("/customer/movies/add/{id}")
|
||||
public MovieDTO assignMovie(@RequestParam("token") String token, @PathVariable("id") Long id)
|
||||
{
|
||||
return new MovieDTO(movieService.assignMovie(customerId,id));
|
||||
String username = customerService.loadUserByToken(token).getUsername();
|
||||
return new MovieDTO(movieService.assignMovie(customerService.findByLogin(username).getId(),id));
|
||||
}
|
||||
@DeleteMapping("/{customerId}/{id}")
|
||||
public MovieDTO deleteMovieCustomer(@PathVariable("customerId") Long customerId,@PathVariable("id") Long id) {
|
||||
return new MovieDTO(movieService.deleteMovieCustomer(id,customerId));
|
||||
@DeleteMapping("/customer/movies/delete/{id}")
|
||||
public MovieDTO deleteMovieCustomer(@RequestParam("token") String token,@PathVariable("id") Long id) {
|
||||
String username = customerService.loadUserByToken(token).getUsername();
|
||||
return new MovieDTO(movieService.deleteMovieCustomer(id,customerService.findByLogin(username).getId()));
|
||||
}
|
||||
|
||||
@PostMapping("/fill")
|
||||
|
@ -1,113 +0,0 @@
|
||||
package ru.ulstu.is.sbapp.Movie.MVC;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.sbapp.Genre.Controller.GenreDTO;
|
||||
import ru.ulstu.is.sbapp.Genre.Model.Genre;
|
||||
import ru.ulstu.is.sbapp.Genre.Service.GenreService;
|
||||
import ru.ulstu.is.sbapp.Movie.Controller.MovieDTO;
|
||||
import ru.ulstu.is.sbapp.Movie.Model.Movie;
|
||||
import ru.ulstu.is.sbapp.Movie.Service.MovieService;
|
||||
import ru.ulstu.is.sbapp.Utilities.CookiesManagement;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Objects;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/movies")
|
||||
public class MovieMVC {
|
||||
private final MovieService movieService;
|
||||
private final GenreService genreService;
|
||||
private final CookiesManagement cookiesManagement;
|
||||
public MovieMVC(MovieService movieService, GenreService genreService)
|
||||
{
|
||||
this.movieService = movieService;
|
||||
this.genreService = genreService;
|
||||
this.cookiesManagement = new CookiesManagement();
|
||||
}
|
||||
|
||||
@GetMapping("/movie/{id}")
|
||||
public String getMovie(@PathVariable Long id, Model model) {
|
||||
model.addAttribute("movie", new MovieDTO(movieService.findMovie(id)));
|
||||
return "Filmpage";
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String getMovies(HttpServletRequest request, Model model) {
|
||||
|
||||
String userId = null;
|
||||
userId = cookiesManagement.GetUserID(request);
|
||||
|
||||
|
||||
model.addAttribute("movies", movieService.findAllMovies().stream()
|
||||
.map(MovieDTO::new)
|
||||
.toList());
|
||||
model.addAttribute("userId", userId);
|
||||
return "Mainpage";
|
||||
}
|
||||
|
||||
@GetMapping("/{genre}")
|
||||
public String getSpecificMovies(@PathVariable("genre") String genre, Model model) {
|
||||
if(!Objects.equals(genre, "null") && !Objects.equals(genre,"all"))
|
||||
{
|
||||
model.addAttribute("movies", movieService.findAllSpecificMovies(genre).stream()
|
||||
.map(MovieDTO::new)
|
||||
.toList());
|
||||
}
|
||||
else
|
||||
{
|
||||
model.addAttribute("movies", movieService.findAllMovies().stream()
|
||||
.map(MovieDTO::new)
|
||||
.toList());
|
||||
}
|
||||
model.addAttribute("genres",genreService.findAllGenres().stream().map(GenreDTO::new).toList());
|
||||
return "Searchpage";
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
public String createMovie(@RequestParam("title") String title,
|
||||
@RequestParam("length") int length,
|
||||
@RequestParam("score") double score,
|
||||
@RequestParam("genre") Long genreId) {
|
||||
|
||||
return "redirect:/movie" + new MovieDTO(movieService.addMovie(title, length, score, genreId)).getID();
|
||||
}
|
||||
|
||||
@PostMapping("/movie/update/{id}")
|
||||
public String updateMovie(@PathVariable("id") Long id,
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam("length") int length,
|
||||
@RequestParam("score") double score) {
|
||||
movieService.updateMovie(id, title, length, score);
|
||||
return "redirect:/movies/movie/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/movie/delete/{id}")
|
||||
public String deleteMovie(@PathVariable("id") Long id) {
|
||||
movieService.deleteMovie(id);
|
||||
return "redirect:/movie";
|
||||
}
|
||||
|
||||
@PostMapping("/customer/{id}")
|
||||
public String assignMovie(HttpServletRequest request,@PathVariable("id") Long id) {
|
||||
|
||||
Long customerId = Long.parseLong(cookiesManagement.GetUserID(request));
|
||||
movieService.assignMovie(customerId, id);
|
||||
return "redirect:/movies";
|
||||
}
|
||||
|
||||
@PostMapping("/customer/delete/{id}")
|
||||
public String deleteMovieCustomer(HttpServletRequest request, @PathVariable("id") Long id) {
|
||||
Long customerId = Long.parseLong(cookiesManagement.GetUserID(request));
|
||||
movieService.deleteMovieCustomer(id, customerId);
|
||||
return "redirect:/customer/movies";
|
||||
}
|
||||
|
||||
@GetMapping("/fill")
|
||||
public String insertMovies() {
|
||||
movieService.fillRepo();
|
||||
return "redirect:/movies";
|
||||
}
|
||||
}
|
@ -168,6 +168,7 @@ public class MovieService
|
||||
customer.getMovies().remove(specificMovie);
|
||||
});
|
||||
|
||||
movieRepository.delete(specificMovie);
|
||||
return specificMovie;
|
||||
}
|
||||
|
||||
|
29
src/main/java/ru/ulstu/is/sbapp/OpenAPI30Configuration.java
Normal file
29
src/main/java/ru/ulstu/is/sbapp/OpenAPI30Configuration.java
Normal file
@ -0,0 +1,29 @@
|
||||
package ru.ulstu.is.sbapp;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import ru.ulstu.is.sbapp.jwt.JwtFilter;
|
||||
|
||||
@Configuration
|
||||
public class OpenAPI30Configuration {
|
||||
|
||||
public static final String API_PREFIX = "/api/1.0";
|
||||
|
||||
@Bean
|
||||
public OpenAPI customizeOpenAPI() {
|
||||
final String securitySchemeName = JwtFilter.TOKEN_BEGIN_STR;
|
||||
return new OpenAPI()
|
||||
.addSecurityItem(new SecurityRequirement()
|
||||
.addList(securitySchemeName))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes(securitySchemeName, new SecurityScheme()
|
||||
.name(securitySchemeName)
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")));
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package ru.ulstu.is.sbapp;
|
||||
|
||||
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 PassowrdEncoderConfiguration {
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
61
src/main/java/ru/ulstu/is/sbapp/SecurityConfiguration.java
Normal file
61
src/main/java/ru/ulstu/is/sbapp/SecurityConfiguration.java
Normal file
@ -0,0 +1,61 @@
|
||||
package ru.ulstu.is.sbapp;
|
||||
|
||||
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.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import ru.ulstu.is.sbapp.Customer.Controller.CustomerController;
|
||||
import ru.ulstu.is.sbapp.Customer.Service.CustomerService;
|
||||
import ru.ulstu.is.sbapp.jwt.JwtFilter;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
public static final String SPA_URL_MASK = "/{path:[^\\.]*}";
|
||||
|
||||
private final CustomerService customerService;
|
||||
private final JwtFilter jwtFilter;
|
||||
|
||||
public SecurityConfiguration(CustomerService customerService) {
|
||||
this.customerService = customerService;
|
||||
this.jwtFilter = new JwtFilter(customerService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http.cors()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/", SPA_URL_MASK).permitAll()
|
||||
.antMatchers(HttpMethod.POST, WebConfiguration.REST_API + "/customer" + CustomerController.URL_LOGIN).permitAll()
|
||||
.antMatchers(HttpMethod.POST, WebConfiguration.REST_API + "/customer").permitAll()
|
||||
.anyRequest()
|
||||
.authenticated()
|
||||
.and()
|
||||
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.anonymous();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
|
||||
builder.userDetailsService(customerService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(WebSecurity web) {
|
||||
web
|
||||
.ignoring()
|
||||
.antMatchers(HttpMethod.OPTIONS, "/**")
|
||||
.antMatchers("/**/*.{js,html,css,png}")
|
||||
.antMatchers("/swagger-ui/index.html")
|
||||
.antMatchers("/webjars/**")
|
||||
.antMatchers("/swagger-resources/**")
|
||||
.antMatchers("/v3/api-docs/**");
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ public class ValidationException extends RuntimeException {
|
||||
public ValidationException(Set<String> errors) {
|
||||
super(String.join("\n", errors));
|
||||
}
|
||||
public ValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,29 @@
|
||||
package ru.ulstu.is.sbapp;
|
||||
|
||||
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 {
|
||||
public static final String REST_API = "/api";
|
||||
public static final String REST_API = OpenAPI30Configuration.API_PREFIX;
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController(SecurityConfiguration.SPA_URL_MASK).setViewName("forward:/");
|
||||
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("*");
|
||||
|
11
src/main/java/ru/ulstu/is/sbapp/jwt/JwtException.java
Normal file
11
src/main/java/ru/ulstu/is/sbapp/jwt/JwtException.java
Normal file
@ -0,0 +1,11 @@
|
||||
package ru.ulstu.is.sbapp.jwt;
|
||||
|
||||
public class JwtException extends RuntimeException{
|
||||
public JwtException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
public JwtException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
73
src/main/java/ru/ulstu/is/sbapp/jwt/JwtFilter.java
Normal file
73
src/main/java/ru/ulstu/is/sbapp/jwt/JwtFilter.java
Normal file
@ -0,0 +1,73 @@
|
||||
package ru.ulstu.is.sbapp.jwt;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
import ru.ulstu.is.sbapp.Customer.Model.Customer;
|
||||
import ru.ulstu.is.sbapp.Customer.Service.CustomerService;
|
||||
|
||||
public class JwtFilter extends GenericFilterBean{
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
public static final String TOKEN_BEGIN_STR = "Bearer ";
|
||||
|
||||
private final CustomerService customerService;
|
||||
|
||||
public JwtFilter(CustomerService customerService) {
|
||||
this.customerService = customerService;
|
||||
}
|
||||
|
||||
private String getTokenFromRequest(HttpServletRequest request) {
|
||||
String bearer = request.getHeader(AUTHORIZATION);
|
||||
if (StringUtils.hasText(bearer) && bearer.startsWith(TOKEN_BEGIN_STR)) {
|
||||
return bearer.substring(TOKEN_BEGIN_STR.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void raiseException(ServletResponse response, int status, String message) throws IOException {
|
||||
if (response instanceof final HttpServletResponse httpResponse) {
|
||||
httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
httpResponse.setStatus(status);
|
||||
final byte[] body = new ObjectMapper().writeValueAsBytes(message);
|
||||
response.getOutputStream().write(body);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request,
|
||||
ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
if (request instanceof final HttpServletRequest httpRequest) {
|
||||
final String token = getTokenFromRequest(httpRequest);
|
||||
if (StringUtils.hasText(token)) {
|
||||
try {
|
||||
final UserDetails user = customerService.loadUserByToken(token);
|
||||
final UsernamePasswordAuthenticationToken auth =
|
||||
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
} catch (JwtException e) {
|
||||
raiseException(response, HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
raiseException(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
|
||||
String.format("Internal error: %s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
27
src/main/java/ru/ulstu/is/sbapp/jwt/JwtProperties.java
Normal file
27
src/main/java/ru/ulstu/is/sbapp/jwt/JwtProperties.java
Normal file
@ -0,0 +1,27 @@
|
||||
package ru.ulstu.is.sbapp.jwt;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "jwt", ignoreInvalidFields = true)
|
||||
public class JwtProperties {
|
||||
private String devToken = "";
|
||||
private Boolean isDev = true;
|
||||
|
||||
public String getDevToken() {
|
||||
return devToken;
|
||||
}
|
||||
|
||||
public void setDevToken(String devToken) {
|
||||
this.devToken = devToken;
|
||||
}
|
||||
|
||||
public Boolean isDev() {
|
||||
return isDev;
|
||||
}
|
||||
|
||||
public void setDev(Boolean dev) {
|
||||
isDev = dev;
|
||||
}
|
||||
}
|
107
src/main/java/ru/ulstu/is/sbapp/jwt/JwtProvider.java
Normal file
107
src/main/java/ru/ulstu/is/sbapp/jwt/JwtProvider.java
Normal file
@ -0,0 +1,107 @@
|
||||
package ru.ulstu.is.sbapp.jwt;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.auth0.jwt.interfaces.JWTVerifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class JwtProvider {
|
||||
private final static Logger LOG = LoggerFactory.getLogger(JwtProvider.class);
|
||||
|
||||
private final static byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
|
||||
private final static String ISSUER = "auth0";
|
||||
|
||||
private final Algorithm algorithm;
|
||||
private final JWTVerifier verifier;
|
||||
|
||||
public JwtProvider(JwtProperties jwtProperties) {
|
||||
if (!jwtProperties.isDev()) {
|
||||
LOG.info("Generate new JWT key for prod");
|
||||
try {
|
||||
final MessageDigest salt = MessageDigest.getInstance("SHA-256");
|
||||
salt.update(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
|
||||
LOG.info("Use generated JWT key for prod \n{}", bytesToHex(salt.digest()));
|
||||
algorithm = Algorithm.HMAC256(bytesToHex(salt.digest()));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new JwtException(e);
|
||||
}
|
||||
} else {
|
||||
LOG.info("Use default JWT key for dev \n{}", jwtProperties.getDevToken());
|
||||
algorithm = Algorithm.HMAC256(jwtProperties.getDevToken());
|
||||
}
|
||||
verifier = JWT.require(algorithm)
|
||||
.withIssuer(ISSUER)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
byte[] hexChars = new byte[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public String generateToken(String login) {
|
||||
final Date issueDate = Date.from(LocalDate.now()
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant());
|
||||
final Date expireDate = Date.from(LocalDate.now()
|
||||
.plusDays(15)
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant());
|
||||
return JWT.create()
|
||||
.withIssuer(ISSUER)
|
||||
.withIssuedAt(issueDate)
|
||||
.withExpiresAt(expireDate)
|
||||
.withSubject(login)
|
||||
.sign(algorithm);
|
||||
}
|
||||
|
||||
private DecodedJWT validateToken(String token) {
|
||||
try {
|
||||
return verifier.verify(token);
|
||||
} catch (JWTVerificationException e) {
|
||||
throw new JwtException(String.format("Token verification error: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTokenValid(String token) {
|
||||
if (!StringUtils.hasText(token)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
validateToken(token);
|
||||
return true;
|
||||
} catch (JwtException e) {
|
||||
LOG.error(e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<String> getLoginFromToken(String token) {
|
||||
try {
|
||||
return Optional.ofNullable(validateToken(token).getSubject());
|
||||
} catch (JwtException e) {
|
||||
LOG.error(e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div><span th:text="${error}"></span></div>
|
||||
<a href="/">Return to registration</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,81 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<div class="flex-container" style="flex-direction: column; display: flex;">
|
||||
<div class="flex-container min-vh-100" style="flex-direction: column; display: flex;">
|
||||
<main style="display: flex; flex: 1; gap: 100px;">
|
||||
<div class="flex-container" style="display: inline-flex; flex: 1; flex-wrap: wrap; gap: 40px;">
|
||||
<div class="flex-item1 align-self-center" style="flex: 1;">
|
||||
<img src="https://www.seekpng.com/png/detail/8-84931_question-mark-question-mark-white-background.png" width="400px" height="600px"/>
|
||||
</div>
|
||||
<div id="movieCard" th:attr="data-movie-id=${movie.getID()}"class="flex-container align-self-center" style="flex: 3;">
|
||||
<div class="flex-item1 text-light" style="font-size: 50px;"><a th:text="${movie.title}"></a></div>
|
||||
<div class="flex-item3 text-light" style="font-size: 35px;">Average score: <span th:text="${movie.score}"></span></div>
|
||||
<div class="flex-item3 text-light" style="font-size: 35px;">Length: <span th:text="${movie.length}"></span></div>
|
||||
<div class="flex-item3 text-light" style="font-size: 35px;">Genre: <span th:text="${movie.genreName}"></span></div>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modal">Edit Movie</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div id="modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="handleModalClose()">×</span>
|
||||
<h2>Edit Movie</h2>
|
||||
<div class="form-group">
|
||||
<label>Title:</label>
|
||||
<input type="text" class="form-control" name="title" th:value="${movie.title}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Score:</label>
|
||||
<input type="text" class="form-control" name="score" th:value="${movie.score}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Length:</label>
|
||||
<input type="text" class="form-control" name="length" th:value="${movie.length}"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="handleModalClose()">Close</button>
|
||||
<button class="btn btn-primary" onclick="handleModalSubmit()">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<th:block layout:fragment="scripts">
|
||||
<script>
|
||||
function handleModalClose()
|
||||
{
|
||||
const modalElement = document.getElementById('modal');
|
||||
console.log("html modals sucks");
|
||||
// Use Bootstrap's Modal API to close the modal
|
||||
const modal = new bootstrap.Modal(modalElement);
|
||||
modal.hide();
|
||||
}
|
||||
function handleModalSubmit() {
|
||||
var titleInput = document.getElementsByName("title")[0];
|
||||
var scoreInput = document.getElementsByName("score")[0];
|
||||
var lengthInput = document.getElementsByName("length")[0];
|
||||
|
||||
var modalData = {
|
||||
title: titleInput.value,
|
||||
score: scoreInput.value,
|
||||
length: lengthInput.value
|
||||
};
|
||||
|
||||
const movieId = document.getElementById('movieCard').getAttribute('data-movie-id');
|
||||
console.log(movieId);
|
||||
fetch(`/movies/movie/update/${movieId}?title=${modalData["title"]}&length=${modalData["length"]}&score=${modalData["score"]}`,{
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
window.location.href = "/movies";
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
|
||||
<div layout:fragment="content">
|
||||
|
||||
|
||||
<div class="flex-container min-vh-100" style="display: flex; margin-left: 20px; margin-top: 50px">
|
||||
|
||||
<th:block th:if="${not #lists.isEmpty(movies)}">
|
||||
<div style="flex-direction: row; display: flex; flex-wrap: wrap; gap: 30px">
|
||||
<th:block th:each="movie : ${movies}">
|
||||
<div className="flex-containerB" style="flex-direction: column; display: flex; width: 250px; height: 350px; gap:40px; align-items:center; color:aliceblue">
|
||||
<img src="https://www.seekpng.com/png/detail/8-84931_question-mark-question-mark-white-background.png" alt = "cover" width="100%" height="300px"/>
|
||||
<h3 th:text="${movie.title}"></h3>
|
||||
<form th:action="@{/movies/customer/delete/{movieId}(movieId=${movie.getID()})}" method="post">
|
||||
<button type="submit">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</th:block>
|
||||
<h1 th:unless="${not #lists.isEmpty(movies)}" style="color: white">No movies available</h1>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<th:block layout:fragment="scripts">
|
||||
<script>
|
||||
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<div class="flex-container" style="flex-direction: column; display: flex; ">
|
||||
<div class="flex-container min-vh-100" style="flex-direction: column; display: flex; align-items: center; justify-content: center; gap: 20px;">
|
||||
<h1 for="userSelect" style="color: white;">Select User:</h1>
|
||||
<select id="userSelect" style="width: 100px;">
|
||||
<option th:each="customer : ${customers}" th:value="${customer.getID()}" th:text="${customer.username}"></option>
|
||||
</select>
|
||||
<button style="margin-top: 50px;" onclick="handleLogin()">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<th:block layout:fragment="scripts">
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
function handleLogin() {
|
||||
localStorage.clear();
|
||||
var selectedUserId = document.getElementById("userSelect").value;
|
||||
document.cookie = "userID=" + selectedUserId;
|
||||
window.location.href = "/genre/fill";
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
|
||||
<div layout:fragment="content">
|
||||
|
||||
|
||||
<div class="flex-container min-vh-100" style="flex-direction: row; display: flex; margin-left: 20px; margin-top: 50px;">
|
||||
<div th:if="${not #lists.isEmpty(movies)}" style="flex-direction: row; display: flex; flex-wrap: wrap; justify-content: space-between;">
|
||||
<div th:each="movie : ${movies}" style="margin-bottom: 20px;">
|
||||
<div className="flex-containerB" style="flex-direction: column; display: flex; width: 250px; height: 350px; gap:40px; align-items:center;">
|
||||
<img src="https://www.seekpng.com/png/detail/8-84931_question-mark-question-mark-white-background.png" alt = "cover" width="100%" height="300px"/>
|
||||
<h3><a th:href="@{/movies/movie/{movieId}(movieId=${movie.getID()})}"
|
||||
th:text="${movie.title}" style="text-decoration: none; color: white;"></a></h3>
|
||||
<button type="button" th:attr="data-movie-id=${movie.getID()}" onclick="handleAcquireMovie(event)">Acquire</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 th:unless="${not #lists.isEmpty(movies)}" style="color: white;">No movies available</h1>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<th:block layout:fragment="scripts">
|
||||
<script th:inline="javascript">
|
||||
|
||||
function handleAcquireMovie(movieId) {
|
||||
var movieId = event.target.getAttribute("data-movie-id");
|
||||
var url = "/movies/customer/" + movieId;
|
||||
fetch(url,{
|
||||
method: "POST",
|
||||
}) ;
|
||||
}
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<div class="flex-container" style="flex-direction: column; display: flex;">
|
||||
<div class="flex-container min-vh-100" style="flex-direction: column; display: flex; flex: 1; align-items: center; justify-content: flex-start; margin-top: 20px">
|
||||
<div>
|
||||
<select id="genre-select">
|
||||
<option value="all">All Genres</option>
|
||||
<option th:each="genre : ${genres}" th:value="${genre.name}" th:text="${genre.name}"></option>
|
||||
</select>
|
||||
<button type="button" onclick="handleSearch()">Search</button>
|
||||
</div>
|
||||
<div class="flex-container min-vh-100" style="flex-direction: row; display: flex; margin-left: 20px; margin-top: 50px">
|
||||
<div style="flex-direction: row; display: flex; flex-wrap: wrap; gap: 30px">
|
||||
<div th:if="${not #lists.isEmpty(movies)}" th:each="movie : ${movies}" style="margin-bottom: 20px;">
|
||||
<div className="flex-containerB" style="flex-direction: column; display: flex; width: 250px; height: 350px; gap:40px; align-items:center; color:aliceblue">
|
||||
<img src="https://www.seekpng.com/png/detail/8-84931_question-mark-question-mark-white-background.png" alt = "cover" width="100%" height="300px"/>
|
||||
<h3><a th:href="@{/movies/movie/{movieId}(movieId=${movie.getID()})}"
|
||||
th:text="${movie.title}" style="text-decoration: none; color: white;"></a></h3>
|
||||
</div>
|
||||
</div>
|
||||
<h1 th:if="${#lists.isEmpty(movies)}" style="color: white">No movies found</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<th:block layout:fragment="scripts">
|
||||
<script>
|
||||
window.onload = function()
|
||||
{
|
||||
var storedOptionValue = localStorage.getItem('genre');
|
||||
if(storedOptionValue !== null)
|
||||
{
|
||||
var selectElement = document.getElementById('genre-select');
|
||||
selectElement.value = storedOptionValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
const genreSelect = document.getElementById("genre-select");
|
||||
const selectedGenre = genreSelect.value;
|
||||
localStorage.setItem('genre',selectedGenre);
|
||||
window.location.href = `/movies/${selectedGenre}`;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
@ -1,50 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>I'M TIRED OF IP</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<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>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<div style="background-image: url('https://img.freepik.com/premium-vector/grainy-gradient-background-using-different-colors_606954-9.jpg');">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/movies" th:classappend="${#strings.equals(activeLink, '/movies')} ? 'active' : ''">
|
||||
Main Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/movies/null" th:classappend="${#strings.equals(activeLink, '/genre')} ? 'active' : ''">
|
||||
Search Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" th:href="@{/customer/movies}">
|
||||
Library Page
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/" th:classappend="${#strings.equals(activeLink, '/')} ? 'active' : ''">
|
||||
Registration Page
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div layout:fragment="content">
|
||||
|
||||
</div>
|
||||
<footer class="flex-item6">
|
||||
<div class="text-center text-light p-2" style="font-size: 30px; font-weight: bold;">
|
||||
@2022 Copyright: BLSJY.com
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<th:block layout:fragment="scripts">
|
||||
</th:block>
|
||||
</html>
|
@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{Template}">
|
||||
<head>
|
||||
</head>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<div class="flex-container" style="flex-direction: column; display: flex;">
|
||||
<div class="flex-container min-vh-100" style="flex-direction: column; display: flex;">
|
||||
<main style="display: flex; flex: 1;">
|
||||
<aside class="flex-item2" style="flex: 2;"></aside>
|
||||
<div class="flex-item3 align-self-center" style="flex: 3;"><img src="https://i.pinimg.com/736x/57/ee/6a/57ee6a23a455b12eff3aa13d63f104cd.jpg" alt="cover" width="100%" height="600px"/></div>
|
||||
<section class="flex-container align-self-center" style="height: 660px; flex: 4; background-color: #ffd79d; flex-direction: column; display: flex;">
|
||||
<form action="#" th:action="@{/customer}" method="post">
|
||||
<section class="flex-itemR1" style="color: #320D3E; font-size: 50px; font-weight: bold; padding-left: 30px; padding-top: 10px;">SIGN UP</section>
|
||||
<section class="flex-itemR2" style="color: #320D3E; font-size: 35px; font-weight: bold; padding-left: 30px; padding-top: 10px;">
|
||||
<label style="color: #320D3E; font-size: 32px; font-weight: bold;">USERNAME</label>
|
||||
</section>
|
||||
<section class="flex-itemR3" style="display: flex; width: 320px; padding-left: 30px;">
|
||||
<input class="form-control" id="inputUsername" type="string" name="fullName" placeholder="Enter username" th:value="${inputUsername}"/>
|
||||
</section>
|
||||
<section class="flex-itemR6" style="color: #320D3E; font-size: 35px; font-weight: bold; padding-left: 30px; padding-top: 10px;">
|
||||
<label style="color: #320D3E; font-size: 32px; font-weight: bold;">PASSWORD</label>
|
||||
</section>
|
||||
<section class="flex-itemR7" style="display: flex; width: 320px; padding-left: 30px;">
|
||||
<input class="form-control" id="inputPassword" type="password" name="password" placeholder="Enter password" th:value="${inputPassword}"/>
|
||||
</section>
|
||||
<button class="btn btn-primary" type="submit" id="register" style="font-size: 20px; margin-left: 30px; margin-top: 15px; width: 150px; background-color: #320D3E; color: white; font-weight: bold;">Register</button>
|
||||
<a href="/customer">Sign in</a>
|
||||
</form>
|
||||
</section>
|
||||
<aside class="flex-item5" style="flex: 2;"></aside>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user