updated login flow
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@simplewebauthn/server": "^13.1.1",
|
"@simplewebauthn/server": "^13.1.1",
|
||||||
"@types/qs": "^6.9.18",
|
"@types/qs": "^6.9.18",
|
||||||
|
"argon2": "^0.43.1",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^3.0.0",
|
"bcryptjs": "^3.0.0",
|
||||||
"fastify": "^5.2.0",
|
"fastify": "^5.2.0",
|
||||||
|
|||||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
|||||||
'@types/qs':
|
'@types/qs':
|
||||||
specifier: ^6.9.18
|
specifier: ^6.9.18
|
||||||
version: 6.9.18
|
version: 6.9.18
|
||||||
|
argon2:
|
||||||
|
specifier: ^0.43.1
|
||||||
|
version: 0.43.1
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.7.9
|
specifier: ^1.7.9
|
||||||
version: 1.7.9
|
version: 1.7.9
|
||||||
@@ -348,6 +351,10 @@ packages:
|
|||||||
'@peculiar/asn1-x509@2.3.15':
|
'@peculiar/asn1-x509@2.3.15':
|
||||||
resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==}
|
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':
|
'@sideway/address@4.1.5':
|
||||||
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
|
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
|
||||||
|
|
||||||
@@ -596,6 +603,10 @@ packages:
|
|||||||
ajv@8.17.1:
|
ajv@8.17.1:
|
||||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
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:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
@@ -940,6 +951,14 @@ packages:
|
|||||||
no-case@3.0.4:
|
no-case@3.0.4:
|
||||||
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
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:
|
object-inspect@1.13.4:
|
||||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1842,6 +1861,8 @@ snapshots:
|
|||||||
pvtsutils: 1.3.6
|
pvtsutils: 1.3.6
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@phc/format@1.0.0': {}
|
||||||
|
|
||||||
'@sideway/address@4.1.5':
|
'@sideway/address@4.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@hapi/hoek': 9.3.0
|
'@hapi/hoek': 9.3.0
|
||||||
@@ -2222,6 +2243,12 @@ snapshots:
|
|||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
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: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
asn1js@3.0.5:
|
asn1js@3.0.5:
|
||||||
@@ -2611,6 +2638,10 @@ snapshots:
|
|||||||
lower-case: 2.0.2
|
lower-case: 2.0.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
node-addon-api@8.5.0: {}
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
object-inspect@1.13.4: {}
|
object-inspect@1.13.4: {}
|
||||||
|
|
||||||
obliterator@2.0.4: {}
|
obliterator@2.0.4: {}
|
||||||
|
|||||||
@@ -1,61 +1,106 @@
|
|||||||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
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 { createSession, deleteSession } from "./auth.service";
|
||||||
|
import { hash, verify } from "argon2";
|
||||||
|
import { validatePassword } from "../utils/password";
|
||||||
|
|
||||||
export async function authRoutes(fastify: FastifyInstance) {
|
export async function authRoutes(fastify: FastifyInstance) {
|
||||||
fastify.get(
|
fastify.post(
|
||||||
"/microsoft/token",
|
"/register",
|
||||||
{},
|
{
|
||||||
|
schema: {
|
||||||
|
body: {
|
||||||
|
type: "object",
|
||||||
|
required: ["password", "token"],
|
||||||
|
properties: {
|
||||||
|
password: { type: "string" },
|
||||||
|
token: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
async (req: FastifyRequest, res: FastifyReply) => {
|
async (req: FastifyRequest, res: FastifyReply) => {
|
||||||
try {
|
const { password, token } = req.body as {
|
||||||
const { token } =
|
password: string;
|
||||||
await fastify.microsoftOauth.getAccessTokenFromAuthorizationCodeFlow(
|
token: string;
|
||||||
req
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = (await fastify.microsoftOauth.userinfo(token)) as {
|
|
||||||
givenname: string;
|
|
||||||
familyname: string;
|
|
||||||
email: string;
|
|
||||||
picture: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const userInDb = await getUserByEmail(user.email);
|
try {
|
||||||
if (userInDb == null)
|
const userInDB = await getUserByToken(token);
|
||||||
return res.code(401).send({ error: "not_allowed" });
|
if (!userInDB) return res.code(404).send({ error: "not found" });
|
||||||
|
if (new Date() > userInDB.token.expiry)
|
||||||
|
return res.code(400).send({ error: "link expired" });
|
||||||
|
|
||||||
await updateUserInternal(userInDb.pid, {
|
if (!validatePassword(password))
|
||||||
firstName: user.givenname,
|
return res.code(400).send({
|
||||||
lastName: user.familyname,
|
error:
|
||||||
email: user.email,
|
"weak password. Make sure the password has atleast 2 uppercase, 2 lowercase, 2 numeric and 2 special characters",
|
||||||
avatar: user.picture,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const session = await createSession(
|
const hashedPassword = await hash(password);
|
||||||
userInDb.id,
|
userInDB.passwordHash = hashedPassword;
|
||||||
|
|
||||||
|
await userInDB.save();
|
||||||
|
} catch (err) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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.ip,
|
||||||
req.headers["user-agent"]
|
req.headers["user-agent"]
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.code(201).send({ session_token: session.sid });
|
userInDB.lastLogin = new Date();
|
||||||
|
await userInDB.save();
|
||||||
|
|
||||||
|
res.send({ session_token: newSession.sid });
|
||||||
} catch (err) {
|
} 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) => {
|
fastify.delete(
|
||||||
|
"/logout",
|
||||||
|
{},
|
||||||
|
async (req: FastifyRequest, res: FastifyReply) => {
|
||||||
if (!req.headers.authorization) return res.code(200).send();
|
if (!req.headers.authorization) return res.code(200).send();
|
||||||
|
|
||||||
const auth = req.headers.authorization.split(" ")[1];
|
const auth = req.headers.authorization.split(" ")[1];
|
||||||
await deleteSession(auth);
|
await deleteSession(auth);
|
||||||
return res.code(200).send();
|
return res.code(200).send();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const userSchema = new mongoose.Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
passwordHash: String,
|
||||||
passKeys: [new mongoose.Schema({}, { _id: false, strict: false })],
|
passKeys: [new mongoose.Schema({}, { _id: false, strict: false })],
|
||||||
challenge: new mongoose.Schema(
|
challenge: new mongoose.Schema(
|
||||||
{
|
{
|
||||||
@@ -68,6 +69,7 @@ const userCore = {
|
|||||||
avatar: z.string().optional(),
|
avatar: z.string().optional(),
|
||||||
role: z.enum(roles),
|
role: z.enum(roles),
|
||||||
orgId: z.string().optional(),
|
orgId: z.string().optional(),
|
||||||
|
password: z.string().optional(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const createUserInput = z
|
const createUserInput = z
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CreateUserInput, UpdateUserInput, userModel } from "./user.schema";
|
|||||||
import { sendMail } from "../utils/mail";
|
import { sendMail } from "../utils/mail";
|
||||||
import { AuthenticatedUser } from "../auth";
|
import { AuthenticatedUser } from "../auth";
|
||||||
import { createUserConfig } from "../userConfig/userConfig.service";
|
import { createUserConfig } from "../userConfig/userConfig.service";
|
||||||
|
import { hash } from "argon2";
|
||||||
|
|
||||||
export const ErrUserNotFound = new Error("user not found");
|
export const ErrUserNotFound = new Error("user not found");
|
||||||
export const ErrOpNotValid = new Error("operation is not valid");
|
export const ErrOpNotValid = new Error("operation is not valid");
|
||||||
@@ -26,7 +27,8 @@ export async function createUser(
|
|||||||
throw ErrMissingOrdId;
|
throw ErrMissingOrdId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await generateToken();
|
let hashedPassword = "";
|
||||||
|
if (input.password) hashedPassword = await hash(input.password);
|
||||||
|
|
||||||
const newUser = await userModel.create({
|
const newUser = await userModel.create({
|
||||||
tenantId: user.tenantId,
|
tenantId: user.tenantId,
|
||||||
@@ -34,16 +36,25 @@ export async function createUser(
|
|||||||
name: input.firstName + " " + input.lastName,
|
name: input.firstName + " " + input.lastName,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
createdBy: user.userId,
|
createdBy: user.userId,
|
||||||
token: {
|
status: input.password ? "active" : "invited",
|
||||||
value: token,
|
passwordHash: hashedPassword,
|
||||||
expiry: new Date(Date.now() + 3600 * 48 * 1000),
|
|
||||||
},
|
|
||||||
status: "invited",
|
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|
||||||
await createUserConfig(newUser.id, newUser.tenantId);
|
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(
|
const sent = await sendMail(
|
||||||
input.email,
|
input.email,
|
||||||
"You have been invited to Quicker Permtis.",
|
"You have been invited to Quicker Permtis.",
|
||||||
|
|||||||
@@ -17,13 +17,3 @@ export async function generateToken(): Promise<string> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateOTP() {
|
|
||||||
let otp = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
otp += crypto.randomInt(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseInt(otp);
|
|
||||||
}
|
|
||||||
|
|||||||
25
src/utils/password.ts
Normal file
25
src/utils/password.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user