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";
|
import { Claim } from "./utils/claims";
|
||||||
|
|
||||||
export type AuthenticatedUser = {
|
export type AuthenticatedUser = {
|
||||||
userId?: String;
|
userId?: string;
|
||||||
|
tenantId: string;
|
||||||
claims: Array<Claim>;
|
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) {
|
export async function authHandler(req: FastifyRequest, res: FastifyReply) {
|
||||||
if (!req.headers.authorization) return res.code(401).send();
|
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" });
|
if (!valid) return res.code(401).send({ error: "invalid token" });
|
||||||
|
|
||||||
req.user = {
|
req.user = {
|
||||||
|
tenantId: tokenInDb.tenantId,
|
||||||
claims: tokenInDb.claims as Array<Claim>,
|
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 { CreateOrgInput } from "./organization.schema";
|
||||||
import { createOrg, getOrg } from "./organization.service";
|
import { createOrg, getOrg } from "./organization.service";
|
||||||
|
|
||||||
export async function createOrgHandler(
|
export async function createOrgHandler(req: FastifyRequest, res: FastifyReply) {
|
||||||
req: FastifyRequest<{ Body: CreateOrgInput }>,
|
const input = req.body as CreateOrgInput;
|
||||||
res: FastifyReply
|
|
||||||
) {
|
|
||||||
const input = req.body;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const org = await createOrg(input);
|
const authUser = req.user;
|
||||||
|
const org = await createOrg(input, authUser.tenantId);
|
||||||
return res.code(201).send(org);
|
return res.code(201).send(org);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrgHandler(
|
export async function getOrgHandler(req: FastifyRequest, res: FastifyReply) {
|
||||||
req: FastifyRequest<{ Params: { orgId: string } }>,
|
const { orgId } = req.params as { orgId: string };
|
||||||
res: FastifyReply
|
|
||||||
) {
|
|
||||||
const { orgId } = req.params;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const org = await getOrg(orgId);
|
const authUser = req.user;
|
||||||
|
const org = await getOrg(orgId, authUser.tenantId);
|
||||||
if (org === null)
|
if (org === null)
|
||||||
return res.code(404).send({ error: "resource not found" });
|
return res.code(404).send({ error: "resource not found" });
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export default function organizationRoutes(fastify: FastifyInstance) {
|
|||||||
201: $org("createOrgResponse"),
|
201: $org("createOrgResponse"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
config: { requiredClaims: ["org:write"] },
|
||||||
|
preHandler: [fastify.authorize],
|
||||||
},
|
},
|
||||||
createOrgHandler
|
createOrgHandler
|
||||||
);
|
);
|
||||||
@@ -27,6 +29,7 @@ export default function organizationRoutes(fastify: FastifyInstance) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
config: { requiredClaims: ["org:read"] },
|
||||||
},
|
},
|
||||||
getOrgHandler
|
getOrgHandler
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { z } from "zod";
|
|||||||
export const orgModel = mongoose.model(
|
export const orgModel = mongoose.model(
|
||||||
"organization",
|
"organization",
|
||||||
new mongoose.Schema({
|
new mongoose.Schema({
|
||||||
tenantId: String,
|
tenantId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
pid: {
|
pid: {
|
||||||
type: String,
|
type: String,
|
||||||
unique: true,
|
unique: true,
|
||||||
@@ -13,7 +16,6 @@ export const orgModel = mongoose.model(
|
|||||||
name: String,
|
name: String,
|
||||||
domain: {
|
domain: {
|
||||||
type: String,
|
type: String,
|
||||||
unique: true,
|
|
||||||
},
|
},
|
||||||
avatar: String,
|
avatar: String,
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { generateId } from "../utils/id";
|
import { generateId } from "../utils/id";
|
||||||
import { CreateOrgInput, orgModel } from "./organization.schema";
|
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({
|
const org = await orgModel.create({
|
||||||
tenantId: "abc",
|
tenantId: tenantId,
|
||||||
pid: generateId(),
|
pid: generateId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
...input,
|
...input,
|
||||||
@@ -12,6 +12,8 @@ export async function createOrg(input: CreateOrgInput) {
|
|||||||
return org;
|
return org;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrg(orgId: string) {
|
export async function getOrg(orgId: string, tenantId: string) {
|
||||||
return await orgModel.findOne({ pid: orgId });
|
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 { orgSchemas } from "./organization/organization.schema";
|
||||||
import { tokenSchemas } from "./tokens/token.schema";
|
import { tokenSchemas } from "./tokens/token.schema";
|
||||||
import { errorHandler } from "./utils/errors";
|
import { errorHandler } from "./utils/errors";
|
||||||
import { AuthenticatedUser, authHandler } from "./auth";
|
import { authHandler, authorize } from "./auth";
|
||||||
|
|
||||||
declare module "fastify" {
|
|
||||||
export interface FastifyRequest {
|
|
||||||
user: AuthenticatedUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = fastify({ logger: true });
|
const app = fastify({ logger: true });
|
||||||
|
|
||||||
@@ -20,9 +14,10 @@ app.get("/health", (req, res) => {
|
|||||||
return { status: "OK" };
|
return { status: "OK" };
|
||||||
});
|
});
|
||||||
|
|
||||||
app.register(routes, { prefix: "/api/v1" });
|
app.decorate("authorize", authorize);
|
||||||
app.setErrorHandler(errorHandler);
|
app.setErrorHandler(errorHandler);
|
||||||
app.addHook("onRequest", authHandler);
|
app.addHook("onRequest", authHandler);
|
||||||
|
app.register(routes, { prefix: "/api/v1" });
|
||||||
|
|
||||||
for (const schema of [...userSchemas, ...orgSchemas, ...tokenSchemas]) {
|
for (const schema of [...userSchemas, ...orgSchemas, ...tokenSchemas]) {
|
||||||
app.addSchema(schema);
|
app.addSchema(schema);
|
||||||
|
|||||||
@@ -3,24 +3,22 @@ import { CreateTokenInput } from "./token.schema";
|
|||||||
import { createToken, getToken } from "./token.service";
|
import { createToken, getToken } from "./token.service";
|
||||||
|
|
||||||
export async function createTokenHandler(
|
export async function createTokenHandler(
|
||||||
req: FastifyRequest<{ Body: CreateTokenInput }>,
|
req: FastifyRequest,
|
||||||
res: FastifyReply
|
res: FastifyReply
|
||||||
) {
|
) {
|
||||||
const input = req.body;
|
const input = req.body as CreateTokenInput;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await createToken(input);
|
const authUser = req.user;
|
||||||
|
const result = await createToken(input, authUser.tenantId);
|
||||||
return res.code(201).send(result);
|
return res.code(201).send(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTokenHandler(
|
export async function getTokenHandler(req: FastifyRequest, res: FastifyReply) {
|
||||||
req: FastifyRequest<{ Params: { tokenId: string } }>,
|
const { tokenId } = req.params as { tokenId: string };
|
||||||
res: FastifyReply
|
|
||||||
) {
|
|
||||||
const { tokenId } = req.params;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = await getToken(tokenId);
|
const token = await getToken(tokenId);
|
||||||
|
|||||||
@@ -12,13 +12,19 @@ export async function tokenRoutes(fastify: FastifyInstance) {
|
|||||||
201: $token("createTokenResponse"),
|
201: $token("createTokenResponse"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
config: { requiredClaims: ["token:write"] },
|
||||||
|
preHandler: [fastify.authorize],
|
||||||
},
|
},
|
||||||
createTokenHandler
|
createTokenHandler
|
||||||
);
|
);
|
||||||
|
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/:tokenId",
|
"/:tokenId",
|
||||||
{ schema: { response: { 200: $token("getTokenResponse") } } },
|
{
|
||||||
|
schema: { response: { 200: $token("getTokenResponse") } },
|
||||||
|
config: { requiredClaims: ["token:read"] },
|
||||||
|
preHandler: [fastify.authorize],
|
||||||
|
},
|
||||||
getTokenHandler
|
getTokenHandler
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { buildJsonSchemas } from "fastify-zod";
|
import { buildJsonSchemas } from "fastify-zod";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Claim } from "../utils/claims";
|
|
||||||
|
|
||||||
export const tokenModel = mongoose.model(
|
export const tokenModel = mongoose.model(
|
||||||
"token",
|
"token",
|
||||||
new mongoose.Schema({
|
new mongoose.Schema({
|
||||||
tenantId: String,
|
tenantId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
pid: {
|
pid: {
|
||||||
type: String,
|
type: String,
|
||||||
unique: true,
|
unique: true,
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import bcrypt from "bcrypt";
|
|||||||
import { generateId, generateToken } from "../utils/id";
|
import { generateId, generateToken } from "../utils/id";
|
||||||
import { CreateTokenInput, tokenModel } from "./token.schema";
|
import { CreateTokenInput, tokenModel } from "./token.schema";
|
||||||
|
|
||||||
export async function createToken(input: CreateTokenInput) {
|
export async function createToken(input: CreateTokenInput, tenantId: string) {
|
||||||
const tokenId = generateId();
|
const tokenId = generateId();
|
||||||
const newToken = await generateToken();
|
const newToken = await generateToken();
|
||||||
const tokenHash = await bcrypt.hash(newToken, 10);
|
const tokenHash = await bcrypt.hash(newToken, 10);
|
||||||
|
|
||||||
const tokenInDb = await tokenModel.create({
|
const tokenInDb = await tokenModel.create({
|
||||||
|
tenantId: tenantId,
|
||||||
pid: tokenId,
|
pid: tokenId,
|
||||||
hash: tokenHash,
|
hash: tokenHash,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
|||||||
@@ -3,29 +3,26 @@ import { createUser, getUser } from "./user.service";
|
|||||||
import { CreateUserInput } from "./user.schema";
|
import { CreateUserInput } from "./user.schema";
|
||||||
|
|
||||||
export async function createUserHandler(
|
export async function createUserHandler(
|
||||||
req: FastifyRequest<{
|
req: FastifyRequest,
|
||||||
Body: CreateUserInput;
|
|
||||||
}>,
|
|
||||||
res: FastifyReply
|
res: FastifyReply
|
||||||
) {
|
) {
|
||||||
const body = req.body;
|
const body = req.body as CreateUserInput;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await createUser(body);
|
const authUser = req.user;
|
||||||
|
const user = await createUser(body, authUser.tenantId);
|
||||||
return res.code(201).send(user);
|
return res.code(201).send(user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserHandler(
|
export async function getUserHandler(req: FastifyRequest, res: FastifyReply) {
|
||||||
req: FastifyRequest<{ Params: { userId: string } }>,
|
const { userId } = req.params as { userId: string };
|
||||||
res: FastifyReply
|
|
||||||
) {
|
|
||||||
const { userId } = req.params;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await getUser(userId);
|
const authUser = req.user;
|
||||||
|
const user = await getUser(userId, authUser.tenantId);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return res.code(404).send({ error: "resource not found" });
|
return res.code(404).send({ error: "resource not found" });
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export default async function userRoutes(fastify: FastifyInstance) {
|
|||||||
201: $user("createUserResponse"),
|
201: $user("createUserResponse"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
config: { requiredClaims: ["user:write"] },
|
||||||
|
preHandler: [fastify.authorize],
|
||||||
},
|
},
|
||||||
createUserHandler
|
createUserHandler
|
||||||
);
|
);
|
||||||
@@ -24,6 +26,8 @@ export default async function userRoutes(fastify: FastifyInstance) {
|
|||||||
200: $user("createUserResponse"),
|
200: $user("createUserResponse"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
config: { requiredClaims: ["user:read"] },
|
||||||
|
preHandler: [fastify.authorize],
|
||||||
},
|
},
|
||||||
getUserHandler
|
getUserHandler
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,16 +5,21 @@ import { z } from "zod";
|
|||||||
export const userModel = mongoose.model(
|
export const userModel = mongoose.model(
|
||||||
"user",
|
"user",
|
||||||
new mongoose.Schema({
|
new mongoose.Schema({
|
||||||
tenantId: String,
|
tenantId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
pid: {
|
pid: {
|
||||||
type: String,
|
type: String,
|
||||||
unique: true,
|
unique: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
firstName: String,
|
firstName: String,
|
||||||
lastName: String,
|
lastName: String,
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
unique: true,
|
unique: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
avatar: String,
|
avatar: String,
|
||||||
status: String,
|
status: String,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { generateId } from "../utils/id";
|
import { generateId } from "../utils/id";
|
||||||
import { CreateUserInput, userModel } from "./user.schema";
|
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({
|
const user = await userModel.create({
|
||||||
tenantId: "abc",
|
tenantId: tenantId,
|
||||||
pid: generateId(),
|
pid: generateId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
...input,
|
...input,
|
||||||
@@ -12,7 +12,9 @@ export async function createUser(input: CreateUserInput) {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUser(userId: string) {
|
export async function getUser(userId: string, tenantId: string) {
|
||||||
const user = await userModel.findOne({ pid: userId });
|
const user = await userModel.findOne({
|
||||||
|
$and: [{ tenantId: tenantId }, { pid: userId }],
|
||||||
|
});
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ export type Claim =
|
|||||||
| "permit:delete"
|
| "permit:delete"
|
||||||
| "file:upload"
|
| "file:upload"
|
||||||
| "file:download"
|
| "file:download"
|
||||||
| "api:read"
|
| "token:read"
|
||||||
| "api:write"
|
| "token:write"
|
||||||
| "api:delete";
|
| "token:delete";
|
||||||
|
|||||||
Reference in New Issue
Block a user