Skip to content

Commit f609d8f

Browse files
authored
Migrate ArticleRouter to Authorization Middlewares (#2568)
1 parent 9b69118 commit f609d8f

File tree

3 files changed

+179
-123
lines changed

3 files changed

+179
-123
lines changed

apps/rpc/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export type { AppRouter } from "./app-router"
22
export type { Pageable } from "./query"
3+
4+
export type * from "./modules/article/article-router"

apps/rpc/src/middlewares.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ type MiddlewareFunction<TContextIn, TContextOut, TInputOut> = trpc.MiddlewareFun
1414
TInputOut
1515
>
1616

17-
type ContextWithPrincipal = Context & {
17+
type WithPrincipal = {
1818
principal: Exclude<Context["principal"], null>
1919
}
2020

21-
type ContextWithTransaction = Context & {
21+
type WithTransaction = {
2222
handle: DBHandle
2323
}
2424

@@ -27,8 +27,10 @@ type ContextWithTransaction = Context & {
2727
*
2828
* Optionally, specify the transaction isolation level, which defaults to read-commited (default in PostgreSQL).
2929
*/
30-
export function withDatabaseTransaction<TInput>(isolationLevel: Prisma.TransactionIsolationLevel = "ReadCommitted") {
31-
const handler: MiddlewareFunction<Context, ContextWithTransaction, TInput> = async ({ ctx, next }) => {
30+
export function withDatabaseTransaction<TContext extends Context, TInput>(
31+
isolationLevel: Prisma.TransactionIsolationLevel = "ReadCommitted"
32+
) {
33+
const handler: MiddlewareFunction<TContext, TContext & WithTransaction, TInput> = async ({ ctx, next }) => {
3234
return await ctx.prisma.$transaction(
3335
async (handle) => {
3436
return await next({
@@ -51,8 +53,8 @@ export function withDatabaseTransaction<TInput>(isolationLevel: Prisma.Transacti
5153
* Audit log entries are stored in the database for most mutations. We use the audit log to keep track of changes to
5254
* the application.
5355
*/
54-
export function withAuditLogEntry<TInput>() {
55-
const handler: MiddlewareFunction<ContextWithTransaction, ContextWithTransaction, TInput> = async ({ ctx, next }) => {
56+
export function withAuditLogEntry<TContext extends Context & WithTransaction, TInput>() {
57+
const handler: MiddlewareFunction<TContext, TContext & WithTransaction, TInput> = async ({ ctx, next }) => {
5658
if (ctx.principal !== null) {
5759
// We use a PostgreSQL configuration parameter, isolated to the current transaction to tell which user is
5860
// performing a change. Additionally, we have a PostgreSQL trigger on most tables to insert entries into the
@@ -69,8 +71,8 @@ export function withAuditLogEntry<TInput>() {
6971
}
7072

7173
/** tRPC Middleware to ensure the caller is signed in */
72-
export function withAuthentication<TInput>() {
73-
const handler: MiddlewareFunction<Context, ContextWithPrincipal, TInput> = async ({ ctx, next }) => {
74+
export function withAuthentication<TContext extends Context, TInput>() {
75+
const handler: MiddlewareFunction<TContext, TContext & WithPrincipal, TInput> = async ({ ctx, next }) => {
7476
ctx.authorize.requireSignIn()
7577
// SAFETY: the above call should ensure this.
7678
invariant(ctx.principal === null)
@@ -88,8 +90,8 @@ export function withAuthentication<TInput>() {
8890
*
8991
* See file /src/authorization.ts for more details on the authorization system.
9092
*/
91-
export function withAuthorization<TInput>(rule: Rule<TInput>) {
92-
const handler: MiddlewareFunction<Context, Context, TInput> = async ({ ctx, next, input }) => {
93+
export function withAuthorization<TContext extends Context, TInput>(rule: Rule<TInput>) {
94+
const handler: MiddlewareFunction<TContext, TContext, TInput> = async ({ ctx, next, input }) => {
9395
async function evaluate<TRuleInput>(rule: Rule<TRuleInput>, context: RuleContext<TRuleInput>): Promise<boolean> {
9496
return await rule.evaluate(context)
9597
}
Lines changed: 165 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,187 @@
11
import type { PresignedPost } from "@aws-sdk/s3-presigned-post"
22
import { ArticleFilterQuerySchema, ArticleSchema, ArticleTagSchema, ArticleWriteSchema } from "@dotkomonline/types"
3+
import type { inferProcedureInput, inferProcedureOutput } from "@trpc/server"
34
import { z } from "zod"
45
import { isEditor } from "../../authorization"
56
import { withAuditLogEntry, withAuthentication, withAuthorization, withDatabaseTransaction } from "../../middlewares"
67
import { BasePaginateInputSchema, PaginateInputSchema } from "../../query"
7-
import { procedure, staffProcedure, t } from "../../trpc"
8+
import { procedure, t } from "../../trpc"
89

9-
export const articleRouter = t.router({
10-
create: procedure
11-
.input(
12-
z.object({
13-
article: ArticleWriteSchema,
14-
tags: z.array(ArticleTagSchema.shape.name),
15-
})
16-
)
17-
.use(withAuthentication())
18-
.use(withAuthorization(isEditor()))
19-
.use(withDatabaseTransaction())
20-
.use(withAuditLogEntry())
21-
.mutation(async ({ input, ctx }) => {
22-
const article = await ctx.articleService.create(ctx.handle, input.article)
23-
const tags = await ctx.articleService.setTags(ctx.handle, article.id, input.tags)
24-
return {
25-
...article,
26-
tags,
27-
}
28-
}),
10+
export type CreateArticleInput = inferProcedureInput<typeof createArticleProcedure>
11+
export type CreateArticleOutput = inferProcedureOutput<typeof createArticleProcedure>
12+
const createArticleProcedure = procedure
13+
.input(
14+
z.object({
15+
article: ArticleWriteSchema,
16+
tags: z.array(ArticleTagSchema.shape.name),
17+
})
18+
)
19+
.use(withAuthentication())
20+
.use(withAuthorization(isEditor()))
21+
.use(withDatabaseTransaction())
22+
.use(withAuditLogEntry())
23+
.mutation(async ({ input, ctx }) => {
24+
const article = await ctx.articleService.create(ctx.handle, input.article)
25+
const tags = await ctx.articleService.setTags(ctx.handle, article.id, input.tags)
26+
return {
27+
...article,
28+
tags,
29+
}
30+
})
2931

30-
edit: staffProcedure
31-
.input(
32-
z.object({
33-
id: ArticleSchema.shape.id,
34-
input: ArticleWriteSchema.partial(),
35-
tags: z.array(ArticleTagSchema.shape.name),
36-
})
37-
)
38-
.mutation(async ({ input, ctx }) => {
39-
return ctx.executeAuditedTransaction(async (handle) => {
40-
const article = await ctx.articleService.update(handle, input.id, input.input)
41-
const tags = await ctx.articleService.setTags(handle, input.id, input.tags)
42-
return { ...article, tags }
43-
})
44-
}),
32+
export type EditArticleInput = inferProcedureInput<typeof editArticleProcedure>
33+
export type EditArticleOutput = inferProcedureOutput<typeof editArticleProcedure>
34+
const editArticleProcedure = procedure
35+
.input(
36+
z.object({
37+
id: ArticleSchema.shape.id,
38+
input: ArticleWriteSchema.partial(),
39+
tags: z.array(ArticleTagSchema.shape.name),
40+
})
41+
)
42+
.use(withAuthentication())
43+
.use(withAuthorization(isEditor()))
44+
.use(withDatabaseTransaction())
45+
.use(withAuditLogEntry())
46+
.mutation(async ({ input, ctx }) => {
47+
const article = await ctx.articleService.update(ctx.handle, input.id, input.input)
48+
const tags = await ctx.articleService.setTags(ctx.handle, input.id, input.tags)
49+
return { ...article, tags }
50+
})
4551

46-
all: procedure
47-
.input(PaginateInputSchema)
48-
.query(async ({ input, ctx }) =>
49-
ctx.executeTransaction(async (handle) => ctx.articleService.findMany(handle, {}, input))
50-
),
52+
export type AllArticlesInput = inferProcedureInput<typeof allArticlesProcedure>
53+
export type AllArticlesOutput = inferProcedureOutput<typeof allArticlesProcedure>
54+
const allArticlesProcedure = procedure
55+
.input(PaginateInputSchema)
56+
.use(withDatabaseTransaction())
57+
.query(async ({ input, ctx }) => ctx.articleService.findMany(ctx.handle, {}, input))
5158

52-
findArticles: procedure
53-
.input(BasePaginateInputSchema.extend({ filters: ArticleFilterQuerySchema }))
54-
.query(async ({ input, ctx }) => {
55-
const items = await ctx.executeTransaction(async (handle) =>
56-
ctx.articleService.findMany(handle, input.filters, input)
57-
)
59+
export type FindArticlesInput = inferProcedureInput<typeof findArticlesProcedure>
60+
export type FindArticlesOutput = inferProcedureOutput<typeof findArticlesProcedure>
61+
const findArticlesProcedure = procedure
62+
.input(BasePaginateInputSchema.extend({ filters: ArticleFilterQuerySchema }))
63+
.use(withDatabaseTransaction())
64+
.query(async ({ input, ctx }) => {
65+
const items = await ctx.articleService.findMany(ctx.handle, input.filters, input)
5866

59-
return {
60-
items,
61-
nextCursor: items.at(-1)?.id,
62-
}
63-
}),
67+
return {
68+
items,
69+
nextCursor: items.at(-1)?.id,
70+
}
71+
})
6472

65-
find: procedure
66-
.input(ArticleSchema.shape.id)
67-
.query(async ({ input, ctx }) =>
68-
ctx.executeTransaction(async (handle) => ctx.articleService.findById(handle, input))
69-
),
73+
export type FindArticleInput = inferProcedureInput<typeof findArticleProcedure>
74+
export type FindArticleOutput = inferProcedureOutput<typeof findArticleProcedure>
75+
const findArticleProcedure = procedure
76+
.input(ArticleSchema.shape.id)
77+
.use(withDatabaseTransaction())
78+
.query(async ({ input, ctx }) => ctx.articleService.findById(ctx.handle, input))
7079

71-
get: procedure
72-
.input(ArticleSchema.shape.id)
73-
.query(async ({ input, ctx }) =>
74-
ctx.executeTransaction(async (handle) => ctx.articleService.getById(handle, input))
75-
),
80+
export type GetArticleInput = inferProcedureInput<typeof getArticleProcedure>
81+
export type GetArticleOutput = inferProcedureOutput<typeof getArticleProcedure>
82+
const getArticleProcedure = procedure
83+
.input(ArticleSchema.shape.id)
84+
.use(withDatabaseTransaction())
85+
.query(async ({ input, ctx }) => ctx.articleService.getById(ctx.handle, input))
7686

77-
related: procedure
78-
.input(ArticleSchema)
79-
.query(async ({ input, ctx }) =>
80-
ctx.executeTransaction(async (handle) => ctx.articleService.findRelated(handle, input))
81-
),
87+
export type FindRelatedArticlesInput = inferProcedureInput<typeof findRelatedArticlesProcedure>
88+
export type FindRelatedArticlesOutput = inferProcedureOutput<typeof findRelatedArticlesProcedure>
89+
const findRelatedArticlesProcedure = procedure
90+
.input(ArticleSchema)
91+
.use(withDatabaseTransaction())
92+
.query(async ({ input, ctx }) => ctx.articleService.findRelated(ctx.handle, input))
8293

83-
featured: procedure.query(async ({ ctx }) =>
84-
ctx.executeTransaction(async (handle) => ctx.articleService.findFeatured(handle))
85-
),
94+
export type FindFeaturedArticlesInput = inferProcedureInput<typeof findFeaturedArticlesProcedure>
95+
export type FindFeaturedArticlesOutput = inferProcedureOutput<typeof findFeaturedArticlesProcedure>
96+
const findFeaturedArticlesProcedure = procedure
97+
.use(withDatabaseTransaction())
98+
.query(async ({ ctx }) => ctx.articleService.findFeatured(ctx.handle))
8699

87-
getTags: procedure.query(async ({ ctx }) =>
88-
ctx.executeTransaction(async (handle) => ctx.articleService.getTags(handle))
89-
),
100+
export type GetArticleTagsInput = inferProcedureInput<typeof getArticleTagsProcedure>
101+
export type GetArticleTagsOutput = inferProcedureOutput<typeof getArticleTagsProcedure>
102+
const getArticleTagsProcedure = procedure
103+
.use(withDatabaseTransaction())
104+
.query(async ({ ctx }) => ctx.articleService.getTags(ctx.handle))
90105

91-
findTagsOrderedByPopularity: procedure.query(async ({ ctx }) =>
92-
ctx.executeTransaction(async (handle) => ctx.articleService.findTagsOrderedByPopularity(handle))
93-
),
106+
export type FindArticleTagsOrderedByPopularityInput = inferProcedureInput<
107+
typeof findArticleTagsOrderedByPopularityProcedure
108+
>
109+
export type FindArticleTagsOrderedByPopularityOutput = inferProcedureOutput<
110+
typeof findArticleTagsOrderedByPopularityProcedure
111+
>
112+
const findArticleTagsOrderedByPopularityProcedure = procedure
113+
.use(withDatabaseTransaction())
114+
.query(async ({ ctx }) => ctx.articleService.findTagsOrderedByPopularity(ctx.handle))
94115

95-
addTag: staffProcedure
96-
.input(
97-
z.object({
98-
id: ArticleSchema.shape.id,
99-
tag: ArticleTagSchema.shape.name,
100-
})
101-
)
102-
.mutation(async ({ input, ctx }) => {
103-
return ctx.executeAuditedTransaction(async (handle) => ctx.articleService.addTag(handle, input.id, input.tag))
104-
}),
116+
export type AddArticleTagInput = inferProcedureInput<typeof addArticleTagProcedure>
117+
export type AddArticleTagOutput = inferProcedureOutput<typeof addArticleTagProcedure>
118+
const addArticleTagProcedure = procedure
119+
.input(
120+
z.object({
121+
id: ArticleSchema.shape.id,
122+
tag: ArticleTagSchema.shape.name,
123+
})
124+
)
125+
.use(withAuthentication())
126+
.use(withAuthorization(isEditor()))
127+
.use(withDatabaseTransaction())
128+
.use(withAuditLogEntry())
129+
.mutation(async ({ input, ctx }) => {
130+
return ctx.articleService.addTag(ctx.handle, input.id, input.tag)
131+
})
105132

106-
removeTag: staffProcedure
107-
.input(
108-
z.object({
109-
id: ArticleSchema.shape.id,
110-
tag: ArticleTagSchema.shape.name,
111-
})
112-
)
113-
.mutation(async ({ input, ctx }) => {
114-
return ctx.executeAuditedTransaction(async (handle) => ctx.articleService.removeTag(handle, input.id, input.tag))
115-
}),
133+
export type RemoveArticleTagInput = inferProcedureInput<typeof removeArticleTagProcedure>
134+
export type RemoveArticleTagOutput = inferProcedureOutput<typeof removeArticleTagProcedure>
135+
const removeArticleTagProcedure = procedure
136+
.input(
137+
z.object({
138+
id: ArticleSchema.shape.id,
139+
tag: ArticleTagSchema.shape.name,
140+
})
141+
)
142+
.use(withAuthentication())
143+
.use(withAuthorization(isEditor()))
144+
.use(withDatabaseTransaction())
145+
.use(withAuditLogEntry())
146+
.mutation(async ({ input, ctx }) => {
147+
return ctx.articleService.removeTag(ctx.handle, input.id, input.tag)
148+
})
116149

117-
createFileUpload: staffProcedure
118-
.input(
119-
z.object({
120-
filename: z.string(),
121-
contentType: z.string(),
122-
})
150+
export type CreateArticleFileUploadInput = inferProcedureInput<typeof createArticleFileUploadProcedure>
151+
export type CreateArticleFileUploadOutput = inferProcedureOutput<typeof createArticleFileUploadProcedure>
152+
const createArticleFileUploadProcedure = procedure
153+
.input(
154+
z.object({
155+
filename: z.string(),
156+
contentType: z.string(),
157+
})
158+
)
159+
.output(z.custom<PresignedPost>())
160+
.use(withAuthentication())
161+
.use(withAuthorization(isEditor()))
162+
.use(withDatabaseTransaction())
163+
.use(withAuditLogEntry())
164+
.mutation(async ({ input, ctx }) => {
165+
return await ctx.articleService.createFileUpload(
166+
ctx.handle,
167+
input.filename,
168+
input.contentType,
169+
ctx.principal.subject
123170
)
124-
.output(z.custom<PresignedPost>())
125-
.mutation(async ({ input, ctx }) => {
126-
return ctx.executeTransaction(async (handle) => {
127-
return await ctx.articleService.createFileUpload(
128-
handle,
129-
input.filename,
130-
input.contentType,
131-
ctx.principal.subject
132-
)
133-
})
134-
}),
171+
})
172+
173+
export const articleRouter = t.router({
174+
create: createArticleProcedure,
175+
edit: editArticleProcedure,
176+
all: allArticlesProcedure,
177+
findArticles: findArticlesProcedure,
178+
find: findArticleProcedure,
179+
get: getArticleProcedure,
180+
related: findRelatedArticlesProcedure,
181+
featured: findFeaturedArticlesProcedure,
182+
getTags: getArticleTagsProcedure,
183+
findTagsOrderedByPopularity: findArticleTagsOrderedByPopularityProcedure,
184+
addTag: addArticleTagProcedure,
185+
removeTag: removeArticleTagProcedure,
186+
createFileUpload: createArticleFileUploadProcedure,
135187
})

0 commit comments

Comments
 (0)