mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
New contact channels (#287)
* removed contact channels from otp * fixed types * fixed bugs * fixed bug * fixed bugs * updated user contact channel * updated tests * updated tests * added unique key to otp and password auth * removed contact channel from user object
This commit is contained in:
parent
d0b3d6e620
commit
28c3f57f31
@ -0,0 +1,64 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `contactChannelId` on the `OtpAuthMethod` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `identifier` on the `PasswordAuthMethod` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[projectId,type,value,usedForAuth]` on the table `ContactChannel` will be added. If there are existing duplicate values, this will fail.
|
||||
- You are about to drop the column `identifierType` on the `PasswordAuthMethod` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `identifierType` on the `PasswordAuthMethodConfig` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "OtpAuthMethod" DROP CONSTRAINT "OtpAuthMethod_projectId_projectUserId_contactChannelId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "OtpAuthMethod_projectId_contactChannelId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "PasswordAuthMethod_projectId_identifierType_identifier_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ContactChannel" ADD COLUMN "usedForAuth" "BooleanTrue";
|
||||
|
||||
-- Set the usedForAuth value to "TRUE" if the contact channel is used in `OtpAuthMethod` or the value is the same as the `PasswordAuthMethod` of the same user
|
||||
UPDATE "ContactChannel" cc
|
||||
SET "usedForAuth" = 'TRUE'
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM "OtpAuthMethod" oam
|
||||
WHERE oam."projectId" = cc."projectId"
|
||||
AND oam."projectUserId" = cc."projectUserId"
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM "PasswordAuthMethod" pam
|
||||
WHERE pam."projectId" = cc."projectId"
|
||||
AND pam."projectUserId" = cc."projectUserId"
|
||||
AND pam."identifier" = cc."value"
|
||||
);
|
||||
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "OtpAuthMethod" DROP COLUMN "contactChannelId";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "PasswordAuthMethod" DROP COLUMN "identifier";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ContactChannel_projectId_type_value_usedForAuth_key" ON "ContactChannel"("projectId", "type", "value", "usedForAuth");
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "PasswordAuthMethod" DROP COLUMN "identifierType";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "PasswordAuthMethodConfig" DROP COLUMN "identifierType";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "PasswordAuthMethodIdentifierType";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OtpAuthMethod_projectId_projectUserId_key" ON "OtpAuthMethod"("projectId", "projectUserId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PasswordAuthMethod_projectId_projectUserId_key" ON "PasswordAuthMethod"("projectId", "projectUserId");
|
||||
|
||||
@ -290,20 +290,22 @@ model ContactChannel {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
type ContactChannelType
|
||||
isPrimary BooleanTrue?
|
||||
isVerified Boolean
|
||||
value String
|
||||
type ContactChannelType
|
||||
isPrimary BooleanTrue?
|
||||
usedForAuth BooleanTrue?
|
||||
isVerified Boolean
|
||||
value String
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)
|
||||
otpAuthMethod OtpAuthMethod[]
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)
|
||||
|
||||
@@id([projectId, projectUserId, id])
|
||||
// each user has at most one primary contact channel of each type
|
||||
@@unique([projectId, projectUserId, type, isPrimary])
|
||||
// value must be unique per user per type
|
||||
@@unique([projectId, projectUserId, type, value])
|
||||
// only one contact channel per project with the same value and type can be used for auth
|
||||
@@unique([projectId, type, value, usedForAuth])
|
||||
}
|
||||
|
||||
model ConnectedAccountConfig {
|
||||
@ -379,11 +381,6 @@ model OtpAuthMethodConfig {
|
||||
@@id([projectConfigId, authMethodConfigId])
|
||||
}
|
||||
|
||||
enum PasswordAuthMethodIdentifierType {
|
||||
EMAIL
|
||||
// USERNAME
|
||||
}
|
||||
|
||||
model PasswordAuthMethodConfig {
|
||||
projectConfigId String @db.Uuid
|
||||
authMethodConfigId String @db.Uuid
|
||||
@ -393,8 +390,6 @@ model PasswordAuthMethodConfig {
|
||||
|
||||
authMethodConfig AuthMethodConfig @relation(fields: [projectConfigId, authMethodConfigId], references: [projectConfigId, id], onDelete: Cascade)
|
||||
|
||||
identifierType PasswordAuthMethodIdentifierType
|
||||
|
||||
@@id([projectConfigId, authMethodConfigId])
|
||||
}
|
||||
|
||||
@ -498,21 +493,19 @@ model AuthMethod {
|
||||
}
|
||||
|
||||
model OtpAuthMethod {
|
||||
projectId String
|
||||
authMethodId String @db.Uuid
|
||||
contactChannelId String @db.Uuid
|
||||
projectUserId String @db.Uuid
|
||||
projectId String
|
||||
authMethodId String @db.Uuid
|
||||
projectUserId String @db.Uuid
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
contactChannel ContactChannel @relation(fields: [projectId, projectUserId, contactChannelId], references: [projectId, projectUserId, id], onDelete: Cascade)
|
||||
authMethod AuthMethod @relation(fields: [projectId, authMethodId], references: [projectId, id], onDelete: Cascade)
|
||||
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)
|
||||
authMethod AuthMethod @relation(fields: [projectId, authMethodId], references: [projectId, id], onDelete: Cascade)
|
||||
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)
|
||||
|
||||
@@id([projectId, authMethodId])
|
||||
// each contact channel can only be used once per project as an otp method
|
||||
@@unique([projectId, contactChannelId])
|
||||
// a user can only have one OTP auth method
|
||||
@@unique([projectId, projectUserId])
|
||||
}
|
||||
|
||||
model PasswordAuthMethod {
|
||||
@ -523,17 +516,14 @@ model PasswordAuthMethod {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
identifierType PasswordAuthMethodIdentifierType
|
||||
// The identifier is the email or username, depending on the type.
|
||||
identifier String
|
||||
passwordHash String
|
||||
passwordHash String
|
||||
|
||||
authMethod AuthMethod @relation(fields: [projectId, authMethodId], references: [projectId, id], onDelete: Cascade)
|
||||
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)
|
||||
|
||||
@@id([projectId, authMethodId])
|
||||
// each identifier of each type can only occur once per project
|
||||
@@unique([projectId, identifierType, identifier])
|
||||
// a user can only have one password auth method
|
||||
@@unique([projectId, projectUserId])
|
||||
}
|
||||
|
||||
// This connects to projectUserOauthAccount, which might be shared between auth method and connected account.
|
||||
|
||||
@ -81,9 +81,7 @@ async function seed() {
|
||||
},
|
||||
{
|
||||
passwordConfig: {
|
||||
create: {
|
||||
identifierType: 'EMAIL',
|
||||
}
|
||||
create: {}
|
||||
}
|
||||
},
|
||||
...(['github', 'spotify', 'google', 'microsoft'] as const).map((id) => ({
|
||||
|
||||
@ -39,33 +39,38 @@ export const POST = createSmartRouteHandler({
|
||||
throw new StatusError(StatusError.Forbidden, "Magic link is not enabled for this project");
|
||||
}
|
||||
|
||||
const authMethods = await prismaClient.otpAuthMethod.findMany({
|
||||
const contactChannel = await prismaClient.contactChannel.findUnique({
|
||||
where: {
|
||||
projectId: project.id,
|
||||
contactChannel: {
|
||||
projectId_type_value_usedForAuth: {
|
||||
projectId: project.id,
|
||||
type: "EMAIL",
|
||||
value: email,
|
||||
},
|
||||
usedForAuth: "TRUE",
|
||||
}
|
||||
},
|
||||
include: {
|
||||
projectUser: true,
|
||||
contactChannel: true,
|
||||
projectUser: {
|
||||
include: {
|
||||
authMethods: {
|
||||
include: {
|
||||
otpAuthMethod: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (authMethods.length > 1) {
|
||||
throw new StackAssertionError("Tried to send OTP sign in code but found multiple auth methods? The uniqueness on the DB schema should prevent this");
|
||||
}
|
||||
const authMethod = authMethods.length === 1 ? authMethods[0] : null;
|
||||
const otpAuthMethod = contactChannel?.projectUser.authMethods.find((m) => m.otpAuthMethod)?.otpAuthMethod;
|
||||
|
||||
const isNewUser = !authMethod;
|
||||
const isNewUser = !otpAuthMethod;
|
||||
if (isNewUser && !project.config.sign_up_enabled) {
|
||||
throw new KnownErrors.SignUpNotEnabled();
|
||||
}
|
||||
|
||||
let user;
|
||||
|
||||
if (!authMethod) {
|
||||
if (!otpAuthMethod) {
|
||||
// TODO this should be in the same transaction as the read above
|
||||
user = await usersCrudHandlers.adminCreate({
|
||||
project,
|
||||
@ -79,7 +84,7 @@ export const POST = createSmartRouteHandler({
|
||||
} else {
|
||||
user = await usersCrudHandlers.adminRead({
|
||||
project,
|
||||
user_id: authMethod.projectUser.projectUserId,
|
||||
user_id: contactChannel.projectUser.projectUserId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -55,33 +55,39 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({
|
||||
};
|
||||
},
|
||||
async handler(project, { email }, data) {
|
||||
const authMethods = await prismaClient.otpAuthMethod.findMany({
|
||||
const contactChannel = await prismaClient.contactChannel.findUnique({
|
||||
where: {
|
||||
projectId: project.id,
|
||||
contactChannel: {
|
||||
projectId_type_value_usedForAuth: {
|
||||
projectId: project.id,
|
||||
type: "EMAIL",
|
||||
value: email,
|
||||
},
|
||||
usedForAuth: "TRUE",
|
||||
}
|
||||
},
|
||||
include: {
|
||||
projectUser: true,
|
||||
projectUser: {
|
||||
include: {
|
||||
authMethods: {
|
||||
include: {
|
||||
otpAuthMethod: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (authMethods.length === 0) {
|
||||
const otpAuthMethod = contactChannel?.projectUser.authMethods.find((m) => m.otpAuthMethod)?.otpAuthMethod;
|
||||
|
||||
if (!contactChannel || !otpAuthMethod) {
|
||||
throw new StackAssertionError("Tried to use OTP sign in but auth method was not found?");
|
||||
}
|
||||
if (authMethods.length > 1) {
|
||||
throw new StackAssertionError("Tried to use OTP sign in but found multiple auth methods? The uniqueness on the DB schema should prevent this");
|
||||
}
|
||||
|
||||
const authMethod = authMethods[0];
|
||||
|
||||
if (authMethod.projectUser.requiresTotpMfa) {
|
||||
if (contactChannel.projectUser.requiresTotpMfa) {
|
||||
throw await createMfaRequiredError({
|
||||
project,
|
||||
isNewUser: data.is_new_user,
|
||||
userId: authMethod.projectUserId,
|
||||
userId: contactChannel.projectUser.projectUserId,
|
||||
});
|
||||
}
|
||||
|
||||
@ -89,7 +95,7 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({
|
||||
where: {
|
||||
projectId_projectUserId_type_value: {
|
||||
projectId: project.id,
|
||||
projectUserId: authMethod.projectUserId,
|
||||
projectUserId: contactChannel.projectUser.projectUserId,
|
||||
type: "EMAIL",
|
||||
value: email,
|
||||
}
|
||||
@ -101,7 +107,7 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({
|
||||
|
||||
const { refreshToken, accessToken } = await createAuthTokens({
|
||||
projectId: project.id,
|
||||
projectUserId: authMethod.projectUserId,
|
||||
projectUserId: contactChannel.projectUser.projectUserId,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -37,39 +37,50 @@ export const POST = createSmartRouteHandler({
|
||||
throw new KnownErrors.PasswordAuthenticationNotEnabled();
|
||||
}
|
||||
|
||||
const authMethod = await prismaClient.passwordAuthMethod.findUnique({
|
||||
const contactChannel = await prismaClient.contactChannel.findUnique({
|
||||
where: {
|
||||
projectId_identifierType_identifier: {
|
||||
projectId_type_value_usedForAuth: {
|
||||
projectId: project.id,
|
||||
identifierType: "EMAIL",
|
||||
identifier: email,
|
||||
type: "EMAIL",
|
||||
value: email,
|
||||
usedForAuth: "TRUE",
|
||||
}
|
||||
},
|
||||
include: {
|
||||
projectUser: true,
|
||||
projectUser: {
|
||||
include: {
|
||||
authMethods: {
|
||||
include: {
|
||||
passwordAuthMethod: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const passwordAuthMethod = contactChannel?.projectUser.authMethods.find((m) => m.passwordAuthMethod)?.passwordAuthMethod;
|
||||
|
||||
// we compare the password even if the authMethod doesn't exist to prevent timing attacks
|
||||
if (!await comparePassword(password, authMethod?.passwordHash || "")) {
|
||||
if (!await comparePassword(password, passwordAuthMethod?.passwordHash || "")) {
|
||||
throw new KnownErrors.EmailPasswordMismatch();
|
||||
}
|
||||
|
||||
if (!authMethod) {
|
||||
if (!contactChannel || !passwordAuthMethod) {
|
||||
throw new StackAssertionError("This should never happen (the comparePassword call should've already caused this to fail)");
|
||||
}
|
||||
|
||||
if (authMethod.projectUser.requiresTotpMfa) {
|
||||
if (contactChannel.projectUser.requiresTotpMfa) {
|
||||
throw await createMfaRequiredError({
|
||||
project,
|
||||
isNewUser: false,
|
||||
userId: authMethod.projectUser.projectUserId,
|
||||
userId: contactChannel.projectUser.projectUserId,
|
||||
});
|
||||
}
|
||||
|
||||
const { refreshToken, accessToken } = await createAuthTokens({
|
||||
projectId: project.id,
|
||||
projectUserId: authMethod.projectUser.projectUserId,
|
||||
projectUserId: contactChannel.projectUser.projectUserId,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -78,7 +89,7 @@ export const POST = createSmartRouteHandler({
|
||||
body: {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
user_id: authMethod.projectUser.projectUserId,
|
||||
user_id: contactChannel.projectUser.projectUserId,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@ -141,9 +141,7 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand
|
||||
...(data.config?.credential_enabled ?? true) ? [{
|
||||
enabled: true,
|
||||
passwordConfig: {
|
||||
create: {
|
||||
identifierType: 'EMAIL',
|
||||
}
|
||||
create: {}
|
||||
},
|
||||
}] : [],
|
||||
]
|
||||
|
||||
@ -339,7 +339,6 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro
|
||||
const passwordAuth = await tx.passwordAuthMethodConfig.findFirst({
|
||||
where: {
|
||||
projectConfigId: oldProject.config.id,
|
||||
identifierType: "EMAIL",
|
||||
},
|
||||
});
|
||||
if (data.config?.credential_enabled !== undefined) {
|
||||
@ -349,9 +348,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro
|
||||
projectConfigId: oldProject.config.id,
|
||||
enabled: data.config.credential_enabled,
|
||||
passwordConfig: {
|
||||
create: {
|
||||
identifierType: "EMAIL",
|
||||
},
|
||||
create: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -22,37 +22,14 @@ export const userFullInclude = {
|
||||
providerConfig: true,
|
||||
},
|
||||
},
|
||||
contactChannels: true,
|
||||
authMethods: {
|
||||
include: {
|
||||
passwordAuthMethod: true,
|
||||
oauthAuthMethod: {
|
||||
include: {
|
||||
oauthProviderConfig: {
|
||||
include: {
|
||||
proxiedOAuthConfig: true,
|
||||
standardOAuthConfig: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
otpAuthMethod: {
|
||||
include: {
|
||||
contactChannel: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
connectedAccounts: {
|
||||
include: {
|
||||
oauthProviderConfig: {
|
||||
include: {
|
||||
proxiedOAuthConfig: true,
|
||||
standardOAuthConfig: true,
|
||||
},
|
||||
}
|
||||
otpAuthMethod: true,
|
||||
oauthAuthMethod: true,
|
||||
}
|
||||
},
|
||||
contactChannels: true,
|
||||
teamMembers: {
|
||||
include: {
|
||||
team: true,
|
||||
@ -70,8 +47,12 @@ export const contactChannelToCrud = (channel: Prisma.ContactChannelGetPayload<{}
|
||||
}
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
type: 'email',
|
||||
email: channel.value,
|
||||
value: channel.value,
|
||||
is_primary: !!channel.isPrimary,
|
||||
is_verified: channel.isVerified,
|
||||
used_for_auth: !!channel.usedForAuth,
|
||||
};
|
||||
};
|
||||
|
||||
@ -105,49 +86,6 @@ export const userPrismaToCrud = (
|
||||
throw new StackAssertionError("User cannot have more than one selected team; this should never happen");
|
||||
}
|
||||
|
||||
const authMethods: UsersCrud["Admin"]["Read"]["auth_methods"] = prisma.authMethods
|
||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
||||
.map((m) => {
|
||||
if ([m.passwordAuthMethod, m.otpAuthMethod, m.oauthAuthMethod].filter(Boolean).length > 1) {
|
||||
throw new StackAssertionError(`AuthMethod ${m.id} violates the union constraint`, m);
|
||||
}
|
||||
|
||||
if (m.passwordAuthMethod) {
|
||||
return {
|
||||
type: 'password',
|
||||
identifier: m.passwordAuthMethod.identifier,
|
||||
};
|
||||
} else if (m.otpAuthMethod) {
|
||||
return {
|
||||
type: 'otp',
|
||||
contact_channel: {
|
||||
type: 'email',
|
||||
email: m.otpAuthMethod.contactChannel.value,
|
||||
},
|
||||
};
|
||||
} else if (m.oauthAuthMethod) {
|
||||
return {
|
||||
type: 'oauth',
|
||||
provider: {
|
||||
...oauthProviderConfigToCrud(m.oauthAuthMethod.oauthProviderConfig),
|
||||
provider_user_id: m.oauthAuthMethod.providerAccountId,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new StackAssertionError("AuthMethod has no auth methods", m);
|
||||
}
|
||||
});
|
||||
|
||||
const connectedAccounts: UsersCrud["Admin"]["Read"]["connected_accounts"] = prisma.connectedAccounts.map((a) => {
|
||||
return {
|
||||
type: 'oauth',
|
||||
provider: {
|
||||
...oauthProviderConfigToCrud(a.oauthProviderConfig),
|
||||
provider_user_id: a.providerAccountId,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const primaryEmailContactChannel = prisma.contactChannels.find((c) => c.type === 'EMAIL' && c.isPrimary);
|
||||
const passwordAuth = prisma.authMethods.find((m) => m.passwordAuthMethod);
|
||||
@ -171,8 +109,6 @@ export const userPrismaToCrud = (
|
||||
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,
|
||||
last_active_at_millis: lastActiveAtMillis,
|
||||
@ -201,30 +137,18 @@ async function checkAuthData(
|
||||
}
|
||||
if (data.primaryEmailAuthEnabled) {
|
||||
if (!data.oldPrimaryEmail || data.oldPrimaryEmail !== data.primaryEmail) {
|
||||
const otpAuth = await tx.otpAuthMethod.findFirst({
|
||||
const otpAuth = await tx.contactChannel.findFirst({
|
||||
where: {
|
||||
projectId: data.projectId,
|
||||
contactChannel: {
|
||||
type: 'EMAIL',
|
||||
value: data.primaryEmail || throwErr("primary_email_auth_enabled is true but primary_email is not set"),
|
||||
},
|
||||
type: 'EMAIL',
|
||||
value: data.primaryEmail || throwErr("primary_email_auth_enabled is true but primary_email is not set"),
|
||||
usedForAuth: BooleanTrue.TRUE,
|
||||
}
|
||||
});
|
||||
|
||||
if (otpAuth) {
|
||||
throw new KnownErrors.UserEmailAlreadyExists();
|
||||
}
|
||||
|
||||
const passwordAuth = await tx.passwordAuthMethod.findFirst({
|
||||
where: {
|
||||
projectId: data.projectId,
|
||||
identifier: data.primaryEmail || throwErr("primary_email_auth_enabled is true but primary_email is not set"),
|
||||
}
|
||||
});
|
||||
|
||||
if (passwordAuth) {
|
||||
throw new KnownErrors.UserEmailAlreadyExists();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -460,7 +384,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
}
|
||||
|
||||
if (data.primary_email) {
|
||||
const contactChannel = await tx.contactChannel.create({
|
||||
await tx.contactChannel.create({
|
||||
data: {
|
||||
projectUserId: newUser.projectUserId,
|
||||
projectId: auth.project.id,
|
||||
@ -468,6 +392,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
value: data.primary_email || throwErr("primary_email_auth_enabled is true but primary_email is not set"),
|
||||
isVerified: data.primary_email_verified ?? false,
|
||||
isPrimary: "TRUE",
|
||||
usedForAuth: data.primary_email_auth_enabled ? BooleanTrue.TRUE : null,
|
||||
}
|
||||
});
|
||||
|
||||
@ -484,7 +409,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
otpAuthMethod: {
|
||||
create: {
|
||||
projectUserId: newUser.projectUserId,
|
||||
contactChannelId: contactChannel.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -507,9 +431,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
authMethodConfigId: passwordConfig.authMethodConfigId,
|
||||
passwordAuthMethod: {
|
||||
create: {
|
||||
identifier: data.primary_email || throwErr("password is set but primary_email is not"),
|
||||
passwordHash: await hashPassword(data.password),
|
||||
identifierType: 'EMAIL',
|
||||
projectUserId: newUser.projectUserId,
|
||||
}
|
||||
}
|
||||
@ -612,8 +534,8 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const primaryEmailContactChannel = oldUser.contactChannels.find((c) => c.type === 'EMAIL' && c.isPrimary);
|
||||
const otpAuth = oldUser.authMethods.find((m) => m.otpAuthMethod && m.otpAuthMethod.contactChannel.id === primaryEmailContactChannel?.id)?.otpAuthMethod;
|
||||
const passwordAuth = oldUser.authMethods.find((m) => m.passwordAuthMethod && m.passwordAuthMethod.identifier === primaryEmailContactChannel?.value)?.passwordAuthMethod;
|
||||
const otpAuth = oldUser.authMethods.find((m) => m.otpAuthMethod)?.otpAuthMethod;
|
||||
const passwordAuth = oldUser.authMethods.find((m) => m.passwordAuthMethod)?.passwordAuthMethod;
|
||||
|
||||
await checkAuthData(tx, {
|
||||
projectId: auth.project.id,
|
||||
@ -627,10 +549,8 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
// if there is a new primary email
|
||||
// - create a new primary email contact channel if it doesn't exist
|
||||
// - update the primary email contact channel if it exists
|
||||
// - update the password auth method if it exists
|
||||
// if the primary email is null
|
||||
// - delete the primary email contact channel if it exists (note that this will also delete the related auth methods)
|
||||
// - delete the password auth method if it exists
|
||||
if (data.primary_email !== undefined) {
|
||||
if (data.primary_email === null) {
|
||||
await tx.contactChannel.delete({
|
||||
@ -643,17 +563,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (passwordAuth) {
|
||||
await tx.authMethod.delete({
|
||||
where: {
|
||||
projectId_id: {
|
||||
projectId: auth.project.id,
|
||||
id: passwordAuth.authMethodId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await tx.contactChannel.upsert({
|
||||
where: {
|
||||
@ -674,22 +583,9 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
},
|
||||
update: {
|
||||
value: data.primary_email,
|
||||
usedForAuth: data.primary_email_auth_enabled ? BooleanTrue.TRUE : null,
|
||||
}
|
||||
});
|
||||
|
||||
if (passwordAuth) {
|
||||
await tx.passwordAuthMethod.update({
|
||||
where: {
|
||||
projectId_authMethodId: {
|
||||
projectId: auth.project.id,
|
||||
authMethodId: passwordAuth.authMethodId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
identifier: data.primary_email,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -743,7 +639,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
otpAuthMethod: {
|
||||
create: {
|
||||
projectUserId: params.user_id,
|
||||
contactChannelId: primaryEmailChannel.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -821,9 +716,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
|
||||
authMethodConfigId: passwordConfig.authMethodConfigId,
|
||||
passwordAuthMethod: {
|
||||
create: {
|
||||
identifier: primaryEmailChannel.value,
|
||||
passwordHash: await hashPassword(data.password),
|
||||
identifierType: 'EMAIL',
|
||||
projectUserId: params.user_id,
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,20 +34,9 @@ describe("with grant_type === 'authorization_code'", async () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"provider": {
|
||||
"id": "spotify",
|
||||
"provider_user_id": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "spotify",
|
||||
},
|
||||
"type": "oauth",
|
||||
},
|
||||
],
|
||||
"auth_with_email": false,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
|
||||
@ -18,22 +18,30 @@ it("should allow signing in to existing accounts", async ({ expect }) => {
|
||||
}
|
||||
`);
|
||||
const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client" });
|
||||
expect(response.body.auth_methods).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"display_name": null,
|
||||
"has_password": true,
|
||||
"id": "<stripped UUID>",
|
||||
"oauth_providers": [],
|
||||
"primary_email": "<stripped UUID>@stack-generated.example.com",
|
||||
"primary_email_verified": false,
|
||||
"profile_image_url": null,
|
||||
"requires_totp_mfa": false,
|
||||
"selected_team": null,
|
||||
"selected_team_id": null,
|
||||
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
|
||||
},
|
||||
{
|
||||
"identifier": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "password",
|
||||
},
|
||||
]
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
});
|
||||
// TODO: check auth methods
|
||||
|
||||
it("should not allow signing in with an e-mail that never signed up", async ({ expect }) => {
|
||||
const response = await niceBackendFetch("/api/v1/auth/password/sign-in", {
|
||||
|
||||
@ -27,20 +27,27 @@ it("should sign up new users", async ({ expect }) => {
|
||||
]
|
||||
`);
|
||||
const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client" });
|
||||
expect(response.body.auth_methods).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"display_name": null,
|
||||
"has_password": true,
|
||||
"id": "<stripped UUID>",
|
||||
"oauth_providers": [],
|
||||
"primary_email": "<stripped UUID>@stack-generated.example.com",
|
||||
"primary_email_verified": false,
|
||||
"profile_image_url": null,
|
||||
"requires_totp_mfa": false,
|
||||
"selected_team": null,
|
||||
"selected_team_id": null,
|
||||
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
|
||||
},
|
||||
{
|
||||
"identifier": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "password",
|
||||
},
|
||||
]
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
@ -68,19 +68,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -96,19 +86,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -154,19 +134,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
|
||||
@ -77,19 +77,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -117,19 +107,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -202,19 +182,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -241,19 +211,9 @@ 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" },
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -390,19 +350,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -429,19 +379,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -566,19 +506,9 @@ 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" },
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -682,19 +612,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -727,19 +647,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -788,19 +698,9 @@ describe("with server access", () => {
|
||||
"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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -838,19 +738,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -881,11 +771,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 201,
|
||||
"body": {
|
||||
"auth_methods": [],
|
||||
"auth_with_email": false,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -920,19 +808,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 201,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Dough",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -967,23 +845,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 201,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
{
|
||||
"identifier": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "password",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": true,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1063,19 +927,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 201,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1128,11 +982,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 201,
|
||||
"body": {
|
||||
"auth_methods": [],
|
||||
"auth_with_email": false,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1164,23 +1016,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 201,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
{
|
||||
"identifier": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "password",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": true,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1227,23 +1065,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 201,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
{
|
||||
"identifier": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "password",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": true,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1272,11 +1096,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 201,
|
||||
"body": {
|
||||
"auth_methods": [],
|
||||
"auth_with_email": false,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1329,19 +1151,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1366,19 +1178,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1412,19 +1214,9 @@ 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,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": "John Doe",
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1458,23 +1250,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
{
|
||||
"identifier": "<stripped UUID>@stack-generated.example.com",
|
||||
"type": "password",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": true,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1557,19 +1335,9 @@ 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": { "key": "client value" },
|
||||
"client_read_only_metadata": { "key": "client read only value" },
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
@ -1614,19 +1382,9 @@ describe("with server access", () => {
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"auth_methods": [
|
||||
{
|
||||
"contact_channel": {
|
||||
"email": "new-primary-email@example.com",
|
||||
"type": "email",
|
||||
},
|
||||
"type": "otp",
|
||||
},
|
||||
],
|
||||
"auth_with_email": true,
|
||||
"client_metadata": null,
|
||||
"client_read_only_metadata": null,
|
||||
"connected_accounts": [],
|
||||
"display_name": null,
|
||||
"has_password": false,
|
||||
"id": "<stripped UUID>",
|
||||
|
||||
@ -26,8 +26,6 @@ const clientReadSchema = usersCrudServerReadSchema.pick([
|
||||
"auth_with_email",
|
||||
"oauth_providers",
|
||||
"selected_team_id",
|
||||
"auth_methods",
|
||||
"connected_accounts",
|
||||
"requires_totp_mfa",
|
||||
]).concat(yupObject({
|
||||
selected_team: teamsCrudClientReadSchema.nullable().defined(),
|
||||
|
||||
@ -27,6 +27,17 @@ 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 } }),
|
||||
client_metadata: fieldSchema.userClientMetadataSchema,
|
||||
client_read_only_metadata: fieldSchema.userClientReadOnlyMetadataSchema,
|
||||
server_metadata: fieldSchema.userServerMetadataSchema,
|
||||
last_active_at_millis: fieldSchema.userLastActiveAtMillisSchema.required(),
|
||||
|
||||
oauth_providers: fieldSchema.yupArray(fieldSchema.yupObject({
|
||||
id: fieldSchema.yupString().required(),
|
||||
account_id: fieldSchema.yupString().required(),
|
||||
email: fieldSchema.yupString().nullable(),
|
||||
}).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' }] } }),
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@ -35,41 +46,6 @@ export const usersCrudServerReadSchema = fieldSchema.yupObject({
|
||||
* @deprecated
|
||||
*/
|
||||
requires_totp_mfa: fieldSchema.yupBoolean().required().meta({ openapiField: { hidden: true, description: 'Whether the user is required to use TOTP MFA to sign in', exampleValue: false } }),
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
oauth_providers: fieldSchema.yupArray(fieldSchema.yupObject({
|
||||
id: fieldSchema.yupString().required(),
|
||||
account_id: fieldSchema.yupString().required(),
|
||||
email: fieldSchema.yupString().nullable(),
|
||||
}).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: { hidden: true, 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: { hidden: true, description: 'A list of connected accounts to this user', exampleValue: [ { "provider": { "provider_user_id": "12345", "type": "google", }, "type": "oauth", } ] } }),
|
||||
client_metadata: fieldSchema.userClientMetadataSchema,
|
||||
client_read_only_metadata: fieldSchema.userClientReadOnlyMetadataSchema,
|
||||
server_metadata: fieldSchema.userServerMetadataSchema,
|
||||
last_active_at_millis: fieldSchema.userLastActiveAtMillisSchema.required(),
|
||||
}).required();
|
||||
|
||||
export const usersCrudServerCreateSchema = usersCrudServerUpdateSchema.omit(['selected_team_id']).concat(fieldSchema.yupObject({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user