feat(frontend): add authorization

This commit is contained in:
Sergey Krylov 2025-08-22 05:28:56 +03:00
parent ee7be12428
commit 2c1a8ea127
43 changed files with 1788 additions and 27 deletions

View File

@ -1,7 +1,9 @@
import pluginQuery from '@tanstack/eslint-plugin-query';
// @ts-expect-error fix package
import config from 'eslint-config-ksv741';
export default [
...pluginQuery.configs['flat/recommended'],
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
...config,
{
@ -10,6 +12,74 @@ export default [
devDependencies: ['eslint.config.ts', 'rsbuild.config.ts'],
}],
'import/no-anonymous-default-export': 'off',
'import/order': [
'error', {
pathGroups: [
{
pattern: '__spec__/**',
group: 'builtin',
position: 'before',
},
{
pattern: 'apps/**',
group: 'internal',
position: 'after',
},
{
pattern: 'pages/**',
group: 'internal',
position: 'after',
},
{
pattern: 'widgets/**',
group: 'internal',
position: 'after',
},
{
pattern: 'features/**',
group: 'internal',
position: 'after',
},
{
pattern: 'entities/**',
group: 'internal',
position: 'after',
},
{
pattern: 'shared/**',
group: 'internal',
position: 'after',
},
],
distinctGroup: true,
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'object',
'index',
'type',
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
},
],
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/consistent-return': 'off',
'func-style': 'off',
'@typescript-eslint/no-confusing-void-expression': 'off',
'@typescript-eslint/await-thenable': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
},
},
{
ignores: ['./src/shared/api/schema.ts'],
},
];

View File

@ -8,19 +8,24 @@
"name": "Investments Control Frontend",
"version": "0.0.0",
"dependencies": {
"@tanstack/react-query": "5.85.5",
"axios": "1.11.0",
"react": "19.1.1",
"react-dom": "19.1.1"
"react-dom": "19.1.1",
"react-router": "7.8.1"
},
"devDependencies": {
"@rsbuild/core": "1.4.15",
"@rsbuild/plugin-eslint": "1.1.2",
"@rsbuild/plugin-react": "1.3.5",
"@rsbuild/plugin-type-check": "1.2.4",
"@tanstack/eslint-plugin-query": "5.83.1",
"@types/react": "19.1.9",
"@types/react-dom": "19.1.7",
"@typescript-eslint/parser": "8.39.0",
"eslint": "9.33.0",
"eslint-config-ksv741": "0.2.0",
"openapi-typescript": "7.9.1",
"typescript": "5.9.2"
}
},
@ -559,6 +564,82 @@
"node": ">= 8"
}
},
"node_modules/@redocly/ajv": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.3.tgz",
"integrity": "sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js-replace": "^1.0.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@redocly/ajv/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/config": {
"version": "0.22.2",
"resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz",
"integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/openapi-core": {
"version": "1.34.5",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.5.tgz",
"integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@redocly/ajv": "^8.11.2",
"@redocly/config": "^0.22.0",
"colorette": "^1.2.0",
"https-proxy-agent": "^7.0.5",
"js-levenshtein": "^1.1.6",
"js-yaml": "^4.1.0",
"minimatch": "^5.0.1",
"pluralize": "^8.0.0",
"yaml-ast-parser": "0.0.43"
},
"engines": {
"node": ">=18.17.0",
"npm": ">=9.5.0"
}
},
"node_modules/@redocly/openapi-core/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==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@redocly/openapi-core/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@rsbuild/core": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.4.15.tgz",
@ -903,6 +984,49 @@
"tslib": "^2.8.0"
}
},
"node_modules/@tanstack/eslint-plugin-query": {
"version": "5.83.1",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.83.1.tgz",
"integrity": "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/utils": "^8.37.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz",
"integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz",
"integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.85.5"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
@ -1531,6 +1655,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -1590,6 +1724,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -1827,6 +1971,12 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -1853,6 +2003,17 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -1930,7 +2091,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -1984,6 +2144,13 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/change-case": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
"dev": true,
"license": "MIT"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -2058,6 +2225,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2065,6 +2251,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/core-js": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
@ -2231,6 +2426,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@ -2248,7 +2452,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@ -2349,7 +2552,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2359,7 +2561,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2397,7 +2598,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@ -2410,7 +2610,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -3474,6 +3673,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -3490,6 +3709,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3509,7 +3744,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -3550,7 +3784,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@ -3575,7 +3808,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@ -3663,7 +3895,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -3742,7 +3973,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -3755,7 +3985,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@ -3771,7 +4000,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -3797,6 +4025,20 @@
],
"license": "MIT"
},
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/hyperdyperid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
@ -3844,6 +4086,19 @@
"node": ">=0.8.19"
}
},
"node_modules/index-to-position": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz",
"integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@ -4391,6 +4646,16 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -4545,7 +4810,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -4615,6 +4879,27 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -4801,6 +5086,40 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/openapi-typescript": {
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.9.1.tgz",
"integrity": "sha512-9gJtoY04mk6iPMbToPjPxEAtfXZ0dTsMZtsgUI8YZta0btPPig9DJFP4jlerQD/7QOwYgb0tl+zLUpDf7vb7VA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@redocly/openapi-core": "^1.34.5",
"ansi-colors": "^4.1.3",
"change-case": "^5.4.4",
"parse-json": "^8.3.0",
"supports-color": "^10.1.0",
"yargs-parser": "^21.1.1"
},
"bin": {
"openapi-typescript": "bin/cli.js"
},
"peerDependencies": {
"typescript": "^5.x"
}
},
"node_modules/openapi-typescript/node_modules/supports-color": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.0.tgz",
"integrity": "sha512-5eG9FQjEjDbAlI5+kdpdyPIBMRH4GfTVDGREVupaZHmVoppknhM29b/S9BkQz7cathp85BVgRi/As3Siln7e0Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -4882,6 +5201,24 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"index-to-position": "^1.1.0",
"type-fest": "^4.39.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -4929,6 +5266,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@ -4961,6 +5308,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -5030,6 +5383,28 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz",
"integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -5331,6 +5706,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -5836,6 +6217,19 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@ -5999,6 +6393,13 @@
"punycode": "^2.1.0"
}
},
"node_modules/uri-js-replace": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
"integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -6114,6 +6515,23 @@
"node": ">=0.10.0"
}
},
"node_modules/yaml-ast-parser": {
"version": "0.0.43",
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -8,22 +8,28 @@
"dev": "rsbuild dev",
"preview": "rsbuild preview",
"lint": "eslint",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"openapi:gen": "npx openapi-typescript http://localhost:3000/docs/swagger.json -o src/shared/api/schema.ts"
},
"dependencies": {
"@tanstack/react-query": "5.85.5",
"axios": "1.11.0",
"react": "19.1.1",
"react-dom": "19.1.1"
"react-dom": "19.1.1",
"react-router": "7.8.1"
},
"devDependencies": {
"@rsbuild/core": "1.4.15",
"@rsbuild/plugin-eslint": "1.1.2",
"@rsbuild/plugin-react": "1.3.5",
"@rsbuild/plugin-type-check": "1.2.4",
"@tanstack/eslint-plugin-query": "5.83.1",
"@types/react": "19.1.9",
"@types/react-dom": "19.1.7",
"@typescript-eslint/parser": "8.39.0",
"eslint": "9.33.0",
"eslint-config-ksv741": "0.2.0",
"openapi-typescript": "7.9.1",
"typescript": "5.9.2"
}
}

View File

@ -1,6 +1,6 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginEslint } from '@rsbuild/plugin-eslint';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
export default defineConfig({
@ -9,6 +9,8 @@ export default defineConfig({
pluginEslint({
eslintPluginOptions: {
configType: 'flat',
failOnError: false,
failOnWarning: false,
},
}),
pluginTypeCheck(),

View File

@ -1,7 +0,0 @@
const App = () => (
<div>
<h1>Investments Control Frontend</h1>
</div>
);
export default App;

View File

@ -0,0 +1 @@
export { default as App } from './ui/App';

View File

@ -0,0 +1,36 @@
import './normalize.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
BrowserRouter,
Route,
Routes,
} from 'react-router';
import { LoginPage, RegisterPage } from '@/pages/Authorization';
import { HomePage } from '@/pages/Home';
import { AuthContextProvider } from '@/shared/context/AuthContext';
import { AuthRoute } from '@/shared/ui/AuthRoute';
const queryClient = new QueryClient();
const App = () => (
<QueryClientProvider client={queryClient}>
<AuthContextProvider>
<BrowserRouter>
<Routes>
<Route element={<AuthRoute />}>
<Route element={<HomePage />} path="/" />
</Route>
<Route element={<AuthRoute needAuth={false} to="/" />}>
<Route element={<LoginPage />} path="/login" />
<Route element={<RegisterPage />} path="/register" />
</Route>
</Routes>
</BrowserRouter>
</AuthContextProvider>
</QueryClientProvider>
);
export default App;

349
frontend/src/app/App/ui/normalize.css vendored Normal file
View File

@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@ -0,0 +1,3 @@
export { useLogin } from './login/useLogin';
export { useRegister } from './register/useRegister';
export { useLogout } from './logout/useLogout';

View File

@ -0,0 +1,37 @@
import { useMutation } from '@tanstack/react-query';
import { useContext } from 'react';
import { useNavigate } from 'react-router';
import { api } from '@/shared/api/client';
import { AuthContext } from '@/shared/context/AuthContext';
import type { paths } from '@/shared/api/schema';
type LoginRequestBody = paths['/auth/login']['post']['requestBody']['content']['application/json'];
type LoginSuccessResponse = paths['/auth/login']['post']['responses']['200']['content']['application/json'];
async function login(data: LoginRequestBody): Promise<LoginSuccessResponse> {
const { data: result } = await api.post<LoginSuccessResponse>('/auth/login', data);
return result;
}
export const useLogin = () => {
const navigate = useNavigate();
const { setAccessToken } = useContext(AuthContext);
return useMutation({
mutationFn: login,
onSuccess: async (data) => {
setAccessToken(data.accessToken);
localStorage.setItem('accessToken', data.accessToken);
await navigate('/another');
},
onError: (error) => {
// eslint-disable-next-line
console.log('Неуспешный логин', error);
throw error;
},
});
};

View File

@ -0,0 +1,30 @@
import { useMutation } from '@tanstack/react-query';
import { useContext } from 'react';
import { useNavigate } from 'react-router';
import { api } from '@/shared/api/client';
import { AuthContext } from '@/shared/context/AuthContext';
import type { paths } from '@/shared/api/schema';
type LogoutSuccessResponse = paths['/auth/logout']['post']['responses']['200']['content']['application/json'];
async function logout() {
const { data: result } = await api.post<LogoutSuccessResponse>('/auth/logout');
return result;
}
export const useLogout = () => {
const navigate = useNavigate();
const { setAccessToken } = useContext(AuthContext);
return useMutation({
mutationFn: logout,
onSuccess: async () => {
setAccessToken('');
localStorage.removeItem('accessToken');
await navigate('/login');
},
});
};

View File

@ -0,0 +1,22 @@
import { useMutation } from '@tanstack/react-query';
import { api } from '@/shared/api/client';
import type { paths } from '@/shared/api/schema';
type RegisterRequestBody = paths['/auth/register']['post']['requestBody']['content']['application/json'];
type RegisterSuccessResponse = paths['/auth/register']['post']['responses']['200']['content']['application/json'];
async function register(data: RegisterRequestBody) {
const { data: result } = await api.post<RegisterSuccessResponse>('auth/register', data);
return result;
}
export const useRegister = () => useMutation({
mutationFn: register,
onError: (error) => {
// eslint-disable-next-line no-console
console.log('Неуспешная регистрация', error);
},
});

View File

@ -0,0 +1 @@
export { default as LogoutButton } from './ui/LogoutButton.ui';

View File

@ -0,0 +1,21 @@
import { useLogout } from '@/entity/account';
const LogoutButton = () => {
const { mutate: logout, isPending } = useLogout();
const onClickHandler = async () => {
await logout();
};
return (
<button
disabled={isPending}
type="button"
onClick={onClickHandler}
>
Logout
</button>
);
};
export default LogoutButton;

View File

@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { App } from '@/app/App';
const rootEl = document.getElementById('root');
if (rootEl) {

View File

@ -0,0 +1,2 @@
export { default as LoginPage } from './ui/Login/LoginPage.ui';
export { default as RegisterPage } from './ui/Register/RegisterPage.ui';

View File

@ -0,0 +1,19 @@
.overlay {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
background: darkgrey;
}
.contentWrapper {
width: 400px;
height: 500px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 10px;
background: lightgray;
}

View File

@ -0,0 +1,20 @@
import { NavLink } from 'react-router';
import { LoginForm } from '@/widgets/auth/LoginForm';
import classes from './LoginPage.module.css';
const LoginPage = () => (
<div className={classes.overlay}>
<div className={classes.contentWrapper}>
<h1>Вход в аккаунт</h1>
<LoginForm />
<NavLink end to="/register">
Регистрация
</NavLink>
</div>
</div>
);
export default LoginPage;

View File

@ -0,0 +1,19 @@
.overlay {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
background: darkgrey;
}
.contentWrapper {
width: 400px;
height: 500px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 10px;
background: lightgray;
}

View File

@ -0,0 +1,21 @@
import { NavLink } from 'react-router';
import { RegisterForm } from '@/widgets/auth/RegisterForm';
import classes from './RegisterPage.module.css';
const RegisterPage = () => (
<div className={classes.overlay}>
<div className={classes.contentWrapper}>
<h1>Регистрация аккаунта</h1>
<RegisterForm />
<NavLink end to="/login">
Вход
</NavLink>
</div>
</div>
);
export default RegisterPage;

View File

@ -0,0 +1 @@
export { default as HomePage } from './ui/HomePage.ui';

View File

@ -0,0 +1,51 @@
import { useContext, useState } from 'react';
import { api } from '@/shared/api/client';
import { AuthContext } from '@/shared/context/AuthContext';
import type { paths } from '@/shared/api/schema';
type Response = paths['/auth/me']['get']['responses']['200']['content']['application/json'];
const fetchProfile = async () => {
const { data } = await api.get<Response>('/auth/me');
return data;
};
const HomePage = () => {
const [info, setInfo] = useState<Response>();
const { accessToken } = useContext(AuthContext);
const onClick = () => {
if (!accessToken) {
return;
}
fetchProfile()
.then((data) => {
// eslint-disable-next-line no-console
console.log('DATA', data);
setInfo(data);
});
};
return (
<div>
Home page only for auth user
<button type="button" onClick={onClick}>
get profile
</button>
<br />
<b>
ID:
{info?.id}
</b>
</div>
);
};
export default HomePage;

View File

@ -0,0 +1,46 @@
import axios from 'axios';
import type { paths } from './schema';
const BASE_URL = 'http://localhost:3000';
export const api = axios.create({
baseURL: BASE_URL,
withCredentials: true,
});
api.interceptors.request.use(
(request) => {
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
request.headers.set('Authorization', `Bearer ${accessToken}`);
}
return request;
},
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
async (error) => Promise.reject(error),
);
type RefreshTokensSuccessResponse = paths['/auth/refresh']['post']['responses']['200']['content']['application/json'];
api.interceptors.response.use((response) => response, async (error) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
const originalRequest = error.config;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error.response?.status === 401 && !originalRequest._retry) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
originalRequest._retry = true;
const { data: result } = await axios.post<RefreshTokensSuccessResponse>(`${BASE_URL}/auth/refresh`, null, {
withCredentials: true,
});
const { accessToken } = result;
localStorage.setItem('accessToken', accessToken);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
return api(error.config);
}
throw error;
});

View File

@ -0,0 +1,335 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/auth/register": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Регистрация
* @description Регистрация аккаунта
*/
post: operations["AuthController_register"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/login": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Логин
* @description Вход в аккаунт
*/
post: operations["AuthController_login"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/logout": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Логаут
* @description Выход из аккаунта
*/
post: operations["AuthController_logout"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/me": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Получить профиль
* @description Получить данные о своем профиле
*/
get: operations["AuthController_me"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/refresh": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Обновление access и refresh токенов */
post: operations["AuthController_refresh"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
RegisterDto: {
/**
* @description Email
* @example mail@example.com
*/
email: string;
/**
* @description Пароль
* @example 123456
*/
password: string;
};
RegisterValidationDto: {
/** @example 400 */
statusCode: number;
/** @example [
* "Email должен быть строкой.",
* "Некорректный формат email.",
* "Email обязателен для заполнения.",
* "Пароль должен быть строкой.",
* "Пароль обязателен для заполнения.",
* "Пароль должен содержать минимум 6 символов."
* ] */
message: string[];
/** @example Bad Request */
error: string;
};
ConflictRegisterDto: {
/** @example 400 */
statusCode: number;
/** @example Пользователь с такой почтой уже зарегистрирован */
message: Record<string, never>;
/** @example Conflict */
error: string;
};
LoginDto: {
/**
* @description Email
* @example mail@example.com
*/
email: string;
/**
* @description Пароль
* @example 123456
*/
password: string;
};
SuccessLoginDto: {
/** @description Access токен */
accessToken: string;
};
IncorrectLoginDto: {
/** @example 400 */
statusCode: number;
/** @example Неверный логин или пароль */
message: Record<string, never>;
/** @example Bad Request */
error: string;
};
CurrentProfileDto: {
/** @description Индетификатор аккаунта */
id: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
AuthController_register: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["RegisterDto"];
};
};
responses: {
/** @description Успешная регистрация */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["RegisterDto"];
};
};
/** @description Ошибка валидации */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["RegisterValidationDto"];
};
};
/** @description Ошибка создания аккаунта */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ConflictRegisterDto"];
};
};
};
};
AuthController_login: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["LoginDto"];
};
};
responses: {
/** @description Успешный вход в аккаунт */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SuccessLoginDto"];
};
};
/** @description Неверный логин или пароль */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["IncorrectLoginDto"];
};
};
};
};
AuthController_logout: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Успешный выход из аккаунт */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": boolean;
};
};
};
};
AuthController_me: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Успешное обновление токенов */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CurrentProfileDto"];
};
};
/** @description Пользователь не авторизован */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
AuthController_refresh: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Успешное обновление токенов */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SuccessLoginDto"];
};
};
/** @description Пользователь не авторизован */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
}

View File

@ -0,0 +1,43 @@
import {
createContext,
useMemo,
useState,
} from 'react';
import type {
Dispatch,
FC,
ReactNode,
SetStateAction,
} from 'react';
export type AuthContextType = {
accessToken: string;
setAccessToken: Dispatch<SetStateAction<string>>;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
export const AuthContext = createContext<AuthContextType>(null as unknown as AuthContextType);
function getInitialToken() {
return localStorage.getItem('accessToken') ?? '';
}
type AuthContextProviderProps = {
children: ReactNode;
};
export const AuthContextProvider: FC<AuthContextProviderProps> = (props) => {
const { children } = props;
const [accessToken, setAccessToken] = useState<string>(getInitialToken);
const value = useMemo(() => ({
accessToken,
setAccessToken,
}), [accessToken]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};

View File

@ -0,0 +1 @@
export { AuthContext, AuthContextProvider } from './AuthContext.ui';

View File

@ -0,0 +1 @@
export { PAGES } from './types';

View File

@ -0,0 +1,5 @@
export enum PAGES {
Home = '/',
Login = '/login',
Register = '/register',
}

View File

@ -0,0 +1 @@
export { default as AuthRoute } from './ui/AuthRoute.ui';

View File

@ -0,0 +1,28 @@
import { type FC, useContext } from 'react';
import { Navigate, Outlet } from 'react-router';
import { AuthContext } from '@/shared/context/AuthContext';
import type { NavigateProps } from 'react-router';
type AuthRouteProps = {
needAuth?: boolean;
to?: NavigateProps['to'];
};
const AuthRoute: FC<AuthRouteProps> = (props) => {
const {
needAuth = true,
to = '/login',
} = props;
const { accessToken } = useContext(AuthContext);
if ((needAuth && accessToken) || (!needAuth && !accessToken)) {
return <Outlet />;
}
return (
<Navigate to={to} />
);
};
export default AuthRoute;

View File

@ -0,0 +1 @@
export { default as Input } from './ui/Input.ui';

View File

@ -0,0 +1,4 @@
.wrapper {
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,24 @@
import classes from './Input.styles.module.css';
import type { FC, HTMLProps } from 'react';
type InputProps = HTMLProps<HTMLInputElement> & {
label: string;
};
const Input: FC<InputProps> = (props) => {
const { label, id, ...rest } = props;
return (
<label className={classes.wrapper} htmlFor={id}>
{label}
<input
id={id}
{...rest}
/>
</label>
);
};
export default Input;

View File

@ -0,0 +1,3 @@
export const wait = async (time: number) => new Promise((res) => {
setTimeout(res, time);
});

View File

@ -0,0 +1 @@
export { default as LoginForm } from './ui/LoginForm.ui';

View File

@ -0,0 +1,10 @@
.form {
display: flex;
flex-direction: column;
gap: 10px;
width: 300px;
button:disabled {
background: red;
}
}

View File

@ -0,0 +1,39 @@
import { Input } from '@/shared/ui/input';
import { useLoginForm } from './hooks/useLoginForm';
import classes from './LoginForm.module.css';
import type { FC } from 'react';
const LoginForm: FC = () => {
const { isPending, onSubmitHandler } = useLoginForm();
return (
<form className={classes.form} onSubmit={onSubmitHandler}>
<Input
disabled={isPending}
id="email"
label="Почта"
name="email"
type="text"
/>
<Input
disabled={isPending}
id="password"
label="Пароль"
name="password"
type="password"
/>
<button
disabled={isPending}
type="submit"
>
Войти
</button>
</form>
);
};
export default LoginForm;

View File

@ -0,0 +1,23 @@
import { useLogin } from '@/entity/account';
import type { FormEventHandler } from 'react';
export const useLoginForm = () => {
const { mutate: login, isPending } = useLogin();
const onSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get('email');
const password = formData.get('password');
if (typeof email === 'string' && typeof password === 'string') {
login({ email, password });
}
};
return {
isPending,
onSubmitHandler,
};
};

View File

@ -0,0 +1 @@
export { default as RegisterForm } from './ui/RegisterForm.ui';

View File

@ -0,0 +1,10 @@
.form {
display: flex;
flex-direction: column;
gap: 10px;
width: 300px;
button:disabled {
background: red;
}
}

View File

@ -0,0 +1,36 @@
import { Input } from '@/shared/ui/input';
import { useRegisterForm } from './hooks/useRegisterForm';
import classes from './RegisterForm.module.css';
import type { FC } from 'react';
const RegisterForm: FC = () => {
const { onSubmitHandler, isPending } = useRegisterForm();
return (
<form className={classes.form} onSubmit={onSubmitHandler}>
<Input
disabled={isPending}
id="email"
label="Почта"
name="email"
type="text"
/>
<Input
disabled={isPending}
id="password"
label="Пароль"
name="password"
type="password"
/>
<button disabled={isPending} type="submit">
Зарегистрироваться
</button>
</form>
);
};
export default RegisterForm;

View File

@ -0,0 +1,23 @@
import { useRegister } from '@/entity/account';
import type { FormEventHandler } from 'react';
export const useRegisterForm = () => {
const { mutate: register, isPending } = useRegister();
const onSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get('email');
const password = formData.get('password');
if (typeof email === 'string' && typeof password === 'string') {
register({ email, password });
}
};
return {
onSubmitHandler,
isPending,
};
};

View File

@ -19,7 +19,10 @@
/* type checking */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
"noUnusedParameters": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"src",