diff --git a/.eslintrc.js b/.eslintrc.js index ef9d1d50..9bbef5fd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,10 +9,12 @@ module.exports = { "plugin:@typescript-eslint/recommended", "plugin:tailwindcss/recommended", ], + parser: "@typescript-eslint/parser", parserOptions: { babelOptions: { presets: [require.resolve("next/babel")], }, + project: "./tsconfig.json", }, plugins: ["@typescript-eslint", "unused-imports"], rules: { @@ -65,6 +67,7 @@ module.exports = { }, ], "max-params": ["error", 4], + "@typescript-eslint/no-unnecessary-condition": "error", }, } diff --git a/package-lock.json b/package-lock.json index f67d00dd..f3170ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@trpc/server": "^10.38.2", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", + "client-only": "^0.0.1", "clsx": "^2.0.0", "crypto-js": "^4.1.1", "dotenv": "^16.3.1", @@ -57,8 +58,7 @@ "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.6", "ua-parser-js": "^1.0.35", - "zod": "^3.21.4", - "zustand": "^4.4.1" + "zod": "^3.21.4" }, "devDependencies": { "@babel/core": "^7.22.17", @@ -23003,33 +23003,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zustand": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz", - "integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==", - "dependencies": { - "use-sync-external-store": "1.2.0" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index 126156a8..5c95ed12 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@trpc/server": "^10.38.2", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", + "client-only": "^0.0.1", "clsx": "^2.0.0", "crypto-js": "^4.1.1", "dotenv": "^16.3.1", @@ -70,8 +71,7 @@ "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.6", "ua-parser-js": "^1.0.35", - "zod": "^3.21.4", - "zustand": "^4.4.1" + "zod": "^3.21.4" }, "devDependencies": { "@babel/core": "^7.22.17", diff --git a/src/app/[lang]/(not-protected)/page.tsx b/src/app/[lang]/(not-protected)/page.tsx index ee9c7409..cad4abf3 100644 --- a/src/app/[lang]/(not-protected)/page.tsx +++ b/src/app/[lang]/(not-protected)/page.tsx @@ -1,9 +1,9 @@ -import { Locale } from "i18n-config" import Link from "next/link" import { ThemeSwitch } from "@/components/theme/theme-switch" import { buttonVariants } from "@/components/ui/button" import { authRoutes } from "@/lib/auth/constants" import { getDictionary } from "@/lib/langs" +import { Locale } from "i18n-config" export default async function Home({ params: { lang }, diff --git a/src/app/[lang]/(sys-auth)/sign-in/page.tsx b/src/app/[lang]/(sys-auth)/sign-in/page.tsx index 783d28fc..1b2dd01e 100644 --- a/src/app/[lang]/(sys-auth)/sign-in/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-in/page.tsx @@ -1,10 +1,10 @@ -import { Locale } from "i18n-config" import Link from "next/link" import { LoginUserAuthForm } from "@/components/auth/login-user-auth-form" import { buttonVariants } from "@/components/ui/button" import { authRoutes } from "@/lib/auth/constants" import { getDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" +import { Locale } from "i18n-config" import PrivacyAcceptance from "../privacy-acceptance" import Providers from "../providers" diff --git a/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx b/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx index 2cb1cbd1..9696e498 100644 --- a/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx @@ -1,9 +1,9 @@ -import { Locale } from "i18n-config" import { redirect } from "next/navigation" import { RegisterUserAuthForm } from "@/components/auth/register-user-auth-form" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { authRoutes } from "@/lib/auth/constants" import { getDictionary } from "@/lib/langs" +import { Locale } from "i18n-config" export default async function SignupByCredentials({ searchParams, @@ -17,7 +17,7 @@ export default async function SignupByCredentials({ const dictionary = await getDictionary(lang) //? If there is no email in the search params, redirect to the sign-up page - if (!searchParams?.email) { + if (!searchParams.email) { redirect(authRoutes.signUp[0]) } diff --git a/src/app/[lang]/(sys-auth)/sign-up/page.tsx b/src/app/[lang]/(sys-auth)/sign-up/page.tsx index f81b6f4a..7157da62 100644 --- a/src/app/[lang]/(sys-auth)/sign-up/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-up/page.tsx @@ -1,10 +1,10 @@ -import { Locale } from "i18n-config" import Link from "next/link" import { RegisterUserAuthForm } from "@/components/auth/register-user-auth-form" import { buttonVariants } from "@/components/ui/button" import { authRoutes } from "@/lib/auth/constants" import { getDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" +import { Locale } from "i18n-config" import PrivacyAcceptance from "../privacy-acceptance" import Providers from "../providers" diff --git a/src/app/[lang]/[...not-found]/page.tsx b/src/app/[lang]/[...not-found]/page.tsx index 50e98bd2..63c3014a 100644 --- a/src/app/[lang]/[...not-found]/page.tsx +++ b/src/app/[lang]/[...not-found]/page.tsx @@ -1,8 +1,8 @@ -import { Locale } from "i18n-config" import Link from "next/link" import React from "react" import { buttonVariants } from "@/components/ui/button" import { getDictionary } from "@/lib/langs" +import { Locale } from "i18n-config" export default async function Page404({ params: { lang }, diff --git a/src/app/api/me/route.ts b/src/app/api/me/route.ts deleted file mode 100644 index 989cb8da..00000000 --- a/src/app/api/me/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextResponse } from "next/server" -import { requireAuthApi } from "@/components/auth/require-auth" -import { apiRateLimiter } from "@/lib/rate-limit" - -export async function GET(request: Request) { - const { session, error } = await requireAuthApi() - if (error) return error - - const { success, errorResponse: throttlerErrorResponse, headers } = await apiRateLimiter(request) - if (!success) return throttlerErrorResponse - - return NextResponse.json( - { - session, - }, - { - headers, - } - ) -} diff --git a/src/app/api/sessions/[id]/route.ts b/src/app/api/sessions/[id]/route.ts deleted file mode 100644 index 293f4af1..00000000 --- a/src/app/api/sessions/[id]/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextResponse } from "next/server" -import { requireAuthApi } from "@/components/auth/require-auth" -import { prisma } from "@/lib/prisma" - -export async function DELETE(request: Request, { params: { id } }: { params: { id: string } }) { - const { session, error } = await requireAuthApi() - if (error) return error - - if (!id) { - return new Response("Missing id", { status: 400 }) - } - - const res = await prisma.session.delete({ - where: { - id: id, - userId: session.user.id, - }, - }) - - return NextResponse.json(res) -} diff --git a/src/app/api/sessions/active/route.ts b/src/app/api/sessions/active/route.ts deleted file mode 100644 index 50ec1ad4..00000000 --- a/src/app/api/sessions/active/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { NextResponse } from "next/server" -import { requireAuthApi } from "@/components/auth/require-auth" -import { getJsonApiSkip, getJsonApiSort, getJsonApiTake, IJsonApiResponse, parseJsonApiQuery } from "@/lib/json-api" -import { prisma } from "@/lib/prisma" - -export async function GET(request: Request) { - const { session, error } = await requireAuthApi() - if (error) return error - - const { searchParams } = new URL(request.url) - const query = parseJsonApiQuery(searchParams) - - const activeSessions = await prisma.session.findMany({ - where: { - userId: session.user.id, - }, - skip: getJsonApiSkip(query), - take: getJsonApiTake(query), - orderBy: getJsonApiSort(query), - }) - - const total = await prisma.session.count({ - where: { - userId: session.user.id, - }, - }) - - const response: IJsonApiResponse<(typeof activeSessions)[number]> = { - data: activeSessions, - meta: { - total: activeSessions.length, - page: query.page, - perPage: query.perPage, - totalPages: Math.ceil(total / query.perPage), - }, - } - - return NextResponse.json(response) -} diff --git a/src/components/auth/github-sign-in.tsx b/src/components/auth/github-sign-in.tsx index 6674af25..448b3e65 100644 --- a/src/components/auth/github-sign-in.tsx +++ b/src/components/auth/github-sign-in.tsx @@ -27,7 +27,7 @@ export default function GithubSignIn({ logger.debug("SignIn result", res) if (res?.error) { - if (res?.error === "OAuthAccountNotLinked") { + if (res.error === "OAuthAccountNotLinked") { } else { throw new Error(dictionary.errors.unknownError) } diff --git a/src/components/auth/login-user-auth-form.tsx b/src/components/auth/login-user-auth-form.tsx index 69ace722..e08a9c3b 100644 --- a/src/components/auth/login-user-auth-form.tsx +++ b/src/components/auth/login-user-auth-form.tsx @@ -31,8 +31,8 @@ export type IForm = z.infer> export function LoginUserAuthForm({ dictionary, searchParams, ...props }: UserAuthFormProps) { const router = useRouter() - const callbackUrl = ensureRelativeUrl(searchParams?.callbackUrl?.toString()) || authRoutes.redirectAfterSignIn - const error = searchParams?.error?.toString() + const callbackUrl = ensureRelativeUrl(searchParams.callbackUrl?.toString()) || authRoutes.redirectAfterSignIn + const error = searchParams.error?.toString() const [isLoading, setIsLoading] = React.useState(false) const [errorDisplayed, setErrorDisplayed] = React.useState(null) diff --git a/src/components/auth/require-auth.tsx b/src/components/auth/require-auth.tsx index 61a659a3..b02923aa 100644 --- a/src/components/auth/require-auth.tsx +++ b/src/components/auth/require-auth.tsx @@ -8,7 +8,7 @@ import { validateSession } from "@/lib/server-utils" export default async function requireAuth(callbackUrl?: string) { const session = await getServerSession(nextAuthOptions) - if (!session || !session.user || !session.user.id) { + if (!session || !session.user.id) { let searchParams = "" if (callbackUrl) { searchParams = "?" + new URLSearchParams({ callbackUrl }).toString() @@ -22,15 +22,13 @@ export default async function requireAuth(callbackUrl?: string) { export async function getAuthApi() { const session = await getServerSession(nextAuthOptions) - if (!session || !session.user || !session.user.id) { + if (!session || !session.user.id) { return { session: null, error: NextResponse.json({ error: "Unauthorized" }, { status: 401 }) } } - if (session) { - const sessionValidationError = await validateSession(session) - if (sessionValidationError) { - return { session: null, error: NextResponse.json({ error: sessionValidationError }, { status: 403 }) } - } + const sessionValidationError = await validateSession(session) + if (sessionValidationError) { + return { session: null, error: NextResponse.json({ error: sessionValidationError }, { status: 403 }) } } return { session, error: null } diff --git a/src/components/locale-switcher.tsx b/src/components/locale-switcher.tsx index ea5970f9..2c21fe3f 100644 --- a/src/components/locale-switcher.tsx +++ b/src/components/locale-switcher.tsx @@ -1,8 +1,8 @@ "use client" -import { i18n } from "i18n-config" import Link from "next/link" import { usePathname } from "next/navigation" +import { i18n } from "i18n-config" export default function LocaleSwitcher() { const pathName = usePathname() diff --git a/src/components/profile/get-device-icon.tsx b/src/components/profile/get-device-icon.tsx index 933c1d02..82614791 100644 --- a/src/components/profile/get-device-icon.tsx +++ b/src/components/profile/get-device-icon.tsx @@ -28,7 +28,7 @@ export default function GetDeviceIcon({ name }: { name?: string }) { return } - if (["Windows"]) { + if (["Windows"].includes(name)) { // return 'windows-logo'; return } diff --git a/src/components/profile/sessions/sessions-table.tsx b/src/components/profile/sessions/sessions-table.tsx index 2724b081..0b66a67e 100644 --- a/src/components/profile/sessions/sessions-table.tsx +++ b/src/components/profile/sessions/sessions-table.tsx @@ -51,7 +51,7 @@ export default function SessionsTable({ dictionary }: { dictionary: TDictionary (prev) => prev && { ...prev, - data: prev?.data?.filter((session) => session.id !== selectedSession), + data: prev.data?.filter((session) => session.id !== selectedSession), } ) @@ -59,7 +59,7 @@ export default function SessionsTable({ dictionary }: { dictionary: TDictionary deleteSessionMutation.mutate({ id: selectedSession }) } - const rows = activeSessions?.data?.data?.map((session) => ( + const rows = activeSessions.data?.data?.map((session) => ( )) @@ -72,7 +72,7 @@ export default function SessionsTable({ dictionary }: { dictionary: TDictionary ) const showPagination = Boolean( - activeSessions?.data && (activeSessions?.data.meta.totalPages > 1 || itemsPerPageInitial !== itemsPerPage) + activeSessions.data && (activeSessions.data.meta.totalPages > 1 || itemsPerPageInitial !== itemsPerPage) ) return ( @@ -81,11 +81,11 @@ export default function SessionsTable({ dictionary }: { dictionary: TDictionary {activeSessions.isFetched ? rows : skelRows} diff --git a/src/components/profile/update-account.tsx b/src/components/profile/update-account.tsx index 3a408129..6a06241a 100644 --- a/src/components/profile/update-account.tsx +++ b/src/components/profile/update-account.tsx @@ -42,16 +42,16 @@ export default function UpdateAccount({ dictionary }: { dictionary: TDictionary const form = useForm({ resolver: zodResolver(nonSensibleSchema(dictionary)), defaultValues: { - username: account?.data?.user?.name || "", + username: account.data?.user.name || "", }, }) const resetForm = useCallback(() => { - logger.debug("Resetting form with", account?.data?.user) + logger.debug("Resetting form with", account.data?.user) form.reset({ - username: account?.data?.user.username ?? "", + username: account.data?.user.username ?? "", }) - }, [account?.data?.user, form]) + }, [account.data?.user, form]) useEffect(() => { resetForm() @@ -76,7 +76,7 @@ export default function UpdateAccount({ dictionary }: { dictionary: TDictionary label={dictionary.profilePage.profileDetails.username.label} placeholder={dictionary.profilePage.profileDetails.username.placeholder} type="text" - disabled={updateUserMutation.isLoading || !account?.isFetched} + disabled={updateUserMutation.isLoading || !account.isFetched} form={form} name="username" /> diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index bd7d2d1b..9766462f 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -31,16 +31,15 @@ const FormField = < } const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) + const fieldContext = React.useContext(FormFieldContext) as FormFieldContextValue | undefined const itemContext = React.useContext(FormItemContext) const { getFieldState, formState } = useFormContext() - const fieldState = getFieldState(fieldContext.name, formState) - if (!fieldContext) { throw new Error("useFormField should be used within ") } + const fieldState = getFieldState(fieldContext.name, formState) const { id } = itemContext return { @@ -111,7 +110,7 @@ FormDescription.displayName = "FormDescription" const FormMessage = React.forwardRef>( ({ className, children, ...props }, ref) => { const { error, formMessageId } = useFormField() - const body = error ? String(error?.message) : children + const body = error ? String(error.message) : children if (!body) { return null diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index bba9ecb3..895e0e49 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -32,7 +32,7 @@ export const nextAuthOptions: NextAuthOptions & { password: { label: "Password", type: "password" }, }, authorize: async (credentials, req) => { - const referer = (req.headers?.referer as string) ?? "" + const referer = (req.headers?.referer as string | undefined) ?? "" const refererUrl = ensureRelativeUrl(referer) ?? "" const lang = i18n.locales.find((locale) => refererUrl.startsWith(`/${locale}/`)) ?? i18n.defaultLocale const dictionary = @@ -123,13 +123,11 @@ export const nextAuthOptions: NextAuthOptions & { jwt: async ({ token, user }) => { // logger.debug("JWT token", token) - if (user) { - token.id = user.id - token.email = user.email - if ("username" in user) token.username = user.username - if ("role" in user) token.role = user.role as string - if ("uuid" in user) token.uuid = user.uuid as string - } + token.id = user.id + token.email = user.email + if ("username" in user) token.username = user.username + if ("role" in user) token.role = user.role as string + if ("uuid" in user) token.uuid = user.uuid as string return token }, @@ -183,7 +181,7 @@ export const nextAuthOptions: NextAuthOptions & { const role = dbUser.role //* Fill session with token data - const uuid = token && "uuid" in token ? token.uuid : undefined + const uuid = "uuid" in token ? token.uuid : undefined const sessionFilled = { ...session, @@ -191,7 +189,7 @@ export const nextAuthOptions: NextAuthOptions & { ...session.user, id: token.id, username: username ?? undefined, - role: role ?? undefined, + role, uuid, }, } diff --git a/src/lib/langs.ts b/src/lib/langs.ts index 48d2e636..5129db0c 100644 --- a/src/lib/langs.ts +++ b/src/lib/langs.ts @@ -1,3 +1,4 @@ +import { ValueOf } from "@/types" import { Locale } from "i18n-config" import "server-only" @@ -8,6 +9,7 @@ const dictionaries = { fr: () => import("../langs/fr.json").then((module) => module.default), } -export const getDictionary = async (locale: Locale) => dictionaries[locale]?.() ?? dictionaries.en() +export const getDictionary = async (locale: Locale) => + (dictionaries[locale] as ValueOf | undefined)?.() ?? dictionaries.en() export type TDictionary = Awaited> diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 3d64599f..4f7736df 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client" const globalForPrisma = global as unknown as { prisma: PrismaClient } export const prisma = + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition globalForPrisma.prisma || new PrismaClient({ log: ["warn", "error"], diff --git a/src/lib/redis.ts b/src/lib/redis.ts index 996d3b2c..f20ff956 100644 --- a/src/lib/redis.ts +++ b/src/lib/redis.ts @@ -42,8 +42,8 @@ export const redisGetSert = async ( return callbackValue } if (value) { - if (options?.logWhenCached) logger.log("Redis value retrieved. key:", keyInfo.key, " value:", value) - if (!options?.disableSWR) executeCallback() + if (options.logWhenCached) logger.log("Redis value retrieved. key:", keyInfo.key, " value:", value) + if (!options.disableSWR) executeCallback() return JSON.parse(value) as T } return executeCallback() diff --git a/src/types/index.ts b/src/types/index.ts index 234a90c0..3df88d3d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,3 +11,5 @@ export type apiInputFromSchema z.Schema) | undefined> = { input: T extends () => z.Schema ? z.infer> : unknown ctx: ITrpcContext } + +export type ValueOf = T[keyof T] diff --git a/tsconfig.json b/tsconfig.json index 98cb9d14..5fa688b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,6 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.mjs", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.mjs", ".next/types/**/*.ts", ".eslintrc.js"], "exclude": ["node_modules"] }