diff --git a/proto/api/v1/auth_service.proto b/proto/api/v1/auth_service.proto index 7c24ecfc8..2ab185f3b 100644 --- a/proto/api/v1/auth_service.proto +++ b/proto/api/v1/auth_service.proto @@ -6,19 +6,20 @@ import "api/v1/user_service.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; option go_package = "gen/api/v1"; service AuthService { // GetCurrentSession returns the current active session information. // This method is idempotent and safe, suitable for checking current session state. - rpc GetCurrentSession(GetCurrentSessionRequest) returns (User) { + rpc GetCurrentSession(GetCurrentSessionRequest) returns (GetCurrentSessionResponse) { option (google.api.http) = {get: "/api/v1/auth/sessions/current"}; } // CreateSession authenticates a user and creates a new session. // Returns the authenticated user information upon successful authentication. - rpc CreateSession(CreateSessionRequest) returns (User) { + rpc CreateSession(CreateSessionRequest) returns (CreateSessionResponse) { option (google.api.http) = { post: "/api/v1/auth/sessions" body: "*" @@ -36,6 +37,9 @@ message GetCurrentSessionRequest {} message GetCurrentSessionResponse { User user = 1; + + // Current session expiration time (if available). + google.protobuf.Timestamp expires_at = 2; } message CreateSessionRequest { @@ -67,7 +71,7 @@ message CreateSessionRequest { // Provide one authentication method (username/password or SSO). // Required field to specify the authentication method. - oneof method { + oneof credentials { // Username and password authentication method. PasswordCredentials password_credentials = 1; @@ -80,4 +84,12 @@ message CreateSessionRequest { bool never_expire = 3 [(google.api.field_behavior) = OPTIONAL]; } +message CreateSessionResponse { + // The authenticated user information. + User user = 1; + + // Token expiration time. + google.protobuf.Timestamp expires_at = 2; +} + message DeleteSessionRequest {} diff --git a/proto/gen/api/v1/auth_service.pb.go b/proto/gen/api/v1/auth_service.pb.go index 69f8488fb..bd8f95e03 100644 --- a/proto/gen/api/v1/auth_service.pb.go +++ b/proto/gen/api/v1/auth_service.pb.go @@ -11,6 +11,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -60,8 +61,10 @@ func (*GetCurrentSessionRequest) Descriptor() ([]byte, []int) { } type GetCurrentSessionResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Current session expiration time (if available). + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -103,16 +106,23 @@ func (x *GetCurrentSessionResponse) GetUser() *User { return nil } +func (x *GetCurrentSessionResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + type CreateSessionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Provide one authentication method (username/password or SSO). // Required field to specify the authentication method. // - // Types that are valid to be assigned to Method: + // Types that are valid to be assigned to Credentials: // // *CreateSessionRequest_PasswordCredentials_ // *CreateSessionRequest_SsoCredentials - Method isCreateSessionRequest_Method `protobuf_oneof:"method"` + Credentials isCreateSessionRequest_Credentials `protobuf_oneof:"credentials"` // Whether the session should never expire. // Optional field that defaults to false for security. NeverExpire bool `protobuf:"varint,3,opt,name=never_expire,json=neverExpire,proto3" json:"never_expire,omitempty"` @@ -150,16 +160,16 @@ func (*CreateSessionRequest) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2} } -func (x *CreateSessionRequest) GetMethod() isCreateSessionRequest_Method { +func (x *CreateSessionRequest) GetCredentials() isCreateSessionRequest_Credentials { if x != nil { - return x.Method + return x.Credentials } return nil } func (x *CreateSessionRequest) GetPasswordCredentials() *CreateSessionRequest_PasswordCredentials { if x != nil { - if x, ok := x.Method.(*CreateSessionRequest_PasswordCredentials_); ok { + if x, ok := x.Credentials.(*CreateSessionRequest_PasswordCredentials_); ok { return x.PasswordCredentials } } @@ -168,7 +178,7 @@ func (x *CreateSessionRequest) GetPasswordCredentials() *CreateSessionRequest_Pa func (x *CreateSessionRequest) GetSsoCredentials() *CreateSessionRequest_SSOCredentials { if x != nil { - if x, ok := x.Method.(*CreateSessionRequest_SsoCredentials); ok { + if x, ok := x.Credentials.(*CreateSessionRequest_SsoCredentials); ok { return x.SsoCredentials } } @@ -182,8 +192,8 @@ func (x *CreateSessionRequest) GetNeverExpire() bool { return false } -type isCreateSessionRequest_Method interface { - isCreateSessionRequest_Method() +type isCreateSessionRequest_Credentials interface { + isCreateSessionRequest_Credentials() } type CreateSessionRequest_PasswordCredentials_ struct { @@ -196,9 +206,63 @@ type CreateSessionRequest_SsoCredentials struct { SsoCredentials *CreateSessionRequest_SSOCredentials `protobuf:"bytes,2,opt,name=sso_credentials,json=ssoCredentials,proto3,oneof"` } -func (*CreateSessionRequest_PasswordCredentials_) isCreateSessionRequest_Method() {} +func (*CreateSessionRequest_PasswordCredentials_) isCreateSessionRequest_Credentials() {} + +func (*CreateSessionRequest_SsoCredentials) isCreateSessionRequest_Credentials() {} + +type CreateSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The authenticated user information. + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Token expiration time. + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSessionResponse) Reset() { + *x = CreateSessionResponse{} + mi := &file_api_v1_auth_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} -func (*CreateSessionRequest_SsoCredentials) isCreateSessionRequest_Method() {} +func (*CreateSessionResponse) ProtoMessage() {} + +func (x *CreateSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_auth_service_proto_msgTypes[3] + 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 CreateSessionResponse.ProtoReflect.Descriptor instead. +func (*CreateSessionResponse) Descriptor() ([]byte, []int) { + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateSessionResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +func (x *CreateSessionResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} type DeleteSessionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -208,7 +272,7 @@ type DeleteSessionRequest struct { func (x *DeleteSessionRequest) Reset() { *x = DeleteSessionRequest{} - mi := &file_api_v1_auth_service_proto_msgTypes[3] + mi := &file_api_v1_auth_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -220,7 +284,7 @@ func (x *DeleteSessionRequest) String() string { func (*DeleteSessionRequest) ProtoMessage() {} func (x *DeleteSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_auth_service_proto_msgTypes[3] + mi := &file_api_v1_auth_service_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -233,7 +297,7 @@ func (x *DeleteSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSessionRequest.ProtoReflect.Descriptor instead. func (*DeleteSessionRequest) Descriptor() ([]byte, []int) { - return file_api_v1_auth_service_proto_rawDescGZIP(), []int{3} + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{4} } // Nested message for password-based authentication credentials. @@ -251,7 +315,7 @@ type CreateSessionRequest_PasswordCredentials struct { func (x *CreateSessionRequest_PasswordCredentials) Reset() { *x = CreateSessionRequest_PasswordCredentials{} - mi := &file_api_v1_auth_service_proto_msgTypes[4] + mi := &file_api_v1_auth_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -263,7 +327,7 @@ func (x *CreateSessionRequest_PasswordCredentials) String() string { func (*CreateSessionRequest_PasswordCredentials) ProtoMessage() {} func (x *CreateSessionRequest_PasswordCredentials) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_auth_service_proto_msgTypes[4] + mi := &file_api_v1_auth_service_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -311,7 +375,7 @@ type CreateSessionRequest_SSOCredentials struct { func (x *CreateSessionRequest_SSOCredentials) Reset() { *x = CreateSessionRequest_SSOCredentials{} - mi := &file_api_v1_auth_service_proto_msgTypes[5] + mi := &file_api_v1_auth_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -323,7 +387,7 @@ func (x *CreateSessionRequest_SSOCredentials) String() string { func (*CreateSessionRequest_SSOCredentials) ProtoMessage() {} func (x *CreateSessionRequest_SSOCredentials) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_auth_service_proto_msgTypes[5] + mi := &file_api_v1_auth_service_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -364,10 +428,12 @@ var File_api_v1_auth_service_proto protoreflect.FileDescriptor const file_api_v1_auth_service_proto_rawDesc = "" + "\n" + - "\x19api/v1/auth_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\"\x1a\n" + - "\x18GetCurrentSessionRequest\"C\n" + + "\x19api/v1/auth_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x1a\n" + + "\x18GetCurrentSessionRequest\"~\n" + "\x19GetCurrentSessionResponse\x12&\n" + - "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\"\xdb\x03\n" + + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x129\n" + + "\n" + + "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\xe0\x03\n" + "\x14CreateSessionRequest\x12k\n" + "\x14password_credentials\x18\x01 \x01(\v26.memos.api.v1.CreateSessionRequest.PasswordCredentialsH\x00R\x13passwordCredentials\x12\\\n" + "\x0fsso_credentials\x18\x02 \x01(\v21.memos.api.v1.CreateSessionRequest.SSOCredentialsH\x00R\x0essoCredentials\x12&\n" + @@ -378,12 +444,16 @@ const file_api_v1_auth_service_proto_rawDesc = "" + "\x0eSSOCredentials\x12\x1a\n" + "\x06idp_id\x18\x01 \x01(\x05B\x03\xe0A\x02R\x05idpId\x12\x17\n" + "\x04code\x18\x02 \x01(\tB\x03\xe0A\x02R\x04code\x12&\n" + - "\fredirect_uri\x18\x03 \x01(\tB\x03\xe0A\x02R\vredirectUriB\b\n" + - "\x06method\"\x16\n" + - "\x14DeleteSessionRequest2\xe4\x02\n" + - "\vAuthService\x12v\n" + - "\x11GetCurrentSession\x12&.memos.api.v1.GetCurrentSessionRequest\x1a\x12.memos.api.v1.User\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/api/v1/auth/sessions/current\x12i\n" + - "\rCreateSession\x12\".memos.api.v1.CreateSessionRequest\x1a\x12.memos.api.v1.User\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/sessions\x12r\n" + + "\fredirect_uri\x18\x03 \x01(\tB\x03\xe0A\x02R\vredirectUriB\r\n" + + "\vcredentials\"z\n" + + "\x15CreateSessionResponse\x12&\n" + + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x129\n" + + "\n" + + "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\x16\n" + + "\x14DeleteSessionRequest2\x8b\x03\n" + + "\vAuthService\x12\x8b\x01\n" + + "\x11GetCurrentSession\x12&.memos.api.v1.GetCurrentSessionRequest\x1a'.memos.api.v1.GetCurrentSessionResponse\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/api/v1/auth/sessions/current\x12z\n" + + "\rCreateSession\x12\".memos.api.v1.CreateSessionRequest\x1a#.memos.api.v1.CreateSessionResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/sessions\x12r\n" + "\rDeleteSession\x12\".memos.api.v1.DeleteSessionRequest\x1a\x16.google.protobuf.Empty\"%\x82\xd3\xe4\x93\x02\x1f*\x1d/api/v1/auth/sessions/currentB\xa8\x01\n" + "\x10com.memos.api.v1B\x10AuthServiceProtoP\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" @@ -399,32 +469,37 @@ func file_api_v1_auth_service_proto_rawDescGZIP() []byte { return file_api_v1_auth_service_proto_rawDescData } -var file_api_v1_auth_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_api_v1_auth_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_api_v1_auth_service_proto_goTypes = []any{ (*GetCurrentSessionRequest)(nil), // 0: memos.api.v1.GetCurrentSessionRequest (*GetCurrentSessionResponse)(nil), // 1: memos.api.v1.GetCurrentSessionResponse (*CreateSessionRequest)(nil), // 2: memos.api.v1.CreateSessionRequest - (*DeleteSessionRequest)(nil), // 3: memos.api.v1.DeleteSessionRequest - (*CreateSessionRequest_PasswordCredentials)(nil), // 4: memos.api.v1.CreateSessionRequest.PasswordCredentials - (*CreateSessionRequest_SSOCredentials)(nil), // 5: memos.api.v1.CreateSessionRequest.SSOCredentials - (*User)(nil), // 6: memos.api.v1.User - (*emptypb.Empty)(nil), // 7: google.protobuf.Empty + (*CreateSessionResponse)(nil), // 3: memos.api.v1.CreateSessionResponse + (*DeleteSessionRequest)(nil), // 4: memos.api.v1.DeleteSessionRequest + (*CreateSessionRequest_PasswordCredentials)(nil), // 5: memos.api.v1.CreateSessionRequest.PasswordCredentials + (*CreateSessionRequest_SSOCredentials)(nil), // 6: memos.api.v1.CreateSessionRequest.SSOCredentials + (*User)(nil), // 7: memos.api.v1.User + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 9: google.protobuf.Empty } var file_api_v1_auth_service_proto_depIdxs = []int32{ - 6, // 0: memos.api.v1.GetCurrentSessionResponse.user:type_name -> memos.api.v1.User - 4, // 1: memos.api.v1.CreateSessionRequest.password_credentials:type_name -> memos.api.v1.CreateSessionRequest.PasswordCredentials - 5, // 2: memos.api.v1.CreateSessionRequest.sso_credentials:type_name -> memos.api.v1.CreateSessionRequest.SSOCredentials - 0, // 3: memos.api.v1.AuthService.GetCurrentSession:input_type -> memos.api.v1.GetCurrentSessionRequest - 2, // 4: memos.api.v1.AuthService.CreateSession:input_type -> memos.api.v1.CreateSessionRequest - 3, // 5: memos.api.v1.AuthService.DeleteSession:input_type -> memos.api.v1.DeleteSessionRequest - 6, // 6: memos.api.v1.AuthService.GetCurrentSession:output_type -> memos.api.v1.User - 6, // 7: memos.api.v1.AuthService.CreateSession:output_type -> memos.api.v1.User - 7, // 8: memos.api.v1.AuthService.DeleteSession:output_type -> google.protobuf.Empty - 6, // [6:9] is the sub-list for method output_type - 3, // [3:6] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 7, // 0: memos.api.v1.GetCurrentSessionResponse.user:type_name -> memos.api.v1.User + 8, // 1: memos.api.v1.GetCurrentSessionResponse.expires_at:type_name -> google.protobuf.Timestamp + 5, // 2: memos.api.v1.CreateSessionRequest.password_credentials:type_name -> memos.api.v1.CreateSessionRequest.PasswordCredentials + 6, // 3: memos.api.v1.CreateSessionRequest.sso_credentials:type_name -> memos.api.v1.CreateSessionRequest.SSOCredentials + 7, // 4: memos.api.v1.CreateSessionResponse.user:type_name -> memos.api.v1.User + 8, // 5: memos.api.v1.CreateSessionResponse.expires_at:type_name -> google.protobuf.Timestamp + 0, // 6: memos.api.v1.AuthService.GetCurrentSession:input_type -> memos.api.v1.GetCurrentSessionRequest + 2, // 7: memos.api.v1.AuthService.CreateSession:input_type -> memos.api.v1.CreateSessionRequest + 4, // 8: memos.api.v1.AuthService.DeleteSession:input_type -> memos.api.v1.DeleteSessionRequest + 1, // 9: memos.api.v1.AuthService.GetCurrentSession:output_type -> memos.api.v1.GetCurrentSessionResponse + 3, // 10: memos.api.v1.AuthService.CreateSession:output_type -> memos.api.v1.CreateSessionResponse + 9, // 11: memos.api.v1.AuthService.DeleteSession:output_type -> google.protobuf.Empty + 9, // [9:12] is the sub-list for method output_type + 6, // [6:9] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_api_v1_auth_service_proto_init() } @@ -443,7 +518,7 @@ func file_api_v1_auth_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_auth_service_proto_rawDesc), len(file_api_v1_auth_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/gen/api/v1/auth_service_grpc.pb.go b/proto/gen/api/v1/auth_service_grpc.pb.go index a91b0fda5..2872b28be 100644 --- a/proto/gen/api/v1/auth_service_grpc.pb.go +++ b/proto/gen/api/v1/auth_service_grpc.pb.go @@ -31,10 +31,10 @@ const ( type AuthServiceClient interface { // GetCurrentSession returns the current active session information. // This method is idempotent and safe, suitable for checking current session state. - GetCurrentSession(ctx context.Context, in *GetCurrentSessionRequest, opts ...grpc.CallOption) (*User, error) + GetCurrentSession(ctx context.Context, in *GetCurrentSessionRequest, opts ...grpc.CallOption) (*GetCurrentSessionResponse, error) // CreateSession authenticates a user and creates a new session. // Returns the authenticated user information upon successful authentication. - CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*User, error) + CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error) // DeleteSession terminates the current user session. // This is an idempotent operation that invalidates the user's authentication. DeleteSession(ctx context.Context, in *DeleteSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) @@ -48,9 +48,9 @@ func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { return &authServiceClient{cc} } -func (c *authServiceClient) GetCurrentSession(ctx context.Context, in *GetCurrentSessionRequest, opts ...grpc.CallOption) (*User, error) { +func (c *authServiceClient) GetCurrentSession(ctx context.Context, in *GetCurrentSessionRequest, opts ...grpc.CallOption) (*GetCurrentSessionResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(User) + out := new(GetCurrentSessionResponse) err := c.cc.Invoke(ctx, AuthService_GetCurrentSession_FullMethodName, in, out, cOpts...) if err != nil { return nil, err @@ -58,9 +58,9 @@ func (c *authServiceClient) GetCurrentSession(ctx context.Context, in *GetCurren return out, nil } -func (c *authServiceClient) CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*User, error) { +func (c *authServiceClient) CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(User) + out := new(CreateSessionResponse) err := c.cc.Invoke(ctx, AuthService_CreateSession_FullMethodName, in, out, cOpts...) if err != nil { return nil, err @@ -84,10 +84,10 @@ func (c *authServiceClient) DeleteSession(ctx context.Context, in *DeleteSession type AuthServiceServer interface { // GetCurrentSession returns the current active session information. // This method is idempotent and safe, suitable for checking current session state. - GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*User, error) + GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*GetCurrentSessionResponse, error) // CreateSession authenticates a user and creates a new session. // Returns the authenticated user information upon successful authentication. - CreateSession(context.Context, *CreateSessionRequest) (*User, error) + CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) // DeleteSession terminates the current user session. // This is an idempotent operation that invalidates the user's authentication. DeleteSession(context.Context, *DeleteSessionRequest) (*emptypb.Empty, error) @@ -101,10 +101,10 @@ type AuthServiceServer interface { // pointer dereference when methods are called. type UnimplementedAuthServiceServer struct{} -func (UnimplementedAuthServiceServer) GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*User, error) { +func (UnimplementedAuthServiceServer) GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*GetCurrentSessionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetCurrentSession not implemented") } -func (UnimplementedAuthServiceServer) CreateSession(context.Context, *CreateSessionRequest) (*User, error) { +func (UnimplementedAuthServiceServer) CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateSession not implemented") } func (UnimplementedAuthServiceServer) DeleteSession(context.Context, *DeleteSessionRequest) (*emptypb.Empty, error) { diff --git a/proto/gen/apidocs.swagger.yaml b/proto/gen/apidocs.swagger.yaml index 916e6c2fa..9d71f541c 100644 --- a/proto/gen/apidocs.swagger.yaml +++ b/proto/gen/apidocs.swagger.yaml @@ -141,7 +141,7 @@ paths: "200": description: A successful response. schema: - $ref: '#/definitions/v1User' + $ref: '#/definitions/v1CreateSessionResponse' default: description: An unexpected error response. schema: @@ -164,7 +164,7 @@ paths: "200": description: A successful response. schema: - $ref: '#/definitions/v1User' + $ref: '#/definitions/v1GetCurrentSessionResponse' default: description: An unexpected error response. schema: @@ -3287,6 +3287,16 @@ definitions: description: |- Whether the session should never expire. Optional field that defaults to false for security. + v1CreateSessionResponse: + type: object + properties: + user: + $ref: '#/definitions/v1User' + description: The authenticated user information. + expiresAt: + type: string + format: date-time + description: Token expiration time. v1EmbeddedContentNode: type: object properties: @@ -3301,6 +3311,15 @@ definitions: properties: symbol: type: string + v1GetCurrentSessionResponse: + type: object + properties: + user: + $ref: '#/definitions/v1User' + expiresAt: + type: string + format: date-time + description: Current session expiration time (if available). v1HTMLElementNode: type: object properties: diff --git a/server/router/api/v1/acl_config.go b/server/router/api/v1/acl_config.go index 25ad4ae31..c9f64ac5e 100644 --- a/server/router/api/v1/acl_config.go +++ b/server/router/api/v1/acl_config.go @@ -3,7 +3,6 @@ package v1 var authenticationAllowlistMethods = map[string]bool{ "/memos.api.v1.WorkspaceService/GetWorkspaceProfile": true, "/memos.api.v1.WorkspaceService/GetWorkspaceSetting": true, - "/memos.api.v1.IdentityProviderService/GetIdentityProvider": true, "/memos.api.v1.IdentityProviderService/ListIdentityProviders": true, "/memos.api.v1.AuthService/CreateSession": true, "/memos.api.v1.AuthService/GetCurrentSession": true, diff --git a/server/router/api/v1/auth_service.go b/server/router/api/v1/auth_service.go index d497d1208..bebb6e6a0 100644 --- a/server/router/api/v1/auth_service.go +++ b/server/router/api/v1/auth_service.go @@ -29,7 +29,7 @@ const ( unmatchedUsernameAndPasswordError = "unmatched username and password" ) -func (s *APIV1Service) GetCurrentSession(ctx context.Context, _ *v1pb.GetCurrentSessionRequest) (*v1pb.User, error) { +func (s *APIV1Service) GetCurrentSession(ctx context.Context, _ *v1pb.GetCurrentSessionRequest) (*v1pb.GetCurrentSessionResponse, error) { user, err := s.GetCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err) @@ -50,10 +50,12 @@ func (s *APIV1Service) GetCurrentSession(ctx context.Context, _ *v1pb.GetCurrent } } - return convertUserFromStore(user), nil + return &v1pb.GetCurrentSessionResponse{ + User: convertUserFromStore(user), + }, nil } -func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSessionRequest) (*v1pb.User, error) { +func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSessionRequest) (*v1pb.CreateSessionResponse, error) { var existingUser *store.User if passwordCredentials := request.GetPasswordCredentials(); passwordCredentials != nil { user, err := s.Store.GetUser(ctx, &store.FindUser{ @@ -173,7 +175,11 @@ func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSe if err := s.doSignIn(ctx, existingUser, expireTime); err != nil { return nil, status.Errorf(codes.Internal, "failed to sign in, error: %v", err) } - return convertUserFromStore(existingUser), nil + + return &v1pb.CreateSessionResponse{ + User: convertUserFromStore(existingUser), + ExpiresAt: timestamppb.New(expireTime), + }, nil } func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User, expireTime time.Time) error { diff --git a/store/test/migrator_test.go b/store/test/migrator_test.go index 36b94d862..8b22eb295 100644 --- a/store/test/migrator_test.go +++ b/store/test/migrator_test.go @@ -13,5 +13,5 @@ func TestGetCurrentSchemaVersion(t *testing.T) { currentSchemaVersion, err := ts.GetCurrentSchemaVersion() require.NoError(t, err) - require.Equal(t, "0.24.2", currentSchemaVersion) + require.Equal(t, "0.25.1", currentSchemaVersion) } diff --git a/web/src/components/PasswordSignInForm.tsx b/web/src/components/PasswordSignInForm.tsx index ae8679909..aba7234f1 100644 --- a/web/src/components/PasswordSignInForm.tsx +++ b/web/src/components/PasswordSignInForm.tsx @@ -45,7 +45,10 @@ const PasswordSignInForm = observer(() => { try { actionBtnLoadingState.setLoading(); - await authServiceClient.createSession({ passwordCredentials: { username, password }, neverExpire: remember }); + await authServiceClient.createSession({ + passwordCredentials: { username, password }, + neverExpire: remember, + }); await initialUserStore(); navigateTo("/"); } catch (error: any) { diff --git a/web/src/store/v2/user.ts b/web/src/store/v2/user.ts index f8e4f96da..3e2b70809 100644 --- a/web/src/store/v2/user.ts +++ b/web/src/store/v2/user.ts @@ -231,7 +231,16 @@ const userStore = (() => { export const initialUserStore = async () => { try { - const currentUser = await authServiceClient.getCurrentSession({}); + const { user: currentUser } = await authServiceClient.getCurrentSession({}); + if (!currentUser) { + // If no user is authenticated, we can skip the rest of the initialization. + userStore.state.setPartial({ + currentUser: undefined, + userSetting: undefined, + userMapByName: {}, + }); + return; + } const userSetting = await userServiceClient.getUserSetting({ name: currentUser.name }); userStore.state.setPartial({ currentUser: currentUser.name, diff --git a/web/src/types/proto/api/v1/auth_service.ts b/web/src/types/proto/api/v1/auth_service.ts index 897afca64..cc6ab4472 100644 --- a/web/src/types/proto/api/v1/auth_service.ts +++ b/web/src/types/proto/api/v1/auth_service.ts @@ -7,6 +7,7 @@ /* eslint-disable */ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; import { Empty } from "../../google/protobuf/empty"; +import { Timestamp } from "../../google/protobuf/timestamp"; import { User } from "./user_service"; export const protobufPackage = "memos.api.v1"; @@ -15,7 +16,11 @@ export interface GetCurrentSessionRequest { } export interface GetCurrentSessionResponse { - user?: User | undefined; + user?: + | User + | undefined; + /** Current session expiration time (if available). */ + expiresAt?: Date | undefined; } export interface CreateSessionRequest { @@ -67,6 +72,15 @@ export interface CreateSessionRequest_SSOCredentials { redirectUri: string; } +export interface CreateSessionResponse { + /** The authenticated user information. */ + user?: + | User + | undefined; + /** Token expiration time. */ + expiresAt?: Date | undefined; +} + export interface DeleteSessionRequest { } @@ -105,7 +119,7 @@ export const GetCurrentSessionRequest: MessageFns = { }; function createBaseGetCurrentSessionResponse(): GetCurrentSessionResponse { - return { user: undefined }; + return { user: undefined, expiresAt: undefined }; } export const GetCurrentSessionResponse: MessageFns = { @@ -113,6 +127,9 @@ export const GetCurrentSessionResponse: MessageFns = if (message.user !== undefined) { User.encode(message.user, writer.uint32(10).fork()).join(); } + if (message.expiresAt !== undefined) { + Timestamp.encode(toTimestamp(message.expiresAt), writer.uint32(18).fork()).join(); + } return writer; }, @@ -131,6 +148,14 @@ export const GetCurrentSessionResponse: MessageFns = message.user = User.decode(reader, reader.uint32()); continue; } + case 2: { + if (tag !== 18) { + break; + } + + message.expiresAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -146,6 +171,7 @@ export const GetCurrentSessionResponse: MessageFns = fromPartial(object: DeepPartial): GetCurrentSessionResponse { const message = createBaseGetCurrentSessionResponse(); message.user = (object.user !== undefined && object.user !== null) ? User.fromPartial(object.user) : undefined; + message.expiresAt = object.expiresAt ?? undefined; return message; }, }; @@ -352,6 +378,64 @@ export const CreateSessionRequest_SSOCredentials: MessageFns = { + encode(message: CreateSessionResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.user !== undefined) { + User.encode(message.user, writer.uint32(10).fork()).join(); + } + if (message.expiresAt !== undefined) { + Timestamp.encode(toTimestamp(message.expiresAt), writer.uint32(18).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): CreateSessionResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCreateSessionResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.user = User.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.expiresAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create(base?: DeepPartial): CreateSessionResponse { + return CreateSessionResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): CreateSessionResponse { + const message = createBaseCreateSessionResponse(); + message.user = (object.user !== undefined && object.user !== null) ? User.fromPartial(object.user) : undefined; + message.expiresAt = object.expiresAt ?? undefined; + return message; + }, +}; + function createBaseDeleteSessionRequest(): DeleteSessionRequest { return {}; } @@ -399,7 +483,7 @@ export const AuthServiceDefinition = { name: "GetCurrentSession", requestType: GetCurrentSessionRequest, requestStream: false, - responseType: User, + responseType: GetCurrentSessionResponse, responseStream: false, options: { _unknownFields: { @@ -450,7 +534,7 @@ export const AuthServiceDefinition = { name: "CreateSession", requestType: CreateSessionRequest, requestStream: false, - responseType: User, + responseType: CreateSessionResponse, responseStream: false, options: { _unknownFields: { @@ -550,6 +634,18 @@ export type DeepPartial = T extends Builtin ? T : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; +function toTimestamp(date: Date): Timestamp { + const seconds = Math.trunc(date.getTime() / 1_000); + const nanos = (date.getTime() % 1_000) * 1_000_000; + return { seconds, nanos }; +} + +function fromTimestamp(t: Timestamp): Date { + let millis = (t.seconds || 0) * 1_000; + millis += (t.nanos || 0) / 1_000_000; + return new globalThis.Date(millis); +} + export interface MessageFns { encode(message: T, writer?: BinaryWriter): BinaryWriter; decode(input: BinaryReader | Uint8Array, length?: number): T; diff --git a/web/src/types/proto/google/protobuf/descriptor.ts b/web/src/types/proto/google/protobuf/descriptor.ts index 9f55f0344..89514564e 100644 --- a/web/src/types/proto/google/protobuf/descriptor.ts +++ b/web/src/types/proto/google/protobuf/descriptor.ts @@ -35,7 +35,7 @@ export enum Edition { EDITION_2024 = "EDITION_2024", /** * EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution. These should not be - * used or relyed on outside of tests. + * used or relied on outside of tests. */ EDITION_1_TEST_ONLY = "EDITION_1_TEST_ONLY", EDITION_2_TEST_ONLY = "EDITION_2_TEST_ONLY", @@ -177,11 +177,19 @@ export interface FileDescriptorProto { * The supported values are "proto2", "proto3", and "editions". * * If `edition` is present, this value must be "editions". + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. */ syntax?: | string | undefined; - /** The edition of the proto file. */ + /** + * The edition of the proto file. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ edition?: Edition | undefined; } @@ -828,7 +836,12 @@ export interface FileOptions { rubyPackage?: | string | undefined; - /** Any features defined in the specific edition. */ + /** + * Any features defined in the specific edition. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ features?: | FeatureSet | undefined; @@ -966,7 +979,12 @@ export interface MessageOptions { deprecatedLegacyJsonFieldConflicts?: | boolean | undefined; - /** Any features defined in the specific edition. */ + /** + * Any features defined in the specific edition. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ features?: | FeatureSet | undefined; @@ -976,12 +994,13 @@ export interface MessageOptions { export interface FieldOptions { /** + * NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead. * The ctype option instructs the C++ code generator to use a different * representation of the field than it normally would. See the specific * options below. This option is only implemented to support use of * [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of - * type "bytes" in the open source release -- sorry, we'll try to include - * other types in a future version! + * type "bytes" in the open source release. + * TODO: make ctype actually deprecated. */ ctype?: | FieldOptions_CType @@ -1070,7 +1089,12 @@ export interface FieldOptions { retention?: FieldOptions_OptionRetention | undefined; targets: FieldOptions_OptionTargetType[]; editionDefaults: FieldOptions_EditionDefault[]; - /** Any features defined in the specific edition. */ + /** + * Any features defined in the specific edition. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ features?: FeatureSet | undefined; featureSupport?: | FieldOptions_FeatureSupport @@ -1169,11 +1193,7 @@ export function fieldOptions_JSTypeToNumber(object: FieldOptions_JSType): number } } -/** - * If set to RETENTION_SOURCE, the option will be omitted from the binary. - * Note: as of January 2023, support for this is in progress and does not yet - * have an effect (b/264593489). - */ +/** If set to RETENTION_SOURCE, the option will be omitted from the binary. */ export enum FieldOptions_OptionRetention { RETENTION_UNKNOWN = "RETENTION_UNKNOWN", RETENTION_RUNTIME = "RETENTION_RUNTIME", @@ -1216,8 +1236,7 @@ export function fieldOptions_OptionRetentionToNumber(object: FieldOptions_Option /** * This indicates the types of entities that the field may apply to when used * as an option. If it is unset, then the field may be freely used as an - * option on any kind of entity. Note: as of January 2023, support for this is - * in progress and does not yet have an effect (b/264593489). + * option on any kind of entity. */ export enum FieldOptions_OptionTargetType { TARGET_TYPE_UNKNOWN = "TARGET_TYPE_UNKNOWN", @@ -1341,7 +1360,12 @@ export interface FieldOptions_FeatureSupport { } export interface OneofOptions { - /** Any features defined in the specific edition. */ + /** + * Any features defined in the specific edition. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ features?: | FeatureSet | undefined; @@ -1379,7 +1403,12 @@ export interface EnumOptions { deprecatedLegacyJsonFieldConflicts?: | boolean | undefined; - /** Any features defined in the specific edition. */ + /** + * Any features defined in the specific edition. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ features?: | FeatureSet | undefined; @@ -1397,7 +1426,12 @@ export interface EnumValueOptions { deprecated?: | boolean | undefined; - /** Any features defined in the specific edition. */ + /** + * Any features defined in the specific edition. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ features?: | FeatureSet | undefined; @@ -1418,7 +1452,12 @@ export interface EnumValueOptions { } export interface ServiceOptions { - /** Any features defined in the specific edition. */ + /** + * Any features defined in the specific edition. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ features?: | FeatureSet | undefined; @@ -1446,7 +1485,12 @@ export interface MethodOptions { idempotencyLevel?: | MethodOptions_IdempotencyLevel | undefined; - /** Any features defined in the specific edition. */ + /** + * Any features defined in the specific edition. + * WARNING: This field should only be used by protobuf plugins or special + * cases like the proto compiler. Other uses are discouraged and + * developers should rely on the protoreflect APIs for their client language. + */ features?: | FeatureSet | undefined; @@ -1549,6 +1593,7 @@ export interface FeatureSet { utf8Validation?: FeatureSet_Utf8Validation | undefined; messageEncoding?: FeatureSet_MessageEncoding | undefined; jsonFormat?: FeatureSet_JsonFormat | undefined; + enforceNamingStyle?: FeatureSet_EnforceNamingStyle | undefined; } export enum FeatureSet_FieldPresence { @@ -1791,6 +1836,45 @@ export function featureSet_JsonFormatToNumber(object: FeatureSet_JsonFormat): nu } } +export enum FeatureSet_EnforceNamingStyle { + ENFORCE_NAMING_STYLE_UNKNOWN = "ENFORCE_NAMING_STYLE_UNKNOWN", + STYLE2024 = "STYLE2024", + STYLE_LEGACY = "STYLE_LEGACY", + UNRECOGNIZED = "UNRECOGNIZED", +} + +export function featureSet_EnforceNamingStyleFromJSON(object: any): FeatureSet_EnforceNamingStyle { + switch (object) { + case 0: + case "ENFORCE_NAMING_STYLE_UNKNOWN": + return FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN; + case 1: + case "STYLE2024": + return FeatureSet_EnforceNamingStyle.STYLE2024; + case 2: + case "STYLE_LEGACY": + return FeatureSet_EnforceNamingStyle.STYLE_LEGACY; + case -1: + case "UNRECOGNIZED": + default: + return FeatureSet_EnforceNamingStyle.UNRECOGNIZED; + } +} + +export function featureSet_EnforceNamingStyleToNumber(object: FeatureSet_EnforceNamingStyle): number { + switch (object) { + case FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN: + return 0; + case FeatureSet_EnforceNamingStyle.STYLE2024: + return 1; + case FeatureSet_EnforceNamingStyle.STYLE_LEGACY: + return 2; + case FeatureSet_EnforceNamingStyle.UNRECOGNIZED: + default: + return -1; + } +} + /** * A compiled specification for the defaults of a set of features. These * messages are generated from FeatureSet extensions and can be used to seed @@ -4914,6 +4998,7 @@ function createBaseFeatureSet(): FeatureSet { utf8Validation: FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN, messageEncoding: FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN, jsonFormat: FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN, + enforceNamingStyle: FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN, }; } @@ -4948,6 +5033,12 @@ export const FeatureSet: MessageFns = { if (message.jsonFormat !== undefined && message.jsonFormat !== FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN) { writer.uint32(48).int32(featureSet_JsonFormatToNumber(message.jsonFormat)); } + if ( + message.enforceNamingStyle !== undefined && + message.enforceNamingStyle !== FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN + ) { + writer.uint32(56).int32(featureSet_EnforceNamingStyleToNumber(message.enforceNamingStyle)); + } return writer; }, @@ -5006,6 +5097,14 @@ export const FeatureSet: MessageFns = { message.jsonFormat = featureSet_JsonFormatFromJSON(reader.int32()); continue; } + case 7: { + if (tag !== 56) { + break; + } + + message.enforceNamingStyle = featureSet_EnforceNamingStyleFromJSON(reader.int32()); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -5027,6 +5126,8 @@ export const FeatureSet: MessageFns = { message.utf8Validation = object.utf8Validation ?? FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN; message.messageEncoding = object.messageEncoding ?? FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN; message.jsonFormat = object.jsonFormat ?? FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN; + message.enforceNamingStyle = object.enforceNamingStyle ?? + FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN; return message; }, };