Add authorization
This commit is contained in:
43
src/auth.ts
43
src/auth.ts
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user