├── .gitignore ├── src └── main │ ├── vue │ ├── src │ │ ├── boot │ │ │ ├── .gitkeep │ │ │ ├── global-components.js │ │ │ └── i18n.js │ │ ├── rest │ │ │ ├── constants.js │ │ │ ├── endpoints.js │ │ │ ├── apiClient.js │ │ │ └── utils.js │ │ ├── i18n │ │ │ ├── index.js │ │ │ └── en-US │ │ │ │ └── index.js │ │ ├── settings │ │ │ ├── projectSettings.js │ │ │ └── encryptionSetting.js │ │ ├── App.vue │ │ ├── pages │ │ │ ├── Users │ │ │ │ ├── service │ │ │ │ │ └── userService.js │ │ │ │ └── UserPage.vue │ │ │ ├── ErrorNotFound.vue │ │ │ ├── Login │ │ │ │ ├── service │ │ │ │ │ └── authService.js │ │ │ │ └── LoginPage.vue │ │ │ └── Register │ │ │ │ └── RegisterPage.vue │ │ ├── utils │ │ │ ├── env.js │ │ │ ├── auth.js │ │ │ ├── cache │ │ │ │ ├── index.js │ │ │ │ ├── memory.js │ │ │ │ ├── storageCache.js │ │ │ │ └── persistent.js │ │ │ ├── cipher.js │ │ │ └── is.js │ │ ├── assets │ │ │ ├── edit.svg │ │ │ ├── endless-constellation.svg │ │ │ └── quasar-logo-vertical.svg │ │ ├── stores │ │ │ ├── store-flag.d.ts │ │ │ ├── useRouterConfig.js │ │ │ ├── index.js │ │ │ └── user.js │ │ ├── components │ │ │ ├── page │ │ │ │ └── FormPage.vue │ │ │ └── EssentialLink.vue │ │ ├── enums │ │ │ └── cacheEnums.js │ │ ├── router │ │ │ ├── routes.js │ │ │ └── index.js │ │ ├── css │ │ │ ├── quasar.variables.scss │ │ │ └── app.scss │ │ └── layouts │ │ │ └── MainLayout.vue │ ├── .env │ ├── .npmrc │ ├── quasar.testing.json │ ├── public │ │ ├── favicon.ico │ │ └── icons │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-96x96.png │ │ │ └── favicon-128x128.png │ ├── .eslintignore │ ├── babel.config.js │ ├── quasar.extensions.json │ ├── .editorconfig │ ├── .gitignore │ ├── jsconfig.json │ ├── postcss.config.js │ ├── vitest.config.js │ ├── index.html │ ├── package.json │ ├── .eslintrc.js │ └── quasar.config.js │ ├── java │ └── com │ │ └── manunin │ │ └── auth │ │ ├── model │ │ ├── ERole.java │ │ ├── Privilege.java │ │ ├── Role.java │ │ └── User.java │ │ ├── secutiry │ │ ├── exception │ │ │ ├── ExpiredTokenException.java │ │ │ ├── AuthMethodNotSupportedException.java │ │ │ └── RestAuthenticationFailureHandler.java │ │ ├── jwt │ │ │ ├── JwtPair.java │ │ │ ├── JwtAuthenticationToken.java │ │ │ ├── RefreshJwtAuthenticationToken.java │ │ │ ├── TokenAuthenticationProvider.java │ │ │ ├── RefreshTokenAuthenticationProvider.java │ │ │ ├── TokenAuthenticationFilter.java │ │ │ ├── RefreshTokenAuthenticationFilter.java │ │ │ └── JwtTokenProvider.java │ │ ├── matcher │ │ │ └── SkipPathRequestMatcher.java │ │ ├── rest │ │ │ └── LoginAuthenticationSuccessHandler.java │ │ ├── login │ │ │ ├── LoginAuthenticationProvider.java │ │ │ └── LoginAuthenticationFilter.java │ │ └── oauth2 │ │ │ └── Oauth2AuthenticationSuccessHandler.java │ │ ├── AuthorizationService.java │ │ ├── repository │ │ ├── RoleRepository.java │ │ └── UserRepository.java │ │ ├── dto │ │ ├── RefreshTokenDTO.java │ │ ├── SigninDTO.java │ │ └── SignupDTO.java │ │ ├── service │ │ ├── UserService.java │ │ ├── DatabaseUserDetailService.java │ │ ├── UserDetailsImpl.java │ │ └── UserServiceImpl.java │ │ ├── exception │ │ ├── ServiceException.java │ │ ├── ErrorCode.java │ │ ├── ErrorResponse.java │ │ └── ErrorResponseHandler.java │ │ ├── utils │ │ └── JsonUtils.java │ │ ├── mapper │ │ └── UserMapper.java │ │ ├── configuration │ │ ├── AuthenticationManagerConfiguration.java │ │ └── SecurityConfiguration.java │ │ └── controller │ │ └── AuthController.java │ └── resources │ ├── docker-compose.yml │ ├── changelog │ ├── changelog-master.xml │ └── 1.0.0 │ │ ├── 20221024_replica_identity_add_user_roles_table.xml │ │ ├── 20221024_replica_identity_add_user_spaces_table.xml │ │ ├── 20221016_replica_identity_add_roles_priveleges_table.xml │ │ ├── changelog.xml │ │ ├── 20220416_create_user_role_table.xml │ │ ├── 20220416_create_roles_privileges_table.xml │ │ ├── 20220416_create_roles_table.xml │ │ ├── 20220416_create_privilege_table.xml │ │ └── 20220415_create_user_table.xml │ └── application.yaml ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /src/main/vue/src/boot/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/vue/.env: -------------------------------------------------------------------------------- 1 | VUE_ROUTER_MODE=history 2 | VUE_ROUTER_BASE=/ 3 | -------------------------------------------------------------------------------- /src/main/vue/src/rest/constants.js: -------------------------------------------------------------------------------- 1 | export const JWS_TOKEN_EXPIRED = 3; 2 | -------------------------------------------------------------------------------- /src/main/vue/.npmrc: -------------------------------------------------------------------------------- 1 | # pnpm-related options 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false 4 | -------------------------------------------------------------------------------- /src/main/vue/quasar.testing.json: -------------------------------------------------------------------------------- 1 | { 2 | "unit-vitest": { 3 | "runnerCommand": "vitest run" 4 | } 5 | } -------------------------------------------------------------------------------- /src/main/vue/src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import enUS from './en-US' 2 | 3 | export default { 4 | 'en-US': enUS 5 | } 6 | -------------------------------------------------------------------------------- /src/main/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manunin/auth-module/HEAD/src/main/vue/public/favicon.ico -------------------------------------------------------------------------------- /src/main/vue/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /src-capacitor 3 | /src-cordova 4 | /.quasar 5 | /node_modules 6 | .eslintrc.js 7 | -------------------------------------------------------------------------------- /src/main/vue/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', {targets: {node: 'current'}}]], 3 | }; 4 | -------------------------------------------------------------------------------- /src/main/vue/public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manunin/auth-module/HEAD/src/main/vue/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/main/vue/public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manunin/auth-module/HEAD/src/main/vue/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/main/vue/public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manunin/auth-module/HEAD/src/main/vue/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /src/main/vue/quasar.extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "@quasar/testing-unit-vitest": { 3 | "options": [ 4 | "scripts" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /src/main/vue/public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manunin/auth-module/HEAD/src/main/vue/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /src/main/vue/src/settings/projectSettings.js: -------------------------------------------------------------------------------- 1 | import {CacheTypeEnum} from "src/enums/cacheEnums"; 2 | 3 | const settings = { 4 | permissionCacheType: CacheTypeEnum.LOCAL 5 | } 6 | 7 | export default settings; 8 | -------------------------------------------------------------------------------- /src/main/vue/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/main/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/main/vue/src/rest/endpoints.js: -------------------------------------------------------------------------------- 1 | export default { 2 | login: '/auth/signin', 3 | loginWithGoogle: '/oauth2/authorization/google', 4 | register: '/auth/signup', 5 | refreshToken: '/auth/refreshToken', 6 | usersCount: '/auth/users/count', 7 | } 8 | -------------------------------------------------------------------------------- /src/main/vue/src/pages/Users/service/userService.js: -------------------------------------------------------------------------------- 1 | import {loadData} from 'src/rest/utils'; 2 | import endpoints from 'src/rest/endpoints'; 3 | const getUsersCount = () => { 4 | return loadData(endpoints.usersCount); 5 | } 6 | 7 | export default { getUsersCount }; 8 | -------------------------------------------------------------------------------- /src/main/vue/src/utils/env.js: -------------------------------------------------------------------------------- 1 | import pkg from '../../package.json'; 2 | 3 | export function getStorageShortName() { 4 | return `${process.env.GLOBAL_APP_NAME}${`__${pkg.version}`}__`.toUpperCase(); 5 | } 6 | 7 | export function isDevMode() { 8 | return process.env.NODE_ENV === 'development'; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/vue/src/assets/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/vue/src/boot/global-components.js: -------------------------------------------------------------------------------- 1 | import { boot } from 'quasar/wrappers'; 2 | import FormPage from "components/page/FormPage.vue"; 3 | 4 | // "async" is optional; 5 | // more info on params: https://v2.quasar.dev/quasar-cli/boot-files 6 | export default boot(async ({app}) => { 7 | app.component('form-page', FormPage); 8 | }) 9 | -------------------------------------------------------------------------------- /src/main/vue/src/stores/store-flag.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED, 3 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING 4 | import "quasar/dist/types/feature-flag"; 5 | 6 | declare module "quasar/dist/types/feature-flag" { 7 | interface QuasarFeatureFlags { 8 | store: true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/model/ERole.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.model; 2 | 3 | public enum ERole { 4 | ADMIN("ADMIN"), 5 | USER("User role"); 6 | 7 | final String name; 8 | 9 | ERole(final String name) { 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Use postgres/example user/password credentials 2 | version: '3.9' 3 | 4 | services: 5 | 6 | db: 7 | image: postgres 8 | restart: always 9 | # set shared memory limit when using docker-compose 10 | shm_size: 128mb 11 | environment: 12 | POSTGRES_PASSWORD: example 13 | ports: 14 | - 5433:5432 -------------------------------------------------------------------------------- /src/main/vue/src/components/page/FormPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/vue/src/boot/i18n.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import messages from 'src/i18n'; 3 | 4 | export default ({ app }) => { 5 | // Create I18n instance 6 | const i18n = createI18n({ 7 | locale: 'en-US', 8 | legacy: false, 9 | globalInjection: true, 10 | messages 11 | }) 12 | 13 | // Tell app to use the I18n instance 14 | app.use(i18n) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/exception/ExpiredTokenException.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.exception; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | 5 | public class ExpiredTokenException extends AuthenticationException { 6 | 7 | public ExpiredTokenException(final String token, final String msg, final Throwable t) { 8 | super(msg, t); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/AuthorizationService.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class AuthorizationService { 8 | public static void main(String[] args) { 9 | SpringApplication.run(AuthorizationService.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/exception/AuthMethodNotSupportedException.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.exception; 2 | 3 | import org.springframework.security.authentication.AuthenticationServiceException; 4 | 5 | public class AuthMethodNotSupportedException extends AuthenticationServiceException { 6 | public AuthMethodNotSupportedException(final String msg) { 7 | super(msg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/vue/src/enums/cacheEnums.js: -------------------------------------------------------------------------------- 1 | export const TOKEN_KEY = 'TOKEN__'; 2 | export const REFRESH_TOKEN_KEY = 'REFRESH_TOKEN__'; 3 | export const USER_INFO_KEY = 'USER_INFO__'; 4 | export const LOCK_INFO_KEY = 'LOCK_INFO__'; 5 | export const APP_LOCAL_CACHE_KEY = 'APP_LOCAL_CACHE__'; 6 | export const APP_SESSION_CACHE_KEY = 'APP_SESSION_CACHE__'; 7 | 8 | export const CacheTypeEnum = { 9 | SESSION: 'SESSION', 10 | LOCAL: 'LOCAL' 11 | }; 12 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/repository/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.repository; 2 | 3 | import com.manunin.auth.model.Role; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface RoleRepository extends JpaRepository { 11 | Optional findByName(String name); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/changelog/changelog-master.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/vue/src/settings/encryptionSetting.js: -------------------------------------------------------------------------------- 1 | import { isDevMode } from 'src/utils/env'; 2 | 3 | // System default cache time, in seconds 4 | export const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7; 5 | 6 | // aes encryption key 7 | export const cacheCipher = { 8 | key: '_11111011001111@', 9 | iv: '@11100110001111_', 10 | }; 11 | 12 | // Whether the system cache is encrypted using aes 13 | export const enableStorageEncryption = !isDevMode(); 14 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/dto/RefreshTokenDTO.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public class RefreshTokenDTO { 7 | 8 | @NotBlank 9 | @Schema(example = "refreshToken", description = "Refresh token") 10 | private String refreshToken; 11 | 12 | public String getRefreshToken() { 13 | return refreshToken; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/vue/src/stores/useRouterConfig.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useRouterConfig = defineStore('routerConfig', { 4 | state: () => ({ 5 | usePageTransition: false, 6 | essentialLinks: [ 7 | { 8 | title: 'Users', 9 | caption: 'Users list', 10 | icon: 'receipt_long', 11 | showOnLoggedOut: false, 12 | link: '/users', 13 | root: '/users' 14 | } 15 | ] 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/main/vue/src/pages/Users/UserPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ `User count is ${userCount}` }} 4 | 5 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.service; 2 | 3 | import com.manunin.auth.exception.ServiceException; 4 | import com.manunin.auth.model.User; 5 | 6 | public interface UserService { 7 | User addUser(User user) throws ServiceException; 8 | boolean existsByUsername(String name); 9 | boolean existsByEmail(String email); 10 | User findByUsername(String username) throws ServiceException; 11 | User findByEmail(String email) throws ServiceException; 12 | Long getUsersCount(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/jwt/JwtPair.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.jwt; 2 | 3 | public class JwtPair { 4 | 5 | private final String token; 6 | private final String refreshToken; 7 | 8 | public JwtPair(final String token, final String refreshToken) { 9 | this.token = token; 10 | this.refreshToken = refreshToken; 11 | } 12 | 13 | public String getToken() { 14 | return token; 15 | } 16 | 17 | public String getRefreshToken() { 18 | return refreshToken; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/vue/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | /dist 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # Log files 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Editor directories and files 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | -------------------------------------------------------------------------------- /src/main/vue/src/stores/index.js: -------------------------------------------------------------------------------- 1 | import { store } from 'quasar/wrappers'; 2 | import { createPinia } from 'pinia'; 3 | 4 | /* 5 | * If not building with SSR mode, you can 6 | * directly export the Store instantiation; 7 | * 8 | * The function below can be async too; either use 9 | * async/await or return a Promise which resolves 10 | * with the Store instance. 11 | */ 12 | 13 | export default store((/* { ssrContext } */) => { 14 | const pinia = createPinia() 15 | 16 | // You can add Pinia plugins here 17 | // pinia.use(SomePiniaPlugin) 18 | 19 | return pinia; 20 | }) 21 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.repository; 2 | 3 | import com.manunin.auth.model.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface UserRepository extends JpaRepository { 11 | Optional findByUsername(String username); 12 | Optional findByEmail(String email); 13 | Boolean existsByUsername(String username); 14 | Boolean existsByEmail(String email); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/exception/ServiceException.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.exception; 2 | 3 | public class ServiceException extends Exception { 4 | private ErrorCode errorCode; 5 | 6 | public ServiceException () { 7 | super(); 8 | } 9 | 10 | public ServiceException(ErrorCode errorCode) { 11 | this.errorCode = errorCode; 12 | } 13 | 14 | public ServiceException(ErrorCode errorCode, String message) { 15 | super(message); 16 | this.errorCode = errorCode; 17 | } 18 | 19 | public ErrorCode getErrorCode() { 20 | return errorCode; 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/main/vue/src/router/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: '/', 4 | component: () => import('layouts/MainLayout.vue'), 5 | children: [ 6 | { 7 | path: '/users', 8 | component: () => import('pages/Users/UserPage.vue'), 9 | }, 10 | { path: '/login', component: () => import('pages/Login/LoginPage.vue') }, 11 | { path: '/register', component: () => import('pages/Register/RegisterPage.vue') } 12 | ] 13 | }, 14 | 15 | // Always leave this as last one, 16 | // but you can also remove it 17 | { 18 | path: '/:catchAll(.*)*', 19 | component: () => import('pages/ErrorNotFound.vue') 20 | } 21 | ] 22 | 23 | export default routes 24 | -------------------------------------------------------------------------------- /src/main/vue/src/pages/ErrorNotFound.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 6 | 7 | 8 | 9 | Oops. Nothing here... 10 | 11 | 12 | 21 | 22 | 23 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /src/main/vue/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "src/*": [ 6 | "src/*" 7 | ], 8 | "app/*": [ 9 | "*" 10 | ], 11 | "components/*": [ 12 | "src/components/*" 13 | ], 14 | "layouts/*": [ 15 | "src/layouts/*" 16 | ], 17 | "pages/*": [ 18 | "src/pages/*" 19 | ], 20 | "assets/*": [ 21 | "src/assets/*" 22 | ], 23 | "boot/*": [ 24 | "src/boot/*" 25 | ], 26 | "stores/*": [ 27 | "src/stores/*" 28 | ], 29 | "vue$": [ 30 | "node_modules/vue/dist/vue.runtime.esm-bundler.js" 31 | ] 32 | } 33 | }, 34 | "exclude": [ 35 | "dist", 36 | ".quasar", 37 | "node_modules" 38 | ] 39 | } -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/model/Privilege.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.model; 2 | 3 | import jakarta.persistence.*; 4 | 5 | @Entity 6 | @Table(name="privileges") 7 | public class Privilege { 8 | 9 | @Id 10 | @GeneratedValue(strategy = GenerationType.IDENTITY) 11 | private long id; 12 | private String name; 13 | 14 | public Privilege(final long id, final String name) { 15 | this.id = id; 16 | this.name = name; 17 | } 18 | 19 | public Privilege() { 20 | } 21 | 22 | public long getId() { 23 | return id; 24 | } 25 | 26 | public void setId(long id) { 27 | this.id = id; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public void setName(String name) { 35 | this.name = name; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/dto/SigninDTO.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public class SigninDTO { 7 | 8 | @NotBlank 9 | @Schema(example = "username", description = "User name") 10 | private String username; 11 | 12 | @NotBlank 13 | @Schema(example = "password", description = "User password") 14 | private String password; 15 | 16 | public String getUsername() { 17 | return username; 18 | } 19 | 20 | public void setUsername(String username) { 21 | this.username = username; 22 | } 23 | 24 | public String getPassword() { 25 | return password; 26 | } 27 | 28 | public void setPassword(String password) { 29 | this.password = password; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/exception/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.exception; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | import org.springframework.http.HttpStatus; 5 | 6 | public enum ErrorCode { 7 | GENERAL(1, HttpStatus.INTERNAL_SERVER_ERROR), 8 | AUTHENTICATION(2, HttpStatus.UNAUTHORIZED), 9 | JWT_TOKEN_EXPIRED(3, HttpStatus.UNAUTHORIZED), 10 | BAD_REQUEST_PARAMS(10, HttpStatus.BAD_REQUEST), 11 | ACCESS_DENIED(20, HttpStatus.FORBIDDEN); 12 | 13 | private final int code; 14 | private HttpStatus status; 15 | 16 | ErrorCode(final int code, final HttpStatus status) { 17 | this.code = code; 18 | this.status = status; 19 | } 20 | 21 | @JsonValue 22 | public int getCode() { 23 | return code; 24 | } 25 | 26 | public HttpStatus getStatus() { 27 | return status; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/vue/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import { Persistent } from 'src/utils/cache/persistent'; 2 | import { CacheTypeEnum, TOKEN_KEY } from 'src/enums/cacheEnums'; 3 | import projectSetting from 'src/settings/projectSettings'; 4 | 5 | const { permissionCacheType } = projectSetting; 6 | const isLocal = permissionCacheType === CacheTypeEnum.LOCAL; 7 | 8 | export function getToken() { 9 | return getAuthCache(TOKEN_KEY); 10 | } 11 | 12 | export function getAuthCache(key) { 13 | const fn = isLocal ? Persistent.getLocal : Persistent.getSession; 14 | return fn(key); 15 | } 16 | 17 | export function setAuthCache(key, value) { 18 | const fn = isLocal ? Persistent.setLocal : Persistent.setSession; 19 | return fn(key, value, true); 20 | } 21 | 22 | export function clearAuthCache(immediate = true) { 23 | const fn = isLocal ? Persistent.clearLocal : Persistent.clearSession; 24 | return fn(immediate); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/vue/src/css/quasar.variables.scss: -------------------------------------------------------------------------------- 1 | // Quasar SCSS (& Sass) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary : #1976D2; 16 | $secondary : #26A69A; 17 | $accent : #9C27B0; 18 | 19 | $dark : #1D1D1D; 20 | $dark-page : #121212; 21 | 22 | $positive : #21BA45; 23 | $negative : #C10015; 24 | $info : #31CCEC; 25 | $warning : #F2C037; 26 | -------------------------------------------------------------------------------- /src/main/vue/postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // https://github.com/michael-ciniawsky/postcss-load-config 3 | 4 | module.exports = { 5 | plugins: [ 6 | // https://github.com/postcss/autoprefixer 7 | require('autoprefixer')({ 8 | overrideBrowserslist: [ 9 | 'last 4 Chrome versions', 10 | 'last 4 Firefox versions', 11 | 'last 4 Edge versions', 12 | 'last 4 Safari versions', 13 | 'last 4 Android versions', 14 | 'last 4 ChromeAndroid versions', 15 | 'last 4 FirefoxAndroid versions', 16 | 'last 4 iOS versions' 17 | ] 18 | }) 19 | 20 | // https://github.com/elchininet/postcss-rtlcss 21 | // If you want to support RTL css, then 22 | // 1. yarn/npm install postcss-rtlcss 23 | // 2. optionally set quasar.config.js > framework > lang to an RTL language 24 | // 3. uncomment the following line: 25 | // require('postcss-rtlcss') 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/main/vue/src/css/app.scss: -------------------------------------------------------------------------------- 1 | .main-layout { 2 | background-color: #f5f5f5; 3 | height: 100vh; 4 | 5 | & .q-drawer { 6 | background-color: $dark; 7 | color: #ffffffb3; 8 | } 9 | 10 | &__manu-button { 11 | padding: 3px; 12 | } 13 | 14 | & .q-toolbar__title { 15 | padding-left: 23px; 16 | } 17 | } 18 | 19 | .flex-grow-0 { 20 | flex-grow: 0; 21 | } 22 | 23 | .flex-grow-1 { 24 | flex-grow: 1; 25 | } 26 | 27 | 28 | .login-form { 29 | padding: 20px; 30 | border-radius: 10px; 31 | background: linear-gradient(to top, #f2f2f2, #1976d2); 32 | box-shadow: 0 0 10px rgb(25, 118, 210); 33 | 34 | & .login-name { 35 | color: white; 36 | font-weight: bold; 37 | top: 20% 38 | } 39 | 40 | & .register-name { 41 | top: 10%; 42 | color: white; 43 | font-weight: bold; 44 | } 45 | } 46 | 47 | 48 | @media (min-width: 600px) { 49 | .q-dialog__inner--minimized > div { 50 | max-width: unset; 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/utils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.utils; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | import java.io.IOException; 6 | import java.io.PrintWriter; 7 | import java.io.Reader; 8 | 9 | public class JsonUtils { 10 | 11 | public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 12 | 13 | public static T fromReader(Reader reader, Class clazz) { 14 | try { 15 | return reader != null ? OBJECT_MAPPER.readValue(reader, clazz) : null; 16 | } catch (IOException e) { 17 | throw new IllegalArgumentException("Invalid request payload", e); 18 | } 19 | } 20 | 21 | public static void writeValue(PrintWriter writer, T val) { 22 | try { 23 | OBJECT_MAPPER.writeValue(writer, val); 24 | } catch (IOException e) { 25 | throw new IllegalArgumentException("Invalid response payload", e); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/vue/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import { quasar, transformAssetUrls } from '@quasar/vite-plugin'; 4 | import jsconfigPaths from 'vite-jsconfig-paths'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | test: { 9 | globals: true, 10 | environment: 'happy-dom', 11 | setupFiles: 'test/vitest/setup-file.js', 12 | include: [ 13 | // Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__' 14 | // Matches all files with extension 'js', 'jsx', 'ts' and 'tsx' 15 | 'src/**/*.vitest.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', 16 | 'test/vitest/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', 17 | ], 18 | }, 19 | plugins: [ 20 | vue({ 21 | template: { transformAssetUrls }, 22 | }), 23 | quasar({ 24 | sassVariables: 'src/quasar-variables.scss', 25 | }), 26 | jsconfigPaths(), 27 | ], 28 | }); 29 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/20221024_replica_identity_add_user_roles_table.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | alter table user_roles REPLICA IDENTITY FULL; 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/20221024_replica_identity_add_user_spaces_table.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | alter table users_spaces REPLICA IDENTITY FULL; 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/20221016_replica_identity_add_roles_priveleges_table.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | alter table roles_privileges REPLICA IDENTITY FULL; 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/changelog.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/vue/src/utils/cache/index.js: -------------------------------------------------------------------------------- 1 | import { getStorageShortName } from 'src/utils/env'; 2 | import { createStorage as create } from './storageCache'; 3 | import { enableStorageEncryption, DEFAULT_CACHE_TIME } from 'src/settings/encryptionSetting'; 4 | 5 | const createOptions = (storage, options) => { 6 | return { 7 | // No encryption in debug mode 8 | hasEncrypt: enableStorageEncryption, 9 | storage, 10 | prefixKey: getStorageShortName(), 11 | ...options, 12 | }; 13 | }; 14 | 15 | export const WebStorage = create(createOptions(sessionStorage)); 16 | 17 | export const createStorage = (storage = sessionStorage, options= {}) => { 18 | return create(createOptions(storage, options)); 19 | }; 20 | 21 | export const createSessionStorage = (options = {}) => { 22 | return createStorage(sessionStorage, { ...options, timeout: DEFAULT_CACHE_TIME }); 23 | }; 24 | 25 | export const createLocalStorage = (options= {}) => { 26 | return createStorage(localStorage, { ...options, timeout: DEFAULT_CACHE_TIME }); 27 | }; 28 | 29 | export default WebStorage; 30 | -------------------------------------------------------------------------------- /src/main/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/matcher/SkipPathRequestMatcher.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.matcher; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 5 | import org.springframework.security.web.util.matcher.OrRequestMatcher; 6 | import org.springframework.security.web.util.matcher.RequestMatcher; 7 | import org.springframework.util.Assert; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | public class SkipPathRequestMatcher implements RequestMatcher { 13 | private final OrRequestMatcher matchers; 14 | 15 | public SkipPathRequestMatcher(final List pathsToSkip) { 16 | Assert.notNull(pathsToSkip, "List of paths to skip is required."); 17 | List m = pathsToSkip.stream().map(AntPathRequestMatcher::new).collect(Collectors.toList()); 18 | matchers = new OrRequestMatcher(m); 19 | } 20 | 21 | @Override 22 | public boolean matches(final HttpServletRequest request) { 23 | return !matchers.matches(request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/vue/src/stores/user.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import {REFRESH_TOKEN_KEY, TOKEN_KEY} from "src/enums/cacheEnums"; 3 | import { getAuthCache, setAuthCache } from 'src/utils/auth'; 4 | 5 | export const useUserStore = defineStore('user', { 6 | state: () => ({ 7 | token: undefined, 8 | refreshToken: undefined 9 | }), 10 | actions: { 11 | login (data) { 12 | this.setToken(data?.token) 13 | this.setRefreshToken(data?.refreshToken) 14 | }, 15 | logout () { 16 | this.setToken(undefined); 17 | }, 18 | setToken(info) { 19 | this.token = info ? info : ''; // for null or undefined value 20 | setAuthCache(TOKEN_KEY, info); 21 | }, 22 | setRefreshToken(info) { 23 | this.refreshToken = info ? info : ''; // for null or undefined value 24 | setAuthCache(REFRESH_TOKEN_KEY, info); 25 | } 26 | }, 27 | getters: { 28 | getToken: (state) => { 29 | return state.token || getAuthCache(TOKEN_KEY) 30 | }, 31 | getRefreshToken: (state) => { 32 | return state.refreshToken || getAuthCache(REFRESH_TOKEN_KEY) 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/jwt/JwtAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.jwt; 2 | 3 | import org.springframework.security.authentication.AbstractAuthenticationToken; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | 6 | public class JwtAuthenticationToken extends AbstractAuthenticationToken { 7 | private String rawAccessToken; 8 | 9 | public JwtAuthenticationToken(final String rawAccessToken) { 10 | super(null); 11 | this.rawAccessToken = rawAccessToken; 12 | setAuthenticated(false); 13 | } 14 | 15 | public JwtAuthenticationToken(final UserDetails userDetails) { 16 | super(userDetails.getAuthorities()); 17 | super.setAuthenticated(true); 18 | super.eraseCredentials(); 19 | } 20 | 21 | @Override 22 | public Object getCredentials() { 23 | return rawAccessToken; 24 | } 25 | 26 | @Override 27 | public Object getPrincipal() { 28 | return null; 29 | } 30 | 31 | @Override 32 | public void eraseCredentials() { 33 | super.eraseCredentials(); 34 | this.rawAccessToken = null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/jwt/RefreshJwtAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.jwt; 2 | 3 | import org.springframework.security.authentication.AbstractAuthenticationToken; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | 6 | public class RefreshJwtAuthenticationToken extends AbstractAuthenticationToken { 7 | private String rawAccessToken; 8 | 9 | public RefreshJwtAuthenticationToken(String rawAccessToken) { 10 | super(null); 11 | this.rawAccessToken = rawAccessToken; 12 | setAuthenticated(false); 13 | } 14 | 15 | public RefreshJwtAuthenticationToken(UserDetails userDetails) { 16 | super(userDetails.getAuthorities()); 17 | super.setAuthenticated(true); 18 | super.eraseCredentials(); 19 | } 20 | 21 | @Override 22 | public Object getCredentials() { 23 | return rawAccessToken; 24 | } 25 | 26 | @Override 27 | public Object getPrincipal() { 28 | return null; 29 | } 30 | 31 | @Override 32 | public void eraseCredentials() { 33 | super.eraseCredentials(); 34 | this.rawAccessToken = null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/vue/src/components/EssentialLink.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | 12 | 13 | 14 | {{ title }} 15 | 16 | 17 | 18 | 19 | 56 | 58 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: ${APP_PORT:9090} 3 | servlet: 4 | context-path: /api/v1 5 | spring: 6 | datasource: 7 | driver-class-name: org.postgresql.Driver 8 | url: ${POSTGRES_URL:jdbc:postgresql://localhost:5433/postgres} 9 | username: ${POSTGRES_USER} 10 | password: ${POSTGRES_PASSWORD} 11 | liquibase: 12 | change-log: /changelog/changelog-master.xml 13 | enabled: true 14 | security: 15 | oauth2: 16 | client: 17 | registration: 18 | google: 19 | clientId: ${GOOGLE_CLIENT_ID} 20 | clientSecret: ${GOOGLE_CLIENT_SECRET} 21 | redirectUri: "http://localhost:9090/api/v1/login/oauth2/code/google" 22 | logging: 23 | level: 24 | org: 25 | springframework: 26 | # web: DEBUG 27 | # security: DEBUG 28 | springdoc: 29 | api-docs: 30 | path: /api-docs 31 | swagger-ui: 32 | path: /swagger-ui.html 33 | security: 34 | # JWT parameters 35 | jwt: 36 | secret: ${JWT_SECRET} 37 | tokenExpirationTime: ${JWT_TOKEN_EXPIRATION_TIME:3600} # in seconds (default: 1h) 38 | refreshTokenExpirationTime: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME:1209600000} # in seconds (default: 14 days) -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/service/DatabaseUserDetailService.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.service; 2 | 3 | import com.manunin.auth.model.User; 4 | import com.manunin.auth.repository.UserRepository; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | @Service 12 | public class DatabaseUserDetailService implements UserDetailsService { 13 | 14 | private final UserRepository userRepository; 15 | 16 | public DatabaseUserDetailService(final UserRepository userRepository) { 17 | this.userRepository = userRepository; 18 | } 19 | 20 | @Override 21 | @Transactional 22 | public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { 23 | User user = userRepository.findByUsername(username) 24 | .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); 25 | return UserDetailsImpl.build(user); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/vue/src/utils/cipher.js: -------------------------------------------------------------------------------- 1 | import { encrypt, decrypt } from 'crypto-js/aes'; 2 | import UTF8, { parse } from 'crypto-js/enc-utf8'; 3 | import pkcs7 from 'crypto-js/pad-pkcs7'; 4 | import ECB from 'crypto-js/mode-ecb'; 5 | import md5 from 'crypto-js/md5'; 6 | import Base64 from 'crypto-js/enc-base64'; 7 | 8 | export class AesEncryption { 9 | constructor(opt = {}) { 10 | const { key, iv } = opt; 11 | if (key) { 12 | this.key = parse(key); 13 | } 14 | if (iv) { 15 | this.iv = parse(iv); 16 | } 17 | } 18 | 19 | get getOptions() { 20 | return { 21 | mode: ECB, 22 | padding: pkcs7, 23 | iv: this.iv, 24 | }; 25 | } 26 | 27 | encryptByAES(cipherText) { 28 | return encrypt(cipherText, this.key, this.getOptions).toString(); 29 | } 30 | 31 | decryptByAES(cipherText) { 32 | return decrypt(cipherText, this.key, this.getOptions).toString(UTF8); 33 | } 34 | } 35 | 36 | export function encryptByBase64(cipherText) { 37 | return UTF8.parse(cipherText).toString(Base64); 38 | } 39 | 40 | export function decodeByBase64(cipherText) { 41 | return Base64.parse(cipherText).toString(UTF8); 42 | } 43 | 44 | export function encryptByMd5(password) { 45 | return md5(password).toString(); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/exception/RestAuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.exception; 2 | 3 | import com.manunin.auth.exception.ErrorResponseHandler; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.core.AuthenticationException; 8 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { 13 | 14 | private final ErrorResponseHandler errorResponseHandler; 15 | 16 | @Autowired 17 | public RestAuthenticationFailureHandler(final ErrorResponseHandler errorResponseHandler) { 18 | this.errorResponseHandler = errorResponseHandler; 19 | } 20 | 21 | @Override 22 | public void onAuthenticationFailure(final HttpServletRequest request, 23 | final HttpServletResponse response, 24 | final AuthenticationException e) { 25 | errorResponseHandler.handle(e, response); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/vue/src/pages/Login/service/authService.js: -------------------------------------------------------------------------------- 1 | import {loadDataWithPost} from 'src/rest/utils'; 2 | import endpoints from 'src/rest/endpoints'; 3 | 4 | const login = ({ login, password }) => { 5 | return loadDataWithPost(endpoints.login, { 6 | username: login, 7 | password: password 8 | }).then(response => { 9 | if (response.token) { 10 | return response; 11 | } 12 | }); 13 | } 14 | 15 | const loginWithGoogle = () => { 16 | return loadDataWithPost(endpoints.loginWithGoogle).then(response => { 17 | if (response.token) { 18 | return response; 19 | } 20 | }); 21 | } 22 | 23 | const register = ({ login, email, password, firstName, lastName }) => { 24 | return loadDataWithPost(endpoints.register, { 25 | username: login, 26 | email: email, 27 | password: password, 28 | firstName: firstName, 29 | lastName: lastName 30 | }); 31 | } 32 | 33 | const refreshToken = (refreshToken) => { 34 | return loadDataWithPost(endpoints.refreshToken, { 35 | refreshToken: refreshToken 36 | }).then(response => { 37 | if (response.refreshToken) { 38 | return response; 39 | } 40 | }); 41 | } 42 | 43 | const logout = () => { 44 | return Promise.resolve(); 45 | } 46 | 47 | export default { login, logout, register, refreshToken, loginWithGoogle }; 48 | -------------------------------------------------------------------------------- /src/main/vue/src/assets/endless-constellation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.mapper; 2 | 3 | import com.manunin.auth.dto.SignupDTO; 4 | import com.manunin.auth.model.ERole; 5 | import com.manunin.auth.model.Role; 6 | import com.manunin.auth.model.User; 7 | import com.manunin.auth.repository.RoleRepository; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Set; 11 | 12 | @Component 13 | public class UserMapper { 14 | 15 | public User fromDto(final SignupDTO signUpDto, final RoleRepository roleRepository) { 16 | User user = createUser(signUpDto); 17 | addRoles(roleRepository, user, signUpDto.getRoles()); 18 | return user; 19 | } 20 | 21 | private static User createUser(final SignupDTO signUpDto) { 22 | return new User(signUpDto.getUsername(), 23 | signUpDto.getEmail(), 24 | signUpDto.getPassword(), 25 | signUpDto.getFirstName(), 26 | signUpDto.getLastName()); 27 | } 28 | 29 | private static void addRoles(final RoleRepository roleRepository, final User user, final Set roles) { 30 | if (roles == null) return; 31 | roles.forEach(role -> user.addRole(getRoleFromRepository(roleRepository, role))); 32 | } 33 | 34 | private static Role getRoleFromRepository(final RoleRepository roleRepository, final String role) { 35 | return roleRepository.findByName(ERole.valueOf(role).getName()) 36 | .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/20220416_create_user_role_table.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | create table USER_ROLES 18 | ( 19 | USER_ID bigserial not null, 20 | ROLE_ID bigserial not null 21 | ); 22 | 23 | create unique index USER_ROLES_USER_ID_ROLE_ID_uindex on USER_ROLES (USER_ID, ROLE_ID); 24 | 25 | COMMENT ON TABLE USER_ROLES IS 'User roles table'; 26 | 27 | COMMENT ON COLUMN USER_ROLES.USER_ID IS 'User id'; 28 | COMMENT ON COLUMN USER_ROLES.ROLE_ID IS 'Role id'; 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/vue/src/rest/apiClient.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {clearAuthCache} from 'src/utils/auth'; 3 | import {JWS_TOKEN_EXPIRED} from "src/rest/constants"; 4 | import authService from "pages/Login/service/authService"; 5 | import {useUserStore} from "stores/user"; 6 | import endpoints from "src/rest/endpoints"; 7 | 8 | const userStore = useUserStore(); 9 | 10 | const apiClient = axios.create({ 11 | baseURL: process.env.API_URL 12 | }); 13 | 14 | function isTokenExpired(error) { 15 | return error.response?.status === 401 && error.response?.data?.code === JWS_TOKEN_EXPIRED; 16 | } 17 | 18 | function isRefreshTokenRequest(config) { 19 | return config.url === endpoints.refreshToken; 20 | } 21 | 22 | function authHeader() { 23 | let token = userStore.getToken; 24 | if (token) { 25 | return {Authorization: 'Bearer ' + token}; 26 | } else { 27 | return {}; 28 | } 29 | } 30 | 31 | apiClient.interceptors.request.use(function (config) { 32 | config.headers = authHeader(); 33 | return config; 34 | }); 35 | 36 | apiClient.interceptors.response.use(function (response) { 37 | return response; 38 | }, function (error) { 39 | const req = error.config; 40 | if (isTokenExpired(error)) { 41 | if (isRefreshTokenRequest(req)) { 42 | clearAuthCache(); 43 | window.location.href = '/login?expired=true'; 44 | } 45 | return authService.refreshToken(userStore.getRefreshToken).then(response => { 46 | userStore.login(response); 47 | return apiClient.request(req); 48 | }); 49 | } 50 | if (error.response?.status === 401) { 51 | clearAuthCache(); 52 | } 53 | return Promise.reject(error); 54 | }); 55 | 56 | export default apiClient; 57 | -------------------------------------------------------------------------------- /src/main/vue/src/rest/utils.js: -------------------------------------------------------------------------------- 1 | import apiClient from './apiClient'; 2 | 3 | function loadData(url, options = {}) { 4 | return apiClient.get(url, options).then((resp) => { 5 | return resp ? resp.data : null; 6 | }).catch((err) => { 7 | if (needLogError(err)) { 8 | console.error(`Unable to load data from ${url} : ${err}`); 9 | } 10 | throw err; 11 | }) 12 | } 13 | 14 | function loadDataWithPost(url, options = {}) { 15 | return apiClient.post(url, options).then((resp) => { 16 | return resp ? resp.data : null; 17 | }).catch((err) => { 18 | if (needLogError(err)) { 19 | console.error(`Unable to load data from ${url} : ${err}`); 20 | } 21 | throw err; 22 | }) 23 | } 24 | 25 | function putData(url, data = {}, options = {}) { 26 | return apiClient.put(url, data, options).catch((err) => { 27 | if (needLogError(err)) { 28 | console.error(`Unable to put data to ${url} : ${err}`); 29 | } 30 | throw err; 31 | }) 32 | } 33 | 34 | function deleteData(url, options = {}) { 35 | return apiClient.delete(url, options).catch((err) => { 36 | if (needLogError(err)) { 37 | console.error(`Unable to delete data from ${url} : ${err}`); 38 | } 39 | throw err; 40 | }) 41 | } 42 | 43 | function postData(url, data = {}, options = {}) { 44 | return apiClient.post(url, data, options).catch((err) => { 45 | if (needLogError(err)) { 46 | console.error(`Unable to post data to ${url} : ${err}`); 47 | } 48 | throw err; 49 | }) 50 | } 51 | 52 | function needLogError(err) { 53 | return err.response == null || err.response.status !== 422; 54 | } 55 | 56 | export {loadData, loadDataWithPost, postData, putData, deleteData}; 57 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/jwt/TokenAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.jwt; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.security.authentication.AuthenticationProvider; 5 | import org.springframework.security.authentication.BadCredentialsException; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.AuthenticationException; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class TokenAuthenticationProvider implements AuthenticationProvider { 13 | 14 | private final JwtTokenProvider jwtTokenProvider; 15 | 16 | public TokenAuthenticationProvider(final JwtTokenProvider jwtTokenProvider) { 17 | this.jwtTokenProvider = jwtTokenProvider; 18 | } 19 | 20 | @Override 21 | public Authentication authenticate(final Authentication authentication) throws AuthenticationException { 22 | String rawAccessToken = (String) authentication.getCredentials(); 23 | UserDetails securityUser = authenticate(rawAccessToken); 24 | return new JwtAuthenticationToken(securityUser); 25 | } 26 | 27 | public UserDetails authenticate(final String accessToken) throws AuthenticationException { 28 | if (StringUtils.isEmpty(accessToken)) { 29 | throw new BadCredentialsException("Token is invalid"); 30 | } 31 | return jwtTokenProvider.parseJwtToken(accessToken); 32 | } 33 | 34 | @Override 35 | public boolean supports(final Class> authentication) { 36 | return (JwtAuthenticationToken.class.isAssignableFrom(authentication)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/rest/LoginAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.rest; 2 | 3 | import com.manunin.auth.secutiry.jwt.JwtPair; 4 | import com.manunin.auth.secutiry.jwt.JwtTokenProvider; 5 | import com.manunin.auth.utils.JsonUtils; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.io.IOException; 16 | 17 | 18 | @Component(value = "loginAuthenticationSuccessHandler") 19 | public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler { 20 | 21 | private final JwtTokenProvider tokenProvider; 22 | 23 | public LoginAuthenticationSuccessHandler(final JwtTokenProvider tokenProvider) { 24 | this.tokenProvider = tokenProvider; 25 | } 26 | 27 | @Override 28 | public void onAuthenticationSuccess(HttpServletRequest request, 29 | HttpServletResponse response, 30 | Authentication authentication) throws IOException { 31 | UserDetails userDetails = (UserDetails) authentication.getPrincipal(); 32 | JwtPair jwtPair = tokenProvider.generateTokenPair(userDetails); 33 | response.setStatus(HttpStatus.OK.value()); 34 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 35 | JsonUtils.writeValue(response.getWriter(), jwtPair); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/20220416_create_roles_privileges_table.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | create table ROLES_PRIVILEGES 18 | ( 19 | ROLE_ID bigserial not null, 20 | PRIVILEGE_ID bigserial not null 21 | ); 22 | 23 | create unique index ROLES_PRIVILEGES_ROLE_ID_PRIVILEGE_ID_uindex on ROLES_PRIVILEGES (ROLE_ID, PRIVILEGE_ID); 24 | 25 | COMMENT ON TABLE ROLES_PRIVILEGES IS 'Role privileges table'; 26 | 27 | COMMENT ON COLUMN ROLES_PRIVILEGES.PRIVILEGE_ID IS 'Privilege id'; 28 | COMMENT ON COLUMN ROLES_PRIVILEGES.ROLE_ID IS 'Role id'; 29 | 30 | INSERT INTO ROLES_PRIVILEGES (role_id, privilege_id) 31 | SELECT roles.id, privileges.id from privileges INNER JOIN roles ON roles.name = privileges.name 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/vue/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { route } from 'quasar/wrappers' 2 | import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router' 3 | import routes from './routes' 4 | import { useUserStore } from 'stores/user'; 5 | 6 | /* 7 | * If not building with SSR mode, you can 8 | * directly export the Router instantiation; 9 | * 10 | * The function below can be async too; either use 11 | * async/await or return a Promise which resolves 12 | * with the Router instance. 13 | */ 14 | 15 | const LOGIN_PATH = '/login'; 16 | const REGISTER_PATH = '/register'; 17 | const HOME_PATH = '/users'; 18 | 19 | const WHITE_PATH_LIST = [LOGIN_PATH, REGISTER_PATH]; 20 | 21 | 22 | export default route(function (/* { store, ssrContext } */) { 23 | const createHistory = process.env.SERVER 24 | ? createMemoryHistory 25 | : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory) 26 | 27 | console.log(process.env.SERVER); 28 | console.log(process.env.VUE_ROUTER_MODE); 29 | console.log(process.env.VUE_ROUTER_BASE); 30 | 31 | const Router = createRouter({ 32 | scrollBehavior: () => ({ left: 0, top: 0 }), 33 | routes, 34 | 35 | // Leave this as is and make changes in quasar.conf.js instead! 36 | // quasar.conf.js -> build -> vueRouterMode 37 | // quasar.conf.js -> build -> publicPath 38 | 39 | history: createHistory(process.env.VUE_ROUTER_BASE) 40 | }) 41 | 42 | Router.beforeEach((to, from, next) => { 43 | const userStore = useUserStore(); 44 | const token = userStore.getToken; 45 | if (token) { 46 | if ([LOGIN_PATH, REGISTER_PATH].includes(to.path)) { 47 | next(HOME_PATH); 48 | return; 49 | } 50 | next(); 51 | } else { 52 | WHITE_PATH_LIST.forEach((path) => { 53 | if (to.path.startsWith(path)) { 54 | next(); 55 | } 56 | }) 57 | next(LOGIN_PATH); 58 | } 59 | }); 60 | 61 | return Router 62 | }) 63 | -------------------------------------------------------------------------------- /src/main/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groups", 3 | "version": "0.0.2", 4 | "description": "Groups application", 5 | "productName": "Groups", 6 | "author": "manunin ", 7 | "private": true, 8 | "scripts": { 9 | "dev": "quasar dev", 10 | "build": "quasar build", 11 | "build:pwa": "quasar build -m pwa", 12 | "lint": "eslint --ext .js,.vue ./", 13 | "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", 14 | "test": "echo \"See package.json => scripts for available tests.\" && exit 0", 15 | "test:unit": "vitest", 16 | "test:unit:ci": "vitest run" 17 | }, 18 | "dependencies": { 19 | "@babel/preset-env": "^7.20.2", 20 | "@pinia/testing": "^0.0.16", 21 | "@quasar/extras": "^1.0.0", 22 | "axios": "^1.3.4", 23 | "crypto-js": "^4.1.1", 24 | "lodash-es": "^4.17.21", 25 | "moment": "^2.29.4", 26 | "pinia": "^2.0.33", 27 | "quasar": "^2.6.0", 28 | "vue": "^3.0.0", 29 | "vue-i18n": "^9.3.0-beta.17", 30 | "vue-router": "^4.0.0" 31 | }, 32 | "devDependencies": { 33 | "@quasar/app-vite": "^1.0.0", 34 | "@quasar/quasar-app-extension-testing-unit-vitest": "^0.2.1", 35 | "@testing-library/jest-dom": "^5.16.5", 36 | "@testing-library/vue": "^7.0.0", 37 | "@vue/test-utils": "^2.0.0", 38 | "autoprefixer": "^10.4.2", 39 | "babel-jest": "^29.5.0", 40 | "eslint": "^8.10.0", 41 | "eslint-config-prettier": "^8.1.0", 42 | "eslint-plugin-vue": "^9.0.0", 43 | "jest": "^29.5.0", 44 | "postcss": "^8.4.14", 45 | "prettier": "^2.5.1", 46 | "vitest": "^0.29.1" 47 | }, 48 | "engines": { 49 | "node": "^18 || ^16 || ^14.19", 50 | "npm": ">= 6.13.4", 51 | "yarn": ">= 1.21.1" 52 | }, 53 | "jest": { 54 | "moduleFileExtensions": [ 55 | "js", 56 | "json", 57 | "vue" 58 | ], 59 | "transform": { 60 | "^.+\\.js$": "babel-jest", 61 | ".*\\.(vue)$": "vue-jest" 62 | }, 63 | "testEnvironment": "jsdom" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/exception/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.exception; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | import org.springframework.http.HttpStatus; 6 | 7 | import java.util.Date; 8 | 9 | public class ErrorResponse { 10 | @NotBlank 11 | @Schema(description = "Error message", example = "Error message") 12 | private final String message; 13 | 14 | @NotBlank 15 | @Schema(description = "Error code: " + 16 | "1 - General error (HTTP: 500 - Internal Server Error), " + 17 | "2 - Authentication failed (HTTP: 401 - Unauthorized), " + 18 | "3 - JWT token expired (HTTP: 401 - Unauthorized), " + 19 | "10 - Bad request parameters (HTTP: 400 - Bad Request), " + 20 | "20 - Access denied (HTTP: 403 - Forbidden)", 21 | example = "2") 22 | private final ErrorCode code; 23 | 24 | @NotBlank 25 | @Schema(description = "Error code", example = "401") 26 | private final HttpStatus status; 27 | 28 | @NotBlank 29 | @Schema(description = "Error timestamp", example = "2021-08-25T15:00:00") 30 | private final Date timestamp; 31 | 32 | public ErrorResponse(final String message, 33 | final ErrorCode code, 34 | final HttpStatus status, 35 | final Date timestamp) { 36 | this.message = message; 37 | this.code = code; 38 | this.status = status; 39 | this.timestamp = timestamp; 40 | } 41 | 42 | public static ErrorResponse of(final String message, final ErrorCode code) { 43 | return new ErrorResponse(message, code, code.getStatus(), new Date()); 44 | } 45 | 46 | public String getMessage() { 47 | return message; 48 | } 49 | 50 | public ErrorCode getCode() { 51 | return code; 52 | } 53 | 54 | public HttpStatus getStatus() { 55 | return status; 56 | } 57 | 58 | public Date getTimestamp() { 59 | return timestamp; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/configuration/AuthenticationManagerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.configuration; 2 | 3 | import com.manunin.auth.secutiry.jwt.TokenAuthenticationProvider; 4 | import com.manunin.auth.secutiry.jwt.RefreshTokenAuthenticationProvider; 5 | import com.manunin.auth.secutiry.login.LoginAuthenticationProvider; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.security.authentication.AuthenticationManager; 9 | import org.springframework.security.config.annotation.ObjectPostProcessor; 10 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 11 | 12 | @Configuration 13 | public class AuthenticationManagerConfiguration { 14 | 15 | private final TokenAuthenticationProvider tokenAuthenticationProvider; 16 | 17 | private final LoginAuthenticationProvider loginAuthenticationProvider; 18 | 19 | private final RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; 20 | 21 | public AuthenticationManagerConfiguration(final TokenAuthenticationProvider tokenAuthenticationProvider, 22 | final LoginAuthenticationProvider loginAuthenticationProvider, 23 | final RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider) { 24 | this.tokenAuthenticationProvider = tokenAuthenticationProvider; 25 | this.loginAuthenticationProvider = loginAuthenticationProvider; 26 | this.refreshTokenAuthenticationProvider = refreshTokenAuthenticationProvider; 27 | } 28 | 29 | @Bean 30 | public AuthenticationManager authenticationManager(final ObjectPostProcessor objectPostProcessor) throws Exception { 31 | var auth = new AuthenticationManagerBuilder(objectPostProcessor); 32 | auth.authenticationProvider(loginAuthenticationProvider); 33 | auth.authenticationProvider(tokenAuthenticationProvider); 34 | auth.authenticationProvider(refreshTokenAuthenticationProvider); 35 | return auth.build(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/20220416_create_roles_table.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | create table ROLES 18 | ( 19 | ID bigserial not null, 20 | NAME text not null, 21 | SYSTEM boolean not null default false, 22 | ACTIVE boolean not null default true 23 | ); 24 | 25 | create unique index ROLES_ID_uindex on ROLES (ID); 26 | create unique index ROLES_NAME_uindex on ROLES (NAME); 27 | 28 | alter table ROLES add constraint ROLES_pk primary key (ID); 29 | 30 | COMMENT ON TABLE ROLES IS 'Roles table'; 31 | 32 | COMMENT ON COLUMN ROLES.ID IS 'Role id'; 33 | COMMENT ON COLUMN ROLES.NAME IS 'Role name'; 34 | COMMENT ON COLUMN ROLES.SYSTEM IS 'Role system flag'; 35 | COMMENT ON COLUMN ROLES.ACTIVE IS 'Role active flag'; 36 | 37 | INSERT INTO ROLES (name, system, active) VALUES ('ADMIN', true, true); 38 | INSERT INTO ROLES (name, system, active) VALUES ('User role', true, true); 39 | INSERT INTO ROLES (name, system, active) VALUES ('Moderator role', false, true); 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/model/Role.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.FetchType; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.JoinColumn; 9 | import jakarta.persistence.JoinTable; 10 | import jakarta.persistence.ManyToMany; 11 | import jakarta.persistence.Table; 12 | 13 | import java.util.HashSet; 14 | import java.util.Set; 15 | 16 | @Entity 17 | @Table(name="roles") 18 | public class Role { 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private long id; 22 | private String name; 23 | private boolean system; 24 | private boolean active; 25 | 26 | @ManyToMany(fetch = FetchType.EAGER) 27 | @JoinTable( name = "roles_privileges", 28 | joinColumns = @JoinColumn(name = "role_id"), 29 | inverseJoinColumns = @JoinColumn(name = "privilege_id")) 30 | public Set privileges = new HashSet<>(); 31 | public Role(final long id, final String name, final boolean system, final boolean active) { 32 | this.id = id; 33 | this.name = name; 34 | this.system = system; 35 | this.active = active; 36 | } 37 | 38 | public Role() { 39 | } 40 | 41 | public long getId() { 42 | return id; 43 | } 44 | 45 | public void setId(long id) { 46 | this.id = id; 47 | } 48 | 49 | public String getName() { 50 | return name; 51 | } 52 | 53 | public void setName(String name) { 54 | this.name = name; 55 | } 56 | 57 | public boolean isSystem() { 58 | return system; 59 | } 60 | 61 | public void setSystem(boolean system) { 62 | this.system = system; 63 | } 64 | 65 | public boolean isActive() { 66 | return active; 67 | } 68 | 69 | public void setActive(boolean active) { 70 | this.active = active; 71 | } 72 | 73 | public Set getPrivileges() { 74 | return privileges; 75 | } 76 | 77 | public void setPrivileges(Set privileges) { 78 | this.privileges = privileges; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/20220416_create_privilege_table.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | create table PRIVILEGES 18 | ( 19 | ID bigserial not null, 20 | NAME text not null 21 | ); 22 | 23 | create unique index PRIVILEGES_ID_uindex on PRIVILEGES (ID); 24 | create unique index PRIVILEGES_NAME_uindex on PRIVILEGES (NAME); 25 | 26 | alter table PRIVILEGES add constraint PRIVILEGES_pk primary key (ID); 27 | 28 | COMMENT ON TABLE PRIVILEGES IS 'Privileges table'; 29 | 30 | COMMENT ON COLUMN PRIVILEGES.ID IS 'Privilege id'; 31 | COMMENT ON COLUMN PRIVILEGES.NAME IS 'Privilege name'; 32 | 33 | INSERT INTO PRIVILEGES (name) VALUES ('ADMIN'); 34 | 35 | INSERT INTO PRIVILEGES (name) VALUES ('USERS_VIEW'); 36 | INSERT INTO PRIVILEGES (name) VALUES ('USERS_MODIFY'); 37 | INSERT INTO PRIVILEGES (name) VALUES ('USERS_DELETE'); 38 | 39 | INSERT INTO PRIVILEGES (name) VALUES ('ROLES_VIEW'); 40 | INSERT INTO PRIVILEGES (name) VALUES ('ROLES_MODIFY'); 41 | INSERT INTO PRIVILEGES (name) VALUES ('ROLES_DELETE'); 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/dto/SignupDTO.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.Size; 7 | 8 | import java.util.Set; 9 | 10 | public class SignupDTO { 11 | 12 | @NotBlank 13 | @Size(min = 3, max = 20) 14 | @Schema(description = "User name") 15 | private String username; 16 | 17 | @NotBlank 18 | @Size(max = 50) 19 | @Email 20 | @Schema(description = "User email") 21 | private String email; 22 | 23 | @Schema(description = "User roles") 24 | private Set roles; 25 | 26 | @NotBlank 27 | @Size(min = 6, max = 40) 28 | @Schema(description = "User password") 29 | private String password; 30 | 31 | @NotBlank 32 | @Size(max = 50) 33 | @Schema(description = "User first name") 34 | private String firstName; 35 | 36 | @NotBlank 37 | @Size(max = 50) 38 | @Schema(description = "User last name") 39 | private String lastName; 40 | 41 | public String getUsername() { 42 | return username; 43 | } 44 | 45 | public void setUsername(final String username) { 46 | this.username = username; 47 | } 48 | 49 | public String getEmail() { 50 | return email; 51 | } 52 | 53 | public void setEmail(final String email) { 54 | this.email = email; 55 | } 56 | 57 | public Set getRoles() { 58 | return roles; 59 | } 60 | 61 | public void setRoles(final Set roles) { 62 | this.roles = roles; 63 | } 64 | 65 | public String getPassword() { 66 | return password; 67 | } 68 | 69 | public void setPassword(final String password) { 70 | this.password = password; 71 | } 72 | 73 | public String getFirstName() { 74 | return firstName; 75 | } 76 | 77 | public void setFirstName(final String firstName) { 78 | this.firstName = firstName; 79 | } 80 | 81 | public String getLastName() { 82 | return lastName; 83 | } 84 | 85 | public void setLastName(final String lastName) { 86 | this.lastName = lastName; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/jwt/RefreshTokenAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.jwt; 2 | 3 | import com.manunin.auth.exception.ServiceException; 4 | import com.manunin.auth.service.UserDetailsImpl; 5 | import com.manunin.auth.service.UserService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.authentication.AuthenticationProvider; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component 15 | public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { 16 | 17 | private final UserService userService; 18 | 19 | private final JwtTokenProvider tokenProvider; 20 | 21 | @Autowired 22 | public RefreshTokenAuthenticationProvider(final UserService userService, 23 | final JwtTokenProvider tokenProvider) { 24 | this.userService = userService; 25 | this.tokenProvider = tokenProvider; 26 | } 27 | 28 | @Override 29 | public Authentication authenticate(final Authentication authentication) throws AuthenticationException { 30 | String token = (String) authentication.getCredentials(); 31 | String username = tokenProvider.getUserNameFromJwtToken(token); 32 | UserDetailsImpl userDetails = getUserDetails(username); 33 | return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); 34 | } 35 | 36 | private UserDetailsImpl getUserDetails(String username) { 37 | try { 38 | return UserDetailsImpl.build(userService.findByUsername(username)); 39 | } catch (ServiceException e) { 40 | throw new UsernameNotFoundException("User not found: " + username); 41 | } 42 | } 43 | 44 | @Override 45 | public boolean supports(final Class> authentication) { 46 | return (RefreshJwtAuthenticationToken.class.isAssignableFrom(authentication)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/vue/src/utils/cache/memory.js: -------------------------------------------------------------------------------- 1 | const NOT_ALIVE = 0; 2 | 3 | export class Memory { 4 | constructor(alive = NOT_ALIVE) { 5 | // Unit second 6 | this.alive = alive * 1000; 7 | this.cache = {}; 8 | } 9 | 10 | get getCache() { 11 | return this.cache; 12 | } 13 | 14 | setCache(cache) { 15 | this.cache = cache; 16 | } 17 | 18 | get(key) { 19 | return this.cache[key]; 20 | } 21 | 22 | set(key, value, expires) { 23 | let item = this.get(key); 24 | 25 | if (!expires || expires <= 0) { 26 | expires = this.alive; 27 | } 28 | if (item) { 29 | if (item.timeoutId) { 30 | clearTimeout(item.timeoutId); 31 | item.timeoutId = undefined; 32 | } 33 | item.value = value; 34 | } else { 35 | item = {value, alive: expires}; 36 | this.cache[key] = item; 37 | } 38 | 39 | if (!expires) { 40 | return value; 41 | } 42 | const now = new Date().getTime(); 43 | /** 44 | * Prevent overflow of the setTimeout Maximum delay value 45 | * Maximum delay value 2,147,483,647 ms 46 | * https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value 47 | */ 48 | item.time = expires > now ? expires : now + expires; 49 | item.timeoutId = setTimeout( 50 | () => { 51 | this.remove(key); 52 | }, 53 | expires > now ? expires - now : expires, 54 | ); 55 | 56 | return value; 57 | } 58 | 59 | remove(key) { 60 | const item = this.get(key); 61 | Reflect.deleteProperty(this.cache, key); 62 | if (item) { 63 | clearTimeout(item.timeoutId); 64 | return item.value; 65 | } 66 | } 67 | 68 | resetCache(cache) { 69 | Object.keys(cache).forEach((key) => { 70 | const k = key; 71 | const item = cache[k]; 72 | if (item && item.time) { 73 | const now = new Date().getTime(); 74 | const expire = item.time; 75 | if (expire > now) { 76 | this.set(k, item.value, expire); 77 | } 78 | } 79 | }); 80 | } 81 | 82 | clear() { 83 | Object.keys(this.cache).forEach((key) => { 84 | const item = this.cache[key]; 85 | item.timeoutId && clearTimeout(item.timeoutId); 86 | }); 87 | this.cache = {}; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/vue/src/utils/is.js: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | 3 | export function is(val, type) { 4 | return toString.call(val) === `[object ${type}]`; 5 | } 6 | 7 | export function isDef(val) { 8 | return typeof val !== 'undefined'; 9 | } 10 | 11 | export function isUnDef(val) { 12 | return !isDef(val); 13 | } 14 | 15 | export function isObject(val) { 16 | return val !== null && is(val, 'Object'); 17 | } 18 | 19 | export function isEmpty(val) { 20 | if (isArray(val) || isString(val)) { 21 | return val.length === 0; 22 | } 23 | 24 | if (val instanceof Map || val instanceof Set) { 25 | return val.size === 0; 26 | } 27 | 28 | if (isObject(val)) { 29 | return Object.keys(val).length === 0; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export function isDate(val) { 36 | return is(val, 'Date'); 37 | } 38 | 39 | export function isNull(val) { 40 | return val === null; 41 | } 42 | 43 | export function isNullAndUnDef(val) { 44 | return isUnDef(val) && isNull(val); 45 | } 46 | 47 | export function isNullOrUnDef(val) { 48 | return isUnDef(val) || isNull(val); 49 | } 50 | 51 | export function isNumber(val) { 52 | return is(val, 'Number'); 53 | } 54 | 55 | export function isPromise(val) { 56 | return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch); 57 | } 58 | 59 | export function isString(val) { 60 | return is(val, 'String'); 61 | } 62 | 63 | export function isFunction(val) { 64 | return typeof val === 'function'; 65 | } 66 | 67 | export function isBoolean(val) { 68 | return is(val, 'Boolean'); 69 | } 70 | 71 | export function isRegExp(val) { 72 | return is(val, 'RegExp'); 73 | } 74 | 75 | export function isArray(val) { 76 | return val && Array.isArray(val); 77 | } 78 | 79 | export function isWindow(val) { 80 | return typeof window !== 'undefined' && is(val, 'Window'); 81 | } 82 | 83 | export function isElement(val) { 84 | return isObject(val) && !!val.tagName; 85 | } 86 | 87 | export function isMap(val) { 88 | return is(val, 'Map'); 89 | } 90 | 91 | export const isServer = typeof window === 'undefined'; 92 | 93 | export const isClient = !isServer; 94 | 95 | export function isUrl(path) { 96 | const reg = /^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?/; 97 | return reg.test(path); 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.controller; 2 | 3 | import com.manunin.auth.dto.SignupDTO; 4 | import com.manunin.auth.exception.ServiceException; 5 | import com.manunin.auth.mapper.UserMapper; 6 | import com.manunin.auth.model.User; 7 | import com.manunin.auth.repository.RoleRepository; 8 | import com.manunin.auth.service.UserService; 9 | import io.swagger.v3.oas.annotations.Operation; 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 11 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | @RestController 19 | @RequestMapping("/auth") 20 | public class AuthController { 21 | private final UserService userService; 22 | private final RoleRepository roleRepository; 23 | private final UserMapper userMapper; 24 | 25 | 26 | public AuthController(final UserService userService, 27 | final RoleRepository roleRepository, 28 | final UserMapper userMapper) { 29 | this.userService = userService; 30 | this.roleRepository = roleRepository; 31 | this.userMapper = userMapper; 32 | } 33 | 34 | @ApiResponses(value = { 35 | @ApiResponse(responseCode = "200", description = "User is successfully signed up"), 36 | @ApiResponse(responseCode = "400", description = "One of Username already exists or Email already exists") 37 | }) 38 | @Operation(summary = "User signup") 39 | @PostMapping("/signup") 40 | public User registerUser(@RequestBody final SignupDTO signUpRequest) throws ServiceException { 41 | return userService.addUser(userMapper.fromDto(signUpRequest, roleRepository)); 42 | } 43 | 44 | @ApiResponses(value = { 45 | @ApiResponse(responseCode = "200", description = "Number of users in the system"), 46 | @ApiResponse(responseCode = "400", description = "Error while getting number of users") 47 | }) 48 | @GetMapping("/users/count") 49 | public Long getUsersCount() { 50 | return userService.getUsersCount(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/changelog/1.0.0/20220415_create_user_table.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | create table APP_USERS 18 | ( 19 | ID bigserial not null, 20 | USERNAME text not null, 21 | FIRST_NAME text not null, 22 | LAST_NAME text not null, 23 | PASSWORD text not null, 24 | EMAIL text not null, 25 | IS_LOCKED boolean not null default false 26 | ); 27 | 28 | create unique index APP_USERS_ID_uindex on APP_USERS (ID); 29 | create unique index APP_USERS_USERNAME_uindex on APP_USERS (USERNAME); 30 | 31 | alter table APP_USERS add constraint APP_USERS_pk primary key (ID); 32 | 33 | COMMENT ON TABLE APP_USERS IS 'User table'; 34 | 35 | COMMENT ON COLUMN APP_USERS.ID IS 'User id'; 36 | COMMENT ON COLUMN APP_USERS.USERNAME IS 'User name (login)'; 37 | COMMENT ON COLUMN APP_USERS.FIRST_NAME IS 'User first name'; 38 | COMMENT ON COLUMN APP_USERS.LAST_NAME IS 'User last name'; 39 | COMMENT ON COLUMN APP_USERS.PASSWORD IS 'User password'; 40 | COMMENT ON COLUMN APP_USERS.EMAIL IS 'User email'; 41 | COMMENT ON COLUMN APP_USERS.IS_LOCKED IS 'Flag says if the user is locked'; 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/vue/src/utils/cache/storageCache.js: -------------------------------------------------------------------------------- 1 | import {cacheCipher} from 'src/settings/encryptionSetting'; 2 | import {AesEncryption} from 'src/utils/cipher'; 3 | import {isNullOrUnDef} from 'src/utils/is'; 4 | 5 | export const createStorage = ({ 6 | prefixKey = '', 7 | storage = sessionStorage, 8 | key = cacheCipher.key, 9 | iv = cacheCipher.iv, 10 | timeout = null, 11 | hasEncrypt = true, 12 | } = {}) => { 13 | if (hasEncrypt && [key.length, iv.length].some((item) => item !== 16)) { 14 | throw new Error('When hasEncrypt is true, the key or iv must be 16 bits!'); 15 | } 16 | 17 | const encryption = new AesEncryption({key, iv}); 18 | const WebStorage = class WebStorage { 19 | constructor() { 20 | this.storage = storage; 21 | this.prefixKey = prefixKey; 22 | this.encryption = encryption; 23 | this.hasEncrypt = hasEncrypt; 24 | } 25 | 26 | getKey(key) { 27 | return `${this.prefixKey}${key}`.toUpperCase(); 28 | } 29 | 30 | set(key, value, expire = timeout) { 31 | const stringData = JSON.stringify({ 32 | value, 33 | time: Date.now(), 34 | expire: !isNullOrUnDef(expire) ? new Date().getTime() + expire * 1000 : null, 35 | }); 36 | const stringifyValue = this.hasEncrypt 37 | ? this.encryption.encryptByAES(stringData) 38 | : stringData; 39 | this.storage.setItem(this.getKey(key), stringifyValue); 40 | } 41 | 42 | get(key, def = null) { 43 | const val = this.storage.getItem(this.getKey(key)); 44 | if (!val) return def; 45 | 46 | try { 47 | const decVal = this.hasEncrypt ? this.encryption.decryptByAES(val) : val; 48 | const data = JSON.parse(decVal); 49 | const {value, expire} = data; 50 | if (isNullOrUnDef(expire) || expire >= new Date().getTime()) { 51 | return value; 52 | } 53 | this.remove(key); 54 | } catch (e) { 55 | return def; 56 | } 57 | } 58 | 59 | remove(key) { 60 | this.storage.removeItem(this.getKey(key)); 61 | } 62 | 63 | /** 64 | * Delete all caches of this instance 65 | */ 66 | clear(){ 67 | this.storage.clear(); 68 | } 69 | }; 70 | return new WebStorage(); 71 | }; 72 | -------------------------------------------------------------------------------- /src/main/vue/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy 3 | // This option interrupts the configuration hierarchy at this file 4 | // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) 5 | root: true, 6 | 7 | parserOptions: { 8 | ecmaVersion: '2021', // Allows for the parsing of modern ECMAScript features 9 | }, 10 | 11 | env: { 12 | node: true, 13 | browser: true, 14 | 'vue/setup-compiler-macros': true 15 | }, 16 | 17 | // Rules order is important, please avoid shuffling them 18 | extends: [ 19 | // Base ESLint recommended rules 20 | // 'eslint:recommended', 21 | 22 | // Uncomment any of the lines below to choose desired strictness, 23 | // but leave only one uncommented! 24 | // See https://eslint.vuejs.org/rules/#available-rules 25 | 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention) 26 | // 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 27 | // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 28 | 29 | // https://github.com/prettier/eslint-config-prettier#installation 30 | // usage with Prettier, provided by 'eslint-config-prettier'. 31 | 'prettier' 32 | ], 33 | 34 | plugins: [ 35 | // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files 36 | // required to lint *.vue files 37 | 'vue', 38 | 39 | // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674 40 | // Prettier has not been included as plugin to avoid performance impact 41 | // add it as an extension for your IDE 42 | 43 | ], 44 | 45 | globals: { 46 | ga: 'readonly', // Google Analytics 47 | cordova: 'readonly', 48 | __statics: 'readonly', 49 | __QUASAR_SSR__: 'readonly', 50 | __QUASAR_SSR_SERVER__: 'readonly', 51 | __QUASAR_SSR_CLIENT__: 'readonly', 52 | __QUASAR_SSR_PWA__: 'readonly', 53 | process: 'readonly', 54 | Capacitor: 'readonly', 55 | chrome: 'readonly' 56 | }, 57 | 58 | // add your custom rules here 59 | rules: { 60 | 61 | 'prefer-promise-reject-errors': 'off', 62 | 63 | // allow debugger during development only 64 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/vue/src/i18n/en-US/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | appName: 'Authorization and Accounting System', 3 | loginPage: { 4 | fields: { 5 | login: 'Username', 6 | password: 'Password', 7 | email: 'Email', 8 | formTitle: 'Sign in', 9 | }, 10 | validations: { 11 | loginMustPresent: 'Login must be set', 12 | mailMustPresent: 'Email must be set', 13 | mailMustBeValid: 'Email must be valid', 14 | passwordMustPresent: 'Password must be set', 15 | passwordLength: 'Password must be between 8 and 20 characters long', 16 | sessionExpired: 'Session expired', 17 | invalidCredentials: 'Invalid username or password', 18 | }, 19 | registration: { 20 | message: 'Don\'t have an account?', 21 | button: 'Register', 22 | } 23 | }, 24 | registerPage: { 25 | fields: { 26 | login: 'Username', 27 | password: 'Password', 28 | email: 'Email', 29 | firstName: 'First name', 30 | lastName: 'Last name', 31 | formTitle: 'Register', 32 | repeatPassword: 'Repeat password', 33 | policy: 'By clicking "Register" you accept the terms of the User Agreement, Privacy Policy, and Cookie Policy of Groups' 34 | }, 35 | validations: { 36 | loginMustPresent: 'Login must be set', 37 | emailMustPresent: 'Email must be set', 38 | mailMustBeValid: 'Email must be valid', 39 | firstNameMustPresent: 'First name must be set', 40 | lastNameMustPresent: 'Last name must be set', 41 | passwordMustPresent: 'Password must be set', 42 | passwordMustBeTheSame: 'Passwords must be the same', 43 | loginMinLength: 'Login must be at least 3 characters long', 44 | passwordComplexity: 'Invalid password. Please meet the requirements: 8 characters minimum, 1 digit, 1 letter, 1 special character', 45 | }, 46 | button: 'Register', 47 | successMessage: 'You have successfully registered' 48 | }, 49 | buttons: { 50 | add: 'Add', 51 | modify: 'Modify', 52 | delete: 'Delete', 53 | cancel: 'Cancel', 54 | save: 'Save', 55 | ok: 'Ok', 56 | login: 'Login', 57 | logout: 'Logout', 58 | backButton: 'Back', 59 | }, 60 | exception: { 61 | tooManyLoginAttempts: 'Too many login attempts. Try again in 1 minute', 62 | invalidCredentials: 'Invalid username or password', 63 | emailExists: 'Email already exists', 64 | usernameExists: 'Username already exists', 65 | authenticationFailed: 'Authentication failed', 66 | badCredentials: 'Authentication Failed. Username or Password not valid.', 67 | tokenExpired: 'Token expired' 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/jwt/TokenAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.jwt; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.core.context.SecurityContext; 10 | import org.springframework.security.core.context.SecurityContextHolder; 11 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 12 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 13 | import org.springframework.security.web.util.matcher.RequestMatcher; 14 | 15 | import java.io.IOException; 16 | 17 | public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 18 | private final JwtTokenProvider tokenProvider; 19 | 20 | private final AuthenticationFailureHandler failureHandler; 21 | 22 | public TokenAuthenticationFilter(final JwtTokenProvider tokenProvider, 23 | final RequestMatcher defaultProcessUrl, 24 | final AuthenticationFailureHandler failureHandler) { 25 | super(defaultProcessUrl); 26 | this.tokenProvider = tokenProvider; 27 | this.failureHandler = failureHandler; 28 | } 29 | 30 | @Override 31 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 32 | return getAuthenticationManager().authenticate(new JwtAuthenticationToken(tokenProvider.getTokenFromRequest(request))); 33 | } 34 | 35 | @Override 36 | protected void successfulAuthentication(final HttpServletRequest request, 37 | final HttpServletResponse response, 38 | final FilterChain chain, 39 | final Authentication authResult) throws IOException, ServletException { 40 | SecurityContext context = SecurityContextHolder.createEmptyContext(); 41 | context.setAuthentication(authResult); 42 | SecurityContextHolder.setContext(context); 43 | chain.doFilter(request, response); 44 | } 45 | 46 | @Override 47 | protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { 48 | this.failureHandler.onAuthenticationFailure(request, response, failed); 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/service/UserDetailsImpl.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.service; 2 | 3 | import com.manunin.auth.model.User; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | 9 | import java.util.Collection; 10 | import java.util.List; 11 | import java.util.Objects; 12 | import java.util.stream.Collectors; 13 | 14 | public class UserDetailsImpl implements UserDetails { 15 | private static final long serialVersionUID = 1L; 16 | private final Long id; 17 | private final String username; 18 | private final String email; 19 | @JsonIgnore 20 | private final String password; 21 | private final Collection extends GrantedAuthority> authorities; 22 | 23 | public UserDetailsImpl(final User user, final Collection extends GrantedAuthority> authorities) { 24 | this.id = user.getId(); 25 | this.username = user.getUsername(); 26 | this.email = user.getEmail(); 27 | this.password = user.getPassword(); 28 | this.authorities = authorities; 29 | } 30 | 31 | 32 | public static UserDetailsImpl build(User user) { 33 | return new UserDetailsImpl(user, buildGrantedAuthorities(user)); 34 | } 35 | 36 | private static List buildGrantedAuthorities(final User user) { 37 | return user.getRoles().stream() 38 | .flatMap(role -> role.getPrivileges().stream()) 39 | .map(privilege -> new SimpleGrantedAuthority(privilege.getName())) 40 | .collect(Collectors.toList()); 41 | } 42 | 43 | public Long getId() { 44 | return id; 45 | } 46 | public String getEmail() { 47 | return email; 48 | } 49 | 50 | @Override 51 | public Collection extends GrantedAuthority> getAuthorities() { 52 | return authorities; 53 | } 54 | 55 | @Override 56 | public String getPassword() { 57 | return password; 58 | } 59 | 60 | @Override 61 | public String getUsername() { 62 | return username; 63 | } 64 | 65 | @Override 66 | public boolean isAccountNonExpired() { 67 | return true; 68 | } 69 | 70 | @Override 71 | public boolean isAccountNonLocked() { 72 | return true; 73 | } 74 | 75 | @Override 76 | public boolean isCredentialsNonExpired() { 77 | return true; 78 | } 79 | 80 | @Override 81 | public boolean isEnabled() { 82 | return true; 83 | } 84 | 85 | @Override 86 | public boolean equals(Object o) { 87 | if (this == o) 88 | return true; 89 | if (o == null || getClass() != o.getClass()) 90 | return false; 91 | UserDetailsImpl user = (UserDetailsImpl) o; 92 | return Objects.equals(id, user.id); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/login/LoginAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.login; 2 | 3 | import com.manunin.auth.exception.ServiceException; 4 | import com.manunin.auth.model.User; 5 | import com.manunin.auth.service.UserDetailsImpl; 6 | import com.manunin.auth.service.UserService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.authentication.AuthenticationProvider; 9 | import org.springframework.security.authentication.BadCredentialsException; 10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 11 | import org.springframework.security.core.Authentication; 12 | import org.springframework.security.core.AuthenticationException; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 15 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 16 | import org.springframework.security.crypto.password.PasswordEncoder; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.util.Assert; 19 | 20 | @Component 21 | public class LoginAuthenticationProvider implements AuthenticationProvider { 22 | 23 | private final UserService userService; 24 | 25 | private final PasswordEncoder encoder; 26 | 27 | @Autowired 28 | public LoginAuthenticationProvider(final UserService userService) { 29 | this.userService = userService; 30 | this.encoder = new BCryptPasswordEncoder(); 31 | } 32 | 33 | @Override 34 | public Authentication authenticate(final Authentication authentication) throws AuthenticationException { 35 | Assert.notNull(authentication, "No authentication data provided"); 36 | String username = (String) authentication.getPrincipal(); 37 | String password = authentication.getCredentials().toString(); 38 | UserDetails securityUser = authenticateByUsernameAndPassword(username, password); 39 | return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); 40 | } 41 | 42 | private UserDetails authenticateByUsernameAndPassword(final String username, final String password) { 43 | User user = getUser(username); 44 | if (!encoder.matches(password, user.getPassword())) { 45 | throw new BadCredentialsException("exception.badCredentials"); 46 | } 47 | return UserDetailsImpl.build(user); 48 | } 49 | 50 | private User getUser(final String username) { 51 | try { 52 | return userService.findByUsername(username); 53 | } catch (ServiceException e) { 54 | throw new UsernameNotFoundException("User not found: " + username); 55 | } 56 | } 57 | 58 | @Override 59 | public boolean supports(final Class> authentication) { 60 | return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/service/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.service; 2 | 3 | import com.manunin.auth.exception.ErrorCode; 4 | import com.manunin.auth.exception.ServiceException; 5 | import com.manunin.auth.model.ERole; 6 | import com.manunin.auth.model.Role; 7 | import com.manunin.auth.model.User; 8 | import com.manunin.auth.repository.RoleRepository; 9 | import com.manunin.auth.repository.UserRepository; 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.util.Set; 16 | 17 | @Component 18 | public class UserServiceImpl implements UserService { 19 | 20 | public static final String EXCEPTION_USERNAME_EXISTS = "exception.usernameExists"; 21 | public static final String EXCEPTION_EMAIL_EXISTS = "exception.emailExists"; 22 | private final UserRepository userRepository; 23 | 24 | private final RoleRepository roleRepository; 25 | 26 | private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); 27 | 28 | public UserServiceImpl(final UserRepository userRepository, 29 | final RoleRepository roleRepository) { 30 | this.userRepository = userRepository; 31 | this.roleRepository = roleRepository; 32 | } 33 | 34 | @Override 35 | public User addUser(final User user) throws ServiceException { 36 | if (existsByUsername(user.getUsername())) { 37 | throw new ServiceException(ErrorCode.BAD_REQUEST_PARAMS, EXCEPTION_USERNAME_EXISTS); 38 | } 39 | if (existsByEmail(user.getEmail())) { 40 | throw new ServiceException(ErrorCode.BAD_REQUEST_PARAMS, EXCEPTION_EMAIL_EXISTS); 41 | } 42 | 43 | user.setPassword(passwordEncoder.encode(user.getPassword())); 44 | 45 | Role userRole = roleRepository.findByName(ERole.USER.getName()) 46 | .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); 47 | 48 | user.setRoles(Set.of(userRole)); 49 | return userRepository.save(user); 50 | } 51 | 52 | @Override 53 | @Transactional 54 | public boolean existsByUsername(final String name) { 55 | return userRepository.existsByUsername(name); 56 | } 57 | 58 | @Override 59 | @Transactional 60 | public boolean existsByEmail(final String email) { 61 | return userRepository.existsByEmail(email); 62 | } 63 | 64 | @Override 65 | public User findByUsername(String username) throws ServiceException { 66 | return userRepository.findByUsername(username) 67 | .orElseThrow(() -> new ServiceException(ErrorCode.BAD_REQUEST_PARAMS, "exception.user.notFound")); 68 | } 69 | 70 | @Override 71 | public User findByEmail(String email) throws ServiceException { 72 | return userRepository.findByEmail(email) 73 | .orElseThrow(() -> new ServiceException(ErrorCode.BAD_REQUEST_PARAMS, "exception.user.notFound")); 74 | } 75 | 76 | @Override 77 | public Long getUsersCount() { 78 | return userRepository.count(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/vue/src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 17 | 18 | {{ $t('appName') }} 19 | 20 | 24 | 28 | 29 | 30 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 98 | 104 | -------------------------------------------------------------------------------- /src/main/vue/src/utils/cache/persistent.js: -------------------------------------------------------------------------------- 1 | import { createLocalStorage, createSessionStorage } from 'src/utils/cache/index.js'; 2 | import { Memory } from './memory'; 3 | import { 4 | TOKEN_KEY, 5 | USER_INFO_KEY, 6 | LOCK_INFO_KEY, 7 | APP_LOCAL_CACHE_KEY, 8 | APP_SESSION_CACHE_KEY, 9 | } from 'src/enums/cacheEnums'; 10 | import { DEFAULT_CACHE_TIME } from 'src/settings/encryptionSetting'; 11 | import { toRaw } from 'vue'; 12 | import { pick, omit } from 'lodash-es'; 13 | 14 | const ls = createLocalStorage(); 15 | const ss = createSessionStorage(); 16 | 17 | const localMemory = new Memory(DEFAULT_CACHE_TIME); 18 | const sessionMemory = new Memory(DEFAULT_CACHE_TIME); 19 | 20 | function initPersistentMemory() { 21 | const localCache = ls.get(APP_LOCAL_CACHE_KEY); 22 | const sessionCache = ss.get(APP_SESSION_CACHE_KEY); 23 | localCache && localMemory.resetCache(localCache); 24 | sessionCache && sessionMemory.resetCache(sessionCache); 25 | } 26 | 27 | export class Persistent { 28 | static getLocal(key) { 29 | return localMemory.get(key)?.value; 30 | } 31 | 32 | static setLocal(key, value, immediate = false) { 33 | localMemory.set(key, toRaw(value)); 34 | immediate && ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache); 35 | } 36 | 37 | static removeLocal(key, immediate = false){ 38 | localMemory.remove(key); 39 | immediate && ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache); 40 | } 41 | 42 | static clearLocal(immediate = false){ 43 | localMemory.clear(); 44 | immediate && ls.clear(); 45 | } 46 | 47 | static getSession(key) { 48 | return sessionMemory.get(key)?.value; 49 | } 50 | 51 | static setSession(key, value, immediate = false) { 52 | sessionMemory.set(key, toRaw(value)); 53 | immediate && ss.set(APP_SESSION_CACHE_KEY, sessionMemory.getCache); 54 | } 55 | 56 | static removeSession(key, immediate = false) { 57 | sessionMemory.remove(key); 58 | immediate && ss.set(APP_SESSION_CACHE_KEY, sessionMemory.getCache); 59 | } 60 | static clearSession(immediate = false){ 61 | sessionMemory.clear(); 62 | immediate && ss.clear(); 63 | } 64 | 65 | static clearAll(immediate = false) { 66 | sessionMemory.clear(); 67 | localMemory.clear(); 68 | if (immediate) { 69 | ls.clear(); 70 | ss.clear(); 71 | } 72 | } 73 | } 74 | 75 | window.addEventListener('beforeunload', function () { 76 | ls.set(APP_LOCAL_CACHE_KEY, { 77 | ...omit(localMemory.getCache, LOCK_INFO_KEY), 78 | ...pick(ls.get(APP_LOCAL_CACHE_KEY), [TOKEN_KEY, USER_INFO_KEY, LOCK_INFO_KEY]), 79 | }); 80 | ss.set(APP_SESSION_CACHE_KEY, { 81 | ...omit(sessionMemory.getCache, LOCK_INFO_KEY), 82 | ...pick(ss.get(APP_SESSION_CACHE_KEY), [TOKEN_KEY, USER_INFO_KEY, LOCK_INFO_KEY]), 83 | }); 84 | }); 85 | 86 | function storageChange(e) { 87 | const { key, newValue, oldValue } = e; 88 | 89 | if (!key) { 90 | Persistent.clearAll(); 91 | return; 92 | } 93 | 94 | if (!!newValue && !!oldValue) { 95 | if (APP_LOCAL_CACHE_KEY === key) { 96 | Persistent.clearLocal(); 97 | } 98 | if (APP_SESSION_CACHE_KEY === key) { 99 | Persistent.clearSession(); 100 | } 101 | } 102 | } 103 | 104 | window.addEventListener('storage', storageChange); 105 | 106 | initPersistentMemory(); 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueJS + Spring Security + OAuth2 + JWT + PostgreSQL template project (Draft) 2 | This is a template project for a VueJS frontend with a Spring Boot backend. The backend is secured with Spring Security and OAuth2. The frontend is secured with JWT. The backend uses PostgreSQL as a database. 3 | 4 | ## Stack 5 | - Java 21 6 | - VueJS (Quasar) 7 | - Spring Boot 3.3.5 8 | - Spring Security 6.3.4 9 | - PostgreSQL 10 | - Liquibase 11 | - Docker 12 | 13 | ## Prerequisites 14 | ### Install Docker Desktop 15 | 1. Go to the [Docker Desktop](https://www.docker.com/products/docker-desktop) website. 16 | 2. Download and install Docker Desktop. 17 | 3. Run Docker Desktop. 18 | 19 | ### Database setup 20 | 1. Run docker-compose to start a PostgreSQL database (database will be available by URL `jdbc:postgresql://localhost:5433/postgres`, username `postgres`, password `example`): 21 | ```shell 22 | docker-compose up -d 23 | ``` 24 | 2. Copy the username and password and add them to environment variables `POSTGRES_USER` and `POSTGRES_PASSWORD`. The `application.yaml` file is already configured to read these environment variables. 25 | 26 | ### JWT secret setup 27 | 1. Set any JWT key to the environment variable JWT_SECRET. The `application.yaml` file is already configured to read this environment variable. 28 | 29 | ### Google OAuth2 setup 30 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/). 31 | 2. Create a new project. 32 | 3. Go to the `APIs & Services` -> `Credentials` section. 33 | 4. Create a new OAuth Client ID. 34 | 6. Add `http://localhost:your_frontend_port/login` as Authorized JavaScript origins. Keep in mind the `your_frontend_port` must be the same as the frontend port that defined in the `quasar.conf.js` file, which is `9000` by default. 35 | 7. Add `http://localhost:your_backend_port/api/v1/login/oauth2/code/google` Authorized redirect URIs. Here port `your_backend_port` is the backend port that defined in the `application.yaml` file, which is `9090` by default. 36 | 8. Copy the `Client ID` and `Client Secret` and add them to environment variables GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. The `application.yaml` file is already configured to read these environment variables. 37 | 38 | 39 | ## Build 40 | ### Build backend 41 | Run the following command in the root directory of the project: 42 | ```shell 43 | maven clean install 44 | ``` 45 | 46 | ### Install frontend 47 | Run the following command in the root directory of the project: 48 | ```shell 49 | cd src/main/vue 50 | npm install 51 | ``` 52 | 53 | ## Run 54 | ### Backend 55 | Run the following command in the root directory of the project: 56 | ```shell 57 | java -jar target/auth-0.0.1-SNAPSHOT.jar 58 | ``` 59 | 60 | ### Frontend 61 | Run the following command in the root directory of the project: 62 | ```shell 63 | cd src/main/vue 64 | quasar dev 65 | ``` 66 | 67 | ## Usage 68 | ### Login with Google 69 | 1. Open the frontend in your browser: `http://localhost:9000/login`. 70 | 2. Click on the `Login with Google` button. 71 | 3. You will be redirected to the Google login page. 72 | 4. After successful login, you will be redirected back to the frontend. 73 | 74 | ### Register 75 | 1. Open the frontend in your browser: `http://localhost:9000/register`. 76 | 2. Fill in the form and click on the `Register` button. 77 | 3. You will be redirected to the login page. 78 | 4. Login with the credentials you just registered. 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/oauth2/Oauth2AuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.oauth2; 2 | 3 | import com.manunin.auth.exception.ErrorCode; 4 | import com.manunin.auth.exception.ServiceException; 5 | import com.manunin.auth.model.User; 6 | import com.manunin.auth.secutiry.jwt.JwtPair; 7 | import com.manunin.auth.secutiry.jwt.JwtTokenProvider; 8 | import com.manunin.auth.service.UserDetailsImpl; 9 | import com.manunin.auth.service.UserService; 10 | import jakarta.servlet.http.HttpServletRequest; 11 | import jakarta.servlet.http.HttpServletResponse; 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; 14 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 15 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 16 | import org.springframework.stereotype.Component; 17 | 18 | import javax.security.sasl.AuthenticationException; 19 | import java.io.IOException; 20 | 21 | @Component(value = "oauth2AuthenticationSuccessHandler") 22 | public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 23 | 24 | public static final String LOGIN_URL = "http://localhost:9000/login"; 25 | public static final String EMAIL_ATTRIBUTE = "email"; 26 | public static final String GIVEN_NAME_ATTRIBUTE = "given_name"; 27 | private final JwtTokenProvider tokenProvider; 28 | private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; 29 | 30 | private final UserService userService; 31 | 32 | public Oauth2AuthenticationSuccessHandler(final JwtTokenProvider tokenProvider, 33 | final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, 34 | final UserService userService) { 35 | this.tokenProvider = tokenProvider; 36 | this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; 37 | this.userService = userService; 38 | } 39 | 40 | @Override 41 | public void onAuthenticationSuccess(final HttpServletRequest request, 42 | final HttpServletResponse response, 43 | final Authentication authentication) throws IOException { 44 | 45 | OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; 46 | String email = token.getPrincipal().getAttribute(EMAIL_ATTRIBUTE); 47 | User user = getUser(token, email); 48 | JwtPair jwtPair = tokenProvider.generateTokenPair(UserDetailsImpl.build(user)); 49 | getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(LOGIN_URL, jwtPair)); 50 | } 51 | 52 | private User getUser(OAuth2AuthenticationToken token, String email) throws AuthenticationException { 53 | boolean existsByEmail = userService.existsByEmail(email); 54 | try { 55 | if (existsByEmail) { 56 | return userService.findByEmail(email); 57 | } 58 | return userService.addUser(new User(email, email, email, token.getPrincipal().getAttribute(GIVEN_NAME_ATTRIBUTE), token.getPrincipal().getAttribute("family_name"))); 59 | } catch (ServiceException e) { 60 | throw new AuthenticationException(ErrorCode.GENERAL.name(), new ServiceException(ErrorCode.GENERAL, "User with email " + email + " already exists")); 61 | } 62 | } 63 | 64 | String getRedirectUrl(final String baseUrl, final JwtPair tokenPair) { 65 | String url = baseUrl; 66 | if (baseUrl.indexOf("?") > 0) { 67 | url += "&"; 68 | } else { 69 | url += "/?"; 70 | } 71 | return url + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken(); 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/model/User.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.FetchType; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.JoinTable; 11 | import jakarta.persistence.ManyToMany; 12 | import jakarta.persistence.Table; 13 | 14 | import java.util.Collections; 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | 18 | @Entity 19 | @Table(name = "app_users") 20 | public class User { 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private long id; 24 | private String username; 25 | private String firstName; 26 | private String lastName; 27 | private String email; 28 | private String password; 29 | 30 | @Column(name = "IS_LOCKED") 31 | private boolean isLocked; 32 | 33 | @ManyToMany(fetch = FetchType.EAGER) 34 | @JoinTable( name = "user_roles", 35 | joinColumns = @JoinColumn(name = "user_id"), 36 | inverseJoinColumns = @JoinColumn(name = "role_id")) 37 | private Set roles = new HashSet<>(); 38 | 39 | public User() { 40 | } 41 | 42 | public User(final String username, final String email, final String password) { 43 | this.username = username; 44 | this.email = email; 45 | this.password = password; 46 | } 47 | 48 | public User(final String username, final String email, final String password, final String firstName, 49 | final String lastName) { 50 | this.username = username; 51 | this.email = email; 52 | this.password = password; 53 | this.firstName = firstName; 54 | this.lastName = lastName; 55 | } 56 | 57 | public User(long id, String username) { 58 | this.id = id; 59 | this.username = username; 60 | } 61 | 62 | public User(String username, String email, String firstName, String lastName, final boolean isLocked) { 63 | this.username = username; 64 | this.email = email; 65 | this.firstName = firstName; 66 | this.lastName = lastName; 67 | this.isLocked = isLocked; 68 | } 69 | 70 | public long getId() { 71 | return id; 72 | } 73 | 74 | public void setId(long id) { 75 | this.id = id; 76 | } 77 | 78 | public String getUsername() { 79 | return username; 80 | } 81 | 82 | public void setUsername(String username) { 83 | this.username = username; 84 | } 85 | 86 | public String getEmail() { 87 | return email; 88 | } 89 | 90 | public void setEmail(String email) { 91 | this.email = email; 92 | } 93 | 94 | public String getPassword() { 95 | return password; 96 | } 97 | 98 | public void setPassword(String password) { 99 | this.password = password; 100 | } 101 | 102 | public boolean isLocked() { 103 | return isLocked; 104 | } 105 | 106 | public void setLocked(boolean locked) { 107 | isLocked = locked; 108 | } 109 | 110 | public Set getRoles() { 111 | return Collections.unmodifiableSet(roles); 112 | } 113 | 114 | public void addRole(Role role) { 115 | roles.add(role); 116 | } 117 | 118 | public void setRoles(Set roles) { 119 | this.roles = new HashSet<>(roles); 120 | } 121 | 122 | public String getFirstName() { 123 | return firstName; 124 | } 125 | 126 | public void setFirstName(String firstName) { 127 | this.firstName = firstName; 128 | } 129 | 130 | public String getLastName() { 131 | return lastName; 132 | } 133 | 134 | public void setLastName(String lastName) { 135 | this.lastName = lastName; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/login/LoginAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.login; 2 | 3 | import com.manunin.auth.dto.SigninDTO; 4 | import com.manunin.auth.secutiry.exception.AuthMethodNotSupportedException; 5 | import com.manunin.auth.utils.JsonUtils; 6 | import jakarta.servlet.FilterChain; 7 | import jakarta.servlet.ServletException; 8 | import jakarta.servlet.http.HttpServletRequest; 9 | import jakarta.servlet.http.HttpServletResponse; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.http.HttpMethod; 14 | import org.springframework.security.authentication.AuthenticationServiceException; 15 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 16 | import org.springframework.security.core.Authentication; 17 | import org.springframework.security.core.AuthenticationException; 18 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 19 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 20 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 21 | 22 | import java.io.IOException; 23 | 24 | public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 25 | 26 | private Logger logger = LoggerFactory.getLogger(LoginAuthenticationFilter.class); 27 | private final AuthenticationSuccessHandler successHandler; 28 | private final AuthenticationFailureHandler failureHandler; 29 | public LoginAuthenticationFilter(final String defaultFilterProcessesUrl, 30 | final AuthenticationSuccessHandler successHandler, 31 | final AuthenticationFailureHandler failureHandler) { 32 | super(defaultFilterProcessesUrl); 33 | this.successHandler = successHandler; 34 | this.failureHandler = failureHandler; 35 | } 36 | 37 | @Override 38 | public Authentication attemptAuthentication(final HttpServletRequest request, 39 | final HttpServletResponse response) throws AuthenticationException { 40 | if (!HttpMethod.POST.name().equals(request.getMethod())) { 41 | if(logger.isDebugEnabled()) { 42 | logger.debug("Authentication method not supported. Request method: " + request.getMethod()); 43 | } 44 | throw new AuthMethodNotSupportedException("Authentication method not supported"); 45 | } 46 | 47 | SigninDTO signinDTO; 48 | try { 49 | signinDTO = JsonUtils.fromReader(request.getReader(), SigninDTO.class); 50 | } catch (Exception e) { 51 | throw new AuthenticationServiceException("Invalid login request payload"); 52 | } 53 | 54 | if (StringUtils.isBlank(signinDTO.getUsername()) || StringUtils.isEmpty(signinDTO.getPassword())) { 55 | throw new AuthenticationServiceException("Username or Password not provided"); 56 | } 57 | 58 | UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(signinDTO.getUsername(), signinDTO.getPassword()); 59 | token.setDetails(authenticationDetailsSource.buildDetails(request)); 60 | return this.getAuthenticationManager().authenticate(token); 61 | } 62 | 63 | @Override 64 | protected void successfulAuthentication(final HttpServletRequest request, 65 | final HttpServletResponse response, 66 | final FilterChain chain, Authentication authResult) throws IOException, ServletException { 67 | this.successHandler.onAuthenticationSuccess(request, response, authResult); 68 | } 69 | 70 | 71 | @Override 72 | protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { 73 | this.failureHandler.onAuthenticationFailure(request, response, failed); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/jwt/RefreshTokenAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.jwt; 2 | 3 | import com.manunin.auth.dto.RefreshTokenDTO; 4 | import com.manunin.auth.secutiry.exception.AuthMethodNotSupportedException; 5 | import com.manunin.auth.utils.JsonUtils; 6 | import jakarta.servlet.FilterChain; 7 | import jakarta.servlet.ServletException; 8 | import jakarta.servlet.http.HttpServletRequest; 9 | import jakarta.servlet.http.HttpServletResponse; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.springframework.http.HttpMethod; 12 | import org.springframework.security.authentication.AuthenticationServiceException; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.AuthenticationException; 15 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 16 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 17 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 18 | 19 | import java.io.IOException; 20 | 21 | public class RefreshTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 22 | 23 | private final AuthenticationSuccessHandler successHandler; 24 | 25 | private final AuthenticationFailureHandler failureHandler; 26 | 27 | public RefreshTokenAuthenticationFilter(final String url, 28 | final AuthenticationSuccessHandler successHandler, 29 | final AuthenticationFailureHandler failureHandler) { 30 | super(url); 31 | this.successHandler = successHandler; 32 | this.failureHandler = failureHandler; 33 | } 34 | 35 | @Override 36 | public Authentication attemptAuthentication(final HttpServletRequest request, 37 | final HttpServletResponse response) throws AuthenticationException { 38 | validateRequest(request); 39 | RefreshTokenDTO refreshTokenDto = getRefreshTokenDTO(request); 40 | validateRefreshToken(refreshTokenDto); 41 | return getAuthenticationManager().authenticate(new RefreshJwtAuthenticationToken(refreshTokenDto.getRefreshToken())); 42 | } 43 | 44 | private void validateRequest(final HttpServletRequest request) { 45 | if (!HttpMethod.POST.name().equals(request.getMethod())) { 46 | if(logger.isDebugEnabled()) { 47 | logger.debug("Authentication method not supported. Request method: " + request.getMethod()); 48 | } 49 | throw new AuthMethodNotSupportedException("Authentication method not supported"); 50 | } 51 | } 52 | 53 | private static void validateRefreshToken(final RefreshTokenDTO refreshTokenDto) { 54 | if (StringUtils.isBlank(refreshTokenDto.getRefreshToken())) { 55 | throw new AuthenticationServiceException("Username or Password not provided"); 56 | } 57 | } 58 | 59 | private static RefreshTokenDTO getRefreshTokenDTO(final HttpServletRequest request) { 60 | RefreshTokenDTO refreshTokenDto; 61 | try { 62 | refreshTokenDto = JsonUtils.fromReader(request.getReader(), RefreshTokenDTO.class); 63 | } catch (Exception e) { 64 | throw new AuthenticationServiceException("Invalid login request payload"); 65 | } 66 | return refreshTokenDto; 67 | } 68 | 69 | @Override 70 | protected void successfulAuthentication(final HttpServletRequest request, 71 | final HttpServletResponse response, 72 | final FilterChain chain, 73 | final Authentication authResult) throws IOException, ServletException { 74 | this.successHandler.onAuthenticationSuccess(request, response, authResult); 75 | } 76 | 77 | @Override 78 | protected void unsuccessfulAuthentication(final HttpServletRequest request, 79 | final HttpServletResponse response, 80 | final AuthenticationException failed) throws IOException, ServletException { 81 | this.failureHandler.onAuthenticationFailure(request, response, failed); 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/main/vue/src/assets/quasar-logo-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/exception/ErrorResponseHandler.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.exception; 2 | 3 | import com.manunin.auth.secutiry.exception.ExpiredTokenException; 4 | import com.manunin.auth.utils.JsonUtils; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.security.access.AccessDeniedException; 12 | import org.springframework.security.authentication.BadCredentialsException; 13 | import org.springframework.security.core.AuthenticationException; 14 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 15 | import org.springframework.security.web.access.AccessDeniedHandler; 16 | import org.springframework.web.bind.annotation.ExceptionHandler; 17 | import org.springframework.web.bind.annotation.RestControllerAdvice; 18 | 19 | import java.io.IOException; 20 | import java.io.PrintWriter; 21 | 22 | @RestControllerAdvice 23 | public class ErrorResponseHandler implements AccessDeniedHandler { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(ErrorResponseHandler.class); 26 | 27 | @ExceptionHandler(Exception.class) 28 | public void handle(final Exception exception, final HttpServletResponse response) { 29 | logger.debug("Processing exception {}", exception.getMessage(), exception); 30 | if (response.isCommitted()) { 31 | return; 32 | } 33 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 34 | if (exception instanceof AuthenticationException) { 35 | handleAuthenticationException((AuthenticationException) exception, response); 36 | } else if (exception instanceof ServiceException) { 37 | handleServiceException((ServiceException) exception, response); 38 | } else { 39 | handleInternalServerError(exception, response); 40 | } 41 | } 42 | 43 | private static void handleInternalServerError(Exception exception, HttpServletResponse response) { 44 | response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 45 | JsonUtils.writeValue(getWriter(response), ErrorResponse.of(exception.getMessage(), ErrorCode.GENERAL)); 46 | } 47 | 48 | private static void handleServiceException(ServiceException exception, HttpServletResponse response) { 49 | ErrorCode errorCode = exception.getErrorCode(); 50 | response.setStatus(errorCode.getStatus().value()); 51 | JsonUtils.writeValue(getWriter(response), ErrorResponse.of(exception.getMessage(), errorCode)); 52 | } 53 | 54 | private static PrintWriter getWriter(HttpServletResponse response) { 55 | try { 56 | return response.getWriter(); 57 | } catch (IOException e) { 58 | throw new RuntimeException(e); 59 | } 60 | } 61 | 62 | private static void handleAuthenticationException(final AuthenticationException authenticationException, 63 | final HttpServletResponse response) { 64 | response.setStatus(HttpStatus.UNAUTHORIZED.value()); 65 | if (authenticationException instanceof ExpiredTokenException) { 66 | JsonUtils.writeValue(getWriter(response), 67 | ErrorResponse.of("exception.tokenExpired", ErrorCode.JWT_TOKEN_EXPIRED)); 68 | } 69 | if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) { 70 | JsonUtils.writeValue(getWriter(response), 71 | ErrorResponse.of("exception.badCredentials", ErrorCode.AUTHENTICATION)); 72 | } else { 73 | JsonUtils.writeValue(getWriter(response), 74 | ErrorResponse.of("exception.authenticationFailed", ErrorCode.AUTHENTICATION)); 75 | } 76 | } 77 | 78 | @Override 79 | public void handle(final HttpServletRequest request, 80 | final HttpServletResponse response, 81 | final AccessDeniedException accessDeniedException) { 82 | if (!response.isCommitted()) { 83 | response.setStatus(HttpStatus.FORBIDDEN.value()); 84 | JsonUtils.writeValue(getWriter(response), ErrorResponse.of("exception.accessDenied", ErrorCode.ACCESS_DENIED)); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/vue/src/pages/Register/RegisterPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ $t('appName') }} 5 | 6 | 7 | 8 | 9 | {{ t('registerPage.fields.formTitle') }} 10 | 11 | 12 | 13 | 21 | 29 | 34 | 39 | 46 | 53 | 58 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 119 | -------------------------------------------------------------------------------- /src/main/vue/src/pages/Login/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{$t('appName')}} 5 | 6 | 7 | 11 | 12 | {{ t('loginPage.fields.formTitle') }} 13 | 14 | 15 | 16 | 23 | 30 | 34 | 38 | 39 | {{ t('loginPage.registration.message') }} 40 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 143 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/secutiry/jwt/JwtTokenProvider.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.secutiry.jwt; 2 | 3 | import com.manunin.auth.secutiry.exception.ExpiredTokenException; 4 | import io.jsonwebtoken.ExpiredJwtException; 5 | import io.jsonwebtoken.Jwts; 6 | import io.jsonwebtoken.MalformedJwtException; 7 | import io.jsonwebtoken.SignatureAlgorithm; 8 | import io.jsonwebtoken.SignatureException; 9 | import io.jsonwebtoken.UnsupportedJwtException; 10 | import jakarta.servlet.http.HttpServletRequest; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.security.authentication.AuthenticationServiceException; 15 | import org.springframework.security.authentication.BadCredentialsException; 16 | import org.springframework.security.core.userdetails.UserDetails; 17 | import org.springframework.security.core.userdetails.UserDetailsService; 18 | import org.springframework.stereotype.Component; 19 | import org.springframework.util.StringUtils; 20 | 21 | import java.util.Date; 22 | 23 | @Component 24 | public class JwtTokenProvider { 25 | 26 | private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class); 27 | public static final String HEADER = "Authorization"; 28 | public static final String JWT_TOKEN_HEADER_PARAM = HEADER; 29 | public static final String HEADER_PREFIX = "Bearer "; 30 | private final UserDetailsService userDetailsService; 31 | @Value("${security.jwt.secret}") 32 | private String jwtSecret; 33 | 34 | @Value("${security.jwt.tokenExpirationTime}") 35 | private int tokenExpirationInSec; 36 | 37 | @Value("${security.jwt.refreshTokenExpirationTime}") 38 | private int refreshTokenExpirationInSec; 39 | 40 | public JwtTokenProvider(final UserDetailsService userDetailsService) { 41 | this.userDetailsService = userDetailsService; 42 | } 43 | 44 | public JwtPair generateTokenPair(final UserDetails user) { 45 | String token = createToken(user); 46 | String refreshToken = createRefreshToken(user); 47 | return new JwtPair(token, refreshToken); 48 | } 49 | 50 | private String createRefreshToken(final UserDetails user) { 51 | return Jwts.builder() 52 | .setSubject(user.getUsername()) 53 | .setIssuedAt(new Date()) 54 | .setExpiration(getExpiryDate(refreshTokenExpirationInSec)) 55 | .signWith(SignatureAlgorithm.HS512, jwtSecret) 56 | .compact(); 57 | } 58 | 59 | private String createToken(final UserDetails user) { 60 | return Jwts.builder() 61 | .setSubject(user.getUsername()) 62 | .setIssuedAt(new Date()) 63 | .setExpiration(getExpiryDate(tokenExpirationInSec)) 64 | .signWith(SignatureAlgorithm.HS512, jwtSecret) 65 | .compact(); 66 | } 67 | 68 | private Date getExpiryDate(final int tokenExpirationInSec) { 69 | Date now = new Date(); 70 | return new Date(now.getTime() + tokenExpirationInSec * 1000L); 71 | } 72 | 73 | public UserDetails parseJwtToken(final String accessToken) { 74 | UserDetails userDetails = null; 75 | if (StringUtils.hasText(accessToken) && validateToken(accessToken)) { 76 | String username = getUserNameFromJwtToken(accessToken); 77 | userDetails = userDetailsService.loadUserByUsername(username); 78 | } 79 | return userDetails; 80 | } 81 | 82 | public String getUserNameFromJwtToken(final String token) { 83 | return Jwts.parser() 84 | .setSigningKey(jwtSecret) 85 | .parseClaimsJws(token) 86 | .getBody().getSubject(); 87 | } 88 | 89 | public boolean validateToken(final String authToken) { 90 | try { 91 | Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); 92 | return true; 93 | } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { 94 | logger.debug("Invalid JWT Token", ex); 95 | throw new BadCredentialsException("Invalid JWT token: ", ex); 96 | } catch (SignatureException | ExpiredJwtException expiredEx) { 97 | logger.debug("JWT Token is expired", expiredEx); 98 | throw new ExpiredTokenException(authToken, "JWT Token expired", expiredEx); 99 | } 100 | } 101 | 102 | public String getTokenFromRequest(final HttpServletRequest request) { 103 | String header = request.getHeader(JWT_TOKEN_HEADER_PARAM); 104 | if (org.apache.commons.lang3.StringUtils.isBlank(header)) { 105 | throw new AuthenticationServiceException("Authorization header cannot be blank!"); 106 | } 107 | if (header.length() < HEADER_PREFIX.length()) { 108 | throw new AuthenticationServiceException("Invalid authorization header size."); 109 | } 110 | return header.substring(HEADER_PREFIX.length()); 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.5 9 | 10 | 11 | com.manunin.basic 12 | auth 13 | 1.0.1-SNAPSHOT 14 | auth 15 | Auth module 16 | 17 | 21 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-actuator 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-security 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-oauth2-client 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 | org.springframework.boot 42 | spring-boot-starter-data-jpa 43 | 44 | 45 | 46 | 47 | 48 | com.h2database 49 | h2 50 | 2.2.220 51 | 52 | 53 | org.postgresql 54 | postgresql 55 | runtime 56 | 57 | 58 | org.liquibase 59 | liquibase-core 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-test 64 | test 65 | 66 | 67 | 68 | 69 | 70 | org.springframework.security 71 | spring-security-test 72 | test 73 | 74 | 75 | io.jsonwebtoken 76 | jjwt 77 | 0.9.1 78 | 79 | 80 | 81 | 82 | 83 | org.springdoc 84 | springdoc-openapi-ui 85 | 1.6.15 86 | 87 | 88 | org.springframework.boot 89 | spring-boot-configuration-processor 90 | true 91 | 92 | 93 | 94 | org.mapstruct 95 | mapstruct 96 | 1.4.1.Final 97 | 98 | 99 | 100 | 101 | com.google.guava 102 | guava 103 | 32.0.0-android 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.springframework.boot 112 | spring-boot-maven-plugin 113 | 114 | com.manunin.auth.AuthorizationService 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-compiler-plugin 120 | 3.5.1 121 | 122 | 123 | 124 | org.mapstruct 125 | mapstruct-processor 126 | 1.4.1.Final 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/main/vue/quasar.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /* 4 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 5 | * the ES6 features that are supported by your Node version. https://node.green/ 6 | */ 7 | 8 | // Configuration for your app 9 | // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js 10 | 11 | 12 | const { configure } = require('quasar/wrappers'); 13 | // const {Dialog, Notify } = require('quasar'); 14 | 15 | 16 | module.exports = configure(function (/* ctx */) { 17 | return { 18 | eslint: { 19 | // fix: true, 20 | // include = [], 21 | // exclude = [], 22 | // rawOptions = {}, 23 | warnings: true, 24 | errors: true 25 | }, 26 | 27 | // https://v2.quasar.dev/quasar-cli/prefetch-feature 28 | // preFetch: true, 29 | 30 | // app boot file (/src/boot) 31 | // --> boot files are part of "main.js" 32 | // https://v2.quasar.dev/quasar-cli/boot-files 33 | boot: [ 34 | 'i18n', 35 | 'global-components', 36 | 'router', 37 | ], 38 | 39 | // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css 40 | css: [ 41 | 'app.scss' 42 | ], 43 | 44 | // https://github.com/quasarframework/quasar/tree/dev/extras 45 | extras: [ 46 | // 'ionicons-v4', 47 | // 'mdi-v5', 48 | // 'fontawesome-v6', 49 | // 'eva-icons', 50 | // 'themify', 51 | // 'line-awesome', 52 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 53 | 54 | 'roboto-font', // optional, you are not bound to it 55 | 'material-icons', // optional, you are not bound to it 56 | ], 57 | 58 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build 59 | build: { 60 | target: { 61 | browser: [ 'es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1' ], 62 | node: 'node16' 63 | }, 64 | 65 | vueRouterMode: 'history', // available values: 'hash', 'history' 66 | // vueRouterBase, 67 | // vueDevtools, 68 | // vueOptionsAPI: false, 69 | 70 | // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup 71 | 72 | // publicPath: '/', 73 | // analyze: true, 74 | env: { 75 | API_URL: 76 | process.env.NODE_ENV === "production" 77 | ? "https://someProdResource.com/api/v1" 78 | : "http://localhost:9090/api/v1", 79 | REG_URL: 'groups.manunin.com/reg/', 80 | GLOBAL_APP_NAME: 'replace', 81 | }, 82 | // rawDefine: {} 83 | // ignorePublicFolder: true, 84 | // minify: false, 85 | // polyfillModulePreload: true, 86 | // distDir 87 | 88 | // extendViteConf (viteConf) {}, 89 | // viteVuePluginOptions: {}, 90 | 91 | 92 | // vitePlugins: [ 93 | // [ 'package-name', { ..options.. } ] 94 | // ] 95 | }, 96 | 97 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer 98 | devServer: { 99 | // https: true 100 | open: true, // opens browser window automatically 101 | }, 102 | 103 | // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework 104 | framework: { 105 | config: { 106 | }, 107 | 108 | // iconSet: 'material-icons', // Quasar icon set 109 | // lang: 'en-US', // Quasar language pack 110 | 111 | // For special cases outside of where the auto-import strategy can have an impact 112 | // (like functional components as one of the examples), 113 | // you can manually specify Quasar components/directives to be available everywhere: 114 | // 115 | // components: [], 116 | // directives: [], 117 | 118 | // Quasar plugins 119 | plugins: ['Notify', 'Dialog'] 120 | }, 121 | 122 | // animations: 'all', // --- includes all animations 123 | // https://v2.quasar.dev/options/animations 124 | animations: [ 125 | 'slideInRight', 126 | 'slideOutRight', 127 | ], 128 | 129 | // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles 130 | // sourceFiles: { 131 | // rootComponent: 'src/App.vue', 132 | // router: 'src/router/index', 133 | // store: 'src/store/index', 134 | // registerServiceWorker: 'src-pwa/register-service-worker', 135 | // serviceWorker: 'src-pwa/custom-service-worker', 136 | // pwaManifestFile: 'src-pwa/manifest.json', 137 | // electronMain: 'src-electron/electron-main', 138 | // electronPreload: 'src-electron/electron-preload' 139 | // }, 140 | 141 | // https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr 142 | ssr: { 143 | // ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name! 144 | // will mess up SSR 145 | 146 | // extendSSRWebserverConf (esbuildConf) {}, 147 | // extendPackageJson (json) {}, 148 | 149 | pwa: false, 150 | 151 | // manualStoreHydration: true, 152 | // manualPostHydrationTrigger: true, 153 | 154 | prodPort: 3000, // The default port that the production server should use 155 | // (gets superseded if process.env.PORT is specified at runtime) 156 | 157 | middlewares: [ 158 | 'render' // keep this as last one 159 | ] 160 | }, 161 | 162 | // https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa 163 | pwa: { 164 | workboxMode: 'generateSW', // or 'injectManifest' 165 | injectPwaMetaTags: true, 166 | swFilename: 'sw.js', 167 | manifestFilename: 'manifest.json', 168 | useCredentialsForManifestTag: false, 169 | // useFilenameHashes: true, 170 | // extendGenerateSWOptions (cfg) {} 171 | // extendInjectManifestOptions (cfg) {}, 172 | // extendManifestJson (json) {} 173 | // extendPWACustomSWConf (esbuildConf) {} 174 | }, 175 | 176 | // Full list of options: https://v2.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 177 | cordova: { 178 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 179 | }, 180 | 181 | // Full list of options: https://v2.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 182 | capacitor: { 183 | hideSplashscreen: true 184 | }, 185 | 186 | // Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 187 | electron: { 188 | // extendElectronMainConf (esbuildConf) 189 | // extendElectronPreloadConf (esbuildConf) 190 | 191 | inspectPort: 5858, 192 | 193 | bundler: 'packager', // 'packager' or 'builder' 194 | 195 | packager: { 196 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 197 | 198 | // OS X / Mac App Store 199 | // appBundleId: '', 200 | // appCategoryType: '', 201 | // osxSign: '', 202 | // protocol: 'myapp://path', 203 | 204 | // Windows only 205 | // win32metadata: { ... } 206 | }, 207 | 208 | builder: { 209 | // https://www.electron.build/configuration/configuration 210 | 211 | appId: 'replace', 212 | 213 | } 214 | }, 215 | 216 | // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex 217 | bex: { 218 | contentScripts: [ 219 | 'my-content-script' 220 | ], 221 | 222 | // extendBexScriptsConf (esbuildConf) {} 223 | // extendBexManifestJson (json) {} 224 | } 225 | } 226 | }); 227 | -------------------------------------------------------------------------------- /src/main/java/com/manunin/auth/configuration/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.manunin.auth.configuration; 2 | 3 | import com.manunin.auth.exception.ErrorResponseHandler; 4 | import com.manunin.auth.secutiry.jwt.JwtTokenProvider; 5 | import com.manunin.auth.secutiry.jwt.RefreshTokenAuthenticationFilter; 6 | import com.manunin.auth.secutiry.jwt.TokenAuthenticationFilter; 7 | import com.manunin.auth.secutiry.login.LoginAuthenticationFilter; 8 | import com.manunin.auth.secutiry.matcher.SkipPathRequestMatcher; 9 | import org.springframework.beans.factory.annotation.Qualifier; 10 | import org.springframework.boot.autoconfigure.security.SecurityProperties; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.annotation.Order; 14 | import org.springframework.security.authentication.AuthenticationManager; 15 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 16 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 17 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 18 | import org.springframework.security.config.http.SessionCreationPolicy; 19 | import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; 20 | import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; 21 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; 22 | import org.springframework.security.web.SecurityFilterChain; 23 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 24 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 25 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 26 | import org.springframework.web.cors.CorsConfiguration; 27 | import org.springframework.web.cors.CorsConfigurationSource; 28 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 29 | 30 | import java.util.ArrayList; 31 | import java.util.Arrays; 32 | import java.util.List; 33 | 34 | @Configuration 35 | @EnableWebSecurity 36 | @Order(SecurityProperties.BASIC_AUTH_ORDER) 37 | public class SecurityConfiguration { 38 | 39 | public static final String SIGNIN_ENTRY_POINT = "/auth/signin"; 40 | public static final String SIGNUP_ENTRY_POINT = "/auth/signup"; 41 | public static final String SWAGGER_ENTRY_POINT = "/swagger-ui/**"; 42 | public static final String API_DOCS_ENTRY_POINT = "/api-docs/**"; 43 | public static final String TOKEN_REFRESH_ENTRY_POINT = "/auth/refreshToken"; 44 | private final JwtTokenProvider jwtTokenProvider; 45 | private final AuthenticationManager authenticationManager; 46 | private final AuthenticationSuccessHandler authenticationSuccessHandler; 47 | private final AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler; 48 | 49 | private final ErrorResponseHandler accessDeniedHandler; 50 | 51 | private final AuthenticationFailureHandler failureHandler; 52 | 53 | public SecurityConfiguration(final JwtTokenProvider jwtTokenProvider, 54 | final AuthenticationManager authenticationManager, 55 | @Qualifier("loginAuthenticationSuccessHandler") final AuthenticationSuccessHandler authenticationSuccessHandler, 56 | @Qualifier("oauth2AuthenticationSuccessHandler") final AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler, 57 | final ErrorResponseHandler accessDeniedHandler, 58 | final AuthenticationFailureHandler failureHandler) { 59 | this.jwtTokenProvider = jwtTokenProvider; 60 | this.authenticationManager = authenticationManager; 61 | this.authenticationSuccessHandler = authenticationSuccessHandler; 62 | this.oauth2AuthenticationSuccessHandler = oauth2AuthenticationSuccessHandler; 63 | this.accessDeniedHandler = accessDeniedHandler; 64 | this.failureHandler = failureHandler; 65 | } 66 | 67 | @Bean 68 | CorsConfigurationSource corsConfigurationSource() { 69 | CorsConfiguration configuration = new CorsConfiguration(); 70 | configuration.setAllowedOrigins(List.of("*")); 71 | configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); 72 | configuration.setAllowedHeaders(List.of("*")); 73 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 74 | source.registerCorsConfiguration("/**", configuration); 75 | return source; 76 | } 77 | 78 | @Bean 79 | SecurityFilterChain filterChain(final HttpSecurity http) throws Exception { 80 | http 81 | .cors(cors -> cors.configurationSource(corsConfigurationSource())) 82 | .csrf(AbstractHttpConfigurer::disable) 83 | .exceptionHandling(configurer -> configurer 84 | .accessDeniedHandler(accessDeniedHandler)) 85 | .sessionManagement(configurer -> configurer 86 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 87 | .authorizeHttpRequests(authorize -> authorize 88 | .requestMatchers(SIGNIN_ENTRY_POINT).permitAll() 89 | .requestMatchers(SIGNUP_ENTRY_POINT).permitAll() 90 | .requestMatchers(SWAGGER_ENTRY_POINT).permitAll() 91 | .requestMatchers(API_DOCS_ENTRY_POINT).permitAll() 92 | .requestMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() 93 | .anyRequest().authenticated() 94 | ) 95 | .addFilterBefore(buildLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) 96 | .addFilterBefore(buildTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) 97 | .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class); 98 | http.oauth2Login(configurer -> configurer 99 | .authorizationEndpoint(config -> config 100 | .authorizationRequestRepository(authorizationRequestRepository())) 101 | .failureHandler(failureHandler) 102 | .successHandler(oauth2AuthenticationSuccessHandler)); 103 | 104 | return http.build(); 105 | } 106 | 107 | @Bean 108 | public AuthorizationRequestRepository authorizationRequestRepository() { 109 | return new HttpSessionOAuth2AuthorizationRequestRepository(); 110 | } 111 | 112 | protected TokenAuthenticationFilter buildTokenAuthenticationFilter() { 113 | List pathsToSkip = new ArrayList<>(Arrays.asList(SIGNIN_ENTRY_POINT, SIGNUP_ENTRY_POINT, 114 | SWAGGER_ENTRY_POINT, API_DOCS_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT)); 115 | SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip); 116 | TokenAuthenticationFilter filter = new TokenAuthenticationFilter(jwtTokenProvider, matcher, failureHandler); 117 | filter.setAuthenticationManager(this.authenticationManager); 118 | return filter; 119 | } 120 | 121 | @Bean 122 | protected LoginAuthenticationFilter buildLoginProcessingFilter() { 123 | LoginAuthenticationFilter filter = new LoginAuthenticationFilter(SIGNIN_ENTRY_POINT, 124 | authenticationSuccessHandler, failureHandler); 125 | filter.setAuthenticationManager(this.authenticationManager); 126 | return filter; 127 | } 128 | 129 | @Bean 130 | protected RefreshTokenAuthenticationFilter buildRefreshTokenProcessingFilter() { 131 | RefreshTokenAuthenticationFilter filter = new RefreshTokenAuthenticationFilter(TOKEN_REFRESH_ENTRY_POINT, 132 | authenticationSuccessHandler, failureHandler); 133 | filter.setAuthenticationManager(this.authenticationManager); 134 | return filter; 135 | } 136 | } 137 | --------------------------------------------------------------------------------