Added token authentication, organization module. Moved server bootstrapping code to server.ts file

This commit is contained in:
2024-12-19 21:49:54 +05:30
parent 970a972b11
commit 4b49c43a0c
16 changed files with 652 additions and 22 deletions

26
src/auth.ts Normal file
View File

@@ -0,0 +1,26 @@
import bcrypt from "bcrypt";
import { FastifyReply, FastifyRequest } from "fastify";
import { getToken } from "./tokens/token.service";
import { Claim } from "./utils/claims";
export type AuthenticatedUser = {
userId?: String;
claims: Array<Claim>;
};
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 tokenInDb = await getToken(tokenId);
if (tokenInDb === null) 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" });
req.user = {
claims: tokenInDb.claims as Array<Claim>,
};
}

View File

@@ -1,28 +1,10 @@
import mongoose from "mongoose";
import fastify from "fastify";
import routes from "./routes";
import { userSchemas } from "./user/user.schema";
import { orgSchemas } from "./organization/organization.schema";
import { errorHandler } from "./utils/errors";
const app = fastify({ logger: true });
app.get("/health", (req, res) => {
return { status: "OK" };
});
app.register(routes, { prefix: "/api/v1" });
app.setErrorHandler(errorHandler);
import app from "./server";
(async () => {
const PORT = parseInt(process.env.PORT ?? "8000");
const DB_URI = process.env.DB_URI ?? "";
for (const schema of [...userSchemas, ...orgSchemas]) {
app.addSchema(schema);
}
await mongoose.connect(DB_URI);
await app.listen({ port: PORT });
})().catch((err) => {

View File

@@ -1,6 +1,6 @@
import { FastifyRequest, FastifyReply } from "fastify";
import { CreateOrgInput } from "./organization.schema";
import { createOrg } from "./organization.service";
import { createOrg, getOrg } from "./organization.service";
export async function createOrgHandler(
req: FastifyRequest<{ Body: CreateOrgInput }>,
@@ -15,3 +15,20 @@ export async function createOrgHandler(
return err;
}
}
export async function getOrgHandler(
req: FastifyRequest<{ Params: { orgId: string } }>,
res: FastifyReply
) {
const { orgId } = req.params;
try {
const org = await getOrg(orgId);
if (org === null)
return res.code(404).send({ error: "resource not found" });
return res.code(200).send(org);
} catch (err) {
return err;
}
}

View File

@@ -1,6 +1,6 @@
import { FastifyInstance } from "fastify";
import { $org } from "./organization.schema";
import { createOrgHandler } from "./organization.controller";
import { createOrgHandler, getOrgHandler } from "./organization.controller";
export default function organizationRoutes(fastify: FastifyInstance) {
fastify.post(
@@ -15,4 +15,19 @@ export default function organizationRoutes(fastify: FastifyInstance) {
},
createOrgHandler
);
fastify.get(
"/:orgId",
{
schema: {
params: {
type: "object",
properties: {
orgId: { type: "string" },
},
},
},
},
getOrgHandler
);
}

View File

@@ -11,3 +11,7 @@ export async function createOrg(input: CreateOrgInput) {
return org;
}
export async function getOrg(orgId: string) {
return await orgModel.findOne({ pid: orgId });
}

View File

@@ -1,8 +1,10 @@
import { FastifyInstance } from "fastify";
import userRoutes from "./user/user.route";
import organizationRoutes from "./organization/organization.route";
import { tokenRoutes } from "./tokens/token.route";
export default async function routes(fastify: FastifyInstance) {
fastify.register(userRoutes, { prefix: "/users" });
fastify.register(organizationRoutes, { prefix: "/orgs" });
fastify.register(tokenRoutes, { prefix: "/tokens" });
}

31
src/server.ts Normal file
View File

@@ -0,0 +1,31 @@
import mongoose from "mongoose";
import fastify from "fastify";
import routes from "./routes";
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;
}
}
const app = fastify({ logger: true });
app.get("/health", (req, res) => {
return { status: "OK" };
});
app.register(routes, { prefix: "/api/v1" });
app.setErrorHandler(errorHandler);
app.addHook("onRequest", authHandler);
for (const schema of [...userSchemas, ...orgSchemas, ...tokenSchemas]) {
app.addSchema(schema);
}
export default app;

View File

@@ -0,0 +1,33 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { CreateTokenInput } from "./token.schema";
import { createToken, getToken } from "./token.service";
export async function createTokenHandler(
req: FastifyRequest<{ Body: CreateTokenInput }>,
res: FastifyReply
) {
const input = req.body;
try {
const result = await createToken(input);
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;
try {
const token = await getToken(tokenId);
if (token === null) return res.code(404).send();
return res.code(200).send(token);
} catch (err) {
return err;
}
}

24
src/tokens/token.route.ts Normal file
View File

@@ -0,0 +1,24 @@
import { FastifyInstance } from "fastify";
import { createTokenHandler, getTokenHandler } from "./token.controller";
import { $token } from "./token.schema";
export async function tokenRoutes(fastify: FastifyInstance) {
fastify.post(
"/",
{
schema: {
body: $token("createTokenInput"),
response: {
201: $token("createTokenResponse"),
},
},
},
createTokenHandler
);
fastify.get(
"/:tokenId",
{ schema: { response: { 200: $token("getTokenResponse") } } },
getTokenHandler
);
}

View File

@@ -0,0 +1,52 @@
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,
pid: {
type: String,
unique: true,
},
name: String,
claims: [],
userId: mongoose.Types.ObjectId,
hash: { type: String, required: true },
createdAt: Date,
})
);
const tokenCore = {
name: z.string().max(30),
claims: z.array(z.string()).nonempty(),
};
const createTokenInput = z.object({
...tokenCore,
});
const createTokenResponse = z.object({
pid: z.string(),
token: z.string(),
...tokenCore,
});
const getTokenResponse = z.object({
pid: z.string(),
name: z.string(),
createdAt: z.string(),
});
export type CreateTokenInput = z.infer<typeof createTokenInput>;
export const { schemas: tokenSchemas, $ref: $token } = buildJsonSchemas(
{
createTokenInput,
createTokenResponse,
getTokenResponse,
},
{ $id: "token" }
);

View File

@@ -0,0 +1,27 @@
import bcrypt from "bcrypt";
import { generateId, generateToken } from "../utils/id";
import { CreateTokenInput, tokenModel } from "./token.schema";
export async function createToken(input: CreateTokenInput) {
const tokenId = generateId();
const newToken = await generateToken();
const tokenHash = await bcrypt.hash(newToken, 10);
const tokenInDb = await tokenModel.create({
pid: tokenId,
hash: tokenHash,
createdAt: new Date(),
...input,
});
return {
pid: tokenInDb.pid,
name: tokenInDb.name,
claims: tokenInDb.claims,
token: `${tokenInDb.pid}.${newToken}`,
};
}
export async function getToken(tokenId: string) {
return await tokenModel.findOne({ pid: tokenId });
}

View File

@@ -26,7 +26,8 @@ export async function getUserHandler(
try {
const user = await getUser(userId);
if (user == null) return res.code(404).send({ error: "user not found" });
if (user == null)
return res.code(404).send({ error: "resource not found" });
return res.code(200).send(user);
} catch (err) {

15
src/utils/claims.ts Normal file
View File

@@ -0,0 +1,15 @@
export type Claim =
| "user:read"
| "user:write"
| "user:delete"
| "org:read"
| "org:write"
| "org:delete"
| "permit:read"
| "permit:write"
| "permit:delete"
| "file:upload"
| "file:download"
| "api:read"
| "api:write"
| "api:delete";

View File

@@ -1,4 +1,5 @@
import { init } from "@paralleldrive/cuid2";
import crypto from "crypto";
const id = init({
length: 15,
@@ -7,3 +8,12 @@ const id = init({
export function generateId(perfix?: string) {
return perfix ? perfix + id() : id();
}
export async function generateToken(): Promise<string> {
return new Promise((resolve, reject) => {
crypto.generateKey("aes", { length: 256 }, (err, key) => {
if (err) reject(err);
resolve(key.export().toString("base64url"));
});
});
}