feat: sync with latest api
This commit is contained in:
parent
7785d7a875
commit
6eb3ccfdd0
60
.github/workflows/container.yml
vendored
60
.github/workflows/container.yml
vendored
@ -1,32 +1,32 @@
|
|||||||
name: Build Container Image
|
name: Build Container Image
|
||||||
on: [ push ]
|
on: [push]
|
||||||
jobs:
|
jobs:
|
||||||
image:
|
image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
DOCKER: podman
|
DOCKER: podman
|
||||||
IMAGE_PREFIX: quay.io/ldcraft
|
IMAGE_PREFIX: quay.io/ldcraft
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# reference: https://github.com/containers/podman/discussions/17868
|
# reference: https://github.com/containers/podman/discussions/17868
|
||||||
- name: Tar as root
|
- name: Tar as root
|
||||||
run: |
|
run: |
|
||||||
sudo mv -fv /usr/bin/tar /usr/bin/tar.orig
|
sudo mv -fv /usr/bin/tar /usr/bin/tar.orig
|
||||||
echo -e '#!/bin/sh\n\nsudo /usr/bin/tar.orig "$@"' | sudo tee -a /usr/bin/tar
|
echo -e '#!/bin/sh\n\nsudo /usr/bin/tar.orig "$@"' | sudo tee -a /usr/bin/tar
|
||||||
sudo chmod +x /usr/bin/tar
|
sudo chmod +x /usr/bin/tar
|
||||||
- name: Cache Podman
|
- name: Cache Podman
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.local/share/containers
|
~/.local/share/containers
|
||||||
~/.config/containers
|
~/.config/containers
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/*.Dockerfile', 'build_image.sh') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/*.Dockerfile', 'build_image.sh') }}
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
uses: redhat-actions/podman-login@v1
|
uses: redhat-actions/podman-login@v1
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.CONTAINER_USERNAME }}
|
username: ${{ secrets.CONTAINER_USERNAME }}
|
||||||
password: ${{ secrets.CONTAINER_PASSWORD }}
|
password: ${{ secrets.CONTAINER_PASSWORD }}
|
||||||
- name: Build UI Image
|
- name: Build UI Image
|
||||||
run: ./build_image.sh rootfs
|
run: ./build_image.sh rootfs
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
|
@ -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'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>
|
||||||
|
@ -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 />,
|
||||||
|
Loading…
Reference in New Issue
Block a user