diff --git a/DOCS_HEADER.md b/DOCS_HEADER.md new file mode 100644 index 00000000..8bcab5d9 --- /dev/null +++ b/DOCS_HEADER.md @@ -0,0 +1,38 @@ +HackIllinois' backend API + +## Authentication + +[Attendee Authentication](https://adonix.hackillinois.org/auth/login/github?device=dev) + +[Staff Authentication](https://adonix.hackillinois.org/auth/login/google?device=dev) + +You can get a JWT to test with using the two links above - you'll have to do it programmatically in the apps, though. + +Authentication is required for most endpoints, and most endpoints have a role requirement as well (staff-only, for example). +You can see these requirements on each endpoint's description. + +## Errors +Errors are formatted like: +```json +{ + "error": "NotFound", + "message": "Couldn't find that", +} +``` +Where `error` is a error type that can be compared in code, and `message` is a user-facing message for that error. + +This way, handling errors is straight-forward. + +## Shared Errors +These error types are **not listed** under endpoints specifically, since each endpoint shares them. + +Authentication: +- `NoToken` - you haven't specified a token on an endpoint that requires one +- `InvalidToken` - the token specified is invalid, try re-logging +- `TokenExpired` - the token has expired, you'll need to get a new one. To prevent this, use `GET /auth/token/refresh/`. +- `Forbidden` - you don't have the role required to use this endpoint + +Specification has one key type as well - `BadRequest`. +If you ever get this error code, you're sending a request in the wrong format. +This error will also include information about what information is wrong, +but you should always consult the docs if you get this error to verify. diff --git a/src/app.ts b/src/app.ts index faa5d248..d1e8ba05 100644 --- a/src/app.ts +++ b/src/app.ts @@ -65,7 +65,7 @@ app.use("/version/", versionRouter); app.use("/user/", database, userRouter); // Docs -app.use("/docs/json", (_req, res) => res.json(getOpenAPISpec())); +app.use("/docs/json", async (_req, res) => res.json(await getOpenAPISpec())); app.use("/docs", swaggerUi.serveFiles(undefined, SWAGGER_UI_OPTIONS), swaggerUi.setup(undefined, SWAGGER_UI_OPTIONS)); // Ensure that API is running diff --git a/src/common/auth.ts b/src/common/auth.ts index d65eb58f..a99948e2 100644 --- a/src/common/auth.ts +++ b/src/common/auth.ts @@ -1,5 +1,5 @@ import ms from "ms"; -import jsonwebtoken, { SignOptions } from "jsonwebtoken"; +import jsonwebtoken, { SignOptions, TokenExpiredError } from "jsonwebtoken"; import { RequestHandler } from "express-serve-static-core"; import passport, { AuthenticateOptions, Profile } from "passport"; @@ -10,11 +10,45 @@ import { Role, JwtPayload, Provider, ProfileData, RoleOperation } from "../servi import Models from "./models"; import { AuthInfo } from "../services/auth/auth-schemas"; import { UpdateQuery } from "mongoose"; +import { IncomingMessage } from "http"; +import StatusCode from "status-code-enum"; +import { APIError } from "./schemas"; type AuthenticateFunction = (strategies: string | string[], options: AuthenticateOptions) => RequestHandler; type VerifyCallback = (err: Error | null, user?: Profile | false, info?: object) => void; type VerifyFunction = (accessToken: string, refreshToken: string, profile: Profile, done: VerifyCallback) => void; +export enum AuthenticationErrorType { + TOKEN_EXPIRED, + NO_TOKEN, + TOKEN_INVALID, +} + +const TYPE_TO_DETAILS = { + [AuthenticationErrorType.TOKEN_EXPIRED]: { + error: "TokenExpired", + message: "Your session has expired, please log in again", + status: StatusCode.ClientErrorForbidden, + }, + [AuthenticationErrorType.NO_TOKEN]: { + error: "NoToken", + message: "A authorization token must be sent for this request", + status: StatusCode.ClientErrorUnauthorized, + }, + [AuthenticationErrorType.TOKEN_INVALID]: { + error: "TokenInvalid", + message: "Your session is invalid, please log in again", + status: StatusCode.ClientErrorUnauthorized, + }, +}; + +export class AuthenticationError extends APIError { + constructor(type: AuthenticationErrorType) { + const { error, message, status } = TYPE_TO_DETAILS[type]; + super(error, message, status); + } +} + /** * Perform authentication step. Use this information to redirect to provider, perform auth, and then redirect user back to main website if successful or unsuccessful. * In the case of a failure, throw an error. @@ -140,13 +174,7 @@ export function generateJwtToken(payload?: JwtPayload, shouldNotExpire?: boolean */ export function decodeJwtToken(token?: string): JwtPayload { if (!token) { - throw new Error("NoToken"); - } - - // Ensure that we have a secret to parse token - const secret: string | undefined = Config.JWT_SECRET; - if (!secret) { - throw new Error("NoSecret"); + throw new AuthenticationError(AuthenticationErrorType.NO_TOKEN); } // Remove Bearer if included @@ -155,7 +183,16 @@ export function decodeJwtToken(token?: string): JwtPayload { } // Verify already ensures that the token isn't expired. If it is, it returns an error - return jsonwebtoken.verify(token, secret) as JwtPayload; + try { + return jsonwebtoken.verify(token, Config.JWT_SECRET) as JwtPayload; + } catch (e) { + // Handle errors by casting them to our type + if (e instanceof TokenExpiredError) { + throw new AuthenticationError(AuthenticationErrorType.TOKEN_EXPIRED); + } else { + throw new AuthenticationError(AuthenticationErrorType.TOKEN_INVALID); + } + } } /** @@ -163,8 +200,12 @@ export function decodeJwtToken(token?: string): JwtPayload { * @param req The request * @returns User payload */ -export function getAuthenticatedUser(req: IncomingMessage): JwtPayload { - return decodeJwtToken(req.headers.authorization); +export function getAuthenticatedUser(req: IncomingMessage & { authorizationPayload?: JwtPayload }): JwtPayload { + // Basic caching if we request the parsed jwt on the same request multiple times + if (!req.authorizationPayload) { + req.authorizationPayload = decodeJwtToken(req.headers.authorization); + } + return req.authorizationPayload; } /** @@ -174,7 +215,7 @@ export function getAuthenticatedUser(req: IncomingMessage): JwtPayload { */ export function tryGetAuthenticatedUser(req: IncomingMessage): JwtPayload | null { try { - return decodeJwtToken(req.headers.authorization); + return getAuthenticatedUser(req); } catch { return null; } @@ -292,62 +333,6 @@ export async function updateRoles(userId: string, role: Role, operation: RoleOpe } } -/** - * Catch-all function to check if a user should have permissions to perform operations on attendees - * @param payload Payload of user performing the actual request - * @returns True if the user is an ADMIN or a STAFF, else false - */ -export function hasElevatedPerms(payload: JwtPayload): boolean { - return hasStaffPerms(payload) || hasAdminPerms(payload); -} - -/** - * Check if a user has permissions to perform staff operations - * @param payload Payload of user performing the actual request - * @returns True if the user is a STAFF, else false - */ - -export function hasStaffPerms(payload?: JwtPayload): boolean { - if (!payload) { - return false; - } - - return payload.roles.includes(Role.STAFF); -} - -/** - * Check if a user has permissions to perform admin operations - * @param payload Payload of user performing the actual request - * @returns True if the user is an ADMIN, else false - */ -export function hasAdminPerms(payload?: JwtPayload): boolean { - if (!payload) { - return false; - } - - return payload.roles.includes(Role.ADMIN); -} - -/** - * Check if a user has PRO permissions - * @param payload Payload of user performing the actual request - * @returns True if the user has PRO, else false - */ -export function isPro(payload?: JwtPayload): boolean { - if (!payload) { - return false; - } - - return payload.roles.includes(Role.PRO); -} - -export function isAttendee(payload?: JwtPayload): boolean { - if (!payload) { - return false; - } - - return payload.roles.includes(Role.ATTENDEE); -} /** * Get all id of users that have a particular role within the database. * @param role role that we want to filter for diff --git a/src/common/openapi.ts b/src/common/openapi.ts index 08ed6222..0d37596e 100644 --- a/src/common/openapi.ts +++ b/src/common/openapi.ts @@ -4,6 +4,7 @@ import type { ExampleObject, InfoObject, OpenAPIObject, SecurityRequirementObjec import Config from "./config"; import { ResponsesObject, Specification } from "../middleware/specification"; import { SwaggerUiOptions } from "swagger-ui-express"; +import { readFileSync } from "fs"; let openAPISpec: OpenAPIObject | undefined = undefined; export const Registry = new OpenAPIRegistry(); @@ -15,6 +16,20 @@ export const SWAGGER_UI_OPTIONS: SwaggerUiOptions = { swaggerOptions: { persistAuthorization: true, }, + customCss: ` +code { + padding: 2px 4px !important; +} +pre > code { + padding: 5px 7px !important; +} +html { + line-height: 1.15; +} +li { + margin: 10px 0; +} +`, }; // Basic metadata @@ -23,10 +38,6 @@ const openapi = "3.1.0"; const info: InfoObject = { title: "adonix", version: "1.0.0", - description: - "HackIllinois' backend API\n\n" + - `[Attendee Authentication](${Config.ROOT_URL}/auth/login/github?device=dev)\n\n` + - `[Staff Authentication](${Config.ROOT_URL}/auth/login/google?device=dev)`, }; const servers: ServerObject[] = [ @@ -43,8 +54,9 @@ const authentication = Registry.registerComponent("securitySchemes", "Authentica bearerFormat: "jwt", }); -function generateOpenAPISpec(): OpenAPIObject { +async function generateOpenAPISpec(): Promise { const generator = new OpenApiGeneratorV31(Registry.definitions); + info.description = (await readFileSync("DOCS_HEADER.md")).toString(); const document = generator.generateDocument({ info, openapi, @@ -78,9 +90,9 @@ function generateOpenAPISpec(): OpenAPIObject { return document; } -export function getOpenAPISpec(): OpenAPIObject { +export async function getOpenAPISpec(): Promise { if (!openAPISpec) { - openAPISpec = generateOpenAPISpec(); + openAPISpec = await generateOpenAPISpec(); } return openAPISpec; diff --git a/src/common/schemas.ts b/src/common/schemas.ts index b57d4f02..654f4820 100644 --- a/src/common/schemas.ts +++ b/src/common/schemas.ts @@ -11,29 +11,29 @@ export const UserIdSchema = z.string().openapi("UserId", { export const EventIdSchema = z.string().openapi("EventId", { example: "event1" }); -/** - * Creates a error object and schema zod given a error type and message - * @param params the error type and message - * @returns an array of the error object and schema - [ErrorObject, ZodSchema] - * @example - * const [SomeError, SomeErrorSchema] = CreateErrorAndSchema({ - * error: "SomeError", - * message: "Some detailed description of some error" - * }) - */ -export function CreateErrorAndSchema(params: { +export class APIError { + constructor( + public error: TError, + public message: TMessage, + public status?: number, + public context?: unknown, + ) {} +} + +interface ErrorParams { error: TError; message: TMessage; -}): [ - { readonly error: TError; readonly message: TMessage }, - z.ZodObject<{ - error: z.ZodLiteral; - message: z.ZodLiteral; - }>, -] { - const { error, message } = params; +} + +export function CreateErrorSchema( + params: ErrorParams, +): z.ZodObject<{ + error: z.ZodLiteral; + message: z.ZodLiteral; +}> { // Zod schema definition, keeping the literal types for error and message - const schema = z + const { error, message } = params; + return z .object({ error: z.literal(error), message: z.literal(message), @@ -46,12 +46,28 @@ export function CreateErrorAndSchema( + params: ErrorParams, +): [ + APIError, + z.ZodObject<{ + error: z.ZodLiteral; + message: z.ZodLiteral; + }>, +] { + const { error, message } = params; + // Return the tuple with literal types preserved + return [new APIError(error, message), CreateErrorSchema(params)] as const; } diff --git a/src/middleware/error-handler.ts b/src/middleware/error-handler.ts index b2efedd8..1bafc597 100644 --- a/src/middleware/error-handler.ts +++ b/src/middleware/error-handler.ts @@ -1,48 +1,32 @@ import { Request, Response, NextFunction } from "express"; import { StatusCode } from "status-code-enum"; +import { tryGetAuthenticatedUser } from "../common/auth"; +import { randomUUID } from "crypto"; +import { APIError } from "../common/schemas"; -export class RouterError { - statusCode: number; - message: string; - // NOTE: eslint is required because the goal of RouterError.data is to "return any necessary data" - mostly used for debugging purposes - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: any | undefined; - catchErrorMessage?: string | undefined; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(statusCode?: number, message?: string, data?: any, catchErrorMessage?: any) { - this.statusCode = statusCode ?? StatusCode.ServerErrorInternal; - this.message = message ?? "InternalServerError"; - this.data = data; - if (catchErrorMessage) { - this.catchErrorMessage = catchErrorMessage; - console.error(catchErrorMessage); - } else { - this.catchErrorMessage = ""; - } - } +function isErrorWithStack(error: unknown): error is { stack: string } { + return typeof error === "object" && error !== null && "stack" in error; } -// _next is intentionally not used in this middleware -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function ErrorHandler(error: RouterError, _req: Request, resp: Response, _next: NextFunction): Response { - const statusCode = error.statusCode || StatusCode.ServerErrorInternal; - const message = error.message; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data: any | undefined = error.data; - const catchErrorMessage: string | undefined = error.catchErrorMessage; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const jsonData: { [key: string]: any } = { - success: statusCode === StatusCode.SuccessOK, - error: message, - }; - if (data) { - jsonData["data"] = data; - } - if (catchErrorMessage) { - jsonData["error_message"] = catchErrorMessage; +export function ErrorHandler(error: unknown, req: Request, res: Response, _next: NextFunction): Response { + // Handle pre-defined errors + if (error instanceof APIError) { + return res.status(error.status || StatusCode.ServerErrorInternal).send({ + error: error.error, + message: error.message, + context: error.context, + }); } + // Otherwise, undefined error - so we display default internal error + const userId = tryGetAuthenticatedUser(req)?.id || "unauthenticated"; + const id = randomUUID(); + const stack = isErrorWithStack(error) ? `${error.stack}` : undefined; + const status = StatusCode.ServerErrorInternal; - return resp.status(statusCode).json(jsonData); + console.error(`ERROR ${id} at ${Date.now()} - ${status} ${req.method} ${req.path} ${userId}:\n` + `${stack || error}`); + return res.status(status).send({ + error: "InternalError", + message: `Something went wrong - we're looking into it! Id: ${id}`, + id, + }); } diff --git a/src/middleware/specification-validator.ts b/src/middleware/specification-validator.ts new file mode 100644 index 00000000..08390422 --- /dev/null +++ b/src/middleware/specification-validator.ts @@ -0,0 +1,68 @@ +import { AnyZodObject, z, ZodIssue, ZodType } from "zod"; +import { ResponseBody, ResponsesObject, Specification } from "./specification"; +import { Request, Response, NextFunction, RequestHandler } from "express-serve-static-core"; +import { getAuthenticatedUser } from "../common/auth"; +import StatusCode from "status-code-enum"; +import { APIError } from "../common/schemas"; +import { Role } from "../services/auth/auth-schemas"; + +export class MissingRoleError extends APIError { + constructor(role: Role) { + super("Forbidden", `You require the role ${role} to do that`, StatusCode.ClientErrorForbidden); + } +} + +export class SpecificationError extends APIError { + constructor(errors: Record) { + const summary = Object.entries(errors) + .flatMap(([location, issues]) => + issues.map( + ({ code, message, path }) => + ` - ${code} in ${location}${path.length > 0 ? `.${path.join(".")}` : ""}: ${message}`, + ), + ) + .join("\n"); + super("BadRequest", `Bad request made - invalid format.\n${summary}`, StatusCode.ClientErrorBadRequest, errors); + } +} + +async function validateSchema(schema: ZodType | undefined, data: unknown): Promise { + if (schema) { + const result = await schema.safeParseAsync(data); + if (!result.success) { + return result.error.errors; + } + } + + return []; +} + +export function specificationValidator< + Params extends AnyZodObject, + Query extends AnyZodObject, + Responses extends ResponsesObject, + Body extends ZodType, +>( + spec: Specification, +): RequestHandler, ResponseBody, z.infer, z.infer> { + return async (req: Request, _res: Response, next: NextFunction) => { + if (spec.role) { + const jwt = getAuthenticatedUser(req); + if (!jwt.roles.includes(spec.role)) { + throw new MissingRoleError(spec.role); + } + } + + const errors: Record = { + parameters: await validateSchema(spec.parameters, req.params), + query: await validateSchema(spec.query, req.query), + body: await validateSchema(spec.body, req.body), + }; + + if (Object.values(errors).flat().length > 0) { + throw new SpecificationError(errors); + } + + return next(); + }; +} diff --git a/src/middleware/specification.ts b/src/middleware/specification.ts index 8af2b8b1..8325dc52 100644 --- a/src/middleware/specification.ts +++ b/src/middleware/specification.ts @@ -1,12 +1,9 @@ import { RequestHandler } from "express"; import { AnyZodObject, z, ZodObject, ZodType, ZodUnknown } from "zod"; -import StatusCode from "status-code-enum"; -import { Response, Request, NextFunction } from "express"; import { registerPathSpecification } from "../common/openapi"; import { RouteConfig } from "@asteasolutions/zod-to-openapi"; import { Role } from "../services/auth/auth-schemas"; -import { getAuthenticatedUser } from "../common/auth"; -import { TokenExpiredError } from "jsonwebtoken"; +import { specificationValidator } from "./specification-validator"; export type Method = RouteConfig["method"]; @@ -60,10 +57,10 @@ type InferResponseBody = T extends ResponseObject : never; // This type indexes each possible key in the ResponsesObject and passes it to InferResponseBody to get the underlying types -type ResponseBody = InferResponseBody; +export type ResponseBody = InferResponseBody; // Utility type for a zod object which is really just empty (not just {} in ts) -type ZodEmptyObject = ZodObject>; +export type ZodEmptyObject = ZodObject>; export default function specification< Responses extends ResponsesObject, @@ -74,69 +71,5 @@ export default function specification< spec: Specification, ): RequestHandler, ResponseBody, z.infer, z.infer> { registerPathSpecification(spec); - - return async (req: Request, res: Response, next: NextFunction) => { - if (spec.role) { - try { - const jwt = getAuthenticatedUser(req); - if (!jwt.roles.includes(spec.role)) { - return res.status(StatusCode.ClientErrorForbidden).json({ - error: "Forbidden", - message: `You require the role ${spec.role} to do that`, - }); - } - } catch (error) { - if (error instanceof TokenExpiredError) { - return res.status(StatusCode.ClientErrorForbidden).json({ - error: "TokenExpired", - message: "Your session has expired, please log in again", - }); - } else if (error instanceof Error && error.message == "NoToken") { - return res.status(StatusCode.ClientErrorUnauthorized).send({ - error: "NoToken", - message: "A authorization token must be sent for this request", - }); - } else { - return res.status(StatusCode.ClientErrorUnauthorized).send({ - error: "TokenInvalid", - message: "Your session is invalid, please log in again", - }); - } - } - } - - if (spec.parameters) { - const result = await spec.parameters.safeParseAsync(req.params); - if (!result.success) { - return res.status(StatusCode.ClientErrorBadRequest).json({ - error: "BadRequest", - message: "Bad request made - invalid parameters format", - validationErrors: result.error.errors, - }); - } - } - - if (spec.query) { - const result = await spec.query.safeParseAsync(req.query); - if (!result.success) { - return res.status(StatusCode.ClientErrorBadRequest).json({ - error: "BadRequest", - message: "Bad request made - invalid query format", - validationErrors: result.error.errors, - }); - } - } - - if (spec.body) { - const result = await spec.body.safeParseAsync(req.body); - if (!result.success) { - return res.status(StatusCode.ClientErrorBadRequest).json({ - error: "BadRequest", - message: "Bad request made - invalid body format", - validationErrors: result.error.errors, - }); - } - } - return next(); - }; + return specificationValidator(spec); } diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 1fceccce..5311ed58 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -33,7 +33,6 @@ import { getAuthenticatedUser, } from "../../common/auth"; import Models from "../../common/models"; -import { RouterError } from "../../middleware/error-handler"; import specification, { Tag } from "../../middleware/specification"; import { z } from "zod"; import { UserNotFoundError, UserNotFoundErrorSchema } from "../user/user-schemas"; @@ -149,19 +148,14 @@ authRouter.get( }), (req, res, next) => { const provider = req.params.provider ?? ""; - try { - const device = req.params.device; - - if (!device || !Config.REDIRECT_URLS.has(device)) { - throw Error(`Bad device ${device}`); - } - - res.locals.device = device; - return SelectAuthProvider(provider, device)(req, res, next); - } catch (error) { - const message = error instanceof Error ? error.message : `${error}`; - return next(new RouterError(undefined, undefined, undefined, message)); + const device = req.params.device; + + if (!device || !Config.REDIRECT_URLS.has(device)) { + throw Error(`Bad device ${device}`); } + + res.locals.device = device; + return SelectAuthProvider(provider, device)(req, res, next); }, async (req, res) => { if (!req.isAuthenticated()) { diff --git a/src/services/mail/mail-router.ts b/src/services/mail/mail-router.ts index 070d60f6..b577f400 100644 --- a/src/services/mail/mail-router.ts +++ b/src/services/mail/mail-router.ts @@ -2,7 +2,6 @@ // ➡️ send confirmation email to the email provided in application import { Router } from "express"; -import { RouterError } from "../../middleware/error-handler"; import { StatusCode } from "status-code-enum"; import { Role } from "../auth/auth-schemas"; import { sendMail } from "./mail-lib"; @@ -29,20 +28,11 @@ mailRouter.post( }, }, }), - async (req, res, next) => { + async (req, res) => { const mailInfo = req.body; - try { - const result = await sendMail(mailInfo); - return res.status(StatusCode.SuccessOK).json(result.data); - } catch (error) { - return next( - new RouterError(StatusCode.ClientErrorBadRequest, "EmailNotSent", { - status: error.response?.status, - code: error.code, - }), - ); - } + const result = await sendMail(mailInfo); + return res.status(StatusCode.SuccessOK).json(result.data); }, ); diff --git a/src/services/notification/notification-router.ts b/src/services/notification/notification-router.ts index 2214e3e4..2ed80162 100644 --- a/src/services/notification/notification-router.ts +++ b/src/services/notification/notification-router.ts @@ -73,11 +73,11 @@ notificationsRouter.post( summary: "Sends a notification to a specified group of users", description: "Can filter by: \n" + - "- `eventId`: users following a event\n\n" + - "- `role`: users that have a role\n\n" + - "- `staffShift`: staff in a staff shift\n\n" + - "- `foodWave`: users in a food wave \n\n" + - "- `userIds`: some set of users\n\n" + + "- `eventId`: users following a event\n" + + "- `role`: users that have a role\n" + + "- `staffShift`: staff in a staff shift\n" + + "- `foodWave`: users in a food wave \n" + + "- `userIds`: some set of users\n" + "Filters are intersecting, so `eventId = 123` and `foodWave = 1` would get users following event 123 AND in food wave 1.", body: NotificationSendRequestSchema, responses: { diff --git a/src/services/registration/registration-router.test.ts b/src/services/registration/registration-router.test.ts index 55a79576..c17c24da 100644 --- a/src/services/registration/registration-router.test.ts +++ b/src/services/registration/registration-router.test.ts @@ -179,7 +179,7 @@ describe("POST /registration/submit/", () => { }); const response = await postAsUser("/registration/submit/").send().expect(StatusCode.ServerErrorInternal); - expect(JSON.parse(response.text)).toHaveProperty("error", "EmailFailedToSend"); + expect(JSON.parse(response.text)).toHaveProperty("error", "InternalError"); // Still stored in DB const stored: RegistrationApplication | null = await Models.RegistrationApplication.findOne({ diff --git a/src/services/staff/staff-schemas.ts b/src/services/staff/staff-schemas.ts index c7e7685b..cfc5bb74 100644 --- a/src/services/staff/staff-schemas.ts +++ b/src/services/staff/staff-schemas.ts @@ -1,6 +1,4 @@ import { prop } from "@typegoose/typegoose"; -import { RouterError } from "../../middleware/error-handler"; -import { AttendeeProfile } from "../profile/profile-schemas"; import { UserIdSchema, EventIdSchema } from "../../common/schemas"; import { z } from "zod"; import { CreateErrorAndSchema, SuccessResponseSchema } from "../../common/schemas"; @@ -26,12 +24,6 @@ export interface EventError { name: string; } -export interface checkInResult { - success: boolean; - error?: RouterError; - profile?: AttendeeProfile; -} - export const StaffAttendanceRequestSchema = z.object({ eventId: EventIdSchema, });