feat(auth): Google OAuth 로그인 연동 #3
@ -21,20 +21,45 @@ jobs:
|
||||
run: |
|
||||
echo "registry=https://nexus.gc-si.dev/repository/npm-public/" > frontend/.npmrc
|
||||
echo "//nexus.gc-si.dev/repository/npm-public/:_auth=${{ secrets.NEXUS_NPM_AUTH }}" >> frontend/.npmrc
|
||||
echo "registry=https://nexus.gc-si.dev/repository/npm-public/" > backend/.npmrc
|
||||
echo "//nexus.gc-si.dev/repository/npm-public/:_auth=${{ secrets.NEXUS_NPM_AUTH }}" >> backend/.npmrc
|
||||
|
||||
- name: Install dependencies
|
||||
# ── Frontend ──
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
|
||||
- name: Build frontend
|
||||
env:
|
||||
VITE_API_URL: /api
|
||||
VITE_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
run: |
|
||||
cd frontend
|
||||
npx vite build
|
||||
|
||||
- name: Deploy
|
||||
- name: Deploy frontend
|
||||
run: |
|
||||
rm -rf /deploy/wing-demo/*
|
||||
cp -r frontend/dist/* /deploy/wing-demo/
|
||||
echo "Deployed at $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
ls -la /deploy/wing-demo/
|
||||
echo "Frontend deployed at $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
# ── Backend ──
|
||||
- name: Install backend dependencies
|
||||
run: |
|
||||
cd backend
|
||||
npm ci --omit=dev
|
||||
|
||||
- name: Build backend
|
||||
run: |
|
||||
cd backend
|
||||
npx tsc
|
||||
|
||||
- name: Deploy backend
|
||||
run: |
|
||||
mkdir -p /deploy/wing-demo-backend/dist
|
||||
cp -r backend/dist/* /deploy/wing-demo-backend/dist/
|
||||
cp -r backend/node_modules /deploy/wing-demo-backend/
|
||||
cp backend/package.json /deploy/wing-demo-backend/
|
||||
date '+%s' > /deploy/wing-demo-backend/.deploy-trigger
|
||||
echo "Backend deployed at $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
698
backend/package-lock.json
generated
698
backend/package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"google-auth-library": "^10.6.1",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.19.0"
|
||||
@ -474,6 +475,33 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
@ -665,12 +693,51 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@ -716,6 +783,15 @@
|
||||
"prebuild-install": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
@ -787,6 +863,15 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
@ -861,6 +946,24 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@ -933,6 +1036,46 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
@ -999,6 +1142,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
@ -1014,6 +1163,12 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@ -1207,6 +1362,35 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@ -1246,6 +1430,34 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -1294,6 +1506,35 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
|
||||
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"rimraf": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
||||
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^7.0.0",
|
||||
"google-logging-utils": "^1.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@ -1350,6 +1591,53 @@
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "10.6.1",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz",
|
||||
"integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"gaxios": "7.1.3",
|
||||
"gcp-metadata": "8.1.2",
|
||||
"google-logging-utils": "1.1.3",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/google-logging-utils": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
|
||||
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@ -1415,6 +1703,19 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@ -1465,6 +1766,45 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
@ -1550,6 +1890,12 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -1631,6 +1977,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@ -1640,6 +2001,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
@ -1688,6 +2058,44 @@
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
@ -1741,6 +2149,12 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@ -1750,6 +2164,31 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
@ -2023,6 +2462,21 @@
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
|
||||
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^10.3.7"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@ -2121,6 +2575,27 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@ -2193,6 +2668,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
@ -2265,6 +2752,102 @@
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
@ -2410,6 +2993,121 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"google-auth-library": "^10.6.1",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.19.0"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Router } from 'express'
|
||||
import { login, getUserInfo, AuthError } from './authService.js'
|
||||
import { googleLogin } from './oauthService.js'
|
||||
import { clearTokenCookie } from './jwtProvider.js'
|
||||
import { requireAuth } from './authMiddleware.js'
|
||||
|
||||
@ -31,6 +32,32 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/auth/oauth/google
|
||||
router.post('/oauth/google', async (req, res) => {
|
||||
try {
|
||||
const { credential } = req.body
|
||||
|
||||
if (!credential) {
|
||||
res.status(400).json({ error: 'Google 인증 토큰이 필요합니다.' })
|
||||
return
|
||||
}
|
||||
|
||||
const ipAddr = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || req.ip || ''
|
||||
const userAgent = req.headers['user-agent'] || ''
|
||||
|
||||
const userInfo = await googleLogin(credential, ipAddr, userAgent, res)
|
||||
|
||||
res.json({ success: true, user: userInfo })
|
||||
} catch (err) {
|
||||
if (err instanceof AuthError) {
|
||||
res.status(err.status).json({ error: err.message })
|
||||
return
|
||||
}
|
||||
console.error('[auth] Google OAuth 오류:', err)
|
||||
res.status(500).json({ error: 'Google 로그인 처리 중 오류가 발생했습니다.' })
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/auth/logout
|
||||
router.post('/logout', requireAuth, (_req, res) => {
|
||||
clearTokenCookie(res)
|
||||
|
||||
@ -9,7 +9,7 @@ const SALT_ROUNDS = 10
|
||||
interface AuthUserRow {
|
||||
user_id: string
|
||||
user_acnt: string
|
||||
pswd_hash: string
|
||||
pswd_hash: string | null
|
||||
user_nm: string
|
||||
rnkp_nm: string | null
|
||||
org_sn: number | null
|
||||
@ -64,6 +64,10 @@ export async function login(
|
||||
throw new AuthError('가입이 거절된 계정입니다. 관리자에게 문의하세요.', 403)
|
||||
}
|
||||
|
||||
if (!user.pswd_hash) {
|
||||
throw new AuthError('이 계정은 Google 로그인만 지원합니다.', 401)
|
||||
}
|
||||
|
||||
const passwordValid = await bcrypt.compare(password, user.pswd_hash)
|
||||
|
||||
if (!passwordValid) {
|
||||
|
||||
178
backend/src/auth/oauthService.ts
Normal file
178
backend/src/auth/oauthService.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { OAuth2Client } from 'google-auth-library'
|
||||
import { authPool } from '../db/authDb.js'
|
||||
import { signToken, setTokenCookie } from './jwtProvider.js'
|
||||
import { getSetting } from '../settings/settingsService.js'
|
||||
import { AuthError, getUserInfo } from './authService.js'
|
||||
import type { Response } from 'express'
|
||||
|
||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''
|
||||
const googleClient = new OAuth2Client(GOOGLE_CLIENT_ID)
|
||||
|
||||
interface GoogleProfile {
|
||||
sub: string
|
||||
email: string
|
||||
name: string
|
||||
picture?: string
|
||||
hd?: string // hosted domain (Google Workspace)
|
||||
}
|
||||
|
||||
export async function googleLogin(
|
||||
credential: string,
|
||||
ipAddr: string,
|
||||
userAgent: string,
|
||||
res: Response
|
||||
) {
|
||||
const profile = await verifyGoogleToken(credential)
|
||||
|
||||
// 1. OAUTH_SUB로 기존 사용자 조회
|
||||
let userResult = await authPool.query(
|
||||
'SELECT USER_ID as user_id, USER_STTS_CD as user_stts_cd FROM AUTH_USER WHERE OAUTH_PROVIDER = $1 AND OAUTH_SUB = $2',
|
||||
['GOOGLE', profile.sub]
|
||||
)
|
||||
|
||||
let userId: string
|
||||
|
||||
if (userResult.rows.length > 0) {
|
||||
// 기존 OAuth 사용자
|
||||
const user = userResult.rows[0]
|
||||
userId = user.user_id
|
||||
|
||||
if (user.user_stts_cd === 'PENDING') {
|
||||
throw new AuthError('계정이 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.', 403)
|
||||
}
|
||||
if (user.user_stts_cd === 'LOCKED') {
|
||||
throw new AuthError('계정이 잠겨있습니다. 관리자에게 문의하세요.', 403)
|
||||
}
|
||||
if (user.user_stts_cd === 'INACTIVE') {
|
||||
throw new AuthError('비활성화된 계정입니다.', 403)
|
||||
}
|
||||
if (user.user_stts_cd === 'REJECTED') {
|
||||
throw new AuthError('가입이 거절된 계정입니다. 관리자에게 문의하세요.', 403)
|
||||
}
|
||||
} else {
|
||||
// EMAIL로 기존 PW 사용자 조회 (계정 연결)
|
||||
userResult = await authPool.query(
|
||||
'SELECT USER_ID as user_id, USER_STTS_CD as user_stts_cd FROM AUTH_USER WHERE EMAIL = $1 OR USER_ACNT = $1',
|
||||
[profile.email]
|
||||
)
|
||||
|
||||
if (userResult.rows.length > 0) {
|
||||
// 기존 계정에 OAuth 연결
|
||||
const user = userResult.rows[0]
|
||||
userId = user.user_id
|
||||
|
||||
await authPool.query(
|
||||
'UPDATE AUTH_USER SET OAUTH_PROVIDER = $1, OAUTH_SUB = $2, EMAIL = $3, MDFCN_DTM = NOW() WHERE USER_ID = $4',
|
||||
['GOOGLE', profile.sub, profile.email, userId]
|
||||
)
|
||||
|
||||
if (user.user_stts_cd !== 'ACTIVE') {
|
||||
throw new AuthError('계정이 활성 상태가 아닙니다. 관리자에게 문의하세요.', 403)
|
||||
}
|
||||
} else {
|
||||
// 신규 사용자 생성
|
||||
userId = await createOAuthUser(profile)
|
||||
}
|
||||
}
|
||||
|
||||
// 로그인 처리
|
||||
await authPool.query(
|
||||
'UPDATE AUTH_USER SET LAST_LOGIN_DTM = NOW(), MDFCN_DTM = NOW() WHERE USER_ID = $1',
|
||||
[userId]
|
||||
)
|
||||
|
||||
await recordLoginHistory(userId, ipAddr, userAgent)
|
||||
|
||||
const userInfo = await getUserInfo(userId)
|
||||
|
||||
// PENDING 사용자는 JWT 발급하지 않음
|
||||
if (userInfo.roles.length === 0) {
|
||||
const userStatus = await authPool.query(
|
||||
'SELECT USER_STTS_CD as user_stts_cd FROM AUTH_USER WHERE USER_ID = $1',
|
||||
[userId]
|
||||
)
|
||||
if (userStatus.rows[0]?.user_stts_cd === 'PENDING') {
|
||||
throw new AuthError('계정이 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.', 403)
|
||||
}
|
||||
}
|
||||
|
||||
const token = signToken({
|
||||
sub: userInfo.id,
|
||||
acnt: userInfo.account,
|
||||
name: userInfo.name,
|
||||
roles: userInfo.roles,
|
||||
})
|
||||
|
||||
setTokenCookie(res, token)
|
||||
return userInfo
|
||||
}
|
||||
|
||||
async function verifyGoogleToken(credential: string): Promise<GoogleProfile> {
|
||||
try {
|
||||
const ticket = await googleClient.verifyIdToken({
|
||||
idToken: credential,
|
||||
audience: GOOGLE_CLIENT_ID,
|
||||
})
|
||||
|
||||
const payload = ticket.getPayload()
|
||||
if (!payload || !payload.email || !payload.sub) {
|
||||
throw new AuthError('Google 인증 정보가 유효하지 않습니다.', 401)
|
||||
}
|
||||
|
||||
return {
|
||||
sub: payload.sub,
|
||||
email: payload.email,
|
||||
name: payload.name || payload.email.split('@')[0],
|
||||
picture: payload.picture,
|
||||
hd: payload.hd,
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof AuthError) throw err
|
||||
throw new AuthError('Google 인증 토큰 검증에 실패했습니다.', 401)
|
||||
}
|
||||
}
|
||||
|
||||
async function createOAuthUser(profile: GoogleProfile): Promise<string> {
|
||||
const domain = profile.email.split('@')[1]
|
||||
|
||||
// 자동 승인 도메인 확인
|
||||
const autoApproveDomains = await getSetting('oauth.auto-approve-domains')
|
||||
const allowedDomains = autoApproveDomains
|
||||
? autoApproveDomains.split(',').map(d => d.trim().toLowerCase())
|
||||
: []
|
||||
|
||||
const isAutoApproved = allowedDomains.includes(domain.toLowerCase())
|
||||
const status = isAutoApproved ? 'ACTIVE' : 'PENDING'
|
||||
|
||||
// 사용자 생성
|
||||
const result = await authPool.query(
|
||||
`INSERT INTO AUTH_USER (USER_ACNT, USER_NM, EMAIL, OAUTH_PROVIDER, OAUTH_SUB, USER_STTS_CD)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) RETURNING USER_ID as user_id`,
|
||||
[profile.email, profile.name, profile.email, 'GOOGLE', profile.sub, status]
|
||||
)
|
||||
|
||||
const userId = result.rows[0].user_id
|
||||
|
||||
// 자동 승인된 사용자에게 기본 역할 할당
|
||||
if (isAutoApproved) {
|
||||
await authPool.query(
|
||||
`INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN)
|
||||
SELECT $1, ROLE_SN FROM AUTH_ROLE WHERE DFLT_YN = 'Y'`,
|
||||
[userId]
|
||||
)
|
||||
}
|
||||
|
||||
return userId
|
||||
}
|
||||
|
||||
async function recordLoginHistory(
|
||||
userId: string,
|
||||
ipAddr: string,
|
||||
userAgent: string
|
||||
): Promise<void> {
|
||||
await authPool.query(
|
||||
`INSERT INTO AUTH_LOGIN_HIST (USER_ID, IP_ADDR, USER_AGENT, SUCCESS_YN)
|
||||
VALUES ($1, $2, $3, 'Y')`,
|
||||
[userId, ipAddr, userAgent?.substring(0, 500)]
|
||||
)
|
||||
}
|
||||
@ -38,7 +38,7 @@ app.use(helmet({
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", "data:", "blob:"],
|
||||
connectSrc: ["'self'", "http://localhost:*", "https://*.data.go.kr", "https://*.khoa.go.kr"],
|
||||
connectSrc: ["'self'", "http://localhost:*", "https://*.gc-si.dev", "https://*.data.go.kr", "https://*.khoa.go.kr"],
|
||||
fontSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
frameSrc: ["'none'"],
|
||||
@ -56,7 +56,8 @@ const allowedOrigins = [
|
||||
'http://localhost:5173', // Vite dev server
|
||||
'http://localhost:5174',
|
||||
'http://localhost:3000',
|
||||
process.env.FRONTEND_URL, // 운영 환경 프론트엔드 URL
|
||||
'https://wing-demo.gc-si.dev',
|
||||
process.env.FRONTEND_URL, // 운영 환경 프론트엔드 URL (추가 도메인)
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
app.use(cors({
|
||||
|
||||
@ -3,6 +3,8 @@ import { requireAuth, requireRole } from '../auth/authMiddleware.js'
|
||||
import {
|
||||
getRegistrationSettings,
|
||||
updateRegistrationSettings,
|
||||
getOAuthSettings,
|
||||
updateOAuthSettings,
|
||||
getAllSettings,
|
||||
} from './settingsService.js'
|
||||
|
||||
@ -46,4 +48,28 @@ router.put('/registration', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// GET /api/settings/oauth — OAuth 설정 조회
|
||||
router.get('/oauth', async (_req, res) => {
|
||||
try {
|
||||
const settings = await getOAuthSettings()
|
||||
res.json(settings)
|
||||
} catch (err) {
|
||||
console.error('[settings] OAuth 설정 조회 오류:', err)
|
||||
res.status(500).json({ error: 'OAuth 설정 조회 중 오류가 발생했습니다.' })
|
||||
}
|
||||
})
|
||||
|
||||
// PUT /api/settings/oauth — OAuth 설정 수정
|
||||
router.put('/oauth', async (req, res) => {
|
||||
try {
|
||||
const { autoApproveDomains } = req.body
|
||||
await updateOAuthSettings({ autoApproveDomains })
|
||||
const updated = await getOAuthSettings()
|
||||
res.json(updated)
|
||||
} catch (err) {
|
||||
console.error('[settings] OAuth 설정 수정 오류:', err)
|
||||
res.status(500).json({ error: 'OAuth 설정 수정 중 오류가 발생했습니다.' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@ -57,6 +57,21 @@ export async function updateRegistrationSettings(settings: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOAuthSettings(): Promise<{
|
||||
autoApproveDomains: string
|
||||
}> {
|
||||
const autoApproveDomains = (await getSetting('oauth.auto-approve-domains')) || ''
|
||||
return { autoApproveDomains }
|
||||
}
|
||||
|
||||
export async function updateOAuthSettings(settings: {
|
||||
autoApproveDomains?: string
|
||||
}): Promise<void> {
|
||||
if (settings.autoApproveDomains !== undefined) {
|
||||
await setSetting('oauth.auto-approve-domains', settings.autoApproveDomains)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllSettings(): Promise<SettingItem[]> {
|
||||
const result = await authPool.query<SettingRow>(
|
||||
'SELECT SETTING_KEY, SETTING_VAL, SETTING_DC FROM AUTH_SETTING ORDER BY SETTING_KEY'
|
||||
|
||||
@ -15,6 +15,8 @@ interface UserListItem {
|
||||
lastLogin: string | null
|
||||
roles: string[]
|
||||
regDtm: string
|
||||
oauthProvider: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
interface CreateUserInput {
|
||||
@ -39,7 +41,8 @@ export async function listUsers(search?: string, status?: string): Promise<UserL
|
||||
u.RNKP_NM as rank, u.ORG_SN as org_sn,
|
||||
o.ORG_NM as org_name, o.ORG_ABBR_NM as org_abbr,
|
||||
u.USER_STTS_CD as status, u.FAIL_CNT as fail_count,
|
||||
u.LAST_LOGIN_DTM as last_login, u.REG_DTM as reg_dtm
|
||||
u.LAST_LOGIN_DTM as last_login, u.REG_DTM as reg_dtm,
|
||||
u.OAUTH_PROVIDER as oauth_provider, u.EMAIL as email
|
||||
FROM AUTH_USER u
|
||||
LEFT JOIN AUTH_ORG o ON u.ORG_SN = o.ORG_SN
|
||||
WHERE 1=1
|
||||
@ -82,6 +85,8 @@ export async function listUsers(search?: string, status?: string): Promise<UserL
|
||||
lastLogin: row.last_login,
|
||||
roles: rolesResult.rows.map((r: { role_cd: string }) => r.role_cd),
|
||||
regDtm: row.reg_dtm,
|
||||
oauthProvider: row.oauth_provider,
|
||||
email: row.email,
|
||||
})
|
||||
}
|
||||
|
||||
@ -94,7 +99,8 @@ export async function getUser(userId: string): Promise<UserListItem> {
|
||||
u.RNKP_NM as rank, u.ORG_SN as org_sn,
|
||||
o.ORG_NM as org_name, o.ORG_ABBR_NM as org_abbr,
|
||||
u.USER_STTS_CD as status, u.FAIL_CNT as fail_count,
|
||||
u.LAST_LOGIN_DTM as last_login, u.REG_DTM as reg_dtm
|
||||
u.LAST_LOGIN_DTM as last_login, u.REG_DTM as reg_dtm,
|
||||
u.OAUTH_PROVIDER as oauth_provider, u.EMAIL as email
|
||||
FROM AUTH_USER u
|
||||
LEFT JOIN AUTH_ORG o ON u.ORG_SN = o.ORG_SN
|
||||
WHERE u.USER_ID = $1`,
|
||||
@ -125,6 +131,8 @@ export async function getUser(userId: string): Promise<UserListItem> {
|
||||
lastLogin: row.last_login,
|
||||
roles: rolesResult.rows.map((r: { role_cd: string }) => r.role_cd),
|
||||
regDtm: row.reg_dtm,
|
||||
oauthProvider: row.oauth_provider,
|
||||
email: row.email,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -74,13 +74,16 @@ COMMENT ON COLUMN AUTH_ROLE.REG_DTM IS '등록일시';
|
||||
CREATE TABLE AUTH_USER (
|
||||
USER_ID UUID NOT NULL DEFAULT uuid_generate_v4(),
|
||||
USER_ACNT VARCHAR(50) NOT NULL,
|
||||
PSWD_HASH VARCHAR(255) NOT NULL,
|
||||
PSWD_HASH VARCHAR(255),
|
||||
USER_NM VARCHAR(50) NOT NULL,
|
||||
RNKP_NM VARCHAR(30),
|
||||
ORG_SN INTEGER,
|
||||
USER_STTS_CD VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
FAIL_CNT INTEGER NOT NULL DEFAULT 0,
|
||||
LAST_LOGIN_DTM TIMESTAMPTZ,
|
||||
OAUTH_PROVIDER VARCHAR(20),
|
||||
OAUTH_SUB VARCHAR(255),
|
||||
EMAIL VARCHAR(255),
|
||||
REG_DTM TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
MDFCN_DTM TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT PK_AUTH_USER PRIMARY KEY (USER_ID),
|
||||
@ -100,6 +103,9 @@ COMMENT ON COLUMN AUTH_USER.USER_STTS_CD IS '사용자상태코드 (PENDING:
|
||||
COMMENT ON COLUMN AUTH_USER.FAIL_CNT IS '로그인실패횟수';
|
||||
COMMENT ON COLUMN AUTH_USER.LAST_LOGIN_DTM IS '최종로그인일시';
|
||||
COMMENT ON COLUMN AUTH_USER.REG_DTM IS '등록일시';
|
||||
COMMENT ON COLUMN AUTH_USER.OAUTH_PROVIDER IS 'OAuth제공자 (GOOGLE 등)';
|
||||
COMMENT ON COLUMN AUTH_USER.OAUTH_SUB IS 'OAuth고유식별자';
|
||||
COMMENT ON COLUMN AUTH_USER.EMAIL IS '이메일주소';
|
||||
COMMENT ON COLUMN AUTH_USER.MDFCN_DTM IS '수정일시';
|
||||
|
||||
|
||||
@ -191,6 +197,8 @@ COMMENT ON COLUMN AUTH_SETTING.MDFCN_DTM IS '수정일시';
|
||||
-- ============================================================
|
||||
CREATE INDEX IDX_AUTH_USER_STTS ON AUTH_USER (USER_STTS_CD);
|
||||
CREATE INDEX IDX_AUTH_USER_ORG ON AUTH_USER (ORG_SN);
|
||||
CREATE UNIQUE INDEX UK_AUTH_USER_OAUTH ON AUTH_USER(OAUTH_PROVIDER, OAUTH_SUB) WHERE OAUTH_PROVIDER IS NOT NULL;
|
||||
CREATE UNIQUE INDEX UK_AUTH_USER_EMAIL ON AUTH_USER(EMAIL) WHERE EMAIL IS NOT NULL;
|
||||
CREATE INDEX IDX_AUTH_PERM_ROLE ON AUTH_PERM (ROLE_SN);
|
||||
CREATE INDEX IDX_AUTH_PERM_RSRC ON AUTH_PERM (RSRC_CD);
|
||||
CREATE INDEX IDX_AUTH_LOGIN_USER ON AUTH_LOGIN_HIST (USER_ID);
|
||||
@ -272,7 +280,8 @@ SELECT USER_ID, 1 FROM AUTH_USER WHERE USER_ACNT = 'admin';
|
||||
-- ============================================================
|
||||
INSERT INTO AUTH_SETTING (SETTING_KEY, SETTING_VAL, SETTING_DC) VALUES
|
||||
('registration.auto-approve', 'true', '신규 사용자 자동 승인 여부 (true: 즉시 ACTIVE, false: PENDING 대기)'),
|
||||
('registration.default-role', 'true', '신규 사용자에게 기본 역할(DFLT_YN=Y) 자동 할당 여부');
|
||||
('registration.default-role', 'true', '신규 사용자에게 기본 역할(DFLT_YN=Y) 자동 할당 여부'),
|
||||
('oauth.auto-approve-domains', 'gcsc.co.kr', 'OAuth 자동 승인 도메인 (쉼표 구분)');
|
||||
|
||||
|
||||
-- ============================================================
|
||||
|
||||
29
frontend/package-lock.json
generated
29
frontend/package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@react-oauth/google": "^0.13.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"axios": "^1.13.5",
|
||||
"leaflet": "^1.9.4",
|
||||
@ -82,7 +83,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@ -1090,6 +1090,16 @@
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-oauth/google": {
|
||||
"version": "0.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.4.tgz",
|
||||
"integrity": "sha512-hGKyNEH+/PK8M0sFEuo3MAEk0txtHpgs94tDQit+s2LXg7b6z53NtzHfqDvoB2X8O6lGB+FRg80hY//X6hfD+w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
@ -1561,7 +1571,6 @@
|
||||
"integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@ -1578,7 +1587,6 @@
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@ -1638,7 +1646,6 @@
|
||||
"integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.55.0",
|
||||
"@typescript-eslint/types": "8.55.0",
|
||||
@ -1900,7 +1907,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -2127,7 +2133,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@ -2554,7 +2559,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -3238,7 +3242,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@ -3324,8 +3327,7 @@
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/lerc": {
|
||||
"version": "3.0.0",
|
||||
@ -3724,7 +3726,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -3772,7 +3773,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@ -4001,7 +4001,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -4011,7 +4010,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@ -4474,7 +4472,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -4586,7 +4583,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@ -4759,7 +4755,6 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-oauth/google": "^0.13.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"axios": "^1.13.5",
|
||||
"leaflet": "^1.9.4",
|
||||
|
||||
BIN
frontend/public/24.png
Normal file
BIN
frontend/public/24.png
Normal file
Binary file not shown.
|
After Width: | Height: | 크기: 3.9 MiB |
@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google'
|
||||
import { MainLayout } from './components/layout/MainLayout'
|
||||
import { LoginPage } from './components/auth/LoginPage'
|
||||
import { registerMainTabSwitcher } from './hooks/useSubMenu'
|
||||
@ -17,6 +18,8 @@ import { RescueView } from './components/views/RescueView'
|
||||
|
||||
export type MainTab = 'prediction' | 'hns' | 'rescue' | 'reports' | 'aerial' | 'assets' | 'scat' | 'incidents' | 'board' | 'weather' | 'admin'
|
||||
|
||||
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID || ''
|
||||
|
||||
function App() {
|
||||
const [activeMainTab, setActiveMainTab] = useState<MainTab>('prediction')
|
||||
const { isAuthenticated, isLoading, checkSession } = useAuthStore()
|
||||
@ -88,4 +91,15 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
function AppWithProviders() {
|
||||
if (!GOOGLE_CLIENT_ID) {
|
||||
return <App />
|
||||
}
|
||||
return (
|
||||
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
||||
<App />
|
||||
</GoogleOAuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppWithProviders
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { GoogleLogin, type CredentialResponse } from '@react-oauth/google'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
|
||||
/* Demo accounts (개발 모드 전용) */
|
||||
@ -10,7 +11,8 @@ export function LoginPage() {
|
||||
const [userId, setUserId] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [remember, setRemember] = useState(false)
|
||||
const { login, isLoading, error, clearError } = useAuthStore()
|
||||
const { login, googleLogin, isLoading, error, clearError } = useAuthStore()
|
||||
const GOOGLE_ENABLED = !!import.meta.env.VITE_GOOGLE_CLIENT_ID
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('wing_remember')
|
||||
@ -21,6 +23,17 @@ export function LoginPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleGoogleSuccess = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
clearError()
|
||||
try {
|
||||
await googleLogin(response.credential)
|
||||
} catch {
|
||||
// 에러는 authStore에서 관리
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
clearError()
|
||||
@ -263,10 +276,25 @@ export function LoginPage() {
|
||||
<div style={{ flex: 1, height: 1, background: 'var(--bd)' }} />
|
||||
</div>
|
||||
|
||||
{/* SSO / Certificate */}
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{/* Google / Certificate */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{GOOGLE_ENABLED && (
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'center',
|
||||
borderRadius: 8, overflow: 'hidden',
|
||||
}}>
|
||||
<GoogleLogin
|
||||
onSuccess={handleGoogleSuccess}
|
||||
onError={() => { /* 팝업 닫힘 등 — 별도 처리 불필요 */ }}
|
||||
theme="filled_black"
|
||||
size="large"
|
||||
shape="rectangular"
|
||||
width={304}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button type="button" style={{
|
||||
flex: 1, padding: '10px', borderRadius: 8,
|
||||
width: '100%', padding: '10px', borderRadius: 8,
|
||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
||||
color: 'var(--t2)', fontSize: 11, fontWeight: 600,
|
||||
fontFamily: 'var(--fK)', cursor: 'pointer',
|
||||
@ -279,20 +307,6 @@ export function LoginPage() {
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
||||
공무원 인증서
|
||||
</button>
|
||||
<button type="button" style={{
|
||||
flex: 1, padding: '10px', borderRadius: 8,
|
||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
||||
color: 'var(--t2)', fontSize: 11, fontWeight: 600,
|
||||
fontFamily: 'var(--fK)', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bgH)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--bg3)'}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.78 7.78 5.5 5.5 0 0 1 7.78-7.78Zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
|
||||
SSO 로그인
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Demo accounts info (DEV only) */}
|
||||
|
||||
@ -10,9 +10,12 @@ import {
|
||||
rejectUserApi,
|
||||
fetchRegistrationSettings,
|
||||
updateRegistrationSettingsApi,
|
||||
fetchOAuthSettings,
|
||||
updateOAuthSettingsApi,
|
||||
type UserListItem,
|
||||
type RoleWithPermissions,
|
||||
type RegistrationSettings,
|
||||
type OAuthSettings,
|
||||
} from '../../services/authApi'
|
||||
|
||||
const roleLabels: Record<string, { label: string; color: string }> = {
|
||||
@ -154,6 +157,7 @@ function UsersPanel() {
|
||||
<th className="px-6 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">계정</th>
|
||||
<th className="px-6 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">소속</th>
|
||||
<th className="px-6 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">역할</th>
|
||||
<th className="px-6 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">인증</th>
|
||||
<th className="px-6 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">상태</th>
|
||||
<th className="px-6 py-3 text-left text-[11px] font-semibold text-text-3 font-korean">최근 로그인</th>
|
||||
<th className="px-6 py-3 text-right text-[11px] font-semibold text-text-3 font-korean">관리</th>
|
||||
@ -181,6 +185,26 @@ function UsersPanel() {
|
||||
{roleInfo.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-3">
|
||||
{user.oauthProvider ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-semibold rounded-md font-mono"
|
||||
style={{ background: 'rgba(66,133,244,0.15)', color: '#4285F4', border: '1px solid rgba(66,133,244,0.3)' }}
|
||||
title={user.email || undefined}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 48 48"><path fill="#4285F4" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#34A853" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59A14.5 14.5 0 019.5 24c0-1.59.28-3.14.76-4.59l-7.98-6.19A23.99 23.99 0 000 24c0 3.77.9 7.35 2.56 10.54l7.97-5.95z"/><path fill="#EA4335" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 5.95C6.51 42.62 14.62 48 24 48z"/></svg>
|
||||
Google
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-semibold rounded-md font-korean"
|
||||
style={{ background: 'rgba(148,163,184,0.15)', color: 'var(--t3)', border: '1px solid rgba(148,163,184,0.2)' }}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
|
||||
ID/PW
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-3">
|
||||
<span className={`inline-flex items-center gap-1.5 text-[10px] font-semibold font-korean ${statusInfo.color}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot}`} />
|
||||
@ -494,8 +518,11 @@ function MenusPanel() {
|
||||
// ─── 시스템 설정 패널 ────────────────────────────────────────
|
||||
function SettingsPanel() {
|
||||
const [settings, setSettings] = useState<RegistrationSettings | null>(null)
|
||||
const [oauthSettings, setOauthSettings] = useState<OAuthSettings | null>(null)
|
||||
const [oauthDomainInput, setOauthDomainInput] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [savingOAuth, setSavingOAuth] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
@ -504,8 +531,13 @@ function SettingsPanel() {
|
||||
const loadSettings = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await fetchRegistrationSettings()
|
||||
setSettings(data)
|
||||
const [regData, oauthData] = await Promise.all([
|
||||
fetchRegistrationSettings(),
|
||||
fetchOAuthSettings(),
|
||||
])
|
||||
setSettings(regData)
|
||||
setOauthSettings(oauthData)
|
||||
setOauthDomainInput(oauthData.autoApproveDomains)
|
||||
} catch (err) {
|
||||
console.error('설정 조회 실패:', err)
|
||||
} finally {
|
||||
@ -598,6 +630,68 @@ function SettingsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OAuth 설정 */}
|
||||
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-border">
|
||||
<h2 className="text-sm font-bold text-text-1 font-korean">Google OAuth 설정</h2>
|
||||
<p className="text-[11px] text-text-3 mt-0.5 font-korean">Google 계정 로그인 시 자동 승인할 이메일 도메인을 설정합니다</p>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<div className="flex-1 mr-4 mb-3">
|
||||
<div className="text-[13px] font-semibold text-text-1 font-korean mb-1">자동 승인 도메인</div>
|
||||
<p className="text-[11px] text-text-3 font-korean leading-relaxed mb-3">
|
||||
지정된 도메인의 Google 계정은 가입 즉시 <span className="text-green-400 font-semibold">ACTIVE</span> 상태가 됩니다.
|
||||
미지정 도메인은 <span className="text-yellow-400 font-semibold">PENDING</span> 상태로 관리자 승인이 필요합니다.
|
||||
여러 도메인은 쉼표(,)로 구분합니다.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={oauthDomainInput}
|
||||
onChange={(e) => setOauthDomainInput(e.target.value)}
|
||||
placeholder="gcsc.co.kr, example.com"
|
||||
className="flex-1 px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setSavingOAuth(true)
|
||||
try {
|
||||
const updated = await updateOAuthSettingsApi({ autoApproveDomains: oauthDomainInput.trim() })
|
||||
setOauthSettings(updated)
|
||||
setOauthDomainInput(updated.autoApproveDomains)
|
||||
} catch (err) {
|
||||
console.error('OAuth 설정 변경 실패:', err)
|
||||
} finally {
|
||||
setSavingOAuth(false)
|
||||
}
|
||||
}}
|
||||
disabled={savingOAuth || oauthDomainInput.trim() === (oauthSettings?.autoApproveDomains || '')}
|
||||
className={`px-4 py-2 text-xs font-semibold rounded-md transition-all font-korean whitespace-nowrap ${
|
||||
oauthDomainInput.trim() !== (oauthSettings?.autoApproveDomains || '')
|
||||
? 'bg-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
|
||||
: 'bg-bg-3 text-text-3 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{savingOAuth ? '저장 중...' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{oauthSettings?.autoApproveDomains && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-3">
|
||||
{oauthSettings.autoApproveDomains.split(',').map(d => d.trim()).filter(Boolean).map(domain => (
|
||||
<span
|
||||
key={domain}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-mono rounded-md"
|
||||
style={{ background: 'rgba(6,182,212,0.1)', color: 'var(--cyan)', border: '1px solid rgba(6,182,212,0.25)' }}
|
||||
>
|
||||
@{domain}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 현재 설정 상태 요약 */}
|
||||
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-border">
|
||||
@ -627,6 +721,17 @@ function SettingsPanel() {
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${oauthSettings?.autoApproveDomains ? 'bg-blue-400' : 'bg-text-3'}`} />
|
||||
<span className="text-text-2">
|
||||
Google OAuth 자동 승인 도메인{' '}
|
||||
{oauthSettings?.autoApproveDomains ? (
|
||||
<span className="text-blue-400 font-semibold font-mono">{oauthSettings.autoApproveDomains}</span>
|
||||
) : (
|
||||
<span className="text-text-3 font-semibold">미설정</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -20,6 +20,11 @@ export async function loginApi(account: string, password: string): Promise<AuthU
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
export async function googleLoginApi(credential: string): Promise<AuthUser> {
|
||||
const response = await api.post<LoginResponse>('/auth/oauth/google', { credential })
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
export async function logoutApi(): Promise<void> {
|
||||
await api.post('/auth/logout')
|
||||
}
|
||||
@ -43,6 +48,8 @@ export interface UserListItem {
|
||||
lastLogin: string | null
|
||||
roles: string[]
|
||||
regDtm: string
|
||||
oauthProvider: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export async function fetchUsers(search?: string, status?: string): Promise<UserListItem[]> {
|
||||
@ -143,3 +150,20 @@ export async function updateRegistrationSettingsApi(
|
||||
const response = await api.put<RegistrationSettings>('/settings/registration', settings)
|
||||
return response.data
|
||||
}
|
||||
|
||||
// OAuth 설정 API (ADMIN 전용)
|
||||
export interface OAuthSettings {
|
||||
autoApproveDomains: string
|
||||
}
|
||||
|
||||
export async function fetchOAuthSettings(): Promise<OAuthSettings> {
|
||||
const response = await api.get<OAuthSettings>('/settings/oauth')
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function updateOAuthSettingsApi(
|
||||
settings: Partial<OAuthSettings>
|
||||
): Promise<OAuthSettings> {
|
||||
const response = await api.put<OAuthSettings>('/settings/oauth', settings)
|
||||
return response.data
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand'
|
||||
import { loginApi, logoutApi, fetchMe } from '../services/authApi'
|
||||
import { loginApi, googleLoginApi, logoutApi, fetchMe } from '../services/authApi'
|
||||
import type { AuthUser } from '../services/authApi'
|
||||
|
||||
interface AuthState {
|
||||
@ -8,6 +8,7 @@ interface AuthState {
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
login: (account: string, password: string) => Promise<void>
|
||||
googleLogin: (credential: string) => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
checkSession: () => Promise<void>
|
||||
hasPermission: (resource: string) => boolean
|
||||
@ -32,6 +33,18 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
googleLogin: async (credential: string) => {
|
||||
set({ isLoading: true, error: null })
|
||||
try {
|
||||
const user = await googleLoginApi(credential)
|
||||
set({ user, isAuthenticated: true, isLoading: false })
|
||||
} catch (err) {
|
||||
const message = (err as { message?: string })?.message || 'Google 로그인에 실패했습니다.'
|
||||
set({ isLoading: false, error: message })
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
await logoutApi()
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user