feat(user): migrate from gRPC to REST

This commit is contained in:
Vyacheslav1557 2025-02-25 18:51:38 +05:00
parent 9160897233
commit e634d8bd56
23 changed files with 684 additions and 376 deletions

View file

@ -1,4 +0,0 @@
gen:
@protoc --proto_path=proto --openapi_out=proto/user/v1 \
proto/user/v1/user.proto
@pnpm openapi-generator-cli generate -g typescript-axios -i ./proto/user/v1/openapi.yaml -o ./proto/user/v1/api

View file

@ -1,9 +0,0 @@
import {Configuration, UserServiceApi} from "./proto/user/v1/api";
const configuration = new Configuration({
basePath: "http://localhost:60001"
})
const userApi = new UserServiceApi(configuration);
export {userApi};

View file

@ -6,27 +6,29 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"gen": "openapi-generator-cli generate -g typescript-axios -i ./proto/user/v1/openapi.yaml -o ./proto/user/v1/api"
},
"dependencies": {
"@mantine/core": "^7.16.3",
"@mantine/dropzone": "^7.16.3",
"@mantine/form": "^7.16.3",
"@mantine/hooks": "^7.16.3",
"@reduxjs/toolkit": "^2.5.1",
"@mantine/core": "^7.17.0",
"@mantine/dropzone": "^7.17.0",
"@mantine/form": "^7.17.0",
"@mantine/hooks": "^7.17.0",
"@tabler/icons-react": "^3.30.0",
"@tanstack/react-query": "^5.66.3",
"@tanstack/react-query": "^5.66.7",
"axios": "^1.7.9",
"next": "15.1.0",
"jsonwebtoken": "^9.0.2",
"next": "^15.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"redux-query": "3.6.1-rc.2"
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.16.3",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.17.19",
"@types/react": "^19.0.9",
"@types/react-dom": "^19.0.3",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"postcss": "^8.5.2",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",

340
pnpm-lock.yaml generated
View file

@ -6,32 +6,32 @@ settings:
dependencies:
'@mantine/core':
specifier: ^7.16.3
version: 7.16.3(@mantine/hooks@7.16.3)(@types/react@19.0.9)(react-dom@19.0.0)(react@19.0.0)
specifier: ^7.17.0
version: 7.17.0(@mantine/hooks@7.17.0)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0)
'@mantine/dropzone':
specifier: ^7.16.3
version: 7.16.3(@mantine/core@7.16.3)(@mantine/hooks@7.16.3)(react-dom@19.0.0)(react@19.0.0)
specifier: ^7.17.0
version: 7.17.0(@mantine/core@7.17.0)(@mantine/hooks@7.17.0)(react-dom@19.0.0)(react@19.0.0)
'@mantine/form':
specifier: ^7.16.3
version: 7.16.3(react@19.0.0)
specifier: ^7.17.0
version: 7.17.0(react@19.0.0)
'@mantine/hooks':
specifier: ^7.16.3
version: 7.16.3(react@19.0.0)
'@reduxjs/toolkit':
specifier: ^2.5.1
version: 2.5.1(react@19.0.0)
specifier: ^7.17.0
version: 7.17.0(react@19.0.0)
'@tabler/icons-react':
specifier: ^3.30.0
version: 3.30.0(react@19.0.0)
'@tanstack/react-query':
specifier: ^5.66.3
version: 5.66.3(react@19.0.0)
specifier: ^5.66.7
version: 5.66.7(react@19.0.0)
axios:
specifier: ^1.7.9
version: 1.7.9
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
next:
specifier: 15.1.0
version: 15.1.0(react-dom@19.0.0)(react@19.0.0)
specifier: ^15.1.7
version: 15.1.7(react-dom@19.0.0)(react@19.0.0)
react:
specifier: ^19.0.0
version: 19.0.0
@ -46,15 +46,18 @@ devDependencies:
'@openapitools/openapi-generator-cli':
specifier: ^2.16.3
version: 2.16.3
'@types/jsonwebtoken':
specifier: ^9.0.9
version: 9.0.9
'@types/node':
specifier: ^20.17.19
version: 20.17.19
'@types/react':
specifier: ^19.0.9
version: 19.0.9
specifier: ^19.0.10
version: 19.0.10
'@types/react-dom':
specifier: ^19.0.3
version: 19.0.3(@types/react@19.0.9)
specifier: ^19.0.4
version: 19.0.4(@types/react@19.0.10)
postcss:
specifier: ^8.5.2
version: 8.5.2
@ -310,43 +313,43 @@ packages:
engines: {node: '>=8'}
dev: true
/@mantine/core@7.16.3(@mantine/hooks@7.16.3)(@types/react@19.0.9)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-cxhIpfd2i0Zmk9TKdejYAoIvWouMGhzK3OOX+VRViZ5HEjnTQCGl2h3db56ThqB6NfVPCno6BPbt5lwekTtmuQ==}
/@mantine/core@7.17.0(@mantine/hooks@7.17.0)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-AU5UFewUNzBCUXIq5Jk6q402TEri7atZW61qHW6P0GufJ2W/JxGHRvgmHOVHTVIcuWQRCt9SBSqZoZ/vHs9LhA==}
peerDependencies:
'@mantine/hooks': 7.16.3
'@mantine/hooks': 7.17.0
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
dependencies:
'@floating-ui/react': 0.26.28(react-dom@19.0.0)(react@19.0.0)
'@mantine/hooks': 7.16.3(react@19.0.0)
'@mantine/hooks': 7.17.0(react@19.0.0)
clsx: 2.1.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-number-format: 5.4.3(react-dom@19.0.0)(react@19.0.0)
react-remove-scroll: 2.6.3(@types/react@19.0.9)(react@19.0.0)
react-textarea-autosize: 8.5.6(@types/react@19.0.9)(react@19.0.0)
type-fest: 4.34.1
react-remove-scroll: 2.6.3(@types/react@19.0.10)(react@19.0.0)
react-textarea-autosize: 8.5.6(@types/react@19.0.10)(react@19.0.0)
type-fest: 4.35.0
transitivePeerDependencies:
- '@types/react'
dev: false
/@mantine/dropzone@7.16.3(@mantine/core@7.16.3)(@mantine/hooks@7.16.3)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-JWKmRMuV0DfgIQWvvtRfokaIopezg2AwxxcXrHs5xxxN1EfiTQWB+aQjz0ISwcAk1gtjLEKHowqsBNbna+BEKw==}
/@mantine/dropzone@7.17.0(@mantine/core@7.17.0)(@mantine/hooks@7.17.0)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-1BGOH/Fs1xxsVl6JUxFAElwqdmtj1nrzc7QSV3vs3xh7zAIAH6wqeor8j8+yycxz4lCfehHSaVAyDDv3AFsX8w==}
peerDependencies:
'@mantine/core': 7.16.3
'@mantine/hooks': 7.16.3
'@mantine/core': 7.17.0
'@mantine/hooks': 7.17.0
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
dependencies:
'@mantine/core': 7.16.3(@mantine/hooks@7.16.3)(@types/react@19.0.9)(react-dom@19.0.0)(react@19.0.0)
'@mantine/hooks': 7.16.3(react@19.0.0)
'@mantine/core': 7.17.0(@mantine/hooks@7.17.0)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0)
'@mantine/hooks': 7.17.0(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-dropzone-esm: 15.2.0(react@19.0.0)
dev: false
/@mantine/form@7.16.3(react@19.0.0):
resolution: {integrity: sha512-GqomUG2Ri5adxYsTU1S5IhKRPcqTG5JkPvMERns8PQAcUz/lvzsnk3wY1v4K5CEbCAdpimle4bSsZTM9g697vg==}
/@mantine/form@7.17.0(react@19.0.0):
resolution: {integrity: sha512-LONdeb+wL8h9fvyQ339ZFLxqrvYff+b+H+kginZhnr45OBTZDLXNVAt/YoKVFEkynF9WDJjdBVrXKcOZvPgmrA==}
peerDependencies:
react: ^18.x || ^19.x
dependencies:
@ -355,8 +358,8 @@ packages:
react: 19.0.0
dev: false
/@mantine/hooks@7.16.3(react@19.0.0):
resolution: {integrity: sha512-B94FBWk5Sc81tAjV+B3dGh/gKzfqzpzVC/KHyBRWOOyJRqeeRbI/FAaJo4zwppyQo1POSl5ArdyjtDRrRIj2SQ==}
/@mantine/hooks@7.17.0(react@19.0.0):
resolution: {integrity: sha512-vo3K49mLy1nJ8LQNb5KDbJgnX0xwt3Y8JOF3ythjB5LEFMptdLSSgulu64zj+QHtzvffFCsMb05DbTLLpVP/JQ==}
peerDependencies:
react: ^18.x || ^19.x
dependencies:
@ -426,12 +429,12 @@ packages:
- encoding
dev: true
/@next/env@15.1.0:
resolution: {integrity: sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==}
/@next/env@15.1.7:
resolution: {integrity: sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==}
dev: false
/@next/swc-darwin-arm64@15.1.0:
resolution: {integrity: sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==}
/@next/swc-darwin-arm64@15.1.7:
resolution: {integrity: sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@ -439,8 +442,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@15.1.0:
resolution: {integrity: sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==}
/@next/swc-darwin-x64@15.1.7:
resolution: {integrity: sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@ -448,8 +451,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@15.1.0:
resolution: {integrity: sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==}
/@next/swc-linux-arm64-gnu@15.1.7:
resolution: {integrity: sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@ -457,8 +460,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@15.1.0:
resolution: {integrity: sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==}
/@next/swc-linux-arm64-musl@15.1.7:
resolution: {integrity: sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@ -466,8 +469,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@15.1.0:
resolution: {integrity: sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==}
/@next/swc-linux-x64-gnu@15.1.7:
resolution: {integrity: sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@ -475,8 +478,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@15.1.0:
resolution: {integrity: sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==}
/@next/swc-linux-x64-musl@15.1.7:
resolution: {integrity: sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@ -484,8 +487,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@15.1.0:
resolution: {integrity: sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==}
/@next/swc-win32-arm64-msvc@15.1.7:
resolution: {integrity: sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@ -493,8 +496,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@15.1.0:
resolution: {integrity: sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==}
/@next/swc-win32-x64-msvc@15.1.7:
resolution: {integrity: sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@ -570,24 +573,6 @@ packages:
- supports-color
dev: true
/@reduxjs/toolkit@2.5.1(react@19.0.0):
resolution: {integrity: sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
dependencies:
immer: 10.1.1
react: 19.0.0
redux: 5.0.1
redux-thunk: 3.1.0(redux@5.0.1)
reselect: 5.1.1
dev: false
/@swc/counter@0.1.3:
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
dev: false
@ -611,16 +596,16 @@ packages:
resolution: {integrity: sha512-c8OKLM48l00u9TFbh2qhSODMONIzML8ajtCyq95rW8vzkWcBrKRPM61tdkThz2j4kd5u17srPGIjqdeRUZdfdw==}
dev: false
/@tanstack/query-core@5.66.3:
resolution: {integrity: sha512-+2iDxH7UFdtwcry766aJszGmbByQDIzTltJ3oQAZF9bhCxHCIN3yDwHa6qDCZxcpMGvUphCRx/RYJvLbM8mucQ==}
/@tanstack/query-core@5.66.4:
resolution: {integrity: sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==}
dev: false
/@tanstack/react-query@5.66.3(react@19.0.0):
resolution: {integrity: sha512-sWMvxZ5VugPDgD1CzP7f0s9yFvjcXP3FXO5IVV2ndXlYqUCwykU8U69Kk05Qn5UvGRqB/gtj4J7vcTC6vtLHtQ==}
/@tanstack/react-query@5.66.7(react@19.0.0):
resolution: {integrity: sha512-qd3q/tUpF2K1xItfPZddk1k/8pSXnovg41XyCqJgPoyYEirMBtB0sVEVVQ/CsAOngzgWtBPXimVf4q4kM9uO6A==}
peerDependencies:
react: ^18 || ^19
dependencies:
'@tanstack/query-core': 5.66.3
'@tanstack/query-core': 5.66.4
react: 19.0.0
dev: false
@ -628,22 +613,33 @@ packages:
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
dev: true
/@types/jsonwebtoken@9.0.9:
resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==}
dependencies:
'@types/ms': 2.1.0
'@types/node': 20.17.19
dev: true
/@types/ms@2.1.0:
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
dev: true
/@types/node@20.17.19:
resolution: {integrity: sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==}
dependencies:
undici-types: 6.19.8
dev: true
/@types/react-dom@19.0.3(@types/react@19.0.9):
resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==}
/@types/react-dom@19.0.4(@types/react@19.0.10):
resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==}
peerDependencies:
'@types/react': ^19.0.0
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
dev: true
/@types/react@19.0.9:
resolution: {integrity: sha512-FedNTYgmMwSZmD1Sru/W1gJKuiYCN/3SuBkmZkcxX+FpO5zL76B22A9YNfAKg4HQO3Neh/30AiynP6BELdU0qQ==}
/@types/react@19.0.10:
resolution: {integrity: sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==}
dependencies:
csstype: 3.1.3
@ -728,6 +724,10 @@ packages:
fill-range: 7.1.1
dev: true
/buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
dev: false
/buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
dependencies:
@ -772,8 +772,8 @@ packages:
engines: {node: '>= 6'}
dev: true
/caniuse-lite@1.0.30001699:
resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==}
/caniuse-lite@1.0.30001700:
resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==}
dev: false
/chalk@4.1.2:
@ -986,6 +986,12 @@ packages:
wcwidth: 1.0.1
dev: true
/ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
safe-buffer: 5.2.1
dev: false
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: true
@ -1259,10 +1265,6 @@ packages:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true
/immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
dev: false
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
@ -1380,11 +1382,70 @@ packages:
resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==}
dev: false
/jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
dependencies:
jws: 3.2.2
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.1
dev: false
/jwa@1.4.1:
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
dev: false
/jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
dependencies:
jwa: 1.4.1
safe-buffer: 5.2.1
dev: false
/klona@2.0.6:
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
engines: {node: '>= 8'}
dev: false
/lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
dev: false
/lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
dev: false
/lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
dev: false
/lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
dev: false
/lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: false
/lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
dev: false
/lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
dev: false
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
@ -1464,7 +1525,6 @@ packages:
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
/mute-stream@0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
@ -1480,8 +1540,8 @@ packages:
engines: {node: '>= 0.4.0'}
dev: true
/next@15.1.0(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==}
/next@15.1.7(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@ -1501,24 +1561,24 @@ packages:
sass:
optional: true
dependencies:
'@next/env': 15.1.0
'@next/env': 15.1.7
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
caniuse-lite: 1.0.30001699
caniuse-lite: 1.0.30001700
postcss: 8.4.31
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
styled-jsx: 5.1.6(react@19.0.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.1.0
'@next/swc-darwin-x64': 15.1.0
'@next/swc-linux-arm64-gnu': 15.1.0
'@next/swc-linux-arm64-musl': 15.1.0
'@next/swc-linux-x64-gnu': 15.1.0
'@next/swc-linux-x64-musl': 15.1.0
'@next/swc-win32-arm64-msvc': 15.1.0
'@next/swc-win32-x64-msvc': 15.1.0
'@next/swc-darwin-arm64': 15.1.7
'@next/swc-darwin-x64': 15.1.7
'@next/swc-linux-arm64-gnu': 15.1.7
'@next/swc-linux-arm64-musl': 15.1.7
'@next/swc-linux-x64-gnu': 15.1.7
'@next/swc-linux-x64-musl': 15.1.7
'@next/swc-win32-arm64-msvc': 15.1.7
'@next/swc-win32-x64-msvc': 15.1.7
sharp: 0.33.5
transitivePeerDependencies:
- '@babel/core'
@ -1574,8 +1634,8 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/pac-proxy-agent@7.1.0:
resolution: {integrity: sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==}
/pac-proxy-agent@7.2.0:
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
engines: {node: '>= 14'}
dependencies:
'@tootallnate/quickjs-emscripten': 0.23.0
@ -1713,7 +1773,7 @@ packages:
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
lru-cache: 7.18.3
pac-proxy-agent: 7.1.0
pac-proxy-agent: 7.2.0
proxy-from-env: 1.1.0
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
@ -1760,7 +1820,7 @@ packages:
react-dom: 19.0.0(react@19.0.0)
dev: false
/react-remove-scroll-bar@2.3.8(@types/react@19.0.9)(react@19.0.0):
/react-remove-scroll-bar@2.3.8(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
peerDependencies:
@ -1770,13 +1830,13 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
react: 19.0.0
react-style-singleton: 2.2.3(@types/react@19.0.9)(react@19.0.0)
react-style-singleton: 2.2.3(@types/react@19.0.10)(react@19.0.0)
tslib: 2.8.1
dev: false
/react-remove-scroll@2.6.3(@types/react@19.0.9)(react@19.0.0):
/react-remove-scroll@2.6.3(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
engines: {node: '>=10'}
peerDependencies:
@ -1786,16 +1846,16 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
react: 19.0.0
react-remove-scroll-bar: 2.3.8(@types/react@19.0.9)(react@19.0.0)
react-style-singleton: 2.2.3(@types/react@19.0.9)(react@19.0.0)
react-remove-scroll-bar: 2.3.8(@types/react@19.0.10)(react@19.0.0)
react-style-singleton: 2.2.3(@types/react@19.0.10)(react@19.0.0)
tslib: 2.8.1
use-callback-ref: 1.3.3(@types/react@19.0.9)(react@19.0.0)
use-sidecar: 1.1.3(@types/react@19.0.9)(react@19.0.0)
use-callback-ref: 1.3.3(@types/react@19.0.10)(react@19.0.0)
use-sidecar: 1.1.3(@types/react@19.0.10)(react@19.0.0)
dev: false
/react-style-singleton@2.2.3(@types/react@19.0.9)(react@19.0.0):
/react-style-singleton@2.2.3(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
peerDependencies:
@ -1805,13 +1865,13 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
get-nonce: 1.0.1
react: 19.0.0
tslib: 2.8.1
dev: false
/react-textarea-autosize@8.5.6(@types/react@19.0.9)(react@19.0.0):
/react-textarea-autosize@8.5.6(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-aT3ioKXMa8f6zHYGebhbdMD2L00tKeRX1zuVuDx9YQK/JLLRSaSxq3ugECEmUB9z2kvk6bFSIoRHLkkUv0RJiw==}
engines: {node: '>=10'}
peerDependencies:
@ -1819,8 +1879,8 @@ packages:
dependencies:
'@babel/runtime': 7.26.9
react: 19.0.0
use-composed-ref: 1.4.0(@types/react@19.0.9)(react@19.0.0)
use-latest: 1.3.0(@types/react@19.0.9)(react@19.0.0)
use-composed-ref: 1.4.0(@types/react@19.0.10)(react@19.0.0)
use-latest: 1.3.0(@types/react@19.0.10)(react@19.0.0)
transitivePeerDependencies:
- '@types/react'
dev: false
@ -1851,24 +1911,12 @@ packages:
reselect: 4.1.8
dev: false
/redux-thunk@3.1.0(redux@5.0.1):
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
peerDependencies:
redux: ^5.0.0
dependencies:
redux: 5.0.1
dev: false
/redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
dependencies:
'@babel/runtime': 7.26.9
dev: false
/redux@5.0.1:
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
dev: false
/reflect-metadata@0.1.13:
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
dev: true
@ -1885,10 +1933,6 @@ packages:
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
dev: false
/reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
dev: false
/restore-cursor@3.1.0:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
@ -1928,7 +1972,6 @@ packages:
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: true
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@ -1944,7 +1987,6 @@ packages:
hasBin: true
requiresBuild: true
dev: false
optional: true
/set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
@ -2154,8 +2196,8 @@ packages:
engines: {node: '>=10'}
dev: true
/type-fest@4.34.1:
resolution: {integrity: sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==}
/type-fest@4.35.0:
resolution: {integrity: sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==}
engines: {node: '>=16'}
dev: false
@ -2181,7 +2223,7 @@ packages:
engines: {node: '>= 10.0.0'}
dev: true
/use-callback-ref@1.3.3(@types/react@19.0.9)(react@19.0.0):
/use-callback-ref@1.3.3(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
peerDependencies:
@ -2191,12 +2233,12 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
react: 19.0.0
tslib: 2.8.1
dev: false
/use-composed-ref@1.4.0(@types/react@19.0.9)(react@19.0.0):
/use-composed-ref@1.4.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==}
peerDependencies:
'@types/react': '*'
@ -2205,11 +2247,11 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
react: 19.0.0
dev: false
/use-isomorphic-layout-effect@1.2.0(@types/react@19.0.9)(react@19.0.0):
/use-isomorphic-layout-effect@1.2.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==}
peerDependencies:
'@types/react': '*'
@ -2218,11 +2260,11 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
react: 19.0.0
dev: false
/use-latest@1.3.0(@types/react@19.0.9)(react@19.0.0):
/use-latest@1.3.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==}
peerDependencies:
'@types/react': '*'
@ -2231,12 +2273,12 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
react: 19.0.0
use-isomorphic-layout-effect: 1.2.0(@types/react@19.0.9)(react@19.0.0)
use-isomorphic-layout-effect: 1.2.0(@types/react@19.0.10)(react@19.0.0)
dev: false
/use-sidecar@1.1.3(@types/react@19.0.9)(react@19.0.0):
/use-sidecar@1.1.3(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
peerDependencies:
@ -2246,7 +2288,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 19.0.9
'@types/react': 19.0.10
detect-node-es: 1.1.0
react: 19.0.0
tslib: 2.8.1

2
proto

@ -1 +1 @@
Subproject commit 11644adc889ee8f0e0e90c11ccf386a081ee000f
Subproject commit b7ea2e6cc71f7393f9afe722204c5c33b6a6f2b6

View file

@ -1,17 +0,0 @@
"use client";
import {AxiosRequestConfig, AxiosResponse} from "axios";
import {userApi} from "../../../configutation";
const Login = async (username: string, password: string): Promise<AxiosResponse<void>> => {
const options: AxiosRequestConfig = {
withCredentials: true
}
return await userApi.userServiceLogin({
username: username,
password: password
}, options);
}
export {Login};

View file

@ -1 +0,0 @@
export {LoginPage} from "./ui";

View file

@ -3,4 +3,4 @@ export const metadata = {
description: '',
};
export {LoginPage as default} from "@/_pages/login"
export {LoginPage as default} from "@/plain-pages/login"

View file

@ -1,35 +1 @@
import React from 'react';
import {AppShell, AppShellHeader, AppShellMain, Button, Group, Select, Stack, TextInput} from "@mantine/core";
import {Header} from "@/components/header";
type PageProps = {
params: {
user_id: number
}
}
const roles = [
"Participant", "Moderator", "Admin"
]
const Page = ({params}: PageProps) => {
return (
<AppShell header={{height: 70}}>
<AppShellHeader>
<Header/>
</AppShellHeader>
<AppShellMain px="16">
<Stack align="center">
<Group align="end" w="fit-content" m="auto" pt="16" gap="16">
<TextInput label="Никнейм" placeholder="Никнейм" defaultValue="user228"/>
<Select data={roles} label="Роль" defaultValue={roles[0]} allowDeselect={false}/>
<Button label="Пароль">Сменить пароль</Button>
</Group>
<Button disabled w="fit-content">Сохранить</Button>
</Stack>
</AppShellMain>
</AppShell>
);
};
export default Page;
export {UserPage as default, generateMetadata} from "@/plain-pages/user";

View file

@ -1,99 +1 @@
import React from 'react';
import {
ActionIcon,
AppShell,
AppShellAside,
AppShellHeader,
AppShellMain,
Pagination,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
TextInput,
Title
} from "@mantine/core";
import {Header} from "@/components/header";
import Link from "next/link";
import {IconPencil} from "@tabler/icons-react";
export const metadata = {
title: 'Пользователи',
description: '',
};
const users = [
{
id: 1,
username: "user228",
email: "use***@gmail.com",
role: "Admin",
},
{
id: 2,
username: "user229",
email: "use***@mail.ru",
role: "Participant",
},
{
id: 3,
username: "user365",
email: "use***@yandex.ru",
role: "Moderator",
},
{
id: 4,
username: "user777",
email: "use***@yahoo.com",
role: "Moderator",
},
]
const Page = () => {
const rows = users.map((user) => (
<TableTr key={user.id}>
<TableTd>{user.username}</TableTd>
<TableTd>{user.email}</TableTd>
<TableTd>{user.role}</TableTd>
<TableTd>{<ActionIcon size="xs" component={Link} href={`/users/${user.id}`}>
<IconPencil/>
</ActionIcon>}
</TableTd>
</TableTr>
));
return (
<AppShell header={{height: 70}}>
<AppShellHeader>
<Header/>
</AppShellHeader>
<AppShellMain px="16">
<Stack align="center" w="fit-content" m="auto" pt="16" gap="16">
<Title>Пользователи</Title>
<Table horizontalSpacing="xl">
<TableThead>
<TableTr>
<TableTh>Никнейм</TableTh>
<TableTh>Почта</TableTh>
<TableTh>Роль</TableTh>
<TableTh></TableTh>
</TableTr>
</TableThead>
<TableTbody>{rows}</TableTbody>
</Table>
<Pagination total={10}/>
</Stack>
</AppShellMain>
<AppShellAside withBorder={false} px="16">
<Stack pt="16">
<TextInput placeholder="Поиск"/>
</Stack>
</AppShellAside>
</AppShell>
);
};
export default Page;
export {UsersPage as default, generateMetadata} from "@/plain-pages/users";

View file

@ -1,24 +0,0 @@
"use client";
import {AxiosRequestConfig, AxiosResponse} from "axios";
import {userApi} from "../../../configutation";
const GetMe = async () => {
const options: AxiosRequestConfig = {
withCredentials: true,
timeout: 50,
}
return await userApi.userServiceGetUser(0, true, options);
}
const Logout = async (): Promise<AxiosResponse<void>> => {
const options: AxiosRequestConfig = {
withCredentials: true
}
return await userApi.userServiceLogout(options);
}
export {GetMe, Logout};

View file

@ -21,8 +21,8 @@ import NextImage from "next/image"
import {IconUser} from "@tabler/icons-react";
import {useDisclosure} from "@mantine/hooks";
import {useMutation, useQuery} from "@tanstack/react-query";
import {GetMe, Logout} from "./api";
import {useRouter} from "next/navigation";
import {GetMe, Logout} from "@/shared/api";
const Profile = () => {
const router = useRouter();
@ -31,6 +31,7 @@ const Profile = () => {
queryKey: ['me'],
queryFn: GetMe,
retry: false,
refetchOnWindowFocus: false,
});
const mutation = useMutation({

View file

@ -0,0 +1 @@
export {Page as LoginPage} from "./page";

View file

@ -12,27 +12,15 @@ import {
Title
} from "@mantine/core";
import {Header} from "@/components/header";
import NextImage from "next/image";
import {Login} from "./api";
import {useForm} from '@mantine/form';
import React from "react";
import {useRouter} from 'next/navigation';
import {useForm} from "@mantine/form";
import {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import axios from "axios";
import NextImage from "next/image";
import {Login} from "@/shared/api";
type Props = {
params: {
redirectTo: string
}
}
type Credentials = {
username: string,
password: string
}
const Page = (props: Props) => {
const Page = () => {
const router = useRouter();
const form = useForm({
@ -43,15 +31,13 @@ const Page = (props: Props) => {
});
const mutation = useMutation({
mutationFn: async (credentials: Credentials) => {
await Login(credentials.username, credentials.password)
},
mutationFn: Login,
onSuccess: async () => {
// FIXME: Possible vulnerability. Should check if the link is from the same resource.
await router.push(props.params.redirectTo || "/")
await router.push("/")
},
onError: (error) => {
form.clearErrors();
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
form.setFieldError("username", "Неверный юзернейм или пароль")
@ -61,6 +47,7 @@ const Page = (props: Props) => {
form.setFieldError("username", "Что-то пошло не так. Попробуйте позже.")
},
retry: false
});
const onSubmit = (event) => {
@ -82,7 +69,7 @@ const Page = (props: Props) => {
justify="center"
w="fit-content"
m="auto"
mt="10%"
mt="5%"
p="md"
style={{color: "var(--mantine-color-bright)"}}
>
@ -120,4 +107,4 @@ const Page = (props: Props) => {
);
};
export {Page as LoginPage};
export {Page};

View file

@ -0,0 +1 @@
export {UserPage, generateMetadata} from "./page";

View file

@ -0,0 +1,50 @@
"use server";
import {Metadata} from "next";
import {ClientPage} from "@/plain-pages/user/ui";
import {GetUser, ParseJWT} from "@/shared/api";
import {notFound} from "next/navigation";
type Props = {
params: Promise<{ user_id: number }>
}
const generateMetadata = async ({params}: Props): Promise<Metadata> => {
const user_id = (await params).user_id;
try {
const response = await GetUser(user_id);
return {
title: `Профиль пользователя ${response.user.username}`,
}
} catch (e) {
notFound();
}
}
const ServerPage = async ({params}: Props) => {
const user_id = (await params).user_id;
try {
const response = await GetUser(user_id);
const jwt = await ParseJWT();
let canEdit = false;
if (jwt.user_id === response.user.id && jwt.hasPermission({action: "update", resource: "me-user"})) {
canEdit = true;
}
if (jwt.user_id !== response.user.id && jwt.hasPermission({action: "read", resource: "another-user"})) {
canEdit = true;
}
return (
<ClientPage user={response.user} canEdit={canEdit}/>
)
} catch (e) {
notFound();
}
}
export {ServerPage as UserPage, generateMetadata};

View file

@ -0,0 +1,66 @@
"use client";
import React, {useState} from 'react';
import {AppShell, AppShellHeader, AppShellMain, Button, Group, Select, Stack, TextInput, Title} from "@mantine/core";
import {Header} from "@/components/header";
import {User} from "../../../proto/user/v1/api";
import {UpdateUser} from "@/shared/api";
const roles = [
"Студент",
"Преподаватель",
"Администратор",
];
type Props = {
user: User
canEdit: boolean
}
const ClientPage = (props: Props) => {
const [user, setUser] = useState(props.user);
return (
<AppShell header={{height: 70}}>
<AppShellHeader>
<Header/>
</AppShellHeader>
<AppShellMain px="sm">
<Stack align="center" mt="sm">
<Title order={2}>
Профиль пользователя {props.user.username}
</Title>
<Group align="end" w="fit-content" m="auto" pt="sm" gap="sm">
<TextInput
label="Никнейм"
placeholder="Никнейм"
defaultValue={user.username}
onChange={(event) => setUser({...user, username: event.target.value})}
disabled={!props.canEdit}
/>
<Select
data={roles}
label="Роль"
defaultValue={roles[user.role]}
allowDeselect={false}
onChange={(event) => setUser({...user, role: roles.indexOf(event)})}
disabled={!props.canEdit}
/>
</Group>
<Button
disabled={JSON.stringify(user) === JSON.stringify(props.user)}
w="fit-content"
onClick={() => UpdateUser(user.id!, user.role, user.username)}
>
Сохранить
</Button>
</Stack>
</AppShellMain>
</AppShell>
);
};
export {ClientPage};

View file

@ -0,0 +1 @@
export * from "./page";

View file

@ -0,0 +1,29 @@
"use server";
import {GetUsers} from "@/shared/api";
import {ClientPage} from "./ui";
import {Metadata} from "next";
const generateMetadata = async ({params}: Props): Promise<Metadata> => {
return {
title: 'Пользователи',
description: '',
};
}
type Props = {
searchParams: Promise<{ page: number }>
}
const Page = async (props: Props) => {
const page = (await props.searchParams).page || 1;
const users = await GetUsers(page, 10);
return (
<ClientPage users={users.users} max_page={users.max_page} page={users.page}/>
)
}
export {Page as UsersPage, generateMetadata};

View file

@ -0,0 +1,108 @@
"use client";
import {
ActionIcon,
AppShell,
AppShellAside,
AppShellHeader,
AppShellMain,
Pagination,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
TextInput,
Title
} from "@mantine/core";
import Link from "next/link";
import {IconPencil} from "@tabler/icons-react";
import {Header} from "@/components/header";
import React from "react";
import {User} from "../../../proto/user/v1/api";
type Props = {
users: User[],
page: number
max_page: number
};
const roles = [
"Студент",
"Преподаватель",
"Администратор",
];
const ClientPage = (props: Props) => {
const rows = props.users.map((user) => (
<TableTr key={user.id}>
<TableTd>{user.username}</TableTd>
{/*<TableTd>{user.email}</TableTd>*/}
<TableTd>{roles[user.role]}</TableTd>
<TableTd>{<ActionIcon size="xs" component={Link} href={`/users/${user.id}`}>
<IconPencil/>
</ActionIcon>}
</TableTd>
</TableTr>
));
return (
<AppShell header={{height: 70}}>
<AppShellHeader>
<Header/>
</AppShellHeader>
<AppShellMain px="16">
<Stack align="center" w="fit-content" m="auto" pt="16" gap="16">
<Title>Пользователи</Title>
<Table horizontalSpacing="xl">
<TableThead>
<TableTr>
<TableTh>Никнейм</TableTh>
{/*<TableTh>Почта</TableTh>*/}
<TableTh>Роль</TableTh>
<TableTh></TableTh>
</TableTr>
</TableThead>
<TableTbody>{rows}</TableTbody>
</Table>
<Pagination total={props.max_page}
value={props.page}
getItemProps={(page) => ({
component: Link,
href: `/users?page=${page}`,
})}
getControlProps={(control) => {
if (control === 'next') {
if (props.page === props.max_page) {
return {component: Link, href: `/users?page=${props.page}`};
}
return {component: Link, href: `/users?page=${+props.page + 1}`};
}
if (control === 'previous') {
if (props.page === 1) {
return {component: Link, href: `/users?page=${props.page}`};
}
return {component: Link, href: `/users?page=${+props.page - 1}`};
}
return {};
}}
/>
</Stack>
</AppShellMain>
<AppShellAside withBorder={false} px="16">
<Stack pt="16">
<TextInput placeholder="Поиск"/>
</Stack>
</AppShellAside>
</AppShell>
);
};
export {ClientPage};

9
src/shared/api/config.ts Normal file
View file

@ -0,0 +1,9 @@
import {Configuration, DefaultApi} from "../../../proto/user/v1/api";
const configuration = new Configuration({
basePath: "http://localhost:60005",
});
const authApi = new DefaultApi(configuration);
export {authApi};

1
src/shared/api/index.ts Normal file
View file

@ -0,0 +1 @@
export * from "./ms-auth";

197
src/shared/api/ms-auth.ts Normal file
View file

@ -0,0 +1,197 @@
"use server";
import {AxiosRequestConfig} from "axios";
import {decode} from "jsonwebtoken";
import {cookies} from "next/headers";
import {authApi} from "./config";
type Credentials = {
username: string,
password: string
}
const CookieName: any = "SESSIONID";
type Grant = {
action: "create" | "read" | "update" | "delete";
resource: "another-user" | "me-user" | "list-user" | "own-session";
}
type JWT = {
session_id: string
user_id: number
role: number
exp: number
iat: number
nbf: number
permissions: Grant[]
}
class JWTWithPermissions {
public session_id: string
public user_id: number
public role: number
public exp: number
public iat: number
public nbf: number
public permissions: Grant[]
constructor(jwt: JWT) {
this.session_id = jwt.session_id;
this.user_id = jwt.user_id;
this.role = jwt.role;
this.exp = jwt.exp;
this.iat = jwt.iat;
this.nbf = jwt.nbf;
this.permissions = jwt.permissions;
}
public hasPermission(grant: Grant): boolean {
return this.permissions.some((permission) => permission.action === grant.action && permission.resource === grant.resource);
}
}
export const ParseJWT = async () => {
const cookieStore = await cookies();
const session = cookieStore.get(CookieName);
if (session === undefined) {
throw new Error("Session id not found");
}
const token = session.value;
return new JWTWithPermissions(decode(token) as JWT);
}
export const Login = async (credentials: Credentials) => {
const options: AxiosRequestConfig = {
headers: {
"Authorization": "Basic " + btoa(credentials.username + ":" + credentials.password)
}
}
const response = await authApi.login(options)
const token = response.headers["authorization"].split(" ")[1];
const decoded = decode(token) as JWT;
const cookieArgs: any = {
httpOnly: true,
expires: new Date(decoded["exp"] * 1000),
sameSite: 'strict',
};
const cookieStore = await cookies();
cookieStore.set(CookieName, token, cookieArgs);
return response.data;
};
export const GetUser = async (id: number) => {
const cookieStore = await cookies();
const session = cookieStore.get(CookieName);
if (session === undefined) {
throw new Error("Session id not found");
}
const options: AxiosRequestConfig = {
headers: {
'Authorization': "Bearer " + session.value
}
};
const response = await authApi.getUser(id, options);
return response.data;
};
export const UpdateUser = async (id: number, role?: any, username?: string) => {
const cookieStore = await cookies();
const session = cookieStore.get(CookieName);
if (session === undefined) {
throw new Error("Session id not found");
}
const options: AxiosRequestConfig = {
headers: {
'Authorization': "Bearer " + session.value
}
};
const req = {
role: role,
username: username
}
const response = await authApi.updateUser(id, req, options);
return response.data;
};
export const Logout = async () => {
const cookieStore = await cookies();
const session = cookieStore.get(CookieName);
if (session === undefined) {
throw new Error("Session id not found");
}
const options: AxiosRequestConfig = {
headers: {
'Authorization': "Bearer " + session.value
}
};
const response = await authApi.logout(options)
return response.data;
};
export const GetMe = async () => {
const cookieStore = await cookies();
const session = cookieStore.get(CookieName);
if (session === undefined) {
throw new Error("Session id not found");
}
const options: AxiosRequestConfig = {
headers: {
'Authorization': "Bearer " + session.value
}
};
const response = await authApi.getMe(options);
return response.data;
};
export const GetUsers = async (page: number, pageSize: number) => {
const cookieStore = await cookies();
const session = cookieStore.get(CookieName);
if (session === undefined) {
throw new Error("Session id not found");
}
const options: AxiosRequestConfig = {
headers: {
'Authorization': "Bearer " + session.value
}
};
const response = await authApi.listUsers(page, pageSize, options);
return response.data;
}