chore: finish frontend ESLint migration #18
Enable every plugin recommended preset and replace Biome with ESLint formatting. Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
This commit is contained in:
@@ -1,23 +1,24 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import { createContext, use } from "react";
|
||||
|
||||
import type { paths } from "@/shared/api";
|
||||
|
||||
type User =
|
||||
paths["/api/v1/auth/me"]["get"]["responses"][200]["content"]["application/json"];
|
||||
type AuthContextValue = {
|
||||
user: User | null;
|
||||
accessToken: string | null;
|
||||
ready: boolean;
|
||||
accessToken: string | undefined;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (email: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
ready: boolean;
|
||||
refresh: () => Promise<boolean>;
|
||||
register: (email: string, password: string) => Promise<void>;
|
||||
user: undefined | User;
|
||||
};
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
type User
|
||||
= paths["/api/v1/auth/me"]["get"]["responses"][200]["content"]["application/json"];
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||
|
||||
function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
const context = use(AuthContext);
|
||||
if (!context) throw new Error("AuthProvider is required");
|
||||
return context;
|
||||
}
|
||||
|
||||
export { AuthContext, type User, useAuth };
|
||||
export { AuthContext, useAuth, type User };
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
|
||||
import { QueryClientProvider, useQueryClient } from "@tanstack/react-query";
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { apiClient, apiQueryClient, useAuthenticatedQuery } from "@/shared/api";
|
||||
import { createTestQueryClient } from "@/test/query";
|
||||
import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import { AuthProvider } from "./auth";
|
||||
import { useAuth } from "./auth-context";
|
||||
|
||||
@@ -14,14 +17,22 @@ const bankCardsKey = apiQueryClient.queryOptions("get", "/api/v1/bank-cards", {
|
||||
params: { query: { includeArchived: "false" } },
|
||||
}).queryKey;
|
||||
|
||||
function createAuthWrapper(queryClient: ReturnType<typeof createTestQueryClient>): FC<PropsWithChildren> {
|
||||
return props => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{props.children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe("AuthProvider", () => {
|
||||
it("authorizes requests after restoring the initial session", async () => {
|
||||
let authorization: string | null = null;
|
||||
let authorization: null | string = /* eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. */ null;
|
||||
server.use(
|
||||
http.post("*/api/v1/auth/refresh", () =>
|
||||
HttpResponse.json({
|
||||
accessToken: "restored-token",
|
||||
user: { id: "user-1", email: "user@example.com" },
|
||||
user: { email: "user@example.com", id: "user-1" },
|
||||
}),
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts", ({ request }) => {
|
||||
@@ -30,16 +41,8 @@ describe("AuthProvider", () => {
|
||||
}),
|
||||
);
|
||||
const queryClient = createTestQueryClient();
|
||||
const wrapper: FC<PropsWithChildren> = (props) => {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
const { result, rerender } = renderHook(
|
||||
const wrapper: FC<PropsWithChildren> = createAuthWrapper(queryClient);
|
||||
const { rerender, result } = renderHook(
|
||||
() => ({ ...useAuth(), queryClient: useQueryClient() }),
|
||||
{ wrapper },
|
||||
);
|
||||
@@ -61,21 +64,13 @@ describe("AuthProvider", () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
queryClient.setQueryData(["cached-query"], { value: "stale" });
|
||||
queryClient.setQueryData(bankCardsKey, [{ id: "bank-card-1" }]);
|
||||
const wrapper: FC<PropsWithChildren> = (props) => {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
const wrapper: FC<PropsWithChildren> = createAuthWrapper(queryClient);
|
||||
const { result } = renderHook(() => useAuth(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.ready).toBe(true));
|
||||
|
||||
expect(result.current.user).toBeNull();
|
||||
expect(result.current.accessToken).toBeNull();
|
||||
expect(result.current.user).toBeUndefined();
|
||||
expect(result.current.accessToken).toBeUndefined();
|
||||
expect(queryClient.getQueryData(["cached-query"])).toBeUndefined();
|
||||
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
|
||||
});
|
||||
@@ -88,7 +83,7 @@ describe("AuthProvider", () => {
|
||||
return refreshCalls === 1
|
||||
? HttpResponse.json({
|
||||
accessToken: "restored-token",
|
||||
user: { id: "user-1", email: "user@example.com" },
|
||||
user: { email: "user@example.com", id: "user-1" },
|
||||
})
|
||||
: HttpResponse.error();
|
||||
}),
|
||||
@@ -99,15 +94,7 @@ describe("AuthProvider", () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
queryClient.setQueryData(["cached-query"], { value: "stale" });
|
||||
queryClient.setQueryData(bankCardsKey, [{ id: "bank-card-1" }]);
|
||||
const wrapper: FC<PropsWithChildren> = (props) => {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
const wrapper: FC<PropsWithChildren> = createAuthWrapper(queryClient);
|
||||
const { result } = renderHook(() => useAuth(), { wrapper });
|
||||
|
||||
await waitFor(() =>
|
||||
@@ -116,8 +103,8 @@ describe("AuthProvider", () => {
|
||||
const response = await apiClient.GET("/api/v1/brokerage-accounts");
|
||||
|
||||
expect(response.response.status).toBe(401);
|
||||
await waitFor(() => expect(result.current.user).toBeNull());
|
||||
expect(result.current.accessToken).toBeNull();
|
||||
await waitFor(() => expect(result.current.user).toBeUndefined());
|
||||
expect(result.current.accessToken).toBeUndefined();
|
||||
expect(queryClient.getQueryData(["cached-query"])).toBeUndefined();
|
||||
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
|
||||
});
|
||||
@@ -127,7 +114,7 @@ describe("AuthProvider", () => {
|
||||
http.post("*/api/v1/auth/refresh", () =>
|
||||
HttpResponse.json({
|
||||
accessToken: "test-token",
|
||||
user: { id: "user-1", email: "user@example.com" },
|
||||
user: { email: "user@example.com", id: "user-1" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -147,14 +134,7 @@ describe("AuthProvider", () => {
|
||||
});
|
||||
queryClient.setQueryData(portfolioKey, { source: "current-connection" });
|
||||
queryClient.setQueryData(bankCardsKey, [{ id: "bank-card-1" }]);
|
||||
const wrapper: FC<PropsWithChildren> = (props) => {
|
||||
const { children } = props;
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
const wrapper: FC<PropsWithChildren> = createAuthWrapper(queryClient);
|
||||
const { result } = renderHook(() => useAuth(), { wrapper });
|
||||
await waitFor(() => expect(result.current.accessToken).toBe("test-token"));
|
||||
|
||||
@@ -163,8 +143,8 @@ describe("AuthProvider", () => {
|
||||
]);
|
||||
await act(() => result.current.logout());
|
||||
|
||||
expect(result.current.user).toBeNull();
|
||||
expect(result.current.accessToken).toBeNull();
|
||||
expect(result.current.user).toBeUndefined();
|
||||
expect(result.current.accessToken).toBeUndefined();
|
||||
expect(queryClient.getQueryData(statusKey)).toBeUndefined();
|
||||
expect(queryClient.getQueryData(portfolioKey)).toBeUndefined();
|
||||
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
|
||||
@@ -177,10 +157,7 @@ describe("AuthProvider", () => {
|
||||
])(
|
||||
"serializes late refresh (failure=%s, login=%s)",
|
||||
async (failure, loginAfterLogout) => {
|
||||
let release!: () => void;
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const { promise: pending, resolve: release } = Promise.withResolvers<void>();
|
||||
let calls = 0;
|
||||
const order: string[] = [];
|
||||
server.use(
|
||||
@@ -194,7 +171,7 @@ describe("AuthProvider", () => {
|
||||
? HttpResponse.error()
|
||||
: HttpResponse.json({
|
||||
accessToken: "a",
|
||||
user: { id: "a", email: "a@example.com" },
|
||||
user: { email: "a@example.com", id: "a" },
|
||||
});
|
||||
}),
|
||||
http.post("*/api/v1/auth/logout", () => {
|
||||
@@ -205,7 +182,7 @@ describe("AuthProvider", () => {
|
||||
order.push("login-cookie");
|
||||
return HttpResponse.json({
|
||||
accessToken: "b",
|
||||
user: { id: "b", email: "b@example.com" },
|
||||
user: { email: "b@example.com", id: "b" },
|
||||
});
|
||||
}),
|
||||
http.get("*/api/v1/brokerage-accounts", () =>
|
||||
@@ -213,22 +190,20 @@ describe("AuthProvider", () => {
|
||||
),
|
||||
);
|
||||
const queryClient = createTestQueryClient();
|
||||
const wrapper: FC<PropsWithChildren> = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
const wrapper: FC<PropsWithChildren> = createAuthWrapper(queryClient);
|
||||
const { result } = renderHook(() => useAuth(), { wrapper });
|
||||
await waitFor(() => expect(result.current.ready).toBe(true));
|
||||
const oldRequest = apiClient
|
||||
.GET("/api/v1/brokerage-accounts")
|
||||
.catch(() => null);
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-await -- Attach rejection handling before releasing the deliberately pending query.
|
||||
.catch(() => {});
|
||||
await waitFor(() => expect(order).toContain("refresh-start"));
|
||||
let logout!: Promise<void>;
|
||||
act(() => {
|
||||
logout = result.current.logout();
|
||||
});
|
||||
await waitFor(() => expect(result.current.user).toBeNull());
|
||||
await waitFor(() => expect(result.current.user).toBeUndefined());
|
||||
let login!: Promise<void>;
|
||||
act(() => {
|
||||
login = loginAfterLogout
|
||||
@@ -239,8 +214,8 @@ describe("AuthProvider", () => {
|
||||
release();
|
||||
await Promise.all([oldRequest, logout, login]);
|
||||
});
|
||||
expect(result.current.user?.id ?? null).toBe(
|
||||
loginAfterLogout ? "b" : null,
|
||||
expect(result.current.user?.id ?? /* eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. */ null).toBe(
|
||||
loginAfterLogout ? "b" : /* eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. */ null,
|
||||
);
|
||||
expect(order).toEqual([
|
||||
"refresh-start",
|
||||
@@ -257,16 +232,12 @@ describe("AuthProvider", () => {
|
||||
http.post("*/api/v1/auth/refresh", () =>
|
||||
HttpResponse.json({
|
||||
accessToken: identity,
|
||||
user: { id: identity, email: `${identity}@example.com` },
|
||||
user: { email: `${identity}@example.com`, id: identity },
|
||||
}),
|
||||
),
|
||||
);
|
||||
const queryClient = createTestQueryClient();
|
||||
const wrapper: FC<PropsWithChildren> = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
const wrapper: FC<PropsWithChildren> = createAuthWrapper(queryClient);
|
||||
const { result } = renderHook(() => useAuth(), { wrapper });
|
||||
await waitFor(() => expect(result.current.ready).toBe(true));
|
||||
queryClient.setQueryData(bankCardsKey, [{ id: "a-card" }]);
|
||||
@@ -274,14 +245,16 @@ describe("AuthProvider", () => {
|
||||
expect(queryClient.getQueryData(bankCardsKey)).toEqual([{ id: "a-card" }]);
|
||||
let release!: (value: string) => void;
|
||||
const pending = queryClient
|
||||
.fetchQuery({
|
||||
.query({
|
||||
queryFn: () => {
|
||||
const deferred = Promise.withResolvers<string>();
|
||||
release = deferred.resolve;
|
||||
return deferred.promise;
|
||||
},
|
||||
queryKey: ["pending-finance"],
|
||||
queryFn: () =>
|
||||
new Promise<string>((resolve) => {
|
||||
release = resolve;
|
||||
}),
|
||||
})
|
||||
.catch(() => null);
|
||||
// eslint-disable-next-line unicorn/prefer-await -- Attach rejection handling before releasing the deliberately pending query.
|
||||
.catch(() => {});
|
||||
identity = "b";
|
||||
await act(() => result.current.refresh());
|
||||
release("a-data");
|
||||
@@ -296,7 +269,7 @@ describe("AuthProvider", () => {
|
||||
http.post("*/api/v1/auth/refresh", () =>
|
||||
HttpResponse.json({
|
||||
accessToken: identity,
|
||||
user: { id: identity, email: `${identity}@example.com` },
|
||||
user: { email: `${identity}@example.com`, id: identity },
|
||||
}),
|
||||
),
|
||||
http.get("*/api/v1/bank-cards", () =>
|
||||
@@ -304,11 +277,7 @@ describe("AuthProvider", () => {
|
||||
),
|
||||
);
|
||||
const queryClient = createTestQueryClient();
|
||||
const wrapper: FC<PropsWithChildren> = (props) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{props.children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
const wrapper: FC<PropsWithChildren> = createAuthWrapper(queryClient);
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
auth: useAuth(),
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { apiClient, configureAuthClient, type paths } from "@/shared/api";
|
||||
|
||||
import { AuthContext, type User } from "./auth-context";
|
||||
|
||||
type Authentication =
|
||||
paths["/api/v1/auth/login"]["post"]["responses"][200]["content"]["application/json"];
|
||||
type Credentials =
|
||||
paths["/api/v1/auth/login"]["post"]["requestBody"]["content"]["application/json"];
|
||||
type Authentication
|
||||
= paths["/api/v1/auth/login"]["post"]["responses"][200]["content"]["application/json"];
|
||||
type Credentials
|
||||
= paths["/api/v1/auth/login"]["post"]["requestBody"]["content"]["application/json"];
|
||||
async function request(
|
||||
path: "/api/v1/auth/login" | "/api/v1/auth/register",
|
||||
email: string,
|
||||
@@ -24,32 +27,34 @@ async function request(
|
||||
const AuthProvider: FC<PropsWithChildren> = (props) => {
|
||||
const { children } = props;
|
||||
const queryClient = useQueryClient();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const accessTokenRef = useRef<string | null>(null);
|
||||
const [user, setUser] = useState<undefined | User>(undefined);
|
||||
const [accessToken, setAccessToken] = useState<string | undefined>(undefined);
|
||||
const accessTokenRef = useRef<string | undefined>(undefined);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const generation = useRef(0);
|
||||
const userRef = useRef<User | null>(null);
|
||||
const authQueue = useRef<Promise<unknown>>(Promise.resolve());
|
||||
const refreshPromise = useRef<Promise<boolean> | null>(null);
|
||||
const generationRef = useRef(0);
|
||||
const userRef = useRef<undefined | User>(undefined);
|
||||
const authQueueRef = useRef<Promise<unknown>>(Promise.resolve());
|
||||
const refreshPromiseRef = useRef<Promise<boolean> | undefined>(undefined);
|
||||
// Cookie-writing requests share ordering, including across tabs with Web Locks.
|
||||
const serialize = useCallback(<T,>(run: () => Promise<T>): Promise<T> => {
|
||||
const next = authQueue.current.then(async () =>
|
||||
// eslint-disable-next-line unicorn/prefer-await -- Attach the handler immediately and retain the pending promise for queue/race ordering.
|
||||
const next = authQueueRef.current.then(async () =>
|
||||
"locks" in navigator
|
||||
? navigator.locks.request("wealth-ledger-refresh", run)
|
||||
: run(),
|
||||
);
|
||||
authQueue.current = next.catch(() => undefined);
|
||||
// eslint-disable-next-line unicorn/prefer-await -- Attach the handler immediately and retain the pending promise for queue/race ordering.
|
||||
authQueueRef.current = next.catch(() => {});
|
||||
return next;
|
||||
}, []);
|
||||
const applyAuthentication = useCallback(
|
||||
(authentication: Authentication) => {
|
||||
const changedIdentity =
|
||||
userRef.current && userRef.current.id !== authentication.user.id;
|
||||
const changedIdentity
|
||||
= userRef.current && userRef.current.id !== authentication.user.id;
|
||||
accessTokenRef.current = authentication.accessToken;
|
||||
if (changedIdentity) {
|
||||
generation.current += 1;
|
||||
generationRef.current += 1;
|
||||
queryClient.removeQueries({ type: "inactive" });
|
||||
queryClient.getMutationCache().clear();
|
||||
void queryClient.resetQueries();
|
||||
@@ -61,36 +66,38 @@ const AuthProvider: FC<PropsWithChildren> = (props) => {
|
||||
[queryClient],
|
||||
);
|
||||
const clearSession = useCallback(() => {
|
||||
generation.current += 1;
|
||||
userRef.current = null;
|
||||
setUser(null);
|
||||
setAccessToken(null);
|
||||
accessTokenRef.current = null;
|
||||
generationRef.current += 1;
|
||||
userRef.current = undefined;
|
||||
setUser(undefined);
|
||||
setAccessToken(undefined);
|
||||
accessTokenRef.current = undefined;
|
||||
queryClient.clear();
|
||||
}, [queryClient]);
|
||||
|
||||
const refresh = useCallback((): Promise<boolean> => {
|
||||
if (refreshPromise.current) return refreshPromise.current;
|
||||
const started = generation.current;
|
||||
if (refreshPromiseRef.current) return refreshPromiseRef.current;
|
||||
const started = generationRef.current;
|
||||
const pending = serialize(async () => {
|
||||
if (started !== generation.current) return false;
|
||||
if (started !== generationRef.current) return false;
|
||||
try {
|
||||
const { data } = await apiClient.POST("/api/v1/auth/refresh");
|
||||
if (started !== generation.current) return false;
|
||||
if (started !== generationRef.current) return false;
|
||||
if (!data) {
|
||||
clearSession();
|
||||
return false;
|
||||
}
|
||||
applyAuthentication(data);
|
||||
return true;
|
||||
} catch {
|
||||
if (started === generation.current) clearSession();
|
||||
}
|
||||
catch {
|
||||
if (started === generationRef.current) clearSession();
|
||||
return false;
|
||||
}
|
||||
// eslint-disable-next-line unicorn/prefer-await -- Attach the handler immediately and retain the pending promise for queue/race ordering.
|
||||
}).finally(() => {
|
||||
if (refreshPromise.current === pending) refreshPromise.current = null;
|
||||
if (refreshPromiseRef.current === pending) refreshPromiseRef.current = undefined;
|
||||
});
|
||||
refreshPromise.current = pending;
|
||||
refreshPromiseRef.current = pending;
|
||||
return pending;
|
||||
}, [applyAuthentication, clearSession, serialize]);
|
||||
|
||||
@@ -104,10 +111,10 @@ const AuthProvider: FC<PropsWithChildren> = (props) => {
|
||||
password: string,
|
||||
) {
|
||||
clearSession();
|
||||
const started = generation.current;
|
||||
const started = generationRef.current;
|
||||
await serialize(async () => {
|
||||
const authentication = await request(path, email, password);
|
||||
if (started === generation.current) applyAuthentication(authentication);
|
||||
if (started === generationRef.current) applyAuthentication(authentication);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,25 +128,25 @@ const AuthProvider: FC<PropsWithChildren> = (props) => {
|
||||
useEffect(() => {
|
||||
configureAuthClient({
|
||||
accessToken: () => accessTokenRef.current,
|
||||
generation: () => generation.current,
|
||||
refresh,
|
||||
clear: clearSession,
|
||||
generation: () => generationRef.current,
|
||||
refresh,
|
||||
});
|
||||
});
|
||||
useEffect(() => () => configureAuthClient(null), []);
|
||||
useEffect(() => () => configureAuthClient(), []);
|
||||
|
||||
return (
|
||||
<AuthContext
|
||||
value={{
|
||||
user,
|
||||
accessToken,
|
||||
ready,
|
||||
login: (email, password) =>
|
||||
authenticate("/api/v1/auth/login", email, password),
|
||||
logout,
|
||||
ready,
|
||||
refresh,
|
||||
register: (email, password) =>
|
||||
authenticate("/api/v1/auth/register", email, password),
|
||||
logout,
|
||||
refresh,
|
||||
user,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { validationMessage } from "@/shared/i18n";
|
||||
|
||||
const EMAIL_PART_COUNT = 2;
|
||||
|
||||
const ASCII_SPACE = 32;
|
||||
const ASCII_DELETE = 127;
|
||||
const MAX_EMAIL_LENGTH = 254;
|
||||
@@ -8,10 +11,11 @@ const MIN_PASSWORD_BYTES = 12;
|
||||
const MAX_PASSWORD_BYTES = 72;
|
||||
|
||||
function isEmailCharacterInvalid(character: string): boolean {
|
||||
const codePoint = character.codePointAt(0);
|
||||
return (
|
||||
/\s/.test(character) ||
|
||||
character.charCodeAt(0) < ASCII_SPACE ||
|
||||
character.charCodeAt(0) === ASCII_DELETE
|
||||
/\s/.test(character)
|
||||
|| (codePoint !== undefined && codePoint < ASCII_SPACE)
|
||||
|| codePoint === ASCII_DELETE
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,18 +25,18 @@ const email = z
|
||||
.toLowerCase()
|
||||
.max(MAX_EMAIL_LENGTH)
|
||||
.refine(
|
||||
(value) =>
|
||||
value.split("@").length === 2 &&
|
||||
value.split("@").every(Boolean) &&
|
||||
!value.split("").some(isEmailCharacterInvalid),
|
||||
value =>
|
||||
value.split("@").length === EMAIL_PART_COUNT
|
||||
&& value.split("@").every(Boolean)
|
||||
&& value.split("").every(element => !isEmailCharacterInvalid(element)),
|
||||
{ error: validationMessage("auth.validation.invalidEmail") },
|
||||
);
|
||||
const password = z
|
||||
.string()
|
||||
.refine(
|
||||
(value) =>
|
||||
new TextEncoder().encode(value).length >= MIN_PASSWORD_BYTES &&
|
||||
new TextEncoder().encode(value).length <= MAX_PASSWORD_BYTES,
|
||||
value =>
|
||||
new TextEncoder().encode(value).length >= MIN_PASSWORD_BYTES
|
||||
&& new TextEncoder().encode(value).length <= MAX_PASSWORD_BYTES,
|
||||
{ error: validationMessage("auth.validation.invalidPasswordLength") },
|
||||
);
|
||||
|
||||
@@ -42,10 +46,10 @@ const loginSchema = z.object({
|
||||
passwordConfirmation: z.string().optional(),
|
||||
});
|
||||
const registerSchema = loginSchema.refine(
|
||||
(value) => value.password === value.passwordConfirmation,
|
||||
value => value.password === value.passwordConfirmation,
|
||||
{
|
||||
path: ["passwordConfirmation"],
|
||||
error: validationMessage("auth.validation.passwordsDoNotMatch"),
|
||||
path: ["passwordConfirmation"],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user