import { Center, Flex, Heading, Text, Input, Button, useColorModeValue, HStack, VStack, Checkbox, Icon, Divider, Image, IconButton, Box, } from "@hope-ui/solid" import { FiUser, FiLock, FiEye, FiEyeOff } from "solid-icons/fi" import { createMemo, createSignal, Show, onMount, onCleanup } from "solid-js" import { SwitchColorMode, SwitchLanguageWhite } from "~/components" import { useFetch, useT, useTitle, useRouter } from "~/hooks" import { changeToken, r, notify, handleRespWithoutNotify, base_path, handleResp, hashPwd, joinBase, } from "~/utils" import { PResp, Resp } from "~/types" import LoginBg from "./LoginBg" import { createStorageSignal } from "@solid-primitives/storage" import { getSetting, getSettingBool, setSettings } from "~/store" import { SSOLogin } from "./SSOLogin" import { IoFingerPrint } from "solid-icons/io" import { register } from "~/utils/api" import { parseRequestOptionsFromJSON, get, AuthenticationPublicKeyCredential, supported, CredentialRequestOptionsJSON, } from "@github/webauthn-json/browser-ponyfill" const Login = () => { const t = useT() const usertitle = createMemo(() => { return `${t("login.login_to")} ${getSetting("site_title")}` }) useTitle(usertitle) const bgColor = useColorModeValue("white", "$neutral1") const [username, setUsername] = createSignal( localStorage.getItem("username") || "", ) const [password, setPassword] = createSignal( localStorage.getItem("password") || "", ) const [showPassword, setShowPassword] = createSignal(false) const [opt, setOpt] = createSignal("") const [useauthn, setuseauthn] = createSignal(false) const [remember, setRemember] = createStorageSignal("remember-pwd", "false") const [useLdap, setUseLdap] = createSignal(false) const [isRegisterMode, setIsRegisterMode] = createSignal(false) // 切换注册模式时清空输入框 const toggleRegisterMode = () => { if (!isRegisterMode()) { // 切换到注册模式时清空输入框 setUsername("") setPassword("") } else { // 切换回登录模式时恢复记住的账号密码 const savedUsername = localStorage.getItem("username") || "" const savedPassword = localStorage.getItem("password") || "" setUsername(savedUsername) setPassword(savedPassword) } setIsRegisterMode(!isRegisterMode()) } // 获取最新的设置数据 const [settingsLoading, getSettings] = useFetch( (): Promise>> => r.get("/public/settings"), ) // 刷新设置数据 const refreshSettings = async () => { const resp = await getSettings() handleResp(resp, (data) => { setSettings(data) }) } // 使用 public/settings 接口中的 use_newui 字段 const useNewVersion = createMemo(() => getSetting("use_newui") === "true") const allowRegister = createMemo( () => getSetting("allow_register") === "true", ) // 页面加载时刷新设置 onMount(() => { refreshSettings() }) const [loading, data] = useFetch( async (): Promise> => { if (useLdap()) { return r.post("/auth/login/ldap", { username: username(), password: password(), otp_code: opt(), }) } else { return r.post("/auth/login/hash", { username: username(), password: hashPwd(password()), otp_code: opt(), }) } }, ) // 注册接口 const [registerLoading, registerData] = useFetch( async (): Promise> => { return register({ username: username(), password: password(), }) }, ) const [, postauthnlogin] = useFetch( ( session: string, credentials: AuthenticationPublicKeyCredential, username: string, signal: AbortSignal | undefined, ): Promise> => r.post( "/authn/webauthn_finish_login?username=" + username, JSON.stringify(credentials), { headers: { session: session, }, signal, }, ), ) interface Webauthntemp { session: string options: CredentialRequestOptionsJSON } const [, getauthntemp] = useFetch( (username, signal: AbortSignal | undefined): PResp => r.get("/authn/webauthn_begin_login?username=" + username, { signal, }), ) const { searchParams, to } = useRouter() const isAuthnConditionalAvailable = async (): Promise => { if ( PublicKeyCredential && "isConditionalMediationAvailable" in PublicKeyCredential ) { return await PublicKeyCredential.isConditionalMediationAvailable() } else { return false } } const AuthnSignEnabled = getSettingBool("webauthn_login_enabled") const AuthnSwitch = async () => { setuseauthn(!useauthn()) } let AuthnSignal: AbortController | null = null const AuthnLogin = async (conditional?: boolean) => { if (!supported()) { if (!conditional) { notify.error(t("users.webauthn_not_supported")) } return } if (conditional && !(await isAuthnConditionalAvailable())) { return } AuthnSignal?.abort() const controller = new AbortController() AuthnSignal = controller const username_login: string = conditional ? "" : username() if (!conditional && remember() === "true") { localStorage.setItem("username", username()) } else { localStorage.removeItem("username") } const resp = await getauthntemp(username_login, controller.signal) handleResp(resp, async (data) => { try { const options = parseRequestOptionsFromJSON(data.options) options.signal = controller.signal if (conditional) { options.mediation = "conditional" } const credentials = await get(options) const resp = await postauthnlogin( data.session, credentials, username_login, controller.signal, ) handleRespWithoutNotify(resp, (data) => { notify.success(t("login.success")) changeToken(data.token) // 保存 device_key 到 localStorage if (data.device_key) { localStorage.setItem("device_key", data.device_key) console.log("=== Login Debug (Hash) ===") console.log("Saved device_key:", data.device_key) console.log("Full response data:", data) console.log("========================") } else { console.log("=== Login Debug (Hash) ===") console.log("No device_key in response") console.log("Full response data:", data) console.log("========================") } to( decodeURIComponent(searchParams.redirect || base_path || "/"), true, ) }) } catch (error: unknown) { if (error instanceof Error && error.name != "AbortError") notify.error(error.message) } }) } const AuthnCleanUpHandler = () => AuthnSignal?.abort() onMount(() => { if (AuthnSignEnabled) { window.addEventListener("beforeunload", AuthnCleanUpHandler) AuthnLogin(true) } }) onCleanup(() => { AuthnSignal?.abort() window.removeEventListener("beforeunload", AuthnCleanUpHandler) }) const Login = async () => { if (isRegisterMode()) { // 注册模式 const resp = await registerData() handleRespWithoutNotify( resp, (data) => { notify.success(t("login.register_success")) // 注册成功后切换到登录模式 setIsRegisterMode(false) // 清空密码,保留用户名 setPassword("") }, (msg, code) => { if (code === 403) { notify.error(t("login.register_disabled")) } else { notify.error(msg) } }, ) } else { // 登录模式 if (!useauthn()) { if (remember() === "true") { localStorage.setItem("username", username()) localStorage.setItem("password", password()) } else { localStorage.removeItem("username") localStorage.removeItem("password") } const resp = await data() handleRespWithoutNotify( resp, (data) => { notify.success(t("login.success")) changeToken(data.token) // 保存 device_key 到 localStorage if (data.device_key) { localStorage.setItem("device_key", data.device_key) console.log("=== Login Debug ===") console.log("Saved device_key:", data.device_key) console.log("Full response data:", data) console.log("==================") } else { console.log("=== Login Debug ===") console.log("No device_key in response") console.log("Full response data:", data) console.log("==================") } to( decodeURIComponent(searchParams.redirect || base_path || "/"), true, ) }, (msg, code) => { if (!needOpt() && code === 402) { setNeedOpt(true) } else { notify.error(msg) } }, ) } else { await AuthnLogin() } } } const [needOpt, setNeedOpt] = createSignal(false) const ldapLoginEnabled = getSettingBool("ldap_login_enabled") const ldapLoginTips = getSetting("ldap_login_tips") if (ldapLoginEnabled) { setUseLdap(true) } const title = () => t("login.password_login") const logo = () => getSetting("logo").split("\n")[0] return (
AList Logo {isRegisterMode() ? t("login.register") : title()} setOpt(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { Login() } }} /> } > setUsername(e.currentTarget.value)} /> setPassword(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { Login() } }} /> setRemember(remember() === "true" ? "false" : "true") } > {t("login.remember")} {t("login.forget")} {t("login.go_login")} setUseLdap(!useLdap())} > {ldapLoginTips} {/* 注册切换 */} } > {/* 新版本的登录表单 */} {isRegisterMode() ? t("login.register") : t("login.password_login")} setOpt(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { Login() } }} /> } > setUsername(e.currentTarget.value)} border="none" backgroundColor="transparent" _focus={{ border: "none", boxShadow: "none", backgroundColor: "transparent", }} _hover={{ border: "none", boxShadow: "none", backgroundColor: "transparent", }} flex={1} /> setPassword(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { Login() } }} border="none" backgroundColor="transparent" _focus={{ border: "none", boxShadow: "none", backgroundColor: "transparent", }} _hover={{ border: "none", boxShadow: "none", backgroundColor: "transparent", }} flex={1} /> : } onClick={() => setShowPassword(!showPassword())} color="$neutral8" aria-label={showPassword() ? "隐藏密码" : "显示密码"} _hover={{ backgroundColor: "$neutral3", }} /> {/* 新版本忘记密码 */} {t("login.forget")} {isRegisterMode() ? t("login.go_login") : t("login.register")} { changeToken() to( decodeURIComponent( searchParams.redirect || base_path || "/", ), true, ) }} color="#3573FF" fontSize="14px" cursor="pointer" _hover={{ textDecoration: "underline", color: "#2B5CD9", }} > {t("login.use_guest")}
) } export default Login