feat: sync with latest api

This commit is contained in:
Paul Pan 2024-04-29 00:16:03 +08:00
parent 7785d7a875
commit 6eb3ccfdd0
6 changed files with 50 additions and 181 deletions

View File

@ -7,18 +7,6 @@ export enum UserRole {
RoleGuest = 10, RoleGuest = 10,
} }
export interface UserRequest {
email?: string;
password?: string;
nickname?: string;
uid?: number;
}
export interface UserLoginResponse {
nickname: string;
token: string;
}
export interface UserProfile { export interface UserProfile {
meta: Meta; meta: Meta;
email: string; email: string;
@ -29,22 +17,6 @@ export interface UserProfile {
export const userApi = api.injectEndpoints({ export const userApi = api.injectEndpoints({
endpoints: (builder) => ({ endpoints: (builder) => ({
register: builder.mutation<Wrap<string>, UserRequest>({
query: (data: UserRequest) => ({
url: "/user/create",
method: "POST",
body: data,
}),
invalidatesTags: ["User", "Status", "Submission"],
}),
login: builder.mutation<Wrap<UserLoginResponse>, UserRequest>({
query: (data: UserRequest) => ({
url: "/user/login",
method: "POST",
body: data,
}),
invalidatesTags: ["User", "Status", "Submission"],
}),
logout: builder.mutation<Wrap<void>, void>({ logout: builder.mutation<Wrap<void>, void>({
query: () => ({ query: () => ({
url: "/user/logout", url: "/user/logout",
@ -63,4 +35,4 @@ export const userApi = api.injectEndpoints({
}), }),
}); });
export const { useRegisterMutation, useLoginMutation, useLogoutMutation, useProfileQuery } = userApi; export const { useLogoutMutation, useProfileQuery } = userApi;

View File

@ -26,23 +26,11 @@ export const authSlice = createAppSlice({
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
.addMatcher(userApi.endpoints.login.matchFulfilled, (_state, action) => {
// Login Success
const { token } = action.payload.body;
localStorage.setItem("token", token);
return { ...initialState, token: token };
})
.addMatcher(userApi.endpoints.logout.matchFulfilled, (_state, _action) => { .addMatcher(userApi.endpoints.logout.matchFulfilled, (_state, _action) => {
// Logout Success // Logout Success
localStorage.removeItem("token"); localStorage.removeItem("token");
return { ...initialState, token: null }; return { ...initialState, token: null };
}) })
.addMatcher(userApi.endpoints.login.matchRejected, (_state, action) => {
// Login Failed
console.error("Login Failed", action.payload);
localStorage.removeItem("token");
return { ...initialState, token: null };
})
.addMatcher(userApi.endpoints.profile.matchRejected, (_state, action) => { .addMatcher(userApi.endpoints.profile.matchRejected, (_state, action) => {
// Profile Failed // Profile Failed
if (action.meta.arg.originalArgs != 0) return; if (action.meta.arg.originalArgs != 0) return;

View File

@ -1,9 +1,11 @@
import { isRouteErrorResponse, useNavigate, useRouteError } from "react-router-dom"; import { isRouteErrorResponse, useNavigate, useRouteError, useSearchParams } from "react-router-dom";
import { Box, Button, ButtonGroup, Flex, Heading, Text } from "@chakra-ui/react"; import { Box, Button, ButtonGroup, Flex, Heading, Text } from "@chakra-ui/react";
import { CloseIcon } from "@chakra-ui/icons"; import { CloseIcon } from "@chakra-ui/icons";
export const ErrorPage = () => { export const ErrorPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, _] = useSearchParams();
const serverError = searchParams.get("message");
const error = useRouteError(); const error = useRouteError();
const convertError = (error: unknown): string => { const convertError = (error: unknown): string => {
@ -13,6 +15,8 @@ export const ErrorPage = () => {
return error.message; return error.message;
} else if (typeof error === "string") { } else if (typeof error === "string") {
return error; return error;
} else if (serverError) {
return serverError;
} else { } else {
console.error(error); console.error(error);
return "Unknown error"; return "Unknown error";

View File

@ -1,32 +1,11 @@
import type React from "react"; import { useEffect } from "react";
import { useEffect, useState } from "react"; import { Box, Button, Center, Container, Heading, Stack, Text, useColorModeValue, useToast } from "@chakra-ui/react";
import { import { useNavigate, useSearchParams } from "react-router-dom";
Box,
Button,
Center,
Container,
Divider,
FormControl,
FormLabel,
Heading,
HStack,
Input,
Link,
Stack,
Text,
useColorModeValue,
useToast,
} from "@chakra-ui/react";
import { Link as ReactRouterLink, useNavigate, useSearchParams } from "react-router-dom";
import Cookies from "universal-cookie"; import Cookies from "universal-cookie";
import { useAppDispatch } from "../hooks/store"; import { useAppDispatch } from "../hooks/store";
import { PasswordField } from "../components/PasswordField";
import { api } from "../app/services/api"; import { api } from "../app/services/api";
import type { UserRequest } from "../app/services/user";
import { useLoginMutation } from "../app/services/user";
import { useOAuthUrlQuery } from "../app/services/oauth"; import { useOAuthUrlQuery } from "../app/services/oauth";
import type { Wrap } from "../app/services/base";
import { RiLoginCircleLine } from "react-icons/ri"; import { RiLoginCircleLine } from "react-icons/ri";
import KeyCloakSVG from "../resources/keycloak.svg"; import KeyCloakSVG from "../resources/keycloak.svg";
@ -34,8 +13,7 @@ import { setToken } from "../features/auth/authSlice";
export default function LoginPage() { export default function LoginPage() {
const bg = useColorModeValue("gray.50", "gray.900"); const bg = useColorModeValue("gray.50", "gray.900");
const fg = "teal"; const fg = "gray";
const fg2 = "gray";
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const toast = useToast(); const toast = useToast();
@ -45,15 +23,9 @@ export default function LoginPage() {
const [searchParams, _] = useSearchParams(); const [searchParams, _] = useSearchParams();
const redirectToken = searchParams.get("redirect_token"); const redirectToken = searchParams.get("redirect_token");
const [login, { isLoading: loginIsLoading }] = useLoginMutation();
const { data: oauthInfo, isSuccess: oauthIsSuccess } = useOAuthUrlQuery(undefined, { const { data: oauthInfo, isSuccess: oauthIsSuccess } = useOAuthUrlQuery(undefined, {
// refresh every minute // refresh every 10 minute
pollingInterval: 60000, pollingInterval: 600000,
});
const [formState, setFormState] = useState<UserRequest>({
email: "",
password: "",
}); });
useEffect(() => { useEffect(() => {
@ -64,40 +36,6 @@ export default function LoginPage() {
navigate("/"); navigate("/");
}, [dispatch, navigate, redirectToken]); }, [dispatch, navigate, redirectToken]);
const handleChange = ({ target: { name, value } }: React.ChangeEvent<HTMLInputElement>) =>
setFormState((prev) => ({ ...prev, [name]: value }));
const validate = () => {
const { email, password } = formState;
return email && password && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
const doLogin = async () => {
if (!validate()) {
toast({
status: "error",
title: "Invalid Input",
description: "Please fill in all fields and make sure your email is valid.",
isClosable: true,
});
return;
}
try {
// authSlice will automatically handle the response
await login(formState).unwrap();
navigate("/");
} catch (err) {
const errTyped = err as { status: number | string; data: Wrap<void> };
toast({
status: "error",
title: "Error",
description: errTyped.status === 200 ? errTyped.data.msg : "Oh no, there was an error!",
isClosable: true,
});
}
};
const doSSOLogin = () => { const doSSOLogin = () => {
if (!oauthIsSuccess || !oauthInfo) { if (!oauthIsSuccess || !oauthInfo) {
toast({ toast({
@ -117,37 +55,9 @@ export default function LoginPage() {
window.open(oauthInfo.body.url, "_self"); window.open(oauthInfo.body.url, "_self");
}; };
const formArea = (
<form
onSubmit={(e) => {
e.preventDefault();
void doLogin();
}}
>
<Stack spacing="5">
<FormControl>
<FormLabel htmlFor="email">Email</FormLabel>
<Input id="email" name="email" type="email" isDisabled={loginIsLoading} onChange={handleChange} />
</FormControl>
<PasswordField isDisabled={loginIsLoading} onChange={handleChange} />
{/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
<Button type="submit" colorScheme={fg} isLoading={loginIsLoading} onClick={doLogin}>
Sign in
</Button>
</Stack>
</form>
);
const ssoArea = ( const ssoArea = (
<Stack spacing="6"> <Stack spacing="6">
<HStack> <Button colorScheme={fg} onClick={doSSOLogin}>
<Divider />
<Text textStyle="sm" whiteSpace="nowrap" color={fg}>
or continue with
</Text>
<Divider />
</HStack>
<Button colorScheme={fg2} onClick={doSSOLogin}>
<img src={KeyCloakSVG} alt="KeyCloak" width="24" height="24" /> <img src={KeyCloakSVG} alt="KeyCloak" width="24" height="24" />
<Text mx={2}>Sign in with SSO</Text> <Text mx={2}>Sign in with SSO</Text>
</Button> </Button>
@ -159,15 +69,9 @@ export default function LoginPage() {
<Center> <Center>
<RiLoginCircleLine size={72} /> <RiLoginCircleLine size={72} />
</Center> </Center>
<Stack spacing={{ base: "2", md: "3" }} textAlign="center"> <Center>
<Heading>Log in to your account</Heading> <Heading>Log in to your account</Heading>
<Text> </Center>
Don&apos;t have an account?{" "}
<Link as={ReactRouterLink} to="/register" color={fg}>
Sign up
</Link>
</Text>
</Stack>
</Stack> </Stack>
); );
@ -182,10 +86,7 @@ export default function LoginPage() {
boxShadow={{ base: "none", sm: "xl" }} boxShadow={{ base: "none", sm: "xl" }}
borderRadius={{ base: "none", sm: "xl" }} borderRadius={{ base: "none", sm: "xl" }}
> >
<Stack spacing="6"> <Stack spacing="6">{ssoArea}</Stack>
{formArea}
{ssoArea}
</Stack>
</Box> </Box>
</Stack> </Stack>
</Container> </Container>

View File

@ -25,6 +25,10 @@ export const router: RouteObject[] = [
index: true, index: true,
element: <HomePage />, element: <HomePage />,
}, },
{
path: "error",
element: <ErrorPage />,
},
{ {
path: "home", path: "home",
element: <HomePage />, element: <HomePage />,