diff --git a/proto/api/v1/auth_service.proto b/proto/api/v1/auth_service.proto index 2ab185f3b..eb3477f99 100644 --- a/proto/api/v1/auth_service.proto +++ b/proto/api/v1/auth_service.proto @@ -38,8 +38,9 @@ message GetCurrentSessionRequest {} message GetCurrentSessionResponse { User user = 1; - // Current session expiration time (if available). - google.protobuf.Timestamp expires_at = 2; + // Last time the session was accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + google.protobuf.Timestamp last_accessed_at = 2; } message CreateSessionRequest { @@ -78,18 +79,15 @@ message CreateSessionRequest { // SSO provider authentication method. SSOCredentials sso_credentials = 2; } - - // Whether the session should never expire. - // Optional field that defaults to false for security. - 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; + // Last time the session was accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + google.protobuf.Timestamp last_accessed_at = 2; } message DeleteSessionRequest {} diff --git a/proto/api/v1/user_service.proto b/proto/api/v1/user_service.proto index 50ffbda0d..1a9ea433a 100644 --- a/proto/api/v1/user_service.proto +++ b/proto/api/v1/user_service.proto @@ -481,14 +481,12 @@ message UserSession { // 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]; + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + google.protobuf.Timestamp last_accessed_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; // Client information associated with this session. - ClientInfo client_info = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; + ClientInfo client_info = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; message ClientInfo { // User agent string of the client. diff --git a/proto/gen/api/v1/auth_service.pb.go b/proto/gen/api/v1/auth_service.pb.go index bd8f95e03..a194df5e6 100644 --- a/proto/gen/api/v1/auth_service.pb.go +++ b/proto/gen/api/v1/auth_service.pb.go @@ -63,10 +63,11 @@ 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"` - // 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 + // Last time the session was accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + LastAccessedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_accessed_at,json=lastAccessedAt,proto3" json:"last_accessed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetCurrentSessionResponse) Reset() { @@ -106,9 +107,9 @@ func (x *GetCurrentSessionResponse) GetUser() *User { return nil } -func (x *GetCurrentSessionResponse) GetExpiresAt() *timestamppb.Timestamp { +func (x *GetCurrentSessionResponse) GetLastAccessedAt() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAt + return x.LastAccessedAt } return nil } @@ -122,10 +123,7 @@ type CreateSessionRequest struct { // // *CreateSessionRequest_PasswordCredentials_ // *CreateSessionRequest_SsoCredentials - 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"` + Credentials isCreateSessionRequest_Credentials `protobuf_oneof:"credentials"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -185,13 +183,6 @@ func (x *CreateSessionRequest) GetSsoCredentials() *CreateSessionRequest_SSOCred return nil } -func (x *CreateSessionRequest) GetNeverExpire() bool { - if x != nil { - return x.NeverExpire - } - return false -} - type isCreateSessionRequest_Credentials interface { isCreateSessionRequest_Credentials() } @@ -214,10 +205,11 @@ 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 + // Last time the session was accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + LastAccessedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_accessed_at,json=lastAccessedAt,proto3" json:"last_accessed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateSessionResponse) Reset() { @@ -257,9 +249,9 @@ func (x *CreateSessionResponse) GetUser() *User { return nil } -func (x *CreateSessionResponse) GetExpiresAt() *timestamppb.Timestamp { +func (x *CreateSessionResponse) GetLastAccessedAt() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAt + return x.LastAccessedAt } return nil } @@ -429,15 +421,13 @@ 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\x1fgoogle/protobuf/timestamp.proto\"\x1a\n" + - "\x18GetCurrentSessionRequest\"~\n" + + "\x18GetCurrentSessionRequest\"\x89\x01\n" + "\x19GetCurrentSessionResponse\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\"\xe0\x03\n" + + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x12D\n" + + "\x10last_accessed_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x0elastAccessedAt\"\xb8\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" + - "\fnever_expire\x18\x03 \x01(\bB\x03\xe0A\x01R\vneverExpire\x1aW\n" + + "\x0fsso_credentials\x18\x02 \x01(\v21.memos.api.v1.CreateSessionRequest.SSOCredentialsH\x00R\x0essoCredentials\x1aW\n" + "\x13PasswordCredentials\x12\x1f\n" + "\busername\x18\x01 \x01(\tB\x03\xe0A\x02R\busername\x12\x1f\n" + "\bpassword\x18\x02 \x01(\tB\x03\xe0A\x02R\bpassword\x1am\n" + @@ -445,11 +435,10 @@ const file_api_v1_auth_service_proto_rawDesc = "" + "\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\r\n" + - "\vcredentials\"z\n" + + "\vcredentials\"\x85\x01\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" + + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x12D\n" + + "\x10last_accessed_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x0elastAccessedAt\"\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" + @@ -484,11 +473,11 @@ var file_api_v1_auth_service_proto_goTypes = []any{ } var file_api_v1_auth_service_proto_depIdxs = []int32{ 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 + 8, // 1: memos.api.v1.GetCurrentSessionResponse.last_accessed_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 + 8, // 5: memos.api.v1.CreateSessionResponse.last_accessed_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 diff --git a/proto/gen/api/v1/user_service.pb.go b/proto/gen/api/v1/user_service.pb.go index 2b2667794..b892f9c09 100644 --- a/proto/gen/api/v1/user_service.pb.go +++ b/proto/gen/api/v1/user_service.pb.go @@ -1434,12 +1434,11 @@ type UserSession struct { 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"` + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + LastAccessedTime *timestamppb.Timestamp `protobuf:"bytes,4,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"` + ClientInfo *UserSession_ClientInfo `protobuf:"bytes,5,opt,name=client_info,json=clientInfo,proto3" json:"client_info,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1495,13 +1494,6 @@ func (x *UserSession) GetCreateTime() *timestamppb.Timestamp { 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 @@ -2055,17 +2047,15 @@ 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\"\xd6\x04\n" + + "\x1cmemos.api.v1/UserAccessTokenR\x04name\"\x94\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" + + "createTime\x12M\n" + + "\x12last_accessed_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\x10lastAccessedTime\x12J\n" + + "\vclient_info\x18\x05 \x01(\v2$.memos.api.v1.UserSession.ClientInfoB\x03\xe0A\x03R\n" + "clientInfo\x1a\xa4\x01\n" + "\n" + "ClientInfo\x12\x1d\n" + @@ -2190,48 +2180,47 @@ var file_api_v1_user_service_proto_depIdxs = []int32{ 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 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 + 31, // 20: memos.api.v1.UserSession.last_accessed_time:type_name -> google.protobuf.Timestamp + 29, // 21: memos.api.v1.UserSession.client_info:type_name -> memos.api.v1.UserSession.ClientInfo + 21, // 22: memos.api.v1.ListUserSessionsResponse.sessions:type_name -> memos.api.v1.UserSession + 11, // 23: memos.api.v1.ListAllUserStatsResponse.user_stats:type_name -> memos.api.v1.UserStats + 2, // 24: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest + 4, // 25: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest + 5, // 26: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest + 6, // 27: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest + 7, // 28: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest + 8, // 29: memos.api.v1.UserService.SearchUsers:input_type -> memos.api.v1.SearchUsersRequest + 10, // 30: memos.api.v1.UserService.GetUserAvatar:input_type -> memos.api.v1.GetUserAvatarRequest + 25, // 31: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest + 12, // 32: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest + 14, // 33: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest + 15, // 34: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest + 17, // 35: memos.api.v1.UserService.ListUserAccessTokens:input_type -> memos.api.v1.ListUserAccessTokensRequest + 19, // 36: memos.api.v1.UserService.CreateUserAccessToken:input_type -> memos.api.v1.CreateUserAccessTokenRequest + 20, // 37: memos.api.v1.UserService.DeleteUserAccessToken:input_type -> memos.api.v1.DeleteUserAccessTokenRequest + 22, // 38: memos.api.v1.UserService.ListUserSessions:input_type -> memos.api.v1.ListUserSessionsRequest + 24, // 39: memos.api.v1.UserService.RevokeUserSession:input_type -> memos.api.v1.RevokeUserSessionRequest + 3, // 40: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse + 1, // 41: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User + 1, // 42: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User + 1, // 43: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User + 33, // 44: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty + 9, // 45: memos.api.v1.UserService.SearchUsers:output_type -> memos.api.v1.SearchUsersResponse + 34, // 46: memos.api.v1.UserService.GetUserAvatar:output_type -> google.api.HttpBody + 26, // 47: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse + 11, // 48: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats + 13, // 49: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting + 13, // 50: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting + 18, // 51: memos.api.v1.UserService.ListUserAccessTokens:output_type -> memos.api.v1.ListUserAccessTokensResponse + 16, // 52: memos.api.v1.UserService.CreateUserAccessToken:output_type -> memos.api.v1.UserAccessToken + 33, // 53: memos.api.v1.UserService.DeleteUserAccessToken:output_type -> google.protobuf.Empty + 23, // 54: memos.api.v1.UserService.ListUserSessions:output_type -> memos.api.v1.ListUserSessionsResponse + 33, // 55: memos.api.v1.UserService.RevokeUserSession:output_type -> google.protobuf.Empty + 40, // [40:56] is the sub-list for method output_type + 24, // [24:40] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_api_v1_user_service_proto_init() } diff --git a/proto/gen/apidocs.swagger.yaml b/proto/gen/apidocs.swagger.yaml index 9d71f541c..a682accfc 100644 --- a/proto/gen/apidocs.swagger.yaml +++ b/proto/gen/apidocs.swagger.yaml @@ -3282,21 +3282,18 @@ definitions: ssoCredentials: $ref: '#/definitions/CreateSessionRequestSSOCredentials' description: SSO provider authentication method. - neverExpire: - type: boolean - 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: + lastAccessedAt: type: string format: date-time - description: Token expiration time. + description: |- + Last time the session was accessed. + Used for sliding expiration calculation (last_accessed_time + 2 weeks). v1EmbeddedContentNode: type: object properties: @@ -3316,10 +3313,12 @@ definitions: properties: user: $ref: '#/definitions/v1User' - expiresAt: + lastAccessedAt: type: string format: date-time - description: Current session expiration time (if available). + description: |- + Last time the session was accessed. + Used for sliding expiration calculation (last_accessed_time + 2 weeks). v1HTMLElementNode: type: object properties: @@ -4152,15 +4151,12 @@ definitions: 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. + description: |- + The timestamp when the session was last accessed. + Used for sliding expiration calculation (last_accessed_time + 2 weeks). readOnly: true clientInfo: $ref: '#/definitions/v1UserSessionClientInfo' diff --git a/proto/gen/store/user_setting.pb.go b/proto/gen/store/user_setting.pb.go index 9c34a4ed3..52d263d79 100644 --- a/proto/gen/store/user_setting.pb.go +++ b/proto/gen/store/user_setting.pb.go @@ -476,12 +476,11 @@ type SessionsUserSetting_Session struct { SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Timestamp when the session was created. CreateTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` - // Timestamp when the session expires. - ExpireTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expire_time,json=expireTime,proto3" json:"expire_time,omitempty"` // Timestamp when the session was last accessed. - LastAccessedTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=last_accessed_time,json=lastAccessedTime,proto3" json:"last_accessed_time,omitempty"` + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + LastAccessedTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_accessed_time,json=lastAccessedTime,proto3" json:"last_accessed_time,omitempty"` // Client information associated with this session. - ClientInfo *SessionsUserSetting_ClientInfo `protobuf:"bytes,5,opt,name=client_info,json=clientInfo,proto3" json:"client_info,omitempty"` + ClientInfo *SessionsUserSetting_ClientInfo `protobuf:"bytes,4,opt,name=client_info,json=clientInfo,proto3" json:"client_info,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -530,13 +529,6 @@ func (x *SessionsUserSetting_Session) GetCreateTime() *timestamppb.Timestamp { return nil } -func (x *SessionsUserSetting_Session) GetExpireTime() *timestamppb.Timestamp { - if x != nil { - return x.ExpireTime - } - return nil -} - func (x *SessionsUserSetting_Session) GetLastAccessedTime() *timestamppb.Timestamp { if x != nil { return x.LastAccessedTime @@ -836,18 +828,16 @@ const file_store_user_setting_proto_rawDesc = "" + "\n" + "appearance\x18\x02 \x01(\tR\n" + "appearance\x12'\n" + - "\x0fmemo_visibility\x18\x03 \x01(\tR\x0ememoVisibility\"\xb0\x04\n" + + "\x0fmemo_visibility\x18\x03 \x01(\tR\x0ememoVisibility\"\xf3\x03\n" + "\x13SessionsUserSetting\x12D\n" + - "\bsessions\x18\x01 \x03(\v2(.memos.store.SessionsUserSetting.SessionR\bsessions\x1a\xba\x02\n" + + "\bsessions\x18\x01 \x03(\v2(.memos.store.SessionsUserSetting.SessionR\bsessions\x1a\xfd\x01\n" + "\aSession\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12;\n" + "\vcreate_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "createTime\x12;\n" + - "\vexpire_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "expireTime\x12H\n" + - "\x12last_accessed_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x10lastAccessedTime\x12L\n" + - "\vclient_info\x18\x05 \x01(\v2+.memos.store.SessionsUserSetting.ClientInfoR\n" + + "createTime\x12H\n" + + "\x12last_accessed_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x10lastAccessedTime\x12L\n" + + "\vclient_info\x18\x04 \x01(\v2+.memos.store.SessionsUserSetting.ClientInfoR\n" + "clientInfo\x1a\x95\x01\n" + "\n" + "ClientInfo\x12\x1d\n" + @@ -919,14 +909,13 @@ var file_store_user_setting_proto_depIdxs = []int32{ 10, // 8: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut 11, // 9: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook 12, // 10: memos.store.SessionsUserSetting.Session.create_time:type_name -> google.protobuf.Timestamp - 12, // 11: memos.store.SessionsUserSetting.Session.expire_time:type_name -> google.protobuf.Timestamp - 12, // 12: memos.store.SessionsUserSetting.Session.last_accessed_time:type_name -> google.protobuf.Timestamp - 8, // 13: memos.store.SessionsUserSetting.Session.client_info:type_name -> memos.store.SessionsUserSetting.ClientInfo - 14, // [14:14] is the sub-list for method output_type - 14, // [14:14] is the sub-list for method input_type - 14, // [14:14] is the sub-list for extension type_name - 14, // [14:14] is the sub-list for extension extendee - 0, // [0:14] is the sub-list for field type_name + 12, // 11: memos.store.SessionsUserSetting.Session.last_accessed_time:type_name -> google.protobuf.Timestamp + 8, // 12: memos.store.SessionsUserSetting.Session.client_info:type_name -> memos.store.SessionsUserSetting.ClientInfo + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_store_user_setting_proto_init() } diff --git a/proto/store/user_setting.proto b/proto/store/user_setting.proto index 6a71fe23b..f2c30eaf4 100644 --- a/proto/store/user_setting.proto +++ b/proto/store/user_setting.proto @@ -48,12 +48,11 @@ message SessionsUserSetting { string session_id = 1; // Timestamp when the session was created. google.protobuf.Timestamp create_time = 2; - // Timestamp when the session expires. - google.protobuf.Timestamp expire_time = 3; // Timestamp when the session was last accessed. - google.protobuf.Timestamp last_accessed_time = 4; + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + google.protobuf.Timestamp last_accessed_time = 3; // Client information associated with this session. - ClientInfo client_info = 5; + ClientInfo client_info = 4; } message ClientInfo { diff --git a/server/router/api/v1/acl.go b/server/router/api/v1/acl.go index 182c8bdb4..4b8d5a07e 100644 --- a/server/router/api/v1/acl.go +++ b/server/router/api/v1/acl.go @@ -203,13 +203,16 @@ func (in *GRPCAuthInterceptor) updateSessionLastAccessed(ctx context.Context, us return in.Store.UpdateUserSessionLastAccessed(ctx, userID, sessionID, timestamppb.Now()) } -// validateUserSession checks if a session exists and is still valid. +// validateUserSession checks if a session exists and is still valid using sliding expiration. func validateUserSession(sessionID string, userSessions []*storepb.SessionsUserSetting_Session) bool { for _, session := range userSessions { if sessionID == session.SessionId { - // Check if session has expired - if session.ExpireTime != nil && session.ExpireTime.AsTime().Before(time.Now()) { - return false + // Use sliding expiration: check if last_accessed_time + 2 weeks > current_time + if session.LastAccessedTime != nil { + expirationTime := session.LastAccessedTime.AsTime().Add(SessionSlidingDuration) + if expirationTime.Before(time.Now()) { + return false + } } return true } diff --git a/server/router/api/v1/auth.go b/server/router/api/v1/auth.go index fca59ee05..d3d5fbb43 100644 --- a/server/router/api/v1/auth.go +++ b/server/router/api/v1/auth.go @@ -19,11 +19,10 @@ const ( KeyID = "v1" // AccessTokenAudienceName is the audience name of the access token. AccessTokenAudienceName = "user.access-token" - AccessTokenDuration = 7 * 24 * time.Hour + // SessionSlidingDuration is the sliding expiration duration for user sessions (2 weeks). + // Sessions are considered valid if last_accessed_time + SessionSlidingDuration > current_time. + SessionSlidingDuration = 14 * 24 * time.Hour - // CookieExpDuration expires slightly earlier than the jwt expiration. Client would be logged out if the user - // cookie expires, thus the client would always logout first before attempting to make a request with the expired jwt. - CookieExpDuration = AccessTokenDuration - 1*time.Minute // SessionCookieName is the cookie name of user session ID. SessionCookieName = "user_session" ) diff --git a/server/router/api/v1/auth_service.go b/server/router/api/v1/auth_service.go index bebb6e6a0..742c2caf8 100644 --- a/server/router/api/v1/auth_service.go +++ b/server/router/api/v1/auth_service.go @@ -42,16 +42,20 @@ func (s *APIV1Service) GetCurrentSession(ctx context.Context, _ *v1pb.GetCurrent return nil, status.Errorf(codes.Unauthenticated, "user not found") } - // Update session last accessed time if we have a session ID + var lastAccessedAt *timestamppb.Timestamp + // Update session last accessed time if we have a session ID and get the current session info if sessionID, ok := ctx.Value(sessionIDContextKey).(string); ok && sessionID != "" { - if err := s.Store.UpdateUserSessionLastAccessed(ctx, user.ID, sessionID, timestamppb.Now()); err != nil { + now := timestamppb.Now() + if err := s.Store.UpdateUserSessionLastAccessed(ctx, user.ID, sessionID, now); err != nil { // Log error but don't fail the request slog.Error("failed to update session last accessed time", "error", err) } + lastAccessedAt = now } return &v1pb.GetCurrentSessionResponse{ - User: convertUserFromStore(user), + User: convertUserFromStore(user), + LastAccessedAt: lastAccessedAt, }, nil } @@ -167,18 +171,15 @@ func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSe return nil, status.Errorf(codes.PermissionDenied, "user has been archived with username %s", existingUser.Username) } - expireTime := time.Now().Add(AccessTokenDuration) - if request.NeverExpire { - // Set the expire time to 100 years. - expireTime = time.Now().Add(100 * 365 * 24 * time.Hour) - } + // Default session expiration time is 100 year + expireTime := time.Now().Add(100 * 365 * 24 * time.Hour) if err := s.doSignIn(ctx, existingUser, expireTime); err != nil { return nil, status.Errorf(codes.Internal, "failed to sign in, error: %v", err) } return &v1pb.CreateSessionResponse{ - User: convertUserFromStore(existingUser), - ExpiresAt: timestamppb.New(expireTime), + User: convertUserFromStore(existingUser), + LastAccessedAt: timestamppb.Now(), }, nil } @@ -190,7 +191,7 @@ func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User, expireTim } // Track session in user settings - if err := s.trackUserSession(ctx, user.ID, sessionID, expireTime); err != nil { + if err := s.trackUserSession(ctx, user.ID, sessionID); err != nil { // Log the error but don't fail the login if session tracking fails // This ensures backward compatibility slog.Error("failed to track user session", "error", err) @@ -308,14 +309,13 @@ func (s *APIV1Service) GetCurrentUser(ctx context.Context) (*store.User, error) } // Helper function to track user session for session management. -func (s *APIV1Service) trackUserSession(ctx context.Context, userID int32, sessionID string, expireTime time.Time) error { +func (s *APIV1Service) trackUserSession(ctx context.Context, userID int32, sessionID string) 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, } diff --git a/server/router/api/v1/user_service.go b/server/router/api/v1/user_service.go index 9482cd8c0..93d5687c5 100644 --- a/server/router/api/v1/user_service.go +++ b/server/router/api/v1/user_service.go @@ -650,7 +650,6 @@ func (s *APIV1Service) ListUserSessions(ctx context.Context, request *v1pb.ListU Name: fmt.Sprintf("users/%d/sessions/%s", userID, userSession.SessionId), SessionId: userSession.SessionId, CreateTime: userSession.CreateTime, - ExpireTime: userSession.ExpireTime, LastAccessedTime: userSession.LastAccessedTime, } @@ -715,7 +714,6 @@ func (s *APIV1Service) UpsertUserSession(ctx context.Context, userID int32, sess 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, } diff --git a/web/src/components/PasswordSignInForm.tsx b/web/src/components/PasswordSignInForm.tsx index aba7234f1..a4e8db3ea 100644 --- a/web/src/components/PasswordSignInForm.tsx +++ b/web/src/components/PasswordSignInForm.tsx @@ -1,4 +1,4 @@ -import { Button, Checkbox, Input } from "@usememos/mui"; +import { Button, Input } from "@usememos/mui"; import { LoaderIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; import { ClientError } from "nice-grpc-web"; @@ -17,7 +17,6 @@ const PasswordSignInForm = observer(() => { const actionBtnLoadingState = useLoading(false); const [username, setUsername] = useState(workspaceStore.state.profile.mode === "demo" ? "yourselfhosted" : ""); const [password, setPassword] = useState(workspaceStore.state.profile.mode === "demo" ? "yourselfhosted" : ""); - const [remember, setRemember] = useState(true); const handleUsernameInputChanged = (e: React.ChangeEvent) => { const text = e.target.value as string; @@ -47,7 +46,6 @@ const PasswordSignInForm = observer(() => { actionBtnLoadingState.setLoading(); await authServiceClient.createSession({ passwordCredentials: { username, password }, - neverExpire: remember, }); await initialUserStore(); navigateTo("/"); @@ -94,9 +92,6 @@ const PasswordSignInForm = observer(() => { /> -
- setRemember(e.target.checked)} /> -
- {userSession.expireTime?.toLocaleString() ?? t("setting.user-sessions-section.never")} +
+ + + {getSessionExpirationDate(userSession)?.toLocaleString() ?? t("setting.user-sessions-section.never")} + {isSessionExpired(userSession) && (Expired)} + +