Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
bff41a89
Commit
bff41a89
authored
Sep 18, 2023
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: invalid username checks
parent
23750014
Changes
22
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
106 additions
and
132 deletions
+106
-132
auth.go
api/v1/auth.go
+7
-0
user.go
api/v1/user.go
+6
-0
user_service.go
api/v2/user_service.go
+8
-0
CreateAccessTokenDialog.tsx
web/src/components/CreateAccessTokenDialog.tsx
+7
-4
Header.tsx
web/src/components/Header.tsx
+4
-4
HomeSidebar.tsx
web/src/components/HomeSidebar.tsx
+2
-7
Memo.tsx
web/src/components/Memo.tsx
+5
-3
MemoList.tsx
web/src/components/MemoList.tsx
+15
-23
AccessTokenSection.tsx
web/src/components/Settings/AccessTokenSection.tsx
+5
-5
UsageHeatMap.tsx
web/src/components/UsageHeatMap.tsx
+7
-7
UserBanner.tsx
web/src/components/UserBanner.tsx
+7
-14
DatePicker.tsx
web/src/components/kit/DatePicker.tsx
+4
-4
api.ts
web/src/helpers/api.ts
+0
-5
useCurrentUser.ts
web/src/hooks/useCurrentUser.ts
+2
-2
Home.tsx
web/src/pages/Home.tsx
+1
-4
Resources.tsx
web/src/pages/Resources.tsx
+4
-4
Setting.tsx
web/src/pages/Setting.tsx
+2
-2
UserProfile.tsx
web/src/pages/UserProfile.tsx
+9
-5
global.ts
web/src/store/module/global.ts
+3
-6
memo.ts
web/src/store/module/memo.ts
+3
-5
user.ts
web/src/store/module/user.ts
+4
-27
vite.config.ts
web/vite.config.ts
+1
-1
No files found.
api/v1/auth.go
View file @
bff41a89
...
...
@@ -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
,
...
...
api/v1/user.go
View file @
bff41a89
...
...
@@ -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
{
...
...
api/v2/user_service.go
View file @
bff41a89
...
...
@@ -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
...
...
web/src/components/CreateAccessTokenDialog.tsx
View file @
bff41a89
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: 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
();
...
...
web/src/components/Header.tsx
View file @
bff41a89
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
:
<
Icon
.
LogIn
className=
"mr-3 w-6 h-auto opacity-70"
/>,
};
const
navLinks
:
NavLinkItem
[]
=
!
isVisitorMode
const
navLinks
:
NavLinkItem
[]
=
user
?
[
homeNavLink
,
dailyReviewNavLink
,
resourcesNavLink
,
exploreNavLink
,
archivedNavLink
,
settingNavLink
]
:
[
exploreNavLink
,
authNavLink
];
...
...
web/src/components/HomeSidebar.tsx
View file @
bff41a89
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 = () => {
<
SearchBar
/>
</
div
>
<
UsageHeatMap
/>
{
!
userStore
.
isVisitorMode
()
&&
(
<>
<
TagList
/>
</>
)
}
<
TagList
/>
</
aside
>
</
div
>
);
...
...
web/src/components/Memo.tsx
View file @
bff41a89
...
...
@@ -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
:
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
<
boolean
>
(
lazyRendering
?
false
:
true
);
const
[
displayTime
,
setDisplayTime
]
=
useState
<
string
>
(
getRelativeTimeString
(
memo
.
displayTs
));
const
memoContainerRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
readonly
=
userStore
.
isVisitorMode
()
||
userStore
.
getCurrentUsername
()
!==
memo
.
creatorU
sername
;
const
readonly
=
memo
.
creatorUsername
!==
user
?.
u
sername
;
const
creator
=
userV1Store
.
getUserByUsername
(
memo
.
creatorUsername
);
// Prepare memo creator.
...
...
@@ -227,7 +229,7 @@ const Memo: React.FC<Props> = (props: Props) => {
<
div
className=
"w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1"
>
{
creator
&&
(
<>
<
Link
className=
"flex flex-row justify-start items-center"
to=
{
`/u/${
memo.creatorUsername
}`
}
>
<
Link
className=
"flex flex-row justify-start items-center"
to=
{
`/u/${
encodeURIComponent(memo.creatorUsername)
}`
}
>
<
UserAvatar
className=
"!w-5 !h-auto mr-1"
avatarUrl=
{
creator
.
avatarUrl
}
/>
<
span
className=
"text-sm text-gray-600 max-w-[8em] truncate dark:text-gray-400"
>
{
creator
.
nickname
}
</
span
>
</
Link
>
...
...
web/src/components/MemoList.tsx
View file @
bff41a89
import
{
useEffect
,
use
Ref
,
use
State
}
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
<
boolean
>
(
true
);
const
[
isComplete
,
setIsComplete
]
=
useState
<
boolean
>
(
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 ===
currentU
sername
&&
memo.rowStatus === "NORMAL");
).filter((memo) =
>
memo.creatorUsername ===
u
sername
&&
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
<
HTMLDivElement
>
(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
);
});
}
, [
currentU
sername]);
}
, [
user?.u
sername]);
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 = () => {
<
p
className=
"status-text"
>
{
t
(
"memo.fetching-data"
)
}
</
p
>
</
div
>
)
:
(
<
div
ref=
{
statusRef
}
className=
"status-text-container"
>
<
p
className=
"status-text"
>
<
div
className=
"status-text-container"
>
<
div
className=
"status-text"
>
{
isComplete
?
(
sortedMemos
.
length
===
0
&&
(
<
div
className=
"w-full mt-12 mb-8 flex flex-col justify-center items-center italic"
>
...
...
@@ -164,7 +156,7 @@ const MemoList: React.FC = () => {
</
span
>
</>
)
}
</
p
>
</
div
>
</
div
>
)
}
</
div
>
...
...
web/src/components/Settings/AccessTokenSection.tsx
View file @
bff41a89
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
<
ListUserAccessTokensResponse
>
(`/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
));
},
});
...
...
web/src/components/UsageHeatMap.tsx
View file @
bff41a89
...
...
@@ -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
<
DailyUsageStat
[]
>
(
getInitialUsageStat
(
usedDaysAmount
,
beginDayTimestamp
));
const
[
currentStat
,
setCurrentStat
]
=
useState
<
DailyUsageStat
|
null
>
(
null
);
const
containerElRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
currentUsername
=
userStore
.
getCurrentUsername
();
useEffect
(()
=>
{
userV1Store
.
getOrFetchUserByUsername
(
currentU
sername
).
then
((
user
)
=>
{
userV1Store
.
getOrFetchUserByUsername
(
user
.
u
sername
).
then
((
user
)
=>
{
if
(
!
user
)
{
return
;
}
setCreatedDays
(
Math
.
ceil
((
Date
.
now
()
-
getTimeStampByDate
(
user
.
createTime
))
/
1000
/
3600
/
24
));
});
},
[
currentU
sername
]);
},
[
user
.
u
sername
]);
useEffect
(()
=>
{
if
(
memos
.
length
===
0
)
{
return
;
}
getMemoStats
(
currentU
sername
)
getMemoStats
(
user
.
u
sername
)
.
then
(({
data
})
=>
{
setMemoAmount
(
data
.
length
);
const
newStat
:
DailyUsageStat
[]
=
getInitialUsageStat
(
usedDaysAmount
,
beginDayTimestamp
);
...
...
@@ -81,7 +81,7 @@ const UsageHeatMap = () => {
.
catch
((
error
)
=>
{
console
.
error
(
error
);
});
},
[
memos
.
length
,
currentU
sername
]);
},
[
memos
.
length
,
user
.
u
sername
]);
const
handleUsageStatItemMouseEnter
=
useCallback
((
event
:
React
.
MouseEvent
,
item
:
DailyUsageStat
)
=>
{
const
tempDiv
=
document
.
createElement
(
"div"
);
...
...
web/src/components/UserBanner.tsx
View file @
bff41a89
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=
{
<
div
className=
"px-4 py-2 max-w-full flex flex-row justify-start items-center cursor-pointer rounded-lg hover:shadow hover:bg-white dark:hover:bg-zinc-700"
>
<
UserAvatar
className=
"shadow"
avatarUrl=
{
user
?.
avatarUrl
}
/>
<span className="px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate">
{user != undefined ? username : systemStatus.customizedProfile.name}
</span>
{user?.role === "HOST" ? (
<
span
className=
"px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate"
>
{
title
}
</
span
>
{
user
?.
role
===
User_Role
.
HOST
?
(
<
span
className=
"text-xs px-1 bg-blue-600 dark:bg-blue-800 rounded text-white dark:text-gray-200 shadow"
>
MOD
</
span
>
)
:
null
}
</
div
>
...
...
web/src/components/kit/DatePicker.tsx
View file @
bff41a89
...
...
@@ -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<DatePickerProps> = (props: DatePickerProps) => {
const
{
className
,
isFutureDateDisabled
,
datestamp
,
handleDateStampChange
}
=
props
;
const
[
currentDateStamp
,
setCurrentDateStamp
]
=
useState
<
number
>
(
getMonthFirstDayDateStamp
(
datestamp
));
const
[
countByDate
,
setCountByDate
]
=
useState
(
new
Map
());
const
currentUsername
=
useUserStore
().
getCurrentUsername
();
const
user
=
useCurrentUser
();
useEffect
(()
=>
{
setCurrentDateStamp
(
getMonthFirstDayDateStamp
(
datestamp
));
},
[
datestamp
]);
useEffect
(()
=>
{
getMemoStats
(
currentU
sername
).
then
(({
data
})
=>
{
getMemoStats
(
user
.
u
sername
).
then
(({
data
})
=>
{
const
m
=
new
Map
();
for
(
const
record
of
data
)
{
const
date
=
getDateStampByDate
(
record
*
1000
);
...
...
@@ -36,7 +36,7 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
}
setCountByDate
(
m
);
});
},
[
currentU
sername
]);
},
[
user
.
u
sername
]);
const
firstDate
=
new
Date
(
currentDateStamp
);
const
firstDateDay
=
firstDate
.
getDay
()
===
0
?
7
:
firstDate
.
getDay
();
...
...
web/src/helpers/api.ts
View file @
bff41a89
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
<
SystemStatus
>
(
"/api/v1/status"
);
...
...
@@ -56,10 +55,6 @@ export function getUserList() {
return
axios
.
get
<
User
[]
>
(
"/api/v1/user"
);
}
export
function
getUserByUsername
(
username
:
string
)
{
return
axios
.
get
<
GetUserResponse
>
(
`/api/v2/users/
${
username
}
`
);
}
export
function
upsertUserSetting
(
upsert
:
UserSettingUpsert
)
{
return
axios
.
post
<
UserSetting
>
(
`/api/v1/user/setting`
,
upsert
);
}
...
...
web/src/hooks/useCurrentUser.ts
View file @
bff41a89
...
...
@@ -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
;
web/src/pages/Home.tsx
View file @
bff41a89
...
...
@@ -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
(
<
div
className=
"w-full flex flex-row justify-start items-start"
>
<
div
className=
"flex-grow shrink w-auto px-4 sm:px-2 sm:pt-4"
>
<
MobileHeader
/>
<
div
className=
"w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg"
>
{
!
userStore
.
isVisitorMode
()
&&
<
MemoEditor
className=
"mb-2"
/>
}
<
MemoEditor
className=
"mb-2"
/>
<
MemoFilter
/>
</
div
>
<
MemoList
/>
...
...
web/src/pages/Resources.tsx
View file @
bff41a89
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
<
ListResourcesResponse
>
("/api/v2/resources"
);
return
data.
resources;
const
{
resources
}
=
await
resourceServiceClient
.
listResources
({}
);
return
resources
;
};
function
groupResourcesByDate
(
resources
:
Resource
[])
{
...
...
web/src/pages/Setting.tsx
View file @
bff41a89
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
<
State
>
({
selectedSection
:
"my-account"
,
});
const
isHost
=
isEqual
(
user
.
role
,
"HOST"
)
;
const
isHost
=
user
.
role
===
User_Role
.
HOST
;
const
handleSectionSelectorItemClick
=
(
settingSection
:
SettingSection
)
=>
{
setState
({
...
...
web/src/pages/UserProfile.tsx
View file @
bff41a89
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
<
User
>
();
useEffect
(()
=>
{
const
currentUsername
=
userStore
.
getCurrentUsername
();
const
username
=
params
.
username
;
if
(
!
username
)
{
throw
new
Error
(
"username is required"
);
}
userV1Store
.
getOrFetchUserByUsername
(
currentU
sername
)
.
getOrFetchUserByUsername
(
u
sername
)
.
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
(
<>
...
...
web/src/store/module/global.ts
View file @
bff41a89
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
;
},
...
...
web/src/store/module/memo.ts
View file @
bff41a89
...
...
@@ -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
(
user
Store
.
isVisitorMode
()
)
{
memoFind
.
creatorUsername
=
getUsernameFromPath
()
;
if
(
user
name
)
{
memoFind
.
creatorUsername
=
username
;
}
const
{
data
}
=
await
api
.
getMemoList
(
memoFind
);
const
fetchedMemos
=
data
.
map
((
m
)
=>
convertResponseModelMemo
(
m
));
...
...
web/src/store/module/user.ts
View file @
bff41a89
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
<
void
>
=>
{
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
)
=>
{
...
...
web/vite.config.ts
View file @
bff41a89
...
...
@@ -27,7 +27,7 @@ export default defineConfig({
target
:
devProxyServer
,
xfwd
:
true
,
},
"/explore/rss.xml"
:
{
"
^
/explore/rss.xml"
:
{
target
:
devProxyServer
,
xfwd
:
true
,
},
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment