diff --git a/.env.example b/.env.example index 78d81f9..6ea1f71 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,10 @@ HTTP_HOST=localhost HTTP_PORT=8080 +# Auth service +AUTH_SERVICE_HOST=localhost +AUTH_SERVICE_PORT=8081 + # Database Options DB_HOST=127.0.0.1 DB_PORT=27017 diff --git a/.eslintrc b/.eslintrc index c31ebe5..e026c68 100644 --- a/.eslintrc +++ b/.eslintrc @@ -130,7 +130,7 @@ "error", { "props": true, - "ignorePropertyModificationsFor": ["accamulator"] + "ignorePropertyModificationsFor": ["accamulator", "request"] } ], "object-shorthand": "off", diff --git a/README.md b/README.md index 1a23db1..3b4541a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,13 @@ DB_PASSWORD - mongo password DB_NAME - database name ``` +**Для подключения к сервису авторизации** + +``` +AUTH_SERVICE_HOST - auth service host +AUTH_SERVICE_PORT - auth service port +``` + ## Среды окружения ``` diff --git a/package.json b/package.json index 8aafd80..781dba2 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@fastify/cookie": "^8.3.0", "@fastify/cors": "^8.2.0", "@fastify/jwt": "^6.3.3", + "axios": "^1.2.1", "bcryptjs": "^2.4.3", "dotenv": "^16.0.3", "fastify": "^4.9.2", diff --git a/src/clients/auth/index.ts b/src/clients/auth/index.ts new file mode 100644 index 0000000..6db7939 --- /dev/null +++ b/src/clients/auth/index.ts @@ -0,0 +1,16 @@ +import { authService } from "./instance"; + +import type { User } from "@/types/models/user.type"; +import type { GetUserRequest } from "./types"; + +export const getUser = async (request: GetUserRequest): Promise => { + try { + const { data } = await authService.get(`/user/${request.id}`); + + return data; + } catch (nativeError) { + console.error(nativeError); + } + + return null; +}; diff --git a/src/clients/auth/instance.ts b/src/clients/auth/instance.ts new file mode 100644 index 0000000..3a3009f --- /dev/null +++ b/src/clients/auth/instance.ts @@ -0,0 +1,7 @@ +import axios from "axios"; + +import { CONFIGURATION } from "@/constants/configuration"; + +export const authService = axios.create({ + baseURL: `${CONFIGURATION.authService.host}:${CONFIGURATION.authService.port}`, +}); diff --git a/src/clients/auth/types.ts b/src/clients/auth/types.ts new file mode 100644 index 0000000..7dad5f5 --- /dev/null +++ b/src/clients/auth/types.ts @@ -0,0 +1,3 @@ +export type GetUserRequest = { + id: string; +}; diff --git a/src/constants/configuration.ts b/src/constants/configuration.ts index 274c665..924ef8d 100644 --- a/src/constants/configuration.ts +++ b/src/constants/configuration.ts @@ -11,10 +11,11 @@ export const CONFIGURATION = { database: process.env.DB_NAME || "database", }, service: { - privateAccessSecretKey: process.env.PRIVATE_ACCESS_SECRET_KEY || "", publicAccessSecretKey: process.env.PUBLIC_ACCESS_SECRET_KEY || "", - privateRefreshSecretKey: process.env.PRIVATE_REFRESH_SECRET_KEY || "", - publicRefreshSecretKey: process.env.PUBLIC_REFRESH_SECRET_KEY || "", salt: Number(process.env.SALT) || 10, }, + authService: { + host: process.env.AUTH_SERVICE_HOST || "", + port: Number(process.env.AUTH_SERVICE_PORT) || 8081, + }, } as const; diff --git a/src/handlers/account/index.ts b/src/handlers/account/index.ts new file mode 100644 index 0000000..719fc09 --- /dev/null +++ b/src/handlers/account/index.ts @@ -0,0 +1,52 @@ +import { Types } from "mongoose"; + +import { AccountModel } from "@/models/account.model"; + +import { getUser } from "@/clients/auth"; +import { validateEmptyFields } from "@/utils/validate-empty-fields"; + +import type { FastifyReply } from "fastify"; +import type { CreateAccountRequest, GetAccountRequest } from "./types"; + +export const createAccount = async (request: CreateAccountRequest, reply: FastifyReply) => { + if (!Types.ObjectId.isValid(request.user.id)) { + reply.status(400); + return new Error("invalid user id"); + } + + const account = await AccountModel.findById(request.user.id).lean(); + + if (account) { + reply.status(409); + return new Error("account already exist"); + } + + const user = await getUser({ id: request.user.id }); + + if (!user) { + reply.status(404); + return new Error("user not found"); + } + + const createdAccount = new AccountModel({ + userId: user._id, + }); + + return createdAccount.save(); +}; + +export const getAccount = async (request: GetAccountRequest, reply: FastifyReply) => { + const [getAccountRequestBody, error] = validateEmptyFields(request.params || {}, ["userId"]); + + if (!getAccountRequestBody || error) { + reply.status(400); + return error; + } + + if (!Types.ObjectId.isValid(getAccountRequestBody.userId)) { + reply.status(400); + return new Error("invalid user id"); + } + + return AccountModel.findById(getAccountRequestBody.userId).lean(); +}; diff --git a/src/handlers/account/middleware.ts b/src/handlers/account/middleware.ts new file mode 100644 index 0000000..5188402 --- /dev/null +++ b/src/handlers/account/middleware.ts @@ -0,0 +1,19 @@ +import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction as Done } from "fastify"; + +export const verifyUser = async (request: FastifyRequest, reply: FastifyReply, done: Done) => { + try { + const { id } = await request.jwtVerify<{ id?: string }>({ onlyCookie: true }); + + if (!id) { + reply.status(401); + return reply.send("user id is empty"); + } + + request.user = { id }; + } catch (nativeError) { + reply.status(401); + return reply.send(nativeError); + } + + done(); +}; diff --git a/src/handlers/account/types.ts b/src/handlers/account/types.ts new file mode 100644 index 0000000..bd9a92a --- /dev/null +++ b/src/handlers/account/types.ts @@ -0,0 +1,6 @@ +import type { FastifyRequest } from "fastify"; + +import type { CreateAccountRoute, GetAccountRoute } from "@/types/routes/account-routes.type"; + +export type CreateAccountRequest = FastifyRequest; +export type GetAccountRequest = FastifyRequest; diff --git a/src/index.ts b/src/index.ts index 1dbdc31..8426a4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,11 @@ const server = new Server({ jwt: { secret: { public: CONFIGURATION.service.publicAccessSecretKey, - private: CONFIGURATION.service.privateAccessSecretKey, + private: "", + }, + verify: { + allowedIss: "pena-auth-service", + allowedAud: "pena", }, }, }, diff --git a/src/models/user.model.ts b/src/models/account.model.ts similarity index 53% rename from src/models/user.model.ts rename to src/models/account.model.ts index 36ecb68..00f217f 100644 --- a/src/models/user.model.ts +++ b/src/models/account.model.ts @@ -2,24 +2,17 @@ import { Schema, model, SchemaDefinition } from "mongoose"; import { eloquentModelSchema } from "./eloquent-model.schema"; -import type { User } from "@/types/models/user.type"; +import type { Account } from "@/types/models/account.type"; -const schema: SchemaDefinition = { - login: { +const schema: SchemaDefinition = { + userId: { type: String, required: true, }, - email: { - type: String, - required: true, - }, - password: { - type: String, - required: true, - }, - phoneNumber: { + nickname: { type: String, required: true, + default: "nickname", }, avatar: { type: String, @@ -34,9 +27,9 @@ const schema: SchemaDefinition = { const schemaSettings = { versionKey: false, - collection: "users", + collection: "accounts", }; -const UserSchema = new Schema(schema, schemaSettings); +const AccountSchema = new Schema(schema, schemaSettings); -export const UserModel = model("User", UserSchema); +export const AccountModel = model("Account", AccountSchema); diff --git a/src/models/eloquent-model.schema.ts b/src/models/eloquent-model.schema.ts index b6920b6..be46084 100644 --- a/src/models/eloquent-model.schema.ts +++ b/src/models/eloquent-model.schema.ts @@ -1,5 +1,5 @@ import type { SchemaDefinition } from "mongoose"; -import type { EloquentModel } from "@/types/models/eloquent-model"; +import type { EloquentModel } from "@/types/models/eloquent-model.type"; export const eloquentModelSchema: SchemaDefinition = { createdAt: { diff --git a/src/models/token.model.ts b/src/models/token.model.ts deleted file mode 100644 index d92fb79..0000000 --- a/src/models/token.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Schema, model, SchemaDefinition } from "mongoose"; - -import type { Token } from "@/types/models/token.type"; - -const schema: SchemaDefinition = { - userId: { - type: String, - required: true, - }, - refreshToken: { - type: String, - required: true, - }, -}; - -const schemaSettings = { - versionKey: false, - collection: "tokens", -}; - -const TokenSchema = new Schema(schema, schemaSettings); - -export const TokenModel = model("Token", TokenSchema); diff --git a/src/routes/account.routes.ts b/src/routes/account.routes.ts new file mode 100644 index 0000000..eb78322 --- /dev/null +++ b/src/routes/account.routes.ts @@ -0,0 +1,12 @@ +import { createAccount, getAccount } from "@/handlers/account"; +import { verifyUser } from "@/handlers/account/middleware"; + +import type { FastifyInstance, FastifyPluginOptions } from "fastify"; +import type { GetAccountRoute, CreateAccountRoute } from "@/types/routes/account-routes.type"; + +export const setRoleRoutes = (server: FastifyInstance, opts: T, done: () => void): void => { + server.get("/:userId", getAccount); + server.post("/", { preHandler: [verifyUser] }, createAccount); + + done(); +}; diff --git a/src/types/fastify-jwt.d.ts b/src/types/fastify-jwt.d.ts new file mode 100644 index 0000000..b1b3f96 --- /dev/null +++ b/src/types/fastify-jwt.d.ts @@ -0,0 +1,8 @@ +import "@fastify/jwt"; + +declare module "@fastify/jwt" { + interface FastifyJWT { + payload: { id: string }; + user: { id: string }; + } +} diff --git a/src/types/models/account.type.ts b/src/types/models/account.type.ts new file mode 100644 index 0000000..06e5beb --- /dev/null +++ b/src/types/models/account.type.ts @@ -0,0 +1,7 @@ +export type Account = { + userId: string; + nickname: string; + avatar: string; + role: string; + privilegies: Record; +}; diff --git a/src/types/models/eloquent-model.ts b/src/types/models/eloquent-model.type.ts similarity index 100% rename from src/types/models/eloquent-model.ts rename to src/types/models/eloquent-model.type.ts diff --git a/src/types/models/role.type.ts b/src/types/models/role.type.ts index e2eb2b1..820b8e8 100644 --- a/src/types/models/role.type.ts +++ b/src/types/models/role.type.ts @@ -1,4 +1,4 @@ -import type { EloquentModel } from "./eloquent-model"; +import type { EloquentModel } from "./eloquent-model.type"; export type Role = EloquentModel & { name: string; diff --git a/src/types/models/user.type.ts b/src/types/models/user.type.ts new file mode 100644 index 0000000..8a0457e --- /dev/null +++ b/src/types/models/user.type.ts @@ -0,0 +1,8 @@ +import type { EloquentModel } from "./eloquent-model.type"; + +export type User = EloquentModel & { + _id: string; + login: string; + email: string; + phoneNumber: string; +}; diff --git a/src/types/routes/account-routes.type.ts b/src/types/routes/account-routes.type.ts new file mode 100644 index 0000000..fe1fc9f --- /dev/null +++ b/src/types/routes/account-routes.type.ts @@ -0,0 +1,13 @@ +import type { RequestGenericInterface } from "fastify"; + +export type GetAccountRoute = RequestGenericInterface & { + Params?: { + userId?: string; + }; +}; + +export type CreateAccountRoute = RequestGenericInterface & { + Params?: { + userId?: string; + }; +}; diff --git a/yarn.lock b/yarn.lock index faa2666..04e14ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2049,6 +2049,11 @@ async-mutex@^0.3.2: dependencies: tslib "^2.3.1" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" @@ -2063,6 +2068,15 @@ avvio@^8.2.0: debug "^4.0.0" fastq "^1.6.1" +axios@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.1.tgz#44cf04a3c9f0c2252ebd85975361c026cb9f864a" + integrity sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" @@ -2366,6 +2380,13 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^9.0.0: version "9.4.1" resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd" @@ -2530,6 +2551,11 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" @@ -3134,6 +3160,20 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4357,6 +4397,18 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4843,6 +4895,11 @@ proxy-addr@^2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pstree.remy@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"