mirror of
https://github.com/hanzoai/trpc-openapi.git
synced 2026-10-30 18:18:55 +00:00
428 lines
9.3 KiB
TypeScript
428 lines
9.3 KiB
TypeScript
import { TRPCError, initTRPC } from '@trpc/server';
|
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
import jwt from 'jsonwebtoken';
|
|
import { type OpenApiMeta } from 'trpc-to-openapi';
|
|
import { v4 as uuid } from 'uuid';
|
|
import { z } from 'zod';
|
|
|
|
import { type Post, type User, database } from './database';
|
|
|
|
const jwtSecret = uuid();
|
|
|
|
export type Context = {
|
|
user: User | null;
|
|
requestId: string;
|
|
};
|
|
|
|
const t = initTRPC
|
|
.context<Context>()
|
|
.meta<OpenApiMeta>()
|
|
.create({
|
|
errorFormatter: ({ error, shape }) => {
|
|
if (error.code === 'INTERNAL_SERVER_ERROR' && process.env.NODE_ENV === 'production') {
|
|
return { ...shape, message: 'Internal server error' };
|
|
}
|
|
return shape;
|
|
},
|
|
});
|
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
export const createContext = async ({
|
|
req,
|
|
res,
|
|
}: {
|
|
req: IncomingMessage;
|
|
res: ServerResponse<IncomingMessage>;
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
}) => {
|
|
const requestId = uuid();
|
|
res.setHeader('x-request-id', requestId);
|
|
|
|
let user: User | null = null;
|
|
|
|
try {
|
|
if (req.headers.authorization) {
|
|
const token = req.headers.authorization.split(' ')[1];
|
|
const userId = jwt.verify(token, jwtSecret) as string;
|
|
if (userId) {
|
|
user = database.users.find((_user) => _user.id === userId) ?? null;
|
|
}
|
|
}
|
|
} catch (cause) {
|
|
console.error(cause);
|
|
}
|
|
|
|
return { user, requestId };
|
|
};
|
|
|
|
const publicProcedure = t.procedure;
|
|
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
|
if (!ctx.user) {
|
|
throw new TRPCError({
|
|
message: 'User not found',
|
|
code: 'UNAUTHORIZED',
|
|
});
|
|
}
|
|
return next({ ctx: { ...ctx, user: ctx.user } });
|
|
});
|
|
|
|
const authRouter = t.router({
|
|
register: publicProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/auth/register',
|
|
tags: ['auth'],
|
|
summary: 'Register as a new user',
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
email: z.string().email(),
|
|
passcode: z.preprocess(
|
|
(arg) => (typeof arg === 'string' ? parseInt(arg) : arg),
|
|
z.number().min(1000).max(9999),
|
|
),
|
|
name: z.string().min(3),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
user: z.object({
|
|
id: z.string().uuid(),
|
|
email: z.string().email(),
|
|
name: z.string().min(3),
|
|
}),
|
|
}),
|
|
)
|
|
.mutation(({ input }) => {
|
|
let user = database.users.find((_user) => _user.email === input.email);
|
|
|
|
if (user) {
|
|
throw new TRPCError({
|
|
message: 'User with email already exists',
|
|
code: 'UNAUTHORIZED',
|
|
});
|
|
}
|
|
|
|
user = {
|
|
id: uuid(),
|
|
email: input.email,
|
|
passcode: input.passcode,
|
|
name: input.name,
|
|
};
|
|
|
|
database.users.push(user);
|
|
|
|
return { user: { id: user.id, email: user.email, name: user.name } };
|
|
}),
|
|
login: publicProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/auth/login',
|
|
tags: ['auth'],
|
|
summary: 'Login as an existing user',
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
email: z.string().email(),
|
|
passcode: z.preprocess(
|
|
(arg) => (typeof arg === 'string' ? parseInt(arg) : arg),
|
|
z.number().min(1000).max(9999),
|
|
),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
token: z.string(),
|
|
}),
|
|
)
|
|
.mutation(({ input }) => {
|
|
const user = database.users.find((_user) => _user.email === input.email);
|
|
|
|
if (!user) {
|
|
throw new TRPCError({
|
|
message: 'User with email not found',
|
|
code: 'UNAUTHORIZED',
|
|
});
|
|
}
|
|
if (user.passcode !== input.passcode) {
|
|
throw new TRPCError({
|
|
message: 'Passcode was incorrect',
|
|
code: 'UNAUTHORIZED',
|
|
});
|
|
}
|
|
|
|
return {
|
|
token: jwt.sign(user.id, jwtSecret),
|
|
};
|
|
}),
|
|
});
|
|
|
|
const usersRouter = t.router({
|
|
getUsers: publicProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'GET',
|
|
path: '/users',
|
|
tags: ['users'],
|
|
summary: 'Read all users',
|
|
},
|
|
})
|
|
.output(
|
|
z.object({
|
|
users: z.array(
|
|
z.object({
|
|
id: z.string().uuid(),
|
|
email: z.string().email(),
|
|
name: z.string(),
|
|
}),
|
|
),
|
|
}),
|
|
)
|
|
.query(() => {
|
|
const users = database.users.map((user) => ({
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
}));
|
|
|
|
return { users };
|
|
}),
|
|
getUserById: publicProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'GET',
|
|
path: '/users/{id}',
|
|
tags: ['users'],
|
|
summary: 'Read a user by id',
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
id: z.string().uuid(),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
user: z.object({
|
|
id: z.string().uuid(),
|
|
email: z.string().email(),
|
|
name: z.string(),
|
|
}),
|
|
}),
|
|
)
|
|
.query(({ input }) => {
|
|
const user = database.users.find((_user) => _user.id === input.id);
|
|
|
|
if (!user) {
|
|
throw new TRPCError({
|
|
message: 'User not found',
|
|
code: 'NOT_FOUND',
|
|
});
|
|
}
|
|
|
|
return { user };
|
|
}),
|
|
});
|
|
|
|
const postsRouter = t.router({
|
|
getPosts: publicProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'GET',
|
|
path: '/posts',
|
|
tags: ['posts'],
|
|
summary: 'Read all posts',
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
userId: z.string().uuid().optional(),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
posts: z.array(
|
|
z.object({
|
|
id: z.string().uuid(),
|
|
content: z.string(),
|
|
userId: z.string().uuid(),
|
|
}),
|
|
),
|
|
}),
|
|
)
|
|
.query(({ input }) => {
|
|
let posts: Post[] = database.posts;
|
|
|
|
if (input.userId) {
|
|
posts = posts.filter((post) => {
|
|
return post.userId === input.userId;
|
|
});
|
|
}
|
|
|
|
return { posts };
|
|
}),
|
|
getPostById: publicProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'GET',
|
|
path: '/posts/{id}',
|
|
tags: ['posts'],
|
|
summary: 'Read a post by id',
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
id: z.string().uuid(),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
post: z.object({
|
|
id: z.string().uuid(),
|
|
content: z.string(),
|
|
userId: z.string().uuid(),
|
|
}),
|
|
}),
|
|
)
|
|
.query(({ input }) => {
|
|
const post = database.posts.find((_post) => _post.id === input.id);
|
|
|
|
if (!post) {
|
|
throw new TRPCError({
|
|
message: 'Post not found',
|
|
code: 'NOT_FOUND',
|
|
});
|
|
}
|
|
|
|
return { post };
|
|
}),
|
|
createPost: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/posts',
|
|
tags: ['posts'],
|
|
protect: true,
|
|
summary: 'Create a new post',
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
content: z.string().min(1).max(140),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
post: z.object({
|
|
id: z.string().uuid(),
|
|
content: z.string(),
|
|
userId: z.string().uuid(),
|
|
}),
|
|
}),
|
|
)
|
|
.mutation(({ input, ctx }) => {
|
|
const post: Post = {
|
|
id: uuid(),
|
|
content: input.content,
|
|
userId: ctx.user.id,
|
|
};
|
|
|
|
database.posts.push(post);
|
|
|
|
return { post };
|
|
}),
|
|
updatePostById: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'PUT',
|
|
path: '/posts/{id}',
|
|
tags: ['posts'],
|
|
protect: true,
|
|
summary: 'Update an existing post',
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
id: z.string().uuid(),
|
|
content: z.string().min(1),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
post: z.object({
|
|
id: z.string().uuid(),
|
|
content: z.string(),
|
|
userId: z.string().uuid(),
|
|
}),
|
|
}),
|
|
)
|
|
.mutation(({ input, ctx }) => {
|
|
const post = database.posts.find((_post) => _post.id === input.id);
|
|
|
|
if (!post) {
|
|
throw new TRPCError({
|
|
message: 'Post not found',
|
|
code: 'NOT_FOUND',
|
|
});
|
|
}
|
|
if (post.userId !== ctx.user.id) {
|
|
throw new TRPCError({
|
|
message: 'Cannot edit post owned by other user',
|
|
code: 'FORBIDDEN',
|
|
});
|
|
}
|
|
|
|
post.content = input.content;
|
|
|
|
return { post };
|
|
}),
|
|
deletePostById: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'DELETE',
|
|
path: '/posts/{id}',
|
|
tags: ['posts'],
|
|
protect: true,
|
|
summary: 'Delete a post',
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
id: z.string().uuid(),
|
|
}),
|
|
)
|
|
.output(z.null())
|
|
.mutation(({ input, ctx }) => {
|
|
const post = database.posts.find((_post) => _post.id === input.id);
|
|
|
|
if (!post) {
|
|
throw new TRPCError({
|
|
message: 'Post not found',
|
|
code: 'NOT_FOUND',
|
|
});
|
|
}
|
|
if (post.userId !== ctx.user.id) {
|
|
throw new TRPCError({
|
|
message: 'Cannot delete post owned by other user',
|
|
code: 'FORBIDDEN',
|
|
});
|
|
}
|
|
|
|
database.posts = database.posts.filter((_post) => _post !== post);
|
|
|
|
return null;
|
|
}),
|
|
});
|
|
|
|
export const appRouter = t.router({
|
|
auth: authRouter,
|
|
users: usersRouter,
|
|
posts: postsRouter,
|
|
});
|
|
|
|
export type AppRouter = typeof appRouter;
|