mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
Compare commits
1 Commits
fix/v2-api
...
cursor/upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c71832183e |
72
SCHEMA_CHANGES_SUMMARY.md
Normal file
72
SCHEMA_CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# User Schema Changes: Separate First and Last Name Fields
|
||||
|
||||
## Overview
|
||||
Successfully updated the User model in the formbricks-poc project to use separate `firstName` and `lastName` fields instead of a single `name` field.
|
||||
|
||||
## Schema Changes
|
||||
|
||||
### 1. Database Schema (packages/database/schema.prisma)
|
||||
- **Changed User model fields:**
|
||||
- Removed: `name String`
|
||||
- Added: `firstName String` and `lastName String`
|
||||
- **Updated documentation comments** to reflect the new field structure
|
||||
|
||||
### 2. Type Definitions (packages/types/user.ts)
|
||||
- **Updated ZUser schema:** Changed from `name: ZUserName` to `firstName: ZUserName, lastName: ZUserName`
|
||||
- **Updated ZUserUpdateInput schema:** Changed from `name: ZUserName.optional()` to `firstName: ZUserName.optional(), lastName: ZUserName.optional()`
|
||||
- **Updated ZUserCreateInput schema:** Changed from `name: ZUserName` to `firstName: ZUserName, lastName: ZUserName`
|
||||
|
||||
## Application Code Updates
|
||||
|
||||
### 3. Core User Functions (apps/web/modules/auth/lib/user.ts)
|
||||
- **Updated createUser function:** Modified select statement to return `firstName` and `lastName` instead of `name`
|
||||
|
||||
### 4. Database ZOD Schema (packages/database/zod/users.ts)
|
||||
- **Updated ZUser description** to reflect that `name` field now represents the full name (concatenated firstName + lastName)
|
||||
|
||||
### 5. User Authentication & Signup (apps/web/modules/auth/signup/actions.ts)
|
||||
- **Updated ZCreatedUser type** to use `firstName` and `lastName`
|
||||
- **Updated ZCreateUserAction schema** to accept `firstName` and `lastName`
|
||||
- **Modified all functions** to handle the new field structure:
|
||||
- `verifyTurnstileIfConfigured()` - now accepts firstName and lastName parameters
|
||||
- `createUserSafely()` - now accepts firstName and lastName parameters
|
||||
- `handleInviteAcceptance()` - concatenates firstName and lastName for display
|
||||
- `handleOrganizationCreation()` - concatenates firstName and lastName for organization name
|
||||
|
||||
### 6. API Endpoints (apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts)
|
||||
- **Updated getUsers function:** Returns concatenated firstName + lastName as `name` field
|
||||
- **Updated createUser function:** Splits input `name` into firstName and lastName for storage
|
||||
- **Updated updateUser function:** Splits input `name` into firstName and lastName for updates
|
||||
- **Maintained backward compatibility:** API still accepts/returns `name` field for external consumers
|
||||
|
||||
### 7. Team Management (apps/web/modules/organization/settings/teams/lib/membership.ts)
|
||||
- **Updated select statements** to fetch `firstName` and `lastName` instead of `name`
|
||||
- **Updated mapping functions** to concatenate firstName and lastName for display
|
||||
|
||||
### 8. Organization & Team Actions
|
||||
- **Updated sendInviteMemberEmail calls** to use concatenated firstName + lastName
|
||||
- **Updated creator name references** to use concatenated firstName + lastName
|
||||
- **Updated user context references** to use concatenated firstName + lastName
|
||||
|
||||
### 9. Additional Files Updated
|
||||
- **Two-factor authentication** (apps/web/modules/ee/two-factor-auth/lib/two-factor-auth.ts)
|
||||
- **Email customization** (apps/web/modules/ee/whitelabel/email-customization/actions.ts)
|
||||
- **Team management** (apps/web/modules/ee/teams/team-list/lib/team.ts)
|
||||
- **Response utilities** (apps/web/lib/response/utils.ts)
|
||||
|
||||
## Migration Required
|
||||
A database migration will be needed to apply these schema changes:
|
||||
```bash
|
||||
npx prisma migrate dev --name "separate-user-first-and-last-name"
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
- **API endpoints** continue to work with the existing `name` field format
|
||||
- **Frontend components** that display user names will show the concatenated firstName + lastName
|
||||
- **Database operations** now use the separate firstName and lastName fields internally
|
||||
|
||||
## Notes
|
||||
- All user display names are now formatted as `${firstName} ${lastName}`
|
||||
- Input validation remains the same using the existing `ZUserName` schema
|
||||
- The change maintains data integrity while providing more flexibility for user name management
|
||||
- All existing functionality has been preserved while using the new field structure
|
||||
@@ -523,7 +523,7 @@ export const getResponsesJson = (
|
||||
"Survey ID": response.surveyId,
|
||||
"Formbricks ID (internal)": response.contact?.id || "",
|
||||
"User ID": response.contact?.userId || "",
|
||||
Notes: response.notes.map((note) => `${note.user.name}: ${note.text}`).join("\n"),
|
||||
Notes: response.notes.map((note) => `${note.user.firstName} ${note.user.lastName}: ${note.text}`).join("\n"),
|
||||
Tags: response.tags.map((tag) => tag.name).join(", "),
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export const getUsers = async (
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
name: `${user.firstName} ${user.lastName}`,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
isActive: user.isActive,
|
||||
role: user.memberships.filter((membership) => membership.organizationId === organizationId)[0].role,
|
||||
@@ -93,8 +93,14 @@ export const createUser = async (
|
||||
}));
|
||||
}
|
||||
|
||||
// Split name into firstName and lastName
|
||||
const nameParts = name.split(" ");
|
||||
const firstName = nameParts[0] || "";
|
||||
const lastName = nameParts.slice(1).join(" ") || "";
|
||||
|
||||
const prismaData: Prisma.UserCreateInput = {
|
||||
name,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
isActive: isActive,
|
||||
memberships: {
|
||||
@@ -133,7 +139,7 @@ export const createUser = async (
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
name: `${user.firstName} ${user.lastName}`,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
isActive: user.isActive,
|
||||
role: user.memberships.filter((membership) => membership.organizationId === organizationId)[0].role,
|
||||
@@ -240,8 +246,18 @@ export const updateUser = async (
|
||||
}
|
||||
});
|
||||
|
||||
// Split name into firstName and lastName if provided
|
||||
let firstName: string | undefined;
|
||||
let lastName: string | undefined;
|
||||
if (name) {
|
||||
const nameParts = name.split(" ");
|
||||
firstName = nameParts[0] || "";
|
||||
lastName = nameParts.slice(1).join(" ") || "";
|
||||
}
|
||||
|
||||
const prismaData: Prisma.UserUpdateInput = {
|
||||
name: name ?? undefined,
|
||||
firstName: firstName ?? undefined,
|
||||
lastName: lastName ?? undefined,
|
||||
email: email ?? undefined,
|
||||
isActive: isActive ?? undefined,
|
||||
memberships: {
|
||||
@@ -281,7 +297,7 @@ export const updateUser = async (
|
||||
createdAt: updatedUser.createdAt,
|
||||
updatedAt: updatedUser.updatedAt,
|
||||
email: updatedUser.email,
|
||||
name: updatedUser.name,
|
||||
name: `${updatedUser.firstName} ${updatedUser.lastName}`,
|
||||
lastLoginAt: updatedUser.lastLoginAt,
|
||||
isActive: updatedUser.isActive,
|
||||
role: updatedUser.memberships.find(
|
||||
|
||||
@@ -124,7 +124,8 @@ export const createUser = async (data: TUserCreateInput) => {
|
||||
const user = await prisma.user.create({
|
||||
data: data,
|
||||
select: {
|
||||
name: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
notificationSettings: true,
|
||||
id: true,
|
||||
email: true,
|
||||
|
||||
@@ -19,7 +19,8 @@ import { InvalidInputError, UnknownError } from "@formbricks/types/errors";
|
||||
import { ZUser, ZUserEmail, ZUserLocale, ZUserName, ZUserPassword } from "@formbricks/types/user";
|
||||
|
||||
const ZCreatedUser = ZUser.pick({
|
||||
name: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
id: true,
|
||||
@@ -29,7 +30,8 @@ const ZCreatedUser = ZUser.pick({
|
||||
type TCreatedUser = z.infer<typeof ZCreatedUser>;
|
||||
|
||||
const ZCreateUserAction = z.object({
|
||||
name: ZUserName,
|
||||
firstName: ZUserName,
|
||||
lastName: ZUserName,
|
||||
email: ZUserEmail,
|
||||
password: ZUserPassword,
|
||||
inviteToken: z.string().optional(),
|
||||
@@ -47,25 +49,27 @@ const ZCreateUserAction = z.object({
|
||||
async function verifyTurnstileIfConfigured(
|
||||
turnstileToken: string | undefined,
|
||||
email: string,
|
||||
name: string
|
||||
firstName: string,
|
||||
lastName: string
|
||||
): Promise<void> {
|
||||
if (!IS_TURNSTILE_CONFIGURED) return;
|
||||
|
||||
if (!turnstileToken || !TURNSTILE_SECRET_KEY) {
|
||||
captureFailedSignup(email, name);
|
||||
captureFailedSignup(email, `${firstName} ${lastName}`);
|
||||
throw new UnknownError("Server configuration error");
|
||||
}
|
||||
|
||||
const isHuman = await verifyTurnstileToken(TURNSTILE_SECRET_KEY, turnstileToken);
|
||||
if (!isHuman) {
|
||||
captureFailedSignup(email, name);
|
||||
captureFailedSignup(email, `${firstName} ${lastName}`);
|
||||
throw new UnknownError("reCAPTCHA verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function createUserSafely(
|
||||
email: string,
|
||||
name: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
hashedPassword: string,
|
||||
userLocale: z.infer<typeof ZUserLocale> | undefined
|
||||
): Promise<{ user: TCreatedUser | undefined; userAlreadyExisted: boolean }> {
|
||||
@@ -75,7 +79,8 @@ async function createUserSafely(
|
||||
try {
|
||||
user = await createUser({
|
||||
email: email.toLowerCase(),
|
||||
name,
|
||||
firstName,
|
||||
lastName,
|
||||
password: hashedPassword,
|
||||
locale: userLocale,
|
||||
});
|
||||
@@ -127,7 +132,7 @@ async function handleInviteAcceptance(
|
||||
},
|
||||
});
|
||||
|
||||
await sendInviteAcceptedEmail(invite.creator.name ?? "", user.name, invite.creator.email);
|
||||
await sendInviteAcceptedEmail(invite.creator.name ?? "", `${user.firstName} ${user.lastName}`, invite.creator.email);
|
||||
await deleteInvite(invite.id);
|
||||
}
|
||||
|
||||
@@ -135,7 +140,7 @@ async function handleOrganizationCreation(ctx: ActionClientCtx, user: TCreatedUs
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled) return;
|
||||
|
||||
const organization = await createOrganization({ name: `${user.name}'s Organization` });
|
||||
const organization = await createOrganization({ name: `${user.firstName} ${user.lastName}'s Organization` });
|
||||
ctx.auditLoggingCtx.organizationId = organization.id;
|
||||
|
||||
await createMembership(organization.id, user.id, {
|
||||
@@ -177,12 +182,13 @@ export const createUserAction = actionClient.schema(ZCreateUserAction).action(
|
||||
"created",
|
||||
"user",
|
||||
async ({ ctx, parsedInput }: { ctx: ActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
await verifyTurnstileIfConfigured(parsedInput.turnstileToken, parsedInput.email, parsedInput.name);
|
||||
await verifyTurnstileIfConfigured(parsedInput.turnstileToken, parsedInput.email, parsedInput.firstName, parsedInput.lastName);
|
||||
|
||||
const hashedPassword = await hashPassword(parsedInput.password);
|
||||
const { user, userAlreadyExisted } = await createUserSafely(
|
||||
parsedInput.email,
|
||||
parsedInput.name,
|
||||
parsedInput.firstName,
|
||||
parsedInput.lastName,
|
||||
hashedPassword,
|
||||
parsedInput.userLocale
|
||||
);
|
||||
|
||||
@@ -219,7 +219,8 @@ export const getTeamDetails = reactCache(async (teamId: string): Promise<TTeamDe
|
||||
role: true,
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -248,7 +249,7 @@ export const getTeamDetails = reactCache(async (teamId: string): Promise<TTeamDe
|
||||
organizationId: team.organizationId,
|
||||
members: team.teamUsers.map((teamUser) => ({
|
||||
userId: teamUser.userId,
|
||||
name: teamUser.user.name,
|
||||
name: `${teamUser.user.firstName} ${teamUser.user.lastName}`,
|
||||
role: teamUser.role,
|
||||
})),
|
||||
projects: team.projectTeams.map((projectTeam) => ({
|
||||
|
||||
@@ -63,7 +63,7 @@ export const setupTwoFactorAuth = async (
|
||||
},
|
||||
});
|
||||
|
||||
const name = user.email || user.name || user.id.toString();
|
||||
const name = user.email || `${user.firstName} ${user.lastName}` || user.id.toString();
|
||||
const keyUri = authenticator.keyuri(name, "Formbricks", secret);
|
||||
const dataUri = await qrcode.toDataURL(keyUri);
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export const sendTestEmailAction = authenticatedActionClient
|
||||
|
||||
await sendEmailCustomizationPreviewEmail(
|
||||
ctx.user.email,
|
||||
ctx.user.name,
|
||||
`${ctx.user.firstName} ${ctx.user.lastName}`,
|
||||
organization?.whitelabel?.logoUrl || ""
|
||||
);
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ export const resendInviteAction = authenticatedActionClient.schema(ZResendInvite
|
||||
await sendInviteMemberEmail(
|
||||
parsedInput.inviteId,
|
||||
updatedInvite.email,
|
||||
invite?.creator?.name ?? "",
|
||||
invite?.creator ? `${invite.creator.firstName} ${invite.creator.lastName}` : "",
|
||||
updatedInvite.name ?? "",
|
||||
undefined,
|
||||
ctx.user.locale
|
||||
@@ -269,7 +269,7 @@ export const inviteUserAction = authenticatedActionClient.schema(ZInviteUserActi
|
||||
await sendInviteMemberEmail(
|
||||
inviteId,
|
||||
parsedInput.email,
|
||||
ctx.user.name ?? "",
|
||||
`${ctx.user.firstName} ${ctx.user.lastName}`,
|
||||
parsedInput.name ?? "",
|
||||
false,
|
||||
undefined
|
||||
|
||||
@@ -20,7 +20,8 @@ export const getMembershipByOrganizationId = reactCache(
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
isActive: true,
|
||||
},
|
||||
@@ -35,7 +36,7 @@ export const getMembershipByOrganizationId = reactCache(
|
||||
|
||||
const members = membersData.map((member) => {
|
||||
return {
|
||||
name: member.user?.name || "",
|
||||
name: member.user ? `${member.user.firstName} ${member.user.lastName}` : "",
|
||||
email: member.user?.email || "",
|
||||
userId: member.userId,
|
||||
accepted: member.accepted,
|
||||
@@ -162,7 +163,8 @@ export const getMembersByOrganizationId = reactCache(
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
role: true,
|
||||
@@ -173,7 +175,7 @@ export const getMembersByOrganizationId = reactCache(
|
||||
const members = membersData.map((member) => {
|
||||
return {
|
||||
id: member.userId,
|
||||
name: member.user?.name || "",
|
||||
name: member.user ? `${member.user.firstName} ${member.user.lastName}` : "",
|
||||
role: member.role,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ export const inviteOrganizationMemberAction = authenticatedActionClient
|
||||
await sendInviteMemberEmail(
|
||||
invitedUserId,
|
||||
parsedInput.email,
|
||||
ctx.user.name,
|
||||
`${ctx.user.firstName} ${ctx.user.lastName}`,
|
||||
"",
|
||||
false, // is onboarding invite
|
||||
undefined
|
||||
|
||||
@@ -833,7 +833,8 @@ enum Intention {
|
||||
/// Central model for user authentication and profile management.
|
||||
///
|
||||
/// @property id - Unique identifier for the user
|
||||
/// @property name - Display name of the user
|
||||
/// @property firstName - User's first name
|
||||
/// @property lastName - User's last name
|
||||
/// @property email - User's email address
|
||||
/// @property role - User's professional role
|
||||
/// @property objective - User's main goal with Formbricks
|
||||
@@ -844,7 +845,8 @@ model User {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
firstName String
|
||||
lastName String
|
||||
email String @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export const ZUser = z.object({
|
||||
example: true,
|
||||
}),
|
||||
name: ZUserName.openapi({
|
||||
description: "The name of the user",
|
||||
description: "The full name of the user",
|
||||
example: "John Doe",
|
||||
}),
|
||||
email: ZUserEmail.openapi({
|
||||
|
||||
@@ -46,7 +46,8 @@ const ZUserIdentityProvider = z.enum(["email", "google", "github", "azuread", "o
|
||||
|
||||
export const ZUser = z.object({
|
||||
id: z.string(),
|
||||
name: ZUserName,
|
||||
firstName: ZUserName,
|
||||
lastName: ZUserName,
|
||||
email: ZUserEmail,
|
||||
emailVerified: z.date().nullable(),
|
||||
imageUrl: z.string().url().nullable(),
|
||||
@@ -65,7 +66,8 @@ export const ZUser = z.object({
|
||||
export type TUser = z.infer<typeof ZUser>;
|
||||
|
||||
export const ZUserUpdateInput = z.object({
|
||||
name: ZUserName.optional(),
|
||||
firstName: ZUserName.optional(),
|
||||
lastName: ZUserName.optional(),
|
||||
email: ZUserEmail.optional(),
|
||||
emailVerified: z.date().nullish(),
|
||||
password: ZUserPassword.optional(),
|
||||
@@ -81,7 +83,8 @@ export const ZUserUpdateInput = z.object({
|
||||
export type TUserUpdateInput = z.infer<typeof ZUserUpdateInput>;
|
||||
|
||||
export const ZUserCreateInput = z.object({
|
||||
name: ZUserName,
|
||||
firstName: ZUserName,
|
||||
lastName: ZUserName,
|
||||
email: ZUserEmail,
|
||||
password: ZUserPassword.optional(),
|
||||
emailVerified: z.date().optional(),
|
||||
|
||||
Reference in New Issue
Block a user