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
f5802a7d
Commit
f5802a7d
authored
Sep 15, 2023
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: update access token ui
parent
33d9c13b
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
373 additions
and
41 deletions
+373
-41
auth.go
api/v1/auth.go
+3
-3
jwt.go
api/v1/jwt.go
+24
-8
acl.go
api/v2/acl.go
+23
-5
user_service.go
api/v2/user_service.go
+1
-1
cache.go
store/cache.go
+4
-0
user_setting.go
store/user_setting.go
+4
-4
CreateAccessTokenDialog.tsx
web/src/components/CreateAccessTokenDialog.tsx
+145
-0
AccessTokenSection.tsx
web/src/components/Settings/AccessTokenSection.tsx
+148
-0
MyAccountSection.tsx
web/src/components/Settings/MyAccountSection.tsx
+4
-1
EmbedMemo.tsx
web/src/pages/EmbedMemo.tsx
+2
-2
Explore.tsx
web/src/pages/Explore.tsx
+2
-2
Home.tsx
web/src/pages/Home.tsx
+7
-9
MemoDetail.tsx
web/src/pages/MemoDetail.tsx
+2
-2
Setting.tsx
web/src/pages/Setting.tsx
+2
-2
UserProfile.tsx
web/src/pages/UserProfile.tsx
+2
-2
No files found.
api/v1/auth.go
View file @
f5802a7d
...
@@ -98,7 +98,7 @@ func (s *APIV1Service) SignIn(c echo.Context) error {
...
@@ -98,7 +98,7 @@ func (s *APIV1Service) SignIn(c echo.Context) error {
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Incorrect login credentials, please try again"
)
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Incorrect login credentials, please try again"
)
}
}
accessToken
,
err
:=
auth
.
GenerateAccessToken
(
user
.
Email
,
user
.
ID
,
time
.
Now
()
.
Add
(
auth
.
AccessTokenDuration
),
s
.
Secret
)
accessToken
,
err
:=
auth
.
GenerateAccessToken
(
user
.
Username
,
user
.
ID
,
time
.
Now
()
.
Add
(
auth
.
AccessTokenDuration
),
s
.
Secret
)
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"failed to generate tokens, err: %s"
,
err
))
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"failed to generate tokens, err: %s"
,
err
))
.
SetInternal
(
err
)
}
}
...
@@ -222,7 +222,7 @@ func (s *APIV1Service) SignInSSO(c echo.Context) error {
...
@@ -222,7 +222,7 @@ func (s *APIV1Service) SignInSSO(c echo.Context) error {
return
echo
.
NewHTTPError
(
http
.
StatusForbidden
,
fmt
.
Sprintf
(
"User has been archived with username %s"
,
userInfo
.
Identifier
))
return
echo
.
NewHTTPError
(
http
.
StatusForbidden
,
fmt
.
Sprintf
(
"User has been archived with username %s"
,
userInfo
.
Identifier
))
}
}
accessToken
,
err
:=
auth
.
GenerateAccessToken
(
user
.
Email
,
user
.
ID
,
time
.
Now
()
.
Add
(
auth
.
AccessTokenDuration
),
s
.
Secret
)
accessToken
,
err
:=
auth
.
GenerateAccessToken
(
user
.
Username
,
user
.
ID
,
time
.
Now
()
.
Add
(
auth
.
AccessTokenDuration
),
s
.
Secret
)
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"failed to generate tokens, err: %s"
,
err
))
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"failed to generate tokens, err: %s"
,
err
))
.
SetInternal
(
err
)
}
}
...
@@ -318,7 +318,7 @@ func (s *APIV1Service) SignUp(c echo.Context) error {
...
@@ -318,7 +318,7 @@ func (s *APIV1Service) SignUp(c echo.Context) error {
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to create user"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to create user"
)
.
SetInternal
(
err
)
}
}
accessToken
,
err
:=
auth
.
GenerateAccessToken
(
user
.
Email
,
user
.
ID
,
time
.
Now
()
.
Add
(
auth
.
AccessTokenDuration
),
s
.
Secret
)
accessToken
,
err
:=
auth
.
GenerateAccessToken
(
user
.
Username
,
user
.
ID
,
time
.
Now
()
.
Add
(
auth
.
AccessTokenDuration
),
s
.
Secret
)
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"failed to generate tokens, err: %s"
,
err
))
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"failed to generate tokens, err: %s"
,
err
))
.
SetInternal
(
err
)
}
}
...
...
api/v1/jwt.go
View file @
f5802a7d
...
@@ -10,6 +10,7 @@ import (
...
@@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/pkg/errors"
"github.com/usememos/memos/api/auth"
"github.com/usememos/memos/api/auth"
"github.com/usememos/memos/common/util"
"github.com/usememos/memos/common/util"
storepb
"github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
...
@@ -66,13 +67,15 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
...
@@ -66,13 +67,15 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
return
next
(
c
)
return
next
(
c
)
}
}
println
(
"path"
,
path
)
// Skip validation for server status endpoints.
// Skip validation for server status endpoints.
if
util
.
HasPrefixes
(
path
,
"/api/v1/ping"
,
"/api/v1/idp"
,
"/api/v1/status"
,
"/api/v1/user"
)
&&
path
!=
"/api/v1/user/me"
&&
method
==
http
.
MethodGet
{
if
util
.
HasPrefixes
(
path
,
"/api/v1/ping"
,
"/api/v1/idp"
,
"/api/v1/status"
,
"/api/v1/user"
)
&&
path
!=
"/api/v1/user/me"
&&
method
==
http
.
MethodGet
{
return
next
(
c
)
return
next
(
c
)
}
}
t
oken
:=
findAccessToken
(
c
)
accessT
oken
:=
findAccessToken
(
c
)
if
t
oken
==
""
{
if
accessT
oken
==
""
{
// Allow the user to access the public endpoints.
// Allow the user to access the public endpoints.
if
util
.
HasPrefixes
(
path
,
"/o"
)
{
if
util
.
HasPrefixes
(
path
,
"/o"
)
{
return
next
(
c
)
return
next
(
c
)
...
@@ -85,7 +88,7 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
...
@@ -85,7 +88,7 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
}
}
claims
:=
&
auth
.
ClaimsMessage
{}
claims
:=
&
auth
.
ClaimsMessage
{}
_
,
err
:=
jwt
.
ParseWithClaims
(
t
oken
,
claims
,
func
(
t
*
jwt
.
Token
)
(
any
,
error
)
{
_
,
err
:=
jwt
.
ParseWithClaims
(
accessT
oken
,
claims
,
func
(
t
*
jwt
.
Token
)
(
any
,
error
)
{
if
t
.
Method
.
Alg
()
!=
jwt
.
SigningMethodHS256
.
Name
{
if
t
.
Method
.
Alg
()
!=
jwt
.
SigningMethodHS256
.
Name
{
return
nil
,
errors
.
Errorf
(
"unexpected access token signing method=%v, expect %v"
,
t
.
Header
[
"alg"
],
jwt
.
SigningMethodHS256
)
return
nil
,
errors
.
Errorf
(
"unexpected access token signing method=%v, expect %v"
,
t
.
Header
[
"alg"
],
jwt
.
SigningMethodHS256
)
}
}
...
@@ -98,6 +101,7 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
...
@@ -98,6 +101,7 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
})
})
if
err
!=
nil
{
if
err
!=
nil
{
RemoveTokensAndCookies
(
c
)
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
errors
.
Wrap
(
err
,
"Invalid or expired access token"
))
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
errors
.
Wrap
(
err
,
"Invalid or expired access token"
))
}
}
if
!
audienceContains
(
claims
.
Audience
,
auth
.
AccessTokenAudienceName
)
{
if
!
audienceContains
(
claims
.
Audience
,
auth
.
AccessTokenAudienceName
)
{
...
@@ -110,6 +114,15 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
...
@@ -110,6 +114,15 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Malformed ID in the token."
)
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Malformed ID in the token."
)
}
}
accessTokens
,
err
:=
server
.
Store
.
GetUserAccessTokens
(
ctx
,
userID
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to get user access tokens."
)
.
WithInternal
(
err
)
}
if
!
validateAccessToken
(
accessToken
,
accessTokens
)
{
RemoveTokensAndCookies
(
c
)
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Invalid access token."
)
}
// Even if there is no error, we still need to make sure the user still exists.
// Even if there is no error, we still need to make sure the user still exists.
user
,
err
:=
server
.
Store
.
GetUser
(
ctx
,
&
store
.
FindUser
{
user
,
err
:=
server
.
Store
.
GetUser
(
ctx
,
&
store
.
FindUser
{
ID
:
&
userID
,
ID
:
&
userID
,
...
@@ -127,13 +140,16 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
...
@@ -127,13 +140,16 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
}
}
}
}
func
(
s
*
APIV1Service
)
defaultAuthSkipper
(
c
echo
.
Context
)
bool
{
func
(
*
APIV1Service
)
defaultAuthSkipper
(
c
echo
.
Context
)
bool
{
path
:=
c
.
Path
()
path
:=
c
.
Path
()
return
util
.
HasPrefixes
(
path
,
"/api/v1/auth"
)
}
// Skip auth.
func
validateAccessToken
(
accessTokenString
string
,
userAccessTokens
[]
*
storepb
.
AccessTokensUserSetting_AccessToken
)
bool
{
if
util
.
HasPrefixes
(
path
,
"/api/v1/auth"
)
{
for
_
,
userAccessToken
:=
range
userAccessTokens
{
return
true
if
accessTokenString
==
userAccessToken
.
AccessToken
{
return
true
}
}
}
return
false
return
false
}
}
api/v2/acl.go
View file @
f5802a7d
...
@@ -8,6 +8,7 @@ import (
...
@@ -8,6 +8,7 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
"github.com/pkg/errors"
"github.com/usememos/memos/api/auth"
"github.com/usememos/memos/api/auth"
storepb
"github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
"google.golang.org/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/codes"
...
@@ -44,12 +45,12 @@ func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, re
...
@@ -44,12 +45,12 @@ func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, re
if
!
ok
{
if
!
ok
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"failed to parse metadata from incoming context"
)
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"failed to parse metadata from incoming context"
)
}
}
accessToken
Str
,
err
:=
getTokenFromMetadata
(
md
)
accessToken
,
err
:=
getTokenFromMetadata
(
md
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
err
.
Error
())
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
err
.
Error
())
}
}
username
,
err
:=
in
.
authenticate
(
ctx
,
accessToken
Str
)
username
,
err
:=
in
.
authenticate
(
ctx
,
accessToken
)
if
err
!=
nil
{
if
err
!=
nil
{
if
isUnauthorizeAllowedMethod
(
serverInfo
.
FullMethod
)
{
if
isUnauthorizeAllowedMethod
(
serverInfo
.
FullMethod
)
{
return
handler
(
ctx
,
request
)
return
handler
(
ctx
,
request
)
...
@@ -74,12 +75,12 @@ func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, re
...
@@ -74,12 +75,12 @@ func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, re
return
handler
(
childCtx
,
request
)
return
handler
(
childCtx
,
request
)
}
}
func
(
in
*
GRPCAuthInterceptor
)
authenticate
(
ctx
context
.
Context
,
accessToken
Str
string
)
(
string
,
error
)
{
func
(
in
*
GRPCAuthInterceptor
)
authenticate
(
ctx
context
.
Context
,
accessToken
string
)
(
string
,
error
)
{
if
accessToken
Str
==
""
{
if
accessToken
==
""
{
return
""
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"access token not found"
)
return
""
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"access token not found"
)
}
}
claims
:=
&
auth
.
ClaimsMessage
{}
claims
:=
&
auth
.
ClaimsMessage
{}
_
,
err
:=
jwt
.
ParseWithClaims
(
accessToken
Str
,
claims
,
func
(
t
*
jwt
.
Token
)
(
any
,
error
)
{
_
,
err
:=
jwt
.
ParseWithClaims
(
accessToken
,
claims
,
func
(
t
*
jwt
.
Token
)
(
any
,
error
)
{
if
t
.
Method
.
Alg
()
!=
jwt
.
SigningMethodHS256
.
Name
{
if
t
.
Method
.
Alg
()
!=
jwt
.
SigningMethodHS256
.
Name
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"unexpected access token signing method=%v, expect %v"
,
t
.
Header
[
"alg"
],
jwt
.
SigningMethodHS256
)
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"unexpected access token signing method=%v, expect %v"
,
t
.
Header
[
"alg"
],
jwt
.
SigningMethodHS256
)
}
}
...
@@ -115,6 +116,14 @@ func (in *GRPCAuthInterceptor) authenticate(ctx context.Context, accessTokenStr
...
@@ -115,6 +116,14 @@ func (in *GRPCAuthInterceptor) authenticate(ctx context.Context, accessTokenStr
return
""
,
errors
.
Errorf
(
"user %q is archived"
,
username
)
return
""
,
errors
.
Errorf
(
"user %q is archived"
,
username
)
}
}
accessTokens
,
err
:=
in
.
Store
.
GetUserAccessTokens
(
ctx
,
user
.
ID
)
if
err
!=
nil
{
return
""
,
errors
.
Wrapf
(
err
,
"failed to get user access tokens"
)
}
if
!
validateAccessToken
(
accessToken
,
accessTokens
)
{
return
""
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"invalid access token"
)
}
return
username
,
nil
return
username
,
nil
}
}
...
@@ -148,3 +157,12 @@ func audienceContains(audience jwt.ClaimStrings, token string) bool {
...
@@ -148,3 +157,12 @@ func audienceContains(audience jwt.ClaimStrings, token string) bool {
}
}
return
false
return
false
}
}
func
validateAccessToken
(
accessTokenString
string
,
userAccessTokens
[]
*
storepb
.
AccessTokensUserSetting_AccessToken
)
bool
{
for
_
,
userAccessToken
:=
range
userAccessTokens
{
if
accessTokenString
==
userAccessToken
.
AccessToken
{
return
true
}
}
return
false
}
api/v2/user_service.go
View file @
f5802a7d
...
@@ -167,7 +167,7 @@ func (s *UserService) CreateUserAccessToken(ctx context.Context, request *apiv2p
...
@@ -167,7 +167,7 @@ func (s *UserService) CreateUserAccessToken(ctx context.Context, request *apiv2p
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get current user: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get current user: %v"
,
err
)
}
}
accessToken
,
err
:=
auth
.
GenerateAccessToken
(
user
.
Email
,
user
.
ID
,
request
.
UserAccessToken
.
ExpiresAt
.
AsTime
(),
s
.
Secret
)
accessToken
,
err
:=
auth
.
GenerateAccessToken
(
user
.
Username
,
user
.
ID
,
request
.
UserAccessToken
.
ExpiresAt
.
AsTime
(),
s
.
Secret
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to generate access token: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to generate access token: %v"
,
err
)
}
}
...
...
store/cache.go
View file @
f5802a7d
...
@@ -7,3 +7,7 @@ import (
...
@@ -7,3 +7,7 @@ import (
func
getUserSettingCacheKey
(
userID
int32
,
key
string
)
string
{
func
getUserSettingCacheKey
(
userID
int32
,
key
string
)
string
{
return
fmt
.
Sprintf
(
"%d-%s"
,
userID
,
key
)
return
fmt
.
Sprintf
(
"%d-%s"
,
userID
,
key
)
}
}
func
getUserSettingV1CacheKey
(
userID
int32
,
key
string
)
string
{
return
fmt
.
Sprintf
(
"%d-%s-v1"
,
userID
,
key
)
}
store/user_setting.go
View file @
f5802a7d
...
@@ -136,7 +136,7 @@ func (s *Store) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSet
...
@@ -136,7 +136,7 @@ func (s *Store) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSet
}
}
userSettingMessage
:=
upsert
userSettingMessage
:=
upsert
s
.
userSettingCache
.
Store
(
getUserSettingCacheKey
(
userSettingMessage
.
UserId
,
userSettingMessage
.
Key
.
String
()),
userSettingMessage
)
s
.
userSettingCache
.
Store
(
getUserSetting
V1
CacheKey
(
userSettingMessage
.
UserId
,
userSettingMessage
.
Key
.
String
()),
userSettingMessage
)
return
userSettingMessage
,
nil
return
userSettingMessage
,
nil
}
}
...
@@ -195,14 +195,14 @@ func (s *Store) ListUserSettingsV1(ctx context.Context, find *FindUserSettingV1)
...
@@ -195,14 +195,14 @@ func (s *Store) ListUserSettingsV1(ctx context.Context, find *FindUserSettingV1)
}
}
for
_
,
userSetting
:=
range
userSettingList
{
for
_
,
userSetting
:=
range
userSettingList
{
s
.
userSettingCache
.
Store
(
getUserSettingCacheKey
(
userSetting
.
UserId
,
userSetting
.
Key
.
String
()),
userSetting
)
s
.
userSettingCache
.
Store
(
getUserSetting
V1
CacheKey
(
userSetting
.
UserId
,
userSetting
.
Key
.
String
()),
userSetting
)
}
}
return
userSettingList
,
nil
return
userSettingList
,
nil
}
}
func
(
s
*
Store
)
GetUserSettingV1
(
ctx
context
.
Context
,
find
*
FindUserSettingV1
)
(
*
storepb
.
UserSetting
,
error
)
{
func
(
s
*
Store
)
GetUserSettingV1
(
ctx
context
.
Context
,
find
*
FindUserSettingV1
)
(
*
storepb
.
UserSetting
,
error
)
{
if
find
.
UserID
!=
nil
{
if
find
.
UserID
!=
nil
{
if
cache
,
ok
:=
s
.
userSettingCache
.
Load
(
getUserSettingCacheKey
(
*
find
.
UserID
,
find
.
Key
.
String
()));
ok
{
if
cache
,
ok
:=
s
.
userSettingCache
.
Load
(
getUserSetting
V1
CacheKey
(
*
find
.
UserID
,
find
.
Key
.
String
()));
ok
{
return
cache
.
(
*
storepb
.
UserSetting
),
nil
return
cache
.
(
*
storepb
.
UserSetting
),
nil
}
}
}
}
...
@@ -217,7 +217,7 @@ func (s *Store) GetUserSettingV1(ctx context.Context, find *FindUserSettingV1) (
...
@@ -217,7 +217,7 @@ func (s *Store) GetUserSettingV1(ctx context.Context, find *FindUserSettingV1) (
}
}
userSetting
:=
list
[
0
]
userSetting
:=
list
[
0
]
s
.
userSettingCache
.
Store
(
getUserSettingCacheKey
(
userSetting
.
UserId
,
userSetting
.
Key
.
String
()),
userSetting
)
s
.
userSettingCache
.
Store
(
getUserSetting
V1
CacheKey
(
userSetting
.
UserId
,
userSetting
.
Key
.
String
()),
userSetting
)
return
userSetting
,
nil
return
userSetting
,
nil
}
}
...
...
web/src/components/CreateAccessTokenDialog.tsx
0 → 100644
View file @
f5802a7d
import
{
Button
,
Input
,
Radio
,
RadioGroup
}
from
"@mui/joy"
;
import
axios
from
"axios"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
Icon
from
"./Icon"
;
interface
Props
extends
DialogProps
{
onConfirm
:
()
=>
void
;
}
const
expirationOptions
=
[
{
label
:
"8 hours"
,
value
:
3600
*
8
,
},
{
label
:
"1 month"
,
value
:
3600
*
24
*
30
,
},
{
label
:
"Never"
,
value
:
0
,
},
];
interface
State
{
description
:
string
;
expiration
:
number
;
}
const
CreateAccessTokenDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
,
onConfirm
}
=
props
;
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
state
,
setState
]
=
useState
({
description
:
""
,
expiration
:
3600
*
8
,
});
const
requestState
=
useLoading
(
false
);
const
setPartialState
=
(
partialState
:
Partial
<
State
>
)
=>
{
setState
({
...
state
,
...
partialState
,
});
};
const
handleDescriptionInputChange
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setPartialState
({
description
:
e
.
target
.
value
,
});
};
const
handleRoleInputChange
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setPartialState
({
expiration
:
Number
(
e
.
target
.
value
),
});
};
const
handleSaveBtnClick
=
async
()
=>
{
if
(
!
state
.
description
)
{
toast
.
error
(
"Description is required"
);
return
;
}
try
{
await
axios
.
post
(
`/api/v2/users/
${
currentUser
.
id
}
/access_tokens`
,
{
description
:
state
.
description
,
expiresAt
:
new
Date
(
Date
.
now
()
+
state
.
expiration
*
1000
),
});
onConfirm
();
destroy
();
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
response
.
data
.
message
);
}
};
return
(
<>
<
div
className=
"dialog-header-container"
>
<
p
className=
"title-text"
>
Create access token
</
p
>
<
button
className=
"btn close-btn"
onClick=
{
()
=>
destroy
()
}
>
<
Icon
.
X
/>
</
button
>
</
div
>
<
div
className=
"dialog-content-container !w-80"
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"mb-2"
>
Description
<
span
className=
"text-red-600"
>
*
</
span
>
</
span
>
<
div
className=
"relative w-full"
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
"Some description"
value=
{
state
.
description
}
onChange=
{
handleDescriptionInputChange
}
/>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"mb-2"
>
Expiration
<
span
className=
"text-red-600"
>
*
</
span
>
</
span
>
<
div
className=
"w-full flex flex-row justify-start items-center text-base"
>
<
RadioGroup
orientation=
"horizontal"
value=
{
state
.
expiration
}
onChange=
{
handleRoleInputChange
}
>
{
expirationOptions
.
map
((
option
)
=>
(
<
Radio
key=
{
option
.
value
}
value=
{
option
.
value
}
checked=
{
state
.
expiration
===
option
.
value
}
label=
{
option
.
label
}
/>
))
}
</
RadioGroup
>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center mt-4 space-x-2"
>
<
Button
color=
"neutral"
variant=
"plain"
disabled=
{
requestState
.
isLoading
}
loading=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
loading=
{
requestState
.
isLoading
}
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
</>
);
};
function
showCreateAccessTokenDialog
(
onConfirm
:
()
=>
void
)
{
generateDialog
(
{
className
:
"create-access-token-dialog"
,
dialogName
:
"create-access-token-dialog"
,
},
CreateAccessTokenDialog
,
{
onConfirm
,
}
);
}
export
default
showCreateAccessTokenDialog
;
web/src/components/Settings/AccessTokenSection.tsx
0 → 100644
View file @
f5802a7d
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
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
ListUserAccessTokensResponse
,
UserAccessToken
}
from
"@/types/proto/api/v2/user_service_pb"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showCreateAccessTokenDialog
from
"../CreateAccessTokenDialog"
;
import
{
showCommonDialog
}
from
"../Dialog/CommonDialog"
;
import
Icon
from
"../Icon"
;
const
listAccessTokens
=
async
(
username
:
string
)
=>
{
const
{
data
}
=
await
axios
.
get
<
ListUserAccessTokensResponse
>
(`/api/v2/users/$
{
username
}
/access_tokens`);
return data.accessTokens;
};
const AccessTokenSection = () =
>
{
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
userAccessTokens
,
setUserAccessTokens
]
=
useState
<
UserAccessToken
[]
>
([]);
useEffect
(()
=>
{
listAccessTokens
(
currentUser
.
username
).
then
((
accessTokens
)
=>
{
setUserAccessTokens
(
accessTokens
);
});
},
[]);
const
handleCreateAccessTokenDialogConfirm
=
async
()
=>
{
const
accessTokens
=
await
listAccessTokens
(
currentUser
.
username
);
setUserAccessTokens
(
accessTokens
);
};
const
copyAccessToken
=
(
accessToken
:
string
)
=>
{
copy
(
accessToken
);
toast
.
success
(
"Access token copied to clipboard"
);
};
const
handleDeleteAccessToken
=
async
(
accessToken
:
string
)
=>
{
showCommonDialog
({
title
:
"Delete Access Token"
,
content
:
`Are you sure to delete access token \`${getFormatedAccessToken(accessToken)}\`? You cannot undo this action.`
,
style
:
"danger"
,
dialogName
:
"delete-access-token-dialog"
,
onConfirm
:
async
()
=>
{
await
axios
.
delete
(
`/api/v2/users/${currentUser.id}/access_tokens/${accessToken}`
);
setUserAccessTokens
(
userAccessTokens
.
filter
((
token
)
=>
token
.
accessToken
!==
accessToken
));
},
});
};
const
getFormatedAccessToken
=
(
accessToken
:
string
)
=>
{
return
`${accessToken.slice(0, 4)}****${accessToken.slice(-4)}`
;
};
return
(
<>
<
div
className=
"mt-8 w-full flex flex-col justify-start items-start space-y-4"
>
<
div
className=
"w-full"
>
<
div
className=
"sm:flex sm:items-center"
>
<
div
className=
"sm:flex-auto"
>
<
p
className=
"text-sm mt-4 first:mt-2 mb-3 font-mono text-gray-500 dark:text-gray-400"
>
Access Tokens
</
p
>
<
p
className=
"mt-2 text-sm text-gray-700"
>
A list of all access tokens for your account.
</
p
>
</
div
>
<
div
className=
"mt-4 sm:ml-16 sm:mt-0 sm:flex-none"
>
<
Button
variant=
"outlined"
color=
"neutral"
onClick=
{
()
=>
{
showCreateAccessTokenDialog
(
handleCreateAccessTokenDialogConfirm
);
}
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
<
div
className=
"mt-2 flow-root"
>
<
div
className=
"overflow-x-auto"
>
<
div
className=
"inline-block min-w-full py-2 align-middle"
>
<
table
className=
"min-w-full divide-y divide-gray-300"
>
<
thead
>
<
tr
>
<
th
scope=
"col"
className=
"px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-400"
>
Token
</
th
>
<
th
scope=
"col"
className=
"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-400"
>
Description
</
th
>
<
th
scope=
"col"
className=
"px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-400"
>
Created At
</
th
>
<
th
scope=
"col"
className=
"px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-400"
>
Expires At
</
th
>
<
th
scope=
"col"
className=
"relative py-3.5 pl-3 pr-4"
>
<
span
className=
"sr-only"
>
{
t
(
"common.delete"
)
}
</
span
>
</
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-gray-200"
>
{
userAccessTokens
.
map
((
userAccessToken
)
=>
(
<
tr
key=
{
userAccessToken
.
accessToken
}
>
<
td
className=
"whitespace-nowrap px-3 py-4 text-sm text-gray-900 dark:text-gray-400 flex flex-row justify-start items-center gap-x-1"
>
<
span
className=
"font-mono"
>
{
getFormatedAccessToken
(
userAccessToken
.
accessToken
)
}
</
span
>
<
IconButton
color=
"neutral"
variant=
"plain"
size=
"sm"
onClick=
{
()
=>
copyAccessToken
(
userAccessToken
.
accessToken
)
}
>
<
Icon
.
Clipboard
className=
"w-4 h-auto text-gray-400"
/>
</
IconButton
>
</
td
>
<
td
className=
"whitespace-nowrap py-4 pl-4 pr-3 text-sm text-gray-900 dark:text-gray-400"
>
{
userAccessToken
.
description
}
</
td
>
<
td
className=
"whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400"
>
{
String
(
userAccessToken
.
issuedAt
)
}
</
td
>
<
td
className=
"whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400"
>
{
String
(
userAccessToken
.
expiresAt
??
"Never"
)
}
</
td
>
<
td
className=
"relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm"
>
<
IconButton
color=
"danger"
variant=
"plain"
size=
"sm"
onClick=
{
()
=>
{
handleDeleteAccessToken
(
userAccessToken
.
accessToken
);
}
}
>
<
Icon
.
Trash
className=
"w-4 h-auto"
/>
</
IconButton
>
</
td
>
</
tr
>
))
}
</
tbody
>
</
table
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</>
);
}
;
export default AccessTokenSection;
web/src/components/Settings/MyAccountSection.tsx
View file @
f5802a7d
...
@@ -4,6 +4,7 @@ import { useTranslate } from "@/utils/i18n";
...
@@ -4,6 +4,7 @@ import { useTranslate } from "@/utils/i18n";
import
showChangePasswordDialog
from
"../ChangePasswordDialog"
;
import
showChangePasswordDialog
from
"../ChangePasswordDialog"
;
import
showUpdateAccountDialog
from
"../UpdateAccountDialog"
;
import
showUpdateAccountDialog
from
"../UpdateAccountDialog"
;
import
UserAvatar
from
"../UserAvatar"
;
import
UserAvatar
from
"../UserAvatar"
;
import
AccessTokenSection
from
"./AccessTokenSection"
;
const
MyAccountSection
=
()
=>
{
const
MyAccountSection
=
()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -12,7 +13,7 @@ const MyAccountSection = () => {
...
@@ -12,7 +13,7 @@ const MyAccountSection = () => {
return
(
return
(
<>
<>
<
div
className=
"section-container account-section-container"
>
<
div
className=
"section-container account-section-container"
>
<
p
className=
"t
itle-text
"
>
{
t
(
"setting.account-section.title"
)
}
</
p
>
<
p
className=
"t
ext-sm mt-4 first:mt-2 mb-3 font-mono text-gray-500 dark:text-gray-400
"
>
{
t
(
"setting.account-section.title"
)
}
</
p
>
<
div
className=
"flex flex-row justify-start items-center"
>
<
div
className=
"flex flex-row justify-start items-center"
>
<
UserAvatar
className=
"mr-2"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
UserAvatar
className=
"mr-2"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
span
className=
"text-2xl leading-10 font-medium"
>
{
user
.
nickname
}
</
span
>
<
span
className=
"text-2xl leading-10 font-medium"
>
{
user
.
nickname
}
</
span
>
...
@@ -27,6 +28,8 @@ const MyAccountSection = () => {
...
@@ -27,6 +28,8 @@ const MyAccountSection = () => {
{
t
(
"setting.account-section.change-password"
)
}
{
t
(
"setting.account-section.change-password"
)
}
</
Button
>
</
Button
>
</
div
>
</
div
>
<
AccessTokenSection
/>
</
div
>
</
div
>
</>
</>
);
);
...
...
web/src/pages/EmbedMemo.tsx
View file @
f5802a7d
...
@@ -42,7 +42,7 @@ const EmbedMemo = () => {
...
@@ -42,7 +42,7 @@ const EmbedMemo = () => {
return
(
return
(
<
section
className=
"w-full h-full flex flex-row justify-start items-start p-2"
>
<
section
className=
"w-full h-full flex flex-row justify-start items-start p-2"
>
{
!
loadingState
.
isLoading
&&
(
{
!
loadingState
.
isLoading
&&
(
<
main
className=
"w-full max-w-lg mx-auto my-auto shadow px-4 py-4 rounded-lg"
>
<
div
className=
"w-full max-w-lg mx-auto my-auto shadow px-4 py-4 rounded-lg"
>
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
div
className=
"w-full mb-2 flex flex-row justify-start items-center text-sm text-gray-400 dark:text-gray-300"
>
<
div
className=
"w-full mb-2 flex flex-row justify-start items-center text-sm text-gray-400 dark:text-gray-300"
>
<
span
>
{
getDateTimeString
(
state
.
memo
.
displayTs
)
}
</
span
>
<
span
>
{
getDateTimeString
(
state
.
memo
.
displayTs
)
}
</
span
>
...
@@ -53,7 +53,7 @@ const EmbedMemo = () => {
...
@@ -53,7 +53,7 @@ const EmbedMemo = () => {
<
MemoContent
className=
"memo-content"
content=
{
state
.
memo
.
content
}
onMemoContentClick=
{
()
=>
undefined
}
/>
<
MemoContent
className=
"memo-content"
content=
{
state
.
memo
.
content
}
onMemoContentClick=
{
()
=>
undefined
}
/>
<
MemoResourceListView
resourceList=
{
state
.
memo
.
resourceList
}
/>
<
MemoResourceListView
resourceList=
{
state
.
memo
.
resourceList
}
/>
</
div
>
</
div
>
</
main
>
</
div
>
)
}
)
}
</
section
>
</
section
>
);
);
...
...
web/src/pages/Explore.tsx
View file @
f5802a7d
...
@@ -90,7 +90,7 @@ const Explore = () => {
...
@@ -90,7 +90,7 @@ const Explore = () => {
<
section
className=
"w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800"
>
<
section
className=
"w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800"
>
<
MobileHeader
showSearch=
{
false
}
/>
<
MobileHeader
showSearch=
{
false
}
/>
{
!
loadingState
.
isLoading
&&
(
{
!
loadingState
.
isLoading
&&
(
<
main
className=
"relative w-full h-auto flex flex-col justify-start items-start"
>
<
div
className=
"relative w-full h-auto flex flex-col justify-start items-start"
>
<
MemoFilter
/>
<
MemoFilter
/>
{
sortedMemos
.
map
((
memo
)
=>
{
{
sortedMemos
.
map
((
memo
)
=>
{
return
<
Memo
key=
{
`${memo.id}-${memo.displayTs}`
}
memo=
{
memo
}
/>;
return
<
Memo
key=
{
`${memo.id}-${memo.displayTs}`
}
memo=
{
memo
}
/>;
...
@@ -107,7 +107,7 @@ const Explore = () => {
...
@@ -107,7 +107,7 @@ const Explore = () => {
{
t
(
"memo.fetch-more"
)
}
{
t
(
"memo.fetch-more"
)
}
</
p
>
</
p
>
)
}
)
}
</
main
>
</
div
>
)
}
)
}
</
section
>
</
section
>
);
);
...
...
web/src/pages/Home.tsx
View file @
f5802a7d
...
@@ -13,15 +13,13 @@ const Home = () => {
...
@@ -13,15 +13,13 @@ const Home = () => {
const
user
=
useCurrentUser
();
const
user
=
useCurrentUser
();
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
user
)
{
if
(
!
user
)
{
return
;
const
systemStatus
=
globalStore
.
state
.
systemStatus
;
}
if
(
systemStatus
.
disablePublicMemos
)
{
window
.
location
.
href
=
"/auth"
;
const
systemStatus
=
globalStore
.
state
.
systemStatus
;
}
else
{
if
(
systemStatus
.
disablePublicMemos
)
{
window
.
location
.
href
=
"/explore"
;
window
.
location
.
href
=
"/auth"
;
}
}
else
{
window
.
location
.
href
=
"/explore"
;
}
}
},
[]);
},
[]);
...
...
web/src/pages/MemoDetail.tsx
View file @
f5802a7d
...
@@ -47,9 +47,9 @@ const MemoDetail = () => {
...
@@ -47,9 +47,9 @@ const MemoDetail = () => {
{
!
loadingState
.
isLoading
&&
{
!
loadingState
.
isLoading
&&
(
memo
?
(
(
memo
?
(
<>
<>
<
main
className=
"relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4"
>
<
div
className=
"relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4"
>
<
Memo
memo=
{
memo
}
/>
<
Memo
memo=
{
memo
}
/>
</
main
>
</
div
>
</>
</>
)
:
(
)
:
(
<>
<>
...
...
web/src/pages/Setting.tsx
View file @
f5802a7d
...
@@ -49,7 +49,7 @@ const Setting = () => {
...
@@ -49,7 +49,7 @@ const Setting = () => {
};
};
return
(
return
(
<
section
className=
"w-full min-h-full flex flex-col md:flex-row justify-start items-start px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800"
>
<
section
className=
"w-full m
ax-w-3xl m
in-h-full flex flex-col md:flex-row justify-start items-start px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800"
>
<
MobileHeader
showSearch=
{
false
}
/>
<
MobileHeader
showSearch=
{
false
}
/>
<
div
className=
"setting-page-wrapper"
>
<
div
className=
"setting-page-wrapper"
>
<
div
className=
"section-selector-container"
>
<
div
className=
"section-selector-container"
>
...
@@ -100,7 +100,7 @@ const Setting = () => {
...
@@ -100,7 +100,7 @@ const Setting = () => {
</>
</>
)
:
null
}
)
:
null
}
</
div
>
</
div
>
<
div
className=
"section-content-container"
>
<
div
className=
"section-content-container
sm:max-w-[calc(100%-14rem)]
"
>
<
Select
<
Select
className=
"block mb-2 sm:!hidden"
className=
"block mb-2 sm:!hidden"
value=
{
state
.
selectedSection
}
value=
{
state
.
selectedSection
}
...
...
web/src/pages/UserProfile.tsx
View file @
f5802a7d
...
@@ -38,7 +38,7 @@ const UserProfile = () => {
...
@@ -38,7 +38,7 @@ const UserProfile = () => {
{
!
loadingState
.
isLoading
&&
{
!
loadingState
.
isLoading
&&
(
user
?
(
(
user
?
(
<>
<>
<
main
className=
"relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4"
>
<
div
className=
"relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4"
>
<
div
className=
"w-full flex flex-row justify-start items-start"
>
<
div
className=
"w-full flex flex-row justify-start items-start"
>
<
div
className=
"flex-grow shrink w-full"
>
<
div
className=
"flex-grow shrink w-full"
>
<
div
className=
"w-full flex flex-col justify-start items-center py-8"
>
<
div
className=
"w-full flex flex-col justify-start items-center py-8"
>
...
@@ -53,7 +53,7 @@ const UserProfile = () => {
...
@@ -53,7 +53,7 @@ const UserProfile = () => {
<
MemoList
/>
<
MemoList
/>
</
div
>
</
div
>
</
div
>
</
div
>
</
main
>
</
div
>
</>
</>
)
:
(
)
:
(
<>
<>
...
...
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