Add session management

This commit is contained in:
2025-01-03 12:32:43 +05:30
parent 83786e2994
commit 14c9b0210c
12 changed files with 341 additions and 16 deletions

View File

@@ -2,8 +2,11 @@ import bcrypt from "bcrypt";
import { FastifyReply, FastifyRequest } from "fastify";
import { getToken } from "./tokens/token.service";
import { Claim } from "./utils/claims";
import { OAuth2Namespace } from "@fastify/oauth2";
import { getSession } from "./auth/auth.service";
export type AuthenticatedUser = {
sid?: string;
userId?: string;
tenantId: string;
claims: Array<Claim>;
@@ -16,6 +19,7 @@ declare module "fastify" {
export interface FastifyInstance {
authorize: (req: FastifyRequest, res: FastifyReply) => Promise<unknown>;
microsoftOauth: OAuth2Namespace;
}
export interface FastifyContextConfig {
@@ -26,19 +30,46 @@ declare module "fastify" {
export async function authHandler(req: FastifyRequest, res: FastifyReply) {
if (!req.headers.authorization) return res.code(401).send();
const [tokenId, token] = req.headers.authorization.split(" ")[1].split(".");
if (!tokenId || !token) return res.code(401).send({ error: "invalid token" });
const authHeader = req.headers.authorization.split(" ")[1];
if (!authHeader || authHeader == "")
return res.code(401).send({ error: "invalid_token" });
const tokenInDb = await getToken(tokenId);
if (tokenInDb === null) return res.code(401).send({ error: "invalid token" });
if (authHeader.includes(".")) {
const [tokenId, token] = authHeader.split(".");
if (!tokenId || !token)
return res.code(401).send({ error: "invalid_token" });
const valid = await bcrypt.compare(token, tokenInDb.hash);
if (!valid) return res.code(401).send({ error: "invalid token" });
const tokenInDb = await getToken(tokenId);
if (tokenInDb === null)
return res.code(401).send({ error: "invalid_token" });
req.user = {
tenantId: tokenInDb.tenantId,
claims: tokenInDb.claims as Array<Claim>,
};
const valid = await bcrypt.compare(token, tokenInDb.hash);
if (!valid) return res.code(401).send({ error: "invalid_token" });
req.user = {
tenantId: tokenInDb.tenantId,
claims: tokenInDb.claims as Array<Claim>,
};
} else {
const sessionInDb = await getSession(authHeader);
if (sessionInDb === null)
return res.code(401).send({ error: "invalid_token" });
if (new Date() > new Date(sessionInDb.expiresAt)) {
await sessionInDb.deleteOne();
return res.code(401).send({ error: "session_expired" });
}
req.user = {
sid: authHeader,
//@ts-ignore
userId: sessionInDb.user.id,
//@ts-ignore
tenantId: sessionInDb.user.tenantId,
//@ts-ignore
claims: sessionInDb.user.claims,
};
}
}
export function hasValidClaims(

64
src/auth/auth.route.ts Normal file
View File

@@ -0,0 +1,64 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { getUserByEmail, updateUser } from "../user/user.service";
import { createSession, getSession } from "./auth.service";
export async function authRoutes(fastify: FastifyInstance) {
fastify.get(
"/microsoft/token",
{},
async (req: FastifyRequest, res: FastifyReply) => {
try {
const { token } =
await fastify.microsoftOauth.getAccessTokenFromAuthorizationCodeFlow(
req
);
const user = (await fastify.microsoftOauth.userinfo(token)) as {
givenname: string;
familyname: string;
email: string;
picture: string;
};
const userInDb = await getUserByEmail(user.email);
if (userInDb == null)
return res.code(401).send({ error: "not_allowed" });
await updateUser(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 });
} catch (err) {
//@ts-ignore
if (err.data) {
//@ts-ignore
fastify.log.warn(err.data.payload);
return res.code(400).send();
} else {
return err;
}
}
}
);
fastify.delete("/logout", {}, async (req, res) => {
if (!req.headers.authorization) return res.code(200).send();
const auth = req.headers.authorization.split(" ")[1];
const sessionInDb = await getSession(auth);
if (sessionInDb === null) return res.code(200).send();
await sessionInDb.deleteOne();
return res.code(200).send();
});
}

20
src/auth/auth.schema.ts Normal file
View File

@@ -0,0 +1,20 @@
import mongoose from "mongoose";
export const sessionModel = mongoose.model(
"session",
new mongoose.Schema({
sid: {
type: String,
unique: true,
required: true,
},
user: { type: mongoose.Types.ObjectId, ref: "user" },
ip: String,
userAgent: String,
createdAt: Date,
expiresAt: {
type: Date,
required: true,
},
})
);

24
src/auth/auth.service.ts Normal file
View File

@@ -0,0 +1,24 @@
import { generateToken } from "../utils/id";
import { sessionModel } from "./auth.schema";
export async function createSession(userId: string, ip?: string, ua?: string) {
const allUserSessions = await sessionModel.find({ user: userId });
for (const session of allUserSessions) {
await session.deleteOne();
}
const newSession = await sessionModel.create({
sid: await generateToken(),
user: userId,
ip: ip,
userAgent: ua,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 3600 * 24 * 30 * 1000),
});
return newSession;
}
export async function getSession(sessionId: string) {
return await sessionModel.findOne({ sid: sessionId }).populate("user");
}

24
src/oauth.ts Normal file
View File

@@ -0,0 +1,24 @@
import oauthPlugin, { FastifyOAuth2Options } from "@fastify/oauth2";
import { FastifyInstance } from "fastify";
export async function oauth(fastify: FastifyInstance) {
fastify.register(oauthPlugin, {
name: "microsoftOauth",
scope: ["openid", "email", "profile"],
credentials: {
client: {
id: process.env.MICROSOFT_CLIENT_ID,
secret: process.env.MICROSOFT_CLIENT_SECRET,
},
},
startRedirectPath: "/auth/microsoft",
callbackUri: process.env.MICROSOFT_REDIRECT_URI,
discovery: {
issuer:
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
},
cookie: {
sameSite: "none",
},
} as FastifyOAuth2Options);
}

View File

@@ -38,7 +38,7 @@ export async function getOrgHandler(req: FastifyRequest, res: FastifyReply) {
export async function listOrgsHandler(req: FastifyRequest, res: FastifyReply) {
const queryParams = req.query as PageQueryParams;
console.log(req.user);
try {
const authUser = req.user;
const orgList = await listOrgs(queryParams, authUser.tenantId);

View File

@@ -1,4 +1,5 @@
import fastify from "fastify";
import cors from "@fastify/cors";
import multipart from "@fastify/multipart";
import routes from "./routes";
@@ -9,6 +10,8 @@ import { errorHandler } from "./utils/errors";
import { authorize } from "./auth";
import { permitSchemas } from "./permit/permit.schema";
import { fileSchemas } from "./file/file.schema";
import { oauth } from "./oauth";
import { authRoutes } from "./auth/auth.route";
const app = fastify({ logger: true });
@@ -16,9 +19,16 @@ app.get("/health", (req, res) => {
return { status: "OK" };
});
oauth(app);
app.decorate("authorize", authorize);
app.setErrorHandler(errorHandler);
app.register(cors, {
origin: [process.env.UI_DOMAIN || ""],
credentials: true,
});
app.register(multipart, { limits: { fileSize: 50000000 } });
app.register(authRoutes, { prefix: "/auth" });
app.register(routes, { prefix: "/api/v1" });
for (const schema of [

View File

@@ -21,8 +21,7 @@ export async function getUserHandler(req: FastifyRequest, res: FastifyReply) {
const { userId } = req.params as { userId: string };
try {
const authUser = req.user;
const user = await getUser(userId, authUser.tenantId);
const user = await getUser(userId);
if (user == null)
return res.code(404).send({ error: "resource not found" });

View File

@@ -24,6 +24,7 @@ export const userModel = mongoose.model(
},
avatar: String,
status: String,
claims: [String],
createdAt: Date,
createdBy: mongoose.Types.ObjectId,
lastLogin: Date,
@@ -40,6 +41,7 @@ const userCore = {
})
.email(),
avatar: z.string().url().optional(),
claims: z.array(z.string()).optional(),
};
const createUserInput = z.object({
@@ -51,7 +53,21 @@ const createUserResponse = z.object({
...userCore,
});
const updateUserInput = z.object({
firstName: z.string().max(30).optional(),
lastName: z.string().max(30).optional(),
email: z
.string({
required_error: "Email is required",
invalid_type_error: "Email must be a valid string",
})
.email()
.optional(),
avatar: z.string().url().optional(),
});
export type CreateUserInput = z.infer<typeof createUserInput>;
export type UpdateUserInput = z.infer<typeof updateUserInput>;
export const { schemas: userSchemas, $ref: $user } = buildJsonSchemas(
{

View File

@@ -1,5 +1,5 @@
import { generateId } from "../utils/id";
import { CreateUserInput, userModel } from "./user.schema";
import { CreateUserInput, UpdateUserInput, userModel } from "./user.schema";
export async function createUser(input: CreateUserInput, tenantId: string) {
const user = await userModel.create({
@@ -13,9 +13,17 @@ export async function createUser(input: CreateUserInput, tenantId: string) {
return user;
}
export async function getUser(userId: string, tenantId: string) {
export async function getUser(userId: string) {
const user = await userModel.findOne({
$and: [{ tenantId: tenantId }, { pid: userId }],
$and: [{ pid: userId }],
});
return user;
}
export async function getUserByEmail(email: string) {
return await userModel.findOne({ email: email });
}
export async function updateUser(userId: string, input: UpdateUserInput) {
return await userModel.findOneAndUpdate({ pid: userId }, input);
}