Commit 77b7fc44 authored by Johnny's avatar Johnny

feat: implement user session

parent 741fe35c
...@@ -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];
......
This diff is collapsed.
...@@ -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
) )
...@@ -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",
......
...@@ -1193,8 +1193,8 @@ paths: ...@@ -1193,8 +1193,8 @@ paths:
tags: tags:
- IdentityProviderService - IdentityProviderService
delete: delete:
summary: DeleteIdentityProvider deletes an identity provider. summary: RevokeUserSession revokes a specific session for a user.
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 delete. Required. The resource name of the session to revoke.
Format: identityProviders/{idp} Format: users/{user}/sessions/{session}
in: path in: path
required: true required: true
type: string type: string
pattern: identityProviders/[^/]+ pattern: users/[^/]+/sessions/[^/]+
tags: tags:
- IdentityProviderService - UserService
/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: DeleteInbox deletes an inbox. summary: DeleteIdentityProvider deletes an identity provider.
operationId: InboxService_DeleteInbox operationId: IdentityProviderService_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 inbox to delete. Required. The resource name of the identity provider to delete.
Format: inboxes/{inbox} Format: identityProviders/{idp}
in: path in: path
required: true required: true
type: string type: string
pattern: inboxes/[^/]+ pattern: identityProviders/[^/]+
tags: tags:
- InboxService - IdentityProviderService
/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: DeleteMemo deletes a memo. summary: DeleteInbox 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:
- MemoService - InboxService
/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: DeleteMemoReaction deletes a reaction for a memo. summary: DeleteMemo deletes a memo.
operationId: MemoService_DeleteMemoReaction 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:
......
...@@ -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
}
...@@ -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,
......
...@@ -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)
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment