feat: oauth2 登录

This commit is contained in:
naiba
2024-12-28 23:50:24 +08:00
parent fbf931293e
commit 97a5deb648
12 changed files with 926 additions and 753 deletions
+29 -1
View File
@@ -1,3 +1,4 @@
import { getOauth2RedirectURL, Oauth2RequestType } from "@/api/oauth2"
import { Button } from "@/components/ui/button"
import {
Form,
@@ -9,10 +10,13 @@ import {
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { useAuth } from "@/hooks/useAuth"
import useSetting from "@/hooks/useSetting"
import { zodResolver } from "@hookform/resolvers/zod"
import i18next from "i18next"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { z } from "zod"
const formSchema = z.object({
@@ -25,7 +29,17 @@ const formSchema = z.object({
})
function Login() {
const { login } = useAuth()
const { login, loginOauth2 } = useAuth()
const { data: settingData } = useSetting()
useEffect(() => {
const oauth2Code = new URLSearchParams(window.location.search).get("code")
const oauth2State = new URLSearchParams(window.location.search).get("state")
const oauth2Provider = new URLSearchParams(window.location.search).get("provider")
if (oauth2Code && oauth2State && oauth2Provider) {
loginOauth2(oauth2Provider, oauth2State, oauth2Code)
}
}, [window.location.search])
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -39,6 +53,15 @@ function Login() {
login(values.username, values.password)
}
async function loginWith(provider: string) {
try {
const redirectUrl = await getOauth2RedirectURL(provider, Oauth2RequestType.LOGIN)
window.location.href = redirectUrl.redirect!
} catch (error: any) {
toast.error(error.message)
}
}
const { t } = useTranslation()
return (
@@ -79,6 +102,11 @@ function Login() {
<Button type="submit">{t("Login")}</Button>
</form>
</Form>
<div className="mt-4">
{settingData?.config?.oauth2_providers?.map((p: string) =>
<Button onClick={() => loginWith(p)}>{p}</Button>
)}
</div>
</div>
)
}
+63 -1
View File
@@ -1,16 +1,62 @@
import { bindOauth2, getOauth2RedirectURL, Oauth2RequestType, unbindOauth2 } from "@/api/oauth2"
import { getProfile } from "@/api/user"
import { ProfileCard } from "@/components/profile"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useMainStore } from "@/hooks/useMainStore"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { useServer } from "@/hooks/useServer"
import useSetting from "@/hooks/useSetting"
import { Boxes, Server } from "lucide-react"
import { useEffect } from "react"
import { toast } from "sonner"
export default function ProfilePage() {
const { profile } = useMainStore()
const { profile, setProfile } = useMainStore()
const { servers, serverGroups } = useServer()
const { data: settingData } = useSetting()
const isDesktop = useMediaQuery("(min-width: 890px)")
useEffect(() => {
const oauth2Code = new URLSearchParams(window.location.search).get("code")
const oauth2State = new URLSearchParams(window.location.search).get("state")
const oauth2Provider = new URLSearchParams(window.location.search).get("provider")
if (oauth2Code && oauth2State && oauth2Provider) {
bindOauth2(oauth2Provider, oauth2State, oauth2Code)
.catch((error) => {
toast.error(error.message)
})
.then(() => {
getProfile().then((profile) => {
setProfile(profile)
})
}).finally(() => {
window.history.replaceState({}, document.title, window.location.pathname)
})
}
}, [window.location.search])
const bindO2 = async (provider: string) => {
try {
const redirectUrl = await getOauth2RedirectURL(provider, Oauth2RequestType.BIND)
window.location.href = redirectUrl.redirect!
} catch (error: any) {
toast.error(error.message)
}
}
const unbindO2 = async (provider: string) => {
try {
await unbindOauth2(provider)
getProfile().then((profile) => {
setProfile(profile)
})
} catch (error: any) {
toast.error(error.message)
}
}
return (
profile && (
<div className={`flex p-8 gap-4 ${isDesktop ? "ml-6" : "flex-col"}`}>
@@ -62,6 +108,22 @@ export default function ProfilePage() {
{serverGroups?.length || 0}
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle className="flex gap-2 text-xl items-center">
<Boxes /> Oauth2 bindings
</CardTitle>
</CardHeader>
<CardContent className="text-lg font-semibold">
{settingData?.config?.oauth2_providers?.map((provider) => <div>
{provider}: {profile.oauth2_bind?.[provider.toLowerCase()]} {profile.oauth2_bind?.[provider.toLowerCase()] ?
<Button size="sm" onClick={() => unbindO2(provider)}>Unbind</Button>
:
<Button size="sm" onClick={() => bindO2(provider)}>Bind</Button>}
</div>)}
</CardContent>
</Card>
</div>
</div>
</div>
+7 -7
View File
@@ -13,14 +13,14 @@ export default function Root() {
const { data: settingData, error } = useSetting()
useEffect(() => {
document.title = settingData?.site_name || "哪吒监控 Nezha Monitoring"
}, [settingData?.site_name])
document.title = settingData?.config?.site_name || "哪吒监控 Nezha Monitoring"
}, [settingData?.config?.site_name])
useEffect(() => {
if (settingData?.custom_code_dashboard) {
InjectContext(settingData?.custom_code_dashboard)
if (settingData?.config?.custom_code_dashboard) {
InjectContext(settingData?.config?.custom_code_dashboard)
}
}, [settingData?.custom_code_dashboard])
}, [settingData?.config?.custom_code_dashboard])
if (error) {
throw error
@@ -30,8 +30,8 @@ export default function Root() {
return null
}
if (settingData?.language && !localStorage.getItem("language")) {
i18n.changeLanguage(settingData?.language)
if (settingData?.config?.language && !localStorage.getItem("language")) {
i18n.changeLanguage(settingData?.config?.language)
}
return (
+27 -27
View File
@@ -67,31 +67,31 @@ export default function SettingsPage() {
resolver: zodResolver(settingFormSchema),
defaultValues: config
? {
...config,
language: config.language,
site_name: config.site_name || "",
user_template:
config.user_template ||
Object.keys(config.frontend_templates?.filter((t) => !t.is_admin) || {})[0] ||
"user-dist",
}
...config,
language: config?.config?.language,
site_name: config.config?.site_name || "",
user_template:
config.config?.user_template ||
Object.keys(config.frontend_templates?.filter((t) => !t.is_admin) || {})[0] ||
"user-dist",
}
: {
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
},
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
},
resetOptions: {
keepDefaultValues: false,
},
})
useEffect(() => {
if (config) {
form.reset(config)
if (config?.config) {
form.reset(config?.config)
}
}, [config, form])
}, [config?.config, form])
const onSubmit = async (values: z.infer<typeof settingFormSchema>) => {
try {
@@ -172,7 +172,7 @@ export default function SettingsPage() {
(t) => t.path === value,
)
if (template) {
form.setValue("user_template", template.path)
form.setValue("user_template", template!.path!)
}
}}
>
@@ -188,7 +188,7 @@ export default function SettingsPage() {
) || []
).map((template) => (
<div key={template.path}>
<SelectItem value={template.path}>
<SelectItem value={template.path!}>
<div className="flex flex-col items-start gap-1">
<div className="font-medium">
{template.name}
@@ -229,15 +229,15 @@ export default function SettingsPage() {
{!config?.frontend_templates?.find(
(t) => t.path === field.value,
)?.is_official && (
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
</div>
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
</div>
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
</div>
)}
)}
</FormItem>
)}
/>