Labwork06 React is done?

This commit is contained in:
Yuee Shiness 2023-06-17 07:16:34 +04:00
parent 9d4f79f1ae
commit af9ea10b78
61 changed files with 629 additions and 1061 deletions

View File

@ -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'

Binary file not shown.

View File

@ -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>

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

View File

@ -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}`);
}

View File

@ -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>

View 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;

View File

@ -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>

View 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;

View File

@ -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(

View File

@ -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>

View File

@ -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 (

View File

@ -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>

View File

@ -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>

View File

@ -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()">&times;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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";
}
}

View File

@ -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;

View File

@ -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";
}
}

View File

@ -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);
}

View File

@ -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)

View File

@ -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";
}
}

View File

@ -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")

View File

@ -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";
}
}

View File

@ -168,6 +168,7 @@ public class MovieService
customer.getMovies().remove(specificMovie);
});
movieRepository.delete(specificMovie);
return specificMovie;
}

View 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")));
}
}

View File

@ -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();
}
}

View 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/**");
}
}

View File

@ -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);
}
}

View File

@ -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("*");

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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();
}
}
}

View File

@ -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>

View File

@ -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()">&times;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>