mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
userIdOrMe support on all yup validations
This commit is contained in:
parent
70a1e65723
commit
f1bffee8b9
@ -9,7 +9,7 @@ import { deindent, typedToLowercase } from "@stackframe/stack-shared/dist/utils/
|
||||
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { SmartRequestAuth } from "./smart-request";
|
||||
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
|
||||
import { yupArray, yupBoolean, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { yupArray, yupBoolean, yupMixed, yupNumber, yupObject, yupString, yupValidate } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
|
||||
|
||||
type GetAdminKey<T extends CrudTypeOf<any>, K extends Capitalize<CrudlOperation>> = K extends keyof T["Admin"] ? T["Admin"][K] : void;
|
||||
@ -155,9 +155,8 @@ export function createCrudHandlers<
|
||||
adminSchemas,
|
||||
invoke: async (options: { params: yup.InferType<PS> | Partial<yup.InferType<PS>>, query: yup.InferType<QS>, data: any, auth: SmartRequestAuth }) => {
|
||||
const actualParamsSchema = typedIncludes(["List", "Create"], crudOperation) ? paramsSchema.partial() : paramsSchema;
|
||||
const paramsValidated = await validate(options.params, actualParamsSchema, "Params validation");
|
||||
|
||||
const adminData = await validate(options.data, adminSchemas.input, "Input validation");
|
||||
const paramsValidated = await validate(options.params, actualParamsSchema, options.auth.user ?? null, "Params validation");
|
||||
const adminData = await validate(options.data, adminSchemas.input, options.auth.user ?? null, "Input validation");
|
||||
|
||||
await optionsAsPartial.onPrepare?.({
|
||||
params: paramsValidated,
|
||||
@ -172,8 +171,8 @@ export function createCrudHandlers<
|
||||
query: options.query,
|
||||
});
|
||||
|
||||
const resultAdminValidated = await validate(result, adminSchemas.output, "Result admin validation");
|
||||
const resultAccessValidated = await validate(resultAdminValidated, accessSchemas.output, `Result ${accessType} validation`);
|
||||
const resultAdminValidated = await validate(result, adminSchemas.output, options.auth.user ?? null, "Result admin validation");
|
||||
const resultAccessValidated = await validate(resultAdminValidated, accessSchemas.output, options.auth.user ?? null, `Result ${accessType} validation`);
|
||||
|
||||
return resultAccessValidated;
|
||||
},
|
||||
@ -270,17 +269,18 @@ export class CrudHandlerInvocationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function validate<T>(obj: unknown, schema: yup.ISchema<T>, name: string): Promise<T> {
|
||||
async function validate<T>(obj: unknown, schema: yup.ISchema<T>, currentUser: UsersCrud["Admin"]["Read"] | null, validationDescription: string): Promise<T> {
|
||||
try {
|
||||
return await schema.validate(obj, {
|
||||
return await yupValidate(schema, obj, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
currentUserId: currentUser?.id ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof yup.ValidationError) {
|
||||
throw new StackAssertionError(
|
||||
deindent`
|
||||
${name} failed in CRUD handler.
|
||||
${validationDescription} failed in CRUD handler.
|
||||
|
||||
Errors:
|
||||
${error.errors.join("\n")}
|
||||
|
||||
@ -7,7 +7,7 @@ import { decodeAccessToken } from "@/lib/tokens";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
|
||||
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
|
||||
import { ReplaceFieldWithOwnUserId, StackAdaptSentinel } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { ReplaceFieldWithOwnUserId, StackAdaptSentinel, yupValidate } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { groupBy, typedIncludes } from "@stackframe/stack-shared/dist/utils/arrays";
|
||||
import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { deepPlainClone } from "@stackframe/stack-shared/dist/utils/objects";
|
||||
@ -51,43 +51,15 @@ export type MergeSmartRequest<T, MSQ = SmartRequest> =
|
||||
|
||||
async function validate<T>(obj: SmartRequest, schema: yup.Schema<T>, req: NextRequest | null): Promise<T> {
|
||||
try {
|
||||
return await schema.validate(obj, {
|
||||
return await yupValidate(schema, obj, {
|
||||
abortEarly: false,
|
||||
context: {
|
||||
noUnknownPathPrefixes: ["body", "query", "params"],
|
||||
},
|
||||
currentUserId: obj.auth?.user?.id ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ReplaceFieldWithOwnUserId) {
|
||||
// parse yup path
|
||||
let pathRemaining = error.path;
|
||||
const fieldPath = [];
|
||||
while (pathRemaining.length > 0) {
|
||||
if (pathRemaining.startsWith("[")) {
|
||||
const index = pathRemaining.indexOf("]");
|
||||
if (index < 0) throw new StackAssertionError("Invalid path");
|
||||
fieldPath.push(JSON.parse(pathRemaining.slice(1, index)));
|
||||
pathRemaining = pathRemaining.slice(index + 1);
|
||||
} else {
|
||||
let dotIndex = pathRemaining.indexOf(".");
|
||||
if (dotIndex === -1) dotIndex = pathRemaining.length;
|
||||
fieldPath.push(pathRemaining.slice(0, dotIndex));
|
||||
pathRemaining = pathRemaining.slice(dotIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const newObj = deepPlainClone(obj);
|
||||
let it = newObj;
|
||||
for (const field of fieldPath.slice(0, -1)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(it, field)) {
|
||||
throw new StackAssertionError(`Segment ${field} of path ${error.path} not found in object`);
|
||||
}
|
||||
it = (it as any)[field];
|
||||
}
|
||||
(it as any)[fieldPath[fieldPath.length - 1]] = obj.auth?.user?.id ?? throwErr(new KnownErrors.CannotGetOwnUserWithoutUser());
|
||||
|
||||
return await validate(newObj, schema, req);
|
||||
} else if (error instanceof yup.ValidationError) {
|
||||
if (error instanceof yup.ValidationError) {
|
||||
if (req === null) {
|
||||
// we weren't called by a HTTP request, so it must be a logical error in a manual invocation
|
||||
throw new StackAssertionError("Request validation failed", {}, { cause: error });
|
||||
|
||||
@ -16,7 +16,7 @@ class InternalServerError extends StatusError {
|
||||
constructor(error: unknown) {
|
||||
super(
|
||||
StatusError.InternalServerError,
|
||||
["development", "test"].includes(getNodeEnvironment()) ? `Internal Server Error. The error message follows, but will be stripped in production. ${error}` : `Something went wrong. Please make sure the data you entered is correct.`,
|
||||
["development", "test"].includes(getNodeEnvironment()) ? `Internal Server Error. The error message follows, but will be stripped in production. ${(error as any)?.stack ?? error}` : `Something went wrong. Please make sure the data you entered is correct.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,62 @@
|
||||
import * as yup from "yup";
|
||||
import { KnownErrors } from ".";
|
||||
import { isBase64 } from "./utils/bytes";
|
||||
import { StackAssertionError } from "./utils/errors";
|
||||
import { allProviders } from "./utils/oauth";
|
||||
import { deepPlainClone, omit } from "./utils/objects";
|
||||
import { isUuid } from "./utils/uuids";
|
||||
|
||||
export async function yupValidate<S extends yup.ISchema<any>>(
|
||||
schema: S,
|
||||
obj: unknown,
|
||||
options?: yup.ValidateOptions & { currentUserId?: string | null }
|
||||
): Promise<yup.InferType<S>> {
|
||||
try {
|
||||
return await schema.validate(obj, {
|
||||
...omit(options ?? {}, ['currentUserId']),
|
||||
context: {
|
||||
...options?.context,
|
||||
stackAllowUserIdMe: options?.currentUserId !== undefined,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ReplaceFieldWithOwnUserId) {
|
||||
const currentUserId = options?.currentUserId;
|
||||
if (!currentUserId) throw new KnownErrors.CannotGetOwnUserWithoutUser();
|
||||
|
||||
// parse yup path
|
||||
let pathRemaining = error.path;
|
||||
const fieldPath = [];
|
||||
while (pathRemaining.length > 0) {
|
||||
if (pathRemaining.startsWith("[")) {
|
||||
const index = pathRemaining.indexOf("]");
|
||||
if (index < 0) throw new StackAssertionError("Invalid path");
|
||||
fieldPath.push(JSON.parse(pathRemaining.slice(1, index)));
|
||||
pathRemaining = pathRemaining.slice(index + 1);
|
||||
} else {
|
||||
let dotIndex = pathRemaining.indexOf(".");
|
||||
if (dotIndex === -1) dotIndex = pathRemaining.length;
|
||||
fieldPath.push(pathRemaining.slice(0, dotIndex));
|
||||
pathRemaining = pathRemaining.slice(dotIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const newObj = deepPlainClone(obj);
|
||||
let it = newObj;
|
||||
for (const field of fieldPath.slice(0, -1)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(it, field)) {
|
||||
throw new StackAssertionError(`Segment ${field} of path ${error.path} not found in object`);
|
||||
}
|
||||
it = (it as any)[field];
|
||||
}
|
||||
(it as any)[fieldPath[fieldPath.length - 1]] = currentUserId;
|
||||
|
||||
return await yupValidate(schema, newObj, options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const _idDescription = (identify: string) => `The unique identifier of this ${identify}`;
|
||||
const _displayNameDescription = (identify: string) => `Human-readable ${identify} display name. This is not a unique identifier.`;
|
||||
const _clientMetaDataDescription = (identify: string) => `Client metadata. Used as a data store, accessible from the client side. Do not store information that should not be exposed to the client.`;
|
||||
@ -190,6 +243,8 @@ export const userIdOrMeSchema = yupString().uuid().transform(v => {
|
||||
if (v === "me") return userIdMeSentinelUuid;
|
||||
else return v;
|
||||
}).test((v, context) => {
|
||||
if (!("stackAllowUserIdMe" in (context.options.context ?? {}))) throw new StackAssertionError('userIdOrMeSchema is not allowed in this context. Make sure you\'re using yupValidate from schema-fields.ts to validate, instead of schema.validate(...).');
|
||||
if (!context.options.context?.stackAllowUserIdMe) throw new StackAssertionError('userIdOrMeSchema is not allowed in this context. Make sure you\'re passing in the currentUserId option in yupValidate.');
|
||||
if (v === userIdMeSentinelUuid) throw new ReplaceFieldWithOwnUserId(context.path);
|
||||
return true;
|
||||
}).meta({ openapiField: { description: 'The ID of the user, or the special value `me` for the currently authenticated user', exampleValue: '3241a285-8329-4d69-8f3d-316e08cf140c' } });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user