diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 00000000..bb13dfc4 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index ed48a299..bd6a178a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,3 @@ node_modules # MacOS .DS_Store - -# env files -*.env -.env* diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 00000000..763fae2e --- /dev/null +++ b/dist/index.js @@ -0,0 +1,647 @@ +// src/createServer.ts +import "dotenv/config"; +import express from "express"; +import cors from "cors"; + +// src/routes/users.routes.ts +import { Router } from "express"; + +// src/db.ts +import "dotenv/config"; +import { PrismaPg } from "@prisma/adapter-pg"; + +// generated/prisma/client.ts +import "process"; +import * as path from "path"; +import { fileURLToPath } from "url"; +import "@prisma/client/runtime/client"; + +// generated/prisma/internal/class.ts +import * as runtime from "@prisma/client/runtime/client"; +var config = { + "previewFeatures": [], + "clientVersion": "7.4.1", + "engineVersion": "55ae170b1ced7fc6ed07a15f110549408c501bb3", + "activeProvider": "postgresql", + "inlineSchema": '// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = "prisma-client"\n output = "../generated/prisma"\n}\n\ndatasource db {\n provider = "postgresql"\n}\n\nmodel User {\n id String @id @default(dbgenerated("gen_random_uuid()"))\n email String @unique\n name String\n password String\n passwordToken String?\n activationToken String? @default(dbgenerated("gen_random_uuid()"))\n refreshTokens RefreshToken[]\n\n @@map("users")\n}\n\nmodel RefreshToken {\n id String @id @default(dbgenerated("gen_random_uuid()"))\n userId String @unique\n user User @relation(fields: [userId], references: [id])\n token String @unique\n createdAt DateTime @default(now())\n}\n', + "runtimeDataModel": { + "models": {}, + "enums": {}, + "types": {} + }, + "parameterizationSchema": { + "strings": [], + "graph": "" + } +}; +config.runtimeDataModel = JSON.parse('{"models":{"User":{"fields":[{"name":"id","kind":"scalar","type":"String"},{"name":"email","kind":"scalar","type":"String"},{"name":"name","kind":"scalar","type":"String"},{"name":"password","kind":"scalar","type":"String"},{"name":"passwordToken","kind":"scalar","type":"String"},{"name":"activationToken","kind":"scalar","type":"String"},{"name":"refreshTokens","kind":"object","type":"RefreshToken","relationName":"RefreshTokenToUser"}],"dbName":"users"},"RefreshToken":{"fields":[{"name":"id","kind":"scalar","type":"String"},{"name":"userId","kind":"scalar","type":"String"},{"name":"user","kind":"object","type":"User","relationName":"RefreshTokenToUser"},{"name":"token","kind":"scalar","type":"String"},{"name":"createdAt","kind":"scalar","type":"DateTime"}],"dbName":null}},"enums":{},"types":{}}'); +config.parameterizationSchema = { + strings: JSON.parse('["where","orderBy","cursor","user","refreshTokens","_count","User.findUnique","User.findUniqueOrThrow","User.findFirst","User.findFirstOrThrow","User.findMany","data","User.createOne","User.createMany","User.createManyAndReturn","User.updateOne","User.updateMany","User.updateManyAndReturn","create","update","User.upsertOne","User.deleteOne","User.deleteMany","having","_min","_max","User.groupBy","User.aggregate","RefreshToken.findUnique","RefreshToken.findUniqueOrThrow","RefreshToken.findFirst","RefreshToken.findFirstOrThrow","RefreshToken.findMany","RefreshToken.createOne","RefreshToken.createMany","RefreshToken.createManyAndReturn","RefreshToken.updateOne","RefreshToken.updateMany","RefreshToken.updateManyAndReturn","RefreshToken.upsertOne","RefreshToken.deleteOne","RefreshToken.deleteMany","RefreshToken.groupBy","RefreshToken.aggregate","AND","OR","NOT","id","userId","token","createdAt","equals","in","notIn","lt","lte","gt","gte","not","contains","startsWith","endsWith","email","name","password","passwordToken","activationToken","every","some","none","is","isNot","connectOrCreate","upsert","createMany","set","disconnect","delete","connect","updateMany","deleteMany"]'), + graph: "ahIgCgQAAEUAICwAAEIAMC0AAAkAEC4AAEIAMC8BAAAAAT4BAAAAAT8BAEMAIUABAEMAIUEBAEQAIUIBAEQAIQEAAAABACAIAwAASAAgLAAARgAwLQAAAwAQLgAARgAwLwEAQwAhMAEAQwAhMQEAQwAhMkAARwAhAQMAAGQAIAgDAABIACAsAABGADAtAAADABAuAABGADAvAQAAAAEwAQAAAAExAQAAAAEyQABHACEDAAAAAwAgAQAABAAwAgAABQAgAQAAAAMAIAEAAAABACAKBAAARQAgLAAAQgAwLQAACQAQLgAAQgAwLwEAQwAhPgEAQwAhPwEAQwAhQAEAQwAhQQEARAAhQgEARAAhAwQAAGMAIEEAAFAAIEIAAFAAIAMAAAAJACABAAAKADACAAABACADAAAACQAgAQAACgAwAgAAAQAgAwAAAAkAIAEAAAoAMAIAAAEAIAcEAABiACAvAQAAAAE-AQAAAAE_AQAAAAFAAQAAAAFBAQAAAAFCAQAAAAEBCwAADgAgBi8BAAAAAT4BAAAAAT8BAAAAAUABAAAAAUEBAAAAAUIBAAAAAQELAAAQADABCwAAEAAwBwQAAFUAIC8BAEwAIT4BAEwAIT8BAEwAIUABAEwAIUEBAFQAIUIBAFQAIQIAAAABACALAAATACAGLwEATAAhPgEATAAhPwEATAAhQAEATAAhQQEAVAAhQgEAVAAhAgAAAAkAIAsAABUAIAIAAAAJACALAAAVACADAAAAAQAgEgAADgAgEwAAEwAgAQAAAAEAIAEAAAAJACAFBQAAUQAgGAAAUwAgGQAAUgAgQQAAUAAgQgAAUAAgCSwAAD0AMC0AABwAEC4AAD0AMC8BADYAIT4BADYAIT8BADYAIUABADYAIUEBAD4AIUIBAD4AIQMAAAAJACABAAAbADAXAAAcACADAAAACQAgAQAACgAwAgAAAQAgAQAAAAUAIAEAAAAFACADAAAAAwAgAQAABAAwAgAABQAgAwAAAAMAIAEAAAQAMAIAAAUAIAMAAAADACABAAAEADACAAAFACAFAwAATwAgLwEAAAABMAEAAAABMQEAAAABMkAAAAABAQsAACQAIAQvAQAAAAEwAQAAAAExAQAAAAEyQAAAAAEBCwAAJgAwAQsAACYAMAUDAABOACAvAQBMACEwAQBMACExAQBMACEyQABNACECAAAABQAgCwAAKQAgBC8BAEwAITABAEwAITEBAEwAITJAAE0AIQIAAAADACALAAArACACAAAAAwAgCwAAKwAgAwAAAAUAIBIAACQAIBMAACkAIAEAAAAFACABAAAAAwAgAwUAAEkAIBgAAEsAIBkAAEoAIAcsAAA1ADAtAAAyABAuAAA1ADAvAQA2ACEwAQA2ACExAQA2ACEyQAA3ACEDAAAAAwAgAQAAMQAwFwAAMgAgAwAAAAMAIAEAAAQAMAIAAAUAIAcsAAA1ADAtAAAyABAuAAA1ADAvAQA2ACEwAQA2ACExAQA2ACEyQAA3ACEOBQAAOQAgGAAAPAAgGQAAPAAgMwEAAAABNAEAAAAENQEAAAAENgEAAAABNwEAAAABOAEAAAABOQEAAAABOgEAOwAhOwEAAAABPAEAAAABPQEAAAABCwUAADkAIBgAADoAIBkAADoAIDNAAAAAATRAAAAABDVAAAAABDZAAAAAATdAAAAAAThAAAAAATlAAAAAATpAADgAIQsFAAA5ACAYAAA6ACAZAAA6ACAzQAAAAAE0QAAAAAQ1QAAAAAQ2QAAAAAE3QAAAAAE4QAAAAAE5QAAAAAE6QAA4ACEIMwIAAAABNAIAAAAENQIAAAAENgIAAAABNwIAAAABOAIAAAABOQIAAAABOgIAOQAhCDNAAAAAATRAAAAABDVAAAAABDZAAAAAATdAAAAAAThAAAAAATlAAAAAATpAADoAIQ4FAAA5ACAYAAA8ACAZAAA8ACAzAQAAAAE0AQAAAAQ1AQAAAAQ2AQAAAAE3AQAAAAE4AQAAAAE5AQAAAAE6AQA7ACE7AQAAAAE8AQAAAAE9AQAAAAELMwEAAAABNAEAAAAENQEAAAAENgEAAAABNwEAAAABOAEAAAABOQEAAAABOgEAPAAhOwEAAAABPAEAAAABPQEAAAABCSwAAD0AMC0AABwAEC4AAD0AMC8BADYAIT4BADYAIT8BADYAIUABADYAIUEBAD4AIUIBAD4AIQ4FAABAACAYAABBACAZAABBACAzAQAAAAE0AQAAAAU1AQAAAAU2AQAAAAE3AQAAAAE4AQAAAAE5AQAAAAE6AQA_ACE7AQAAAAE8AQAAAAE9AQAAAAEOBQAAQAAgGAAAQQAgGQAAQQAgMwEAAAABNAEAAAAFNQEAAAAFNgEAAAABNwEAAAABOAEAAAABOQEAAAABOgEAPwAhOwEAAAABPAEAAAABPQEAAAABCDMCAAAAATQCAAAABTUCAAAABTYCAAAAATcCAAAAATgCAAAAATkCAAAAAToCAEAAIQszAQAAAAE0AQAAAAU1AQAAAAU2AQAAAAE3AQAAAAE4AQAAAAE5AQAAAAE6AQBBACE7AQAAAAE8AQAAAAE9AQAAAAEKBAAARQAgLAAAQgAwLQAACQAQLgAAQgAwLwEAQwAhPgEAQwAhPwEAQwAhQAEAQwAhQQEARAAhQgEARAAhCzMBAAAAATQBAAAABDUBAAAABDYBAAAAATcBAAAAATgBAAAAATkBAAAAAToBADwAITsBAAAAATwBAAAAAT0BAAAAAQszAQAAAAE0AQAAAAU1AQAAAAU2AQAAAAE3AQAAAAE4AQAAAAE5AQAAAAE6AQBBACE7AQAAAAE8AQAAAAE9AQAAAAEDQwAAAwAgRAAAAwAgRQAAAwAgCAMAAEgAICwAAEYAMC0AAAMAEC4AAEYAMC8BAEMAITABAEMAITEBAEMAITJAAEcAIQgzQAAAAAE0QAAAAAQ1QAAAAAQ2QAAAAAE3QAAAAAE4QAAAAAE5QAAAAAE6QAA6ACEMBAAARQAgLAAAQgAwLQAACQAQLgAAQgAwLwEAQwAhPgEAQwAhPwEAQwAhQAEAQwAhQQEARAAhQgEARAAhRgAACQAgRwAACQAgAAAAAUsBAAAAAQFLQAAAAAEFEgAAZgAgEwAAaQAgSAAAZwAgSQAAaAAgTgAAAQAgAxIAAGYAIEgAAGcAIE4AAAEAIAAAAAABSwEAAAABCxIAAFYAMBMAAFsAMEgAAFcAMEkAAFgAMEoAAFkAIEsAAFoAMEwAAFoAME0AAFoAME4AAFoAME8AAFwAMFAAAF0AMAMvAQAAAAExAQAAAAEyQAAAAAECAAAABQAgEgAAYQAgAwAAAAUAIBIAAGEAIBMAAGAAIAELAABlADAIAwAASAAgLAAARgAwLQAAAwAQLgAARgAwLwEAAAABMAEAAAABMQEAAAABMkAARwAhAgAAAAUAIAsAAGAAIAIAAABeACALAABfACAHLAAAXQAwLQAAXgAQLgAAXQAwLwEAQwAhMAEAQwAhMQEAQwAhMkAARwAhBywAAF0AMC0AAF4AEC4AAF0AMC8BAEMAITABAEMAITEBAEMAITJAAEcAIQMvAQBMACExAQBMACEyQABNACEDLwEATAAhMQEATAAhMkAATQAhAy8BAAAAATEBAAAAATJAAAAAAQQSAABWADBIAABXADBKAABZACBOAABaADAAAwQAAGMAIEEAAFAAIEIAAFAAIAMvAQAAAAExAQAAAAEyQAAAAAEGLwEAAAABPgEAAAABPwEAAAABQAEAAAABQQEAAAABQgEAAAABAgAAAAEAIBIAAGYAIAMAAAAJACASAABmACATAABqACAIAAAACQAgCwAAagAgLwEATAAhPgEATAAhPwEATAAhQAEATAAhQQEAVAAhQgEAVAAhBi8BAEwAIT4BAEwAIT8BAEwAIUABAEwAIUEBAFQAIUIBAFQAIQIEBgIFAAMBAwABAQQHAAAAAAMFAAgYAAkZAAoAAAADBQAIGAAJGQAKAQMAAQEDAAEDBQAPGAAQGQARAAAAAwUADxgAEBkAEQYCAQcIAQgLAQkMAQoNAQwPAQ0RBA4SBQ8UARAWBBEXBhQYARUZARYaBBodBxseCxwfAh0gAh4hAh8iAiAjAiElAiInBCMoDCQqAiUsBCYtDScuAigvAikwBCozDis0Eg" +}; +async function decodeBase64AsWasm(wasmBase64) { + const { Buffer } = await import("buffer"); + const wasmArray = Buffer.from(wasmBase64, "base64"); + return new WebAssembly.Module(wasmArray); +} +config.compilerWasm = { + getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.mjs"), + getQueryCompilerWasmModule: async () => { + const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.mjs"); + return await decodeBase64AsWasm(wasm); + }, + importName: "./query_compiler_fast_bg.js" +}; +function getPrismaClientClass() { + return runtime.getPrismaClient(config); +} + +// generated/prisma/internal/prismaNamespace.ts +import * as runtime2 from "@prisma/client/runtime/client"; +var getExtensionContext = runtime2.Extensions.getExtensionContext; +var NullTypes2 = { + DbNull: runtime2.NullTypes.DbNull, + JsonNull: runtime2.NullTypes.JsonNull, + AnyNull: runtime2.NullTypes.AnyNull +}; +var TransactionIsolationLevel = runtime2.makeStrictEnum({ + ReadUncommitted: "ReadUncommitted", + ReadCommitted: "ReadCommitted", + RepeatableRead: "RepeatableRead", + Serializable: "Serializable" +}); +var defineExtension = runtime2.Extensions.defineExtension; + +// generated/prisma/client.ts +globalThis["__dirname"] = path.dirname(fileURLToPath(import.meta.url)); +var PrismaClient = getPrismaClientClass(); + +// src/db.ts +console.log(process.env); +var connectionString = `${process.env.DATABASE_URL || ""}`; +var adapter = new PrismaPg({ connectionString }); +var prisma = new PrismaClient({ adapter }); + +// src/repository/users.repository.ts +var normalize = ({ id, name, email }) => { + return { + id, + name, + email + }; +}; +var create = async ({ name, email, password }) => { + return prisma.user.create({ + data: { + name, + email, + password + } + }); +}; +var getByEmail = async (email) => { + return prisma.user.findFirst({ + where: { + email + } + }); +}; +var getById = async (id) => { + return prisma.user.findFirst({ + where: { + id + } + }); +}; +var deleteActivationToken = async (id) => { + return prisma.user.update({ + where: { + id + }, + data: { activationToken: null } + }); +}; +var change = async (userId, toChange) => { + return prisma.user.update({ + where: { + id: userId + }, + data: toChange + }); +}; +var updatePasswordToken = async (id, passwordToken) => { + return prisma.user.update({ + where: { + id + }, + data: { + passwordToken + } + }); +}; + +// src/service/mailer.service.ts +import "dotenv/config"; +import nodemailer from "nodemailer"; +var transporter = nodemailer.createTransport({ + host: process.env.SMTP_SERVER, + port: parseInt(process.env.SMTP_PORT || "587", 10), + secure: false, + // Use true for port 465, false for port 587 + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD + } +}); +var sendMail = async (to, html, subject = "Activate your account") => { + return transporter.sendMail({ + to, + subject, + html + }); +}; + +// src/service/jws.service.ts +import "dotenv/config"; +import pkg from "jsonwebtoken"; +var { sign, verify } = pkg; +var accessSecret = process.env.ACCESS_SECRET; +var refreshSecret = process.env.REFRESH_SECRET; +function wrap(callback) { + try { + return callback(); + } catch (err) { + return null; + } +} +var generateAccessToken = (normalizedUser) => { + return wrap(() => sign( + normalizedUser, + accessSecret, + { expiresIn: process.env.ACCESS_TOKEN_EXPIRY || "5s" } + )); +}; +var validateAccessToken = (token) => { + return wrap(() => { + return verify(token, accessSecret); + }); +}; +var generateRefreshToken = (normalizedUser) => { + return wrap(() => sign( + normalizedUser, + refreshSecret, + { expiresIn: process.env.REFRESH_TOKEN_EXPIRY || "30d" } + )); +}; +var validateRefreshToken = (token) => { + return wrap(() => verify(token, refreshSecret)); +}; + +// src/types/index.ts +var ApiError = class _ApiError extends Error { + status; + errors; + constructor(message, status, errors) { + super(message); + this.status = status; + this.errors = errors; + } + static badRequest(messages) { + return new _ApiError( + "Bad request", + 400, + { errors: messages } + ); + } + static authError(messages) { + return new _ApiError( + "Unauthorized", + 401, + { errors: messages } + ); + } + static accountAlreadyExist(messages) { + return new _ApiError("Conflict", 409, { errors: messages }); + } + static notFound(messages) { + return new _ApiError("Not found", 404, { errors: messages }); + } +}; + +// src/repository/tokens.repository.ts +var upsert = (normalizedUser) => { + return prisma.refreshToken.upsert({ + where: { + userId: normalizedUser.id + }, + update: { + token: generateRefreshToken(normalizedUser) + }, + create: { + userId: normalizedUser.id, + token: generateRefreshToken(normalizedUser) + } + }); +}; +var deleteToken = (token) => { + return prisma.refreshToken.delete({ + where: { + token + } + }); +}; + +// src/controllers/users.controller.ts +import { v4 as uuidv4 } from "uuid"; + +// src/utils/validators.ts +function validateEmail(email) { + const emailPattern = /^[\w.+-]+@([\w-]+\.){1,3}[\w-]{2,}$/; + if (!email) return "Email is required"; + if (!emailPattern.test(email)) return "Email is not valid"; +} +function validatePassword(password) { + if (!password) return "Password is required"; + if (password.length < 6) return "At least 6 characters"; +} + +// src/controllers/users.controller.ts +import * as bcrypt from "bcrypt"; + +// src/service/auth.service.ts +var saveAuthorization = async (res, normalizedUser) => { + const refreshToken = await upsert(normalizedUser); + res.cookie("refreshToken", refreshToken.token, { + maxAge: 30 * 24 * 60 * 60 * 1e3, + httpOnly: true, + sameSite: "lax" + }); +}; +var auth_service_default = { + saveAuthorization +}; + +// src/controllers/users.controller.ts +var create2 = async (req, res) => { + const { name, email, password } = req.body; + let user = await getByEmail(email); + const emailValidation = validateEmail(email) || null; + const passValidation = validatePassword(password) || null; + const errors = [emailValidation, passValidation].filter( + (validation) => validation !== null + ).map((err) => ({ message: err })); + if (errors.length) { + throw ApiError.badRequest(errors); + } + if (user?.activationToken) { + throw ApiError.badRequest([{ + message: "User already registered, check your mailbox for activation email" + }]); + } + if (user) { + throw ApiError.badRequest([{ + message: "User has already been registered" + }]); + } + if (name && !validateEmail(email) && !validatePassword(password) && !user) { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + user = await create({ + name, + email, + password: hashedPassword + }); + } + if (!user) { + throw ApiError.badRequest( + [{ + message: "Invalid sign-up data" + }] + ); + } + const href = `${process.env.CLIENT_URL}/activate/${email}/${user?.activationToken}`; + const html = ` +
[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise []>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromiseAccount activation
\n Click to activate \n `;\n\n await mailer.sendMail(email, html);\n\n res.statusCode = 200;\n res.send(userRepository.normalize(user));\n};\n\nconst activate = async (req: Request, res: Response) => {\n const { email, activationToken } = req.params;\n\n if (typeof email !== 'string' || typeof activationToken !== 'string') {\n throw ApiError.badRequest([{\n message: 'Invalid data'\n }]\n );\n }\n\n const user = await userRepository.getByEmail(email);\n\n if (!user) {\n throw ApiError.badRequest(\n [{message: 'User not found'}]\n );\n }\n\n if (!user.activationToken) {\n throw ApiError.accountAlreadyExist([{\n message: 'Account already activated'\n }]);\n }\n\n if (user.activationToken !== activationToken) {\n throw ApiError.badRequest([{\n message: 'Invalid activation token'\n }]);\n }\n\n const normalizedUser = userRepository.normalize(user);\n\n await auth.saveAuthorization(res, normalizedUser);\n await userRepository.deleteActivationToken(user.id);\n\n res.statusCode = 200;\n res.send({\n user: normalizedUser,\n accessToken: jws.generateAccessToken(normalizedUser),\n });\n};\n\nconst login = async (req: Request, res: Response) => {\n const { email, password } = req.body;\n\n if (typeof email !== 'string' || typeof password !== 'string') {\n throw ApiError.badRequest([{\n message: 'Invalid login data'\n }])\n }\n\n const user = await userRepository.getByEmail(email);\n\n if (!user) {\n throw ApiError.notFound([{\n message: 'User not found'\n }])\n }\n\n if (user.activationToken) {\n throw ApiError.authError([{\n message: 'User isnt activated, check your mailbox for activation link'\n }]);\n }\n\n const compare = await bcrypt.compare(password, user?.password);\n\n if (!compare) {\n throw ApiError.authError([{\n message: 'Invalid password',\n }])\n }\n\n const normalizedUser = userRepository.normalize(user);\n\n await auth.saveAuthorization(res, normalizedUser);\n\n res.statusCode = 200;\n res.send({\n user: normalizedUser,\n accessToken: jws.generateAccessToken(normalizedUser),\n });\n};\n\nconst profile = async (req: Request, res: Response) => {\n const normalizedUser = req.normalizedUser;\n\n res.statusCode = 200;\n res.send(normalizedUser);\n};\n\nconst logout = async (req: Request, res: Response) => {\n const refreshToken = req.cookies['refreshToken'];\n\n res.clearCookie('refreshToken');\n\n const deleted = await tokenRepository.deleteToken(refreshToken);\n\n if (!deleted) {\n throw ApiError.notFound([{\n message: 'Account not found'\n }]);\n }\n\n res.statusCode = 204;\n}\n\nconst changeName = async (req: Request, res: Response) => {\n const { name }: { name: string } = req.body;\n const userToUpdate = req.normalizedUser as NormalizedUser || null;\n\n if (typeof name !== 'string' || name.length < 2) {\n throw ApiError.badRequest([{\n message: 'Invalid data for an update'\n }])\n }\n\n const updatedUser = await userRepository.change(userToUpdate?.id, { name });\n const normalizedUser = userRepository.normalize(updatedUser);\n\n await auth.saveAuthorization(res, normalizedUser)\n\n res.statusCode = 200;\n res.send({\n user: normalizedUser,\n accessToken: jws.generateAccessToken(normalizedUser)\n });\n}\n\nconst changeSensetive = async (req: Request, res: Response) => {\n const loginData: LoginData | null = req.body.loginData || null;\n const toChange: UserPropsToUpdate | null = req.body.toChange || null;\n\n if (!loginData || !toChange) {\n throw ApiError.badRequest([{\n message: 'Bad request'\n }]);\n }\n\n const user = await userRepository.getById(req.normalizedUser?.id) || null;\n\n if (toChange?.email && await userRepository.getByEmail(toChange?.email)) {\n throw ApiError.badRequest([{\n for: 'email',\n message: 'Email already registered',\n }])\n }\n\n if (!user) {\n throw ApiError.notFound([{\n for: 'email',\n message: 'Account not found'\n }])\n }\n\n const compare = await bcrypt.compare(loginData.password, user.password)\n\n if (!compare) {\n throw ApiError.authError([{\n for: 'currentPassword',\n message: 'Invalid Password'\n }])\n }\n\n const updatedToChange = { ...toChange }\n\n if (Object.keys(updatedToChange).includes('password')) {\n const salt = await bcrypt.genSalt(10);\n const hashedPassword = await bcrypt.hash(updatedToChange.password, salt);\n updatedToChange.password = hashedPassword\n }\n\n const updatedUser = await userRepository.change(user.id, updatedToChange);\n\n if (!updatedUser) {\n throw new ApiError(\n 'User not found',\n 404,\n { errors: [{ message: 'User not found'}]}\n );\n };\n\n if (toChange?.email) {\n const html = `\n Changes in your auth account
\n Email changed to ${toChange?.email}
\n `;\n\n mailer.sendMail(user.email, html, 'Update');\n }\n\n const normalizedUser = userRepository.normalize(updatedUser);\n\n await auth.saveAuthorization(res, normalizedUser)\n\n res.statusCode = 200;\n res.send({\n user: normalizedUser,\n accessToken: jws.generateAccessToken(normalizedUser)\n })\n}\n\nconst generatePasswordToken = async (req: Request, res: Response) => {\n const { email } = req.body;\n\n\n const user = await userRepository.getByEmail(email);\n\n if (!user) {\n throw ApiError.notFound([{\n for: 'email',\n message: 'User not found'\n }])\n }\n\n const passwordToken = uuidv4();\n\n const updatedUser = await userRepository.updatePasswordToken(user.id, passwordToken);\n\n if (!updatedUser) {\n throw new Error('Internal Server Error');\n }\n\n const href = `${process.env.CLIENT_URL}/changePassword/${email}/${passwordToken}`\n const html = `\n Password reset
\n Click to update your password\n `\n\n mailer.sendMail(email, html, 'Password reset');\n\n res.sendStatus(204);\n}\n\nconst changePassword = async (req: Request, res: Response) => {\n const { email, passwordToken, password } = req.body;\n\n if (!email || !passwordToken || !password ) {\n throw ApiError.badRequest([{ message: 'Bad request'}]);\n }\n\n const user = await userRepository.getByEmail(email);\n\n if (!user) {\n throw ApiError.notFound([{ message: 'User not found '}])\n }\n\n if (user?.passwordToken !== passwordToken) {\n throw ApiError.badRequest([{ for: 'general', message: 'Invalid or expired link'}]);\n }\n\n const salt = await bcrypt.genSalt(10);\n const hashedPassword = await bcrypt.hash(password, salt);\n\n await userRepository.change(user?.id, { password: hashedPassword, passwordToken: null})\n\n res.sendStatus(204);\n}\n\nexport { create, activate, login, profile, logout, changeName, changeSensetive, generatePasswordToken, changePassword };\n","export function validateEmail(email: string) {\n const emailPattern = /^[\\w.+-]+@([\\w-]+\\.){1,3}[\\w-]{2,}$/;\n\n if (!email) return 'Email is required';\n if (!emailPattern.test(email)) return 'Email is not valid';\n}\n\nexport function validatePassword(password: string) {\n if (!password) return 'Password is required';\n if (password.length < 6) return 'At least 6 characters';\n}\n","import type { Response } from 'express';\nimport * as tokenRepository from '../repository/tokens.repository.ts';\nimport type { NormalizedUser } from '../types/index.ts';\n\nconst saveAuthorization = async (res: Response, normalizedUser: NormalizedUser) => {\n const refreshToken = await tokenRepository.upsert(normalizedUser);\n\n res.cookie('refreshToken', refreshToken.token, {\n maxAge: 30 * 24 * 60 * 60 * 1000,\n httpOnly: true,\n sameSite: 'lax'\n })\n}\n\nexport default {\n saveAuthorization,\n}\n","import type { NextFunction, Request, Response } from \"express\";\n\ntype AsyncHandler = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise