
+++ client/resources/api/index.js
... | ... | @@ -0,0 +1,104 @@ |
1 | +import axios from 'axios'; | |
2 | +import store from "../../views/pages/AppStore"; | |
3 | + | |
4 | +// 공통 API 요청 인터셉터 설정 | |
5 | +const apiClient = axios.create({ | |
6 | + baseURL: '/', | |
7 | + headers: { | |
8 | + 'Content-Type': 'application/json; charset=UTF-8', | |
9 | + } | |
10 | +}); | |
11 | + | |
12 | +// 요청 인터셉터 | |
13 | +apiClient.interceptors.request.use( | |
14 | + config => { | |
15 | + // 요청 전에 토큰을 헤더에 추가 | |
16 | + const token = store.state.authorization; | |
17 | + if (token) { | |
18 | + config.headers.Authorization = store.state.authorization; | |
19 | + } | |
20 | + return config; | |
21 | + }, | |
22 | + error => { | |
23 | + return Promise.reject(error); | |
24 | + } | |
25 | +); | |
26 | + | |
27 | +// 응답 인터셉터 | |
28 | +apiClient.interceptors.response.use( | |
29 | + response => response, | |
30 | + async error => { | |
31 | + if (!error.response) return Promise.reject(error); | |
32 | + | |
33 | + const { status, data } = error.response; | |
34 | + const originalReq = error.config; | |
35 | + | |
36 | + // 로그인 요청은 토큰 리프레시 대상이 아님 | |
37 | + if (originalReq?.skipAuthRefresh) { | |
38 | + return Promise.reject(error); | |
39 | + } | |
40 | + | |
41 | + // 권한 없음 | |
42 | + if (status === 403 && data.message === '접근 권한이 없습니다.') { | |
43 | + window.history.back(); | |
44 | + return Promise.reject(error); | |
45 | + } | |
46 | + | |
47 | + // 리프레시 토큰 요청은 재시도하지 않음 | |
48 | + if (originalReq.url.includes('/refresh/tknReissue.json')) { | |
49 | + return Promise.reject(error); | |
50 | + } | |
51 | + | |
52 | + // 토큰 만료 시 한 번만 재시도 | |
53 | + if (status === 401 && !originalReq._retry) { | |
54 | + originalReq._retry = true; | |
55 | + | |
56 | + try { | |
57 | + // 리프레시 요청은 별도 인스턴스로 처리 (apiClient로 하면 무한 루프 위험) | |
58 | + const refreshClient = axios.create(); | |
59 | + const res = await refreshClient.post('/refresh/tknReissue.json'); | |
60 | + const newToken = res.headers.authorization; | |
61 | + | |
62 | + // 토큰 저장 | |
63 | + store.commit('setAuthorization', newToken); | |
64 | + originalReq.headers.Authorization = newToken; | |
65 | + | |
66 | + // 유저 정보 다시 저장 | |
67 | + /** jwt토큰 디코딩 **/ | |
68 | + const base64String = store.state.authorization.split('.')[1]; | |
69 | + const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/'); | |
70 | + const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { | |
71 | + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | |
72 | + }).join('')); | |
73 | + const user = JSON.parse(jsonPayload); | |
74 | + store.commit("setUserInfo", { | |
75 | + userId: user.userId, | |
76 | + loginId: user.loginId, | |
77 | + userNm: user.userNm, | |
78 | + roles: Array.isArray(user.roles) ? user.roles.map(r => r.authority) : [], | |
79 | + }); | |
80 | + | |
81 | + // 실패했던 요청 재시도 | |
82 | + return apiClient(originalReq); | |
83 | + } catch (refreshError) { | |
84 | + // 로그인 요청은 리프레시 실패 시에도 처리 | |
85 | + if (originalReq?.url?.includes('/login.json')) { | |
86 | + return Promise.reject(refreshError); | |
87 | + } | |
88 | + | |
89 | + // 리프레시 실패 - 세션 만료 처리 | |
90 | + sessionStorage.setItem("redirect", window.location.pathname + window.location.search); | |
91 | + alert('세션이 종료 되었습니다.\n로그인을 새로 해주세요.'); | |
92 | + store.commit("setStoreReset"); | |
93 | + localStorage.clear(); | |
94 | + sessionStorage.clear(); | |
95 | + window.location.href = '/login.page'; | |
96 | + return Promise.reject(refreshError); | |
97 | + } | |
98 | + } | |
99 | + | |
100 | + return Promise.reject(error); | |
101 | + } | |
102 | +); | |
103 | + | |
104 | +export default apiClient;(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/api/login.js
... | ... | @@ -0,0 +1,9 @@ |
1 | +import apiClient from "./index"; | |
2 | + | |
3 | +export const loginProc = userInfo => { | |
4 | + return apiClient.post('/user/login.json', userInfo, {skipAuthRefresh: true}); | |
5 | +} | |
6 | + | |
7 | +export const logoutProc = () => { | |
8 | + return apiClient.post('/user/logout.json'); | |
9 | +}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/index.js
+++ client/views/index.js
... | ... | @@ -7,9 +7,10 @@ |
7 | 7 |
|
8 | 8 |
import AppRouter from './pages/AppRouter.js'; |
9 | 9 |
import App from './pages/App.vue'; |
10 |
+import store from "./pages/AppStore.js"; |
|
10 | 11 |
import "../resources/scss/main.scss"; |
11 | 12 |
|
12 | 13 |
|
13 |
-const vue = createApp(App).use(AppRouter).mount('#root'); |
|
14 |
+const vue = createApp(App).use(AppRouter).use(store).mount('#root'); |
|
14 | 15 |
|
15 | 16 |
|
--- client/views/layout/Header.vue
+++ client/views/layout/Header.vue
... | ... | @@ -1,40 +1,51 @@ |
1 | 1 |
<template> |
2 |
- <header> |
|
3 |
- <div class="title"><router-link to="/"><img :src="logo" alt="로고"></router-link></div> |
|
4 |
- <Menu></Menu> |
|
5 |
- <div class="user-info"> |
|
6 |
- <div class="user-name"> <router-link |
|
7 |
- to="/MyPage.page"><img :src="accounticon" alt="">관리자</router-link></div> |
|
8 |
- <button @click="logout" class="user-logout"><img :src="logouticon" alt=""></button> |
|
9 |
- </div> |
|
10 |
- </header> |
|
2 |
+ <header> |
|
3 |
+ <div class="title"><router-link to="/"><img :src="logo" alt="로고"></router-link></div> |
|
4 |
+ <Menu></Menu> |
|
5 |
+ <div class="user-info"> |
|
6 |
+ <div class="user-name"> |
|
7 |
+ <router-link to="/MyPage.page"><img :src="accounticon" alt="">관리자</router-link> |
|
8 |
+ </div> |
|
9 |
+ <button @click="logout" class="user-logout"><img :src="logouticon" alt=""></button> |
|
10 |
+ </div> |
|
11 |
+ </header> |
|
11 | 12 |
</template> |
12 | 13 |
|
13 | 14 |
<script> |
14 | 15 |
import Menu from '../layout/Menu.vue'; |
15 |
-export default { |
|
16 |
- data() { |
|
17 |
- return { |
|
18 |
- logo: "/client/resources/img/logo.png", |
|
19 |
- accounticon: "/client/resources/img/account.png", |
|
20 |
- logouticon: "/client/resources/img/logout.png", |
|
21 |
- }; |
|
22 |
- }, |
|
23 |
- methods: { |
|
24 |
- logout(){ |
|
25 |
- localStorage.removeItem('isLoggedIn'); |
|
26 |
-this.$router.push('/login.page'); |
|
16 |
+import { logoutProc } from "../../resources/api/login.js"; |
|
27 | 17 |
|
28 |
- } |
|
29 |
- }, |
|
30 |
- watch: {}, |
|
31 |
- computed: {}, |
|
32 |
- components: { |
|
33 |
- 'Menu': Menu, |
|
34 |
- }, |
|
35 |
- created() {}, |
|
36 |
- mounted() {}, |
|
37 |
- beforeUnmount() {}, |
|
18 |
+export default { |
|
19 |
+ data() { |
|
20 |
+ return { |
|
21 |
+ logo: "/client/resources/img/logo.png", |
|
22 |
+ accounticon: "/client/resources/img/account.png", |
|
23 |
+ logouticon: "/client/resources/img/logout.png", |
|
24 |
+ }; |
|
25 |
+ }, |
|
26 |
+ methods: { |
|
27 |
+ async logout(){ |
|
28 |
+ try{ |
|
29 |
+ const response = await logoutProc(); |
|
30 |
+ // 스토어 초기화 |
|
31 |
+ this.$store.commit('setStoreReset'); |
|
32 |
+ |
|
33 |
+ localStorage.removeItem('isLoggedIn'); |
|
34 |
+ this.$router.push('/login.page'); |
|
35 |
+ } catch (error) { |
|
36 |
+ console.error("로그아웃 중 오류 발생:", error); |
|
37 |
+ alert("로그아웃에 실패했습니다. 다시 시도해주세요."); |
|
38 |
+ } |
|
39 |
+ } |
|
40 |
+ }, |
|
41 |
+ watch: {}, |
|
42 |
+ computed: {}, |
|
43 |
+ components: { |
|
44 |
+ 'Menu': Menu, |
|
45 |
+ }, |
|
46 |
+ created() {}, |
|
47 |
+ mounted() {}, |
|
48 |
+ beforeUnmount() {}, |
|
38 | 49 |
}; |
39 | 50 |
|
40 | 51 |
</script>(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/pages/AppStore.js
... | ... | @@ -0,0 +1,43 @@ |
1 | +import { createStore } from "vuex"; | |
2 | +import createPersistedState from "vuex-persistedstate"; | |
3 | + | |
4 | +export default createStore({ | |
5 | + plugins: [createPersistedState({ | |
6 | + paths: ['userInfo', 'roles', 'pageAuth'], | |
7 | + })], | |
8 | + state: { | |
9 | + authorization: null, | |
10 | + userInfo: { | |
11 | + userId: null, | |
12 | + loginId: null, | |
13 | + userNm: null, | |
14 | + roles: ['ROLE_NONE'], | |
15 | + }, | |
16 | + }, | |
17 | + getters: { | |
18 | + | |
19 | + }, | |
20 | + mutations: { | |
21 | + setAuthorization(state, token) { | |
22 | + state.authorization = token; | |
23 | + }, | |
24 | + setUserInfo(state, userInfo) { | |
25 | + state.userInfo = userInfo; | |
26 | + }, | |
27 | + setUserNm(state, userNm) { | |
28 | + state.userInfo.userNm = userNm; | |
29 | + }, | |
30 | + setStoreReset(state) { | |
31 | + state.authorization = null; | |
32 | + state.userInfo = { | |
33 | + userNm: null, | |
34 | + loginId: null, | |
35 | + userId: null, | |
36 | + roles: ['ROLE_NONE'], | |
37 | + }; | |
38 | + }, | |
39 | + }, | |
40 | + actions: { | |
41 | + | |
42 | + }, | |
43 | +}); |
--- client/views/pages/User/Login.vue
+++ client/views/pages/User/Login.vue
... | ... | @@ -10,13 +10,13 @@ |
10 | 10 |
<div class="box"> |
11 | 11 |
<label for="yourUsername" class="form-label"><img :src="idIcon" alt="아이디 아이콘">아이디</label> |
12 | 12 |
<div class="input-group has-validation"> |
13 |
- <input v-model="id" type="text" name="username" class="form-control" id="yourUsername" placeholder="아이디를 입력하세요." required> |
|
13 |
+ <input v-model="userInfo.loginId" ref="loginId" type="text" name="username" class="form-control" id="yourUsername" placeholder="아이디를 입력하세요." required> |
|
14 | 14 |
</div> |
15 | 15 |
</div> |
16 | 16 |
|
17 | 17 |
<div class="box"> |
18 | 18 |
<label for="yourPassword" class="form-label"><img :src="passwordIcon" alt="비밀번호 아이콘">비밀번호</label> |
19 |
- <input v-model="password" type="password" name="password" class="form-control" id="yourPassword" placeholder="비밀번호를 입력하세요." required> |
|
19 |
+ <input v-model="userInfo.password" ref="password" type="password" name="password" class="form-control" id="yourPassword" placeholder="비밀번호를 입력하세요." required> |
|
20 | 20 |
</div> |
21 | 21 |
|
22 | 22 |
<div class="box"> |
... | ... | @@ -29,6 +29,8 @@ |
29 | 29 |
</template> |
30 | 30 |
|
31 | 31 |
<script> |
32 |
+import { loginProc } from "../../../resources/api/login.js"; |
|
33 |
+ |
|
32 | 34 |
export default { |
33 | 35 |
data() { |
34 | 36 |
return { |
... | ... | @@ -38,24 +40,64 @@ |
38 | 40 |
loginIcon: "/client/resources/img/loginicon.png", |
39 | 41 |
formSubmitted: false, |
40 | 42 |
logo: "/client/resources/img/logo.png", // 경로를 Vue 프로젝트 구조에 맞게 설정 |
41 |
- id: "admin", // 임시 아이디 |
|
42 |
- password: "1234", // 임시 비밀번호 |
|
43 |
+ // id: "admin", // 임시 아이디 |
|
44 |
+ // password: "1234", // 임시 비밀번호 |
|
45 |
+ userInfo:{ |
|
46 |
+ loginId: "", |
|
47 |
+ password: "", |
|
48 |
+ } |
|
43 | 49 |
}; |
44 | 50 |
}, |
45 | 51 |
methods: { |
46 |
- handleLogin() { |
|
47 |
- this.formSubmitted = true; |
|
48 |
- |
|
49 |
- // Check if credentials are correct |
|
50 |
- if (this.id === "admin" && this.password === "1234") { |
|
51 |
- // Set logged in status in localStorage |
|
52 |
- localStorage.setItem('isLoggedIn', 'true'); |
|
53 |
- |
|
54 |
- // Redirect to the main page after successful login |
|
55 |
- this.$router.push('/'); |
|
56 |
- } else { |
|
57 |
- // Handle incorrect login attempt |
|
58 |
- console.log("아이디 또는 비밀번호가 틀렸습니다."); |
|
52 |
+ // 유효성 검사 함수 |
|
53 |
+ validation(){ |
|
54 |
+ if(this.userInfo.loginId === ""){ |
|
55 |
+ alert("아이디를 입력해주세요."); |
|
56 |
+ this.$refs.loginId.focus(); |
|
57 |
+ return false; |
|
58 |
+ } |
|
59 |
+ if(this.userInfo.password === ""){ |
|
60 |
+ alert("비밀번호를 입력해주세요."); |
|
61 |
+ this.$refs.password.focus(); |
|
62 |
+ return false; |
|
63 |
+ } |
|
64 |
+ return true; |
|
65 |
+ }, |
|
66 |
+ // 로그인 처리 함수 |
|
67 |
+ async handleLogin() { |
|
68 |
+ if(!this.validation()) { |
|
69 |
+ return; // 유효성 검사 실패 시 함수 종료 |
|
70 |
+ } |
|
71 |
+ try { |
|
72 |
+ const response = await loginProc(this.userInfo); |
|
73 |
+ if(response.status === 200) { |
|
74 |
+ const newToken = response.headers.authorization; |
|
75 |
+ // 토큰 저장 |
|
76 |
+ this.$store.commit('setAuthorization', newToken); |
|
77 |
+ // 사용자 정보 저장 |
|
78 |
+ /** jwt토큰 디코딩 **/ |
|
79 |
+ const base64String = newToken.split('.')[1]; |
|
80 |
+ const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/'); |
|
81 |
+ const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { |
|
82 |
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); |
|
83 |
+ }).join('')); |
|
84 |
+ const user = JSON.parse(jsonPayload); |
|
85 |
+ this.$store.commit("setUserInfo", { |
|
86 |
+ loginId: user.loginId, |
|
87 |
+ userId: user.userId, |
|
88 |
+ userNm: user.userNm, |
|
89 |
+ roles: Array.isArray(user.roles) ? user.roles.map(r => r.authority) : [], |
|
90 |
+ }); |
|
91 |
+ // 로그인 성공 시 |
|
92 |
+ localStorage.setItem('isLoggedIn', 'true'); |
|
93 |
+ // 메인 페이지로 이동 |
|
94 |
+ this.$router.push('/'); |
|
95 |
+ } else { |
|
96 |
+ alert("로그인에 실패했습니다. 아이디와 비밀번호를 확인해주세요."); |
|
97 |
+ } |
|
98 |
+ |
|
99 |
+ } catch (err) { |
|
100 |
+ alert(err.response.data.message); |
|
59 | 101 |
} |
60 | 102 |
}, |
61 | 103 |
}, |
--- package-lock.json
+++ package-lock.json
... | ... | @@ -12,6 +12,7 @@ |
12 | 12 |
"@fullcalendar/core": "^6.1.15", |
13 | 13 |
"@fullcalendar/daygrid": "^6.1.15", |
14 | 14 |
"@fullcalendar/vue3": "^6.1.15", |
15 |
+ "axios": "^1.10.0", |
|
15 | 16 |
"babel-loader": "8.2.5", |
16 | 17 |
"css-loader": "6.7.1", |
17 | 18 |
"express": "4.18.1", |
... | ... | @@ -28,6 +29,8 @@ |
28 | 29 |
"vue-router": "4.1.5", |
29 | 30 |
"vue-style-loader": "4.1.3", |
30 | 31 |
"vue3-sfc-loader": "^0.8.4", |
32 |
+ "vuex": "^4.1.0", |
|
33 |
+ "vuex-persistedstate": "^4.1.0", |
|
31 | 34 |
"webpack": "5.74.0", |
32 | 35 |
"webpack-cli": "4.10.0" |
33 | 36 |
}, |
... | ... | @@ -1176,6 +1179,11 @@ |
1176 | 1179 |
"node": "*" |
1177 | 1180 |
} |
1178 | 1181 |
}, |
1182 |
+ "node_modules/asynckit": { |
|
1183 |
+ "version": "0.4.0", |
|
1184 |
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", |
|
1185 |
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" |
|
1186 |
+ }, |
|
1179 | 1187 |
"node_modules/autoprefixer": { |
1180 | 1188 |
"version": "10.4.21", |
1181 | 1189 |
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", |
... | ... | @@ -1212,6 +1220,16 @@ |
1212 | 1220 |
}, |
1213 | 1221 |
"peerDependencies": { |
1214 | 1222 |
"postcss": "^8.1.0" |
1223 |
+ } |
|
1224 |
+ }, |
|
1225 |
+ "node_modules/axios": { |
|
1226 |
+ "version": "1.10.0", |
|
1227 |
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", |
|
1228 |
+ "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", |
|
1229 |
+ "dependencies": { |
|
1230 |
+ "follow-redirects": "^1.15.6", |
|
1231 |
+ "form-data": "^4.0.0", |
|
1232 |
+ "proxy-from-env": "^1.1.0" |
|
1215 | 1233 |
} |
1216 | 1234 |
}, |
1217 | 1235 |
"node_modules/babel-loader": { |
... | ... | @@ -1694,6 +1712,17 @@ |
1694 | 1712 |
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", |
1695 | 1713 |
"license": "MIT" |
1696 | 1714 |
}, |
1715 |
+ "node_modules/combined-stream": { |
|
1716 |
+ "version": "1.0.8", |
|
1717 |
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", |
|
1718 |
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", |
|
1719 |
+ "dependencies": { |
|
1720 |
+ "delayed-stream": "~1.0.0" |
|
1721 |
+ }, |
|
1722 |
+ "engines": { |
|
1723 |
+ "node": ">= 0.8" |
|
1724 |
+ } |
|
1725 |
+ }, |
|
1697 | 1726 |
"node_modules/commander": { |
1698 | 1727 |
"version": "4.1.1", |
1699 | 1728 |
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", |
... | ... | @@ -1901,6 +1930,22 @@ |
1901 | 1930 |
"node": ">=0.10.0" |
1902 | 1931 |
} |
1903 | 1932 |
}, |
1933 |
+ "node_modules/deepmerge": { |
|
1934 |
+ "version": "4.3.1", |
|
1935 |
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", |
|
1936 |
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", |
|
1937 |
+ "engines": { |
|
1938 |
+ "node": ">=0.10.0" |
|
1939 |
+ } |
|
1940 |
+ }, |
|
1941 |
+ "node_modules/delayed-stream": { |
|
1942 |
+ "version": "1.0.0", |
|
1943 |
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", |
|
1944 |
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", |
|
1945 |
+ "engines": { |
|
1946 |
+ "node": ">=0.4.0" |
|
1947 |
+ } |
|
1948 |
+ }, |
|
1904 | 1949 |
"node_modules/delegates": { |
1905 | 1950 |
"version": "1.0.0", |
1906 | 1951 |
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", |
... | ... | @@ -2104,6 +2149,20 @@ |
2104 | 2149 |
"license": "MIT", |
2105 | 2150 |
"dependencies": { |
2106 | 2151 |
"es-errors": "^1.3.0" |
2152 |
+ }, |
|
2153 |
+ "engines": { |
|
2154 |
+ "node": ">= 0.4" |
|
2155 |
+ } |
|
2156 |
+ }, |
|
2157 |
+ "node_modules/es-set-tostringtag": { |
|
2158 |
+ "version": "2.1.0", |
|
2159 |
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", |
|
2160 |
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", |
|
2161 |
+ "dependencies": { |
|
2162 |
+ "es-errors": "^1.3.0", |
|
2163 |
+ "get-intrinsic": "^1.2.6", |
|
2164 |
+ "has-tostringtag": "^1.0.2", |
|
2165 |
+ "hasown": "^2.0.2" |
|
2107 | 2166 |
}, |
2108 | 2167 |
"engines": { |
2109 | 2168 |
"node": ">= 0.4" |
... | ... | @@ -2452,6 +2511,40 @@ |
2452 | 2511 |
"flat": "cli.js" |
2453 | 2512 |
} |
2454 | 2513 |
}, |
2514 |
+ "node_modules/follow-redirects": { |
|
2515 |
+ "version": "1.15.9", |
|
2516 |
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", |
|
2517 |
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", |
|
2518 |
+ "funding": [ |
|
2519 |
+ { |
|
2520 |
+ "type": "individual", |
|
2521 |
+ "url": "https://github.com/sponsors/RubenVerborgh" |
|
2522 |
+ } |
|
2523 |
+ ], |
|
2524 |
+ "engines": { |
|
2525 |
+ "node": ">=4.0" |
|
2526 |
+ }, |
|
2527 |
+ "peerDependenciesMeta": { |
|
2528 |
+ "debug": { |
|
2529 |
+ "optional": true |
|
2530 |
+ } |
|
2531 |
+ } |
|
2532 |
+ }, |
|
2533 |
+ "node_modules/form-data": { |
|
2534 |
+ "version": "4.0.3", |
|
2535 |
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", |
|
2536 |
+ "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", |
|
2537 |
+ "dependencies": { |
|
2538 |
+ "asynckit": "^0.4.0", |
|
2539 |
+ "combined-stream": "^1.0.8", |
|
2540 |
+ "es-set-tostringtag": "^2.1.0", |
|
2541 |
+ "hasown": "^2.0.2", |
|
2542 |
+ "mime-types": "^2.1.12" |
|
2543 |
+ }, |
|
2544 |
+ "engines": { |
|
2545 |
+ "node": ">= 6" |
|
2546 |
+ } |
|
2547 |
+ }, |
|
2455 | 2548 |
"node_modules/forwarded": { |
2456 | 2549 |
"version": "0.2.0", |
2457 | 2550 |
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", |
... | ... | @@ -2793,6 +2886,20 @@ |
2793 | 2886 |
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", |
2794 | 2887 |
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", |
2795 | 2888 |
"license": "MIT", |
2889 |
+ "engines": { |
|
2890 |
+ "node": ">= 0.4" |
|
2891 |
+ }, |
|
2892 |
+ "funding": { |
|
2893 |
+ "url": "https://github.com/sponsors/ljharb" |
|
2894 |
+ } |
|
2895 |
+ }, |
|
2896 |
+ "node_modules/has-tostringtag": { |
|
2897 |
+ "version": "1.0.2", |
|
2898 |
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", |
|
2899 |
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", |
|
2900 |
+ "dependencies": { |
|
2901 |
+ "has-symbols": "^1.0.3" |
|
2902 |
+ }, |
|
2796 | 2903 |
"engines": { |
2797 | 2904 |
"node": ">= 0.4" |
2798 | 2905 |
}, |
... | ... | @@ -4628,6 +4735,11 @@ |
4628 | 4735 |
"node": ">= 0.10" |
4629 | 4736 |
} |
4630 | 4737 |
}, |
4738 |
+ "node_modules/proxy-from-env": { |
|
4739 |
+ "version": "1.1.0", |
|
4740 |
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", |
|
4741 |
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" |
|
4742 |
+ }, |
|
4631 | 4743 |
"node_modules/punycode": { |
4632 | 4744 |
"version": "2.3.1", |
4633 | 4745 |
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", |
... | ... | @@ -5208,6 +5320,12 @@ |
5208 | 5320 |
"engines": { |
5209 | 5321 |
"node": ">=8" |
5210 | 5322 |
} |
5323 |
+ }, |
|
5324 |
+ "node_modules/shvl": { |
|
5325 |
+ "version": "2.0.3", |
|
5326 |
+ "resolved": "https://registry.npmjs.org/shvl/-/shvl-2.0.3.tgz", |
|
5327 |
+ "integrity": "sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==", |
|
5328 |
+ "deprecated": "older versions vulnerable to prototype pollution" |
|
5211 | 5329 |
}, |
5212 | 5330 |
"node_modules/side-channel": { |
5213 | 5331 |
"version": "1.1.0", |
... | ... | @@ -6064,6 +6182,30 @@ |
6064 | 6182 |
"integrity": "sha512-eziaIrk/N9f9OCpyFEkR6vMsZUHcF5mQslXjffwcb5Iq6EuU74QrlpBeJqA04MvAGT7f5O8la2v9k3NtQnJb3Q==", |
6065 | 6183 |
"license": "MIT" |
6066 | 6184 |
}, |
6185 |
+ "node_modules/vuex": { |
|
6186 |
+ "version": "4.1.0", |
|
6187 |
+ "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", |
|
6188 |
+ "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==", |
|
6189 |
+ "dependencies": { |
|
6190 |
+ "@vue/devtools-api": "^6.0.0-beta.11" |
|
6191 |
+ }, |
|
6192 |
+ "peerDependencies": { |
|
6193 |
+ "vue": "^3.2.0" |
|
6194 |
+ } |
|
6195 |
+ }, |
|
6196 |
+ "node_modules/vuex-persistedstate": { |
|
6197 |
+ "version": "4.1.0", |
|
6198 |
+ "resolved": "https://registry.npmjs.org/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz", |
|
6199 |
+ "integrity": "sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ==", |
|
6200 |
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", |
|
6201 |
+ "dependencies": { |
|
6202 |
+ "deepmerge": "^4.2.2", |
|
6203 |
+ "shvl": "^2.0.3" |
|
6204 |
+ }, |
|
6205 |
+ "peerDependencies": { |
|
6206 |
+ "vuex": "^3.0 || ^4.0.0-rc" |
|
6207 |
+ } |
|
6208 |
+ }, |
|
6067 | 6209 |
"node_modules/watchpack": { |
6068 | 6210 |
"version": "2.4.2", |
6069 | 6211 |
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", |
--- package.json
+++ package.json
... | ... | @@ -7,6 +7,7 @@ |
7 | 7 |
"@fullcalendar/core": "^6.1.15", |
8 | 8 |
"@fullcalendar/daygrid": "^6.1.15", |
9 | 9 |
"@fullcalendar/vue3": "^6.1.15", |
10 |
+ "axios": "^1.10.0", |
|
10 | 11 |
"babel-loader": "8.2.5", |
11 | 12 |
"css-loader": "6.7.1", |
12 | 13 |
"express": "4.18.1", |
... | ... | @@ -23,6 +24,8 @@ |
23 | 24 |
"vue-router": "4.1.5", |
24 | 25 |
"vue-style-loader": "4.1.3", |
25 | 26 |
"vue3-sfc-loader": "^0.8.4", |
27 |
+ "vuex": "^4.1.0", |
|
28 |
+ "vuex-persistedstate": "^4.1.0", |
|
26 | 29 |
"webpack": "5.74.0", |
27 | 30 |
"webpack-cli": "4.10.0" |
28 | 31 |
}, |
--- server/modules/web/Server.js
+++ server/modules/web/Server.js
... | ... | @@ -89,6 +89,10 @@ |
89 | 89 |
proxyReqPathResolver: function (request) { |
90 | 90 |
return `${request.params["0"]}.json`; |
91 | 91 |
}, |
92 |
+ proxyReqOptDecorator: function (proxyReqOpts, srcReq) { |
|
93 |
+ proxyReqOpts.headers['X-Forwarded-For'] = srcReq.headers['x-forwarded-for'] || srcReq.connection.remoteAddress; |
|
94 |
+ return proxyReqOpts; |
|
95 |
+ } |
|
92 | 96 |
}) |
93 | 97 |
); |
94 | 98 |
|
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?