Add session management
This commit is contained in:
51
src/auth.ts
51
src/auth.ts
@@ -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
64
src/auth/auth.route.ts
Normal 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
20
src/auth/auth.schema.ts
Normal 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
24
src/auth/auth.service.ts
Normal 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
24
src/oauth.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user