mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Auth methods and connected accounts (#164)
This commit is contained in:
parent
94a3edd77d
commit
dfb51b8346
@ -222,11 +222,15 @@ model ProjectUser {
|
||||
projectUserRefreshTokens ProjectUserRefreshToken[]
|
||||
projectUserAuthorizationCodes ProjectUserAuthorizationCode[]
|
||||
projectUserOAuthAccounts ProjectUserOAuthAccount[]
|
||||
projectUserEmailVerificationCode ProjectUserEmailVerificationCode[]
|
||||
projectUserPasswordResetCode ProjectUserPasswordResetCode[]
|
||||
projectUserMagicLinkCode ProjectUserMagicLinkCode[]
|
||||
teamMembers TeamMember[]
|
||||
|
||||
// @deprecated
|
||||
projectUserEmailVerificationCode ProjectUserEmailVerificationCode[]
|
||||
// @deprecated
|
||||
projectUserPasswordResetCode ProjectUserPasswordResetCode[]
|
||||
// @deprecated
|
||||
projectUserMagicLinkCode ProjectUserMagicLinkCode[]
|
||||
|
||||
primaryEmail String?
|
||||
primaryEmailVerified Boolean
|
||||
profileImageUrl String?
|
||||
|
||||
@ -3,16 +3,20 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler";
|
||||
import { BooleanTrue, Prisma } from "@prisma/client";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { currentUserCrud } from "@stackframe/stack-shared/dist/interface/crud/current-user";
|
||||
import { usersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
|
||||
import { UsersCrud, usersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
|
||||
import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { StackAssertionError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { hashPassword } from "@stackframe/stack-shared/dist/utils/password";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
import { teamPrismaToCrud } from "../teams/crud";
|
||||
import { sendUserCreatedWebhook, sendUserDeletedWebhook, sendUserUpdatedWebhook } from "@/lib/webhooks";
|
||||
|
||||
const fullInclude = {
|
||||
projectUserOAuthAccounts: true,
|
||||
projectUserOAuthAccounts: {
|
||||
include: {
|
||||
providerConfig: true,
|
||||
},
|
||||
},
|
||||
teamMembers: {
|
||||
include: {
|
||||
team: true,
|
||||
@ -21,13 +25,51 @@ const fullInclude = {
|
||||
isSelected: BooleanTrue.TRUE,
|
||||
},
|
||||
},
|
||||
};
|
||||
} satisfies Prisma.ProjectUserInclude;
|
||||
|
||||
const prismaToCrud = (prisma: Prisma.ProjectUserGetPayload<{ include: typeof fullInclude}>) => {
|
||||
const prismaToCrud = (prisma: Prisma.ProjectUserGetPayload<{ include: typeof fullInclude}>): UsersCrud["Admin"]["Read"] => {
|
||||
const selectedTeamMembers = prisma.teamMembers;
|
||||
if (selectedTeamMembers.length > 1) {
|
||||
throw new StackAssertionError("User cannot have more than one selected team; this should never happen");
|
||||
}
|
||||
|
||||
if (prisma.passwordHash && !prisma.authWithEmail) {
|
||||
captureError("prismaToCrud", new StackAssertionError("User has password but authWithEmail is false; this is an assertion error that should never happen", { prisma }));
|
||||
}
|
||||
if (prisma.authWithEmail && !prisma.primaryEmail) {
|
||||
captureError("prismaToCrud", new StackAssertionError("User has authWithEmail but no primary email; this is an assertion error that should never happen", { prisma }));
|
||||
}
|
||||
const authMethods: UsersCrud["Admin"]["Read"]["auth_methods"] = [
|
||||
...prisma.passwordHash ? [{
|
||||
type: 'password',
|
||||
identifier: prisma.primaryEmail ?? "",
|
||||
}] as const : [],
|
||||
...prisma.authWithEmail ? [{
|
||||
type: 'otp',
|
||||
contact_channel: {
|
||||
type: 'email',
|
||||
email: prisma.primaryEmail ?? "",
|
||||
},
|
||||
}] as const : [],
|
||||
...prisma.projectUserOAuthAccounts.map((a) => ({
|
||||
type: 'oauth',
|
||||
provider: {
|
||||
type: a.oauthProviderConfigId,
|
||||
provider_user_id: a.providerAccountId,
|
||||
},
|
||||
} as const)),
|
||||
] as const;
|
||||
|
||||
const connectedAccounts: UsersCrud["Admin"]["Read"]["connected_accounts"] = [
|
||||
...prisma.projectUserOAuthAccounts.map((a) => ({
|
||||
type: 'oauth',
|
||||
provider: {
|
||||
type: a.oauthProviderConfigId,
|
||||
provider_user_id: a.providerAccountId,
|
||||
},
|
||||
} as const)),
|
||||
];
|
||||
|
||||
return {
|
||||
id: prisma.projectUserId,
|
||||
display_name: prisma.displayName || null,
|
||||
@ -37,7 +79,6 @@ const prismaToCrud = (prisma: Prisma.ProjectUserGetPayload<{ include: typeof ful
|
||||
signed_up_at_millis: prisma.createdAt.getTime(),
|
||||
client_metadata: prisma.clientMetadata,
|
||||
server_metadata: prisma.serverMetadata,
|
||||
auth_method: prisma.passwordHash ? 'credential' as const : 'oauth' as const, // not used anymore, for backwards compatibility
|
||||
has_password: !!prisma.passwordHash,
|
||||
auth_with_email: prisma.authWithEmail,
|
||||
oauth_providers: prisma.projectUserOAuthAccounts.map((a) => ({
|
||||
@ -45,6 +86,8 @@ const prismaToCrud = (prisma: Prisma.ProjectUserGetPayload<{ include: typeof ful
|
||||
account_id: a.providerAccountId,
|
||||
email: a.email,
|
||||
})),
|
||||
auth_methods: authMethods,
|
||||
connected_accounts: connectedAccounts,
|
||||
selected_team_id: selectedTeamMembers[0]?.teamId ?? null,
|
||||
selected_team: selectedTeamMembers[0] ? teamPrismaToCrud(selectedTeamMembers[0]?.team) : null,
|
||||
};
|
||||
|
||||
@ -222,11 +222,15 @@ model ProjectUser {
|
||||
projectUserRefreshTokens ProjectUserRefreshToken[]
|
||||
projectUserAuthorizationCodes ProjectUserAuthorizationCode[]
|
||||
projectUserOAuthAccounts ProjectUserOAuthAccount[]
|
||||
projectUserEmailVerificationCode ProjectUserEmailVerificationCode[]
|
||||
projectUserPasswordResetCode ProjectUserPasswordResetCode[]
|
||||
projectUserMagicLinkCode ProjectUserMagicLinkCode[]
|
||||
teamMembers TeamMember[]
|
||||
|
||||
// @deprecated
|
||||
projectUserEmailVerificationCode ProjectUserEmailVerificationCode[]
|
||||
// @deprecated
|
||||
projectUserPasswordResetCode ProjectUserPasswordResetCode[]
|
||||
// @deprecated
|
||||
projectUserMagicLinkCode ProjectUserMagicLinkCode[]
|
||||
|
||||
primaryEmail String?
|
||||
primaryEmailVerified Boolean
|
||||
profileImageUrl String?
|
||||
|
||||
@ -115,6 +115,7 @@ export namespace Auth {
|
||||
headers: expect.anything(),
|
||||
body: expect.anything(),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function expectToBeSignedOut() {
|
||||
|
||||
@ -17,7 +17,22 @@ it("should allow signing in to existing accounts", async ({ expect }) => {
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
await Auth.expectToBeSignedIn();
|
||||
const response = await Auth.expectToBeSignedIn();
|
||||
expect(response.body.auth_methods).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"identifier": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "password",
|
||||
},
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not allow signing in with an e-mail that never signed up", async ({ expect }) => {
|
||||
|
||||
@ -26,7 +26,22 @@ it("should sign up new users", async ({ expect }) => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
await Auth.expectToBeSignedIn();
|
||||
const response = await Auth.expectToBeSignedIn();
|
||||
expect(response.body.auth_methods).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"identifier": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "password",
|
||||
},
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not allow signing up with an e-mail that already exists", async ({ expect }) => {
|
||||
|
||||
@ -65,8 +65,18 @@ it("creates a team and manage users on the server", async ({ expect }) => {
|
||||
"is_paginated": false,
|
||||
"items": [
|
||||
{
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -80,8 +90,18 @@ it("creates a team and manage users on the server", async ({ expect }) => {
|
||||
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
|
||||
},
|
||||
{
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -125,8 +145,18 @@ it("creates a team and manage users on the server", async ({ expect }) => {
|
||||
"is_paginated": false,
|
||||
"items": [
|
||||
{
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
|
||||
@ -76,8 +76,18 @@ describe("with client access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -104,8 +114,18 @@ describe("with client access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -177,8 +197,18 @@ describe("with client access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -204,8 +234,18 @@ describe("with client access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": { "key": "value" },
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -299,8 +339,18 @@ describe("with client access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -326,8 +376,18 @@ describe("with client access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -413,8 +473,18 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -445,8 +515,18 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -497,8 +577,18 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -540,8 +630,18 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -564,8 +664,18 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
|
||||
@ -23,6 +23,8 @@ const clientReadSchema = usersCrudServerReadSchema.pick([
|
||||
"auth_with_email",
|
||||
"oauth_providers",
|
||||
"selected_team_id",
|
||||
"auth_methods",
|
||||
"connected_accounts",
|
||||
]).concat(yupObject({
|
||||
selected_team: teamsCrudClientReadSchema.nullable().defined(),
|
||||
})).nullable().defined(); // TODO: next-release: make required
|
||||
|
||||
@ -25,12 +25,41 @@ export const usersCrudServerReadSchema = fieldSchema.yupObject({
|
||||
profile_image_url: fieldSchema.profileImageUrlSchema.nullable().defined(),
|
||||
signed_up_at_millis: fieldSchema.signedUpAtMillisSchema.required(),
|
||||
has_password: fieldSchema.yupBoolean().required().meta({ openapiField: { description: 'Whether the user has a password associated with their account', exampleValue: true } }),
|
||||
auth_with_email: fieldSchema.yupBoolean().required().meta({ openapiField: { description: 'Whether the user can authenticate with their primary e-mail. If set to true, the user can log-in with credentials and/or magic link, if enabled in the project settings.', exampleValue: true } }),
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
auth_with_email: fieldSchema.yupBoolean().required().meta({ openapiField: { hidden: true, description: 'Whether the user can authenticate with their primary e-mail. If set to true, the user can log-in with credentials and/or magic link, if enabled in the project settings.', exampleValue: true } }),
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
oauth_providers: fieldSchema.yupArray(fieldSchema.yupObject({
|
||||
id: fieldSchema.yupString().required(),
|
||||
account_id: fieldSchema.yupString().required(),
|
||||
email: fieldSchema.yupString().nullable(),
|
||||
}).required()).required().meta({ openapiField: { description: 'A list of OAuth providers connected to this account', exampleValue: [{ id: 'google', account_id: '12345', email: 'john.doe@gmail.com' }] } }),
|
||||
}).required()).required().meta({ openapiField: { hidden: true, description: 'A list of OAuth providers connected to this account', exampleValue: [{ id: 'google', account_id: '12345', email: 'john.doe@gmail.com' }] } }),
|
||||
auth_methods: fieldSchema.yupArray(fieldSchema.yupUnion(
|
||||
fieldSchema.yupObject({
|
||||
type: fieldSchema.yupString().oneOf(['password']).required(),
|
||||
identifier: fieldSchema.yupString().required(),
|
||||
}).required(),
|
||||
fieldSchema.yupObject({
|
||||
type: fieldSchema.yupString().oneOf(['otp']).required(),
|
||||
contact_channel: fieldSchema.yupObject({
|
||||
type: fieldSchema.yupString().oneOf(['email']).required(),
|
||||
email: fieldSchema.yupString().required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
fieldSchema.yupObject({
|
||||
type: fieldSchema.yupString().oneOf(['oauth']).required(),
|
||||
provider: fieldSchema.userOAuthProviderSchema.required(),
|
||||
}).required(),
|
||||
)).required().meta({ openapiField: { description: 'A list of authentication methods available for this user to sign in with', exampleValue: [ { "contact_channel": { "email": "john.doe@gmail.com", "type": "email", }, "type": "otp", } ] } }),
|
||||
connected_accounts: fieldSchema.yupArray(fieldSchema.yupUnion(
|
||||
fieldSchema.yupObject({
|
||||
type: fieldSchema.yupString().oneOf(['oauth']).required(),
|
||||
provider: fieldSchema.userOAuthProviderSchema.required(),
|
||||
}).required(),
|
||||
)).required().meta({ openapiField: { description: 'A list of connected accounts to this user', exampleValue: [ { "provider": { "provider_user_id": "12345", "type": "google", }, "type": "oauth", } ] } }),
|
||||
client_metadata: fieldSchema.userClientMetadataSchema,
|
||||
server_metadata: fieldSchema.userServerMetadataSchema,
|
||||
}).required();
|
||||
@ -112,4 +141,4 @@ export const userDeletedWebhookEvent = {
|
||||
description: "This event is triggered when a user is deleted.",
|
||||
tags: ["Users"],
|
||||
},
|
||||
} satisfies WebhookEvent<typeof webhookUserDeletedSchema>;
|
||||
} satisfies WebhookEvent<typeof webhookUserDeletedSchema>;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import * as yup from "yup";
|
||||
import { StackAssertionError } from "./utils/errors";
|
||||
import { allProviders } from "./utils/oauth";
|
||||
import { isUuid } from "./utils/uuids";
|
||||
|
||||
@ -67,6 +68,30 @@ export function yupObject<A extends yup.Maybe<yup.AnyObject>, B extends yup.Obje
|
||||
}
|
||||
/* eslint-enable no-restricted-syntax */
|
||||
|
||||
export function yupUnion<T extends yup.ISchema<any>[]>(...args: T): yup.ISchema<yup.InferType<T[number]>> {
|
||||
if (args.length === 0) throw new Error('yupUnion must have at least one schema');
|
||||
|
||||
const [first] = args;
|
||||
const firstDesc = first.describe();
|
||||
for (const schema of args) {
|
||||
const desc = schema.describe();
|
||||
if (desc.type !== firstDesc.type) throw new StackAssertionError(`yupUnion must have schemas of the same type (got: ${firstDesc.type} and ${desc.type})`, { first, schema, firstDesc, desc });
|
||||
}
|
||||
|
||||
return yupMixed().required().test('is-one-of', 'Invalid value', async (value, context) => {
|
||||
const errors = [];
|
||||
for (const schema of args) {
|
||||
try {
|
||||
await schema.validate(value, context.options);
|
||||
return true;
|
||||
} catch (e) {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
throw new AggregateError(errors, 'Invalid value; must be one of the provided schemas');
|
||||
});
|
||||
}
|
||||
|
||||
// Common
|
||||
export const adaptSchema = yupMixed<StackAdaptSentinel>();
|
||||
/**
|
||||
@ -165,6 +190,10 @@ export const profileImageUrlSchema = yupString().meta({ openapiField: { descript
|
||||
export const signedUpAtMillisSchema = yupNumber().meta({ openapiField: { description: _signedUpAtMillisDescription, exampleValue: 1630000000000 } });
|
||||
export const userClientMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientMetaDataDescription('user'), exampleValue: { key: 'value' } } });
|
||||
export const userServerMetadataSchema = jsonSchema.meta({ openapiField: { description: _serverMetaDataDescription('user'), exampleValue: { key: 'value' } } });
|
||||
export const userOAuthProviderSchema = yupObject({
|
||||
type: yupString().required(),
|
||||
provider_user_id: yupString().required(),
|
||||
});
|
||||
|
||||
// Auth
|
||||
export const signInEmailSchema = emailSchema.meta({ openapiField: { description: 'The email to sign in with.', exampleValue: 'johndoe@example.com' } });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user