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", provider: "pg",
usePlural: true, usePlural: true,
}), }),
advanced: {
database: {
generateId: "uuid",
},
},
user: { user: {
fields: { fields: {
name: "display_name", name: "displayName",
image: "image_url", image: "imageUrl",
}, },
additionalFields: { additionalFields: {
firstName: { type: "string", required: false, fieldName: "first_name" }, firstName: { type: "string", required: false, fieldName: "firstName" },
lastName: { type: "string", required: false, fieldName: "last_name" }, lastName: { type: "string", required: false, fieldName: "lastName" },
username: { type: "string", required: false, fieldName: "username" }, username: { type: "string", required: false, fieldName: "username" },
isAdmin: { type: "boolean", defaultValue: false, fieldName: "is_admin" }, isAdmin: { type: "boolean", defaultValue: false, fieldName: "isAdmin" },
}, },
}, },
emailAndPassword: { emailAndPassword: {

View file

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

View file

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

View file

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

View file

@ -75,3 +75,37 @@ export function generateUniqueTeamNames(count: number): string[] {
return Array.from(names); 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;
}