Fix BetterAuth field mapping and add owner-prefixed team names

- Fix auth.server.ts: use camelCase Drizzle field names for BetterAuth
  adapter (was snake_case, causing user inserts to fail); add
  generateId: "uuid" so PostgreSQL UUID columns accept generated IDs
- Add prependOwnerToTeamName / stripOwnerFromTeamName utilities
- Invite join, league creation, and settings assign/remove all now
  manage the owner-prefix on team names atomically

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-04-29 08:31:16 -07:00
parent e201ecd28a
commit d2282a263e
5 changed files with 82 additions and 14 deletions

View file

@ -23,16 +23,21 @@ export const auth = betterAuth({
provider: "pg",
usePlural: true,
}),
advanced: {
database: {
generateId: "uuid",
},
},
user: {
fields: {
name: "display_name",
image: "image_url",
name: "displayName",
image: "imageUrl",
},
additionalFields: {
firstName: { type: "string", required: false, fieldName: "first_name" },
lastName: { type: "string", required: false, fieldName: "last_name" },
firstName: { type: "string", required: false, fieldName: "firstName" },
lastName: { type: "string", required: false, fieldName: "lastName" },
username: { type: "string", required: false, fieldName: "username" },
isAdmin: { type: "boolean", defaultValue: false, fieldName: "is_admin" },
isAdmin: { type: "boolean", defaultValue: false, fieldName: "isAdmin" },
},
},
emailAndPassword: {

View file

@ -10,8 +10,8 @@ import {
claimTeam,
isUserLeagueMember,
findUserById,
getUserDisplayName,
} from "~/models";
import { prependOwnerToTeamName } from "~/utils/team-names";
import { Button } from "~/components/ui/button";
import {
Card,
@ -105,7 +105,8 @@ export async function action(args: Route.ActionArgs) {
// Claim the first available team, setting owner and name atomically
const team = availableTeams[0];
const teamName = `Team ${getUserDisplayName(user) ?? "Member"}`;
const username = user.username ?? user.displayName ?? "Member";
const teamName = prependOwnerToTeamName(team.name, username);
await claimTeam(team.id, userId, teamName);
// Redirect to league page

View file

@ -15,8 +15,9 @@ import {
removeCommissionerByLeagueAndUser,
} from "~/models/commissioner";
import { findCurrentSeasonWithSports, updateSeason, type NewSeason } from "~/models/season";
import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team";
import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam, findTeamById } from "~/models/team";
import { findAllUsers, findUsersByIds, findUserById, getUserDisplayName, isUserAdmin } from "~/models/user";
import { prependOwnerToTeamName, stripOwnerFromTeamName, generateUniqueTeamNames } from "~/utils/team-names";
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
import { findDraftableSportsSeasons, findSportsSeasonsByIds } from "~/models/sports-season";
import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot";
@ -400,7 +401,23 @@ export async function action(args: Route.ActionArgs) {
}
try {
await removeTeamOwner(teamId);
const team = await findTeamById(teamId);
if (team?.ownerId) {
const owner = await findUserById(team.ownerId);
const username = owner?.username ?? owner?.displayName;
if (username) {
const strippedName = stripOwnerFromTeamName(team.name, username);
await removeTeamOwner(teamId);
if (strippedName !== team.name) {
const { renameTeam } = await import("~/models/team");
await renameTeam(teamId, strippedName);
}
} else {
await removeTeamOwner(teamId);
}
} else {
await removeTeamOwner(teamId);
}
return { success: true, message: "Owner removed successfully" };
} catch (error) {
logger.error("Error removing team owner:", error);
@ -438,12 +455,18 @@ export async function action(args: Route.ActionArgs) {
// Check if the target team already has an owner
const targetTeam = teams.find(team => team.id === teamId);
if (targetTeam?.ownerId) {
if (!targetTeam) {
return { error: "Team not found" };
}
if (targetTeam.ownerId) {
return { error: "This team already has an owner. Remove the current owner first." };
}
try {
const teamName = `Team ${getUserDisplayName(assignedUser) ?? "Member"}`;
const teamName = prependOwnerToTeamName(
targetTeam.name,
assignedUser.username ?? assignedUser.displayName ?? "Member"
);
await claimTeam(teamId, assignedUserId, teamName);
return { success: true, message: "Owner assigned successfully" };
@ -718,11 +741,12 @@ export async function action(args: Route.ActionArgs) {
// Add or remove teams as needed
if (newTeamCount > currentTeamCount) {
const newNames = generateUniqueTeamNames(newTeamCount - currentTeamCount);
const teamsToAdd = Array.from(
{ length: newTeamCount - currentTeamCount },
(_, i) => ({
seasonId: season.id,
name: `Team ${currentTeamCount + i + 1}`,
name: newNames[i],
ownerId: null as null,
})
);

View file

@ -13,7 +13,7 @@ import { createManyTeams } from "~/models/team";
import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template";
import { findDraftableSportsSeasons } from "~/models/sports-season";
import { findUserById, updateUser } from "~/models/user";
import { generateUniqueTeamNames } from "~/utils/team-names";
import { generateUniqueTeamNames, prependOwnerToTeamName } from "~/utils/team-names";
import { format } from "date-fns";
import { CalendarClock, Check, Star, Swords, Trophy } from "lucide-react";
import { Badge } from "~/components/ui/badge";
@ -247,9 +247,13 @@ export async function action(args: Route.ActionArgs) {
const joinAsPlayer = formData.get("joinAsPlayer") === "on";
const teamNames = generateUniqueTeamNames(teamCountNum);
const creator = await findUserById(userId);
const creatorUsername = creator?.username ?? creator?.displayName;
const teams = teamNames.map((teamName, i) => ({
seasonId: season.id,
name: teamName,
name: joinAsPlayer && i === 0 && creatorUsername
? prependOwnerToTeamName(teamName, creatorUsername)
: teamName,
ownerId: joinAsPlayer && i === 0 ? userId : null,
}));

View file

@ -75,3 +75,37 @@ export function generateUniqueTeamNames(count: number): string[] {
return Array.from(names);
}
function toTitleCase(str: string): string {
if (!str) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
function possessiveSuffix(name: string): string {
return name.endsWith("s") ? "'" : "'s";
}
export function prependOwnerToTeamName(
baseName: string,
username: string
): string {
const titleName = toTitleCase(username);
return `${titleName}${possessiveSuffix(titleName)} ${baseName}`;
}
export function stripOwnerFromTeamName(
teamName: string,
username: string
): string {
const titleName = toTitleCase(username);
const prefixWithS = `${titleName}'s `;
const prefixWithoutS = `${titleName}' `;
if (teamName.startsWith(prefixWithS)) {
return teamName.slice(prefixWithS.length);
}
if (teamName.startsWith(prefixWithoutS)) {
return teamName.slice(prefixWithoutS.length);
}
return teamName;
}