Add authorization

This commit is contained in:
2024-12-20 13:17:53 +05:30
parent 4b49c43a0c
commit a584fc91b5
16 changed files with 112 additions and 58 deletions

View File

@@ -4,10 +4,25 @@ import { getToken } from "./tokens/token.service";
import { Claim } from "./utils/claims";
export type AuthenticatedUser = {
userId?: String;
userId?: string;
tenantId: string;
claims: Array<Claim>;
};
declare module "fastify" {
export interface FastifyRequest {
user: AuthenticatedUser;
}
export interface FastifyInstance {
authorize: (req: FastifyRequest, res: FastifyReply) => Promise<unknown>;
}
export interface FastifyContextConfig {
requiredClaims: Claim[];
}
}
export async function authHandler(req: FastifyRequest, res: FastifyReply) {
if (!req.headers.authorization) return res.code(401).send();
@@ -21,6 +36,32 @@ export async function authHandler(req: FastifyRequest, res: FastifyReply) {
if (!valid) return res.code(401).send({ error: "invalid token" });
req.user = {
tenantId: tokenInDb.tenantId,
claims: tokenInDb.claims as Array<Claim>,
};
}
export function hasValidClaims(
user: AuthenticatedUser,
requiredClaims: Claim[]
): boolean {
let isValid = true;
for (const claim of requiredClaims) {
if (!user.claims.includes(claim)) {
isValid = false;
break;
}
}
return isValid;
}
export async function authorize(req: FastifyRequest, res: FastifyReply) {
const { requiredClaims } = req.routeOptions.config;
const authUser = req.user;
if (!hasValidClaims(authUser, requiredClaims))
return res
.code(401)
.send({ error: "Missing permissions", params: requiredClaims });
}

View File

@@ -2,28 +2,24 @@ import { FastifyRequest, FastifyReply } from "fastify";
import { CreateOrgInput } from "./organization.schema";
import { createOrg, getOrg } from "./organization.service";
export async function createOrgHandler(
req: FastifyRequest<{ Body: CreateOrgInput }>,
res: FastifyReply
) {
const input = req.body;
export async function createOrgHandler(req: FastifyRequest, res: FastifyReply) {
const input = req.body as CreateOrgInput;
try {
const org = await createOrg(input);
const authUser = req.user;
const org = await createOrg(input, authUser.tenantId);
return res.code(201).send(org);
} catch (err) {
return err;
}
}
export async function getOrgHandler(
req: FastifyRequest<{ Params: { orgId: string } }>,
res: FastifyReply
) {
const { orgId } = req.params;
export async function getOrgHandler(req: FastifyRequest, res: FastifyReply) {
const { orgId } = req.params as { orgId: string };
try {
const org = await getOrg(orgId);
const authUser = req.user;
const org = await getOrg(orgId, authUser.tenantId);
if (org === null)
return res.code(404).send({ error: "resource not found" });

View File

@@ -12,6 +12,8 @@ export default function organizationRoutes(fastify: FastifyInstance) {
201: $org("createOrgResponse"),
},
},
config: { requiredClaims: ["org:write"] },
preHandler: [fastify.authorize],
},
createOrgHandler
);
@@ -27,6 +29,7 @@ export default function organizationRoutes(fastify: FastifyInstance) {
},
},
},
config: { requiredClaims: ["org:read"] },
},
getOrgHandler
);

View File

@@ -5,7 +5,10 @@ import { z } from "zod";
export const orgModel = mongoose.model(
"organization",
new mongoose.Schema({
tenantId: String,
tenantId: {
type: String,
required: true,
},
pid: {
type: String,
unique: true,
@@ -13,7 +16,6 @@ export const orgModel = mongoose.model(
name: String,
domain: {
type: String,
unique: true,
},
avatar: String,
type: String,

View File

@@ -1,9 +1,9 @@
import { generateId } from "../utils/id";
import { CreateOrgInput, orgModel } from "./organization.schema";
export async function createOrg(input: CreateOrgInput) {
export async function createOrg(input: CreateOrgInput, tenantId: string) {
const org = await orgModel.create({
tenantId: "abc",
tenantId: tenantId,
pid: generateId(),
createdAt: new Date(),
...input,
@@ -12,6 +12,8 @@ export async function createOrg(input: CreateOrgInput) {
return org;
}
export async function getOrg(orgId: string) {
return await orgModel.findOne({ pid: orgId });
export async function getOrg(orgId: string, tenantId: string) {
return await orgModel.findOne({
$and: [{ tenantId: tenantId }, { pid: orgId }],
});
}

View File

@@ -6,13 +6,7 @@ import { userSchemas } from "./user/user.schema";
import { orgSchemas } from "./organization/organization.schema";
import { tokenSchemas } from "./tokens/token.schema";
import { errorHandler } from "./utils/errors";
import { AuthenticatedUser, authHandler } from "./auth";
declare module "fastify" {
export interface FastifyRequest {
user: AuthenticatedUser;
}
}
import { authHandler, authorize } from "./auth";
const app = fastify({ logger: true });
@@ -20,9 +14,10 @@ app.get("/health", (req, res) => {
return { status: "OK" };
});
app.register(routes, { prefix: "/api/v1" });
app.decorate("authorize", authorize);
app.setErrorHandler(errorHandler);
app.addHook("onRequest", authHandler);
app.register(routes, { prefix: "/api/v1" });
for (const schema of [...userSchemas, ...orgSchemas, ...tokenSchemas]) {
app.addSchema(schema);

View File

@@ -3,24 +3,22 @@ import { CreateTokenInput } from "./token.schema";
import { createToken, getToken } from "./token.service";
export async function createTokenHandler(
req: FastifyRequest<{ Body: CreateTokenInput }>,
req: FastifyRequest,
res: FastifyReply
) {
const input = req.body;
const input = req.body as CreateTokenInput;
try {
const result = await createToken(input);
const authUser = req.user;
const result = await createToken(input, authUser.tenantId);
return res.code(201).send(result);
} catch (err) {
return err;
}
}
export async function getTokenHandler(
req: FastifyRequest<{ Params: { tokenId: string } }>,
res: FastifyReply
) {
const { tokenId } = req.params;
export async function getTokenHandler(req: FastifyRequest, res: FastifyReply) {
const { tokenId } = req.params as { tokenId: string };
try {
const token = await getToken(tokenId);

View File

@@ -12,13 +12,19 @@ export async function tokenRoutes(fastify: FastifyInstance) {
201: $token("createTokenResponse"),
},
},
config: { requiredClaims: ["token:write"] },
preHandler: [fastify.authorize],
},
createTokenHandler
);
fastify.get(
"/:tokenId",
{ schema: { response: { 200: $token("getTokenResponse") } } },
{
schema: { response: { 200: $token("getTokenResponse") } },
config: { requiredClaims: ["token:read"] },
preHandler: [fastify.authorize],
},
getTokenHandler
);
}

View File

@@ -1,12 +1,14 @@
import { buildJsonSchemas } from "fastify-zod";
import mongoose from "mongoose";
import { z } from "zod";
import { Claim } from "../utils/claims";
export const tokenModel = mongoose.model(
"token",
new mongoose.Schema({
tenantId: String,
tenantId: {
type: String,
required: true,
},
pid: {
type: String,
unique: true,

View File

@@ -2,12 +2,13 @@ import bcrypt from "bcrypt";
import { generateId, generateToken } from "../utils/id";
import { CreateTokenInput, tokenModel } from "./token.schema";
export async function createToken(input: CreateTokenInput) {
export async function createToken(input: CreateTokenInput, tenantId: string) {
const tokenId = generateId();
const newToken = await generateToken();
const tokenHash = await bcrypt.hash(newToken, 10);
const tokenInDb = await tokenModel.create({
tenantId: tenantId,
pid: tokenId,
hash: tokenHash,
createdAt: new Date(),

View File

@@ -3,29 +3,26 @@ import { createUser, getUser } from "./user.service";
import { CreateUserInput } from "./user.schema";
export async function createUserHandler(
req: FastifyRequest<{
Body: CreateUserInput;
}>,
req: FastifyRequest,
res: FastifyReply
) {
const body = req.body;
const body = req.body as CreateUserInput;
try {
const user = await createUser(body);
const authUser = req.user;
const user = await createUser(body, authUser.tenantId);
return res.code(201).send(user);
} catch (err) {
return err;
}
}
export async function getUserHandler(
req: FastifyRequest<{ Params: { userId: string } }>,
res: FastifyReply
) {
const { userId } = req.params;
export async function getUserHandler(req: FastifyRequest, res: FastifyReply) {
const { userId } = req.params as { userId: string };
try {
const user = await getUser(userId);
const authUser = req.user;
const user = await getUser(userId, authUser.tenantId);
if (user == null)
return res.code(404).send({ error: "resource not found" });

View File

@@ -12,6 +12,8 @@ export default async function userRoutes(fastify: FastifyInstance) {
201: $user("createUserResponse"),
},
},
config: { requiredClaims: ["user:write"] },
preHandler: [fastify.authorize],
},
createUserHandler
);
@@ -24,6 +26,8 @@ export default async function userRoutes(fastify: FastifyInstance) {
200: $user("createUserResponse"),
},
},
config: { requiredClaims: ["user:read"] },
preHandler: [fastify.authorize],
},
getUserHandler
);

View File

@@ -5,16 +5,21 @@ import { z } from "zod";
export const userModel = mongoose.model(
"user",
new mongoose.Schema({
tenantId: String,
tenantId: {
type: String,
required: true,
},
pid: {
type: String,
unique: true,
required: true,
},
firstName: String,
lastName: String,
email: {
type: String,
unique: true,
required: true,
},
avatar: String,
status: String,

View File

@@ -1,9 +1,9 @@
import { generateId } from "../utils/id";
import { CreateUserInput, userModel } from "./user.schema";
export async function createUser(input: CreateUserInput) {
export async function createUser(input: CreateUserInput, tenantId: string) {
const user = await userModel.create({
tenantId: "abc",
tenantId: tenantId,
pid: generateId(),
createdAt: new Date(),
...input,
@@ -12,7 +12,9 @@ export async function createUser(input: CreateUserInput) {
return user;
}
export async function getUser(userId: string) {
const user = await userModel.findOne({ pid: userId });
export async function getUser(userId: string, tenantId: string) {
const user = await userModel.findOne({
$and: [{ tenantId: tenantId }, { pid: userId }],
});
return user;
}

View File

@@ -10,6 +10,6 @@ export type Claim =
| "permit:delete"
| "file:upload"
| "file:download"
| "api:read"
| "api:write"
| "api:delete";
| "token:read"
| "token:write"
| "token:delete";