feat: add cerbos access control
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import z, { ZodType } from "zod";
|
||||
|
||||
import { ServerError, ServerErrorClass } from "./errors.ts";
|
||||
import { Access, ServerContext } from "./types.ts";
|
||||
import { RouteAccess, ServerContext } from "./route.ts";
|
||||
|
||||
export class Procedure<const TState extends State = State> {
|
||||
readonly type = "procedure" as const;
|
||||
@@ -64,7 +64,7 @@ export class Procedure<const TState extends State = State> {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
access<TAccess extends Access>(access: TAccess): Procedure<Omit<TState, "access"> & { access: TAccess }> {
|
||||
access<TAccess extends RouteAccess>(access: TAccess): Procedure<Omit<TState, "access"> & { access: TAccess }> {
|
||||
return new Procedure({ ...this.state, access: access as TAccess });
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ export type Procedures = {
|
||||
|
||||
type State = {
|
||||
method: string;
|
||||
access?: Access;
|
||||
access?: RouteAccess;
|
||||
params?: ZodType;
|
||||
errors?: ServerErrorClass[];
|
||||
response?: ZodType;
|
||||
|
||||
@@ -3,7 +3,6 @@ import z, { ZodObject, ZodRawShape, ZodType } from "zod";
|
||||
|
||||
import { ServerError, ServerErrorClass } from "./errors.ts";
|
||||
import { Hooks } from "./hooks.ts";
|
||||
import { ServerContext } from "./types.ts";
|
||||
|
||||
export class Route<const TState extends RouteState = RouteState> {
|
||||
readonly type = "route" as const;
|
||||
@@ -81,7 +80,7 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
* route.post("/foo").meta({ description: "Super route" });
|
||||
* ```
|
||||
*/
|
||||
meta<TRouteMeta extends RouteMeta>(meta: TRouteMeta): Route<Omit<TState, "meta"> & { meta: TRouteMeta }> {
|
||||
meta<TRouteMeta extends RouteMeta>(meta: TRouteMeta): Route<Prettify<Omit<TState, "meta"> & { meta: TRouteMeta }>> {
|
||||
return new Route({ ...this.state, meta });
|
||||
}
|
||||
|
||||
@@ -134,7 +133,7 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
access<TAccess extends RouteAccess>(access: TAccess): Route<Omit<TState, "access"> & { access: TAccess }> {
|
||||
access<TAccess extends RouteAccess>(access: TAccess): Route<Prettify<Omit<TState, "access"> & { access: TAccess }>> {
|
||||
return new Route({ ...this.state, access: access as TAccess });
|
||||
}
|
||||
|
||||
@@ -157,7 +156,9 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
params<TParams extends ZodRawShape>(params: TParams): Route<Omit<TState, "params"> & { params: ZodObject<TParams> }> {
|
||||
params<TParams extends ZodRawShape>(
|
||||
params: TParams,
|
||||
): Route<Prettify<Omit<TState, "params"> & { params: ZodObject<TParams> }>> {
|
||||
return new Route({ ...this.state, params: z.object(params) as any });
|
||||
}
|
||||
|
||||
@@ -180,7 +181,9 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
query<TQuery extends ZodRawShape>(query: TQuery): Route<Omit<TState, "search"> & { query: ZodObject<TQuery> }> {
|
||||
query<TQuery extends ZodRawShape>(
|
||||
query: TQuery,
|
||||
): Route<Prettify<Omit<TState, "search"> & { query: ZodObject<TQuery> }>> {
|
||||
return new Route({ ...this.state, query: z.object(query) as any });
|
||||
}
|
||||
|
||||
@@ -205,7 +208,7 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
body<TBody extends ZodType>(body: TBody): Route<Omit<TState, "body"> & { body: TBody }> {
|
||||
body<TBody extends ZodType>(body: TBody): Route<Prettify<Omit<TState, "body"> & { body: TBody }>> {
|
||||
return new Route({ ...this.state, body });
|
||||
}
|
||||
|
||||
@@ -227,7 +230,9 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
errors<TErrors extends ServerErrorClass[]>(errors: TErrors): Route<Omit<TState, "errors"> & { errors: TErrors }> {
|
||||
errors<TErrors extends ServerErrorClass[]>(
|
||||
errors: TErrors,
|
||||
): Route<Prettify<Omit<TState, "errors"> & { errors: TErrors }>> {
|
||||
return new Route({ ...this.state, errors });
|
||||
}
|
||||
|
||||
@@ -254,7 +259,9 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
response<TResponse extends ZodType>(response: TResponse): Route<Omit<TState, "response"> & { response: TResponse }> {
|
||||
response<TResponse extends ZodType>(
|
||||
response: TResponse,
|
||||
): Route<Prettify<Omit<TState, "response"> & { response: TResponse }>> {
|
||||
return new Route({ ...this.state, response });
|
||||
}
|
||||
|
||||
@@ -292,7 +299,7 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
*
|
||||
* @param hooks - Hooks to register with the route.
|
||||
*/
|
||||
hooks<THooks extends Hooks>(hooks: THooks): Route<Omit<TState, "hooks"> & { hooks: THooks }> {
|
||||
hooks<THooks extends Hooks>(hooks: THooks): Route<Prettify<Omit<TState, "hooks"> & { hooks: THooks }>> {
|
||||
return new Route({ ...this.state, hooks });
|
||||
}
|
||||
}
|
||||
@@ -444,9 +451,10 @@ export type RouteMeta = {
|
||||
|
||||
export type RouteMethod = "POST" | "GET" | "PUT" | "PATCH" | "DELETE";
|
||||
|
||||
export type RouteAccess = "public" | "session" | (() => boolean)[];
|
||||
export type RouteAccess = "public" | "authenticated";
|
||||
|
||||
export type AccessFn = (resource: string, action: string) => () => boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ServerContext {}
|
||||
|
||||
type HandleFn<TArgs extends Array<any> = any[], TResponse = any> = (
|
||||
...args: TArgs
|
||||
@@ -471,3 +479,7 @@ type HasInputArgs<TState extends RouteState> = TState["params"] extends ZodObjec
|
||||
: TState["body"] extends ZodType
|
||||
? true
|
||||
: false;
|
||||
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export type Access = "public" | "session" | (() => boolean)[];
|
||||
|
||||
export type AccessFn = (resource: string, action: string) => () => boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ServerContext {}
|
||||
@@ -1,14 +0,0 @@
|
||||
import z from "zod";
|
||||
|
||||
import { makeSchemaParser } from "../database.ts";
|
||||
|
||||
export const RoleSchema = z.object({
|
||||
id: z.uuid(),
|
||||
name: z.string(),
|
||||
permissions: z.record(z.string(), z.array(z.string())),
|
||||
});
|
||||
|
||||
export const parseRole = makeSchemaParser(RoleSchema);
|
||||
|
||||
export type Role = z.infer<typeof RoleSchema>;
|
||||
export type RoleDocument = z.infer<typeof RoleSchema>;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { RoleSchema } from "../access/role.ts";
|
||||
import { AvatarSchema } from "../avatar.ts";
|
||||
import { ContactSchema } from "../contact.ts";
|
||||
import { makeSchemaParser } from "../database.ts";
|
||||
import { NameSchema } from "../name.ts";
|
||||
import { RoleSchema } from "./role.ts";
|
||||
import { StrategySchema } from "./strategies.ts";
|
||||
|
||||
export const AccountSchema = z.object({
|
||||
@@ -18,10 +18,8 @@ export const AccountSchema = z.object({
|
||||
roles: z.array(RoleSchema).default([]),
|
||||
});
|
||||
|
||||
export const AccountDocumentSchema = AccountSchema.omit({ roles: true }).extend({ roles: z.array(z.string()) });
|
||||
|
||||
export const toAccountDocument = makeSchemaParser(AccountDocumentSchema);
|
||||
export const toAccountDocument = makeSchemaParser(AccountSchema);
|
||||
export const fromAccountDocument = makeSchemaParser(AccountSchema);
|
||||
|
||||
export type Account = z.infer<typeof AccountSchema>;
|
||||
export type AccountDocument = z.infer<typeof AccountDocumentSchema>;
|
||||
export type AccountDocument = z.infer<typeof AccountSchema>;
|
||||
|
||||
5
spec/schemas/account/role.ts
Normal file
5
spec/schemas/account/role.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import z from "zod";
|
||||
|
||||
export const RoleSchema = z.union([z.literal("user"), z.literal("admin")]);
|
||||
|
||||
export type Role = z.infer<typeof RoleSchema>;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { route } from "@spec/relay";
|
||||
import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@spec/relay";
|
||||
import z from "zod";
|
||||
|
||||
import { NameSchema } from "../name.ts";
|
||||
import { AccountSchema } from "./account.ts";
|
||||
import { AccountEmailClaimedError } from "./errors.ts";
|
||||
|
||||
export const create = route
|
||||
@@ -15,6 +16,15 @@ export const create = route
|
||||
.errors([AccountEmailClaimedError])
|
||||
.response(z.uuid());
|
||||
|
||||
export const getById = route
|
||||
.get("/api/v1/accounts/:id")
|
||||
.params({
|
||||
id: z.string(),
|
||||
})
|
||||
.errors([UnauthorizedError, ForbiddenError, NotFoundError])
|
||||
.response(AccountSchema);
|
||||
|
||||
export const routes = {
|
||||
create,
|
||||
getById,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user