fix: resolve lint issues in settings components
- Fix react-hooks/set-state-in-effect by inlining data fetch in useEffect - Replace useCallback-wrapped fetch functions with plain async functions - Replace window.location.href with window.location.assign() - Remove unused eslint-disable directives - Fix double res.json() call in passkey refresh
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
"@better-auth/passkey": "^1.6.9",
|
"@better-auth/passkey": "^1.6.9",
|
||||||
"@elysia/eden": "^1.4.10",
|
"@elysia/eden": "^1.4.10",
|
||||||
"@tiptap/core": "^3.22.4",
|
"@tiptap/core": "^3.22.4",
|
||||||
|
"@tiptap/extension-link": "^3.22.4",
|
||||||
|
"@tiptap/extension-placeholder": "^3.22.4",
|
||||||
"@tiptap/pm": "^3.22.4",
|
"@tiptap/pm": "^3.22.4",
|
||||||
"@tiptap/react": "^3.22.4",
|
"@tiptap/react": "^3.22.4",
|
||||||
"@tiptap/starter-kit": "^3.22.4",
|
"@tiptap/starter-kit": "^3.22.4",
|
||||||
@@ -633,6 +635,8 @@
|
|||||||
|
|
||||||
"@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-de6dFkIhigiENESY6rNJ3yTVS/337ybfP30dNPudTwGe9oAu9ZCS+04j6QCvXSjhlI3ULiv7wiSHqrP26Gd+Hw=="],
|
"@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-de6dFkIhigiENESY6rNJ3yTVS/337ybfP30dNPudTwGe9oAu9ZCS+04j6QCvXSjhlI3ULiv7wiSHqrP26Gd+Hw=="],
|
||||||
|
|
||||||
|
"@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.22.4", "", { "peerDependencies": { "@tiptap/extensions": "3.22.4" } }, "sha512-Z3wtWL+KufwkC7CkJge5enAxx4q8C3oOYixme02snY9zfjX3V/1pjAmEfP4wxScgM5GIuTEJ83B9Yz3wRzPA6Q=="],
|
||||||
|
|
||||||
"@tiptap/extension-strike": ["@tiptap/extension-strike@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-aRHWQj42HiailXSC9LkKYM3jWMcSeGwOjbqM4PiuxQZmHVDRFmeHkfJItOdn2cSHaO0vuEVK+TvrWUWsBFi3pg=="],
|
"@tiptap/extension-strike": ["@tiptap/extension-strike@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-aRHWQj42HiailXSC9LkKYM3jWMcSeGwOjbqM4PiuxQZmHVDRFmeHkfJItOdn2cSHaO0vuEVK+TvrWUWsBFi3pg=="],
|
||||||
|
|
||||||
"@tiptap/extension-text": ["@tiptap/extension-text@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-mM69uUW5cSxIhyEpWXi/YcfyupcJMDLCPEfYi62awH0iOP/LRoCv/nHjJq4Hyj/KxRJbe8HKwIUnqaCUf7m5Pg=="],
|
"@tiptap/extension-text": ["@tiptap/extension-text@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-mM69uUW5cSxIhyEpWXi/YcfyupcJMDLCPEfYi62awH0iOP/LRoCv/nHjJq4Hyj/KxRJbe8HKwIUnqaCUf7m5Pg=="],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Loader2, Link as LinkIcon, Unlink, Shield } from "lucide-react"
|
import { Loader2, Link as LinkIcon, Unlink, Shield } from "lucide-react"
|
||||||
import { motion } from "motion/react"
|
import { motion } from "motion/react"
|
||||||
import { FaGoogle, FaDiscord } from "react-icons/fa"
|
import { FaGoogle, FaDiscord } from "react-icons/fa"
|
||||||
@@ -33,7 +33,7 @@ export function SettingsAccountsTab() {
|
|||||||
const [unlinking, setUnlinking] = useState<string | null>(null)
|
const [unlinking, setUnlinking] = useState<string | null>(null)
|
||||||
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null)
|
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null)
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const refreshData = async () => {
|
||||||
try {
|
try {
|
||||||
const [accountsRes, methodsRes] = await Promise.all([
|
const [accountsRes, methodsRes] = await Promise.all([
|
||||||
fetch("/api/auth/list-accounts", { credentials: "include" }),
|
fetch("/api/auth/list-accounts", { credentials: "include" }),
|
||||||
@@ -49,16 +49,26 @@ export function SettingsAccountsTab() {
|
|||||||
const data = await methodsRes.json()
|
const data = await methodsRes.json()
|
||||||
setAuthMethods(data)
|
setAuthMethods(data)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.error("Failed to fetch account data:", err)
|
// silently fail
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData()
|
if (accounts.length > 0 || authMethods !== null) return
|
||||||
}, [fetchData])
|
let cancelled = false
|
||||||
|
Promise.all([
|
||||||
|
fetch("/api/auth/list-accounts", { credentials: "include" }).then(r => r.ok ? r.json() : []).catch(() => []),
|
||||||
|
fetch("/api/user/me/auth-methods").then(r => r.ok ? r.json() : null).catch(() => null),
|
||||||
|
]).then(([accountsData, methodsData]) => {
|
||||||
|
if (cancelled) return
|
||||||
|
if (accountsData) setAccounts(Array.isArray(accountsData) ? accountsData : [])
|
||||||
|
if (methodsData) setAuthMethods(methodsData)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
return () => { cancelled = true }
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: fetch only on mount
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleLink = async (provider: "google" | "discord") => {
|
const handleLink = async (provider: "google" | "discord") => {
|
||||||
try {
|
try {
|
||||||
@@ -81,9 +91,9 @@ export function SettingsAccountsTab() {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
// If the API returns a URL, redirect to it
|
// If the API returns a URL, redirect to it
|
||||||
if (data && data.url) {
|
if (data && data.url) {
|
||||||
window.location.href = data.url
|
window.location.assign(data.url as string)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
setMessage({ type: "error", text: "Failed to initiate account linking" })
|
setMessage({ type: "error", text: "Failed to initiate account linking" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +114,7 @@ export function SettingsAccountsTab() {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setMessage({ type: "success", text: `${providerConfig[providerId]?.name || providerId} account unlinked` })
|
setMessage({ type: "success", text: `${providerConfig[providerId]?.name || providerId} account unlinked` })
|
||||||
await fetchData()
|
await refreshData()
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setMessage({ type: "error", text: data.message || "Failed to unlink account" })
|
setMessage({ type: "error", text: data.message || "Failed to unlink account" })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react"
|
import { useState, useRef, useEffect } from "react"
|
||||||
import { authClient } from "@/lib/auth-client"
|
import { authClient } from "@/lib/auth-client"
|
||||||
import { Loader2, Key, Fingerprint, Plus, Trash2, Pencil, Check, X, Shield } from "lucide-react"
|
import { Loader2, Key, Fingerprint, Plus, Trash2, Pencil, Check, X, Shield } from "lucide-react"
|
||||||
import { motion, AnimatePresence } from "motion/react"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
@@ -39,42 +39,38 @@ export function SettingsSecurityTab() {
|
|||||||
const [isDeletingPasskey, setIsDeletingPasskey] = useState<string | null>(null)
|
const [isDeletingPasskey, setIsDeletingPasskey] = useState<string | null>(null)
|
||||||
const [isAddingPasskey, setIsAddingPasskey] = useState(false)
|
const [isAddingPasskey, setIsAddingPasskey] = useState(false)
|
||||||
|
|
||||||
const fetchAuthMethods = useCallback(async () => {
|
const fetchRef = useRef(false)
|
||||||
try {
|
|
||||||
const res = await fetch("/api/user/me/auth-methods", {
|
|
||||||
credentials: "include",
|
|
||||||
})
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
setAuthMethods(data)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silently fail
|
|
||||||
} finally {
|
|
||||||
setIsLoadingAuthMethods(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchPasskeys = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/auth/passkey/list-user-passkeys", {
|
|
||||||
credentials: "include",
|
|
||||||
})
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
setPasskeys(Array.isArray(data) ? data : [])
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silently fail
|
|
||||||
} finally {
|
|
||||||
setIsLoadingPasskeys(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAuthMethods()
|
if (fetchRef.current) return
|
||||||
fetchPasskeys()
|
fetchRef.current = true
|
||||||
}, [fetchAuthMethods, fetchPasskeys])
|
|
||||||
|
let cancelled = false
|
||||||
|
Promise.all([
|
||||||
|
fetch("/api/user/me/auth-methods", { credentials: "include" }).then(r => r.ok ? r.json() : null).catch(() => null),
|
||||||
|
fetch("/api/auth/passkey/list-user-passkeys", { credentials: "include" }).then(r => r.ok ? r.json() : []).catch(() => []),
|
||||||
|
]).then(([methodsData, passkeyData]) => {
|
||||||
|
if (cancelled) return
|
||||||
|
if (methodsData) setAuthMethods(methodsData)
|
||||||
|
if (passkeyData) setPasskeys(Array.isArray(passkeyData) ? passkeyData : [])
|
||||||
|
setIsLoadingAuthMethods(false)
|
||||||
|
setIsLoadingPasskeys(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const refreshAuthMethods = async () => {
|
||||||
|
const res = await fetch("/api/user/me/auth-methods", { credentials: "include" })
|
||||||
|
if (res.ok) setAuthMethods(await res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshPasskeys = async () => {
|
||||||
|
const res = await fetch("/api/auth/passkey/list-user-passkeys", { credentials: "include" })
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setPasskeys(Array.isArray(data) ? data : [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handlePasswordSubmit = async (e: React.FormEvent) => {
|
const handlePasswordSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -121,7 +117,7 @@ export function SettingsSecurityTab() {
|
|||||||
setPasswordMessage({ type: "success", text: "Password set successfully" })
|
setPasswordMessage({ type: "success", text: "Password set successfully" })
|
||||||
setNewPassword("")
|
setNewPassword("")
|
||||||
setConfirmPassword("")
|
setConfirmPassword("")
|
||||||
await fetchAuthMethods()
|
await refreshAuthMethods()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,11 +129,10 @@ export function SettingsSecurityTab() {
|
|||||||
try {
|
try {
|
||||||
const { error } = await authClient.passkey.addPasskey()
|
const { error } = await authClient.passkey.addPasskey()
|
||||||
if (error) {
|
if (error) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error("Failed to add passkey:", error)
|
console.error("Failed to add passkey:", error)
|
||||||
} else {
|
} else {
|
||||||
await fetchPasskeys()
|
await refreshPasskeys()
|
||||||
await fetchAuthMethods()
|
await refreshAuthMethods()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// silently fail
|
// silently fail
|
||||||
@@ -158,8 +153,8 @@ export function SettingsSecurityTab() {
|
|||||||
credentials: "include",
|
credentials: "include",
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await fetchPasskeys()
|
await refreshPasskeys()
|
||||||
await fetchAuthMethods()
|
await refreshAuthMethods()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// silently fail
|
// silently fail
|
||||||
@@ -188,7 +183,7 @@ export function SettingsSecurityTab() {
|
|||||||
credentials: "include",
|
credentials: "include",
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await fetchPasskeys()
|
await refreshPasskeys()
|
||||||
setEditingPasskeyId(null)
|
setEditingPasskeyId(null)
|
||||||
setEditingName("")
|
setEditingName("")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE "saved_games" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"game_id" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "saved_games_user_game_unique" UNIQUE("user_id","game_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "performance_entries" ADD COLUMN "launch_options" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "saved_games" ADD CONSTRAINT "saved_games_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "saved_games" ADD CONSTRAINT "saved_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "7f7353e5-646d-4e21-92e6-250b5b28ee50",
|
"id": "66b4e2ff-53cc-46d0-a9fd-321a378683f1",
|
||||||
"prevId": "273a7a41-0813-4612-ad50-bc53aa0eac5f",
|
"prevId": "7f7353e5-646d-4e21-92e6-250b5b28ee50",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -1282,6 +1282,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"launch_options": {
|
||||||
|
"name": "launch_options",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"settings_json": {
|
"settings_json": {
|
||||||
"name": "settings_json",
|
"name": "settings_json",
|
||||||
"type": "jsonb",
|
"type": "jsonb",
|
||||||
@@ -1460,6 +1466,80 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.saved_games": {
|
||||||
|
"name": "saved_games",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"game_id": {
|
||||||
|
"name": "game_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"saved_games_user_id_user_id_fk": {
|
||||||
|
"name": "saved_games_user_id_user_id_fk",
|
||||||
|
"tableFrom": "saved_games",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"saved_games_game_id_games_id_fk": {
|
||||||
|
"name": "saved_games_game_id_games_id_fk",
|
||||||
|
"tableFrom": "saved_games",
|
||||||
|
"tableTo": "games",
|
||||||
|
"columnsFrom": [
|
||||||
|
"game_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"saved_games_user_game_unique": {
|
||||||
|
"name": "saved_games_user_game_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"game_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
|
|||||||
@@ -36,6 +36,13 @@
|
|||||||
"when": 1777189057323,
|
"when": 1777189057323,
|
||||||
"tag": "0005_add_preset_performance_link",
|
"tag": "0005_add_preset_performance_link",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777217663641,
|
||||||
|
"tag": "0005_brief_marvel_zombies",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+1
-1
@@ -7,5 +7,5 @@ import type { App } from "@/app/api/[[...slugs]]/route"
|
|||||||
export const api =
|
export const api =
|
||||||
typeof window === "undefined"
|
typeof window === "undefined"
|
||||||
? // eslint-disable-next-line @typescript-eslint/no-require-imports
|
? // eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
treaty((require("../../app/api/[[...slugs]]/route") as { app: App }).app).api
|
treaty((require("../app/api/[[...slugs]]/route") as { app: App }).app).api
|
||||||
: treaty<App>(process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000").api
|
: treaty<App>(process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000").api
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"@better-auth/passkey": "^1.6.9",
|
"@better-auth/passkey": "^1.6.9",
|
||||||
"@elysia/eden": "^1.4.10",
|
"@elysia/eden": "^1.4.10",
|
||||||
"@tiptap/core": "^3.22.4",
|
"@tiptap/core": "^3.22.4",
|
||||||
|
"@tiptap/extension-link": "^3.22.4",
|
||||||
|
"@tiptap/extension-placeholder": "^3.22.4",
|
||||||
"@tiptap/pm": "^3.22.4",
|
"@tiptap/pm": "^3.22.4",
|
||||||
"@tiptap/react": "^3.22.4",
|
"@tiptap/react": "^3.22.4",
|
||||||
"@tiptap/starter-kit": "^3.22.4",
|
"@tiptap/starter-kit": "^3.22.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user