mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
feat(web): improve UX error interaction and update dependencies (#313)
* fix: remove react-cookie since we can't delete the user_session cookie using JS (due to httponly being set) * chore: update dependencies and set a global react dependency override (react-ridge-state creates a problem, this fixes it) * chore: tidy up APIClient, login and logout pages * fix: catch canOnboard() error in login.tsx enhancement: add toast notify on intentional logout and nicely reset the context state; make sure screen-blinking is down to a minimum by having min-h-screen present. * fix: let onboarding redirect to / instead of /login (/login isn't used anymore) * fix: use normal <input /> caret cursor for SearchColumnFilter, instead of pointer * chore(web): remove react-cookie package
This commit is contained in:
parent
0256ea52fd
commit
a84a7364e2
7 changed files with 220 additions and 144 deletions
|
@ -1,13 +1,12 @@
|
|||
import { baseUrl, sseBaseUrl } from "../utils";
|
||||
import { AuthContext } from "../utils/Context";
|
||||
import { Cookies } from "react-cookie";
|
||||
|
||||
interface ConfigType {
|
||||
body?: BodyInit | Record<string, unknown> | unknown | null;
|
||||
body?: BodyInit | Record<string, unknown> | unknown;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
type PostBody = BodyInit | Record<string, unknown> | unknown | null;
|
||||
type PostBody = BodyInit | Record<string, unknown> | unknown;
|
||||
|
||||
export async function HttpClient<T>(
|
||||
endpoint: string,
|
||||
|
@ -16,7 +15,7 @@ export async function HttpClient<T>(
|
|||
): Promise<T> {
|
||||
const config = {
|
||||
method: method,
|
||||
body: body ? JSON.stringify(body) : null,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
|
@ -24,44 +23,34 @@ export async function HttpClient<T>(
|
|||
...customConfig
|
||||
} as RequestInit;
|
||||
|
||||
|
||||
return window.fetch(`${baseUrl()}${endpoint}`, config)
|
||||
.then(async response => {
|
||||
if (response.status === 401) {
|
||||
if (!response.ok) {
|
||||
// if 401 consider the session expired and force logout
|
||||
const cookies = new Cookies();
|
||||
cookies.remove("user_session");
|
||||
AuthContext.reset();
|
||||
if (response.status === 401) {
|
||||
// Remove auth info from localStorage
|
||||
AuthContext.reset();
|
||||
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
// Show an error toast to notify the user what occurred
|
||||
return Promise.reject(new Error("Unauthorized."));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(await response.text()));
|
||||
}
|
||||
|
||||
if ([403, 404].includes(response.status))
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
|
||||
// 201 comes from a POST and can contain data
|
||||
if ([201].includes(response.status))
|
||||
return await response.json();
|
||||
|
||||
// 204 ok no data
|
||||
if ([204].includes(response.status))
|
||||
// Resolve immediately since 204 contains no data
|
||||
if (response.status === 204)
|
||||
return Promise.resolve(response);
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
const errorMessage = await response.text();
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
});
|
||||
}
|
||||
|
||||
const appClient = {
|
||||
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
|
||||
Post: <T>(endpoint: string, data: PostBody) => HttpClient<void | T>(endpoint, "POST", { body: data }),
|
||||
PostBody: <T>(endpoint: string, data: PostBody) => HttpClient<T>(endpoint, "POST", { body: data }),
|
||||
Post: <T = void>(endpoint: string, data: PostBody = undefined) => HttpClient<T>(endpoint, "POST", { body: data }),
|
||||
Put: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PUT", { body: data }),
|
||||
Patch: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
||||
Patch: (endpoint: string, data: PostBody = undefined) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
||||
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
|
||||
};
|
||||
|
||||
|
@ -71,7 +60,7 @@ export const APIClient = {
|
|||
username: username,
|
||||
password: password
|
||||
}),
|
||||
logout: () => appClient.Post("api/auth/logout", null),
|
||||
logout: () => appClient.Post("api/auth/logout"),
|
||||
validate: () => appClient.Get<void>("api/auth/validate"),
|
||||
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", {
|
||||
username: username,
|
||||
|
@ -83,7 +72,7 @@ export const APIClient = {
|
|||
create: (action: Action) => appClient.Post("api/actions", action),
|
||||
update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action),
|
||||
delete: (id: number) => appClient.Delete(`api/actions/${id}`),
|
||||
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null)
|
||||
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`)
|
||||
},
|
||||
config: {
|
||||
get: () => appClient.Get<Config>("api/config")
|
||||
|
@ -118,7 +107,7 @@ export const APIClient = {
|
|||
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
|
||||
// returns all possible indexer definitions
|
||||
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
|
||||
create: (indexer: Indexer) => appClient.PostBody<Indexer>("api/indexer", indexer),
|
||||
create: (indexer: Indexer) => appClient.Post<Indexer>("api/indexer", indexer),
|
||||
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
|
||||
delete: (id: number) => appClient.Delete(`api/indexer/${id}`)
|
||||
},
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMutation } from "react-query";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
||||
import logo from "../../logo.png";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { AuthContext } from "../../utils/Context";
|
||||
import { useEffect } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { PasswordInput, TextInput } from "../../components/inputs/text";
|
||||
|
||||
export type LoginFormFields = {
|
||||
|
@ -15,7 +14,7 @@ export type LoginFormFields = {
|
|||
};
|
||||
|
||||
export const Login = () => {
|
||||
const { handleSubmit, register, formState: { errors } } = useForm<LoginFormFields>({
|
||||
const { handleSubmit, register, formState } = useForm<LoginFormFields>({
|
||||
defaultValues: { username: "", password: "" },
|
||||
mode: "onBlur"
|
||||
});
|
||||
|
@ -26,8 +25,9 @@ export const Login = () => {
|
|||
// Check if onboarding is available for this instance
|
||||
// and redirect if needed
|
||||
APIClient.auth.canOnboard()
|
||||
.then(() => navigate("/onboard"));
|
||||
}, [history]);
|
||||
.then(() => navigate("/onboard"))
|
||||
.catch(() => { /*don't log to console PAHLLEEEASSSE*/ });
|
||||
}, []);
|
||||
|
||||
const loginMutation = useMutation(
|
||||
(data: LoginFormFields) => APIClient.auth.login(data.username, data.password),
|
||||
|
@ -42,7 +42,7 @@ export const Login = () => {
|
|||
}
|
||||
);
|
||||
|
||||
const onSubmit: SubmitHandler<LoginFormFields> = (data: LoginFormFields) => loginMutation.mutate(data);
|
||||
const onSubmit = (data: LoginFormFields) => loginMutation.mutate(data);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
|
@ -55,7 +55,6 @@ export const Login = () => {
|
|||
</div>
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md shadow-lg">
|
||||
<div className="bg-white dark:bg-gray-800 py-8 px-4 sm:rounded-lg sm:px-10">
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-6">
|
||||
<TextInput<LoginFormFields>
|
||||
|
@ -65,7 +64,7 @@ export const Login = () => {
|
|||
type="text"
|
||||
register={register}
|
||||
rules={{ required: "Username is required" }}
|
||||
errors={errors}
|
||||
errors={formState.errors}
|
||||
autoComplete="username"
|
||||
/>
|
||||
<PasswordInput<LoginFormFields>
|
||||
|
@ -74,7 +73,7 @@ export const Login = () => {
|
|||
label="password"
|
||||
register={register}
|
||||
rules={{ required: "Password is required" }}
|
||||
errors={errors}
|
||||
errors={formState.errors}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
@ -88,7 +87,6 @@ export const Login = () => {
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,32 +1,27 @@
|
|||
import { useEffect } from "react";
|
||||
import { useCookies } from "react-cookie";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import { AuthContext } from "../../utils/Context";
|
||||
|
||||
export const Logout = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [, setAuthContext] = AuthContext.use();
|
||||
const [,, removeCookie] = useCookies(["user_session"]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
APIClient.auth.logout()
|
||||
.then(() => {
|
||||
removeCookie("user_session");
|
||||
setAuthContext({ username: "", isLoggedIn: false });
|
||||
|
||||
navigate("/login");
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body="You have been logged out. Goodbye!" t={t} />
|
||||
));
|
||||
AuthContext.reset();
|
||||
});
|
||||
},
|
||||
[history, removeCookie, setAuthContext]
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-800 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<p>Logged out</p>
|
||||
<div className="min-h-screen flex justify-center items-center">
|
||||
{/*<h1 className="font-bold text-7xl">Goodbye!</h1>*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -34,7 +34,7 @@ export const Onboarding = () => {
|
|||
|
||||
const mutation = useMutation(
|
||||
(data: InputValues) => APIClient.auth.onboard(data.username, data.password1),
|
||||
{ onSuccess: () => navigate("/login") }
|
||||
{ onSuccess: () => navigate("/") }
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -157,7 +157,7 @@ export const SearchColumnFilter = ({
|
|||
id="filter"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default dark:text-gray-400 sm:text-sm border-none"
|
||||
className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md dark:text-gray-400 sm:text-sm border-none"
|
||||
placeholder="Search releases..."
|
||||
/>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue