feat: add oauth login
This commit is contained in:
parent
09ef8c9c98
commit
b7289dd09c
@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
/dist/
|
/dist/
|
||||||
pnpm-lock.yaml
|
/pnpm-lock.yaml
|
||||||
|
/postcss.config.cjs
|
||||||
|
@ -37,7 +37,8 @@
|
|||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-emoji": "^4.0.1",
|
"remark-emoji": "^4.0.1",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0"
|
"remark-math": "^6.0.0",
|
||||||
|
"universal-cookie": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
|
@ -74,6 +74,9 @@ dependencies:
|
|||||||
remark-math:
|
remark-math:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
|
universal-cookie:
|
||||||
|
specifier: ^7.1.0
|
||||||
|
version: 7.1.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
@ -2400,6 +2403,10 @@ packages:
|
|||||||
'@babel/types': 7.24.0
|
'@babel/types': 7.24.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/cookie@0.6.0:
|
||||||
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/debug@4.1.12:
|
/@types/debug@4.1.12:
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3098,6 +3105,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cookie@0.6.0:
|
||||||
|
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/copy-to-clipboard@3.3.3:
|
/copy-to-clipboard@3.3.3:
|
||||||
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6515,6 +6527,13 @@ packages:
|
|||||||
unist-util-visit-parents: 6.0.1
|
unist-util-visit-parents: 6.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/universal-cookie@7.1.0:
|
||||||
|
resolution: {integrity: sha512-LCLHwP0whxTqkBYMptW1dzNS0xxIVJmU6c51N5CfPNheVxuJW7fVxPa6MUGX7boUSyOlpMveBO96hMs5Gee6Fg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/cookie': 0.6.0
|
||||||
|
cookie: 0.6.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/universalify@0.2.0:
|
/universalify@0.2.0:
|
||||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
|
24
src/app/services/oauth.ts
Normal file
24
src/app/services/oauth.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { Wrap } from "./base";
|
||||||
|
import { api } from "./api";
|
||||||
|
|
||||||
|
export interface OAuthLoginResponse {
|
||||||
|
url: string;
|
||||||
|
cookie: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
live: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const oauthApi = api.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
OAuthUrl: builder.query<Wrap<OAuthLoginResponse>, void>({
|
||||||
|
query: () => ({
|
||||||
|
url: "/oauth/login",
|
||||||
|
method: "POST",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useOAuthUrlQuery } = oauthApi;
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import { userApi } from "../../app/services/user";
|
import { userApi } from "../../app/services/user";
|
||||||
import type { RootState } from "../../app/store";
|
import type { RootState } from "../../app/store";
|
||||||
@ -17,12 +18,16 @@ export const authSlice = createSlice({
|
|||||||
load: (state) => {
|
load: (state) => {
|
||||||
state.token = localStorage.getItem("token");
|
state.token = localStorage.getItem("token");
|
||||||
},
|
},
|
||||||
|
setToken: (state, action: PayloadAction<string>) => {
|
||||||
|
const token = action.payload;
|
||||||
|
localStorage.setItem("token", token);
|
||||||
|
state.token = token;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
.addMatcher(userApi.endpoints.login.matchFulfilled, (_state, action) => {
|
.addMatcher(userApi.endpoints.login.matchFulfilled, (_state, action) => {
|
||||||
// Login Success
|
// Login Success
|
||||||
console.log(action);
|
|
||||||
const { token } = action.payload.body;
|
const { token } = action.payload.body;
|
||||||
localStorage.setItem("token", token);
|
localStorage.setItem("token", token);
|
||||||
return { ...initialState, token: token };
|
return { ...initialState, token: token };
|
||||||
@ -41,6 +46,6 @@ export const authSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { load } = authSlice.actions;
|
export const { load, setToken } = authSlice.actions;
|
||||||
|
|
||||||
export const selectCurrentUser = (state: RootState) => state.auth;
|
export const selectCurrentUser = (state: RootState) => state.auth;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@ -17,29 +17,53 @@ import {
|
|||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Link as ReactRouterLink, useNavigate } from "react-router-dom";
|
import { Link as ReactRouterLink, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { RiLoginCircleLine } from "react-icons/ri";
|
import Cookies from "universal-cookie";
|
||||||
|
|
||||||
import KeyCloakSVG from "../resources/keycloak.svg";
|
import { useAppDispatch } from "../hooks/store";
|
||||||
import { PasswordField } from "../components/PasswordField";
|
import { PasswordField } from "../components/PasswordField";
|
||||||
|
import { api } from "../app/services/api";
|
||||||
import type { UserRequest } from "../app/services/user";
|
import type { UserRequest } from "../app/services/user";
|
||||||
import { useLoginMutation } from "../app/services/user";
|
import { useLoginMutation } from "../app/services/user";
|
||||||
|
import { useOAuthUrlQuery } from "../app/services/oauth";
|
||||||
import type { Wrap } from "../app/services/base";
|
import type { Wrap } from "../app/services/base";
|
||||||
|
|
||||||
export default function LoginPage() {
|
import { RiLoginCircleLine } from "react-icons/ri";
|
||||||
const toast = useToast();
|
import KeyCloakSVG from "../resources/keycloak.svg";
|
||||||
const navigate = useNavigate();
|
import { setToken } from "../features/auth/authSlice";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
const bg = useColorModeValue("gray.50", "gray.900");
|
const bg = useColorModeValue("gray.50", "gray.900");
|
||||||
const fg = "teal";
|
const fg = "teal";
|
||||||
const fg2 = "gray";
|
const fg2 = "gray";
|
||||||
|
|
||||||
const [login, { isLoading }] = useLoginMutation();
|
const dispatch = useAppDispatch();
|
||||||
|
const toast = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const cookies = new Cookies(null);
|
||||||
|
|
||||||
|
const [searchParams, _] = useSearchParams();
|
||||||
|
const redirectToken = searchParams.get("redirect_token");
|
||||||
|
|
||||||
|
const [login, { isLoading: loginIsLoading }] = useLoginMutation();
|
||||||
|
const { data: oauthInfo, isSuccess: oauthIsSuccess } = useOAuthUrlQuery(undefined, {
|
||||||
|
// refresh every minute
|
||||||
|
pollingInterval: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
const [formState, setFormState] = useState<UserRequest>({
|
const [formState, setFormState] = useState<UserRequest>({
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// callback from sso login
|
||||||
|
if (!redirectToken) return;
|
||||||
|
dispatch(setToken(redirectToken));
|
||||||
|
dispatch(api.util.invalidateTags(["User", "Status", "Submission"]));
|
||||||
|
navigate("/");
|
||||||
|
}, [dispatch, navigate, redirectToken]);
|
||||||
|
|
||||||
const handleChange = ({ target: { name, value } }: React.ChangeEvent<HTMLInputElement>) =>
|
const handleChange = ({ target: { name, value } }: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
setFormState((prev) => ({ ...prev, [name]: value }));
|
setFormState((prev) => ({ ...prev, [name]: value }));
|
||||||
|
|
||||||
@ -75,12 +99,22 @@ export default function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const doSSOLogin = () => {
|
const doSSOLogin = () => {
|
||||||
|
if (!oauthIsSuccess || !oauthInfo) {
|
||||||
toast({
|
toast({
|
||||||
status: "error",
|
status: "loading",
|
||||||
title: "Error",
|
title: "Loading",
|
||||||
description: "Not Implemented",
|
description: "Please wait for a while and try again ...",
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies.set(oauthInfo.body.cookie.name, oauthInfo.body.cookie.value, {
|
||||||
|
path: "/",
|
||||||
|
maxAge: oauthInfo.body.cookie.live,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(oauthInfo.body.url, "_self");
|
||||||
};
|
};
|
||||||
|
|
||||||
const formArea = (
|
const formArea = (
|
||||||
@ -93,11 +127,11 @@ export default function LoginPage() {
|
|||||||
<Stack spacing="5">
|
<Stack spacing="5">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel htmlFor="email">Email</FormLabel>
|
<FormLabel htmlFor="email">Email</FormLabel>
|
||||||
<Input id="email" name="email" type="email" isDisabled={isLoading} onChange={handleChange} />
|
<Input id="email" name="email" type="email" isDisabled={loginIsLoading} onChange={handleChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<PasswordField isDisabled={isLoading} onChange={handleChange} />
|
<PasswordField isDisabled={loginIsLoading} onChange={handleChange} />
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
|
{/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
|
||||||
<Button type="submit" colorScheme={fg} isLoading={isLoading} onClick={doLogin}>
|
<Button type="submit" colorScheme={fg} isLoading={loginIsLoading} onClick={doLogin}>
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
Loading…
Reference in New Issue
Block a user