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
77b7fc44
Commit
77b7fc44
authored
Jun 22, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement user session
parent
741fe35c
Changes
9
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
1687 additions
and
98 deletions
+1687
-98
user_service.proto
proto/api/v1/user_service.proto
+82
-0
user_service.pb.go
proto/gen/api/v1/user_service.pb.go
+432
-67
user_service.pb.gw.go
proto/gen/api/v1/user_service.pb.gw.go
+156
-0
user_service_grpc.pb.go
proto/gen/api/v1/user_service_grpc.pb.go
+80
-0
apidocs.swagger.yaml
proto/gen/apidocs.swagger.yaml
+141
-31
auth_service.go
server/router/api/v1/auth_service.go
+46
-0
user_service.go
server/router/api/v1/user_service.go
+103
-0
user_setting.go
store/user_setting.go
+122
-0
user_service.ts
web/src/types/proto/api/v1/user_service.ts
+525
-0
No files found.
proto/api/v1/user_service.proto
View file @
77b7fc44
...
@@ -108,6 +108,18 @@ service UserService {
...
@@ -108,6 +108,18 @@ service UserService {
option
(
google.api.http
)
=
{
delete
:
"/api/v1/{name=users/*/accessTokens/*}"
};
option
(
google.api.http
)
=
{
delete
:
"/api/v1/{name=users/*/accessTokens/*}"
};
option
(
google.api.method_signature
)
=
"name"
;
option
(
google.api.method_signature
)
=
"name"
;
}
}
// ListUserSessions returns a list of active sessions for a user.
rpc
ListUserSessions
(
ListUserSessionsRequest
)
returns
(
ListUserSessionsResponse
)
{
option
(
google.api.http
)
=
{
get
:
"/api/v1/{parent=users/*}/sessions"
};
option
(
google.api.method_signature
)
=
"parent"
;
}
// RevokeUserSession revokes a specific session for a user.
rpc
RevokeUserSession
(
RevokeUserSessionRequest
)
returns
(
google.protobuf.Empty
)
{
option
(
google.api.http
)
=
{
delete
:
"/api/v1/{name=users/*/sessions/*}"
};
option
(
google.api.method_signature
)
=
"name"
;
}
}
}
message
User
{
message
User
{
...
@@ -458,6 +470,76 @@ message DeleteUserAccessTokenRequest {
...
@@ -458,6 +470,76 @@ message DeleteUserAccessTokenRequest {
];
];
}
}
message
UserSession
{
option
(
google.api.resource
)
=
{
type
:
"memos.api.v1/UserSession"
pattern
:
"users/{user}/sessions/{session}"
name_field
:
"name"
};
// The resource name of the session.
// Format: users/{user}/sessions/{session}
string
name
=
1
[(
google.api.field_behavior
)
=
IDENTIFIER
];
// The session ID.
string
session_id
=
2
[(
google.api.field_behavior
)
=
OUTPUT_ONLY
];
// The timestamp when the session was created.
google.protobuf.Timestamp
create_time
=
3
[(
google.api.field_behavior
)
=
OUTPUT_ONLY
];
// The timestamp when the session expires.
google.protobuf.Timestamp
expire_time
=
4
[(
google.api.field_behavior
)
=
OUTPUT_ONLY
];
// The timestamp when the session was last accessed.
google.protobuf.Timestamp
last_accessed_time
=
5
[(
google.api.field_behavior
)
=
OUTPUT_ONLY
];
// Client information associated with this session.
ClientInfo
client_info
=
6
[(
google.api.field_behavior
)
=
OUTPUT_ONLY
];
message
ClientInfo
{
// User agent string of the client.
string
user_agent
=
1
;
// IP address of the client.
string
ip_address
=
2
;
// Optional. Device type (e.g., "mobile", "desktop", "tablet").
string
device_type
=
3
[(
google.api.field_behavior
)
=
OPTIONAL
];
// Optional. Operating system (e.g., "iOS 17.0", "Windows 11").
string
os
=
4
[(
google.api.field_behavior
)
=
OPTIONAL
];
// Optional. Browser name and version (e.g., "Chrome 119.0").
string
browser
=
5
[(
google.api.field_behavior
)
=
OPTIONAL
];
// Optional. Geographic location (country code, e.g., "US").
string
country
=
6
[(
google.api.field_behavior
)
=
OPTIONAL
];
}
}
message
ListUserSessionsRequest
{
// Required. The resource name of the parent.
// Format: users/{user}
string
parent
=
1
[
(
google.api.field_behavior
)
=
REQUIRED
,
(
google.api.resource_reference
)
=
{
type
:
"memos.api.v1/User"
}
];
}
message
ListUserSessionsResponse
{
// The list of user sessions.
repeated
UserSession
sessions
=
1
;
}
message
RevokeUserSessionRequest
{
// Required. The resource name of the session to revoke.
// Format: users/{user}/sessions/{session}
string
name
=
1
[
(
google.api.field_behavior
)
=
REQUIRED
,
(
google.api.resource_reference
)
=
{
type
:
"memos.api.v1/UserSession"
}
];
}
message
ListAllUserStatsRequest
{
message
ListAllUserStatsRequest
{
// Optional. The maximum number of user stats to return.
// Optional. The maximum number of user stats to return.
int32
page_size
=
1
[(
google.api.field_behavior
)
=
OPTIONAL
];
int32
page_size
=
1
[(
google.api.field_behavior
)
=
OPTIONAL
];
...
...
proto/gen/api/v1/user_service.pb.go
View file @
77b7fc44
This diff is collapsed.
Click to expand it.
proto/gen/api/v1/user_service.pb.gw.go
View file @
77b7fc44
...
@@ -717,6 +717,84 @@ func local_request_UserService_DeleteUserAccessToken_0(ctx context.Context, mars
...
@@ -717,6 +717,84 @@ func local_request_UserService_DeleteUserAccessToken_0(ctx context.Context, mars
return
msg
,
metadata
,
err
return
msg
,
metadata
,
err
}
}
func
request_UserService_ListUserSessions_0
(
ctx
context
.
Context
,
marshaler
runtime
.
Marshaler
,
client
UserServiceClient
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
(
proto
.
Message
,
runtime
.
ServerMetadata
,
error
)
{
var
(
protoReq
ListUserSessionsRequest
metadata
runtime
.
ServerMetadata
err
error
)
if
req
.
Body
!=
nil
{
_
,
_
=
io
.
Copy
(
io
.
Discard
,
req
.
Body
)
}
val
,
ok
:=
pathParams
[
"parent"
]
if
!
ok
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"missing parameter %s"
,
"parent"
)
}
protoReq
.
Parent
,
err
=
runtime
.
String
(
val
)
if
err
!=
nil
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"type mismatch, parameter: %s, error: %v"
,
"parent"
,
err
)
}
msg
,
err
:=
client
.
ListUserSessions
(
ctx
,
&
protoReq
,
grpc
.
Header
(
&
metadata
.
HeaderMD
),
grpc
.
Trailer
(
&
metadata
.
TrailerMD
))
return
msg
,
metadata
,
err
}
func
local_request_UserService_ListUserSessions_0
(
ctx
context
.
Context
,
marshaler
runtime
.
Marshaler
,
server
UserServiceServer
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
(
proto
.
Message
,
runtime
.
ServerMetadata
,
error
)
{
var
(
protoReq
ListUserSessionsRequest
metadata
runtime
.
ServerMetadata
err
error
)
val
,
ok
:=
pathParams
[
"parent"
]
if
!
ok
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"missing parameter %s"
,
"parent"
)
}
protoReq
.
Parent
,
err
=
runtime
.
String
(
val
)
if
err
!=
nil
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"type mismatch, parameter: %s, error: %v"
,
"parent"
,
err
)
}
msg
,
err
:=
server
.
ListUserSessions
(
ctx
,
&
protoReq
)
return
msg
,
metadata
,
err
}
func
request_UserService_RevokeUserSession_0
(
ctx
context
.
Context
,
marshaler
runtime
.
Marshaler
,
client
UserServiceClient
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
(
proto
.
Message
,
runtime
.
ServerMetadata
,
error
)
{
var
(
protoReq
RevokeUserSessionRequest
metadata
runtime
.
ServerMetadata
err
error
)
if
req
.
Body
!=
nil
{
_
,
_
=
io
.
Copy
(
io
.
Discard
,
req
.
Body
)
}
val
,
ok
:=
pathParams
[
"name"
]
if
!
ok
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"missing parameter %s"
,
"name"
)
}
protoReq
.
Name
,
err
=
runtime
.
String
(
val
)
if
err
!=
nil
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"type mismatch, parameter: %s, error: %v"
,
"name"
,
err
)
}
msg
,
err
:=
client
.
RevokeUserSession
(
ctx
,
&
protoReq
,
grpc
.
Header
(
&
metadata
.
HeaderMD
),
grpc
.
Trailer
(
&
metadata
.
TrailerMD
))
return
msg
,
metadata
,
err
}
func
local_request_UserService_RevokeUserSession_0
(
ctx
context
.
Context
,
marshaler
runtime
.
Marshaler
,
server
UserServiceServer
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
(
proto
.
Message
,
runtime
.
ServerMetadata
,
error
)
{
var
(
protoReq
RevokeUserSessionRequest
metadata
runtime
.
ServerMetadata
err
error
)
val
,
ok
:=
pathParams
[
"name"
]
if
!
ok
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"missing parameter %s"
,
"name"
)
}
protoReq
.
Name
,
err
=
runtime
.
String
(
val
)
if
err
!=
nil
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"type mismatch, parameter: %s, error: %v"
,
"name"
,
err
)
}
msg
,
err
:=
server
.
RevokeUserSession
(
ctx
,
&
protoReq
)
return
msg
,
metadata
,
err
}
// RegisterUserServiceHandlerServer registers the http handlers for service UserService to "mux".
// RegisterUserServiceHandlerServer registers the http handlers for service UserService to "mux".
// UnaryRPC :call UserServiceServer directly.
// UnaryRPC :call UserServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
...
@@ -1003,6 +1081,46 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
...
@@ -1003,6 +1081,46 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
}
}
forward_UserService_DeleteUserAccessToken_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
forward_UserService_DeleteUserAccessToken_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
})
mux
.
Handle
(
http
.
MethodGet
,
pattern_UserService_ListUserSessions_0
,
func
(
w
http
.
ResponseWriter
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
req
.
Context
())
defer
cancel
()
var
stream
runtime
.
ServerTransportStream
ctx
=
grpc
.
NewContextWithServerTransportStream
(
ctx
,
&
stream
)
inboundMarshaler
,
outboundMarshaler
:=
runtime
.
MarshalerForRequest
(
mux
,
req
)
annotatedContext
,
err
:=
runtime
.
AnnotateIncomingContext
(
ctx
,
mux
,
req
,
"/memos.api.v1.UserService/ListUserSessions"
,
runtime
.
WithHTTPPathPattern
(
"/api/v1/{parent=users/*}/sessions"
))
if
err
!=
nil
{
runtime
.
HTTPError
(
ctx
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
resp
,
md
,
err
:=
local_request_UserService_ListUserSessions_0
(
annotatedContext
,
inboundMarshaler
,
server
,
req
,
pathParams
)
md
.
HeaderMD
,
md
.
TrailerMD
=
metadata
.
Join
(
md
.
HeaderMD
,
stream
.
Header
()),
metadata
.
Join
(
md
.
TrailerMD
,
stream
.
Trailer
())
annotatedContext
=
runtime
.
NewServerMetadataContext
(
annotatedContext
,
md
)
if
err
!=
nil
{
runtime
.
HTTPError
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
forward_UserService_ListUserSessions_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
mux
.
Handle
(
http
.
MethodDelete
,
pattern_UserService_RevokeUserSession_0
,
func
(
w
http
.
ResponseWriter
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
req
.
Context
())
defer
cancel
()
var
stream
runtime
.
ServerTransportStream
ctx
=
grpc
.
NewContextWithServerTransportStream
(
ctx
,
&
stream
)
inboundMarshaler
,
outboundMarshaler
:=
runtime
.
MarshalerForRequest
(
mux
,
req
)
annotatedContext
,
err
:=
runtime
.
AnnotateIncomingContext
(
ctx
,
mux
,
req
,
"/memos.api.v1.UserService/RevokeUserSession"
,
runtime
.
WithHTTPPathPattern
(
"/api/v1/{name=users/*/sessions/*}"
))
if
err
!=
nil
{
runtime
.
HTTPError
(
ctx
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
resp
,
md
,
err
:=
local_request_UserService_RevokeUserSession_0
(
annotatedContext
,
inboundMarshaler
,
server
,
req
,
pathParams
)
md
.
HeaderMD
,
md
.
TrailerMD
=
metadata
.
Join
(
md
.
HeaderMD
,
stream
.
Header
()),
metadata
.
Join
(
md
.
TrailerMD
,
stream
.
Trailer
())
annotatedContext
=
runtime
.
NewServerMetadataContext
(
annotatedContext
,
md
)
if
err
!=
nil
{
runtime
.
HTTPError
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
forward_UserService_RevokeUserSession_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
return
nil
return
nil
}
}
...
@@ -1281,6 +1399,40 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
...
@@ -1281,6 +1399,40 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
}
}
forward_UserService_DeleteUserAccessToken_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
forward_UserService_DeleteUserAccessToken_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
})
mux
.
Handle
(
http
.
MethodGet
,
pattern_UserService_ListUserSessions_0
,
func
(
w
http
.
ResponseWriter
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
req
.
Context
())
defer
cancel
()
inboundMarshaler
,
outboundMarshaler
:=
runtime
.
MarshalerForRequest
(
mux
,
req
)
annotatedContext
,
err
:=
runtime
.
AnnotateContext
(
ctx
,
mux
,
req
,
"/memos.api.v1.UserService/ListUserSessions"
,
runtime
.
WithHTTPPathPattern
(
"/api/v1/{parent=users/*}/sessions"
))
if
err
!=
nil
{
runtime
.
HTTPError
(
ctx
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
resp
,
md
,
err
:=
request_UserService_ListUserSessions_0
(
annotatedContext
,
inboundMarshaler
,
client
,
req
,
pathParams
)
annotatedContext
=
runtime
.
NewServerMetadataContext
(
annotatedContext
,
md
)
if
err
!=
nil
{
runtime
.
HTTPError
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
forward_UserService_ListUserSessions_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
mux
.
Handle
(
http
.
MethodDelete
,
pattern_UserService_RevokeUserSession_0
,
func
(
w
http
.
ResponseWriter
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
req
.
Context
())
defer
cancel
()
inboundMarshaler
,
outboundMarshaler
:=
runtime
.
MarshalerForRequest
(
mux
,
req
)
annotatedContext
,
err
:=
runtime
.
AnnotateContext
(
ctx
,
mux
,
req
,
"/memos.api.v1.UserService/RevokeUserSession"
,
runtime
.
WithHTTPPathPattern
(
"/api/v1/{name=users/*/sessions/*}"
))
if
err
!=
nil
{
runtime
.
HTTPError
(
ctx
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
resp
,
md
,
err
:=
request_UserService_RevokeUserSession_0
(
annotatedContext
,
inboundMarshaler
,
client
,
req
,
pathParams
)
annotatedContext
=
runtime
.
NewServerMetadataContext
(
annotatedContext
,
md
)
if
err
!=
nil
{
runtime
.
HTTPError
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
forward_UserService_RevokeUserSession_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
return
nil
return
nil
}
}
...
@@ -1299,6 +1451,8 @@ var (
...
@@ -1299,6 +1451,8 @@ var (
pattern_UserService_ListUserAccessTokens_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
,
2
,
4
},
[]
string
{
"api"
,
"v1"
,
"users"
,
"parent"
,
"accessTokens"
},
""
))
pattern_UserService_ListUserAccessTokens_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
,
2
,
4
},
[]
string
{
"api"
,
"v1"
,
"users"
,
"parent"
,
"accessTokens"
},
""
))
pattern_UserService_CreateUserAccessToken_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
,
2
,
4
},
[]
string
{
"api"
,
"v1"
,
"users"
,
"parent"
,
"accessTokens"
},
""
))
pattern_UserService_CreateUserAccessToken_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
,
2
,
4
},
[]
string
{
"api"
,
"v1"
,
"users"
,
"parent"
,
"accessTokens"
},
""
))
pattern_UserService_DeleteUserAccessToken_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
2
,
3
,
1
,
0
,
4
,
4
,
5
,
4
},
[]
string
{
"api"
,
"v1"
,
"users"
,
"accessTokens"
,
"name"
},
""
))
pattern_UserService_DeleteUserAccessToken_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
2
,
3
,
1
,
0
,
4
,
4
,
5
,
4
},
[]
string
{
"api"
,
"v1"
,
"users"
,
"accessTokens"
,
"name"
},
""
))
pattern_UserService_ListUserSessions_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
,
2
,
4
},
[]
string
{
"api"
,
"v1"
,
"users"
,
"parent"
,
"sessions"
},
""
))
pattern_UserService_RevokeUserSession_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
2
,
3
,
1
,
0
,
4
,
4
,
5
,
4
},
[]
string
{
"api"
,
"v1"
,
"users"
,
"sessions"
,
"name"
},
""
))
)
)
var
(
var
(
...
@@ -1316,4 +1470,6 @@ var (
...
@@ -1316,4 +1470,6 @@ var (
forward_UserService_ListUserAccessTokens_0
=
runtime
.
ForwardResponseMessage
forward_UserService_ListUserAccessTokens_0
=
runtime
.
ForwardResponseMessage
forward_UserService_CreateUserAccessToken_0
=
runtime
.
ForwardResponseMessage
forward_UserService_CreateUserAccessToken_0
=
runtime
.
ForwardResponseMessage
forward_UserService_DeleteUserAccessToken_0
=
runtime
.
ForwardResponseMessage
forward_UserService_DeleteUserAccessToken_0
=
runtime
.
ForwardResponseMessage
forward_UserService_ListUserSessions_0
=
runtime
.
ForwardResponseMessage
forward_UserService_RevokeUserSession_0
=
runtime
.
ForwardResponseMessage
)
)
proto/gen/api/v1/user_service_grpc.pb.go
View file @
77b7fc44
...
@@ -35,6 +35,8 @@ const (
...
@@ -35,6 +35,8 @@ const (
UserService_ListUserAccessTokens_FullMethodName
=
"/memos.api.v1.UserService/ListUserAccessTokens"
UserService_ListUserAccessTokens_FullMethodName
=
"/memos.api.v1.UserService/ListUserAccessTokens"
UserService_CreateUserAccessToken_FullMethodName
=
"/memos.api.v1.UserService/CreateUserAccessToken"
UserService_CreateUserAccessToken_FullMethodName
=
"/memos.api.v1.UserService/CreateUserAccessToken"
UserService_DeleteUserAccessToken_FullMethodName
=
"/memos.api.v1.UserService/DeleteUserAccessToken"
UserService_DeleteUserAccessToken_FullMethodName
=
"/memos.api.v1.UserService/DeleteUserAccessToken"
UserService_ListUserSessions_FullMethodName
=
"/memos.api.v1.UserService/ListUserSessions"
UserService_RevokeUserSession_FullMethodName
=
"/memos.api.v1.UserService/RevokeUserSession"
)
)
// UserServiceClient is the client API for UserService service.
// UserServiceClient is the client API for UserService service.
...
@@ -69,6 +71,10 @@ type UserServiceClient interface {
...
@@ -69,6 +71,10 @@ type UserServiceClient interface {
CreateUserAccessToken
(
ctx
context
.
Context
,
in
*
CreateUserAccessTokenRequest
,
opts
...
grpc
.
CallOption
)
(
*
UserAccessToken
,
error
)
CreateUserAccessToken
(
ctx
context
.
Context
,
in
*
CreateUserAccessTokenRequest
,
opts
...
grpc
.
CallOption
)
(
*
UserAccessToken
,
error
)
// DeleteUserAccessToken deletes an access token.
// DeleteUserAccessToken deletes an access token.
DeleteUserAccessToken
(
ctx
context
.
Context
,
in
*
DeleteUserAccessTokenRequest
,
opts
...
grpc
.
CallOption
)
(
*
emptypb
.
Empty
,
error
)
DeleteUserAccessToken
(
ctx
context
.
Context
,
in
*
DeleteUserAccessTokenRequest
,
opts
...
grpc
.
CallOption
)
(
*
emptypb
.
Empty
,
error
)
// ListUserSessions returns a list of active sessions for a user.
ListUserSessions
(
ctx
context
.
Context
,
in
*
ListUserSessionsRequest
,
opts
...
grpc
.
CallOption
)
(
*
ListUserSessionsResponse
,
error
)
// RevokeUserSession revokes a specific session for a user.
RevokeUserSession
(
ctx
context
.
Context
,
in
*
RevokeUserSessionRequest
,
opts
...
grpc
.
CallOption
)
(
*
emptypb
.
Empty
,
error
)
}
}
type
userServiceClient
struct
{
type
userServiceClient
struct
{
...
@@ -219,6 +225,26 @@ func (c *userServiceClient) DeleteUserAccessToken(ctx context.Context, in *Delet
...
@@ -219,6 +225,26 @@ func (c *userServiceClient) DeleteUserAccessToken(ctx context.Context, in *Delet
return
out
,
nil
return
out
,
nil
}
}
func
(
c
*
userServiceClient
)
ListUserSessions
(
ctx
context
.
Context
,
in
*
ListUserSessionsRequest
,
opts
...
grpc
.
CallOption
)
(
*
ListUserSessionsResponse
,
error
)
{
cOpts
:=
append
([]
grpc
.
CallOption
{
grpc
.
StaticMethod
()},
opts
...
)
out
:=
new
(
ListUserSessionsResponse
)
err
:=
c
.
cc
.
Invoke
(
ctx
,
UserService_ListUserSessions_FullMethodName
,
in
,
out
,
cOpts
...
)
if
err
!=
nil
{
return
nil
,
err
}
return
out
,
nil
}
func
(
c
*
userServiceClient
)
RevokeUserSession
(
ctx
context
.
Context
,
in
*
RevokeUserSessionRequest
,
opts
...
grpc
.
CallOption
)
(
*
emptypb
.
Empty
,
error
)
{
cOpts
:=
append
([]
grpc
.
CallOption
{
grpc
.
StaticMethod
()},
opts
...
)
out
:=
new
(
emptypb
.
Empty
)
err
:=
c
.
cc
.
Invoke
(
ctx
,
UserService_RevokeUserSession_FullMethodName
,
in
,
out
,
cOpts
...
)
if
err
!=
nil
{
return
nil
,
err
}
return
out
,
nil
}
// UserServiceServer is the server API for UserService service.
// UserServiceServer is the server API for UserService service.
// All implementations must embed UnimplementedUserServiceServer
// All implementations must embed UnimplementedUserServiceServer
// for forward compatibility.
// for forward compatibility.
...
@@ -251,6 +277,10 @@ type UserServiceServer interface {
...
@@ -251,6 +277,10 @@ type UserServiceServer interface {
CreateUserAccessToken
(
context
.
Context
,
*
CreateUserAccessTokenRequest
)
(
*
UserAccessToken
,
error
)
CreateUserAccessToken
(
context
.
Context
,
*
CreateUserAccessTokenRequest
)
(
*
UserAccessToken
,
error
)
// DeleteUserAccessToken deletes an access token.
// DeleteUserAccessToken deletes an access token.
DeleteUserAccessToken
(
context
.
Context
,
*
DeleteUserAccessTokenRequest
)
(
*
emptypb
.
Empty
,
error
)
DeleteUserAccessToken
(
context
.
Context
,
*
DeleteUserAccessTokenRequest
)
(
*
emptypb
.
Empty
,
error
)
// ListUserSessions returns a list of active sessions for a user.
ListUserSessions
(
context
.
Context
,
*
ListUserSessionsRequest
)
(
*
ListUserSessionsResponse
,
error
)
// RevokeUserSession revokes a specific session for a user.
RevokeUserSession
(
context
.
Context
,
*
RevokeUserSessionRequest
)
(
*
emptypb
.
Empty
,
error
)
mustEmbedUnimplementedUserServiceServer
()
mustEmbedUnimplementedUserServiceServer
()
}
}
...
@@ -303,6 +333,12 @@ func (UnimplementedUserServiceServer) CreateUserAccessToken(context.Context, *Cr
...
@@ -303,6 +333,12 @@ func (UnimplementedUserServiceServer) CreateUserAccessToken(context.Context, *Cr
func
(
UnimplementedUserServiceServer
)
DeleteUserAccessToken
(
context
.
Context
,
*
DeleteUserAccessTokenRequest
)
(
*
emptypb
.
Empty
,
error
)
{
func
(
UnimplementedUserServiceServer
)
DeleteUserAccessToken
(
context
.
Context
,
*
DeleteUserAccessTokenRequest
)
(
*
emptypb
.
Empty
,
error
)
{
return
nil
,
status
.
Errorf
(
codes
.
Unimplemented
,
"method DeleteUserAccessToken not implemented"
)
return
nil
,
status
.
Errorf
(
codes
.
Unimplemented
,
"method DeleteUserAccessToken not implemented"
)
}
}
func
(
UnimplementedUserServiceServer
)
ListUserSessions
(
context
.
Context
,
*
ListUserSessionsRequest
)
(
*
ListUserSessionsResponse
,
error
)
{
return
nil
,
status
.
Errorf
(
codes
.
Unimplemented
,
"method ListUserSessions not implemented"
)
}
func
(
UnimplementedUserServiceServer
)
RevokeUserSession
(
context
.
Context
,
*
RevokeUserSessionRequest
)
(
*
emptypb
.
Empty
,
error
)
{
return
nil
,
status
.
Errorf
(
codes
.
Unimplemented
,
"method RevokeUserSession not implemented"
)
}
func
(
UnimplementedUserServiceServer
)
mustEmbedUnimplementedUserServiceServer
()
{}
func
(
UnimplementedUserServiceServer
)
mustEmbedUnimplementedUserServiceServer
()
{}
func
(
UnimplementedUserServiceServer
)
testEmbeddedByValue
()
{}
func
(
UnimplementedUserServiceServer
)
testEmbeddedByValue
()
{}
...
@@ -576,6 +612,42 @@ func _UserService_DeleteUserAccessToken_Handler(srv interface{}, ctx context.Con
...
@@ -576,6 +612,42 @@ func _UserService_DeleteUserAccessToken_Handler(srv interface{}, ctx context.Con
return
interceptor
(
ctx
,
in
,
info
,
handler
)
return
interceptor
(
ctx
,
in
,
info
,
handler
)
}
}
func
_UserService_ListUserSessions_Handler
(
srv
interface
{},
ctx
context
.
Context
,
dec
func
(
interface
{})
error
,
interceptor
grpc
.
UnaryServerInterceptor
)
(
interface
{},
error
)
{
in
:=
new
(
ListUserSessionsRequest
)
if
err
:=
dec
(
in
);
err
!=
nil
{
return
nil
,
err
}
if
interceptor
==
nil
{
return
srv
.
(
UserServiceServer
)
.
ListUserSessions
(
ctx
,
in
)
}
info
:=
&
grpc
.
UnaryServerInfo
{
Server
:
srv
,
FullMethod
:
UserService_ListUserSessions_FullMethodName
,
}
handler
:=
func
(
ctx
context
.
Context
,
req
interface
{})
(
interface
{},
error
)
{
return
srv
.
(
UserServiceServer
)
.
ListUserSessions
(
ctx
,
req
.
(
*
ListUserSessionsRequest
))
}
return
interceptor
(
ctx
,
in
,
info
,
handler
)
}
func
_UserService_RevokeUserSession_Handler
(
srv
interface
{},
ctx
context
.
Context
,
dec
func
(
interface
{})
error
,
interceptor
grpc
.
UnaryServerInterceptor
)
(
interface
{},
error
)
{
in
:=
new
(
RevokeUserSessionRequest
)
if
err
:=
dec
(
in
);
err
!=
nil
{
return
nil
,
err
}
if
interceptor
==
nil
{
return
srv
.
(
UserServiceServer
)
.
RevokeUserSession
(
ctx
,
in
)
}
info
:=
&
grpc
.
UnaryServerInfo
{
Server
:
srv
,
FullMethod
:
UserService_RevokeUserSession_FullMethodName
,
}
handler
:=
func
(
ctx
context
.
Context
,
req
interface
{})
(
interface
{},
error
)
{
return
srv
.
(
UserServiceServer
)
.
RevokeUserSession
(
ctx
,
req
.
(
*
RevokeUserSessionRequest
))
}
return
interceptor
(
ctx
,
in
,
info
,
handler
)
}
// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service.
// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service.
// It's only intended for direct use with grpc.RegisterService,
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
// and not to be introspected or modified (even as a copy)
...
@@ -639,6 +711,14 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
...
@@ -639,6 +711,14 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName
:
"DeleteUserAccessToken"
,
MethodName
:
"DeleteUserAccessToken"
,
Handler
:
_UserService_DeleteUserAccessToken_Handler
,
Handler
:
_UserService_DeleteUserAccessToken_Handler
,
},
},
{
MethodName
:
"ListUserSessions"
,
Handler
:
_UserService_ListUserSessions_Handler
,
},
{
MethodName
:
"RevokeUserSession"
,
Handler
:
_UserService_RevokeUserSession_Handler
,
},
},
},
Streams
:
[]
grpc
.
StreamDesc
{},
Streams
:
[]
grpc
.
StreamDesc
{},
Metadata
:
"api/v1/user_service.proto"
,
Metadata
:
"api/v1/user_service.proto"
,
...
...
proto/gen/apidocs.swagger.yaml
View file @
77b7fc44
...
@@ -1193,8 +1193,8 @@ paths:
...
@@ -1193,8 +1193,8 @@ paths:
tags
:
tags
:
-
IdentityProviderService
-
IdentityProviderService
delete
:
delete
:
summary
:
DeleteIdentityProvider deletes an identity provid
er.
summary
:
RevokeUserSession revokes a specific session for a us
er.
operationId
:
IdentityProviderService_DeleteIdentityProvider
operationId
:
UserService_RevokeUserSession
responses
:
responses
:
"
200"
:
"
200"
:
description
:
A successful response.
description
:
A successful response.
...
@@ -1208,14 +1208,14 @@ paths:
...
@@ -1208,14 +1208,14 @@ paths:
parameters
:
parameters
:
-
name
:
name_3
-
name
:
name_3
description
:
|-
description
:
|-
Required. The resource name of the
identity provider to delet
e.
Required. The resource name of the
session to revok
e.
Format:
identityProviders/{idp
}
Format:
users/{user}/sessions/{session
}
in: path
in: path
required: true
required: true
type: string
type: string
pattern:
identityProvider
s/[^/]+
pattern:
users/[^/]+/session
s/[^/]+
tags
:
tags
:
-
IdentityProvid
erService
-
Us
erService
/api/v1/{name_4}
:
/api/v1/{name_4}
:
get
:
get
:
summary
:
GetMemo gets a memo.
summary
:
GetMemo gets a memo.
...
@@ -1248,8 +1248,8 @@ paths:
...
@@ -1248,8 +1248,8 @@ paths:
tags
:
tags
:
-
MemoService
-
MemoService
delete
:
delete
:
summary
:
DeleteI
nbox deletes an inbox
.
summary
:
DeleteI
dentityProvider deletes an identity provider
.
operationId
:
I
nboxService_DeleteInbox
operationId
:
I
dentityProviderService_DeleteIdentityProvider
responses
:
responses
:
"
200"
:
"
200"
:
description
:
A successful response.
description
:
A successful response.
...
@@ -1263,14 +1263,14 @@ paths:
...
@@ -1263,14 +1263,14 @@ paths:
parameters
:
parameters
:
-
name
:
name_4
-
name
:
name_4
description
:
|-
description
:
|-
Required. The resource name of the i
nbox
to delete.
Required. The resource name of the i
dentity provider
to delete.
Format: i
nboxes/{inbox
}
Format: i
dentityProviders/{idp
}
in: path
in: path
required: true
required: true
type: string
type: string
pattern: i
nboxe
s/[^/]+
pattern: i
dentityProvider
s/[^/]+
tags
:
tags
:
-
I
nbox
Service
-
I
dentityProvider
Service
/api/v1/{name_5}
:
/api/v1/{name_5}
:
get
:
get
:
summary
:
GetShortcut gets a shortcut by name.
summary
:
GetShortcut gets a shortcut by name.
...
@@ -1296,8 +1296,8 @@ paths:
...
@@ -1296,8 +1296,8 @@ paths:
tags
:
tags
:
-
ShortcutService
-
ShortcutService
delete
:
delete
:
summary
:
Delete
Memo deletes a memo
.
summary
:
Delete
Inbox deletes an inbox
.
operationId
:
MemoService_DeleteMemo
operationId
:
InboxService_DeleteInbox
responses
:
responses
:
"
200"
:
"
200"
:
description
:
A successful response.
description
:
A successful response.
...
@@ -1311,19 +1311,14 @@ paths:
...
@@ -1311,19 +1311,14 @@ paths:
parameters
:
parameters
:
-
name
:
name_5
-
name
:
name_5
description
:
|-
description
:
|-
Required. The resource name of the
memo
to delete.
Required. The resource name of the
inbox
to delete.
Format:
memos/{memo
}
Format:
inboxes/{inbox
}
in: path
in: path
required: true
required: true
type: string
type: string
pattern: memos/[^/]+
pattern: inboxes/[^/]+
-
name
:
force
description
:
Optional. If set to
true
, the memo will be deleted even if it has associated data.
in
:
query
required
:
false
type
:
boolean
tags
:
tags
:
-
Memo
Service
-
Inbox
Service
/api/v1/{name_6}
:
/api/v1/{name_6}
:
get
:
get
:
summary
:
GetWebhook gets a webhook by name.
summary
:
GetWebhook gets a webhook by name.
...
@@ -1356,8 +1351,8 @@ paths:
...
@@ -1356,8 +1351,8 @@ paths:
tags
:
tags
:
-
WebhookService
-
WebhookService
delete
:
delete
:
summary
:
DeleteMemo
Reaction deletes a reaction for
a memo.
summary
:
DeleteMemo
deletes
a memo.
operationId
:
MemoService_DeleteMemo
Reaction
operationId
:
MemoService_DeleteMemo
responses
:
responses
:
"
200"
:
"
200"
:
description
:
A successful response.
description
:
A successful response.
...
@@ -1371,12 +1366,17 @@ paths:
...
@@ -1371,12 +1366,17 @@ paths:
parameters
:
parameters
:
-
name
:
name_6
-
name
:
name_6
description
:
|-
description
:
|-
Required. The resource name of the
reaction
to delete.
Required. The resource name of the
memo
to delete.
Format:
reactions/{reaction
}
Format:
memos/{memo
}
in: path
in: path
required: true
required: true
type: string
type: string
pattern: reactions/[^/]+
pattern: memos/[^/]+
-
name
:
force
description
:
Optional. If set to
true
, the memo will be deleted even if it has associated data.
in
:
query
required
:
false
type
:
boolean
tags
:
tags
:
-
MemoService
-
MemoService
/api/v1/{name_7}
:
/api/v1/{name_7}
:
...
@@ -1403,6 +1403,31 @@ paths:
...
@@ -1403,6 +1403,31 @@ paths:
pattern: workspace/settings/[^/]+
pattern: workspace/settings/[^/]+
tags
:
tags
:
-
WorkspaceService
-
WorkspaceService
delete
:
summary
:
DeleteMemoReaction deletes a reaction for a memo.
operationId
:
MemoService_DeleteMemoReaction
responses
:
"
200"
:
description
:
A successful response.
schema
:
type
:
object
properties
:
{}
default
:
description
:
An unexpected error response.
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
name_7
description
:
|-
Required. The resource name of the reaction to delete.
Format: reactions/{reaction}
in: path
required: true
type: string
pattern: reactions/[^/]+
tags
:
-
MemoService
/api/v1/{name_8}
:
delete
:
delete
:
summary
:
DeleteShortcut deletes a shortcut for a user.
summary
:
DeleteShortcut deletes a shortcut for a user.
operationId
:
ShortcutService_DeleteShortcut
operationId
:
ShortcutService_DeleteShortcut
...
@@ -1417,7 +1442,7 @@ paths:
...
@@ -1417,7 +1442,7 @@ paths:
schema
:
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
parameters
:
-
name
:
name_
7
-
name
:
name_
8
description
:
|-
description
:
|-
Required. The resource name of the shortcut to delete.
Required. The resource name of the shortcut to delete.
Format: users/{user}/shortcuts/{shortcut}
Format: users/{user}/shortcuts/{shortcut}
...
@@ -1427,7 +1452,7 @@ paths:
...
@@ -1427,7 +1452,7 @@ paths:
pattern: users/[^/]+/shortcuts/[^/]+
pattern: users/[^/]+/shortcuts/[^/]+
tags
:
tags
:
-
ShortcutService
-
ShortcutService
/api/v1/{name_
8
}
:
/api/v1/{name_
9
}
:
delete
:
delete
:
summary
:
DeleteWebhook deletes a webhook.
summary
:
DeleteWebhook deletes a webhook.
operationId
:
WebhookService_DeleteWebhook
operationId
:
WebhookService_DeleteWebhook
...
@@ -1442,7 +1467,7 @@ paths:
...
@@ -1442,7 +1467,7 @@ paths:
schema
:
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
parameters
:
-
name
:
name_
8
-
name
:
name_
9
description
:
|-
description
:
|-
Required. The resource name of the webhook to delete.
Required. The resource name of the webhook to delete.
Format: webhooks/{webhook}
Format: webhooks/{webhook}
...
@@ -2054,6 +2079,30 @@ paths:
...
@@ -2054,6 +2079,30 @@ paths:
type: string
type: string
tags
:
tags
:
-
MemoService
-
MemoService
/api/v1/{parent}/sessions
:
get
:
summary
:
ListUserSessions returns a list of active sessions for a user.
operationId
:
UserService_ListUserSessions
responses
:
"
200"
:
description
:
A successful response.
schema
:
$ref
:
'
#/definitions/v1ListUserSessionsResponse'
default
:
description
:
An unexpected error response.
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
parent
description
:
|-
Required. The resource name of the parent.
Format: users/{user}
in: path
required: true
type: string
pattern: users/[^/]+
tags
:
-
UserService
/api/v1/{parent}/shortcuts
:
/api/v1/{parent}/shortcuts
:
get
:
get
:
summary
:
ListShortcuts returns a list of shortcuts for a user.
summary
:
ListShortcuts returns a list of shortcuts for a user.
...
@@ -3676,6 +3725,15 @@ definitions:
...
@@ -3676,6 +3725,15 @@ definitions:
type
:
integer
type
:
integer
format
:
int32
format
:
int32
description
:
The total count of access tokens.
description
:
The total count of access tokens.
v1ListUserSessionsResponse
:
type
:
object
properties
:
sessions
:
type
:
array
items
:
type
:
object
$ref
:
'
#/definitions/v1UserSession'
description
:
The list of user sessions.
v1ListUsersResponse
:
v1ListUsersResponse
:
type
:
object
type
:
object
properties
:
properties
:
...
@@ -4233,6 +4291,58 @@ definitions:
...
@@ -4233,6 +4291,58 @@ definitions:
format
:
date-time
format
:
date-time
description
:
Optional. The expiration timestamp.
description
:
Optional. The expiration timestamp.
title
:
User access token message
title
:
User access token message
v1UserSession
:
type
:
object
properties
:
name
:
type
:
string
title
:
|-
The resource name of the session.
Format: users/{user}/sessions/{session}
sessionId
:
type
:
string
description
:
The session ID.
readOnly
:
true
createTime
:
type
:
string
format
:
date-time
description
:
The timestamp when the session was created.
readOnly
:
true
expireTime
:
type
:
string
format
:
date-time
description
:
The timestamp when the session expires.
readOnly
:
true
lastAccessedTime
:
type
:
string
format
:
date-time
description
:
The timestamp when the session was last accessed.
readOnly
:
true
clientInfo
:
$ref
:
'
#/definitions/v1UserSessionClientInfo'
description
:
Client information associated with this session.
readOnly
:
true
v1UserSessionClientInfo
:
type
:
object
properties
:
userAgent
:
type
:
string
description
:
User agent string of the client.
ipAddress
:
type
:
string
description
:
IP address of the client.
deviceType
:
type
:
string
description
:
Optional. Device type (e.g., "mobile", "desktop", "tablet").
os
:
type
:
string
description
:
Optional. Operating system (e.g., "iOS 17.0", "Windows 11").
browser
:
type
:
string
description
:
Optional. Browser name and version (e.g., "Chrome 119.0").
country
:
type
:
string
description
:
Optional. Geographic location (country code, e.g., "US").
v1UserStats
:
v1UserStats
:
type
:
object
type
:
object
properties
:
properties
:
...
...
server/router/api/v1/auth_service.go
View file @
77b7fc44
...
@@ -15,6 +15,7 @@ import (
...
@@ -15,6 +15,7 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/memos/internal/base"
"github.com/usememos/memos/internal/base"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/internal/util"
...
@@ -176,6 +177,13 @@ func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User, expireTim
...
@@ -176,6 +177,13 @@ func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User, expireTim
return
status
.
Errorf
(
codes
.
Internal
,
"failed to upsert access token to store, error: %v"
,
err
)
return
status
.
Errorf
(
codes
.
Internal
,
"failed to upsert access token to store, error: %v"
,
err
)
}
}
// Track session in user settings
if
err
:=
s
.
trackUserSession
(
ctx
,
user
.
ID
,
accessToken
,
expireTime
);
err
!=
nil
{
// Log the error but don't fail the login if session tracking fails
// This ensures backward compatibility
// TODO: Add proper logging here
}
cookie
,
err
:=
s
.
buildAccessTokenCookie
(
ctx
,
accessToken
,
expireTime
)
cookie
,
err
:=
s
.
buildAccessTokenCookie
(
ctx
,
accessToken
,
expireTime
)
if
err
!=
nil
{
if
err
!=
nil
{
return
status
.
Errorf
(
codes
.
Internal
,
"failed to build access token cookie, error: %v"
,
err
)
return
status
.
Errorf
(
codes
.
Internal
,
"failed to build access token cookie, error: %v"
,
err
)
...
@@ -313,3 +321,41 @@ func (s *APIV1Service) GetCurrentUser(ctx context.Context) (*store.User, error)
...
@@ -313,3 +321,41 @@ func (s *APIV1Service) GetCurrentUser(ctx context.Context) (*store.User, error)
}
}
return
user
,
nil
return
user
,
nil
}
}
// Helper function to track user session for session management
func
(
s
*
APIV1Service
)
trackUserSession
(
ctx
context
.
Context
,
userID
int32
,
sessionID
string
,
expireTime
time
.
Time
)
error
{
// Extract client information from the context
clientInfo
:=
s
.
extractClientInfo
(
ctx
)
session
:=
&
storepb
.
SessionsUserSetting_Session
{
SessionId
:
sessionID
,
CreateTime
:
timestamppb
.
Now
(),
ExpireTime
:
timestamppb
.
New
(
expireTime
),
LastAccessedTime
:
timestamppb
.
Now
(),
ClientInfo
:
clientInfo
,
}
return
s
.
Store
.
AddUserSession
(
ctx
,
userID
,
session
)
}
// Helper function to extract client information from the gRPC context
func
(
s
*
APIV1Service
)
extractClientInfo
(
ctx
context
.
Context
)
*
storepb
.
SessionsUserSetting_ClientInfo
{
clientInfo
:=
&
storepb
.
SessionsUserSetting_ClientInfo
{}
// Extract user agent from metadata if available
if
md
,
ok
:=
metadata
.
FromIncomingContext
(
ctx
);
ok
{
if
userAgents
:=
md
.
Get
(
"user-agent"
);
len
(
userAgents
)
>
0
{
clientInfo
.
UserAgent
=
userAgents
[
0
]
}
if
forwardedFor
:=
md
.
Get
(
"x-forwarded-for"
);
len
(
forwardedFor
)
>
0
{
clientInfo
.
IpAddress
=
forwardedFor
[
0
]
}
else
if
realIP
:=
md
.
Get
(
"x-real-ip"
);
len
(
realIP
)
>
0
{
clientInfo
.
IpAddress
=
realIP
[
0
]
}
}
// TODO: Parse user agent to extract device type, OS, browser info
// This could be done using a user agent parsing library
return
clientInfo
}
server/router/api/v1/user_service.go
View file @
77b7fc44
...
@@ -588,6 +588,108 @@ func (s *APIV1Service) DeleteUserAccessToken(ctx context.Context, request *v1pb.
...
@@ -588,6 +588,108 @@ func (s *APIV1Service) DeleteUserAccessToken(ctx context.Context, request *v1pb.
return
&
emptypb
.
Empty
{},
nil
return
&
emptypb
.
Empty
{},
nil
}
}
func
(
s
*
APIV1Service
)
ListUserSessions
(
ctx
context
.
Context
,
request
*
v1pb
.
ListUserSessionsRequest
)
(
*
v1pb
.
ListUserSessionsResponse
,
error
)
{
userID
,
err
:=
ExtractUserIDFromName
(
request
.
Parent
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid user name: %v"
,
err
)
}
currentUser
,
err
:=
s
.
GetCurrentUser
(
ctx
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get current user: %v"
,
err
)
}
if
currentUser
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
if
currentUser
.
ID
!=
userID
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
userSessions
,
err
:=
s
.
Store
.
GetUserSessions
(
ctx
,
userID
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list sessions: %v"
,
err
)
}
sessions
:=
[]
*
v1pb
.
UserSession
{}
for
_
,
userSession
:=
range
userSessions
{
sessionResponse
:=
&
v1pb
.
UserSession
{
Name
:
fmt
.
Sprintf
(
"users/%d/sessions/%s"
,
userID
,
userSession
.
SessionId
),
SessionId
:
userSession
.
SessionId
,
CreateTime
:
userSession
.
CreateTime
,
ExpireTime
:
userSession
.
ExpireTime
,
LastAccessedTime
:
userSession
.
LastAccessedTime
,
}
if
userSession
.
ClientInfo
!=
nil
{
sessionResponse
.
ClientInfo
=
&
v1pb
.
UserSession_ClientInfo
{
UserAgent
:
userSession
.
ClientInfo
.
UserAgent
,
IpAddress
:
userSession
.
ClientInfo
.
IpAddress
,
DeviceType
:
userSession
.
ClientInfo
.
DeviceType
,
Os
:
userSession
.
ClientInfo
.
Os
,
Browser
:
userSession
.
ClientInfo
.
Browser
,
Country
:
userSession
.
ClientInfo
.
Country
,
}
}
sessions
=
append
(
sessions
,
sessionResponse
)
}
// Sort by last accessed time in descending order.
slices
.
SortFunc
(
sessions
,
func
(
i
,
j
*
v1pb
.
UserSession
)
int
{
return
int
(
j
.
LastAccessedTime
.
Seconds
-
i
.
LastAccessedTime
.
Seconds
)
})
response
:=
&
v1pb
.
ListUserSessionsResponse
{
Sessions
:
sessions
,
}
return
response
,
nil
}
func
(
s
*
APIV1Service
)
RevokeUserSession
(
ctx
context
.
Context
,
request
*
v1pb
.
RevokeUserSessionRequest
)
(
*
emptypb
.
Empty
,
error
)
{
// Extract user ID and session ID from the session resource name
// Format: users/{user}/sessions/{session}
parts
:=
strings
.
Split
(
request
.
Name
,
"/"
)
if
len
(
parts
)
!=
4
||
parts
[
0
]
!=
"users"
||
parts
[
2
]
!=
"sessions"
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid session name format: %s"
,
request
.
Name
)
}
userID
,
err
:=
ExtractUserIDFromName
(
fmt
.
Sprintf
(
"users/%s"
,
parts
[
1
]))
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid user name: %v"
,
err
)
}
sessionIDToRevoke
:=
parts
[
3
]
currentUser
,
err
:=
s
.
GetCurrentUser
(
ctx
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get current user: %v"
,
err
)
}
if
currentUser
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
if
currentUser
.
ID
!=
userID
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
if
err
:=
s
.
Store
.
RemoveUserSession
(
ctx
,
userID
,
sessionIDToRevoke
);
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to revoke session: %v"
,
err
)
}
return
&
emptypb
.
Empty
{},
nil
}
// Helper function to add or update a user session
func
(
s
*
APIV1Service
)
UpsertUserSession
(
ctx
context
.
Context
,
userID
int32
,
sessionID
string
,
clientInfo
*
storepb
.
SessionsUserSetting_ClientInfo
)
error
{
session
:=
&
storepb
.
SessionsUserSetting_Session
{
SessionId
:
sessionID
,
CreateTime
:
timestamppb
.
Now
(),
ExpireTime
:
timestamppb
.
New
(
time
.
Now
()
.
Add
(
30
*
24
*
time
.
Hour
)),
// 30 days default
LastAccessedTime
:
timestamppb
.
Now
(),
ClientInfo
:
clientInfo
,
}
return
s
.
Store
.
AddUserSession
(
ctx
,
userID
,
session
)
}
func
(
s
*
APIV1Service
)
UpsertAccessTokenToStore
(
ctx
context
.
Context
,
user
*
store
.
User
,
accessToken
,
description
string
)
error
{
func
(
s
*
APIV1Service
)
UpsertAccessTokenToStore
(
ctx
context
.
Context
,
user
*
store
.
User
,
accessToken
,
description
string
)
error
{
userAccessTokens
,
err
:=
s
.
Store
.
GetUserAccessTokens
(
ctx
,
user
.
ID
)
userAccessTokens
,
err
:=
s
.
Store
.
GetUserAccessTokens
(
ctx
,
user
.
ID
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -598,6 +700,7 @@ func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store
...
@@ -598,6 +700,7 @@ func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store
Description
:
description
,
Description
:
description
,
}
}
userAccessTokens
=
append
(
userAccessTokens
,
&
userAccessToken
)
userAccessTokens
=
append
(
userAccessTokens
,
&
userAccessToken
)
if
_
,
err
:=
s
.
Store
.
UpsertUserSetting
(
ctx
,
&
storepb
.
UserSetting
{
if
_
,
err
:=
s
.
Store
.
UpsertUserSetting
(
ctx
,
&
storepb
.
UserSetting
{
UserId
:
user
.
ID
,
UserId
:
user
.
ID
,
Key
:
storepb
.
UserSettingKey_ACCESS_TOKENS
,
Key
:
storepb
.
UserSettingKey_ACCESS_TOKENS
,
...
...
store/user_setting.go
View file @
77b7fc44
...
@@ -5,6 +5,7 @@ import (
...
@@ -5,6 +5,7 @@ import (
"github.com/pkg/errors"
"github.com/pkg/errors"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"
storepb
"github.com/usememos/memos/proto/gen/store"
storepb
"github.com/usememos/memos/proto/gen/store"
)
)
...
@@ -132,6 +133,114 @@ func (s *Store) RemoveUserAccessToken(ctx context.Context, userID int32, token s
...
@@ -132,6 +133,114 @@ func (s *Store) RemoveUserAccessToken(ctx context.Context, userID int32, token s
return
err
return
err
}
}
// GetUserSessions returns the sessions of the user.
func
(
s
*
Store
)
GetUserSessions
(
ctx
context
.
Context
,
userID
int32
)
([]
*
storepb
.
SessionsUserSetting_Session
,
error
)
{
userSetting
,
err
:=
s
.
GetUserSetting
(
ctx
,
&
FindUserSetting
{
UserID
:
&
userID
,
Key
:
storepb
.
UserSettingKey_SESSIONS
,
})
if
err
!=
nil
{
return
nil
,
err
}
if
userSetting
==
nil
{
return
[]
*
storepb
.
SessionsUserSetting_Session
{},
nil
}
sessionsUserSetting
:=
userSetting
.
GetSessions
()
return
sessionsUserSetting
.
Sessions
,
nil
}
// RemoveUserSession removes the session of the user.
func
(
s
*
Store
)
RemoveUserSession
(
ctx
context
.
Context
,
userID
int32
,
sessionID
string
)
error
{
oldSessions
,
err
:=
s
.
GetUserSessions
(
ctx
,
userID
)
if
err
!=
nil
{
return
err
}
newSessions
:=
make
([]
*
storepb
.
SessionsUserSetting_Session
,
0
,
len
(
oldSessions
))
for
_
,
session
:=
range
oldSessions
{
if
sessionID
!=
session
.
SessionId
{
newSessions
=
append
(
newSessions
,
session
)
}
}
_
,
err
=
s
.
UpsertUserSetting
(
ctx
,
&
storepb
.
UserSetting
{
UserId
:
userID
,
Key
:
storepb
.
UserSettingKey_SESSIONS
,
Value
:
&
storepb
.
UserSetting_Sessions
{
Sessions
:
&
storepb
.
SessionsUserSetting
{
Sessions
:
newSessions
,
},
},
})
return
err
}
// AddUserSession adds a new session for the user.
func
(
s
*
Store
)
AddUserSession
(
ctx
context
.
Context
,
userID
int32
,
session
*
storepb
.
SessionsUserSetting_Session
)
error
{
existingSessions
,
err
:=
s
.
GetUserSessions
(
ctx
,
userID
)
if
err
!=
nil
{
return
err
}
// Check if session already exists, update if it does
var
updatedSessions
[]
*
storepb
.
SessionsUserSetting_Session
sessionExists
:=
false
for
_
,
existing
:=
range
existingSessions
{
if
existing
.
SessionId
==
session
.
SessionId
{
updatedSessions
=
append
(
updatedSessions
,
session
)
sessionExists
=
true
}
else
{
updatedSessions
=
append
(
updatedSessions
,
existing
)
}
}
// If session doesn't exist, add it
if
!
sessionExists
{
updatedSessions
=
append
(
updatedSessions
,
session
)
}
_
,
err
=
s
.
UpsertUserSetting
(
ctx
,
&
storepb
.
UserSetting
{
UserId
:
userID
,
Key
:
storepb
.
UserSettingKey_SESSIONS
,
Value
:
&
storepb
.
UserSetting_Sessions
{
Sessions
:
&
storepb
.
SessionsUserSetting
{
Sessions
:
updatedSessions
,
},
},
})
return
err
}
// UpdateUserSessionLastAccessed updates the last accessed time of a session.
func
(
s
*
Store
)
UpdateUserSessionLastAccessed
(
ctx
context
.
Context
,
userID
int32
,
sessionID
string
,
lastAccessedTime
*
timestamppb
.
Timestamp
)
error
{
sessions
,
err
:=
s
.
GetUserSessions
(
ctx
,
userID
)
if
err
!=
nil
{
return
err
}
for
_
,
session
:=
range
sessions
{
if
session
.
SessionId
==
sessionID
{
session
.
LastAccessedTime
=
lastAccessedTime
break
}
}
_
,
err
=
s
.
UpsertUserSetting
(
ctx
,
&
storepb
.
UserSetting
{
UserId
:
userID
,
Key
:
storepb
.
UserSettingKey_SESSIONS
,
Value
:
&
storepb
.
UserSetting_Sessions
{
Sessions
:
&
storepb
.
SessionsUserSetting
{
Sessions
:
sessions
,
},
},
})
return
err
}
func
convertUserSettingFromRaw
(
raw
*
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
func
convertUserSettingFromRaw
(
raw
*
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
userSetting
:=
&
storepb
.
UserSetting
{
userSetting
:=
&
storepb
.
UserSetting
{
UserId
:
raw
.
UserID
,
UserId
:
raw
.
UserID
,
...
@@ -145,6 +254,12 @@ func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) {
...
@@ -145,6 +254,12 @@ func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) {
return
nil
,
err
return
nil
,
err
}
}
userSetting
.
Value
=
&
storepb
.
UserSetting_AccessTokens
{
AccessTokens
:
accessTokensUserSetting
}
userSetting
.
Value
=
&
storepb
.
UserSetting_AccessTokens
{
AccessTokens
:
accessTokensUserSetting
}
case
storepb
.
UserSettingKey_SESSIONS
:
sessionsUserSetting
:=
&
storepb
.
SessionsUserSetting
{}
if
err
:=
protojsonUnmarshaler
.
Unmarshal
([]
byte
(
raw
.
Value
),
sessionsUserSetting
);
err
!=
nil
{
return
nil
,
err
}
userSetting
.
Value
=
&
storepb
.
UserSetting_Sessions
{
Sessions
:
sessionsUserSetting
}
case
storepb
.
UserSettingKey_SHORTCUTS
:
case
storepb
.
UserSettingKey_SHORTCUTS
:
shortcutsUserSetting
:=
&
storepb
.
ShortcutsUserSetting
{}
shortcutsUserSetting
:=
&
storepb
.
ShortcutsUserSetting
{}
if
err
:=
protojsonUnmarshaler
.
Unmarshal
([]
byte
(
raw
.
Value
),
shortcutsUserSetting
);
err
!=
nil
{
if
err
:=
protojsonUnmarshaler
.
Unmarshal
([]
byte
(
raw
.
Value
),
shortcutsUserSetting
);
err
!=
nil
{
...
@@ -177,6 +292,13 @@ func convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, er
...
@@ -177,6 +292,13 @@ func convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, er
return
nil
,
err
return
nil
,
err
}
}
raw
.
Value
=
string
(
value
)
raw
.
Value
=
string
(
value
)
case
storepb
.
UserSettingKey_SESSIONS
:
sessionsUserSetting
:=
userSetting
.
GetSessions
()
value
,
err
:=
protojson
.
Marshal
(
sessionsUserSetting
)
if
err
!=
nil
{
return
nil
,
err
}
raw
.
Value
=
string
(
value
)
case
storepb
.
UserSettingKey_SHORTCUTS
:
case
storepb
.
UserSettingKey_SHORTCUTS
:
shortcutsUserSetting
:=
userSetting
.
GetShortcuts
()
shortcutsUserSetting
:=
userSetting
.
GetShortcuts
()
value
,
err
:=
protojson
.
Marshal
(
shortcutsUserSetting
)
value
,
err
:=
protojson
.
Marshal
(
shortcutsUserSetting
)
...
...
web/src/types/proto/api/v1/user_service.ts
View file @
77b7fc44
This diff is collapsed.
Click to expand it.
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