├── 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 |
34 | {userPage.content.map((user) => {
35 | return ;
36 | })}
37 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
onSelectLanguage("tr")}
19 | >
20 |
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 extends GrantedAuthority> 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 extends Payload>[] 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 extends Payload>[] 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 |
--------------------------------------------------------------------------------