From c0acbd6ddc6e1975d5d77ede5d88ddc6df21aa05 Mon Sep 17 00:00:00 2001 From: Paul Pan Date: Fri, 15 Mar 2024 19:04:16 +0800 Subject: [PATCH] feat: add oauth login --- .prettierignore | 3 +- package.json | 3 +- pnpm-lock.yaml | 19 ++++++++++ src/app/services/oauth.ts | 24 +++++++++++++ src/features/auth/authSlice.ts | 13 ++++--- src/pages/LoginPage.tsx | 66 +++++++++++++++++++++++++--------- 6 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 src/app/services/oauth.ts diff --git a/.prettierignore b/.prettierignore index 294f9e8..f9e671f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ node_modules/ /dist/ -pnpm-lock.yaml +/pnpm-lock.yaml +/postcss.config.cjs diff --git a/package.json b/package.json index 778c3f0..b7b7244 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "rehype-raw": "^7.0.0", "remark-emoji": "^4.0.1", "remark-gfm": "^4.0.0", - "remark-math": "^6.0.0" + "remark-math": "^6.0.0", + "universal-cookie": "^7.1.0" }, "devDependencies": { "@types/react": "^18.2.66", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb15492..084193e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ dependencies: remark-math: specifier: ^6.0.0 version: 6.0.0 + universal-cookie: + specifier: ^7.1.0 + version: 7.1.0 devDependencies: '@types/react': @@ -2400,6 +2403,10 @@ packages: '@babel/types': 7.24.0 dev: true + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: false + /@types/debug@4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} dependencies: @@ -3098,6 +3105,11 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + /copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} dependencies: @@ -6515,6 +6527,13 @@ packages: unist-util-visit-parents: 6.0.1 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: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} diff --git a/src/app/services/oauth.ts b/src/app/services/oauth.ts new file mode 100644 index 0000000..0f05c8a --- /dev/null +++ b/src/app/services/oauth.ts @@ -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, void>({ + query: () => ({ + url: "/oauth/login", + method: "POST", + }), + }), + }), +}); + +export const { useOAuthUrlQuery } = oauthApi; diff --git a/src/features/auth/authSlice.ts b/src/features/auth/authSlice.ts index 546363f..f2344e8 100644 --- a/src/features/auth/authSlice.ts +++ b/src/features/auth/authSlice.ts @@ -1,6 +1,7 @@ -import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; import { userApi } from "../../app/services/user"; import type { RootState } from "../../app/store"; +import { createAppSlice } from "../../app/createAppSlice"; type AuthState = { token: string | null; @@ -10,19 +11,23 @@ const initialState: AuthState = { token: localStorage.getItem("token"), }; -export const authSlice = createSlice({ +export const authSlice = createAppSlice({ name: "auth", initialState, reducers: { load: (state) => { state.token = localStorage.getItem("token"); }, + setToken: (state, action: PayloadAction) => { + const token = action.payload; + localStorage.setItem("token", token); + state.token = token; + }, }, extraReducers: (builder) => { builder .addMatcher(userApi.endpoints.login.matchFulfilled, (_state, action) => { // Login Success - console.log(action); const { token } = action.payload.body; localStorage.setItem("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; diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index f544dd8..bec9da5 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,5 +1,5 @@ import type React from "react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Box, Button, @@ -17,29 +17,53 @@ import { useColorModeValue, useToast, } from "@chakra-ui/react"; -import { Link as ReactRouterLink, useNavigate } from "react-router-dom"; -import { RiLoginCircleLine } from "react-icons/ri"; +import { Link as ReactRouterLink, useNavigate, useSearchParams } from "react-router-dom"; +import Cookies from "universal-cookie"; -import KeyCloakSVG from "../resources/keycloak.svg"; +import { useAppDispatch } from "../hooks/store"; import { PasswordField } from "../components/PasswordField"; +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 type { Wrap } from "../app/services/base"; -export default function LoginPage() { - const toast = useToast(); - const navigate = useNavigate(); +import { RiLoginCircleLine } from "react-icons/ri"; +import KeyCloakSVG from "../resources/keycloak.svg"; +import { setToken } from "../features/auth/authSlice"; +export default function LoginPage() { const bg = useColorModeValue("gray.50", "gray.900"); const fg = "teal"; 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({ email: "", 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) => setFormState((prev) => ({ ...prev, [name]: value })); @@ -75,12 +99,22 @@ export default function LoginPage() { }; const doSSOLogin = () => { - toast({ - status: "error", - title: "Error", - description: "Not Implemented", - isClosable: true, + if (!oauthIsSuccess || !oauthInfo) { + toast({ + status: "loading", + title: "Loading", + description: "Please wait for a while and try again ...", + 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 = ( @@ -93,11 +127,11 @@ export default function LoginPage() { Email - + - + {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} -