feat: implement user session

pull/4780/head^2
Johnny 5 days ago
parent 741fe35c2a
commit 77b7fc4441

@ -108,6 +108,18 @@ service UserService {
option (google.api.http) = {delete: "/api/v1/{name=users/*/accessTokens/*}"};
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 {
@ -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 {
// Optional. The maximum number of user stats to return.
int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];

@ -1443,6 +1443,234 @@ func (x *DeleteUserAccessTokenRequest) GetName() string {
return ""
}
type UserSession struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The resource name of the session.
// Format: users/{user}/sessions/{session}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// The session ID.
SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
// The timestamp when the session was created.
CreateTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"`
// The timestamp when the session expires.
ExpireTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expire_time,json=expireTime,proto3" json:"expire_time,omitempty"`
// The timestamp when the session was last accessed.
LastAccessedTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_accessed_time,json=lastAccessedTime,proto3" json:"last_accessed_time,omitempty"`
// Client information associated with this session.
ClientInfo *UserSession_ClientInfo `protobuf:"bytes,6,opt,name=client_info,json=clientInfo,proto3" json:"client_info,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserSession) Reset() {
*x = UserSession{}
mi := &file_api_v1_user_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserSession) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserSession) ProtoMessage() {}
func (x *UserSession) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserSession.ProtoReflect.Descriptor instead.
func (*UserSession) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20}
}
func (x *UserSession) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *UserSession) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *UserSession) GetCreateTime() *timestamppb.Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *UserSession) GetExpireTime() *timestamppb.Timestamp {
if x != nil {
return x.ExpireTime
}
return nil
}
func (x *UserSession) GetLastAccessedTime() *timestamppb.Timestamp {
if x != nil {
return x.LastAccessedTime
}
return nil
}
func (x *UserSession) GetClientInfo() *UserSession_ClientInfo {
if x != nil {
return x.ClientInfo
}
return nil
}
type ListUserSessionsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the parent.
// Format: users/{user}
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListUserSessionsRequest) Reset() {
*x = ListUserSessionsRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListUserSessionsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListUserSessionsRequest) ProtoMessage() {}
func (x *ListUserSessionsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListUserSessionsRequest.ProtoReflect.Descriptor instead.
func (*ListUserSessionsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{21}
}
func (x *ListUserSessionsRequest) GetParent() string {
if x != nil {
return x.Parent
}
return ""
}
type ListUserSessionsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The list of user sessions.
Sessions []*UserSession `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListUserSessionsResponse) Reset() {
*x = ListUserSessionsResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListUserSessionsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListUserSessionsResponse) ProtoMessage() {}
func (x *ListUserSessionsResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListUserSessionsResponse.ProtoReflect.Descriptor instead.
func (*ListUserSessionsResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{22}
}
func (x *ListUserSessionsResponse) GetSessions() []*UserSession {
if x != nil {
return x.Sessions
}
return nil
}
type RevokeUserSessionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the session to revoke.
// Format: users/{user}/sessions/{session}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RevokeUserSessionRequest) Reset() {
*x = RevokeUserSessionRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RevokeUserSessionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RevokeUserSessionRequest) ProtoMessage() {}
func (x *RevokeUserSessionRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RevokeUserSessionRequest.ProtoReflect.Descriptor instead.
func (*RevokeUserSessionRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{23}
}
func (x *RevokeUserSessionRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type ListAllUserStatsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Optional. The maximum number of user stats to return.
@ -1455,7 +1683,7 @@ type ListAllUserStatsRequest struct {
func (x *ListAllUserStatsRequest) Reset() {
*x = ListAllUserStatsRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[20]
mi := &file_api_v1_user_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1467,7 +1695,7 @@ func (x *ListAllUserStatsRequest) String() string {
func (*ListAllUserStatsRequest) ProtoMessage() {}
func (x *ListAllUserStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[20]
mi := &file_api_v1_user_service_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1480,7 +1708,7 @@ func (x *ListAllUserStatsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAllUserStatsRequest.ProtoReflect.Descriptor instead.
func (*ListAllUserStatsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{24}
}
func (x *ListAllUserStatsRequest) GetPageSize() int32 {
@ -1511,7 +1739,7 @@ type ListAllUserStatsResponse struct {
func (x *ListAllUserStatsResponse) Reset() {
*x = ListAllUserStatsResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[21]
mi := &file_api_v1_user_service_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1523,7 +1751,7 @@ func (x *ListAllUserStatsResponse) String() string {
func (*ListAllUserStatsResponse) ProtoMessage() {}
func (x *ListAllUserStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[21]
mi := &file_api_v1_user_service_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1536,7 +1764,7 @@ func (x *ListAllUserStatsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAllUserStatsResponse.ProtoReflect.Descriptor instead.
func (*ListAllUserStatsResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{21}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{25}
}
func (x *ListAllUserStatsResponse) GetUserStats() []*UserStats {
@ -1573,7 +1801,7 @@ type UserStats_MemoTypeStats struct {
func (x *UserStats_MemoTypeStats) Reset() {
*x = UserStats_MemoTypeStats{}
mi := &file_api_v1_user_service_proto_msgTypes[23]
mi := &file_api_v1_user_service_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1585,7 +1813,7 @@ func (x *UserStats_MemoTypeStats) String() string {
func (*UserStats_MemoTypeStats) ProtoMessage() {}
func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[23]
mi := &file_api_v1_user_service_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1629,6 +1857,96 @@ func (x *UserStats_MemoTypeStats) GetUndoCount() int32 {
return 0
}
type UserSession_ClientInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
// User agent string of the client.
UserAgent string `protobuf:"bytes,1,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"`
// IP address of the client.
IpAddress string `protobuf:"bytes,2,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"`
// Optional. Device type (e.g., "mobile", "desktop", "tablet").
DeviceType string `protobuf:"bytes,3,opt,name=device_type,json=deviceType,proto3" json:"device_type,omitempty"`
// Optional. Operating system (e.g., "iOS 17.0", "Windows 11").
Os string `protobuf:"bytes,4,opt,name=os,proto3" json:"os,omitempty"`
// Optional. Browser name and version (e.g., "Chrome 119.0").
Browser string `protobuf:"bytes,5,opt,name=browser,proto3" json:"browser,omitempty"`
// Optional. Geographic location (country code, e.g., "US").
Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserSession_ClientInfo) Reset() {
*x = UserSession_ClientInfo{}
mi := &file_api_v1_user_service_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserSession_ClientInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserSession_ClientInfo) ProtoMessage() {}
func (x *UserSession_ClientInfo) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserSession_ClientInfo.ProtoReflect.Descriptor instead.
func (*UserSession_ClientInfo) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20, 0}
}
func (x *UserSession_ClientInfo) GetUserAgent() string {
if x != nil {
return x.UserAgent
}
return ""
}
func (x *UserSession_ClientInfo) GetIpAddress() string {
if x != nil {
return x.IpAddress
}
return ""
}
func (x *UserSession_ClientInfo) GetDeviceType() string {
if x != nil {
return x.DeviceType
}
return ""
}
func (x *UserSession_ClientInfo) GetOs() string {
if x != nil {
return x.Os
}
return ""
}
func (x *UserSession_ClientInfo) GetBrowser() string {
if x != nil {
return x.Browser
}
return ""
}
func (x *UserSession_ClientInfo) GetCountry() string {
if x != nil {
return x.Country
}
return ""
}
var File_api_v1_user_service_proto protoreflect.FileDescriptor
const file_api_v1_user_service_proto_rawDesc = "" +
@ -1766,7 +2084,38 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\x0faccess_token_id\x18\x03 \x01(\tB\x03\xe0A\x01R\raccessTokenId\"X\n" +
"\x1cDeleteUserAccessTokenRequest\x128\n" +
"\x04name\x18\x01 \x01(\tB$\xe0A\x02\xfaA\x1e\n" +
"\x1cmemos.api.v1/UserAccessTokenR\x04name\"_\n" +
"\x1cmemos.api.v1/UserAccessTokenR\x04name\"\xf5\x04\n" +
"\vUserSession\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\"\n" +
"\n" +
"session_id\x18\x02 \x01(\tB\x03\xe0A\x03R\tsessionId\x12@\n" +
"\vcreate_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" +
"createTime\x12@\n" +
"\vexpire_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" +
"expireTime\x12M\n" +
"\x12last_accessed_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\x10lastAccessedTime\x12J\n" +
"\vclient_info\x18\x06 \x01(\v2$.memos.api.v1.UserSession.ClientInfoB\x03\xe0A\x03R\n" +
"clientInfo\x1a\xc3\x01\n" +
"\n" +
"ClientInfo\x12\x1d\n" +
"\n" +
"user_agent\x18\x01 \x01(\tR\tuserAgent\x12\x1d\n" +
"\n" +
"ip_address\x18\x02 \x01(\tR\tipAddress\x12$\n" +
"\vdevice_type\x18\x03 \x01(\tB\x03\xe0A\x01R\n" +
"deviceType\x12\x13\n" +
"\x02os\x18\x04 \x01(\tB\x03\xe0A\x01R\x02os\x12\x1d\n" +
"\abrowser\x18\x05 \x01(\tB\x03\xe0A\x01R\abrowser\x12\x1d\n" +
"\acountry\x18\x06 \x01(\tB\x03\xe0A\x01R\acountry:D\xeaAA\n" +
"\x18memos.api.v1/UserSession\x12\x1fusers/{user}/sessions/{session}\x1a\x04name\"L\n" +
"\x17ListUserSessionsRequest\x121\n" +
"\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x06parent\"Q\n" +
"\x18ListUserSessionsResponse\x125\n" +
"\bsessions\x18\x01 \x03(\v2\x19.memos.api.v1.UserSessionR\bsessions\"P\n" +
"\x18RevokeUserSessionRequest\x124\n" +
"\x04name\x18\x01 \x01(\tB \xe0A\x02\xfaA\x1a\n" +
"\x18memos.api.v1/UserSessionR\x04name\"_\n" +
"\x17ListAllUserStatsRequest\x12 \n" +
"\tpage_size\x18\x01 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" +
"\n" +
@ -1776,7 +2125,7 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"user_stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\tuserStats\x12&\n" +
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" +
"\n" +
"total_size\x18\x03 \x01(\x05R\ttotalSize2\xc2\x0e\n" +
"total_size\x18\x03 \x01(\x05R\ttotalSize2\xe2\x10\n" +
"\vUserService\x12c\n" +
"\tListUsers\x12\x1e.memos.api.v1.ListUsersRequest\x1a\x1f.memos.api.v1.ListUsersResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/users\x12b\n" +
"\aGetUser\x12\x1c.memos.api.v1.GetUserRequest\x1a\x12.memos.api.v1.User\"%\xdaA\x04name\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/{name=users/*}\x12e\n" +
@ -1794,7 +2143,9 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\x11UpdateUserSetting\x12&.memos.api.v1.UpdateUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"S\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x027:\asetting2,/api/v1/{setting.name=users/*}:updateSetting\x12\xa5\x01\n" +
"\x14ListUserAccessTokens\x12).memos.api.v1.ListUserAccessTokensRequest\x1a*.memos.api.v1.ListUserAccessTokensResponse\"6\xdaA\x06parent\x82\xd3\xe4\x93\x02'\x12%/api/v1/{parent=users/*}/accessTokens\x12\xb5\x01\n" +
"\x15CreateUserAccessToken\x12*.memos.api.v1.CreateUserAccessTokenRequest\x1a\x1d.memos.api.v1.UserAccessToken\"Q\xdaA\x13parent,access_token\x82\xd3\xe4\x93\x025:\faccess_token\"%/api/v1/{parent=users/*}/accessTokens\x12\x91\x01\n" +
"\x15DeleteUserAccessToken\x12*.memos.api.v1.DeleteUserAccessTokenRequest\x1a\x16.google.protobuf.Empty\"4\xdaA\x04name\x82\xd3\xe4\x93\x02'*%/api/v1/{name=users/*/accessTokens/*}B\xa8\x01\n" +
"\x15DeleteUserAccessToken\x12*.memos.api.v1.DeleteUserAccessTokenRequest\x1a\x16.google.protobuf.Empty\"4\xdaA\x04name\x82\xd3\xe4\x93\x02'*%/api/v1/{name=users/*/accessTokens/*}\x12\x95\x01\n" +
"\x10ListUserSessions\x12%.memos.api.v1.ListUserSessionsRequest\x1a&.memos.api.v1.ListUserSessionsResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/sessions\x12\x85\x01\n" +
"\x11RevokeUserSession\x12&.memos.api.v1.RevokeUserSessionRequest\x1a\x16.google.protobuf.Empty\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#*!/api/v1/{name=users/*/sessions/*}B\xa8\x01\n" +
"\x10com.memos.api.v1B\x10UserServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3"
var (
@ -1810,7 +2161,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte {
}
var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
var file_api_v1_user_service_proto_goTypes = []any{
(User_Role)(0), // 0: memos.api.v1.User.Role
(*User)(nil), // 1: memos.api.v1.User
@ -1833,70 +2184,84 @@ var file_api_v1_user_service_proto_goTypes = []any{
(*ListUserAccessTokensResponse)(nil), // 18: memos.api.v1.ListUserAccessTokensResponse
(*CreateUserAccessTokenRequest)(nil), // 19: memos.api.v1.CreateUserAccessTokenRequest
(*DeleteUserAccessTokenRequest)(nil), // 20: memos.api.v1.DeleteUserAccessTokenRequest
(*ListAllUserStatsRequest)(nil), // 21: memos.api.v1.ListAllUserStatsRequest
(*ListAllUserStatsResponse)(nil), // 22: memos.api.v1.ListAllUserStatsResponse
nil, // 23: memos.api.v1.UserStats.TagCountEntry
(*UserStats_MemoTypeStats)(nil), // 24: memos.api.v1.UserStats.MemoTypeStats
(State)(0), // 25: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 26: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 27: google.protobuf.FieldMask
(*emptypb.Empty)(nil), // 28: google.protobuf.Empty
(*httpbody.HttpBody)(nil), // 29: google.api.HttpBody
(*UserSession)(nil), // 21: memos.api.v1.UserSession
(*ListUserSessionsRequest)(nil), // 22: memos.api.v1.ListUserSessionsRequest
(*ListUserSessionsResponse)(nil), // 23: memos.api.v1.ListUserSessionsResponse
(*RevokeUserSessionRequest)(nil), // 24: memos.api.v1.RevokeUserSessionRequest
(*ListAllUserStatsRequest)(nil), // 25: memos.api.v1.ListAllUserStatsRequest
(*ListAllUserStatsResponse)(nil), // 26: memos.api.v1.ListAllUserStatsResponse
nil, // 27: memos.api.v1.UserStats.TagCountEntry
(*UserStats_MemoTypeStats)(nil), // 28: memos.api.v1.UserStats.MemoTypeStats
(*UserSession_ClientInfo)(nil), // 29: memos.api.v1.UserSession.ClientInfo
(State)(0), // 30: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 32: google.protobuf.FieldMask
(*emptypb.Empty)(nil), // 33: google.protobuf.Empty
(*httpbody.HttpBody)(nil), // 34: google.api.HttpBody
}
var file_api_v1_user_service_proto_depIdxs = []int32{
0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role
25, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
26, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
26, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
30, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
31, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
31, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
1, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User
27, // 5: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
32, // 5: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
1, // 6: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
1, // 7: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
27, // 8: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
32, // 8: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
1, // 9: memos.api.v1.SearchUsersResponse.users:type_name -> memos.api.v1.User
26, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
24, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
23, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
31, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
28, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
27, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
13, // 13: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
27, // 14: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
26, // 15: memos.api.v1.UserAccessToken.issued_at:type_name -> google.protobuf.Timestamp
26, // 16: memos.api.v1.UserAccessToken.expires_at:type_name -> google.protobuf.Timestamp
32, // 14: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
31, // 15: memos.api.v1.UserAccessToken.issued_at:type_name -> google.protobuf.Timestamp
31, // 16: memos.api.v1.UserAccessToken.expires_at:type_name -> google.protobuf.Timestamp
16, // 17: memos.api.v1.ListUserAccessTokensResponse.access_tokens:type_name -> memos.api.v1.UserAccessToken
16, // 18: memos.api.v1.CreateUserAccessTokenRequest.access_token:type_name -> memos.api.v1.UserAccessToken
11, // 19: memos.api.v1.ListAllUserStatsResponse.user_stats:type_name -> memos.api.v1.UserStats
2, // 20: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
4, // 21: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
5, // 22: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
6, // 23: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
7, // 24: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
8, // 25: memos.api.v1.UserService.SearchUsers:input_type -> memos.api.v1.SearchUsersRequest
10, // 26: memos.api.v1.UserService.GetUserAvatar:input_type -> memos.api.v1.GetUserAvatarRequest
21, // 27: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
12, // 28: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
14, // 29: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
15, // 30: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
17, // 31: memos.api.v1.UserService.ListUserAccessTokens:input_type -> memos.api.v1.ListUserAccessTokensRequest
19, // 32: memos.api.v1.UserService.CreateUserAccessToken:input_type -> memos.api.v1.CreateUserAccessTokenRequest
20, // 33: memos.api.v1.UserService.DeleteUserAccessToken:input_type -> memos.api.v1.DeleteUserAccessTokenRequest
3, // 34: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
1, // 35: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
1, // 36: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
1, // 37: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
28, // 38: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
9, // 39: memos.api.v1.UserService.SearchUsers:output_type -> memos.api.v1.SearchUsersResponse
29, // 40: memos.api.v1.UserService.GetUserAvatar:output_type -> google.api.HttpBody
22, // 41: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
11, // 42: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
13, // 43: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
13, // 44: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
18, // 45: memos.api.v1.UserService.ListUserAccessTokens:output_type -> memos.api.v1.ListUserAccessTokensResponse
16, // 46: memos.api.v1.UserService.CreateUserAccessToken:output_type -> memos.api.v1.UserAccessToken
28, // 47: memos.api.v1.UserService.DeleteUserAccessToken:output_type -> google.protobuf.Empty
34, // [34:48] is the sub-list for method output_type
20, // [20:34] is the sub-list for method input_type
20, // [20:20] is the sub-list for extension type_name
20, // [20:20] is the sub-list for extension extendee
0, // [0:20] is the sub-list for field type_name
31, // 19: memos.api.v1.UserSession.create_time:type_name -> google.protobuf.Timestamp
31, // 20: memos.api.v1.UserSession.expire_time:type_name -> google.protobuf.Timestamp
31, // 21: memos.api.v1.UserSession.last_accessed_time:type_name -> google.protobuf.Timestamp
29, // 22: memos.api.v1.UserSession.client_info:type_name -> memos.api.v1.UserSession.ClientInfo
21, // 23: memos.api.v1.ListUserSessionsResponse.sessions:type_name -> memos.api.v1.UserSession
11, // 24: memos.api.v1.ListAllUserStatsResponse.user_stats:type_name -> memos.api.v1.UserStats
2, // 25: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
4, // 26: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
5, // 27: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
6, // 28: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
7, // 29: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
8, // 30: memos.api.v1.UserService.SearchUsers:input_type -> memos.api.v1.SearchUsersRequest
10, // 31: memos.api.v1.UserService.GetUserAvatar:input_type -> memos.api.v1.GetUserAvatarRequest
25, // 32: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
12, // 33: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
14, // 34: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
15, // 35: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
17, // 36: memos.api.v1.UserService.ListUserAccessTokens:input_type -> memos.api.v1.ListUserAccessTokensRequest
19, // 37: memos.api.v1.UserService.CreateUserAccessToken:input_type -> memos.api.v1.CreateUserAccessTokenRequest
20, // 38: memos.api.v1.UserService.DeleteUserAccessToken:input_type -> memos.api.v1.DeleteUserAccessTokenRequest
22, // 39: memos.api.v1.UserService.ListUserSessions:input_type -> memos.api.v1.ListUserSessionsRequest
24, // 40: memos.api.v1.UserService.RevokeUserSession:input_type -> memos.api.v1.RevokeUserSessionRequest
3, // 41: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
1, // 42: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
1, // 43: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
1, // 44: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
33, // 45: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
9, // 46: memos.api.v1.UserService.SearchUsers:output_type -> memos.api.v1.SearchUsersResponse
34, // 47: memos.api.v1.UserService.GetUserAvatar:output_type -> google.api.HttpBody
26, // 48: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
11, // 49: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
13, // 50: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
13, // 51: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
18, // 52: memos.api.v1.UserService.ListUserAccessTokens:output_type -> memos.api.v1.ListUserAccessTokensResponse
16, // 53: memos.api.v1.UserService.CreateUserAccessToken:output_type -> memos.api.v1.UserAccessToken
33, // 54: memos.api.v1.UserService.DeleteUserAccessToken:output_type -> google.protobuf.Empty
23, // 55: memos.api.v1.UserService.ListUserSessions:output_type -> memos.api.v1.ListUserSessionsResponse
33, // 56: memos.api.v1.UserService.RevokeUserSession:output_type -> google.protobuf.Empty
41, // [41:57] is the sub-list for method output_type
25, // [25:41] is the sub-list for method input_type
25, // [25:25] is the sub-list for extension type_name
25, // [25:25] is the sub-list for extension extendee
0, // [0:25] is the sub-list for field type_name
}
func init() { file_api_v1_user_service_proto_init() }
@ -1911,7 +2276,7 @@ func file_api_v1_user_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)),
NumEnums: 1,
NumMessages: 24,
NumMessages: 29,
NumExtensions: 0,
NumServices: 1,
},

@ -717,6 +717,84 @@ func local_request_UserService_DeleteUserAccessToken_0(ctx context.Context, mars
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".
// UnaryRPC :call UserServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@ -1003,6 +1081,46 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
}
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
}
@ -1281,6 +1399,40 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
}
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
}
@ -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_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_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 (
@ -1316,4 +1470,6 @@ var (
forward_UserService_ListUserAccessTokens_0 = runtime.ForwardResponseMessage
forward_UserService_CreateUserAccessToken_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUserAccessToken_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserSessions_0 = runtime.ForwardResponseMessage
forward_UserService_RevokeUserSession_0 = runtime.ForwardResponseMessage
)

@ -35,6 +35,8 @@ const (
UserService_ListUserAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListUserAccessTokens"
UserService_CreateUserAccessToken_FullMethodName = "/memos.api.v1.UserService/CreateUserAccessToken"
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.
@ -69,6 +71,10 @@ type UserServiceClient interface {
CreateUserAccessToken(ctx context.Context, in *CreateUserAccessTokenRequest, opts ...grpc.CallOption) (*UserAccessToken, error)
// DeleteUserAccessToken deletes an access token.
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 {
@ -219,6 +225,26 @@ func (c *userServiceClient) DeleteUserAccessToken(ctx context.Context, in *Delet
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.
// All implementations must embed UnimplementedUserServiceServer
// for forward compatibility.
@ -251,6 +277,10 @@ type UserServiceServer interface {
CreateUserAccessToken(context.Context, *CreateUserAccessTokenRequest) (*UserAccessToken, error)
// DeleteUserAccessToken deletes an access token.
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()
}
@ -303,6 +333,12 @@ func (UnimplementedUserServiceServer) CreateUserAccessToken(context.Context, *Cr
func (UnimplementedUserServiceServer) DeleteUserAccessToken(context.Context, *DeleteUserAccessTokenRequest) (*emptypb.Empty, error) {
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) testEmbeddedByValue() {}
@ -576,6 +612,42 @@ func _UserService_DeleteUserAccessToken_Handler(srv interface{}, ctx context.Con
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.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -639,6 +711,14 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "DeleteUserAccessToken",
Handler: _UserService_DeleteUserAccessToken_Handler,
},
{
MethodName: "ListUserSessions",
Handler: _UserService_ListUserSessions_Handler,
},
{
MethodName: "RevokeUserSession",
Handler: _UserService_RevokeUserSession_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/v1/user_service.proto",

@ -1193,8 +1193,8 @@ paths:
tags:
- IdentityProviderService
delete:
summary: DeleteIdentityProvider deletes an identity provider.
operationId: IdentityProviderService_DeleteIdentityProvider
summary: RevokeUserSession revokes a specific session for a user.
operationId: UserService_RevokeUserSession
responses:
"200":
description: A successful response.
@ -1208,14 +1208,14 @@ paths:
parameters:
- name: name_3
description: |-
Required. The resource name of the identity provider to delete.
Format: identityProviders/{idp}
Required. The resource name of the session to revoke.
Format: users/{user}/sessions/{session}
in: path
required: true
type: string
pattern: identityProviders/[^/]+
pattern: users/[^/]+/sessions/[^/]+
tags:
- IdentityProviderService
- UserService
/api/v1/{name_4}:
get:
summary: GetMemo gets a memo.
@ -1248,8 +1248,8 @@ paths:
tags:
- MemoService
delete:
summary: DeleteInbox deletes an inbox.
operationId: InboxService_DeleteInbox
summary: DeleteIdentityProvider deletes an identity provider.
operationId: IdentityProviderService_DeleteIdentityProvider
responses:
"200":
description: A successful response.
@ -1263,14 +1263,14 @@ paths:
parameters:
- name: name_4
description: |-
Required. The resource name of the inbox to delete.
Format: inboxes/{inbox}
Required. The resource name of the identity provider to delete.
Format: identityProviders/{idp}
in: path
required: true
type: string
pattern: inboxes/[^/]+
pattern: identityProviders/[^/]+
tags:
- InboxService
- IdentityProviderService
/api/v1/{name_5}:
get:
summary: GetShortcut gets a shortcut by name.
@ -1296,8 +1296,8 @@ paths:
tags:
- ShortcutService
delete:
summary: DeleteMemo deletes a memo.
operationId: MemoService_DeleteMemo
summary: DeleteInbox deletes an inbox.
operationId: InboxService_DeleteInbox
responses:
"200":
description: A successful response.
@ -1311,19 +1311,14 @@ paths:
parameters:
- name: name_5
description: |-
Required. The resource name of the memo to delete.
Format: memos/{memo}
Required. The resource name of the inbox to delete.
Format: inboxes/{inbox}
in: path
required: true
type: string
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
pattern: inboxes/[^/]+
tags:
- MemoService
- InboxService
/api/v1/{name_6}:
get:
summary: GetWebhook gets a webhook by name.
@ -1356,8 +1351,8 @@ paths:
tags:
- WebhookService
delete:
summary: DeleteMemoReaction deletes a reaction for a memo.
operationId: MemoService_DeleteMemoReaction
summary: DeleteMemo deletes a memo.
operationId: MemoService_DeleteMemo
responses:
"200":
description: A successful response.
@ -1371,12 +1366,17 @@ paths:
parameters:
- name: name_6
description: |-
Required. The resource name of the reaction to delete.
Format: reactions/{reaction}
Required. The resource name of the memo to delete.
Format: memos/{memo}
in: path
required: true
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:
- MemoService
/api/v1/{name_7}:
@ -1403,6 +1403,31 @@ paths:
pattern: workspace/settings/[^/]+
tags:
- 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:
summary: DeleteShortcut deletes a shortcut for a user.
operationId: ShortcutService_DeleteShortcut
@ -1417,7 +1442,7 @@ paths:
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: name_7
- name: name_8
description: |-
Required. The resource name of the shortcut to delete.
Format: users/{user}/shortcuts/{shortcut}
@ -1427,7 +1452,7 @@ paths:
pattern: users/[^/]+/shortcuts/[^/]+
tags:
- ShortcutService
/api/v1/{name_8}:
/api/v1/{name_9}:
delete:
summary: DeleteWebhook deletes a webhook.
operationId: WebhookService_DeleteWebhook
@ -1442,7 +1467,7 @@ paths:
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: name_8
- name: name_9
description: |-
Required. The resource name of the webhook to delete.
Format: webhooks/{webhook}
@ -2054,6 +2079,30 @@ paths:
type: string
tags:
- 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:
get:
summary: ListShortcuts returns a list of shortcuts for a user.
@ -3676,6 +3725,15 @@ definitions:
type: integer
format: int32
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:
type: object
properties:
@ -4233,6 +4291,58 @@ definitions:
format: date-time
description: Optional. The expiration timestamp.
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:
type: object
properties:

@ -15,6 +15,7 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"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/util"
@ -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)
}
// 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)
if err != nil {
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)
}
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
}

@ -588,6 +588,108 @@ func (s *APIV1Service) DeleteUserAccessToken(ctx context.Context, request *v1pb.
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 {
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
if err != nil {
@ -598,6 +700,7 @@ func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store
Description: description,
}
userAccessTokens = append(userAccessTokens, &userAccessToken)
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSettingKey_ACCESS_TOKENS,

@ -5,6 +5,7 @@ import (
"github.com/pkg/errors"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"
storepb "github.com/usememos/memos/proto/gen/store"
)
@ -132,6 +133,114 @@ func (s *Store) RemoveUserAccessToken(ctx context.Context, userID int32, token s
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) {
userSetting := &storepb.UserSetting{
UserId: raw.UserID,
@ -145,6 +254,12 @@ func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) {
return nil, err
}
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:
shortcutsUserSetting := &storepb.ShortcutsUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), shortcutsUserSetting); err != nil {
@ -177,6 +292,13 @@ func convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, er
return nil, err
}
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:
shortcutsUserSetting := userSetting.GetShortcuts()
value, err := protojson.Marshal(shortcutsUserSetting)

@ -359,6 +359,66 @@ export interface DeleteUserAccessTokenRequest {
name: string;
}
export interface UserSession {
/**
* The resource name of the session.
* Format: users/{user}/sessions/{session}
*/
name: string;
/** The session ID. */
sessionId: string;
/** The timestamp when the session was created. */
createTime?:
| Date
| undefined;
/** The timestamp when the session expires. */
expireTime?:
| Date
| undefined;
/** The timestamp when the session was last accessed. */
lastAccessedTime?:
| Date
| undefined;
/** Client information associated with this session. */
clientInfo?: UserSession_ClientInfo | undefined;
}
export interface UserSession_ClientInfo {
/** User agent string of the client. */
userAgent: string;
/** IP address of the client. */
ipAddress: string;
/** Optional. Device type (e.g., "mobile", "desktop", "tablet"). */
deviceType: string;
/** Optional. Operating system (e.g., "iOS 17.0", "Windows 11"). */
os: string;
/** Optional. Browser name and version (e.g., "Chrome 119.0"). */
browser: string;
/** Optional. Geographic location (country code, e.g., "US"). */
country: string;
}
export interface ListUserSessionsRequest {
/**
* Required. The resource name of the parent.
* Format: users/{user}
*/
parent: string;
}
export interface ListUserSessionsResponse {
/** The list of user sessions. */
sessions: UserSession[];
}
export interface RevokeUserSessionRequest {
/**
* Required. The resource name of the session to revoke.
* Format: users/{user}/sessions/{session}
*/
name: string;
}
export interface ListAllUserStatsRequest {
/** Optional. The maximum number of user stats to return. */
pageSize: number;
@ -2046,6 +2106,365 @@ export const DeleteUserAccessTokenRequest: MessageFns<DeleteUserAccessTokenReque
},
};
function createBaseUserSession(): UserSession {
return {
name: "",
sessionId: "",
createTime: undefined,
expireTime: undefined,
lastAccessedTime: undefined,
clientInfo: undefined,
};
}
export const UserSession: MessageFns<UserSession> = {
encode(message: UserSession, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.name !== "") {
writer.uint32(10).string(message.name);
}
if (message.sessionId !== "") {
writer.uint32(18).string(message.sessionId);
}
if (message.createTime !== undefined) {
Timestamp.encode(toTimestamp(message.createTime), writer.uint32(26).fork()).join();
}
if (message.expireTime !== undefined) {
Timestamp.encode(toTimestamp(message.expireTime), writer.uint32(34).fork()).join();
}
if (message.lastAccessedTime !== undefined) {
Timestamp.encode(toTimestamp(message.lastAccessedTime), writer.uint32(42).fork()).join();
}
if (message.clientInfo !== undefined) {
UserSession_ClientInfo.encode(message.clientInfo, writer.uint32(50).fork()).join();
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): UserSession {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseUserSession();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.name = reader.string();
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.sessionId = reader.string();
continue;
}
case 3: {
if (tag !== 26) {
break;
}
message.createTime = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
}
case 4: {
if (tag !== 34) {
break;
}
message.expireTime = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
}
case 5: {
if (tag !== 42) {
break;
}
message.lastAccessedTime = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
}
case 6: {
if (tag !== 50) {
break;
}
message.clientInfo = UserSession_ClientInfo.decode(reader, reader.uint32());
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
create(base?: DeepPartial<UserSession>): UserSession {
return UserSession.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<UserSession>): UserSession {
const message = createBaseUserSession();
message.name = object.name ?? "";
message.sessionId = object.sessionId ?? "";
message.createTime = object.createTime ?? undefined;
message.expireTime = object.expireTime ?? undefined;
message.lastAccessedTime = object.lastAccessedTime ?? undefined;
message.clientInfo = (object.clientInfo !== undefined && object.clientInfo !== null)
? UserSession_ClientInfo.fromPartial(object.clientInfo)
: undefined;
return message;
},
};
function createBaseUserSession_ClientInfo(): UserSession_ClientInfo {
return { userAgent: "", ipAddress: "", deviceType: "", os: "", browser: "", country: "" };
}
export const UserSession_ClientInfo: MessageFns<UserSession_ClientInfo> = {
encode(message: UserSession_ClientInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.userAgent !== "") {
writer.uint32(10).string(message.userAgent);
}
if (message.ipAddress !== "") {
writer.uint32(18).string(message.ipAddress);
}
if (message.deviceType !== "") {
writer.uint32(26).string(message.deviceType);
}
if (message.os !== "") {
writer.uint32(34).string(message.os);
}
if (message.browser !== "") {
writer.uint32(42).string(message.browser);
}
if (message.country !== "") {
writer.uint32(50).string(message.country);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): UserSession_ClientInfo {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseUserSession_ClientInfo();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.userAgent = reader.string();
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.ipAddress = reader.string();
continue;
}
case 3: {
if (tag !== 26) {
break;
}
message.deviceType = reader.string();
continue;
}
case 4: {
if (tag !== 34) {
break;
}
message.os = reader.string();
continue;
}
case 5: {
if (tag !== 42) {
break;
}
message.browser = reader.string();
continue;
}
case 6: {
if (tag !== 50) {
break;
}
message.country = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
create(base?: DeepPartial<UserSession_ClientInfo>): UserSession_ClientInfo {
return UserSession_ClientInfo.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<UserSession_ClientInfo>): UserSession_ClientInfo {
const message = createBaseUserSession_ClientInfo();
message.userAgent = object.userAgent ?? "";
message.ipAddress = object.ipAddress ?? "";
message.deviceType = object.deviceType ?? "";
message.os = object.os ?? "";
message.browser = object.browser ?? "";
message.country = object.country ?? "";
return message;
},
};
function createBaseListUserSessionsRequest(): ListUserSessionsRequest {
return { parent: "" };
}
export const ListUserSessionsRequest: MessageFns<ListUserSessionsRequest> = {
encode(message: ListUserSessionsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.parent !== "") {
writer.uint32(10).string(message.parent);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): ListUserSessionsRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseListUserSessionsRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.parent = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
create(base?: DeepPartial<ListUserSessionsRequest>): ListUserSessionsRequest {
return ListUserSessionsRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<ListUserSessionsRequest>): ListUserSessionsRequest {
const message = createBaseListUserSessionsRequest();
message.parent = object.parent ?? "";
return message;
},
};
function createBaseListUserSessionsResponse(): ListUserSessionsResponse {
return { sessions: [] };
}
export const ListUserSessionsResponse: MessageFns<ListUserSessionsResponse> = {
encode(message: ListUserSessionsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.sessions) {
UserSession.encode(v!, writer.uint32(10).fork()).join();
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): ListUserSessionsResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseListUserSessionsResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.sessions.push(UserSession.decode(reader, reader.uint32()));
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
create(base?: DeepPartial<ListUserSessionsResponse>): ListUserSessionsResponse {
return ListUserSessionsResponse.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<ListUserSessionsResponse>): ListUserSessionsResponse {
const message = createBaseListUserSessionsResponse();
message.sessions = object.sessions?.map((e) => UserSession.fromPartial(e)) || [];
return message;
},
};
function createBaseRevokeUserSessionRequest(): RevokeUserSessionRequest {
return { name: "" };
}
export const RevokeUserSessionRequest: MessageFns<RevokeUserSessionRequest> = {
encode(message: RevokeUserSessionRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.name !== "") {
writer.uint32(10).string(message.name);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): RevokeUserSessionRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseRevokeUserSessionRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.name = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
create(base?: DeepPartial<RevokeUserSessionRequest>): RevokeUserSessionRequest {
return RevokeUserSessionRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<RevokeUserSessionRequest>): RevokeUserSessionRequest {
const message = createBaseRevokeUserSessionRequest();
message.name = object.name ?? "";
return message;
},
};
function createBaseListAllUserStatsRequest(): ListAllUserStatsRequest {
return { pageSize: 0, pageToken: "" };
}
@ -2903,6 +3322,112 @@ export const UserServiceDefinition = {
},
},
},
/** ListUserSessions returns a list of active sessions for a user. */
listUserSessions: {
name: "ListUserSessions",
requestType: ListUserSessionsRequest,
requestStream: false,
responseType: ListUserSessionsResponse,
responseStream: false,
options: {
_unknownFields: {
8410: [new Uint8Array([6, 112, 97, 114, 101, 110, 116])],
578365826: [
new Uint8Array([
35,
18,
33,
47,
97,
112,
105,
47,
118,
49,
47,
123,
112,
97,
114,
101,
110,
116,
61,
117,
115,
101,
114,
115,
47,
42,
125,
47,
115,
101,
115,
115,
105,
111,
110,
115,
]),
],
},
},
},
/** RevokeUserSession revokes a specific session for a user. */
revokeUserSession: {
name: "RevokeUserSession",
requestType: RevokeUserSessionRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {
_unknownFields: {
8410: [new Uint8Array([4, 110, 97, 109, 101])],
578365826: [
new Uint8Array([
35,
42,
33,
47,
97,
112,
105,
47,
118,
49,
47,
123,
110,
97,
109,
101,
61,
117,
115,
101,
114,
115,
47,
42,
47,
115,
101,
115,
115,
105,
111,
110,
115,
47,
42,
125,
]),
],
},
},
},
},
} as const;

Loading…
Cancel
Save