├── frontend ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.jsx │ ├── assets │ │ ├── hoaxify.png │ │ └── profile.png │ ├── lib │ │ └── http.js │ ├── locales │ │ ├── index.js │ │ └── translations │ │ │ ├── en.json │ │ │ └── tr.json │ ├── main.jsx │ ├── pages │ │ ├── Activation │ │ │ ├── api.js │ │ │ └── index.jsx │ │ ├── Home │ │ │ ├── components │ │ │ │ ├── UserList.jsx │ │ │ │ ├── UserListItem.jsx │ │ │ │ └── api.js │ │ │ └── index.jsx │ │ ├── Login │ │ │ ├── api.js │ │ │ └── index.jsx │ │ ├── PasswordReset │ │ │ ├── Request │ │ │ │ ├── api.js │ │ │ │ ├── index.jsx │ │ │ │ └── usePasswordResetRequest.js │ │ │ └── SetPassword │ │ │ │ ├── api.js │ │ │ │ ├── index.jsx │ │ │ │ └── useSetPassword.js │ │ ├── SignUp │ │ │ ├── api.js │ │ │ └── index.jsx │ │ └── User │ │ │ ├── api.js │ │ │ ├── components │ │ │ └── ProfileCard │ │ │ │ ├── UserDeleteButton │ │ │ │ ├── api.js │ │ │ │ ├── index.jsx │ │ │ │ └── useUserDeleteButton.js │ │ │ │ ├── UserEditForm.jsx │ │ │ │ ├── api.js │ │ │ │ └── index.jsx │ │ │ └── index.jsx │ ├── router │ │ └── index.js │ ├── shared │ │ ├── components │ │ │ ├── Alert.jsx │ │ │ ├── Button.jsx │ │ │ ├── Input.jsx │ │ │ ├── LanguageSelector.jsx │ │ │ ├── NavBar │ │ │ │ ├── api.js │ │ │ │ └── index.jsx │ │ │ ├── ProfileImage.jsx │ │ │ └── Spinner.jsx │ │ ├── hooks │ │ │ └── useRouteParamApiRequest.js │ │ └── state │ │ │ ├── context.jsx │ │ │ └── storage.js │ └── styles.scss └── vite.config.js └── ws ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── hoaxify │ │ └── ws │ │ ├── WsApplication.java │ │ ├── auth │ │ ├── AuthController.java │ │ ├── AuthService.java │ │ ├── dto │ │ │ ├── AuthResponse.java │ │ │ └── Credentials.java │ │ ├── exception │ │ │ └── AuthenticationException.java │ │ └── token │ │ │ ├── BasicAuthTokenService.java │ │ │ ├── JwtTokenService.java │ │ │ ├── OpaqueTokenService.java │ │ │ ├── Token.java │ │ │ ├── TokenRepository.java │ │ │ └── TokenService.java │ │ ├── configuration │ │ ├── AppUserDetailsService.java │ │ ├── AuthEntryPoint.java │ │ ├── CurrentUser.java │ │ ├── HoaxifyProperties.java │ │ ├── SecurityBeans.java │ │ ├── SecurityConfiguration.java │ │ ├── StaticResourceConfiguration.java │ │ └── TokenFilter.java │ │ ├── email │ │ └── EmailService.java │ │ ├── error │ │ ├── ApiError.java │ │ ├── ErrorHandler.java │ │ └── GlobalErrorHandler.java │ │ ├── file │ │ └── FileService.java │ │ ├── shared │ │ ├── GenericMessage.java │ │ └── Messages.java │ │ └── user │ │ ├── User.java │ │ ├── UserController.java │ │ ├── UserRepository.java │ │ ├── UserService.java │ │ ├── dto │ │ ├── PasswordResetRequest.java │ │ ├── PasswordUpdate.java │ │ ├── UserCreate.java │ │ ├── UserDTO.java │ │ └── UserUpdate.java │ │ ├── exception │ │ ├── ActivationNotificationException.java │ │ ├── InvalidTokenException.java │ │ ├── NotFoundException.java │ │ └── NotUniqueEmailException.java │ │ └── validation │ │ ├── FileType.java │ │ ├── FileTypeValidator.java │ │ ├── UniqueEmail.java │ │ └── UniqueEmailValidator.java └── resources │ ├── ValidationMessages.properties │ ├── ValidationMessages_tr.properties │ ├── application.properties │ ├── messages.properties │ └── messages_tr.properties └── test └── java └── com └── hoaxify └── ws └── WsApplicationTests.java /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | 'react/jsx-no-target-blank': false 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + React 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.4.0", 14 | "bootstrap": "^5.3.1", 15 | "i18next": "^23.4.4", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-i18next": "^13.1.2", 19 | "react-router-dom": "^6.15.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.2.15", 23 | "@types/react-dom": "^18.2.7", 24 | "@vitejs/plugin-react": "^4.0.3", 25 | "eslint": "^8.45.0", 26 | "eslint-plugin-react": "^7.32.2", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.4.3", 29 | "sass": "^1.65.1", 30 | "vite": "^4.4.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import { LanguageSelector } from "./shared/components/LanguageSelector"; 3 | import { NavBar } from "./shared/components/NavBar"; 4 | import { AuthenticationContext } from "./shared/state/context"; 5 | 6 | function App() { 7 | 8 | return ( 9 | 10 | 11 |
12 | 13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /frontend/src/assets/hoaxify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basarbk/spring-n-react-tr-course/ee3303eed976c11bb0ce4ea1a6ce4ec5b5efd49d/frontend/src/assets/hoaxify.png -------------------------------------------------------------------------------- /frontend/src/assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basarbk/spring-n-react-tr-course/ee3303eed976c11bb0ce4ea1a6ce4ec5b5efd49d/frontend/src/assets/profile.png -------------------------------------------------------------------------------- /frontend/src/lib/http.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { i18nInstance } from "@/locales"; 3 | 4 | const http = axios.create(); 5 | 6 | http.interceptors.request.use((config) => { 7 | config.headers["Accept-Language"] = i18nInstance.language 8 | return config; 9 | }) 10 | 11 | 12 | export default http; -------------------------------------------------------------------------------- /frontend/src/locales/index.js: -------------------------------------------------------------------------------- 1 | 2 | import i18n from "i18next"; 3 | import { initReactI18next } from "react-i18next"; 4 | import en from "./translations/en.json" 5 | import tr from "./translations/tr.json" 6 | 7 | const initialLanguage = localStorage.getItem('lang') || navigator.language || 'en' 8 | 9 | export const i18nInstance = i18n.use(initReactI18next) 10 | 11 | i18nInstance.init({ 12 | resources: { 13 | en: { 14 | translation: en 15 | }, 16 | tr: { 17 | translation: tr 18 | } 19 | }, 20 | fallbackLng: initialLanguage, 21 | 22 | interpolation: { 23 | escapeValue: false 24 | }}); -------------------------------------------------------------------------------- /frontend/src/locales/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "signUp": "Sign Up", 3 | "username": "Username", 4 | "email": "E-mail", 5 | "password": "Password", 6 | "passwordRepeat": "Password Repeat", 7 | "passwordMismatch": "Password mismatch", 8 | "genericError": "Unexpected error occured. Please try again", 9 | "userNotFoundError": "User not found", 10 | "login": "Login" 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/locales/translations/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "signUp": "Kayit ol", 3 | "username": "Kullanici adi", 4 | "email": "E-posta", 5 | "password": "Şifre", 6 | "passwordRepeat": "Şifre Tekrari", 7 | "passwordMismatch": "Sifreniz eslesmiyor", 8 | "genericError": "Beklenmedik bir hata olustu. Lutfen tekrar deneyin", 9 | "userNotFoundError": "Kullanici bulunamadi", 10 | "login": "Giris yap" 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import "./styles.scss" 4 | import "./locales" 5 | import {RouterProvider } from "react-router-dom"; 6 | import router from "./router"; 7 | 8 | ReactDOM.createRoot(document.getElementById('root')).render( 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /frontend/src/pages/Activation/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function activateUser(token){ 4 | return http.patch(`/api/v1/users/${token}/active`) 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/Activation/index.jsx: -------------------------------------------------------------------------------- 1 | import { activateUser } from "./api"; 2 | import { Alert } from "@/shared/components/Alert"; 3 | import { Spinner } from "@/shared/components/Spinner"; 4 | import { useRouteParamApiRequest } from "@/shared/hooks/useRouteParamApiRequest"; 5 | 6 | export function Activation() { 7 | const { apiProgress, data, error } = useRouteParamApiRequest( 8 | "token", 9 | activateUser 10 | ); 11 | return ( 12 | <> 13 | {apiProgress && ( 14 | 15 | 16 | 17 | )} 18 | {data?.message && {data.message}} 19 | {error && {error}} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/pages/Home/components/UserList.jsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { loadUsers } from "./api"; 3 | import { Spinner } from "@/shared/components/Spinner"; 4 | import { UserListItem } from "./UserListItem"; 5 | 6 | export function UserList() { 7 | const [userPage, setUserPage] = useState({ 8 | content: [], 9 | last: false, 10 | first: false, 11 | number: 0, 12 | }); 13 | const [apiProgress, setApiProgress] = useState(false); 14 | 15 | const getUsers = useCallback(async (page) => { 16 | setApiProgress(true); 17 | try { 18 | const response = await loadUsers(page); 19 | setUserPage(response.data); 20 | } catch { 21 | } finally { 22 | setApiProgress(false) 23 | } 24 | }, []); 25 | 26 | useEffect(() => { 27 | getUsers(); 28 | }, []); 29 | 30 | return ( 31 |
32 |
User List
33 | 38 |
39 | {apiProgress && } 40 | {!apiProgress && !userPage.first && ( 41 | 47 | )} 48 | {!apiProgress && !userPage.last && ( 49 | 55 | )} 56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/pages/Home/components/UserListItem.jsx: -------------------------------------------------------------------------------- 1 | import { ProfileImage } from "@/shared/components/ProfileImage"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export function UserListItem({ user }) { 5 | return ( 6 | 11 | 12 | {user.username} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/pages/Home/components/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function loadUsers(page = 0){ 4 | return http.get("/api/v1/users", { params: { page, size: 3} }); 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/Home/index.jsx: -------------------------------------------------------------------------------- 1 | import { UserList } from "./components/UserList"; 2 | 3 | export function Home(){ 4 | return 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/Login/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function login(credentials){ 4 | return http.post("/api/v1/auth", credentials); 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/Login/index.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { Alert } from "@/shared/components/Alert"; 4 | import { Input } from "@/shared/components/Input"; 5 | import { Button } from "@/shared/components/Button"; 6 | import { login } from "./api"; 7 | import { useAuthDispatch } from "@/shared/state/context"; 8 | import { Link, useNavigate } from "react-router-dom"; 9 | 10 | export function Login() { 11 | const [email, setEmail] = useState(); 12 | const [password, setPassword] = useState(); 13 | const [apiProgress, setApiProgress] = useState(); 14 | const [errors, setErrors] = useState({}); 15 | const [generalError, setGeneralError] = useState(); 16 | const { t } = useTranslation(); 17 | const navigate = useNavigate(); 18 | const dispatch = useAuthDispatch(); 19 | 20 | useEffect(() => { 21 | setErrors(function (lastErrors) { 22 | return { 23 | ...lastErrors, 24 | email: undefined, 25 | }; 26 | }); 27 | }, [email]); 28 | 29 | useEffect(() => { 30 | setErrors(function (lastErrors) { 31 | return { 32 | ...lastErrors, 33 | password: undefined, 34 | }; 35 | }); 36 | }, [password]); 37 | 38 | const onSubmit = async (event) => { 39 | event.preventDefault(); 40 | setGeneralError(); 41 | setApiProgress(true); 42 | 43 | try { 44 | const response = await login({ email, password }) 45 | dispatch({type: 'login-success', data: response.data}) 46 | navigate("/") 47 | } catch (axiosError) { 48 | if (axiosError.response?.data) { 49 | if (axiosError.response.data.status === 400) { 50 | setErrors(axiosError.response.data.validationErrors); 51 | } else { 52 | setGeneralError(axiosError.response.data.message); 53 | } 54 | } else { 55 | setGeneralError(t("genericError")); 56 | } 57 | } finally { 58 | setApiProgress(false); 59 | } 60 | }; 61 | 62 | return ( 63 |
64 |
65 |
66 |
67 |

{t("login")}

68 |
69 |
70 | setEmail(event.target.value)} 75 | /> 76 | setPassword(event.target.value)} 81 | type="password" 82 | /> 83 | {generalError && {generalError}} 84 |
85 | 88 |
89 |
90 |
91 | Forget password? 92 |
93 |
94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /frontend/src/pages/PasswordReset/Request/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function passwordResetRequest(body) { 4 | return http.post('/api/v1/users/password-reset', body); 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/PasswordReset/Request/index.jsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/shared/components/Input"; 2 | import { usePasswordResetRequest } from "./usePasswordResetRequest"; 3 | import { Alert } from "@/shared/components/Alert"; 4 | import { Button } from "@/shared/components/Button"; 5 | 6 | export function PasswordResetRequest() { 7 | const {onSubmit, onChangeEmail, apiProgress, success, error, generalError} = usePasswordResetRequest(); 8 | return ( 9 |
10 |
11 |
12 |
13 | Reset your password 14 |
15 |
16 | 22 | {success && ( 23 | {success} 24 | )} 25 | {generalError && ( 26 | {generalError} 27 | )} 28 |
29 | 34 |
35 |
36 |
37 |
38 |
39 | ); 40 | } -------------------------------------------------------------------------------- /frontend/src/pages/PasswordReset/Request/usePasswordResetRequest.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { passwordResetRequest } from "./api"; 3 | 4 | export function usePasswordResetRequest() { 5 | const [apiProgress, setApiProgress] = useState(false); 6 | const [email, setEmail] = useState(); 7 | const [success, setSuccess] = useState(); 8 | const [generalError, setGeneralError] = useState() 9 | const [errors, setErrors] = useState({}); 10 | 11 | const onSubmit = useCallback(async (event) => { 12 | event.preventDefault(); 13 | setApiProgress(true); 14 | setSuccess(); 15 | setErrors({}); 16 | setGeneralError() 17 | try { 18 | const response = await passwordResetRequest({ email }); 19 | setSuccess(response.data.message); 20 | } catch (axiosError) { 21 | if(axiosError.response.data.status === 400) { 22 | setErrors(axiosError.response?.data.validationErrors); 23 | } else { 24 | setGeneralError(axiosError.response.data.message); 25 | } 26 | } finally { 27 | setApiProgress(false); 28 | } 29 | }, [email]); 30 | 31 | return { 32 | apiProgress, 33 | onChangeEmail: (event) => setEmail(event.target.value), 34 | onSubmit, 35 | success, 36 | error: errors.email, 37 | generalError 38 | 39 | }; 40 | } -------------------------------------------------------------------------------- /frontend/src/pages/PasswordReset/SetPassword/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function resetPassword(token, body) { 4 | return http.patch(`/api/v1/users/${token}/password`, body); 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/PasswordReset/SetPassword/index.jsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/shared/components/Input"; 2 | import { useSetPassword } from "./useSetPassword"; 3 | import { Alert } from "@/shared/components/Alert"; 4 | import { Button } from "@/shared/components/Button"; 5 | 6 | export function SetPassword() { 7 | const {apiProgress, errors, generalError, onChangePassword, onChangePasswordRepeat, onSubmit, success, disabled} = useSetPassword(); 8 | return ( 9 |
10 |
11 |
12 |
13 | Set your password 14 |
15 |
16 | 23 | 30 | {success && ( 31 | {success} 32 | )} 33 | {generalError && ( 34 | {generalError} 35 | )} 36 |
37 | 43 |
44 |
45 |
46 |
47 |
48 | ); 49 | } -------------------------------------------------------------------------------- /frontend/src/pages/PasswordReset/SetPassword/useSetPassword.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { resetPassword } from "./api"; 3 | import { useNavigate, useSearchParams } from "react-router-dom"; 4 | 5 | export function useSetPassword() { 6 | const [apiProgress, setApiProgress] = useState(false); 7 | const [password, setPassword] = useState(); 8 | const [passwordRepeat, setPasswordRepeat] = useState(); 9 | const [success, setSuccess] = useState(); 10 | const [generalError, setGeneralError] = useState() 11 | const [errors, setErrors] = useState({}); 12 | const navigate = useNavigate() 13 | const [searchParams] = useSearchParams() 14 | 15 | const onSubmit = useCallback(async (event) => { 16 | event.preventDefault(); 17 | setApiProgress(true); 18 | setSuccess(); 19 | setErrors({}); 20 | setGeneralError() 21 | try { 22 | const response = await resetPassword(searchParams.get("tk"), { password }); 23 | setSuccess(response.data.message); 24 | navigate("/login") 25 | } catch (axiosError) { 26 | if(axiosError.response.data.status === 400) { 27 | setErrors(axiosError.response?.data.validationErrors); 28 | } else { 29 | setGeneralError(axiosError.response.data.message); 30 | } 31 | } finally { 32 | setApiProgress(false); 33 | } 34 | }, [password, searchParams]); 35 | 36 | 37 | 38 | return { 39 | apiProgress, 40 | onChangePassword: (event) => { 41 | setPassword(event.target.value), 42 | setErrors({}) 43 | }, 44 | onChangePasswordRepeat: (event) => setPasswordRepeat(event.target.value), 45 | onSubmit, 46 | success, 47 | errors: { 48 | password: errors.password, 49 | passwordRepeat: password !== passwordRepeat ? 'Password mismatch' : '' 50 | }, 51 | generalError, 52 | disabled: password ? password !== passwordRepeat : true 53 | 54 | }; 55 | } -------------------------------------------------------------------------------- /frontend/src/pages/SignUp/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function signUp(body){ 4 | return http.post('/api/v1/users', body); 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/SignUp/index.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from "react"; 2 | import { signUp } from "./api"; 3 | import { Input } from "@/shared/components/Input"; 4 | import { useTranslation } from "react-i18next"; 5 | import { Alert } from "@/shared/components/Alert"; 6 | import { Spinner } from "@/shared/components/Spinner"; 7 | import { Button } from "@/shared/components/Button"; 8 | 9 | export function SignUp() { 10 | const [username, setUsername] = useState(); 11 | const [email, setEmail] = useState(); 12 | const [password, setPassword] = useState(); 13 | const [passwordRepeat, setPasswordRepeat] = useState(); 14 | const [apiProgress, setApiProgress] = useState(); 15 | const [successMessage, setSuccessMessage] = useState(); 16 | const [errors, setErrors] = useState({}); 17 | const [generalError, setGeneralError] = useState(); 18 | const { t } = useTranslation(); 19 | 20 | useEffect(() => { 21 | setErrors(function (lastErrors) { 22 | return { 23 | ...lastErrors, 24 | username: undefined, 25 | }; 26 | }); 27 | }, [username]); 28 | 29 | useEffect(() => { 30 | setErrors(function (lastErrors) { 31 | return { 32 | ...lastErrors, 33 | email: undefined, 34 | }; 35 | }); 36 | }, [email]); 37 | 38 | useEffect(() => { 39 | setErrors(function (lastErrors) { 40 | return { 41 | ...lastErrors, 42 | password: undefined, 43 | }; 44 | }); 45 | }, [password]); 46 | 47 | const onSubmit = async (event) => { 48 | event.preventDefault(); 49 | setSuccessMessage(); 50 | setGeneralError(); 51 | setApiProgress(true); 52 | 53 | try { 54 | const response = await signUp({ 55 | username, 56 | email, 57 | password, 58 | }); 59 | setSuccessMessage(response.data.message); 60 | } catch (axiosError) { 61 | if (axiosError.response?.data) { 62 | if (axiosError.response.data.status === 400) { 63 | setErrors(axiosError.response.data.validationErrors); 64 | } else { 65 | setGeneralError(axiosError.response.data.message); 66 | } 67 | } else { 68 | setGeneralError(t("genericError")); 69 | } 70 | } finally { 71 | setApiProgress(false); 72 | } 73 | }; 74 | 75 | const passwordRepeatError = useMemo(() => { 76 | if (password && password !== passwordRepeat) { 77 | return t("passwordMismatch"); 78 | } 79 | return ""; 80 | }, [password, passwordRepeat]); 81 | 82 | return ( 83 |
84 |
85 |
86 |
87 |

{t("signUp")}

88 |
89 |
90 | setUsername(event.target.value)} 95 | /> 96 | setEmail(event.target.value)} 101 | /> 102 | setPassword(event.target.value)} 107 | type="password" 108 | /> 109 | setPasswordRepeat(event.target.value)} 114 | type="password" 115 | /> 116 | {successMessage && {successMessage}} 117 | {generalError && {generalError}} 118 |
119 | 125 |
126 |
127 |
128 |
129 |
130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /frontend/src/pages/User/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function getUser(id) { 4 | return http.get(`/api/v1/users/${id}`); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/pages/User/components/ProfileCard/UserDeleteButton/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function deleteUser(id){ 4 | return http.delete(`/api/v1/users/${id}`) 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/User/components/ProfileCard/UserDeleteButton/index.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/shared/components/Button"; 2 | import { useUserDeleteButton } from "./useUserDeleteButton"; 3 | 4 | export function UserDeleteButton(){ 5 | const {apiProgress, onClick} = useUserDeleteButton(); 6 | 7 | return 8 | } -------------------------------------------------------------------------------- /frontend/src/pages/User/components/ProfileCard/UserDeleteButton/useUserDeleteButton.js: -------------------------------------------------------------------------------- 1 | import { useAuthDispatch, useAuthState } from "@/shared/state/context" 2 | import { deleteUser } from "./api" 3 | import { useCallback, useState } from "react"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | export function useUserDeleteButton(){ 7 | const [apiProgress, setApiProgress] = useState(false); 8 | const { id } = useAuthState(); 9 | const dispatch = useAuthDispatch() 10 | const navigate = useNavigate(); 11 | 12 | const onClick = useCallback(async () => { 13 | const result = confirm("Are you sure?") 14 | if(result) { 15 | setApiProgress(true); 16 | try { 17 | await deleteUser(id) 18 | dispatch({type: 'logout-success'}); 19 | navigate("/") 20 | } catch { 21 | 22 | } finally { 23 | setApiProgress(false); 24 | } 25 | } 26 | }, [id]) 27 | 28 | return { 29 | apiProgress, onClick 30 | } 31 | } -------------------------------------------------------------------------------- /frontend/src/pages/User/components/ProfileCard/UserEditForm.jsx: -------------------------------------------------------------------------------- 1 | import { useAuthDispatch, useAuthState } from "@/shared/state/context"; 2 | import { useState } from "react"; 3 | import { useTranslation } from "react-i18next"; 4 | import { updateUser } from "./api"; 5 | import { Input } from "@/shared/components/Input"; 6 | import { Alert } from "@/shared/components/Alert"; 7 | import { Button } from "@/shared/components/Button"; 8 | 9 | export function UserEditForm({ setEditMode, setTempImage }) { 10 | const authState = useAuthState(); 11 | const { t } = useTranslation(); 12 | const [newUsername, setNewUsername] = useState(authState.username); 13 | const [apiProgress, setApiProgress] = useState(false); 14 | const [errors, setErrors] = useState({}); 15 | const [generalError, setGeneralError] = useState(); 16 | const dispatch = useAuthDispatch(); 17 | const [newImage, setNewImage] = useState(); 18 | 19 | const onChangeUsername = (event) => { 20 | setNewUsername(event.target.value); 21 | setErrors(function (lastErrors) { 22 | return { 23 | ...lastErrors, 24 | username: undefined, 25 | }; 26 | }); 27 | }; 28 | 29 | const onClickCancel = () => { 30 | setEditMode(false); 31 | setNewUsername(authState.username); 32 | setNewImage(); 33 | setTempImage(); 34 | }; 35 | 36 | const onSelectImage = (event) => { 37 | setErrors(function (lastErrors) { 38 | return { 39 | ...lastErrors, 40 | image: undefined, 41 | }; 42 | }); 43 | if(event.target.files.length < 1) return; 44 | const file = event.target.files[0] 45 | const fileReader = new FileReader(); 46 | fileReader.onloadend = () => { 47 | const data = fileReader.result 48 | setNewImage(data); 49 | setTempImage(data); 50 | } 51 | fileReader.readAsDataURL(file); 52 | } 53 | 54 | const onSubmit = async (event) => { 55 | event.preventDefault(); 56 | setApiProgress(true); 57 | setErrors({}); 58 | setGeneralError(); 59 | try { 60 | const { data } = await updateUser(authState.id, { username: newUsername, image: newImage }); 61 | dispatch({ 62 | type: "user-update-success", 63 | data: { username: data.username, image: data.image }, 64 | }); 65 | setEditMode(false); 66 | } catch (axiosError) { 67 | if (axiosError.response?.data) { 68 | if (axiosError.response.data.status === 400) { 69 | setErrors(axiosError.response.data.validationErrors); 70 | } else { 71 | setGeneralError(axiosError.response.data.message); 72 | } 73 | } else { 74 | setGeneralError(t("genericError")); 75 | } 76 | } finally { 77 | setApiProgress(false); 78 | } 79 | }; 80 | return ( 81 |
82 | 88 | 94 | {generalError && {generalError}} 95 | 98 |
99 | 102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /frontend/src/pages/User/components/ProfileCard/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function updateUser(id, body){ 4 | return http.put(`/api/v1/users/${id}`, body) 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/User/components/ProfileCard/index.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/shared/components/Button"; 2 | import { useAuthState } from "@/shared/state/context"; 3 | import { useState } from "react"; 4 | import { ProfileImage } from "@/shared/components/ProfileImage"; 5 | import { UserEditForm } from "./UserEditForm"; 6 | import { UserDeleteButton } from "./UserDeleteButton"; 7 | 8 | export function ProfileCard({ user }) { 9 | const authState = useAuthState(); 10 | const [editMode, setEditMode] = useState(false); 11 | const [tempImage, setTempImage] = useState(); 12 | 13 | const isLoggedInUser = !editMode && authState.id === user.id; 14 | 15 | const visibleUsername = authState.id === user.id ? authState.username : user.username; 16 | 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 | {!editMode && {visibleUsername}} 24 | {isLoggedInUser && ( 25 | <> 26 | 27 |
28 | 29 | 30 | )} 31 | {editMode && } 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/pages/User/index.jsx: -------------------------------------------------------------------------------- 1 | import { getUser } from "./api"; 2 | import { Alert } from "@/shared/components/Alert"; 3 | import { Spinner } from "@/shared/components/Spinner"; 4 | import { useRouteParamApiRequest } from "@/shared/hooks/useRouteParamApiRequest"; 5 | import { ProfileCard } from "./components/ProfileCard"; 6 | 7 | export function User() { 8 | const { 9 | apiProgress, 10 | data: user, 11 | error, 12 | } = useRouteParamApiRequest("id", getUser); 13 | 14 | return ( 15 | <> 16 | {apiProgress && ( 17 | 18 | 19 | 20 | )} 21 | {user && } 22 | {error && {error}} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import {createBrowserRouter } from "react-router-dom"; 2 | 3 | import { Home } from "@/pages/Home"; 4 | import { SignUp } from "@/pages/SignUp"; 5 | import App from "@/App"; 6 | import { Activation } from "@/pages/Activation"; 7 | import { User } from "@/pages/User"; 8 | import { Login } from "@/pages/Login"; 9 | import { PasswordResetRequest } from "@/pages/PasswordReset/Request"; 10 | import { SetPassword } from "@/pages/PasswordReset/SetPassword"; 11 | 12 | export default createBrowserRouter([ 13 | { 14 | path: "/", 15 | Component: App, 16 | children: [ 17 | { 18 | path: "/", 19 | index: true, 20 | Component: Home, 21 | }, 22 | { 23 | path: "/signup", 24 | Component: SignUp 25 | }, 26 | { 27 | path: "/activation/:token", 28 | Component: Activation 29 | }, 30 | { 31 | path: "/user/:id", 32 | Component: User 33 | }, 34 | { 35 | path: '/login', 36 | Component: Login 37 | }, 38 | { 39 | path: "/password-reset/request", 40 | Component: PasswordResetRequest 41 | }, 42 | { 43 | path: "/password-reset/set", 44 | Component: SetPassword 45 | }, 46 | ] 47 | } 48 | ]) -------------------------------------------------------------------------------- /frontend/src/shared/components/Alert.jsx: -------------------------------------------------------------------------------- 1 | export function Alert(props) { 2 | const { children, styleType, center } = props; 3 | return ( 4 |
{children}
5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Button.jsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from "./Spinner"; 2 | 3 | export function Button({ 4 | apiProgress, 5 | disabled, 6 | children, 7 | onClick, 8 | styleType = "primary", 9 | type 10 | }) { 11 | return ( 12 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Input.jsx: -------------------------------------------------------------------------------- 1 | export function Input(props) { 2 | const { id, label, error, onChange, type, defaultValue } = props; 3 | 4 | return ( 5 |
6 | 9 | 16 |
{error}
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/shared/components/LanguageSelector.jsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | 3 | export function LanguageSelector() { 4 | const { i18n } = useTranslation(); 5 | 6 | const onSelectLanguage = (language) => { 7 | i18n.changeLanguage(language); 8 | localStorage.setItem("lang", language); 9 | }; 10 | return ( 11 | <> 12 | Turkce onSelectLanguage("tr")} 19 | > 20 | English onSelectLanguage("en")} 27 | > 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/shared/components/NavBar/api.js: -------------------------------------------------------------------------------- 1 | import http from "@/lib/http"; 2 | 3 | export function logout(){ 4 | return http.post("/api/v1/logout"); 5 | } -------------------------------------------------------------------------------- /frontend/src/shared/components/NavBar/index.jsx: -------------------------------------------------------------------------------- 1 | import logo from "@/assets/hoaxify.png"; 2 | import { useTranslation } from "react-i18next"; 3 | import { Link } from "react-router-dom"; 4 | import { useAuthDispatch, useAuthState } from "../../state/context"; 5 | import { ProfileImage } from "../ProfileImage"; 6 | import { logout } from "./api"; 7 | 8 | export function NavBar() { 9 | const { t } = useTranslation(); 10 | const authState = useAuthState(); 11 | const dispatch = useAuthDispatch(); 12 | 13 | const onClickLogout = async () => { 14 | try { 15 | await logout(); 16 | } catch { 17 | 18 | } finally { 19 | dispatch({type: 'logout-success'}); 20 | } 21 | } 22 | return ( 23 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/shared/components/ProfileImage.jsx: -------------------------------------------------------------------------------- 1 | import defaultProfileImage from "@/assets/profile.png"; 2 | 3 | export function ProfileImage({ width, tempImage, image }) { 4 | 5 | const profileImage = image ? `/assets/profile/${image}` : defaultProfileImage; 6 | 7 | return ( 8 | { 14 | target.src = defaultProfileImage 15 | }} 16 | /> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Spinner.jsx: -------------------------------------------------------------------------------- 1 | export function Spinner(props) { 2 | const { sm } = props; 3 | return ( 4 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/shared/hooks/useRouteParamApiRequest.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | export function useRouteParamApiRequest(param, httpFunction){ 5 | 6 | const params = useParams(); 7 | 8 | const pathParam = params[param]; 9 | 10 | const [apiProgress, setApiProgress] = useState(); 11 | const [data, setData] = useState(); 12 | const [error, setError] = useState(); 13 | 14 | useEffect(() => { 15 | async function sendRequest() { 16 | setApiProgress(true); 17 | try { 18 | const response = await httpFunction(pathParam); 19 | setData(response.data); 20 | } catch (axiosError) { 21 | setError(axiosError.response.data.message); 22 | } finally { 23 | setApiProgress(false); 24 | } 25 | } 26 | 27 | sendRequest(); 28 | }, [pathParam]); 29 | 30 | return { apiProgress, data, error }; 31 | 32 | } -------------------------------------------------------------------------------- /frontend/src/shared/state/context.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useReducer } from "react"; 2 | import { loadAuthState, storeAuthState } from "./storage"; 3 | 4 | export const AuthContext = createContext(); 5 | 6 | export const AuthDispatchContext = createContext(); 7 | 8 | export function useAuthState(){ 9 | return useContext(AuthContext); 10 | } 11 | 12 | export function useAuthDispatch(){ 13 | return useContext(AuthDispatchContext); 14 | } 15 | 16 | const authReducer = (authState, action) => { 17 | switch (action.type) { 18 | case "login-success": 19 | return action.data.user; 20 | case "logout-success": 21 | return { id: 0 }; 22 | case "user-update-success": 23 | return { 24 | ...authState, 25 | username: action.data.username, 26 | image: action.data.image 27 | } 28 | 29 | default: 30 | throw new Error(`unknown action: ${action.type}`); 31 | } 32 | }; 33 | 34 | export function AuthenticationContext({ children }) { 35 | const [authState, dispatch] = useReducer(authReducer, loadAuthState()); 36 | 37 | useEffect(() => { 38 | storeAuthState(authState); 39 | }, [authState]); 40 | 41 | return ( 42 | 43 | 44 | {children} 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/shared/state/storage.js: -------------------------------------------------------------------------------- 1 | export function storeAuthState(auth){ 2 | localStorage.setItem('auth', JSON.stringify(auth)); 3 | } 4 | 5 | export function loadAuthState(){ 6 | const defaultState = { id: 0 }; 7 | const authStateInStorage = localStorage.getItem('auth'); 8 | if(!authStateInStorage) return defaultState; 9 | try { 10 | return JSON.parse(authStateInStorage) 11 | } catch { 12 | return defaultState; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | $primary: #818; 2 | 3 | @import "node_modules/bootstrap/scss/bootstrap"; -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | import { fileURLToPath, URL } from "node:url"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react()], 9 | server: { 10 | proxy: { 11 | '/api': 'http://localhost:8080', 12 | '/assets': 'http://localhost:8080', 13 | } 14 | }, 15 | resolve: { 16 | alias: { 17 | "@": fileURLToPath(new URL("./src", import.meta.url)) 18 | } 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /ws/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | uploads** 35 | dev** -------------------------------------------------------------------------------- /ws/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basarbk/spring-n-react-tr-course/ee3303eed976c11bb0ce4ea1a6ce4ec5b5efd49d/ws/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /ws/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /ws/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /ws/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /ws/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.1.2 9 | 10 | 11 | com.hoaxify 12 | ws 13 | 0.0.1-SNAPSHOT 14 | ws 15 | Demo project for Spring Boot 16 | 17 | 17 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-mail 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-security 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-validation 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-web 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-devtools 44 | runtime 45 | true 46 | 47 | 48 | com.h2database 49 | h2 50 | runtime 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-test 55 | test 56 | 57 | 58 | org.springframework.security 59 | spring-security-test 60 | test 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-configuration-processor 65 | annotationProcessor 66 | 67 | 68 | org.apache.tika 69 | tika-core 70 | 2.8.0 71 | 72 | 73 | io.jsonwebtoken 74 | jjwt-api 75 | 0.11.5 76 | 77 | 78 | io.jsonwebtoken 79 | jjwt-impl 80 | 0.11.5 81 | runtime 82 | 83 | 84 | io.jsonwebtoken 85 | jjwt-jackson 86 | 0.11.5 87 | runtime 88 | 89 | 90 | 91 | 92 | 93 | 94 | org.springframework.boot 95 | spring-boot-maven-plugin 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/WsApplication.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws; 2 | 3 | import org.springframework.boot.CommandLineRunner; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.security.crypto.password.PasswordEncoder; 9 | 10 | import com.hoaxify.ws.user.User; 11 | import com.hoaxify.ws.user.UserRepository; 12 | 13 | @SpringBootApplication 14 | public class WsApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(WsApplication.class, args); 18 | } 19 | 20 | @Bean 21 | @Profile("dev") 22 | CommandLineRunner userCreator(UserRepository userRepository, PasswordEncoder passwordEncoder){ 23 | return (args) -> { 24 | var userInDB = userRepository.findByEmail("user1@mail.com"); 25 | if(userInDB != null) return; 26 | for(var i = 1; i <= 25;i++){ 27 | User user = new User(); 28 | user.setUsername("user"+i); 29 | user.setEmail("user"+i+"@mail.com"); 30 | user.setPassword(passwordEncoder.encode("P4ssword")); 31 | user.setActive(true); 32 | userRepository.save(user); 33 | } 34 | }; 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpHeaders; 5 | import org.springframework.http.ResponseCookie; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.CookieValue; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestHeader; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import com.hoaxify.ws.auth.dto.AuthResponse; 14 | import com.hoaxify.ws.auth.dto.Credentials; 15 | import com.hoaxify.ws.shared.GenericMessage; 16 | 17 | import jakarta.validation.Valid; 18 | 19 | @RestController 20 | public class AuthController { 21 | 22 | @Autowired 23 | AuthService authService; 24 | 25 | @PostMapping("/api/v1/auth") 26 | ResponseEntity handleAuthentication(@Valid @RequestBody Credentials creds) { 27 | var authResponse = authService.authenticate(creds); 28 | var cookie = ResponseCookie.from("hoax-token", authResponse.getToken().getToken()).path("/").httpOnly(true).build(); 29 | return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(authResponse); 30 | } 31 | 32 | @PostMapping("/api/v1/logout") 33 | ResponseEntity handleLogout(@RequestHeader(name="Authorization", required = false) String authorizationHeader, @CookieValue(name="hoax-token", required = false) String cookieValue){ 34 | var tokenWithPrefix = authorizationHeader; 35 | if(cookieValue != null){ 36 | tokenWithPrefix = "AnyPrefix " +cookieValue; 37 | } 38 | authService.logout(tokenWithPrefix); 39 | var cookie = ResponseCookie.from("hoax-token", "").path("/").maxAge(0).httpOnly(true).build(); 40 | return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(new GenericMessage("Logout success")); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/AuthService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.security.crypto.password.PasswordEncoder; 5 | import org.springframework.stereotype.Service; 6 | 7 | import com.hoaxify.ws.auth.dto.AuthResponse; 8 | import com.hoaxify.ws.auth.dto.Credentials; 9 | import com.hoaxify.ws.auth.exception.AuthenticationException; 10 | import com.hoaxify.ws.auth.token.Token; 11 | import com.hoaxify.ws.auth.token.TokenService; 12 | import com.hoaxify.ws.user.User; 13 | import com.hoaxify.ws.user.UserService; 14 | import com.hoaxify.ws.user.dto.UserDTO; 15 | 16 | @Service 17 | public class AuthService { 18 | 19 | @Autowired 20 | UserService userService; 21 | 22 | @Autowired 23 | PasswordEncoder passwordEncoder; 24 | 25 | @Autowired 26 | TokenService tokenService; 27 | 28 | public AuthResponse authenticate(Credentials creds) { 29 | User inDB = userService.findByEmail(creds.email()); 30 | if(inDB == null) throw new AuthenticationException(); 31 | if(!passwordEncoder.matches(creds.password(), inDB.getPassword())) throw new AuthenticationException(); 32 | Token token = tokenService.createToken(inDB, creds); 33 | AuthResponse authResponse = new AuthResponse(); 34 | authResponse.setToken(token); 35 | authResponse.setUser(new UserDTO(inDB)); 36 | return authResponse; 37 | 38 | } 39 | 40 | public void logout(String authorizationHeader) { 41 | tokenService.logout(authorizationHeader); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/dto/AuthResponse.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.dto; 2 | 3 | import com.hoaxify.ws.auth.token.Token; 4 | import com.hoaxify.ws.user.dto.UserDTO; 5 | 6 | public class AuthResponse { 7 | 8 | UserDTO user; 9 | 10 | Token token; 11 | 12 | public Token getToken() { 13 | return token; 14 | } 15 | 16 | public void setToken(Token token) { 17 | this.token = token; 18 | } 19 | 20 | public UserDTO getUser() { 21 | return user; 22 | } 23 | 24 | public void setUser(UserDTO user) { 25 | this.user = user; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/dto/Credentials.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.dto; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record Credentials(@Email String email, @NotBlank String password) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/exception/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.exception; 2 | 3 | import org.springframework.context.i18n.LocaleContextHolder; 4 | 5 | import com.hoaxify.ws.shared.Messages; 6 | 7 | public class AuthenticationException extends RuntimeException { 8 | 9 | public AuthenticationException(){ 10 | super(Messages.getMessageForLocale("hoaxify.auth.invalid.credentials", LocaleContextHolder.getLocale())); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/token/BasicAuthTokenService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.token; 2 | 3 | import java.util.Base64; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | 10 | import com.hoaxify.ws.auth.dto.Credentials; 11 | import com.hoaxify.ws.user.User; 12 | import com.hoaxify.ws.user.UserService; 13 | 14 | @Service 15 | @ConditionalOnProperty(name = "hoaxify.token-type", havingValue = "basic") 16 | public class BasicAuthTokenService implements TokenService { 17 | 18 | @Autowired 19 | UserService userService; 20 | 21 | @Autowired 22 | PasswordEncoder passwordEncoder; 23 | 24 | @Override 25 | public Token createToken(User user, Credentials creds) { 26 | String emailColonPassword = creds.email() + ":" + creds.password(); 27 | String token = Base64.getEncoder().encodeToString(emailColonPassword.getBytes()); 28 | return new Token("Basic", token); 29 | } 30 | 31 | @Override 32 | public User verifyToken(String authorizationHeader) { 33 | if(authorizationHeader == null) return null; 34 | var base64Encoded = authorizationHeader.split("Basic ")[1]; 35 | var decoded = new String(Base64.getDecoder().decode(base64Encoded)); 36 | var credentials = decoded.split(":"); 37 | var email = credentials[0]; 38 | var password = credentials[1]; 39 | User inDB = userService.findByEmail(email); 40 | if(inDB == null) return null; 41 | if(!passwordEncoder.matches(password, inDB.getPassword())) return null; 42 | return inDB; 43 | } 44 | 45 | @Override 46 | public void logout(String authorizationHeader) { 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/token/JwtTokenService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.token; 2 | 3 | import javax.crypto.SecretKey; 4 | 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.stereotype.Service; 7 | 8 | import com.fasterxml.jackson.core.JsonProcessingException; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import com.hoaxify.ws.auth.dto.Credentials; 11 | import com.hoaxify.ws.user.User; 12 | 13 | import io.jsonwebtoken.Claims; 14 | import io.jsonwebtoken.Jws; 15 | import io.jsonwebtoken.JwtException; 16 | import io.jsonwebtoken.JwtParser; 17 | import io.jsonwebtoken.Jwts; 18 | import io.jsonwebtoken.security.Keys; 19 | 20 | @Service 21 | @ConditionalOnProperty(name = "hoaxify.token-type", havingValue = "jwt") 22 | public class JwtTokenService implements TokenService{ 23 | 24 | SecretKey key = Keys.hmacShaKeyFor("secret-must-be-at-least-32-chars".getBytes()); 25 | 26 | ObjectMapper mapper = new ObjectMapper(); 27 | 28 | @Override 29 | public Token createToken(User user, Credentials creds) { 30 | TokenSubject tokenSubject = new TokenSubject(user.getId(), user.isActive()); 31 | try { 32 | String subject = mapper.writeValueAsString(tokenSubject); 33 | String token = Jwts.builder().setSubject(subject).signWith(key).compact(); 34 | return new Token("Bearer", token); 35 | } catch (JsonProcessingException e) { 36 | e.printStackTrace(); 37 | } 38 | return null; 39 | } 40 | 41 | @Override 42 | public User verifyToken(String authorizationHeader) { 43 | if(authorizationHeader == null) return null; 44 | var token = authorizationHeader.split(" ")[1]; 45 | JwtParser parser = Jwts.parserBuilder().setSigningKey(key).build(); 46 | try { 47 | Jws claims = parser.parseClaimsJws(token); 48 | var subject = claims.getBody().getSubject(); 49 | var tokenSubject = mapper.readValue(subject, TokenSubject.class); 50 | User user = new User(); 51 | user.setId(tokenSubject.id()); 52 | user.setActive(tokenSubject.active()); 53 | return user; 54 | } catch (JwtException | JsonProcessingException e) { 55 | e.printStackTrace(); 56 | } 57 | return null; 58 | } 59 | 60 | 61 | 62 | public static record TokenSubject(long id, boolean active) {} 63 | 64 | 65 | 66 | @Override 67 | public void logout(String authorizationHeader) { 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/token/OpaqueTokenService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.token; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.stereotype.Service; 9 | 10 | import com.hoaxify.ws.auth.dto.Credentials; 11 | import com.hoaxify.ws.user.User; 12 | 13 | @Service 14 | @ConditionalOnProperty(name = "hoaxify.token-type", havingValue = "opaque") 15 | public class OpaqueTokenService implements TokenService { 16 | 17 | @Autowired 18 | TokenRepository tokenRepository; 19 | 20 | @Override 21 | public Token createToken(User user, Credentials creds) { 22 | String randomValue = UUID.randomUUID().toString(); 23 | Token token = new Token(); 24 | token.setToken(randomValue); 25 | token.setUser(user); 26 | return tokenRepository.save(token); 27 | } 28 | 29 | @Override 30 | public User verifyToken(String authorizationHeader) { 31 | var tokenInDB = getToken(authorizationHeader); 32 | if(!tokenInDB.isPresent()) return null; 33 | return tokenInDB.get().getUser(); 34 | } 35 | 36 | @Override 37 | public void logout(String authorizationHeader) { 38 | var tokenInDB = getToken(authorizationHeader); 39 | if(!tokenInDB.isPresent()) return; 40 | tokenRepository.delete(tokenInDB.get()); 41 | } 42 | 43 | private Optional getToken(String authorizationHeader){ 44 | if(authorizationHeader == null) return Optional.empty(); 45 | var token = authorizationHeader.split(" ")[1]; 46 | return tokenRepository.findById(token); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/token/Token.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.token; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.hoaxify.ws.user.User; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.ManyToOne; 9 | import jakarta.persistence.Transient; 10 | 11 | @Entity 12 | public class Token { 13 | 14 | @Id 15 | String token; 16 | 17 | @Transient 18 | String prefix = "Bearer"; 19 | 20 | @JsonIgnore 21 | @ManyToOne 22 | User user; 23 | 24 | public Token(String prefix, String token) { 25 | this.prefix = prefix; 26 | this.token = token; 27 | } 28 | 29 | public Token(){} 30 | 31 | public String getToken() { 32 | return token; 33 | } 34 | 35 | public void setToken(String token) { 36 | this.token = token; 37 | } 38 | 39 | public String getPrefix() { 40 | return prefix; 41 | } 42 | 43 | public void setPrefix(String prefix) { 44 | this.prefix = prefix; 45 | } 46 | 47 | public User getUser() { 48 | return user; 49 | } 50 | 51 | public void setUser(User user) { 52 | this.user = user; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/token/TokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.token; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface TokenRepository extends JpaRepository { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/auth/token/TokenService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.auth.token; 2 | 3 | import com.hoaxify.ws.auth.dto.Credentials; 4 | import com.hoaxify.ws.user.User; 5 | 6 | public interface TokenService { 7 | 8 | public Token createToken(User user, Credentials creds); 9 | 10 | public User verifyToken(String authorizationHeader); 11 | 12 | public void logout(String authorizationHeader); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/configuration/AppUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.configuration; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | import org.springframework.security.core.userdetails.UserDetailsService; 6 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 7 | import org.springframework.stereotype.Service; 8 | 9 | import com.hoaxify.ws.user.User; 10 | import com.hoaxify.ws.user.UserService; 11 | 12 | @Service 13 | public class AppUserDetailsService implements UserDetailsService{ 14 | 15 | @Autowired 16 | UserService userService; 17 | 18 | @Override 19 | public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { 20 | User inDB = userService.findByEmail(email); 21 | if(inDB == null) { 22 | throw new UsernameNotFoundException(email + " is not found"); 23 | } 24 | return new CurrentUser(inDB); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/configuration/AuthEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.configuration; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.security.core.AuthenticationException; 8 | import org.springframework.security.web.AuthenticationEntryPoint; 9 | import org.springframework.web.servlet.HandlerExceptionResolver; 10 | 11 | import jakarta.servlet.ServletException; 12 | import jakarta.servlet.http.HttpServletRequest; 13 | import jakarta.servlet.http.HttpServletResponse; 14 | 15 | public class AuthEntryPoint implements AuthenticationEntryPoint{ 16 | 17 | @Autowired 18 | @Qualifier("handlerExceptionResolver") 19 | private HandlerExceptionResolver exceptionResolver; 20 | 21 | @Override 22 | public void commence(HttpServletRequest request, HttpServletResponse response, 23 | AuthenticationException authException) throws IOException, ServletException { 24 | exceptionResolver.resolveException(request, response, null, authException); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/configuration/CurrentUser.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.configuration; 2 | 3 | import java.util.Collection; 4 | 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.authority.AuthorityUtils; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | 9 | import com.hoaxify.ws.user.User; 10 | 11 | public class CurrentUser implements UserDetails { 12 | 13 | long id; 14 | 15 | String username; 16 | 17 | String password; 18 | 19 | boolean enabled; 20 | 21 | 22 | public CurrentUser(User user){ 23 | this.id = user.getId(); 24 | this.username = user.getUsername(); 25 | this.password = user.getPassword(); 26 | this.enabled = user.isActive(); 27 | } 28 | 29 | public long getId() { 30 | return id; 31 | } 32 | 33 | public void setId(long id) { 34 | this.id = id; 35 | } 36 | @Override 37 | public Collection getAuthorities() { 38 | return AuthorityUtils.createAuthorityList("ROLE_USER"); 39 | } 40 | 41 | @Override 42 | public String getPassword() { 43 | return this.password; 44 | } 45 | 46 | @Override 47 | public String getUsername() { 48 | return this.username; 49 | } 50 | 51 | @Override 52 | public boolean isAccountNonExpired() { 53 | return true; 54 | } 55 | 56 | @Override 57 | public boolean isAccountNonLocked() { 58 | return true; 59 | } 60 | 61 | @Override 62 | public boolean isCredentialsNonExpired() { 63 | return true; 64 | } 65 | 66 | @Override 67 | public boolean isEnabled() { 68 | return enabled; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/configuration/HoaxifyProperties.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.configuration; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @ConfigurationProperties(prefix = "hoaxify") 7 | @Configuration 8 | public class HoaxifyProperties { 9 | 10 | private Email email; 11 | 12 | private Client client; 13 | 14 | private Storage storage = new Storage(); 15 | 16 | private String tokenType; 17 | 18 | public String getTokenType() { 19 | return tokenType; 20 | } 21 | 22 | public void setTokenType(String tokenType) { 23 | this.tokenType = tokenType; 24 | } 25 | 26 | public Storage getStorage() { 27 | return storage; 28 | } 29 | 30 | public void setStorage(Storage storage) { 31 | this.storage = storage; 32 | } 33 | 34 | public Client getClient() { 35 | return client; 36 | } 37 | 38 | public void setClient(Client client) { 39 | this.client = client; 40 | } 41 | 42 | public Email getEmail() { 43 | return email; 44 | } 45 | 46 | public void setEmail(Email email) { 47 | this.email = email; 48 | } 49 | 50 | public static record Email( 51 | String username, 52 | String password, 53 | String host, 54 | int port, 55 | String from 56 | ){} 57 | 58 | public static record Client( 59 | String host 60 | ){} 61 | 62 | public static class Storage { 63 | String root = "uploads"; 64 | String profile = "profile"; 65 | 66 | public String getRoot() { 67 | return root; 68 | } 69 | public void setRoot(String root) { 70 | this.root = root; 71 | } 72 | 73 | public String getProfile() { 74 | return profile; 75 | } 76 | public void setProfile(String profile) { 77 | this.profile = profile; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/configuration/SecurityBeans.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.configuration; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 5 | import org.springframework.security.crypto.password.PasswordEncoder; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class SecurityBeans { 10 | 11 | @Bean 12 | PasswordEncoder passwordEncoder(){ 13 | return new BCryptPasswordEncoder(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/configuration/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.configuration; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.http.HttpMethod; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.web.SecurityFilterChain; 11 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 12 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 13 | 14 | @Configuration 15 | @EnableWebSecurity 16 | @EnableMethodSecurity(prePostEnabled = true) 17 | public class SecurityConfiguration { 18 | 19 | @Autowired 20 | TokenFilter tokenFilter; 21 | 22 | @Bean 23 | SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{ 24 | 25 | http.authorizeHttpRequests((authentication) -> 26 | authentication.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.PUT, "/api/v1/users/{id}")).authenticated() 27 | .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.DELETE, "/api/v1/users/{id}")).authenticated() 28 | .anyRequest().permitAll() 29 | ); 30 | http.httpBasic(httpBasic -> httpBasic.authenticationEntryPoint(new AuthEntryPoint())); 31 | 32 | http.csrf(csrf -> csrf.disable()); 33 | http.headers(headers -> headers.disable()); 34 | 35 | http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); 36 | 37 | return http.build(); 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/configuration/StaticResourceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.configuration; 2 | 3 | import java.io.File; 4 | import java.nio.file.Path; 5 | import java.nio.file.Paths; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.http.CacheControl; 13 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 14 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 15 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 16 | 17 | @Configuration 18 | @EnableWebMvc 19 | public class StaticResourceConfiguration implements WebMvcConfigurer { 20 | 21 | @Autowired 22 | HoaxifyProperties hoaxifyProperties; 23 | 24 | @Override 25 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 26 | String path = Paths.get(hoaxifyProperties.getStorage().getRoot()).toAbsolutePath().toString() + "/"; 27 | registry.addResourceHandler("/assets/**") 28 | .addResourceLocations("file:" + path) 29 | .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); 30 | } 31 | 32 | @Bean 33 | CommandLineRunner createStorageDirs(){ 34 | return args -> { 35 | createFolder(Paths.get(hoaxifyProperties.getStorage().getRoot())); 36 | createFolder(Paths.get(hoaxifyProperties.getStorage().getRoot(), hoaxifyProperties.getStorage().getProfile())); 37 | }; 38 | } 39 | 40 | private void createFolder(Path path){ 41 | File file = path.toFile(); 42 | boolean isFolderExist = file.exists() && file.isDirectory(); 43 | if(!isFolderExist) { 44 | file.mkdir(); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/configuration/TokenFilter.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.configuration; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.security.authentication.DisabledException; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.filter.OncePerRequestFilter; 13 | import org.springframework.web.servlet.HandlerExceptionResolver; 14 | 15 | import com.hoaxify.ws.auth.token.TokenService; 16 | import com.hoaxify.ws.user.User; 17 | 18 | import jakarta.servlet.FilterChain; 19 | import jakarta.servlet.ServletException; 20 | import jakarta.servlet.http.HttpServletRequest; 21 | import jakarta.servlet.http.HttpServletResponse; 22 | 23 | @Component 24 | public class TokenFilter extends OncePerRequestFilter{ 25 | 26 | @Autowired 27 | TokenService tokenService; 28 | 29 | @Autowired 30 | @Qualifier("handlerExceptionResolver") 31 | private HandlerExceptionResolver exceptionResolver; 32 | 33 | @Override 34 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 35 | throws ServletException, IOException { 36 | String tokenWithPrefix = getTokenWithPrefix(request); 37 | if(tokenWithPrefix != null) { 38 | User user = tokenService.verifyToken(tokenWithPrefix); 39 | if(user != null) { 40 | if(!user.isActive()) { 41 | exceptionResolver.resolveException(request, response, null, new DisabledException("User is disabled")); 42 | return; 43 | } 44 | CurrentUser currentUser = new CurrentUser(user); 45 | UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(currentUser, null, currentUser.getAuthorities()); 46 | authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 47 | SecurityContextHolder.getContext().setAuthentication(authentication); 48 | } 49 | } 50 | filterChain.doFilter(request, response); 51 | } 52 | 53 | private String getTokenWithPrefix(HttpServletRequest request) { 54 | var tokenWithPrefix = request.getHeader("Authorization"); 55 | var cookies = request.getCookies(); 56 | if(cookies == null) return tokenWithPrefix; 57 | for(var cookie: cookies){ 58 | if(!cookie.getName().equals("hoax-token")) continue; 59 | if(cookie.getValue() == null || cookie.getValue().isEmpty()) continue; 60 | return "AnyPrefix " + cookie.getValue(); 61 | } 62 | return tokenWithPrefix; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/email/EmailService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.email; 2 | 3 | import java.util.Properties; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.MessageSource; 7 | import org.springframework.context.i18n.LocaleContextHolder; 8 | import org.springframework.mail.javamail.JavaMailSenderImpl; 9 | import org.springframework.mail.javamail.MimeMessageHelper; 10 | import org.springframework.stereotype.Service; 11 | 12 | import com.hoaxify.ws.configuration.HoaxifyProperties; 13 | 14 | import jakarta.annotation.PostConstruct; 15 | import jakarta.mail.MessagingException; 16 | import jakarta.mail.internet.MimeMessage; 17 | 18 | @Service 19 | public class EmailService { 20 | 21 | JavaMailSenderImpl mailSender; 22 | 23 | @Autowired 24 | HoaxifyProperties hoaxifyProperties; 25 | 26 | @Autowired 27 | MessageSource messageSource; 28 | 29 | @PostConstruct 30 | public void initialize(){ 31 | this.mailSender = new JavaMailSenderImpl(); 32 | mailSender.setHost(hoaxifyProperties.getEmail().host()); 33 | mailSender.setPort(hoaxifyProperties.getEmail().port()); 34 | mailSender.setUsername(hoaxifyProperties.getEmail().username()); 35 | mailSender.setPassword(hoaxifyProperties.getEmail().password()); 36 | 37 | Properties properties = mailSender.getJavaMailProperties(); 38 | properties.put("mail.smtp.starttls.enable", "true"); 39 | 40 | } 41 | 42 | String activationEmail = """ 43 | 44 | 45 |

${title}

46 | ${clickHere} 47 | 48 | 49 | """; 50 | 51 | public void sendActivationEmail(String email, String activationToken) { 52 | var activationUrl = hoaxifyProperties.getClient().host() + "/activation/" + activationToken; 53 | var title = messageSource.getMessage("hoaxify.mail.user.created.title", null, LocaleContextHolder.getLocale()); 54 | var clickHere = messageSource.getMessage("hoaxify.mail.click.here", null, LocaleContextHolder.getLocale()); 55 | 56 | 57 | var mailBody = activationEmail 58 | .replace("${url}", activationUrl) 59 | .replace("${title}", title) 60 | .replace("${clickHere}", clickHere); 61 | 62 | MimeMessage mimeMessage = mailSender.createMimeMessage(); 63 | MimeMessageHelper message = new MimeMessageHelper(mimeMessage, "UTF-8"); 64 | try { 65 | message.setFrom(hoaxifyProperties.getEmail().from()); 66 | message.setTo(email); 67 | message.setSubject(title); 68 | message.setText(mailBody, true); 69 | } catch (MessagingException e) { 70 | e.printStackTrace(); 71 | } 72 | 73 | this.mailSender.send(mimeMessage); 74 | } 75 | 76 | public void sendPasswordResetEmail(String email, String passwordResetToken) { 77 | String passwordResetUrl = hoaxifyProperties.getClient().host() + "/password-reset/set?tk=" + passwordResetToken; 78 | MimeMessage mimeMessage = mailSender.createMimeMessage(); 79 | MimeMessageHelper message = new MimeMessageHelper(mimeMessage, "UTF-8"); 80 | var title = "Reset your password"; 81 | var clickHere = messageSource.getMessage("hoaxify.mail.click.here", null, LocaleContextHolder.getLocale()); 82 | var mailBody = activationEmail.replace("${url}", passwordResetUrl).replace("${title}", title).replace("${clickHere}", clickHere); 83 | try { 84 | message.setFrom(hoaxifyProperties.getEmail().from()); 85 | message.setTo(email); 86 | message.setSubject(title); 87 | message.setText(mailBody, true); 88 | } catch (MessagingException e) { 89 | e.printStackTrace(); 90 | } 91 | this.mailSender.send(mimeMessage); 92 | } 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/error/ApiError.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.error; 2 | 3 | import java.util.Date; 4 | import java.util.Map; 5 | 6 | import com.fasterxml.jackson.annotation.JsonInclude; 7 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 8 | 9 | @JsonInclude(value = Include.NON_NULL) 10 | public class ApiError { 11 | 12 | private int status; 13 | 14 | private String message; 15 | 16 | private String path; 17 | 18 | private long timestamp = new Date().getTime(); 19 | 20 | private Map validationErrors = null; 21 | 22 | public Map getValidationErrors() { 23 | return validationErrors; 24 | } 25 | 26 | public void setValidationErrors(Map validationErrors) { 27 | this.validationErrors = validationErrors; 28 | } 29 | 30 | public long getTimestamp() { 31 | return timestamp; 32 | } 33 | 34 | public void setTimestamp(long timestamp) { 35 | this.timestamp = timestamp; 36 | } 37 | 38 | public String getPath() { 39 | return path; 40 | } 41 | 42 | public void setPath(String path) { 43 | this.path = path; 44 | } 45 | 46 | public String getMessage() { 47 | return message; 48 | } 49 | 50 | public void setMessage(String message) { 51 | this.message = message; 52 | } 53 | 54 | public int getStatus() { 55 | return status; 56 | } 57 | 58 | public void setStatus(int status) { 59 | this.status = status; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/error/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.error; 2 | 3 | import java.util.stream.Collectors; 4 | 5 | import org.springframework.context.i18n.LocaleContextHolder; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.validation.FieldError; 8 | import org.springframework.web.bind.MethodArgumentNotValidException; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.RestControllerAdvice; 11 | 12 | import com.hoaxify.ws.auth.exception.AuthenticationException; 13 | import com.hoaxify.ws.shared.Messages; 14 | import com.hoaxify.ws.user.exception.ActivationNotificationException; 15 | import com.hoaxify.ws.user.exception.InvalidTokenException; 16 | import com.hoaxify.ws.user.exception.NotFoundException; 17 | import com.hoaxify.ws.user.exception.NotUniqueEmailException; 18 | 19 | import jakarta.servlet.http.HttpServletRequest; 20 | 21 | @RestControllerAdvice 22 | public class ErrorHandler { 23 | 24 | @ExceptionHandler({ 25 | MethodArgumentNotValidException.class, 26 | NotUniqueEmailException.class, 27 | ActivationNotificationException.class, 28 | InvalidTokenException.class, 29 | NotFoundException.class, 30 | AuthenticationException.class, 31 | }) 32 | ResponseEntity handleException(Exception exception, HttpServletRequest request){ 33 | ApiError apiError = new ApiError(); 34 | apiError.setPath(request.getRequestURI()); 35 | apiError.setMessage(exception.getMessage()); 36 | if(exception instanceof MethodArgumentNotValidException) { 37 | String message = Messages.getMessageForLocale("hoaxify.error.validation", LocaleContextHolder.getLocale()); 38 | apiError.setMessage(message); 39 | apiError.setStatus(400); 40 | var validationErrors = ((MethodArgumentNotValidException)exception).getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (existing, replacing) -> existing)); 41 | apiError.setValidationErrors(validationErrors); 42 | } else if (exception instanceof NotUniqueEmailException) { 43 | apiError.setStatus(400); 44 | apiError.setValidationErrors(((NotUniqueEmailException)exception).getValidationErrors()); 45 | } else if (exception instanceof ActivationNotificationException) { 46 | apiError.setStatus(502); 47 | } else if (exception instanceof InvalidTokenException) { 48 | apiError.setStatus(400); 49 | } else if (exception instanceof NotFoundException) { 50 | apiError.setStatus(404); 51 | } else if (exception instanceof AuthenticationException) { 52 | apiError.setStatus(401); 53 | } 54 | 55 | return ResponseEntity.status(apiError.getStatus()).body(apiError); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/error/GlobalErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.error; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.security.access.AccessDeniedException; 5 | import org.springframework.security.authentication.DisabledException; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 9 | 10 | import jakarta.servlet.http.HttpServletRequest; 11 | 12 | @ControllerAdvice 13 | public class GlobalErrorHandler extends ResponseEntityExceptionHandler { 14 | 15 | @ExceptionHandler({DisabledException.class, AccessDeniedException.class}) 16 | ResponseEntity handleDisabledException(Exception exception, HttpServletRequest request){ 17 | ApiError error = new ApiError(); 18 | error.setMessage(exception.getMessage()); 19 | error.setPath(request.getRequestURI()); 20 | if(exception instanceof DisabledException) { 21 | error.setStatus(401); 22 | } else if (exception instanceof AccessDeniedException){ 23 | error.setStatus(403); 24 | } 25 | return ResponseEntity.status(error.getStatus()).body(error); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/file/FileService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.file; 2 | 3 | import java.io.FileOutputStream; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.util.Base64; 10 | import java.util.UUID; 11 | 12 | import org.apache.tika.Tika; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | 16 | import com.hoaxify.ws.configuration.HoaxifyProperties; 17 | 18 | @Service 19 | public class FileService { 20 | 21 | @Autowired 22 | HoaxifyProperties hoaxifyProperties; 23 | 24 | Tika tika = new Tika(); 25 | 26 | public String saveBase64StringAsFile(String image) { 27 | String filename = UUID.randomUUID().toString(); 28 | 29 | Path path = getProfileImagePath(filename); 30 | try { 31 | OutputStream outputStream = new FileOutputStream(path.toFile()); 32 | outputStream.write(decodedImage(image)); 33 | outputStream.close(); 34 | return filename; 35 | } catch (IOException e) { 36 | e.printStackTrace(); 37 | } 38 | return null; 39 | 40 | } 41 | 42 | public String detectType(String value) { 43 | return tika.detect(decodedImage(value)); 44 | } 45 | 46 | private byte[] decodedImage(String encodedImage) { 47 | return Base64.getDecoder().decode(encodedImage.split(",")[1]); 48 | } 49 | 50 | public void deleteProfileImage(String image) { 51 | if(image == null) return; 52 | Path path = getProfileImagePath(image); 53 | try { 54 | Files.deleteIfExists(path); 55 | } catch (IOException e) { 56 | e.printStackTrace(); 57 | } 58 | } 59 | 60 | private Path getProfileImagePath(String filename){ 61 | return Paths.get(hoaxifyProperties.getStorage().getRoot(), hoaxifyProperties.getStorage().getProfile(), filename); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/shared/GenericMessage.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.shared; 2 | 3 | public record GenericMessage(String message) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/shared/Messages.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.shared; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.Locale; 5 | import java.util.ResourceBundle; 6 | 7 | public class Messages { 8 | 9 | public static String getMessageForLocale(String messageKey, Locale locale) { 10 | return ResourceBundle.getBundle("messages", locale).getString(messageKey); 11 | } 12 | 13 | public static String getMessageForLocale(String messageKey, Locale locale, Object... arguments) { 14 | String message = getMessageForLocale(messageKey, locale); 15 | return MessageFormat.format(message, arguments); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/User.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import com.hoaxify.ws.auth.token.Token; 7 | 8 | import jakarta.persistence.CascadeType; 9 | import jakarta.persistence.Entity; 10 | import jakarta.persistence.GeneratedValue; 11 | import jakarta.persistence.Id; 12 | import jakarta.persistence.Lob; 13 | import jakarta.persistence.OneToMany; 14 | import jakarta.persistence.Table; 15 | import jakarta.persistence.UniqueConstraint; 16 | 17 | @Entity 18 | @Table(name="users", uniqueConstraints = @UniqueConstraint(columnNames = {"email"})) 19 | public class User { 20 | 21 | @Id 22 | @GeneratedValue 23 | long id; 24 | 25 | String username; 26 | 27 | String email; 28 | 29 | @JsonIgnore 30 | String password; 31 | 32 | @JsonIgnore 33 | boolean active = false; 34 | 35 | @JsonIgnore 36 | String activationToken; 37 | 38 | @Lob 39 | String image; 40 | 41 | @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) 42 | List tokens; 43 | 44 | String passwordResetToken; 45 | 46 | public List getTokens() { 47 | return tokens; 48 | } 49 | 50 | public void setTokens(List tokens) { 51 | this.tokens = tokens; 52 | } 53 | 54 | public String getPasswordResetToken() { 55 | return passwordResetToken; 56 | } 57 | 58 | public void setPasswordResetToken(String passwordResetToken) { 59 | this.passwordResetToken = passwordResetToken; 60 | } 61 | 62 | public String getImage() { 63 | return image; 64 | } 65 | 66 | public void setImage(String image) { 67 | this.image = image; 68 | } 69 | 70 | public String getActivationToken() { 71 | return activationToken; 72 | } 73 | 74 | public void setActivationToken(String activationToken) { 75 | this.activationToken = activationToken; 76 | } 77 | 78 | public boolean isActive() { 79 | return active; 80 | } 81 | 82 | public void setActive(boolean active) { 83 | this.active = active; 84 | } 85 | 86 | public long getId() { 87 | return id; 88 | } 89 | 90 | public void setId(long id) { 91 | this.id = id; 92 | } 93 | 94 | public String getUsername() { 95 | return username; 96 | } 97 | 98 | public void setUsername(String username) { 99 | this.username = username; 100 | } 101 | 102 | public String getEmail() { 103 | return email; 104 | } 105 | 106 | public void setEmail(String email) { 107 | this.email = email; 108 | } 109 | 110 | public String getPassword() { 111 | return password; 112 | } 113 | 114 | public void setPassword(String password) { 115 | this.password = password; 116 | } 117 | 118 | 119 | 120 | } 121 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/UserController.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.i18n.LocaleContextHolder; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 9 | import org.springframework.web.bind.annotation.DeleteMapping; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PatchMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.PutMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import com.hoaxify.ws.configuration.CurrentUser; 19 | import com.hoaxify.ws.shared.GenericMessage; 20 | import com.hoaxify.ws.shared.Messages; 21 | import com.hoaxify.ws.user.dto.PasswordResetRequest; 22 | import com.hoaxify.ws.user.dto.PasswordUpdate; 23 | import com.hoaxify.ws.user.dto.UserCreate; 24 | import com.hoaxify.ws.user.dto.UserDTO; 25 | import com.hoaxify.ws.user.dto.UserUpdate; 26 | 27 | import jakarta.validation.Valid; 28 | 29 | @RestController 30 | public class UserController { 31 | 32 | @Autowired 33 | UserService userService; 34 | 35 | @PostMapping("/api/v1/users") 36 | GenericMessage createUser(@Valid @RequestBody UserCreate user){ 37 | userService.save(user.toUser()); 38 | String message = Messages.getMessageForLocale("hoaxify.create.user.success.message", LocaleContextHolder.getLocale()); 39 | return new GenericMessage(message); 40 | } 41 | 42 | @PatchMapping("/api/v1/users/{token}/active") 43 | GenericMessage activateUser(@PathVariable String token){ 44 | userService.activateUser(token); 45 | String message = Messages.getMessageForLocale("hoaxify.activate.user.success.message", LocaleContextHolder.getLocale()); 46 | return new GenericMessage(message); 47 | } 48 | 49 | @GetMapping("/api/v1/users") 50 | Page getUsers(Pageable page, @AuthenticationPrincipal CurrentUser currentUser){ 51 | return userService.getUsers(page, currentUser).map(UserDTO::new); 52 | } 53 | 54 | @GetMapping("/api/v1/users/{id}") 55 | UserDTO getUserById(@PathVariable long id){ 56 | return new UserDTO(userService.getUser(id)); 57 | } 58 | 59 | @PutMapping("/api/v1/users/{id}") 60 | @PreAuthorize("#id == principal.id") 61 | UserDTO updateUser(@PathVariable long id, @Valid @RequestBody UserUpdate userUpdate){ 62 | return new UserDTO(userService.updateUser(id, userUpdate)); 63 | } 64 | 65 | @DeleteMapping("/api/v1/users/{id}") 66 | @PreAuthorize("#id == principal.id") 67 | GenericMessage deleteUser(@PathVariable long id){ 68 | userService.deleteUser(id); 69 | return new GenericMessage("User is deleted"); 70 | } 71 | 72 | @PostMapping("/api/v1/users/password-reset") 73 | GenericMessage passwordResetRequest(@Valid @RequestBody PasswordResetRequest passwordResetRequest) { 74 | userService.handleResetRequest(passwordResetRequest); 75 | return new GenericMessage("Check your email address to reset your password"); 76 | } 77 | 78 | @PatchMapping("/api/v1/users/{token}/password") 79 | GenericMessage setPassword(@PathVariable String token, @Valid @RequestBody PasswordUpdate passwordUpdate){ 80 | userService.updatePassword(token, passwordUpdate); 81 | return new GenericMessage("Password updated successfully"); 82 | 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface UserRepository extends JpaRepository { 8 | 9 | User findByEmail(String email); 10 | 11 | User findByActivationToken(String token); 12 | 13 | Page findByIdNot(long id, Pageable page); 14 | 15 | User findByPasswordResetToken(String passwordResetToken); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/UserService.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.dao.DataIntegrityViolationException; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.mail.MailException; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.stereotype.Service; 12 | 13 | import com.hoaxify.ws.configuration.CurrentUser; 14 | import com.hoaxify.ws.email.EmailService; 15 | import com.hoaxify.ws.file.FileService; 16 | import com.hoaxify.ws.user.dto.PasswordResetRequest; 17 | import com.hoaxify.ws.user.dto.PasswordUpdate; 18 | import com.hoaxify.ws.user.dto.UserUpdate; 19 | import com.hoaxify.ws.user.exception.ActivationNotificationException; 20 | import com.hoaxify.ws.user.exception.InvalidTokenException; 21 | import com.hoaxify.ws.user.exception.NotFoundException; 22 | import com.hoaxify.ws.user.exception.NotUniqueEmailException; 23 | 24 | import jakarta.transaction.Transactional; 25 | 26 | @Service 27 | public class UserService { 28 | 29 | @Autowired 30 | UserRepository userRepository; 31 | 32 | @Autowired 33 | PasswordEncoder passwordEncoder; 34 | 35 | @Autowired 36 | EmailService emailService; 37 | 38 | @Autowired 39 | FileService fileService; 40 | 41 | @Transactional(rollbackOn = MailException.class) 42 | public void save(User user){ 43 | try { 44 | user.setPassword(passwordEncoder.encode(user.getPassword())); 45 | user.setActivationToken(UUID.randomUUID().toString()); 46 | userRepository.saveAndFlush(user); 47 | emailService.sendActivationEmail(user.getEmail(), user.getActivationToken()); 48 | } catch (DataIntegrityViolationException ex){ 49 | throw new NotUniqueEmailException(); 50 | } catch (MailException ex) { 51 | throw new ActivationNotificationException(); 52 | } 53 | } 54 | 55 | public void activateUser(String token) { 56 | User inDB = userRepository.findByActivationToken(token); 57 | if(inDB == null) { 58 | throw new InvalidTokenException(); 59 | } 60 | inDB.setActive(true); 61 | inDB.setActivationToken(null); 62 | userRepository.save(inDB); 63 | } 64 | 65 | public Page getUsers(Pageable page, CurrentUser currentUser) { 66 | if(currentUser == null) { 67 | return userRepository.findAll(page); 68 | } 69 | return userRepository.findByIdNot(currentUser.getId(), page); 70 | } 71 | 72 | public User getUser(long id) { 73 | return userRepository.findById(id).orElseThrow(() -> new NotFoundException(id)); 74 | } 75 | 76 | public User findByEmail(String email) { 77 | return userRepository.findByEmail(email); 78 | } 79 | 80 | public User updateUser(long id, UserUpdate userUpdate) { 81 | User inDB = getUser(id); 82 | inDB.setUsername(userUpdate.username()); 83 | if(userUpdate.image() != null) { 84 | String fileName = fileService.saveBase64StringAsFile(userUpdate.image()); 85 | fileService.deleteProfileImage(inDB.getImage()); 86 | inDB.setImage(fileName); 87 | } 88 | return userRepository.save(inDB); 89 | } 90 | 91 | public void deleteUser(long id) { 92 | User inDB = getUser(id); 93 | if(inDB.getImage() != null) { 94 | fileService.deleteProfileImage(inDB.getImage()); 95 | } 96 | userRepository.delete(inDB); 97 | } 98 | 99 | public void handleResetRequest(PasswordResetRequest passwordResetRequest) { 100 | User inDB = findByEmail(passwordResetRequest.email()); 101 | if(inDB == null) throw new NotFoundException(0); 102 | inDB.setPasswordResetToken(UUID.randomUUID().toString()); 103 | this.userRepository.save(inDB); 104 | this.emailService.sendPasswordResetEmail(inDB.getEmail(), inDB.getPasswordResetToken()); 105 | } 106 | 107 | public void updatePassword(String token, PasswordUpdate passwordUpdate) { 108 | User inDB = userRepository.findByPasswordResetToken(token); 109 | if(inDB == null) { 110 | throw new InvalidTokenException(); 111 | } 112 | inDB.setPasswordResetToken(null); 113 | inDB.setPassword(passwordEncoder.encode(passwordUpdate.password())); 114 | inDB.setActive(true); 115 | userRepository.save(inDB); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/dto/PasswordResetRequest.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.dto; 2 | 3 | import jakarta.validation.constraints.Email; 4 | 5 | public record PasswordResetRequest(@Email String email) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/dto/PasswordUpdate.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.dto; 2 | import jakarta.validation.constraints.Pattern; 3 | import jakarta.validation.constraints.Size; 4 | 5 | public record PasswordUpdate( 6 | @Size(min=8, max=255) 7 | @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$", message="{hoaxify.constraint.password.pattern}") 8 | String password) { 9 | 10 | } -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/dto/UserCreate.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.dto; 2 | 3 | import com.hoaxify.ws.user.User; 4 | import com.hoaxify.ws.user.validation.UniqueEmail; 5 | 6 | import jakarta.validation.constraints.Email; 7 | import jakarta.validation.constraints.NotBlank; 8 | import jakarta.validation.constraints.Pattern; 9 | import jakarta.validation.constraints.Size; 10 | 11 | public record UserCreate( 12 | @NotBlank(message = "{hoaxify.constraint.username.notblank}") 13 | @Size(min = 4, max=255) 14 | String username, 15 | 16 | @NotBlank 17 | @Email 18 | @UniqueEmail 19 | String email, 20 | 21 | @Size(min = 8, max=255) 22 | @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$", message = "{hoaxify.constraint.password.pattern}") 23 | String password 24 | ) { 25 | 26 | public User toUser(){ 27 | User user = new User(); 28 | user.setEmail(email); 29 | user.setUsername(username); 30 | user.setPassword(password); 31 | return user; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.dto; 2 | 3 | import com.hoaxify.ws.user.User; 4 | 5 | public class UserDTO { 6 | 7 | long id; 8 | 9 | String username; 10 | 11 | String email; 12 | 13 | String image; 14 | 15 | public UserDTO(User user){ 16 | setId(user.getId()); 17 | setUsername(user.getUsername()); 18 | setEmail(user.getEmail()); 19 | setImage(user.getImage()); 20 | } 21 | 22 | public String getImage() { 23 | return this.image; 24 | } 25 | 26 | public void setImage(String image) { 27 | this.image = image; 28 | } 29 | 30 | public long getId() { 31 | return id; 32 | } 33 | 34 | public void setId(long id) { 35 | this.id = id; 36 | } 37 | 38 | public String getUsername() { 39 | return username; 40 | } 41 | 42 | public void setUsername(String username) { 43 | this.username = username; 44 | } 45 | 46 | public String getEmail() { 47 | return email; 48 | } 49 | 50 | public void setEmail(String email) { 51 | this.email = email; 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/dto/UserUpdate.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.dto; 2 | 3 | import com.hoaxify.ws.user.validation.FileType; 4 | 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.Size; 7 | 8 | public record UserUpdate( 9 | @NotBlank(message = "{hoaxify.constraint.username.notblank}") 10 | @Size(min = 4, max=255) 11 | String username, 12 | 13 | @FileType(types = {"jpeg", "png"}) 14 | String image 15 | ) { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/exception/ActivationNotificationException.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.exception; 2 | 3 | import org.springframework.context.i18n.LocaleContextHolder; 4 | 5 | import com.hoaxify.ws.shared.Messages; 6 | 7 | public class ActivationNotificationException extends RuntimeException{ 8 | 9 | public ActivationNotificationException(){ 10 | super(Messages.getMessageForLocale("hoaxify.create.user.email.failure", LocaleContextHolder.getLocale())); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/exception/InvalidTokenException.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.exception; 2 | 3 | import org.springframework.context.i18n.LocaleContextHolder; 4 | 5 | import com.hoaxify.ws.shared.Messages; 6 | 7 | public class InvalidTokenException extends RuntimeException{ 8 | 9 | public InvalidTokenException(){ 10 | super(Messages.getMessageForLocale("hoaxify.activate.user.invalid.token", LocaleContextHolder.getLocale())); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.exception; 2 | 3 | import org.springframework.context.i18n.LocaleContextHolder; 4 | 5 | import com.hoaxify.ws.shared.Messages; 6 | 7 | public class NotFoundException extends RuntimeException { 8 | 9 | public NotFoundException(long id){ 10 | super(Messages.getMessageForLocale("hoaxify.user.not.found", LocaleContextHolder.getLocale(), id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/exception/NotUniqueEmailException.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.exception; 2 | 3 | import java.util.Collections; 4 | import java.util.Map; 5 | 6 | import org.springframework.context.i18n.LocaleContextHolder; 7 | 8 | import com.hoaxify.ws.shared.Messages; 9 | 10 | public class NotUniqueEmailException extends RuntimeException{ 11 | 12 | public NotUniqueEmailException() { 13 | super(Messages.getMessageForLocale("hoaxify.error.validation", LocaleContextHolder.getLocale())); 14 | } 15 | 16 | public Map getValidationErrors(){ 17 | return Collections.singletonMap("email", Messages.getMessageForLocale("hoaxify.constraint.email.notunique", LocaleContextHolder.getLocale())); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/validation/FileType.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.validation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import jakarta.validation.Constraint; 9 | import jakarta.validation.Payload; 10 | 11 | @Constraint(validatedBy = FileTypeValidator.class) 12 | @Target({ ElementType.FIELD }) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface FileType { 15 | 16 | String message() default "Only {types} are allowed"; 17 | 18 | Class[] groups() default { }; 19 | 20 | Class[] payload() default { }; 21 | 22 | String[] types(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/validation/FileTypeValidator.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.validation; 2 | 3 | import java.util.Arrays; 4 | import java.util.stream.Collectors; 5 | 6 | import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | 9 | import com.hoaxify.ws.file.FileService; 10 | 11 | import jakarta.validation.ConstraintValidator; 12 | import jakarta.validation.ConstraintValidatorContext; 13 | 14 | public class FileTypeValidator implements ConstraintValidator { 15 | 16 | @Autowired 17 | FileService fileService; 18 | 19 | String[] types; 20 | 21 | @Override 22 | public void initialize(FileType constraintAnnotation) { 23 | this.types = constraintAnnotation.types(); 24 | } 25 | 26 | @Override 27 | public boolean isValid(String value, ConstraintValidatorContext context) { 28 | if(value == null || value.isEmpty()) return true; 29 | String type = fileService.detectType(value); 30 | for(String validType : types) { 31 | if(type.contains(validType)) return true; 32 | } 33 | 34 | String validTypes = Arrays.stream(types).collect(Collectors.joining(", ")); 35 | context.disableDefaultConstraintViolation(); 36 | HibernateConstraintValidatorContext hibernateConstraintValidatorContext= context.unwrap(HibernateConstraintValidatorContext.class); 37 | hibernateConstraintValidatorContext.addMessageParameter("types", validTypes); 38 | hibernateConstraintValidatorContext.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addConstraintViolation(); 39 | return false; 40 | } 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/validation/UniqueEmail.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.validation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import jakarta.validation.Constraint; 9 | import jakarta.validation.Payload; 10 | 11 | @Constraint(validatedBy = UniqueEmailValidator.class) 12 | @Target({ ElementType.FIELD }) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface UniqueEmail { 15 | 16 | String message() default "{hoaxify.constraint.email.notunique}"; 17 | 18 | Class[] groups() default { }; 19 | 20 | Class[] payload() default { }; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ws/src/main/java/com/hoaxify/ws/user/validation/UniqueEmailValidator.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws.user.validation; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import com.hoaxify.ws.user.User; 6 | import com.hoaxify.ws.user.UserRepository; 7 | 8 | import jakarta.validation.ConstraintValidator; 9 | import jakarta.validation.ConstraintValidatorContext; 10 | 11 | public class UniqueEmailValidator implements ConstraintValidator { 12 | 13 | @Autowired 14 | UserRepository userRepository; 15 | 16 | @Override 17 | public boolean isValid(String value, ConstraintValidatorContext context) { 18 | User inDB = userRepository.findByEmail(value); 19 | return inDB == null; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ws/src/main/resources/ValidationMessages.properties: -------------------------------------------------------------------------------- 1 | hoaxify.constraint.username.notblank = Username cannot be blank 2 | hoaxify.constraint.email.notunique = E-mail in use 3 | hoaxify.constraint.password.pattern = Must have uppercase, lowercase letters and number -------------------------------------------------------------------------------- /ws/src/main/resources/ValidationMessages_tr.properties: -------------------------------------------------------------------------------- 1 | hoaxify.constraint.username.notblank = Kullanici adi bos olamaz 2 | hoaxify.constraint.email.notunique = E-posta adresi kullaniliyor 3 | hoaxify.constraint.password.pattern = Buyuk harf, kucuk harf ve sayilardan olusmali -------------------------------------------------------------------------------- /ws/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.generate-unique-name=false 2 | hoaxify.email.username=isaac.goyette73@ethereal.email 3 | hoaxify.email.password=${HOAXIFY_EMAIL_PASSWORD} 4 | hoaxify.email.host=smtp.ethereal.email 5 | hoaxify.email.port=587 6 | hoaxify.email.from=noreply@my-app.com 7 | hoaxify.client.host=http://localhost:5173 8 | spring.profiles.active=dev 9 | logging.level.org.springframework.security=DEBUG 10 | hoaxify.token-type=opaque 11 | #--- 12 | spring.config.activate.on-profile=production 13 | hoaxify.client.host=http://hoaxify.com 14 | #--- 15 | spring.config.activate.on-profile=dev 16 | hoaxify.storage.root=uploads-dev 17 | spring.datasource.url=jdbc:h2:file:./dev.db 18 | spring.jpa.hibernate.ddl-auto=update 19 | spring.datasource.username=sa 20 | spring.datasource.password= -------------------------------------------------------------------------------- /ws/src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | hoaxify.constraint.email.notunique = E-mail in use 2 | hoaxify.create.user.success.message = Please check your mailbox to activate your account 3 | hoaxify.error.validation = Validation error 4 | hoaxify.create.user.email.failure = Account activation e-mail couldn't be delivered. Please try again 5 | hoaxify.mail.user.created.title = Activate your Account 6 | hoaxify.mail.click.here=Click here 7 | hoaxify.activate.user.invalid.token = Invalid activation token 8 | hoaxify.activate.user.success.message = Account is activated 9 | hoaxify.user.not.found= User with ID {0} does not exist 10 | hoaxify.auth.invalid.credentials = Invalid credentials -------------------------------------------------------------------------------- /ws/src/main/resources/messages_tr.properties: -------------------------------------------------------------------------------- 1 | hoaxify.constraint.email.notunique = E-posta adresi kullaniliyor 2 | hoaxify.create.user.success.message = Kullanicinizi aktiflestirmek icin lutfen mailinizi kontrol edin 3 | hoaxify.error.validation = Dogrulama hatasi 4 | hoaxify.create.user.email.failure = Hesap aktiflestirme e-postasini gonderemedik, lutfen tekrar deneyin 5 | hoaxify.mail.user.created.title = Hesab\u0131n\u0131z\u0131 aktifle\u015ftirin 6 | hoaxify.mail.click.here = Buraya t\u0131klay\u0131n 7 | hoaxify.activate.user.invalid.token = Aktiflestirme degeri gecersiz 8 | hoaxify.activate.user.success.message = Hesabiniz aktiflestirildi 9 | hoaxify.user.not.found= {0} numarali kullanici bulunamadi 10 | hoaxify.auth.invalid.credentials = Hatali bilgiler girdiniz -------------------------------------------------------------------------------- /ws/src/test/java/com/hoaxify/ws/WsApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.hoaxify.ws; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class WsApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------