mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-11 19:08:20 +00:00
Merge pull request #3 from speatzle/client-auth
Add Client Auth, Session management and API call
This commit is contained in:
commit
b69cc43561
15 changed files with 414 additions and 186 deletions
|
@ -12,17 +12,19 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^0.8.2",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"@vueuse/head": "^1.1.15",
|
||||
"axios": "^1.3.4",
|
||||
"events": "^3.3.0",
|
||||
"focus-trap": "^7.3.1",
|
||||
"focus-trap-vue": "^4.0.2",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"markdown-it-shiki": "^0.8.0",
|
||||
"simple-jsonrpc-js": "^1.2.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-i18n": "9",
|
||||
"vue-router": "4"
|
||||
"vue-router": "4",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.30",
|
||||
|
|
146
client/pnpm-lock.yaml
generated
146
client/pnpm-lock.yaml
generated
|
@ -3,7 +3,6 @@ lockfileVersion: 5.4
|
|||
specifiers:
|
||||
'@iconify/json': ^2.2.30
|
||||
'@intlify/unplugin-vue-i18n': ^0.8.2
|
||||
'@open-rpc/client-js': ^1.8.1
|
||||
'@types/events': ^3.0.0
|
||||
'@types/markdown-it-link-attributes': ^3.0.1
|
||||
'@typescript-eslint/parser': ^5.54.1
|
||||
|
@ -12,6 +11,7 @@ specifiers:
|
|||
'@vue-macros/volar': ^0.8.4
|
||||
'@vueuse/core': ^9.13.0
|
||||
'@vueuse/head': ^1.1.15
|
||||
axios: ^1.3.4
|
||||
eslint: ^8.35.0
|
||||
eslint-plugin-vue: ^9.9.0
|
||||
events: ^3.3.0
|
||||
|
@ -19,6 +19,7 @@ specifiers:
|
|||
focus-trap-vue: ^4.0.2
|
||||
markdown-it-link-attributes: ^4.0.1
|
||||
markdown-it-shiki: ^0.8.0
|
||||
simple-jsonrpc-js: ^1.2.0
|
||||
typescript: ^4.9.3
|
||||
unplugin-auto-import: ^0.15.0
|
||||
unplugin-icons: ^0.15.3
|
||||
|
@ -31,20 +32,23 @@ specifiers:
|
|||
vue-i18n: '9'
|
||||
vue-router: '4'
|
||||
vue-tsc: ^1.0.24
|
||||
ws: ^8.13.0
|
||||
|
||||
dependencies:
|
||||
'@intlify/unplugin-vue-i18n': 0.8.2_vue-i18n@9.2.2
|
||||
'@open-rpc/client-js': 1.8.1
|
||||
'@vueuse/core': 9.13.0_vue@3.2.47
|
||||
'@vueuse/head': 1.1.15_vue@3.2.47
|
||||
axios: 1.3.4
|
||||
events: 3.3.0
|
||||
focus-trap: 7.3.1
|
||||
focus-trap-vue: 4.0.2_oggptlzwchqpaguemspe4ract4
|
||||
markdown-it-link-attributes: 4.0.1
|
||||
markdown-it-shiki: 0.8.0
|
||||
simple-jsonrpc-js: 1.2.0
|
||||
vue: 3.2.47
|
||||
vue-i18n: 9.2.2_vue@3.2.47
|
||||
vue-router: 4.1.6_vue@3.2.47
|
||||
ws: 8.13.0
|
||||
|
||||
devDependencies:
|
||||
'@iconify/json': 2.2.30
|
||||
|
@ -775,19 +779,6 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@open-rpc/client-js/1.8.1:
|
||||
resolution: {integrity: sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==}
|
||||
dependencies:
|
||||
isomorphic-fetch: 3.0.0
|
||||
isomorphic-ws: 5.0.0_ws@7.5.9
|
||||
strict-event-emitter-types: 2.0.0
|
||||
ws: 7.5.9
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- encoding
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/@pkgr/utils/2.3.1:
|
||||
resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
@ -1507,11 +1498,25 @@ packages:
|
|||
'@babel/types': 7.21.2
|
||||
dev: true
|
||||
|
||||
/asynckit/0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: false
|
||||
|
||||
/available-typed-arrays/1.0.5:
|
||||
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: true
|
||||
|
||||
/axios/1.3.4:
|
||||
resolution: {integrity: sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.2
|
||||
form-data: 4.0.0
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/balanced-match/1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
dev: true
|
||||
|
@ -1646,6 +1651,13 @@ packages:
|
|||
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
|
||||
dev: true
|
||||
|
||||
/combined-stream/1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: false
|
||||
|
||||
/concat-map/0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
dev: true
|
||||
|
@ -1741,6 +1753,11 @@ packages:
|
|||
resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==}
|
||||
dev: true
|
||||
|
||||
/delayed-stream/1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/destr/1.2.2:
|
||||
resolution: {integrity: sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==}
|
||||
dev: true
|
||||
|
@ -2140,12 +2157,31 @@ packages:
|
|||
tabbable: 6.1.1
|
||||
dev: false
|
||||
|
||||
/follow-redirects/1.15.2:
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dev: false
|
||||
|
||||
/for-each/0.3.3:
|
||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
dev: true
|
||||
|
||||
/form-data/4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
dev: false
|
||||
|
||||
/fs-minipass/2.1.0:
|
||||
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -2572,23 +2608,6 @@ packages:
|
|||
/isexe/2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
/isomorphic-fetch/3.0.0:
|
||||
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
|
||||
dependencies:
|
||||
node-fetch: 2.6.9
|
||||
whatwg-fetch: 3.6.2
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/isomorphic-ws/5.0.0_ws@7.5.9:
|
||||
resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
|
||||
peerDependencies:
|
||||
ws: '*'
|
||||
dependencies:
|
||||
ws: 7.5.9
|
||||
dev: false
|
||||
|
||||
/jiti/1.17.1:
|
||||
resolution: {integrity: sha512-NZIITw8uZQFuzQimqjUxIrIcEdxYDFIe/0xYfIlVXTkiBjjyBEvgasj5bb0/cHtPRD/NziPbT312sFrkI5ALpw==}
|
||||
hasBin: true
|
||||
|
@ -2800,6 +2819,18 @@ packages:
|
|||
braces: 3.0.2
|
||||
picomatch: 2.3.1
|
||||
|
||||
/mime-db/1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/mime-types/2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: false
|
||||
|
||||
/mimic-fn/2.1.0:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -2894,18 +2925,6 @@ packages:
|
|||
resolution: {integrity: sha512-KIkvH1jl6b3O7es/0ShyCgWLcfXxlBrLBbP3rOr23WArC66IMcU4DeZEeYEOwnopYhawLTn7/y+YtmASe8DFVQ==}
|
||||
dev: true
|
||||
|
||||
/node-fetch/2.6.9:
|
||||
resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: false
|
||||
|
||||
/node-releases/2.0.10:
|
||||
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
|
||||
dev: true
|
||||
|
@ -3086,6 +3105,10 @@ packages:
|
|||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
dev: true
|
||||
|
||||
/proxy-from-env/1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
dev: false
|
||||
|
||||
/prr/1.0.1:
|
||||
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
||||
dev: true
|
||||
|
@ -3235,6 +3258,10 @@ packages:
|
|||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
dev: true
|
||||
|
||||
/simple-jsonrpc-js/1.2.0:
|
||||
resolution: {integrity: sha512-owkAmh7fjSYBUZVestTPCZMKYQvNiDejqZ/iGfVaKs1nrC1ZBDA3qGraf94+JNFJmu536Tb8oPe8PSPuq7GO6Q==}
|
||||
dev: false
|
||||
|
||||
/sirv/2.0.2:
|
||||
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
|
||||
engines: {node: '>= 10'}
|
||||
|
@ -3281,10 +3308,6 @@ packages:
|
|||
internal-slot: 1.0.5
|
||||
dev: true
|
||||
|
||||
/strict-event-emitter-types/2.0.0:
|
||||
resolution: {integrity: sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==}
|
||||
dev: false
|
||||
|
||||
/string_decoder/1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
dependencies:
|
||||
|
@ -3393,10 +3416,6 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/tr46/0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: false
|
||||
|
||||
/tslib/1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
dev: true
|
||||
|
@ -3846,10 +3865,6 @@ packages:
|
|||
'@vue/server-renderer': 3.2.47_vue@3.2.47
|
||||
'@vue/shared': 3.2.47
|
||||
|
||||
/webidl-conversions/3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
|
||||
/webpack-sources/3.2.3:
|
||||
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
@ -3857,17 +3872,6 @@ packages:
|
|||
/webpack-virtual-modules/0.5.0:
|
||||
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
|
||||
|
||||
/whatwg-fetch/3.6.2:
|
||||
resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==}
|
||||
dev: false
|
||||
|
||||
/whatwg-url/5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: false
|
||||
|
||||
/which-boxed-primitive/1.0.2:
|
||||
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
|
||||
dependencies:
|
||||
|
@ -3915,12 +3919,12 @@ packages:
|
|||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
dev: true
|
||||
|
||||
/ws/7.5.9:
|
||||
resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==}
|
||||
engines: {node: '>=8.3.0'}
|
||||
/ws/8.13.0:
|
||||
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
|
|
|
@ -3,22 +3,76 @@ import IDashboard from '~icons/ri/dashboard-2-line';
|
|||
import IRule from '~icons/material-symbols/rule-folder-outline-sharp';
|
||||
import IAddress from '~icons/eos-icons/ip';
|
||||
|
||||
|
||||
import { authenticate, logout, checkAuthentication, setup } from "./api";
|
||||
|
||||
enum NavState { Open, Reduced, Collapsed };
|
||||
const NavStateCount = 3;
|
||||
let navState = $ref(NavState.Open);
|
||||
let loggedOut = $ref(false);
|
||||
|
||||
const navRoutes = {
|
||||
"/": { icon: IDashboard, caption: "Dashboard" },
|
||||
"/rules": { icon: IRule, caption: "Rules" },
|
||||
"/firewall/rules": { icon: IRule, caption: "Rules" },
|
||||
"/addresses": { icon: IAddress, caption: "Addresses" },
|
||||
};
|
||||
|
||||
enum AuthState { Unauthenticated, MfaRequired, Authenticated };
|
||||
let authState = $ref(AuthState.Unauthenticated);
|
||||
let loginDisabled = $ref(true);
|
||||
|
||||
let username = $ref("");
|
||||
let password = $ref("");
|
||||
|
||||
|
||||
async function tryLogin() {
|
||||
loginDisabled = true;
|
||||
const res = await authenticate(username, password);
|
||||
password = "";
|
||||
loginDisabled = false;
|
||||
if (res.error != null) {
|
||||
console.info("authentication error");
|
||||
} else {
|
||||
// TODO Check for MFA here
|
||||
authState = AuthState.Authenticated;
|
||||
}
|
||||
}
|
||||
|
||||
async function tryLogout() {
|
||||
console.info("Logging out...");
|
||||
authState = AuthState.Unauthenticated;
|
||||
logout();
|
||||
}
|
||||
|
||||
function UnauthorizedCallback() {
|
||||
console.info("Unauthenticated");
|
||||
authState = AuthState.Unauthenticated;
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
console.info("Checking Auth State...");
|
||||
let res = await checkAuthentication();
|
||||
authState = res.auth;
|
||||
loginDisabled = false;
|
||||
if (authState === AuthState.Authenticated) {
|
||||
console.info("Already Authenticated ", authState);
|
||||
} else if (res.error == null) {
|
||||
console.info("Unauthorized");
|
||||
}
|
||||
else console.info("Check Authentication error",res.error);
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
setup(UnauthorizedCallback);
|
||||
await checkAuth();
|
||||
setInterval(function () {
|
||||
if (authState === AuthState.Authenticated && !document.hidden) {
|
||||
checkAuth();
|
||||
}
|
||||
}.bind(this), 120000);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!loggedOut" :class="{
|
||||
<div v-if="authState === AuthState.Authenticated" :class="{
|
||||
'layout': 1,
|
||||
'nav-state-open': navState === NavState.Open,
|
||||
'nav-state-collapsed': navState === NavState.Collapsed,
|
||||
|
@ -41,10 +95,10 @@ const navRoutes = {
|
|||
<div class="flex-row">
|
||||
<router-link class="button" to="/help"><i-material-symbols-help-outline/></router-link>
|
||||
<router-link class="button" to="/settings"><i-material-symbols-settings/></router-link>
|
||||
<button @click="() => loggedOut = true"><i-material-symbols-logout/></button>
|
||||
<button @click="tryLogout"><i-material-symbols-logout/></button>
|
||||
</div>
|
||||
</div>
|
||||
<router-view v-slot="{ Component, route }" v-if="!loggedOut">
|
||||
<router-view v-slot="{ Component, route }" v-if="authState === AuthState.Authenticated">
|
||||
<Transition name="fade">
|
||||
<component :is="Component" :key="{route}" class="page-content pad gap"/>
|
||||
</Transition>
|
||||
|
@ -52,16 +106,16 @@ const navRoutes = {
|
|||
</div>
|
||||
|
||||
<Transition name="fade">
|
||||
<div class="login" v-if="loggedOut">
|
||||
<div class="login" v-if="authState === AuthState.Unauthenticated">
|
||||
<FocusTrap>
|
||||
<form @submit="$event => $event.preventDefault()">
|
||||
<form @submit="$event => $event.preventDefault()" :disabled="loginDisabled">
|
||||
<h1>nfSense Login</h1>
|
||||
<label for="username" v-text="'Username'"/>
|
||||
<input name="username"/>
|
||||
<label for="password" v-text="'Password'" type="password"/>
|
||||
<input name="password"/>
|
||||
|
||||
<button @click="() => loggedOut = false">Login</button>
|
||||
<h2 :hidden="!loginDisabled">Logging in...</h2>
|
||||
<label for="username" v-text="'Username'" :hidden="loginDisabled" />
|
||||
<input name="username" v-model="username" :hidden="loginDisabled" :disabled="loginDisabled"/>
|
||||
<label for="password" v-text="'Password'" type="password" :hidden="loginDisabled"/>
|
||||
<input name="password" v-model="password" :hidden="loginDisabled" :disabled="loginDisabled"/>
|
||||
<button @click="tryLogin">Login</button>
|
||||
</form>
|
||||
</FocusTrap>
|
||||
</div>
|
||||
|
|
|
@ -1,14 +1,71 @@
|
|||
import { RequestManager, HTTPTransport, WebSocketTransport, Client } from "@open-rpc/client-js";
|
||||
const httpTransport = new HTTPTransport("http://"+ window.location.host +"/api");
|
||||
const socktransport = new WebSocketTransport("ws://"+ window.location.host + "/ws/api");
|
||||
const manager = new RequestManager([socktransport, httpTransport], () => crypto.randomUUID());
|
||||
const client = new Client(manager);
|
||||
// import WebSocketServer from 'ws';
|
||||
import JsonRPC from 'simple-jsonrpc-js';
|
||||
import axios from "axios";
|
||||
|
||||
export async function apiCall(method: string, params: Record<string, any>){
|
||||
let jrpc = new JsonRPC.connect_xhr('/api');
|
||||
// let socket = new WebSocket("ws://"+ window.location.host +"/ws/api");
|
||||
|
||||
let UnauthorizedCallback: Function;
|
||||
|
||||
export function setup(_UnauthorizedCallback: () => void) {
|
||||
UnauthorizedCallback = _UnauthorizedCallback;
|
||||
}
|
||||
|
||||
export async function apiCall(method: string, params: Record<string, any>): Promise<any>{
|
||||
console.debug("Starting API Call...");
|
||||
try {
|
||||
const result = await client.request({method, params});
|
||||
const result = await jrpc.call(method, params);
|
||||
console.debug("api call result", result);
|
||||
} catch (ex){
|
||||
return { Data: result, Error: null};
|
||||
} catch (ex: any){
|
||||
if (ex.code === 401) {
|
||||
UnauthorizedCallback();
|
||||
} else {
|
||||
console.debug("api call epic fail", ex);
|
||||
}
|
||||
return { Data: null, Error: ex};
|
||||
}
|
||||
}
|
||||
|
||||
export async function authenticate(username: string, password: string): Promise<any> {
|
||||
const pResponse = axios.post("/login", { username, password }, {timeout: 10100});
|
||||
try {
|
||||
const response = await pResponse;
|
||||
// Dont log this as the user password is inside: console.debug(response);
|
||||
return { data: response.data, error: null};
|
||||
} catch (error) {
|
||||
return { data: null, error: error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(): Promise<any> {
|
||||
const pResponse = axios.post("/logout", null, {timeout: 10100});
|
||||
try {
|
||||
const response = await pResponse;
|
||||
return { data: response.data, error: null};
|
||||
} catch (error) {
|
||||
return { data: null, error: error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAuthentication() {
|
||||
const pResponse = axios.post("/session", null, {timeout: 10100});
|
||||
try {
|
||||
const response = await pResponse;
|
||||
const last_hash = window.localStorage.getItem("commit_hash");
|
||||
|
||||
if (last_hash) {
|
||||
if (last_hash !== response.data.commit_hash) {
|
||||
console.log("Detected New Backend Version, Reloading...");
|
||||
window.localStorage.removeItem("commit_hash");
|
||||
window.location.reload();
|
||||
}
|
||||
} else window.localStorage.setItem("commit_hash", response.data.commit_hash);
|
||||
return {auth: 2, error: null};
|
||||
} catch (error: any) {
|
||||
if (error.response.status == 401) {
|
||||
return {auth: 0, error: null};
|
||||
}
|
||||
return {auth: 0, error: error};
|
||||
}
|
||||
}
|
29
client/src/pages/firewall/Rules.vue
Normal file
29
client/src/pages/firewall/Rules.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../../api";
|
||||
|
||||
let rules = $ref([]);
|
||||
|
||||
async function loadRules(){
|
||||
let res = await apiCall("Firewall.GetForwardRules", {});
|
||||
if (res.Error === null) {
|
||||
rules = res.Data.ForwardRules;
|
||||
console.debug("rules", rules);
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
loadRules();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader title="Forward Rules">
|
||||
<button @click="loadRules">Load Rules</button>
|
||||
</PageHeader>
|
||||
<NiceTable :columns="{name: 'Name', verdict: 'Verdict'}" :sort-self="false" v-model:data="rules"/>
|
||||
</div>
|
||||
</template>
|
|
@ -18,6 +18,9 @@ export default defineConfig({
|
|||
server: {
|
||||
"proxy": {
|
||||
"/api": "http://localhost:8080",
|
||||
"/login": "http://localhost:8080",
|
||||
"/logout": "http://localhost:8080",
|
||||
"/session": "http://localhost:8080",
|
||||
"/ws": {
|
||||
target: "ws://localhost:8080",
|
||||
ws: true,
|
||||
|
|
|
@ -34,7 +34,7 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("Validating Config")
|
||||
slog.Info("Validating Config...")
|
||||
|
||||
if *applyPtr {
|
||||
slog.Info("Applying Config...")
|
||||
|
@ -54,7 +54,7 @@ func main() {
|
|||
slog.Info("Starting Webserver...")
|
||||
server.StartWebserver(conf, apiHandler)
|
||||
|
||||
slog.Info("Ready")
|
||||
slog.Info("Ready.")
|
||||
|
||||
// Handle Exit Signal
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
|
|
|
@ -14,7 +14,6 @@ type Config struct {
|
|||
|
||||
func ValidateConfig(conf *Config) error {
|
||||
val := validator.New()
|
||||
slog.Info("Registering validator")
|
||||
val.RegisterValidation("test", nilIfOtherNil)
|
||||
return val.Struct(conf)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"runtime/debug"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
"nfsense.net/nfsense/pkg/session"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
@ -25,7 +26,7 @@ func NewHandler(maxRequestSize int64) *Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) error {
|
||||
func (h *Handler) HandleRequest(ctx context.Context, s *session.Session, r io.Reader, w io.Writer) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("Recovered Panic Handling JSONRPC Request", fmt.Errorf("%v", r), "stack", debug.Stack())
|
||||
|
@ -45,13 +46,17 @@ func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) e
|
|||
dec.DisallowUnknownFields()
|
||||
err = dec.Decode(&req)
|
||||
if err != nil {
|
||||
return respondError(w, "", ErrParse, fmt.Errorf("Parsing Request: %w", err))
|
||||
return respondError(w, "", ErrParse, fmt.Errorf("Decodeing Request: %w", err))
|
||||
}
|
||||
|
||||
if req.Jsonrpc != "2.0" {
|
||||
return respondError(w, req.ID, ErrMethodNotFound, fmt.Errorf("Unsupported Jsonrpc version %v", req.Jsonrpc))
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return respondError(w, req.ID, 401, fmt.Errorf("Unauthorized"))
|
||||
}
|
||||
|
||||
method, ok := h.methods[req.Method]
|
||||
if !ok {
|
||||
return respondError(w, req.ID, ErrMethodNotFound, fmt.Errorf("Unknown Method %v", req.Method))
|
||||
|
@ -60,11 +65,13 @@ func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) e
|
|||
p := reflect.New(method.inType)
|
||||
paramPointer := p.Interface()
|
||||
|
||||
if len(req.Params) != 0 {
|
||||
dec = json.NewDecoder(bytes.NewReader(req.Params))
|
||||
dec.DisallowUnknownFields()
|
||||
err = dec.Decode(paramPointer)
|
||||
if err != nil {
|
||||
return respondError(w, req.ID, ErrInvalidParams, fmt.Errorf("Parsing Request: %w", err))
|
||||
return respondError(w, req.ID, ErrInvalidParams, fmt.Errorf("Decoding Parameters: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
params := make([]reflect.Value, 3)
|
||||
|
|
|
@ -8,14 +8,17 @@ import (
|
|||
"time"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
"nfsense.net/nfsense/pkg/session"
|
||||
)
|
||||
|
||||
func HandleAPI(w http.ResponseWriter, r *http.Request) {
|
||||
_, s := GetSession(r)
|
||||
slog.Info("Api Handler hit")
|
||||
_, s := session.GetSession(r)
|
||||
if s == nil {
|
||||
// Fallthrough after so that jsonrpc can still deliver a valid jsonrpc error
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("Recovered Panic Handling HTTP API Request", fmt.Errorf("%v", r), "stack", debug.Stack())
|
||||
|
@ -23,10 +26,10 @@ func HandleAPI(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.WithValue(r.Context(), SessionKey, s), time.Second*10)
|
||||
ctx, cancel := context.WithTimeout(context.WithValue(r.Context(), session.SessionKey, s), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
err := apiHandler.HandleRequest(ctx, r.Body, w)
|
||||
err := apiHandler.HandleRequest(ctx, s, r.Body, w)
|
||||
if err != nil {
|
||||
slog.Error("Handling HTTP API Request", err)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"nfsense.net/nfsense/pkg/definitions"
|
||||
"nfsense.net/nfsense/pkg/jsonrpc"
|
||||
"nfsense.net/nfsense/pkg/session"
|
||||
)
|
||||
|
||||
var server http.Server
|
||||
|
@ -32,7 +33,7 @@ func StartWebserver(conf *definitions.Config, _apiHandler *jsonrpc.Handler) {
|
|||
|
||||
stopCleanup = make(chan struct{})
|
||||
|
||||
go CleanupSessions(stopCleanup)
|
||||
go session.CleanupSessions(stopCleanup)
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
|
|
|
@ -1,98 +1,63 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/exp/slog"
|
||||
"nfsense.net/nfsense/pkg/session"
|
||||
)
|
||||
|
||||
type SessionKeyType string
|
||||
|
||||
const SessionKey SessionKeyType = "session"
|
||||
|
||||
type Session struct {
|
||||
Username string
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
var sessionsSync sync.Mutex
|
||||
var sessions map[string]*Session = map[string]*Session{}
|
||||
|
||||
func GetSession(r *http.Request) (string, *Session) {
|
||||
c, err := r.Cookie("session")
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
s, ok := sessions[c.Value]
|
||||
if ok {
|
||||
return c.Value, s
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func GenerateSession(w http.ResponseWriter, username string) {
|
||||
id := uuid.New().String()
|
||||
expires := time.Now().Add(time.Minute * 5)
|
||||
sessionsSync.Lock()
|
||||
defer sessionsSync.Unlock()
|
||||
sessions[id] = &Session{
|
||||
Username: username,
|
||||
Expires: expires,
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: "session", HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: expires})
|
||||
}
|
||||
|
||||
func CleanupSessions(stop chan struct{}) {
|
||||
tick := time.NewTicker(time.Minute)
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
ids := []string{}
|
||||
sessionsSync.Lock()
|
||||
for id, s := range sessions {
|
||||
if time.Now().After(s.Expires) {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
for _, id := range ids {
|
||||
delete(sessions, id)
|
||||
}
|
||||
sessionsSync.Unlock()
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.PostFormValue("username")
|
||||
password := r.PostFormValue("password")
|
||||
if username == "admin" && password == "12345" {
|
||||
GenerateSession(w, username)
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
slog.Error("Reading Body", err)
|
||||
return
|
||||
}
|
||||
var req LoginRequest
|
||||
err = json.Unmarshal(buf, &req)
|
||||
if err != nil {
|
||||
slog.Error("Unmarshal", err)
|
||||
return
|
||||
}
|
||||
if req.Username == "admin" && req.Password == "12345" {
|
||||
slog.Info("User Login Successfull")
|
||||
session.GenerateSession(w, req.Username)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{Name: "session", Value: "", Expires: time.Now()})
|
||||
http.SetCookie(w, session.GetCookie("", time.Now()))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func HandleSession(w http.ResponseWriter, r *http.Request) {
|
||||
id, s := GetSession(r)
|
||||
id, s := session.GetSession(r)
|
||||
if s == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
sessionsSync.Lock()
|
||||
defer sessionsSync.Unlock()
|
||||
if s != nil {
|
||||
s.Expires = time.Now().Add(time.Minute * 5)
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: "session", HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: s.Expires})
|
||||
session.ExtendSession(s)
|
||||
http.SetCookie(w, session.GetCookie(id, s.Expires))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
resp := session.SessionResponse{
|
||||
CommitHash: session.CommitHash,
|
||||
}
|
||||
res, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(res)
|
||||
}
|
||||
|
|
|
@ -9,17 +9,18 @@ import (
|
|||
"time"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
"nfsense.net/nfsense/pkg/session"
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
func HandleWebsocketAPI(w http.ResponseWriter, r *http.Request) {
|
||||
_, s := GetSession(r)
|
||||
_, s := session.GetSession(r)
|
||||
if s == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.WithValue(r.Context(), SessionKey, s))
|
||||
ctx, cancel := context.WithCancel(context.WithValue(r.Context(), session.SessionKey, s))
|
||||
defer cancel()
|
||||
c, err := websocket.Accept(w, r, nil)
|
||||
if err != nil {
|
||||
|
@ -51,7 +52,7 @@ func HandleWebsocketAPI(w http.ResponseWriter, r *http.Request) {
|
|||
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
err := apiHandler.HandleRequest(ctx, bytes.NewReader(m), w)
|
||||
err := apiHandler.HandleRequest(ctx, s, bytes.NewReader(m), w)
|
||||
if err != nil {
|
||||
slog.Error("Handling Websocket API Request", err)
|
||||
}
|
||||
|
|
10
pkg/session/cookie.go
Normal file
10
pkg/session/cookie.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetCookie(value string, expires time.Time) *http.Cookie {
|
||||
return &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: value, Expires: expires}
|
||||
}
|
93
pkg/session/session.go
Normal file
93
pkg/session/session.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SessionKeyType string
|
||||
|
||||
const SessionKey SessionKeyType = "session"
|
||||
const SessionCookieName string = "session"
|
||||
|
||||
type Session struct {
|
||||
Username string
|
||||
Expires time.Time
|
||||
// TODO Add []websocket.Conn pointer to close all active websockets, alternativly do this via context cancelation
|
||||
}
|
||||
|
||||
type SessionResponse struct {
|
||||
CommitHash string `json:"commit_hash"`
|
||||
}
|
||||
|
||||
var sessionsSync sync.Mutex
|
||||
var sessions map[string]*Session = map[string]*Session{}
|
||||
|
||||
var CommitHash = func() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.revision" {
|
||||
return setting.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
return "asd"
|
||||
}()
|
||||
|
||||
func ExtendSession(s *Session) {
|
||||
sessionsSync.Lock()
|
||||
defer sessionsSync.Unlock()
|
||||
if s != nil {
|
||||
s.Expires = time.Now().Add(time.Minute * 5)
|
||||
}
|
||||
}
|
||||
|
||||
func GetSession(r *http.Request) (string, *Session) {
|
||||
c, err := r.Cookie("session")
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
s, ok := sessions[c.Value]
|
||||
if ok {
|
||||
return c.Value, s
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func GenerateSession(w http.ResponseWriter, username string) {
|
||||
id := uuid.New().String()
|
||||
expires := time.Now().Add(time.Minute * 5)
|
||||
sessionsSync.Lock()
|
||||
defer sessionsSync.Unlock()
|
||||
sessions[id] = &Session{
|
||||
Username: username,
|
||||
Expires: expires,
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: expires})
|
||||
}
|
||||
|
||||
func CleanupSessions(stop chan struct{}) {
|
||||
tick := time.NewTicker(time.Minute)
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
ids := []string{}
|
||||
sessionsSync.Lock()
|
||||
for id, s := range sessions {
|
||||
if time.Now().After(s.Expires) {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
for _, id := range ids {
|
||||
delete(sessions, id)
|
||||
}
|
||||
sessionsSync.Unlock()
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue