diff --git a/api/v1/auth.go b/api/v1/auth.go index 0dae91bc..d124be94 100644 --- a/api/v1/auth.go +++ b/api/v1/auth.go @@ -20,6 +20,10 @@ import ( "github.com/usememos/memos/store" ) +var ( + usernameMatcher = regexp.MustCompile("^[a-z]([a-z0-9-]{2,30}[a-z0-9])?$") +) + type SignIn struct { Username string `json:"username"` Password string `json:"password"` @@ -279,6 +283,9 @@ func (s *APIV1Service) SignUp(c echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err) } + if !usernameMatcher.MatchString(signup.Username) { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err) + } userCreate := &store.User{ Username: signup.Username, diff --git a/api/v1/user.go b/api/v1/user.go index a2f8f870..ed509330 100644 --- a/api/v1/user.go +++ b/api/v1/user.go @@ -140,6 +140,9 @@ func (s *APIV1Service) CreateUser(c echo.Context) error { if err := userCreate.Validate(); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err) } + if !usernameMatcher.MatchString(userCreate.Username) { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", userCreate.Username)).SetInternal(err) + } // Disallow host user to be created. if userCreate.Role == RoleHost { return echo.NewHTTPError(http.StatusForbidden, "Could not create host user") @@ -362,6 +365,9 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error { userUpdate.RowStatus = &rowStatus } if request.Username != nil { + if !usernameMatcher.MatchString(*request.Username) { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", *request.Username)).SetInternal(err) + } userUpdate.Username = request.Username } if request.Email != nil { diff --git a/api/v2/user_service.go b/api/v2/user_service.go index 2a57d37a..0d22b200 100644 --- a/api/v2/user_service.go +++ b/api/v2/user_service.go @@ -3,6 +3,7 @@ package v2 import ( "context" "net/http" + "regexp" "time" "github.com/golang-jwt/jwt/v4" @@ -20,6 +21,10 @@ import ( "github.com/usememos/memos/store" ) +var ( + usernameMatcher = regexp.MustCompile("^[a-z]([a-z0-9-]{2,30}[a-z0-9])?$") +) + type UserService struct { apiv2pb.UnimplementedUserServiceServer @@ -72,6 +77,9 @@ func (s *UserService) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUse } for _, path := range request.UpdateMask { if path == "username" { + if !usernameMatcher.MatchString(request.User.Username) { + return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username) + } update.Username = &request.User.Username } else if path == "nickname" { update.Nickname = &request.User.Nickname diff --git a/web/src/components/CreateAccessTokenDialog.tsx b/web/src/components/CreateAccessTokenDialog.tsx index 7be95b44..79575a23 100644 --- a/web/src/components/CreateAccessTokenDialog.tsx +++ b/web/src/components/CreateAccessTokenDialog.tsx @@ -1,7 +1,7 @@ import { Button, Input, Radio, RadioGroup } from "@mui/joy"; -import axios from "axios"; import React, { useState } from "react"; import { toast } from "react-hot-toast"; +import { userServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; import useLoading from "@/hooks/useLoading"; import { useTranslate } from "@/utils/i18n"; @@ -68,9 +68,12 @@ const CreateAccessTokenDialog: React.FC = (props: Props) => { } try { - await axios.post(`/api/v2/users/${currentUser.id}/access_tokens`, { - description: state.description, - expiresAt: new Date(Date.now() + state.expiration * 1000), + await userServiceClient.createUserAccessToken({ + username: currentUser.username, + userAccessToken: { + description: state.description, + expiresAt: new Date(Date.now() + state.expiration * 1000), + }, }); onConfirm(); diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 31e15326..41591854 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,7 +1,8 @@ import classNames from "classnames"; import { useEffect } from "react"; import { NavLink, useLocation } from "react-router-dom"; -import { useLayoutStore, useUserStore } from "@/store/module"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { useLayoutStore } from "@/store/module"; import { useTranslate } from "@/utils/i18n"; import { resolution } from "@/utils/layout"; import Icon from "./Icon"; @@ -17,10 +18,9 @@ interface NavLinkItem { const Header = () => { const t = useTranslate(); const location = useLocation(); - const userStore = useUserStore(); const layoutStore = useLayoutStore(); const showHeader = layoutStore.state.showHeader; - const isVisitorMode = userStore.isVisitorMode() && !userStore.state.user; + const user = useCurrentUser(); useEffect(() => { const handleWindowResize = () => { @@ -77,7 +77,7 @@ const Header = () => { icon: , }; - const navLinks: NavLinkItem[] = !isVisitorMode + const navLinks: NavLinkItem[] = user ? [homeNavLink, dailyReviewNavLink, resourcesNavLink, exploreNavLink, archivedNavLink, settingNavLink] : [exploreNavLink, authNavLink]; diff --git a/web/src/components/HomeSidebar.tsx b/web/src/components/HomeSidebar.tsx index b75e77f4..d09c6754 100644 --- a/web/src/components/HomeSidebar.tsx +++ b/web/src/components/HomeSidebar.tsx @@ -1,11 +1,10 @@ -import { useLayoutStore, useUserStore } from "../store/module"; +import { useLayoutStore } from "../store/module"; import SearchBar from "./SearchBar"; import TagList from "./TagList"; import UsageHeatMap from "./UsageHeatMap"; const HomeSidebar = () => { const layoutStore = useLayoutStore(); - const userStore = useUserStore(); const showHomeSidebar = layoutStore.state.showHomeSidebar; return ( @@ -29,11 +28,7 @@ const HomeSidebar = () => { - {!userStore.isVisitorMode() && ( - <> - - - )} + ); diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 5aad3d4f..e148d87b 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { UNKNOWN_ID } from "@/helpers/consts"; import { getRelativeTimeString } from "@/helpers/datetime"; +import useCurrentUser from "@/hooks/useCurrentUser"; import { useFilterStore, useMemoStore, useUserStore } from "@/store/module"; import { useUserV1Store } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; @@ -28,16 +29,17 @@ interface Props { const Memo: React.FC = (props: Props) => { const { memo, lazyRendering } = props; - const { i18n } = useTranslation(); const t = useTranslate(); + const { i18n } = useTranslation(); const filterStore = useFilterStore(); const userStore = useUserStore(); const memoStore = useMemoStore(); const userV1Store = useUserV1Store(); + const user = useCurrentUser(); const [shouldRender, setShouldRender] = useState(lazyRendering ? false : true); const [displayTime, setDisplayTime] = useState(getRelativeTimeString(memo.displayTs)); const memoContainerRef = useRef(null); - const readonly = userStore.isVisitorMode() || userStore.getCurrentUsername() !== memo.creatorUsername; + const readonly = memo.creatorUsername !== user?.username; const creator = userV1Store.getUserByUsername(memo.creatorUsername); // Prepare memo creator. @@ -227,7 +229,7 @@ const Memo: React.FC = (props: Props) => {
{creator && ( <> - + {creator.nickname} diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index 90120346..cdffb5a4 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -1,9 +1,11 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; +import { useParams } from "react-router-dom"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import { getTimeStampByDate } from "@/helpers/datetime"; +import useCurrentUser from "@/hooks/useCurrentUser"; import { TAG_REG } from "@/labs/marked/parser"; -import { useFilterStore, useMemoStore, useUserStore } from "@/store/module"; +import { useFilterStore, useMemoStore } from "@/store/module"; import { useTranslate } from "@/utils/i18n"; import Empty from "./Empty"; import Memo from "./Memo"; @@ -11,17 +13,17 @@ import "@/less/memo-list.less"; const MemoList: React.FC = () => { const t = useTranslate(); + const params = useParams(); const memoStore = useMemoStore(); - const userStore = useUserStore(); const filterStore = useFilterStore(); const filter = filterStore.state; const { memos } = memoStore.state; const [isFetching, setIsFetching] = useState(true); const [isComplete, setIsComplete] = useState(false); - - const currentUsername = userStore.getCurrentUsername(); + const user = useCurrentUser(); const { tag: tagQuery, duration, text: textQuery, visibility } = filter; const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || textQuery || visibility); + const username = params.username || user?.username || ""; const shownMemos = ( showMemoFilter @@ -61,7 +63,7 @@ const MemoList: React.FC = () => { return shouldShow; }) : memos - ).filter((memo) => memo.creatorUsername === currentUsername && memo.rowStatus === "NORMAL"); + ).filter((memo) => memo.creatorUsername === username && memo.rowStatus === "NORMAL"); const pinnedMemos = shownMemos.filter((m) => m.pinned); const unpinnedMemos = shownMemos.filter((m) => !m.pinned); @@ -72,11 +74,9 @@ const MemoList: React.FC = () => { unpinnedMemos.sort(memoSort); const sortedMemos = pinnedMemos.concat(unpinnedMemos).filter((m) => m.rowStatus === "NORMAL"); - const statusRef = useRef(null); - useEffect(() => { memoStore - .fetchMemos() + .fetchMemos(username) .then((fetchedMemos) => { if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) { setIsComplete(true); @@ -89,7 +89,7 @@ const MemoList: React.FC = () => { console.error(error); toast.error(error.response.data.message); }); - }, [currentUsername]); + }, [user?.username]); useEffect(() => { const pageWrapper = document.body.querySelector(".page-wrapper"); @@ -112,20 +112,12 @@ const MemoList: React.FC = () => { observer.unobserve(entry.target); } }); - if (statusRef?.current) { - observer.observe(statusRef.current); - } - return () => { - if (statusRef?.current) { - observer.unobserve(statusRef.current); - } - }; - }, [isFetching, isComplete, filter, sortedMemos.length, statusRef]); + }, [isFetching, isComplete, filter, sortedMemos.length]); const handleFetchMoreClick = async () => { try { setIsFetching(true); - const fetchedMemos = await memoStore.fetchMemos(DEFAULT_MEMO_LIMIT, memos.length); + const fetchedMemos = await memoStore.fetchMemos(username, DEFAULT_MEMO_LIMIT, memos.length); if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) { setIsComplete(true); } else { @@ -148,8 +140,8 @@ const MemoList: React.FC = () => {

{t("memo.fetching-data")}

) : ( -
-

+

+
{isComplete ? ( sortedMemos.length === 0 && (
@@ -164,7 +156,7 @@ const MemoList: React.FC = () => { )} -

+
)}
diff --git a/web/src/components/Settings/AccessTokenSection.tsx b/web/src/components/Settings/AccessTokenSection.tsx index 92f2a83f..4bc1b16b 100644 --- a/web/src/components/Settings/AccessTokenSection.tsx +++ b/web/src/components/Settings/AccessTokenSection.tsx @@ -1,10 +1,10 @@ import { Button, IconButton } from "@mui/joy"; -import axios from "axios"; import copy from "copy-to-clipboard"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; +import { userServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { ListUserAccessTokensResponse, UserAccessToken } from "@/types/proto/api/v2/user_service"; +import { UserAccessToken } from "@/types/proto/api/v2/user_service"; import { useTranslate } from "@/utils/i18n"; import showCreateAccessTokenDialog from "../CreateAccessTokenDialog"; import { showCommonDialog } from "../Dialog/CommonDialog"; @@ -12,8 +12,8 @@ import Icon from "../Icon"; import LearnMore from "../LearnMore"; const listAccessTokens = async (username: string) => { - const { data } = await axios.get(`/api/v2/users/${username}/access_tokens`); - return data.accessTokens; + const { accessTokens } = await userServiceClient.listUserAccessTokens({ username: username }); + return accessTokens; }; const AccessTokenSection = () => { @@ -44,7 +44,7 @@ const AccessTokenSection = () => { style: "danger", dialogName: "delete-access-token-dialog", onConfirm: async () => { - await axios.delete(`/api/v2/users/${currentUser.id}/access_tokens/${accessToken}`); + await userServiceClient.deleteUserAccessToken({ username: currentUser.username, accessToken: accessToken }); setUserAccessTokens(userAccessTokens.filter((token) => token.accessToken !== accessToken)); }, }); diff --git a/web/src/components/UsageHeatMap.tsx b/web/src/components/UsageHeatMap.tsx index 9f543cb2..dba8ed97 100644 --- a/web/src/components/UsageHeatMap.tsx +++ b/web/src/components/UsageHeatMap.tsx @@ -3,9 +3,10 @@ import { getMemoStats } from "@/helpers/api"; import { DAILY_TIMESTAMP } from "@/helpers/consts"; import { getDateStampByDate, getDateString, getTimeStampByDate } from "@/helpers/datetime"; import * as utils from "@/helpers/utils"; +import useCurrentUser from "@/hooks/useCurrentUser"; import { useUserV1Store } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; -import { useFilterStore, useMemoStore, useUserStore } from "../store/module"; +import { useFilterStore, useMemoStore } from "../store/module"; import "@/less/usage-heat-map.less"; const tableConfig = { @@ -32,8 +33,8 @@ interface DailyUsageStat { const UsageHeatMap = () => { const t = useTranslate(); const filterStore = useFilterStore(); - const userStore = useUserStore(); const userV1Store = useUserV1Store(); + const user = useCurrentUser(); const memoStore = useMemoStore(); const todayTimeStamp = getDateStampByDate(Date.now()); const todayDay = new Date(todayTimeStamp).getDay() + 1; @@ -46,23 +47,22 @@ const UsageHeatMap = () => { const [allStat, setAllStat] = useState(getInitialUsageStat(usedDaysAmount, beginDayTimestamp)); const [currentStat, setCurrentStat] = useState(null); const containerElRef = useRef(null); - const currentUsername = userStore.getCurrentUsername(); useEffect(() => { - userV1Store.getOrFetchUserByUsername(currentUsername).then((user) => { + userV1Store.getOrFetchUserByUsername(user.username).then((user) => { if (!user) { return; } setCreatedDays(Math.ceil((Date.now() - getTimeStampByDate(user.createTime)) / 1000 / 3600 / 24)); }); - }, [currentUsername]); + }, [user.username]); useEffect(() => { if (memos.length === 0) { return; } - getMemoStats(currentUsername) + getMemoStats(user.username) .then(({ data }) => { setMemoAmount(data.length); const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp); @@ -81,7 +81,7 @@ const UsageHeatMap = () => { .catch((error) => { console.error(error); }); - }, [memos.length, currentUsername]); + }, [memos.length, user.username]); const handleUsageStatItemMouseEnter = useCallback((event: React.MouseEvent, item: DailyUsageStat) => { const tempDiv = document.createElement("div"); diff --git a/web/src/components/UserBanner.tsx b/web/src/components/UserBanner.tsx index f1d1e185..f0ba8348 100644 --- a/web/src/components/UserBanner.tsx +++ b/web/src/components/UserBanner.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import useCurrentUser from "@/hooks/useCurrentUser"; import { useGlobalStore, useUserStore } from "@/store/module"; +import { User_Role } from "@/types/proto/api/v2/user_service"; import { useTranslate } from "@/utils/i18n"; import showAboutSiteDialog from "./AboutSiteDialog"; import Icon from "./Icon"; @@ -13,17 +14,11 @@ const UserBanner = () => { const globalStore = useGlobalStore(); const userStore = useUserStore(); const { systemStatus } = globalStore.state; - const { user } = userStore.state; - const [username, setUsername] = useState("Memos"); - - useEffect(() => { - if (user) { - setUsername(user.nickname || user.username); - } - }, [user]); + const user = useCurrentUser(); + const title = user ? user.nickname : systemStatus.customizedProfile.name || "memos"; const handleMyAccountClick = () => { - navigate(`/u/${user?.username}`); + navigate(`/u/${encodeURIComponent(user.username)}`); }; const handleAboutBtnClick = () => { @@ -42,10 +37,8 @@ const UserBanner = () => { trigger={
- - {user != undefined ? username : systemStatus.customizedProfile.name} - - {user?.role === "HOST" ? ( + {title} + {user?.role === User_Role.HOST ? ( MOD ) : null}
diff --git a/web/src/components/kit/DatePicker.tsx b/web/src/components/kit/DatePicker.tsx index d014bd6d..dc9a81e5 100644 --- a/web/src/components/kit/DatePicker.tsx +++ b/web/src/components/kit/DatePicker.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import { getMemoStats } from "@/helpers/api"; import { DAILY_TIMESTAMP } from "@/helpers/consts"; import { getDateStampByDate, isFutureDate } from "@/helpers/datetime"; -import { useUserStore } from "@/store/module"; +import useCurrentUser from "@/hooks/useCurrentUser"; import { useTranslate } from "@/utils/i18n"; import Icon from "../Icon"; import "@/less/common/date-picker.less"; @@ -21,14 +21,14 @@ const DatePicker: React.FC = (props: DatePickerProps) => { const { className, isFutureDateDisabled, datestamp, handleDateStampChange } = props; const [currentDateStamp, setCurrentDateStamp] = useState(getMonthFirstDayDateStamp(datestamp)); const [countByDate, setCountByDate] = useState(new Map()); - const currentUsername = useUserStore().getCurrentUsername(); + const user = useCurrentUser(); useEffect(() => { setCurrentDateStamp(getMonthFirstDayDateStamp(datestamp)); }, [datestamp]); useEffect(() => { - getMemoStats(currentUsername).then(({ data }) => { + getMemoStats(user.username).then(({ data }) => { const m = new Map(); for (const record of data) { const date = getDateStampByDate(record * 1000); @@ -36,7 +36,7 @@ const DatePicker: React.FC = (props: DatePickerProps) => { } setCountByDate(m); }); - }, [currentUsername]); + }, [user.username]); const firstDate = new Date(currentDateStamp); const firstDateDay = firstDate.getDay() === 0 ? 7 : firstDate.getDay(); diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index b83795f6..f2849480 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -1,6 +1,5 @@ import axios from "axios"; import { Resource } from "@/types/proto/api/v2/resource_service"; -import { GetUserResponse } from "@/types/proto/api/v2/user_service"; export function getSystemStatus() { return axios.get("/api/v1/status"); @@ -56,10 +55,6 @@ export function getUserList() { return axios.get("/api/v1/user"); } -export function getUserByUsername(username: string) { - return axios.get(`/api/v2/users/${username}`); -} - export function upsertUserSetting(upsert: UserSettingUpsert) { return axios.post(`/api/v1/user/setting`, upsert); } diff --git a/web/src/hooks/useCurrentUser.ts b/web/src/hooks/useCurrentUser.ts index bf530eb3..22a8b6ba 100644 --- a/web/src/hooks/useCurrentUser.ts +++ b/web/src/hooks/useCurrentUser.ts @@ -5,7 +5,7 @@ import { useUserV1Store } from "@/store/v1"; const useCurrentUser = () => { const userStore = useUserStore(); const userV1Store = useUserV1Store(); - const currentUsername = userStore.getCurrentUsername(); + const currentUsername = userStore.state.user?.username; useEffect(() => { if (currentUsername) { @@ -13,7 +13,7 @@ const useCurrentUser = () => { } }, [currentUsername]); - return userV1Store.getUserByUsername(currentUsername); + return userV1Store.getUserByUsername(currentUsername || ""); }; export default useCurrentUser; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index a3b04ed2..ccc39555 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -3,17 +3,14 @@ import MemoEditor from "@/components/MemoEditor"; import MemoFilter from "@/components/MemoFilter"; import MemoList from "@/components/MemoList"; import MobileHeader from "@/components/MobileHeader"; -import { useUserStore } from "@/store/module"; const Home = () => { - const userStore = useUserStore(); - return (
- {!userStore.isVisitorMode() && } +
diff --git a/web/src/pages/Resources.tsx b/web/src/pages/Resources.tsx index 143a7297..7ee0aa12 100644 --- a/web/src/pages/Resources.tsx +++ b/web/src/pages/Resources.tsx @@ -1,17 +1,17 @@ -import axios from "axios"; import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import Empty from "@/components/Empty"; import Icon from "@/components/Icon"; import MobileHeader from "@/components/MobileHeader"; import ResourceIcon from "@/components/ResourceIcon"; +import { resourceServiceClient } from "@/grpcweb"; import useLoading from "@/hooks/useLoading"; -import { ListResourcesResponse, Resource } from "@/types/proto/api/v2/resource_service"; +import { Resource } from "@/types/proto/api/v2/resource_service"; import { useTranslate } from "@/utils/i18n"; const fetchAllResources = async () => { - const { data } = await axios.get("/api/v2/resources"); - return data.resources; + const { resources } = await resourceServiceClient.listResources({}); + return resources; }; function groupResourcesByDate(resources: Resource[]) { diff --git a/web/src/pages/Setting.tsx b/web/src/pages/Setting.tsx index 707d2a6b..7585273a 100644 --- a/web/src/pages/Setting.tsx +++ b/web/src/pages/Setting.tsx @@ -1,5 +1,4 @@ import { Option, Select } from "@mui/joy"; -import { isEqual } from "lodash-es"; import { useState } from "react"; import BetaBadge from "@/components/BetaBadge"; import Icon from "@/components/Icon"; @@ -11,6 +10,7 @@ import SSOSection from "@/components/Settings/SSOSection"; import StorageSection from "@/components/Settings/StorageSection"; import SystemSection from "@/components/Settings/SystemSection"; import useCurrentUser from "@/hooks/useCurrentUser"; +import { User_Role } from "@/types/proto/api/v2/user_service"; import { useTranslate } from "@/utils/i18n"; import "@/less/setting.less"; @@ -26,7 +26,7 @@ const Setting = () => { const [state, setState] = useState({ selectedSection: "my-account", }); - const isHost = isEqual(user.role, "HOST"); + const isHost = user.role === User_Role.HOST; const handleSectionSelectorItemClick = (settingSection: SettingSection) => { setState({ diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx index 990f2bab..abb0ad7f 100644 --- a/web/src/pages/UserProfile.tsx +++ b/web/src/pages/UserProfile.tsx @@ -1,26 +1,30 @@ import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; +import { useParams } from "react-router-dom"; import FloatingNavButton from "@/components/FloatingNavButton"; import MemoFilter from "@/components/MemoFilter"; import MemoList from "@/components/MemoList"; import UserAvatar from "@/components/UserAvatar"; import useLoading from "@/hooks/useLoading"; -import { useUserStore } from "@/store/module"; import { useUserV1Store } from "@/store/v1"; import { User } from "@/types/proto/api/v2/user_service"; import { useTranslate } from "@/utils/i18n"; const UserProfile = () => { const t = useTranslate(); - const userStore = useUserStore(); + const params = useParams(); const userV1Store = useUserV1Store(); const loadingState = useLoading(); const [user, setUser] = useState(); useEffect(() => { - const currentUsername = userStore.getCurrentUsername(); + const username = params.username; + if (!username) { + throw new Error("username is required"); + } + userV1Store - .getOrFetchUserByUsername(currentUsername) + .getOrFetchUserByUsername(username) .then((user) => { setUser(user); loadingState.setFinish(); @@ -29,7 +33,7 @@ const UserProfile = () => { console.error(error); toast.error(t("message.user-not-found")); }); - }, [userStore.getCurrentUsername()]); + }, [params.username]); return ( <> diff --git a/web/src/store/module/global.ts b/web/src/store/module/global.ts index 5cc42237..1469b266 100644 --- a/web/src/store/module/global.ts +++ b/web/src/store/module/global.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import { systemServiceClient } from "@/grpcweb"; import * as api from "@/helpers/api"; import storage from "@/helpers/storage"; import i18n from "@/i18n"; @@ -75,11 +75,8 @@ export const useGlobalStore = () => { }, fetchSystemStatus: async () => { const { data: systemStatus } = await api.getSystemStatus(); - // TODO: update this when api v2 is ready. - const { - data: { systemInfo }, - } = await axios.get("/api/v2/system/info"); - systemStatus.dbSize = Number(systemInfo.dbSize); + const { systemInfo } = await systemServiceClient.getSystemInfo({}); + systemStatus.dbSize = systemInfo?.dbSize || 0; store.dispatch(setGlobalState({ systemStatus: systemStatus })); return systemStatus; }, diff --git a/web/src/store/module/memo.ts b/web/src/store/module/memo.ts index 2cd6da77..664b0b62 100644 --- a/web/src/store/module/memo.ts +++ b/web/src/store/module/memo.ts @@ -4,7 +4,6 @@ import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import store, { useAppSelector } from "../"; import { createMemo, deleteMemo, patchMemo, upsertMemos } from "../reducer/memo"; import { useMemoCacheStore } from "../v1"; -import { getUsernameFromPath, useUserStore } from "./"; export const convertResponseModelMemo = (memo: Memo): Memo => { return { @@ -17,7 +16,6 @@ export const convertResponseModelMemo = (memo: Memo): Memo => { export const useMemoStore = () => { const state = useAppSelector((state) => state.memo); - const userStore = useUserStore(); const memoCacheStore = useMemoCacheStore(); const fetchMemoById = async (memoId: MemoId) => { @@ -33,14 +31,14 @@ export const useMemoStore = () => { getState: () => { return store.getState().memo; }, - fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => { + fetchMemos: async (username = "", limit = DEFAULT_MEMO_LIMIT, offset = 0) => { const memoFind: MemoFind = { rowStatus: "NORMAL", limit, offset, }; - if (userStore.isVisitorMode()) { - memoFind.creatorUsername = getUsernameFromPath(); + if (username) { + memoFind.creatorUsername = username; } const { data } = await api.getMemoList(memoFind); const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); diff --git a/web/src/store/module/user.ts b/web/src/store/module/user.ts index 471e8d0d..23b28110 100644 --- a/web/src/store/module/user.ts +++ b/web/src/store/module/user.ts @@ -1,6 +1,5 @@ import { camelCase } from "lodash-es"; import * as api from "@/helpers/api"; -import { UNKNOWN_USERNAME } from "@/helpers/consts"; import storage from "@/helpers/storage"; import { getSystemColorScheme } from "@/helpers/utils"; import store, { useAppSelector } from ".."; @@ -86,38 +85,16 @@ const doSignOut = async () => { await api.signout(); }; -export const getUsernameFromPath = () => { - const pathname = window.location.pathname; - const usernameRegex = /^\/u\/(\w+).*/; - const result = pathname.match(usernameRegex); - if (result && result.length === 2) { - return String(result[1]); - } - return undefined; -}; - export const useUserStore = () => { const state = useAppSelector((state) => state.user); - const isVisitorMode = () => { - return state.user === undefined || getUsernameFromPath(); - }; - return { state, getState: () => { return store.getState().user; }, - isVisitorMode, doSignIn, doSignOut, - getCurrentUsername: () => { - if (isVisitorMode()) { - return getUsernameFromPath() || UNKNOWN_USERNAME; - } else { - return state.user?.username || UNKNOWN_USERNAME; - } - }, upsertUserSetting: async (key: string, value: any) => { await api.upsertUserSetting({ key: key as any, @@ -130,10 +107,10 @@ export const useUserStore = () => { store.dispatch(patchUser({ localSetting })); }, patchUser: async (userPatch: UserPatch): Promise => { - const { data } = await api.patchUser(userPatch); - if (userPatch.id === store.getState().user.user?.id) { - const user = convertResponseModelUser(data); - store.dispatch(patchUser(user)); + await api.patchUser(userPatch); + // If the user is the current user and the username is changed, reload the page. + if (userPatch.id === store.getState().user.user?.id && userPatch.username) { + window.location.reload(); } }, deleteUser: async (userDelete: UserDelete) => { diff --git a/web/vite.config.ts b/web/vite.config.ts index adaa7511..d92719d9 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ target: devProxyServer, xfwd: true, }, - "/explore/rss.xml": { + "^/explore/rss.xml": { target: devProxyServer, xfwd: true, },