From fa1a34372b755a22da55d52e464a0862ab0aafc5 Mon Sep 17 00:00:00 2001 From: Akhil Meka Date: Fri, 25 Jul 2025 15:38:04 +0530 Subject: [PATCH] updated login flow --- package.json | 1 + pnpm-lock.yaml | 31 +++++++++ src/auth/auth.route.ts | 135 ++++++++++++++++++++++++++------------- src/user/user.schema.ts | 2 + src/user/user.service.ts | 23 +++++-- src/utils/id.ts | 10 --- src/utils/password.ts | 25 ++++++++ 7 files changed, 166 insertions(+), 61 deletions(-) create mode 100644 src/utils/password.ts diff --git a/package.json b/package.json index 4e3771f..dcc807e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@paralleldrive/cuid2": "^2.2.2", "@simplewebauthn/server": "^13.1.1", "@types/qs": "^6.9.18", + "argon2": "^0.43.1", "axios": "^1.7.9", "bcryptjs": "^3.0.0", "fastify": "^5.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa09e77..15f6ac4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@types/qs': specifier: ^6.9.18 version: 6.9.18 + argon2: + specifier: ^0.43.1 + version: 0.43.1 axios: specifier: ^1.7.9 version: 1.7.9 @@ -348,6 +351,10 @@ packages: '@peculiar/asn1-x509@2.3.15': resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==} + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + '@sideway/address@4.1.5': resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} @@ -596,6 +603,10 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + argon2@0.43.1: + resolution: {integrity: sha512-TfOzvDWUaQPurCT1hOwIeFNkgrAJDpbBGBGWDgzDsm11nNhImc13WhdGdCU6K7brkp8VpeY07oGtSex0Wmhg8w==} + engines: {node: '>=16.17.0'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -940,6 +951,14 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-addon-api@8.5.0: + resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} + engines: {node: ^18 || ^20 || >= 21} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -1842,6 +1861,8 @@ snapshots: pvtsutils: 1.3.6 tslib: 2.8.1 + '@phc/format@1.0.0': {} + '@sideway/address@4.1.5': dependencies: '@hapi/hoek': 9.3.0 @@ -2222,6 +2243,12 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + argon2@0.43.1: + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 8.5.0 + node-gyp-build: 4.8.4 + argparse@2.0.1: {} asn1js@3.0.5: @@ -2611,6 +2638,10 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-addon-api@8.5.0: {} + + node-gyp-build@4.8.4: {} + object-inspect@1.13.4: {} obliterator@2.0.4: {} diff --git a/src/auth/auth.route.ts b/src/auth/auth.route.ts index 39c0907..41a4da0 100644 --- a/src/auth/auth.route.ts +++ b/src/auth/auth.route.ts @@ -1,61 +1,106 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; -import { getUserByEmail, updateUserInternal } from "../user/user.service"; +import { getUserByEmail, getUserByToken } from "../user/user.service"; import { createSession, deleteSession } from "./auth.service"; +import { hash, verify } from "argon2"; +import { validatePassword } from "../utils/password"; export async function authRoutes(fastify: FastifyInstance) { - fastify.get( - "/microsoft/token", - {}, + fastify.post( + "/register", + { + schema: { + body: { + type: "object", + required: ["password", "token"], + properties: { + password: { type: "string" }, + token: { type: "string" }, + }, + }, + }, + }, async (req: FastifyRequest, res: FastifyReply) => { + const { password, token } = req.body as { + password: string; + token: string; + }; + try { - const { token } = - await fastify.microsoftOauth.getAccessTokenFromAuthorizationCodeFlow( - req - ); + const userInDB = await getUserByToken(token); + if (!userInDB) return res.code(404).send({ error: "not found" }); + if (new Date() > userInDB.token.expiry) + return res.code(400).send({ error: "link expired" }); - const user = (await fastify.microsoftOauth.userinfo(token)) as { - givenname: string; - familyname: string; - email: string; - picture: string; - }; + if (!validatePassword(password)) + return res.code(400).send({ + error: + "weak password. Make sure the password has atleast 2 uppercase, 2 lowercase, 2 numeric and 2 special characters", + }); - const userInDb = await getUserByEmail(user.email); - if (userInDb == null) - return res.code(401).send({ error: "not_allowed" }); + const hashedPassword = await hash(password); + userInDB.passwordHash = hashedPassword; - await updateUserInternal(userInDb.pid, { - firstName: user.givenname, - lastName: user.familyname, - email: user.email, - avatar: user.picture, - }); - - const session = await createSession( - userInDb.id, - req.ip, - req.headers["user-agent"] - ); - - return res.code(201).send({ session_token: session.sid }); + await userInDB.save(); } catch (err) { - //@ts-ignore - if (err.data) { - //@ts-ignore - fastify.log.warn(err.data.payload); - return res.code(400).send(); - } else { - return err; - } + return err; } } ); - fastify.delete("/logout", {}, async (req, res) => { - if (!req.headers.authorization) return res.code(200).send(); + fastify.post( + "/login", + { + schema: { + body: { + type: "object", + required: ["email", "password"], + properties: { + email: { type: "string" }, + password: { type: "string" }, + }, + }, + }, + }, + async (req: FastifyRequest, res: FastifyReply) => { + const { email, password } = req.body as { + email: string; + password: string; + }; - const auth = req.headers.authorization.split(" ")[1]; - await deleteSession(auth); - return res.code(200).send(); - }); + try { + const userInDB = await getUserByEmail(email); + if (!userInDB) + return res.code(401).send({ error: "invalid email or password" }); + + const match = await verify(userInDB.passwordHash, password); + if (!match) + return res.code(401).send({ error: "invalid email or password" }); + + const newSession = await createSession( + userInDB.id, + req.ip, + req.headers["user-agent"] + ); + + userInDB.lastLogin = new Date(); + await userInDB.save(); + + res.send({ session_token: newSession.sid }); + } catch (err) { + return err; + } + } + ); + + fastify.delete( + "/logout", + {}, + async (req: FastifyRequest, res: FastifyReply) => { + if (!req.headers.authorization) return res.code(200).send(); + + const auth = req.headers.authorization.split(" ")[1]; + await deleteSession(auth); + return res.code(200).send(); + } + ); } diff --git a/src/user/user.schema.ts b/src/user/user.schema.ts index b5aab13..5646018 100644 --- a/src/user/user.schema.ts +++ b/src/user/user.schema.ts @@ -28,6 +28,7 @@ const userSchema = new mongoose.Schema({ type: String, required: true, }, + passwordHash: String, passKeys: [new mongoose.Schema({}, { _id: false, strict: false })], challenge: new mongoose.Schema( { @@ -68,6 +69,7 @@ const userCore = { avatar: z.string().optional(), role: z.enum(roles), orgId: z.string().optional(), + password: z.string().optional(), }; const createUserInput = z diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 49e5ac2..ca2ff47 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -4,6 +4,7 @@ import { CreateUserInput, UpdateUserInput, userModel } from "./user.schema"; import { sendMail } from "../utils/mail"; import { AuthenticatedUser } from "../auth"; import { createUserConfig } from "../userConfig/userConfig.service"; +import { hash } from "argon2"; export const ErrUserNotFound = new Error("user not found"); export const ErrOpNotValid = new Error("operation is not valid"); @@ -26,7 +27,8 @@ export async function createUser( throw ErrMissingOrdId; } - const token = await generateToken(); + let hashedPassword = ""; + if (input.password) hashedPassword = await hash(input.password); const newUser = await userModel.create({ tenantId: user.tenantId, @@ -34,16 +36,25 @@ export async function createUser( name: input.firstName + " " + input.lastName, createdAt: new Date(), createdBy: user.userId, - token: { - value: token, - expiry: new Date(Date.now() + 3600 * 48 * 1000), - }, - status: "invited", + status: input.password ? "active" : "invited", + passwordHash: hashedPassword, ...input, }); await createUserConfig(newUser.id, newUser.tenantId); + if (input.password) + return newUser.populate({ path: "orgId", select: "pid name avatar" }); + + const token = await generateToken(); + + newUser.token = { + value: token, + expiry: new Date(Date.now() + 3600 * 48 * 1000), + }; + + await newUser.save(); + const sent = await sendMail( input.email, "You have been invited to Quicker Permtis.", diff --git a/src/utils/id.ts b/src/utils/id.ts index c81f4dd..d68f997 100644 --- a/src/utils/id.ts +++ b/src/utils/id.ts @@ -17,13 +17,3 @@ export async function generateToken(): Promise { }); }); } - -export function generateOTP() { - let otp = ""; - - for (let i = 0; i < 6; i++) { - otp += crypto.randomInt(10); - } - - return parseInt(otp); -} diff --git a/src/utils/password.ts b/src/utils/password.ts new file mode 100644 index 0000000..6ba5dfd --- /dev/null +++ b/src/utils/password.ts @@ -0,0 +1,25 @@ +export function validatePassword(password: string): boolean { + const charCounts = { + upper: 0, + lower: 0, + number: 0, + special: 0, + }; + + for (const char of password.split("")) { + if ("ABCDEFGHIJKLMNOPQRSTUVWXYZ".includes(char)) charCounts.upper++; + if ("abcdefghijklmnopqrstuvwxyz".includes(char)) charCounts.lower++; + if ("0123456789".includes(char)) charCounts.number++; + if ("!@#$%^&*()<>?;:{}[]~".includes(char)) charCounts.special++; + } + + if ( + charCounts.upper >= 2 && + charCounts.lower >= 2 && + charCounts.number >= 2 && + charCounts.special >= 2 + ) + return true; + + return false; +}