From f0abd792c7722a7f29ed151d4072e7673c8200bf Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 29 Aug 2024 00:06:15 +0800 Subject: [PATCH] chore: update auth service --- server/router/api/v1/auth_service.go | 23 +++- web/src/components/PasswordSignInForm.tsx | 125 ++++++++++++++++++++++ web/src/pages/AdminSignIn.tsx | 43 ++++++++ web/src/pages/SignIn.tsx | 116 +------------------- web/src/router/index.tsx | 5 + 5 files changed, 196 insertions(+), 116 deletions(-) create mode 100644 web/src/components/PasswordSignInForm.tsx create mode 100644 web/src/pages/AdminSignIn.tsx diff --git a/server/router/api/v1/auth_service.go b/server/router/api/v1/auth_service.go index 1887930e..aef7af83 100644 --- a/server/router/api/v1/auth_service.go +++ b/server/router/api/v1/auth_service.go @@ -24,6 +24,10 @@ import ( "github.com/usememos/memos/store" ) +const ( + unmatchedEmailAndPasswordError = "unmatched email and password" +) + func (s *APIV1Service) GetAuthStatus(ctx context.Context, _ *v1pb.GetAuthStatusRequest) (*v1pb.User, error) { user, err := s.GetCurrentUser(ctx) if err != nil { @@ -47,14 +51,23 @@ func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest) return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to find user by username %s", request.Username)) } if user == nil { - return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("user not found with username %s", request.Username)) - } else if user.RowStatus == store.Archived { - return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("user has been archived with username %s", request.Username)) + return nil, status.Errorf(codes.InvalidArgument, unmatchedEmailAndPasswordError) } - // Compare the stored hashed password, with the hashed version of the password that was received. if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(request.Password)); err != nil { - return nil, status.Errorf(codes.InvalidArgument, "unmatched email and password") + return nil, status.Errorf(codes.InvalidArgument, unmatchedEmailAndPasswordError) + } + + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get workspace general setting, err: %s", err)) + } + // Check if the password sign in is allowed. + if workspaceGeneralSetting.DisallowPasswordSignin && user.Role == store.RoleUser { + return nil, status.Errorf(codes.PermissionDenied, "password signin is not allowed") + } + if user.RowStatus == store.Archived { + return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("user has been archived with username %s", request.Username)) } expireTime := time.Now().Add(AccessTokenDuration) diff --git a/web/src/components/PasswordSignInForm.tsx b/web/src/components/PasswordSignInForm.tsx new file mode 100644 index 00000000..64584333 --- /dev/null +++ b/web/src/components/PasswordSignInForm.tsx @@ -0,0 +1,125 @@ +import { Button, Checkbox, Input } from "@mui/joy"; +import { ClientError } from "nice-grpc-web"; +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { authServiceClient } from "@/grpcweb"; +import useLoading from "@/hooks/useLoading"; +import useNavigateTo from "@/hooks/useNavigateTo"; +import { useCommonContext } from "@/layouts/CommonContextProvider"; +import { useUserStore } from "@/store/v1"; +import { useTranslate } from "@/utils/i18n"; + +const PasswordSignInForm = () => { + const t = useTranslate(); + const navigateTo = useNavigateTo(); + const commonContext = useCommonContext(); + const userStore = useUserStore(); + const actionBtnLoadingState = useLoading(false); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [remember, setRemember] = useState(true); + + useEffect(() => { + if (commonContext.profile.mode === "demo") { + setUsername("yourselfhosted"); + setPassword("yourselfhosted"); + } + }, [commonContext.profile.mode]); + + const handleUsernameInputChanged = (e: React.ChangeEvent) => { + const text = e.target.value as string; + setUsername(text); + }; + + const handlePasswordInputChanged = (e: React.ChangeEvent) => { + const text = e.target.value as string; + setPassword(text); + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleSignInButtonClick(); + }; + + const handleSignInButtonClick = async () => { + if (username === "" || password === "") { + return; + } + + if (actionBtnLoadingState.isLoading) { + return; + } + + try { + actionBtnLoadingState.setLoading(); + await authServiceClient.signIn({ username, password, neverExpire: remember }); + await userStore.fetchCurrentUser(); + navigateTo("/"); + } catch (error: any) { + console.error(error); + toast.error((error as ClientError).details || "Failed to sign in."); + } + actionBtnLoadingState.setFinish(); + }; + + return ( +
+
+
+ {t("common.username")} + +
+
+ {t("common.password")} + +
+
+
+ setRemember(e.target.checked)} + /> +
+
+ +
+
+ ); +}; + +export default PasswordSignInForm; diff --git a/web/src/pages/AdminSignIn.tsx b/web/src/pages/AdminSignIn.tsx new file mode 100644 index 00000000..fae4f69b --- /dev/null +++ b/web/src/pages/AdminSignIn.tsx @@ -0,0 +1,43 @@ +import AppearanceSelect from "@/components/AppearanceSelect"; +import LocaleSelect from "@/components/LocaleSelect"; +import PasswordSignInForm from "@/components/PasswordSignInForm"; +import { useCommonContext } from "@/layouts/CommonContextProvider"; +import { useWorkspaceSettingStore } from "@/store/v1"; +import { WorkspaceGeneralSetting } from "@/types/proto/api/v1/workspace_setting_service"; +import { WorkspaceSettingKey } from "@/types/proto/store/workspace_setting"; + +const AdminSignIn = () => { + const commonContext = useCommonContext(); + const workspaceSettingStore = useWorkspaceSettingStore(); + const workspaceGeneralSetting = + workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.GENERAL).generalSetting || WorkspaceGeneralSetting.fromPartial({}); + + const handleLocaleSelectChange = (locale: Locale) => { + commonContext.setLocale(locale); + }; + + const handleAppearanceSelectChange = (appearance: Appearance) => { + commonContext.setAppearance(appearance); + }; + + return ( +
+
+
+ +

+ {workspaceGeneralSetting.customProfile?.title || "Memos"} +

+
+

Sign in with admin accounts

+ +
+
+ + +
+
+ ); +}; + +export default AdminSignIn; diff --git a/web/src/pages/SignIn.tsx b/web/src/pages/SignIn.tsx index 3d44973f..567c7645 100644 --- a/web/src/pages/SignIn.tsx +++ b/web/src/pages/SignIn.tsx @@ -1,16 +1,14 @@ -import { Button, Checkbox, Divider, Input } from "@mui/joy"; -import { ClientError } from "nice-grpc-web"; +import { Button, Divider } from "@mui/joy"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { Link } from "react-router-dom"; import AppearanceSelect from "@/components/AppearanceSelect"; import LocaleSelect from "@/components/LocaleSelect"; -import { authServiceClient, identityProviderServiceClient } from "@/grpcweb"; +import PasswordSignInForm from "@/components/PasswordSignInForm"; +import { identityProviderServiceClient } from "@/grpcweb"; import { absolutifyLink } from "@/helpers/utils"; -import useLoading from "@/hooks/useLoading"; -import useNavigateTo from "@/hooks/useNavigateTo"; import { useCommonContext } from "@/layouts/CommonContextProvider"; -import { extractIdentityProviderIdFromName, useUserStore, useWorkspaceSettingStore } from "@/store/v1"; +import { extractIdentityProviderIdFromName, useWorkspaceSettingStore } from "@/store/v1"; import { IdentityProvider, IdentityProvider_Type } from "@/types/proto/api/v1/idp_service"; import { WorkspaceGeneralSetting } from "@/types/proto/api/v1/workspace_setting_service"; import { WorkspaceSettingKey } from "@/types/proto/store/workspace_setting"; @@ -18,14 +16,8 @@ import { useTranslate } from "@/utils/i18n"; const SignIn = () => { const t = useTranslate(); - const navigateTo = useNavigateTo(); const commonContext = useCommonContext(); const workspaceSettingStore = useWorkspaceSettingStore(); - const userStore = useUserStore(); - const actionBtnLoadingState = useLoading(false); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [remember, setRemember] = useState(true); const [identityProviderList, setIdentityProviderList] = useState([]); const workspaceGeneralSetting = workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.GENERAL).generalSetting || WorkspaceGeneralSetting.fromPartial({}); @@ -38,23 +30,6 @@ const SignIn = () => { fetchIdentityProviderList(); }, []); - useEffect(() => { - if (commonContext.profile.mode === "demo") { - setUsername("yourselfhosted"); - setPassword("yourselfhosted"); - } - }, [commonContext.profile.mode]); - - const handleUsernameInputChanged = (e: React.ChangeEvent) => { - const text = e.target.value as string; - setUsername(text); - }; - - const handlePasswordInputChanged = (e: React.ChangeEvent) => { - const text = e.target.value as string; - setPassword(text); - }; - const handleLocaleSelectChange = (locale: Locale) => { commonContext.setLocale(locale); }; @@ -63,32 +38,6 @@ const SignIn = () => { commonContext.setAppearance(appearance); }; - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleSignInButtonClick(); - }; - - const handleSignInButtonClick = async () => { - if (username === "" || password === "") { - return; - } - - if (actionBtnLoadingState.isLoading) { - return; - } - - try { - actionBtnLoadingState.setLoading(); - await authServiceClient.signIn({ username, password, neverExpire: remember }); - await userStore.fetchCurrentUser(); - navigateTo("/"); - } catch (error: any) { - console.error(error); - toast.error((error as ClientError).details || "Failed to sign in."); - } - actionBtnLoadingState.setFinish(); - }; - const handleSignInWithIdentityProvider = async (identityProvider: IdentityProvider) => { const stateQueryParameter = `auth.signin.${identityProvider.title}-${extractIdentityProviderIdFromName(identityProvider.name)}`; if (identityProvider.type === IdentityProvider_Type.OAUTH2) { @@ -117,62 +66,7 @@ const SignIn = () => {

{!workspaceGeneralSetting.disallowPasswordSignin ? ( -
-
-
- {t("common.username")} - -
-
- {t("common.password")} - -
-
-
- setRemember(e.target.checked)} - /> -
-
- -
-
+ ) : (

Password auth is not allowed.

)} diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 6cacedf3..6be39e6b 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -3,6 +3,7 @@ import App from "@/App"; import RootLayout from "@/layouts/RootLayout"; import SuspenseWrapper from "@/layouts/SuspenseWrapper"; import About from "@/pages/About"; +import AdminSignIn from "@/pages/AdminSignIn"; import Archived from "@/pages/Archived"; import AuthCallback from "@/pages/AuthCallback"; import Explore from "@/pages/Explore"; @@ -41,6 +42,10 @@ const router = createBrowserRouter([ path: "", element: , }, + { + path: "admin", + element: , + }, { path: "signup", element: ,