diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5171c54 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..c183c10 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,39 @@ +module.exports = { + "env": { + "browser": true, + "node": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "never" + ] + } +} diff --git a/README.md b/README.md index b8ec3e8..1b4000f 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ -# 2work.shop \ No newline at end of file +
+ +# 2workshop +Freelance platform, where every client may find talents which realize their dreams. Platform use crypto-payments through smart-contract. Thats meaning only clear and straight interaction, based on trust in blockchain technology, without third parties. + +[2workshop.site here! Click!](http://2workshop.site/) + +| Part | Tech | +| ------------- |:-------------: | +| Server | ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) | +| Client | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white) | +| Database | ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) + knex | +| Smart-contract | ![Solidity](https://img.shields.io/badge/Solidity-%23363636.svg?style=for-the-badge&logo=solidity&logoColor=white) ![Ethereum](https://img.shields.io/badge/Ethereum-3C3C3D?style=for-the-badge&logo=Ethereum&logoColor=white) | +| Security and features | ![Socket.io](https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![JWT](https://img.shields.io/badge/JWT-black?style=for-the-badge&logo=JSON%20web%20tokens) + bcrypt | + +
diff --git a/app.js b/app.js index 5a1edfa..c0d76ef 100644 --- a/app.js +++ b/app.js @@ -1,11 +1,7 @@ -const express = require('express') -const config = require('config') -const body_parser = require('body-parser') -const {Client} = require('pg') -const knex = require('./knex/knex') -const cors = require('cors') -//const http = require('http') -//const { Server } = require('.io') +const express = require("express") +const config = require("config") +const body_parser = require("body-parser") +const cors = require("cors") const app = express() @@ -13,53 +9,41 @@ app.use(cors()) app.use(express.json()) app.use(body_parser.json()) app.use(body_parser.urlencoded({ extended: true })) -app.use('/auth', require('./routes/auth')) -app.use('/api', require('./routes/api')) +app.use("/auth", require("./routes/auth")) +app.use("/api", require("./routes/api")) -const PORT = config.get('port') || 8000 +const PORT = config.get("port") || 8000 const http = require("http").createServer(app) const socket = require("socket.io")(http, { - cors: { - origin: `*`, - methods: ["GET", "POST"]} - }) - -/*const io = new Server(app, { - cors: { - origin: `http://localhost:${PORT}`, - methods: ["GET", "POST"] - } -})*/ - + cors: { + origin: "*", + methods: ["GET", "POST"]} +}) async function start() { - try { + try { - http.listen(PORT, () => console.log(`App has been started at port ${PORT}...`)) - socket.on("connection", (socket) => { - console.log("Socket connected, ID = ", socket.id) + http.listen(PORT, () => console.log(`App has been started at port ${PORT}...`)) + socket.on("connection", (socket) => { - socket.on("joinChat", (data) => { - socket.join(data) - console.log(`User ID ${socket.id} joined to chat ID ${data}`) - }) - - socket.on("sendMessage", (data) => { - console.log("data ", data) - socket.to(data.chatId).emit("receiveMessage", data) - }) - - socket.on("disconnect", () => { - console.log("User disconnected, ID = ", socket.id) - - }) - }) - } catch (e) { - console.log('Server error', e.message) - process.exit(1) - } + socket.on("joinChat", (data) => { + socket.join(data) + }) + + socket.on("sendMessage", (data) => { + socket.to(data.chatId).emit("receiveMessage", data) + }) + + socket.on("disconnect", () => { + + }) + }) + } catch (e) { + console.log("Server error", e.message) + process.exit(1) + } } start() diff --git a/controllers/authController.js b/controllers/authController.js new file mode 100644 index 0000000..4f7a1cb --- /dev/null +++ b/controllers/authController.js @@ -0,0 +1,78 @@ +const jwt = require("jsonwebtoken") +const config = require("config") +const {validationResult} = require("express-validator") +const bcrypt = require("bcrypt") +const knex = require("../knex/knex") + +exports.register = async (req, res) => { + try { + console.log("auth") + const errors = validationResult(req) + if (!errors.isEmpty()){ + return res.status(400).json({ + errors: errors.array(), + message: "Register error", + test: req.body + }) + } + const {email, nickname, password} = req.body + console.log(req.body) + const existUser = await knex("Users").where("email", email) + console.log(existUser) + console.log("jjj", existUser.length) + if (existUser.length !== 0) { + return res.status(400).json({message: "User is already exists"}) + } + const hashedPassword = await bcrypt.hash(password, 12) + await knex("Users").insert({ email: email, nickname: nickname, password: hashedPassword }).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "User has been created"}) + + } catch (e) { + res.status(500).json({ + message: "Server error", + error: e.message + }) + } + +} + +exports.login = async (req, res) => { + try { + console.log("post on login") + const errors = validationResult(req) + if (!errors.isEmpty()){ + return res.status(400).json({ + errors: errors.array(), + message: "Login error" + }) + } + + const {email, password} = req.body + + const user = await knex("Users").where("email", email).first() + if (user == undefined || user == null) { + return res.status(400).json({ + message: "User doesnt exist" + }) + } + + const isMatch = await bcrypt.compare(password, user.password) + + if (!isMatch) { + return res.status(400).json({message: "Error. Check input credentials"}) + } + + const token = jwt.sign( + {userId: user.id, roleId: user.roleId}, + config.get("jwtSecret"), + {expiresIn: "24h"} + ) + res.json({token: token, userId: user.id, nickname: user.nickname, roleId: user.roleId}) + + } catch (e) { + res.status(500).json({ + message: "Server error", + error: e.message + }) + } +} \ No newline at end of file diff --git a/controllers/commentsController.js b/controllers/commentsController.js new file mode 100644 index 0000000..f8b7dc7 --- /dev/null +++ b/controllers/commentsController.js @@ -0,0 +1,29 @@ +const knex = require("../knex/knex") + +exports.getCommentsByOrderId = async (req, res) => { + try { + knex.raw(`SELECT * FROM "Comments" WHERE "orderId"=${req.params.order_id} `).then((comments) =>{ + res.send(comments.rows) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.addCommentByOrderId = async (req, res) => { + try { + const {message} = req.body + const comment_dct = { orderId: req.params.order_id, userId: req.user.userId, message: message} + console.log(comment_dct) + await knex("Comments").insert(comment_dct).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Comment has been created"}) + + } catch (e) { + res.status(500).json({ + message: "Server error {api:post:comments}", + error: e.message + }) + } +} \ No newline at end of file diff --git a/controllers/messagesController.js b/controllers/messagesController.js new file mode 100644 index 0000000..55095f7 --- /dev/null +++ b/controllers/messagesController.js @@ -0,0 +1,28 @@ +const knex = require("../knex/knex") + +exports.getMessageByChatId = async (req, res) => { + try { + let query = knex("ChatMessages").select("*").where("chatId", req.params.id) + query.then(response => { + res.send(response) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.addMessageByChatId = async (req, res) => { + try { + const {chatId, userId, message, timestamp} = req.body + const message_dct = { chatId: chatId, userId: userId, message: message, timestamp: timestamp} + await knex("ChatMessages").insert(message_dct).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "ChatMessage has been created"}) + } catch (e) { + res.status(500).json({ + message: "Server error {api:post:comments}", + error: e.message + }) + } +} \ No newline at end of file diff --git a/controllers/ordersController.js b/controllers/ordersController.js new file mode 100644 index 0000000..3cf9556 --- /dev/null +++ b/controllers/ordersController.js @@ -0,0 +1,170 @@ +const knex = require("../knex/knex") + +exports.getOrdersAll = async (req, res) => { + try { + knex.raw("select * from \"Orders\"").then((orders) =>{ + res.send(orders.rows) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error {api:orders}", + error: e.message + }) + } +} + +exports.getOrdersById = async (req, res) => { + try { + let query = knex("Orders").select("*").where("id", req.params.id).first() + query.then(response => { + res.send(response) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.getOrdersByTagId = async (req, res) => { + try { + knex.raw(`SELECT * FROM "Orders" WHERE "tagId"=${req.params.id} `).then((orders) =>{ + res.send(orders.rows) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } + +} + +exports.getOrdersByAuthorId = async (req, res) => { + try { + knex.raw(`SELECT * FROM "Orders" WHERE "authorId"=${req.params.authorId} `).then((orders) =>{ + res.send(orders.rows) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.getOrdersByWorkerId = async (req, res) => { + try { + knex.raw(`SELECT * FROM "Orders" WHERE "workerId"=${req.params.workerId} `).then((orders) =>{ + res.send(orders.rows) + console.log("worker orders ", orders) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.getOrdersByAuthorName = async (req, res) => { + try { + let query = knex("Users").select("id").where("nickname", req.params.name) + let authorId = undefined + query.then(response => { + response.forEach(element => { + authorId = element["id"] + }) + + let second_query = knex("Orders").select("*").where("authorId", authorId) + second_query.then(response => { + res.send(response) + console.log(response) + }) + + }).catch(err => console.log("Transaction", err)) + + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.getOrdersByWorkerName = async (req, res) => { + try { + let query = knex("Users").select("id").where("nickname", req.params.name) + let workerId = undefined + query.then(response => { + response.forEach(element => { + workerId = element["id"] + }) + + let second_query = knex("Orders").select("*").where("workerId", workerId) + second_query.then(response => { + res.send(response) + console.log(response) + }) + + }).catch(err => console.log("Transaction", err)) + + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.addOrder = async (req, res) => { + try { + const {title, description, price} = req.body + const order_dct = { title: title, description: description, + authorId: req.user.userId, price: price} + console.log(order_dct) + await knex("Orders").insert(order_dct).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Order has been created"}) + + } catch (e) { + res.status(500).json({ + message: "Server error {api:create_order}", + error: e.message + }) + } +} + +exports.deleteOrderById = async (req, res) => { + try { + let orderId = req.params.id + const order = await knex("Orders").where("id", orderId).first().catch(err => console.log("Transaction", err)) + console.log("Author", order.authorId) + console.log("User", req.user.userId) + if (req.user.userId == order.authorId){ + console.log("Delete order", req.user.userId) + console.log("Author ", order.authorId) + await knex("Orders").del().where("id", orderId).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Order has been delete"}) + } else { + res.status(500).json({ + message: "DB error {api:delete:orders}" + }) + } + } catch (e) { + res.status(500).json({ + message: "Server error {api:delete:orders}", + error: e.message + }) + } +} + +exports.changeOrderById = async (req, res) => { + try { + const {workerId} = req.body + const order_dct = { workerId: workerId } + console.log(order_dct) + await knex("Orders").update(order_dct).where("id", req.params.id).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Order has been updated"}) + + } catch (e) { + res.status(500).json({ + message: "Server error {api:put:orders}", + error: e.message + }) + } +} diff --git a/controllers/paymentsController.js b/controllers/paymentsController.js new file mode 100644 index 0000000..b16e6ec --- /dev/null +++ b/controllers/paymentsController.js @@ -0,0 +1,29 @@ +const knex = require("../knex/knex") + +exports.getPaymentsByOrderId = async (req, res) => { + try { + knex.raw(`SELECT * FROM "Payments" WHERE "orderId"=${req.params.id} `).then((payments) =>{ + res.send(payments.rows) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.addPaymentByOrderId = async (req, res) => { + try { + const {txHash, value, status, comment} = req.body + const payment_dict = { orderId: req.params.order_id, userId: req.user.userId, txHash: txHash, value: value, status: status, comment: comment} + console.log(payment_dict) + await knex("Payments").insert(payment_dict).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Payment has been created"}) + + } catch (e) { + res.status(500).json({ + message: "Server error {api:post:comments}", + error: e.message + }) + } +} diff --git a/controllers/respondsController.js b/controllers/respondsController.js new file mode 100644 index 0000000..90c55c5 --- /dev/null +++ b/controllers/respondsController.js @@ -0,0 +1,47 @@ +const knex = require("../knex/knex") + +exports.getRespondsByOrderId = async (req, res) => { + try { + let query = knex("Responds").select("*").where("orderId", req.params.id) + query.then(response => { + res.json(response) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.addRespondsByOrderId = async (req, res) => { + try { + const respond_dct = { orderId: req.params.id, userId: req.user.userId} + await knex("Responds").insert(respond_dct).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Respond has been created"}) + } catch (e) { + res.status(500).json({ + message: "Server error {api:post:responds}", + error: e.message + }) + } +} + +exports.deleteRespondsByOrderId = async (req, res) => { + try { + console.log(req.user.roleId) + if (req.user.userId == req.body.userId || req.user.roleId == 2){ + console.log(req.params.id, req.body.userId) + await knex("Responds").del().where("orderId", req.params.id).where("userId", req.body.userId).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Respond has been deleted"}) + } else { + res.status(400).json({ + message: "Restricted" + }) + } + } catch (e) { + res.status(500).json({ + message: "Server error {api:post:responds}", + error: e.message + }) + } +} \ No newline at end of file diff --git a/controllers/statusesController.js b/controllers/statusesController.js new file mode 100644 index 0000000..a631923 --- /dev/null +++ b/controllers/statusesController.js @@ -0,0 +1,53 @@ +const knex = require("../knex/knex") + +exports.getStatusByOrderId = async (req, res) => { + try { + let query = knex("PaymentsStatus").select("*").where("orderId", req.params.id).first() + query.then(response => { + res.send(response) + }) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.addStatusByOrderId = async (req, res) => { + try { + await knex("PaymentsStatus").insert({orderId: req.params.id}).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Status has been created"}) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.changeWorkerStatusByOrderId = async (req, res) => { + try { + const order_dct = { workerStatus: "submit" } + await knex("PaymentsStatus").update(order_dct).where("orderId", req.params.id).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Status has been updated"}) + + } catch (e) { + res.status(500).json({ + message: "Server error {api:put:orders}", + error: e.message + }) + } +} + +exports.changeAuthorStatusByOrderId = async (req, res) => { + try { + const order_dct = { authorStatus: "submit" } + await knex("PaymentsStatus").update(order_dct).where("orderId", req.params.id).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Status has been updated"}) + + } catch (e) { + res.status(500).json({ + message: "Server error {api:put:orders}", + error: e.message + }) + } +} diff --git a/controllers/tagsControllers.js b/controllers/tagsControllers.js new file mode 100644 index 0000000..b350966 --- /dev/null +++ b/controllers/tagsControllers.js @@ -0,0 +1,29 @@ +const knex = require("../knex/knex") + +exports.getTagsAll = async (req, res) => { + try { + knex.raw("SELECT * FROM \"Tags\"").then((tags) =>{ + res.send(tags.rows) + }).catch(err => console.log("Transaction", err)) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.addTag = async (req, res) => { + try { + const {tag} = req.body + const tag_dct = { tag: tag} + console.log(tag_dct) + await knex("Tags").insert(tag_dct).catch(err => console.log("Transaction", err)) + res.status(201).json({message: "Tag has been created"}) + + } catch (e) { + res.status(500).json({ + message: "Server error {api:post:comments}", + error: e.message + }) + } +} \ No newline at end of file diff --git a/controllers/usersController.js b/controllers/usersController.js new file mode 100644 index 0000000..0373a4c --- /dev/null +++ b/controllers/usersController.js @@ -0,0 +1,27 @@ +const knex = require("../knex/knex") + +exports.getNicknameByUserId = async (req, res) => { + try { + let query = knex("Users").select("nickname").where("id", req.params.id).first() + query.then(response => { + res.send(response) + }) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} + +exports.getNicknameByUserAuth = async (req, res) => { + try { + let query = knex("Users").select("nickname").where("id", req.user.userId).first() + query.then(response => { + res.send(response) + }) + } catch (e) { + res.status(500).json({ + message: "Server error" + }) + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1b99e7e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' +services: + postgres: + image: 'postgres:latest' + environment: + - POSTGRES_PASSWORD=root + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + ports: + - 5433:5433 + server: + image: '2work:server' + ports: + - "8000:3000" + depends_on: + - postgres \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..c70ce4a --- /dev/null +++ b/dockerfile @@ -0,0 +1,8 @@ +FROM node:18-alpine +WORKDIR /app +COPY ./package.json ./ +RUN npm install bcrypt@5.0.0 +RUN npm i +COPY . . +EXPOSE 3000 +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..5171c54 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/frontend/dockerfile b/frontend/dockerfile new file mode 100644 index 0000000..8045d8f --- /dev/null +++ b/frontend/dockerfile @@ -0,0 +1,7 @@ +FROM node:18-alpine +WORKDIR /app +COPY ./package.json ./ +RUN npm i +COPY . . +EXPOSE 3000 +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bbf5ff0..ef62003 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", "ethers": "^5.6.9", + "http-proxy-middleware": "^2.0.6", "mobx": "^6.6.1", "mobx-react-lite": "^3.4.0", "react": "^18.2.0", @@ -8202,7 +8203,8 @@ }, "node_modules/http-proxy-middleware": { "version": "2.0.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -19792,6 +19794,8 @@ }, "http-proxy-middleware": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", "requires": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", diff --git a/frontend/package.json b/frontend/package.json index e3f0f06..bcb3614 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,20 +7,21 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", + "ethers": "^5.6.9", + "http-proxy-middleware": "^2.0.6", "mobx": "^6.6.1", "mobx-react-lite": "^3.4.0", - "ethers": "^5.6.9", "react": "^18.2.0", "react-dom": "^18.2.0", "react-preloaders": "^3.0.3", "react-responsive": "^9.0.0-beta.10", "react-router-dom": "^6.3.0", - "react-scripts": "5.0.1", + "react-scripts": "4.0.3", "socket.io-client": "^4.5.1", "web-vitals": "^2.1.4" }, "scripts": { - "start": "react-scripts start", + "start": "PORT=80 DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/frontend/src/App.js b/frontend/src/App.js index 9dae426..179e4d7 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -16,6 +16,8 @@ import AuthPage from "./pages/AuthPage/AuthPage"; import CreateOrder from "./pages/CreateOrder/CreateOrder"; import OrderPage from "./pages/OrderPage/OrderPage"; import MyOrdersPage from "./pages/MyOrdersPage/MyOrdersPage"; +import authUser from "./store/authUser"; +import AdminPage from "./pages/AdminPage/AdminPage"; @@ -54,16 +56,13 @@ const App = () => {
- - - - }/> + }/> } exact/> } exact/> } exact/> } exact/> } exact/> + {authUser.roleId == 2 ? } exact/> : null}