From 9d35d93d9e4116c21951af9c05d68eaf93290dc0 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Sun, 30 Mar 2025 17:36:49 +0100 Subject: [PATCH] feat: usernames also change userId to number --- package.json | 5 +- pnpm-lock.yaml | 63 +++++-------------- .../migrations/20250329220850_/migration.sql | 2 - .../migration.sql | 23 ++++--- prisma/schema.prisma | 17 ++--- src/app/api/auth/username/route.ts | 38 +++++++++++ src/app/components/like-button.tsx | 2 +- src/app/components/login-buttons.tsx | 10 ++- src/app/components/profile-overview.tsx | 2 +- src/app/components/username-form.tsx | 45 +++++++++++++ src/app/create-username/page.tsx | 22 +++++++ src/app/middleware.ts | 1 - src/app/page.tsx | 24 +++++-- src/lib/auth.ts | 10 +++ src/types.d.ts | 14 +++++ 15 files changed, 200 insertions(+), 78 deletions(-) delete mode 100644 prisma/migrations/20250329220850_/migration.sql rename prisma/migrations/{20250329215353_ => 20250330163530_init}/migration.sql (86%) create mode 100644 src/app/api/auth/username/route.ts create mode 100644 src/app/components/username-form.tsx create mode 100644 src/app/create-username/page.tsx delete mode 100644 src/app/middleware.ts create mode 100644 src/types.d.ts diff --git a/package.json b/package.json index 0d79894..b174ebf 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,13 @@ "postinstall": "prisma generate" }, "dependencies": { - "@auth/prisma-adapter": "^2.8.0", + "@auth/prisma-adapter": "2.7.2", "@prisma/client": "^6.5.0", "next": "15.2.4", "next-auth": "5.0.0-beta.25", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "zod": "^3.24.2" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d4effe..ef65071 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@auth/prisma-adapter': - specifier: ^2.8.0 - version: 2.8.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)) + specifier: 2.7.2 + version: 2.7.2(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)) '@prisma/client': specifier: ^6.5.0 version: 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2) @@ -26,6 +26,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + zod: + specifier: ^3.24.2 + version: 3.24.2 devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -81,24 +84,10 @@ packages: nodemailer: optional: true - '@auth/core@0.38.0': - resolution: {integrity: sha512-ClHl44x4cY3wfJmHLpW+XrYqED0fZIzbHmwbExltzroCjR5ts3DLTWzADRba8mJFYZ8JIEJDa+lXnGl0E9Bl7Q==} + '@auth/prisma-adapter@2.7.2': + resolution: {integrity: sha512-orznIVt6aQMoJ4/rfWFSpRPU8LoZn6jVtDuEkZgLud2xSnCalq6x+hX+rqlk4E5LM13NW1GIJojOPQnM4aM4Gw==} peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - nodemailer: ^6.8.0 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - - '@auth/prisma-adapter@2.8.0': - resolution: {integrity: sha512-g0Bmq3l5xUDyBBiDgm/y3Zqb582CnRHzFqbloV7scrLia5AbVC0xy+ntn+CQCAWW9ibpwiqJrQKKboIWN1oGqw==} - peerDependencies: - '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6' + '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5' '@emnapi/core@1.4.0': resolution: {integrity: sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==} @@ -1411,9 +1400,6 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.0.10: - resolution: {integrity: sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1691,17 +1677,9 @@ packages: peerDependencies: preact: '>=10' - preact-render-to-string@6.5.11: - resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} - peerDependencies: - preact: '>=10' - preact@10.11.3: resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==} - preact@10.24.3: - resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1996,6 +1974,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@3.24.2: + resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -2010,17 +1991,9 @@ snapshots: preact: 10.11.3 preact-render-to-string: 5.2.3(preact@10.11.3) - '@auth/core@0.38.0': + '@auth/prisma-adapter@2.7.2(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))': dependencies: - '@panva/hkdf': 1.2.1 - jose: 6.0.10 - oauth4webapi: 3.3.2 - preact: 10.24.3 - preact-render-to-string: 6.5.11(preact@10.24.3) - - '@auth/prisma-adapter@2.8.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))': - dependencies: - '@auth/core': 0.38.0 + '@auth/core': 0.37.2 '@prisma/client': 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2) transitivePeerDependencies: - '@simplewebauthn/browser' @@ -3417,8 +3390,6 @@ snapshots: jose@5.10.0: {} - jose@6.0.10: {} - js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -3670,14 +3641,8 @@ snapshots: preact: 10.11.3 pretty-format: 3.8.0 - preact-render-to-string@6.5.11(preact@10.24.3): - dependencies: - preact: 10.24.3 - preact@10.11.3: {} - preact@10.24.3: {} - prelude-ls@1.2.1: {} pretty-format@3.8.0: {} @@ -4081,3 +4046,5 @@ snapshots: word-wrap@1.2.5: {} yocto-queue@0.1.0: {} + + zod@3.24.2: {} diff --git a/prisma/migrations/20250329220850_/migration.sql b/prisma/migrations/20250329220850_/migration.sql deleted file mode 100644 index 21e404e..0000000 --- a/prisma/migrations/20250329220850_/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "miis" ADD COLUMN "tags" TEXT[]; diff --git a/prisma/migrations/20250329215353_/migration.sql b/prisma/migrations/20250330163530_init/migration.sql similarity index 86% rename from prisma/migrations/20250329215353_/migration.sql rename to prisma/migrations/20250330163530_init/migration.sql index 1823126..8fee47a 100644 --- a/prisma/migrations/20250329215353_/migration.sql +++ b/prisma/migrations/20250330163530_init/migration.sql @@ -1,7 +1,8 @@ -- CreateTable CREATE TABLE "users" ( - "id" TEXT NOT NULL, - "name" TEXT, + "id" SERIAL NOT NULL, + "username" TEXT, + "name" TEXT NOT NULL, "email" TEXT NOT NULL, "emailVerified" TIMESTAMP(3), "image" TEXT, @@ -13,7 +14,7 @@ CREATE TABLE "users" ( -- CreateTable CREATE TABLE "accounts" ( - "userId" TEXT NOT NULL, + "userId" INTEGER NOT NULL, "type" TEXT NOT NULL, "provider" TEXT NOT NULL, "providerAccountId" TEXT NOT NULL, @@ -33,7 +34,7 @@ CREATE TABLE "accounts" ( -- CreateTable CREATE TABLE "sessions" ( "sessionToken" TEXT NOT NULL, - "userId" TEXT NOT NULL, + "userId" INTEGER NOT NULL, "expires" TIMESTAMP(3) NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL @@ -41,10 +42,11 @@ CREATE TABLE "sessions" ( -- CreateTable CREATE TABLE "miis" ( - "id" BIGSERIAL NOT NULL, - "userId" TEXT NOT NULL, - "name" TEXT NOT NULL, + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "name" VARCHAR(64) NOT NULL, "pictures" TEXT[], + "tags" TEXT[], "likes" INTEGER NOT NULL DEFAULT 0, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -53,12 +55,15 @@ CREATE TABLE "miis" ( -- CreateTable CREATE TABLE "likes" ( - "userId" TEXT NOT NULL, - "miiId" BIGINT NOT NULL, + "userId" INTEGER NOT NULL, + "miiId" INTEGER NOT NULL, CONSTRAINT "likes_pkey" PRIMARY KEY ("userId","miiId") ); +-- CreateIndex +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); + -- CreateIndex CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index aa0af65..617e6c0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,8 +8,9 @@ datasource db { } model User { - id String @id @default(cuid()) - name String? + id Int @id @default(autoincrement()) + username String? @unique + name String email String @unique emailVerified DateTime? image String? @@ -26,7 +27,7 @@ model User { } model Account { - userId String + userId Int type String provider String providerAccountId String @@ -49,7 +50,7 @@ model Account { model Session { sessionToken String @unique - userId String + userId Int expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -60,8 +61,8 @@ model Session { } model Mii { - id BigInt @id @default(autoincrement()) - userId String + id Int @id @default(autoincrement()) + userId Int name String @db.VarChar(64) pictures String[] tags String[] @@ -76,8 +77,8 @@ model Mii { } model Like { - userId String - miiId BigInt + userId Int + miiId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) mii Mii @relation(fields: [miiId], references: [id], onDelete: Cascade) diff --git a/src/app/api/auth/username/route.ts b/src/app/api/auth/username/route.ts new file mode 100644 index 0000000..a6e9e3f --- /dev/null +++ b/src/app/api/auth/username/route.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +const usernameSchema = z + .string() + .min(3, "Username must be at least 3 characters long") + .max(20, "Username cannot be more than 20 characters long") + .regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores"); + +export async function GET() { + const session = await auth(); + if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 }); + + return Response.json({ username: session.user.username }); +} + +export async function PATCH(request: Request) { + const session = await auth(); + if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 }); + + const { username } = await request.json(); + if (!username) return Response.json({ error: "Username is required" }, { status: 400 }); + + const validation = usernameSchema.safeParse(username); + if (!validation.success) return Response.json({ error: validation.error.errors[0].message }, { status: 400 }); + + const existingUser = await prisma.user.findUnique({ where: { username } }); + if (existingUser) return Response.json({ error: "Username is already taken" }, { status: 400 }); + + await prisma.user.update({ + where: { email: session.user?.email ?? undefined }, + data: { username }, + }); + + return Response.json({ success: true }); +} diff --git a/src/app/components/like-button.tsx b/src/app/components/like-button.tsx index f95030d..1f541c8 100644 --- a/src/app/components/like-button.tsx +++ b/src/app/components/like-button.tsx @@ -23,7 +23,7 @@ export default function LikeButton({ likes, isLoggedIn }: Props) { }; return ( - diff --git a/src/app/components/login-buttons.tsx b/src/app/components/login-buttons.tsx index 768f1df..d5baa04 100644 --- a/src/app/components/login-buttons.tsx +++ b/src/app/components/login-buttons.tsx @@ -6,11 +6,17 @@ import { signIn } from "next-auth/react"; export default function LoginButtons() { return (
- - diff --git a/src/app/components/profile-overview.tsx b/src/app/components/profile-overview.tsx index ce923b8..b881d4c 100644 --- a/src/app/components/profile-overview.tsx +++ b/src/app/components/profile-overview.tsx @@ -7,7 +7,7 @@ export default async function ProfileOverview() {
  • ); diff --git a/src/app/components/username-form.tsx b/src/app/components/username-form.tsx new file mode 100644 index 0000000..3631e52 --- /dev/null +++ b/src/app/components/username-form.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { redirect } from "next/navigation"; + +export default function UsernameForm() { + const [username, setUsername] = useState(""); + const [error, setError] = useState(null); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + + const response = await fetch("/api/auth/username", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username }), + }); + + if (!response.ok) { + const { error } = await response.json(); + setError(error); + return; + } + + redirect("/"); + }; + + return ( +
    + setUsername(e.target.value)} + required + className="pill !bg-orange-200 outline-0 focus:ring-[3px] ring-orange-400/50 transition w-96 mt-8 mb-2" + /> + + + {error &&

    Error: {error}

    } +
    + ); +} diff --git a/src/app/create-username/page.tsx b/src/app/create-username/page.tsx new file mode 100644 index 0000000..863b559 --- /dev/null +++ b/src/app/create-username/page.tsx @@ -0,0 +1,22 @@ +import { auth } from "@/lib/auth"; + +import UsernameForm from "../components/username-form"; +import { redirect } from "next/navigation"; + +export default async function CreateUsernamePage() { + const session = await auth(); + + // If the user is not logged in or already has a username, redirect + if (!session || session?.user.username) { + redirect("/"); + } + + return ( +
    +

    Welcome to TomodachiShare!

    +

    Please create a username

    + + +
    + ); +} diff --git a/src/app/middleware.ts b/src/app/middleware.ts deleted file mode 100644 index 1b5fac3..0000000 --- a/src/app/middleware.ts +++ /dev/null @@ -1 +0,0 @@ -export { auth as middleware } from "@/lib/auth"; diff --git a/src/app/page.tsx b/src/app/page.tsx index a447d33..69842a4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,4 @@ +import { redirect } from "next/navigation"; import { Prisma } from "@prisma/client"; import { auth } from "@/lib/auth"; @@ -36,8 +37,19 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [ const miis = await prisma.mii.findMany({ where: where, orderBy, + include: { + user: { + select: { + username: true, + }, + }, + }, }); + if (session?.user && !session.user.username) { + redirect("/create-username"); + } + return (
    @@ -74,20 +86,24 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [ {miis.map((mii) => (
    mii -
    +

    {mii.name}

    -
    +
    {mii.tags.map((tag) => ( {tag} ))}
    - +
    + + + @{mii.user?.username} +
    ))} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 54901a8..5cf4662 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -11,4 +11,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ pages: { signIn: "/login", }, + callbacks: { + async session({ session, user }) { + if (user) { + session.user.id = user.id; + session.user.username = user.username; + session.user.email = user.email; + } + return session; + }, + }, }); diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..a2705f5 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,14 @@ +import { DefaultSession, User } from "next-auth"; +import { User as PrismaUser } from "@prisma/client"; + +declare module "next-auth" { + interface Session { + user: { + username?: string; + } & DefaultSession["user"]; + } + + interface User { + username?: string; + } +}