Compare commits

...

4 Commits

Author SHA1 Message Date
Ilia Mashkov
8d283064a0 feat(LoginForm): use component as prop pattern to add customizable input and button components 2026-03-31 12:54:43 +03:00
Ilia Mashkov
a6f4b993dd feat(auth): set the token after login/register/logout 2026-03-31 12:53:41 +03:00
Ilia Mashkov
3d7eb850ec fix(auth): remove unnecessary token header setup 2026-03-31 12:52:43 +03:00
Ilia Mashkov
69d026afce feat(auth): add selectors for email and password 2026-03-31 12:51:41 +03:00
6 changed files with 96 additions and 32 deletions

View File

@@ -2,17 +2,7 @@ import { api as baseApi, useTokenStore } from "shared/api";
export const authHttpClient = baseApi.extend({ export const authHttpClient = baseApi.extend({
hooks: { hooks: {
beforeRequest: [ beforeRequest: [],
(request) => {
const token = useTokenStore.getState().accessToken;
if (token) {
request.headers.set("Authorization", `Bearer ${token}`);
}
return request;
},
],
afterResponse: [ afterResponse: [
async (request, options, response) => { async (request, options, response) => {
if (response.status !== 401) { if (response.status !== 401) {

View File

@@ -1,3 +1,5 @@
export * from "./selectFormValid/selectFormValid"; export * from "./selectFormValid/selectFormValid";
export * from "./selectAuthData/selectAuthData"; export * from "./selectAuthData/selectAuthData";
export * from "./selectStatusIsLoading/selectStatusIsLoading"; export * from "./selectStatusIsLoading/selectStatusIsLoading";
export * from "./selectEmail/selectEmail";
export * from "./selectPassword/selectPassword";

View File

@@ -0,0 +1,4 @@
import type { AuthStore } from "../../types/store";
export const selectEmail = (state: AuthStore) =>
state.formData?.email?.value ?? "";

View File

@@ -0,0 +1,4 @@
import type { AuthStore } from "../../types/store";
export const selectPassword = (state: AuthStore) =>
state.formData?.password?.value ?? "";

View File

@@ -2,7 +2,7 @@ import { create } from "zustand";
import type { AuthStore, AuthStoreState } from "../../types/store"; import type { AuthStore, AuthStoreState } from "../../types/store";
import { login, logout, register } from "../../../api"; import { login, logout, register } from "../../../api";
import { callApi } from "shared/utils"; import { callApi } from "shared/utils";
import { UNEXPECTED_ERROR_MESSAGE } from "shared/api"; import { UNEXPECTED_ERROR_MESSAGE, useTokenStore } from "shared/api";
import { selectAuthData, selectFormValid } from "../../selectors"; import { selectAuthData, selectFormValid } from "../../selectors";
import { validateEmail, validatePassword } from "../../../lib"; import { validateEmail, validatePassword } from "../../../lib";
@@ -68,6 +68,7 @@ export const useAuthStore = create<AuthStore>()((set, get) => ({
user: responseData?.user, user: responseData?.user,
error: null, error: null,
}); });
useTokenStore.setState({ accessToken: responseData?.accessToken });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
set({ set({
@@ -108,6 +109,7 @@ export const useAuthStore = create<AuthStore>()((set, get) => ({
user: responseData?.user, user: responseData?.user,
error: null, error: null,
}); });
useTokenStore.setState({ accessToken: responseData?.accessToken });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
set({ set({
@@ -138,7 +140,7 @@ export const useAuthStore = create<AuthStore>()((set, get) => ({
error: null, error: null,
}); });
// useTokenStore.setState({ accessToken: null }); useTokenStore.setState({ accessToken: null });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
set({ error: new Error(UNEXPECTED_ERROR_MESSAGE) }); set({ error: new Error(UNEXPECTED_ERROR_MESSAGE) });

View File

@@ -1,16 +1,52 @@
import { import {
selectEmail,
selectFormValid, selectFormValid,
selectPassword,
selectStatusIsLoading, selectStatusIsLoading,
useAuthStore, useAuthStore,
} from "../../model"; } from "../../model";
import type { SubmitEvent, ChangeEvent } from "react"; import {
type SubmitEvent,
type ChangeEvent,
type HTMLAttributes,
type InputHTMLAttributes,
type ButtonHTMLAttributes,
cloneElement,
type ReactElement,
} from "react";
export type InputAttributes = InputHTMLAttributes<HTMLInputElement>;
export type ButtonAttributes = ButtonHTMLAttributes<HTMLButtonElement>;
export interface Props {
InputComponent?: ReactElement<InputAttributes>;
ButtonComponent?: ReactElement<ButtonAttributes>;
}
const DefaultInputComponent = ({
value,
onChange,
["aria-label"]: ariaLabel,
}: InputAttributes) => (
<input
type="email"
value={value}
onChange={onChange}
aria-label={ariaLabel}
/>
);
const DefaultButtonComponent = ({ disabled }: ButtonAttributes) => (
<button type="submit" disabled={disabled} />
);
/** /**
* Login form component * Login form component
*/ */
export function LoginForm() { export function LoginForm({ InputComponent, ButtonComponent }: Props) {
const { formData, setEmail, setPassword, login } = useAuthStore(); const { setEmail, setPassword, login } = useAuthStore();
const email = useAuthStore(selectEmail);
const password = useAuthStore(selectPassword);
const formValid = useAuthStore(selectFormValid); const formValid = useAuthStore(selectFormValid);
const isLoading = useAuthStore(selectStatusIsLoading); const isLoading = useAuthStore(selectStatusIsLoading);
@@ -31,21 +67,47 @@ export function LoginForm() {
return ( return (
<form aria-label="Login form" onSubmit={handleSubmit}> <form aria-label="Login form" onSubmit={handleSubmit}>
<input {InputComponent ? (
cloneElement(InputComponent, {
type: "email",
value: email,
onChange: handleEmailChange,
"aria-label": "Email",
})
) : (
<DefaultInputComponent
type="email" type="email"
aria-label="Email" value={email}
value={formData?.email?.value ?? ""}
onChange={handleEmailChange} onChange={handleEmailChange}
aria-label="Email"
/> />
<input )}
{InputComponent ? (
cloneElement(InputComponent, {
type: "password",
value: password,
onChange: handlePasswordChange,
"aria-label": "Password",
})
) : (
<DefaultInputComponent
type="password" type="password"
aria-label="Password" value={password}
value={formData?.password?.value ?? ""}
onChange={handlePasswordChange} onChange={handlePasswordChange}
aria-label="Password"
/> />
<button type="submit" disabled={disabled}> )}
{ButtonComponent ? (
cloneElement(ButtonComponent, {
type: "submit",
disabled: disabled,
children: "Login",
})
) : (
<DefaultButtonComponent type="submit" disabled={disabled}>
Login Login
</button> </DefaultButtonComponent>
)}
</form> </form>
); );
} }