Move loginRequest and logoutRequest to server/fleet/ (#45908)

Resolves #36087 (one of several small PRs).

- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Reorganized internal API session models for improved code structure
and maintainability.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45908?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Lucas Manuel Rodriguez
2026-05-21 09:57:07 -03:00
committed by GitHub
parent 6f019ca8ea
commit ce66f3dd18
11 changed files with 81 additions and 74 deletions
+39
View File
@@ -0,0 +1,39 @@
package fleet
import (
"time"
)
////////////////////////////////////////////////////////////////////////////////
// Login
////////////////////////////////////////////////////////////////////////////////
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
// If false/omitted, users that require email verification (Fleet MFA) to log in will fail to log in, rather than
// sending an MFA email, since the MFA email will land the user in a browser and complete the login there, rather
// than e.g. in the CLI that initiated the login. As with SSO, the expected behavior for users with MFA is to log
// in with MFA, then grab an API token for use elsewhere.
SupportsEmailVerification bool `json:"supports_email_verification"`
}
type LoginResponse struct {
User *User `json:"user,omitempty"`
AvailableTeams []*TeamSummary `json:"available_teams" renameto:"available_fleets"`
Token string `json:"token,omitempty"`
TokenExpiresAt *time.Time `json:"token_expires_at,omitempty"`
Err error `json:"error,omitempty"`
}
func (r LoginResponse) Error() error { return r.Err }
////////////////////////////////////////////////////////////////////////////////
// Logout
////////////////////////////////////////////////////////////////////////////////
type LogoutResponse struct {
Err error `json:"error,omitempty"`
}
func (r LogoutResponse) Error() error { return r.Err }
+4 -4
View File
@@ -5,13 +5,13 @@ import (
"fmt"
"net/http"
"github.com/fleetdm/fleet/v4/server/service/contract"
"github.com/fleetdm/fleet/v4/server/fleet"
)
// Login attempts to login to the current Fleet instance. If login is successful,
// an auth token is returned.
func (c *Client) Login(email, password string) (string, error) {
params := contract.LoginRequest{
params := fleet.LoginRequest{
Email: email,
Password: password,
}
@@ -33,7 +33,7 @@ func (c *Client) Login(email, password string) (string, error) {
)
}
var responseBody loginResponse
var responseBody fleet.LoginResponse
err = json.NewDecoder(response.Body).Decode(&responseBody)
if err != nil {
return "", fmt.Errorf("decode login response: %w", err)
@@ -49,6 +49,6 @@ func (c *Client) Login(email, password string) (string, error) {
// Logout attempts to logout to the current Fleet instance.
func (c *Client) Logout() error {
verb, path := "POST", "/api/latest/fleet/logout"
var responseBody logoutResponse
var responseBody fleet.LogoutResponse
return c.authenticatedRequest(nil, verb, path, &responseBody)
}
-11
View File
@@ -1,11 +0,0 @@
package contract
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
// If false/omitted, users that require email verification (Fleet MFA) to log in will fail to log in, rather than
// sending an MFA email, since the MFA email will land the user in a browser and complete the login there, rather
// than e.g. in the CLI that initiated the login. As with SSO, the expected behavior for users with MFA is to log
// in with MFA, then grab an API token for use elsewhere.
SupportsEmailVerification bool `json:"supports_email_verification"`
}
+1 -1
View File
@@ -1168,7 +1168,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
}
ne.WithCustomMiddleware(loginLimiter).
POST("/api/_version_/fleet/login", loginEndpoint, contract.LoginRequest{})
POST("/api/_version_/fleet/login", loginEndpoint, fleet.LoginRequest{})
ne.WithCustomMiddleware(limiter.Limit("mfa", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})).
POST("/api/_version_/fleet/sessions", sessionCreateEndpoint, sessionCreateRequest{})
+2 -3
View File
@@ -18,7 +18,6 @@ import (
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/service/contract"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -56,7 +55,7 @@ func TestLogin(t *testing.T) {
// test sessions
testUser := users[tt.email]
params := contract.LoginRequest{
params := fleet.LoginRequest{
Email: tt.email,
Password: tt.password,
}
@@ -192,7 +191,7 @@ func getTestAdminToken(t *testing.T, server *httptest.Server) string {
func getTestUserToken(t *testing.T, server *httptest.Server, testUserId string) string {
testUser := testUsers[testUserId]
params := contract.LoginRequest{
params := fleet.LoginRequest{
Email: testUser.Email,
Password: testUser.PlaintextPassword,
}
@@ -20,7 +20,6 @@ import (
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/contract"
scep_server "github.com/fleetdm/fleet/v4/server/service/integrationtest/scep_server"
"github.com/fleetdm/fleet/v4/server/worker"
"github.com/google/uuid"
@@ -1398,8 +1397,8 @@ func (s *integrationMDMTestSuite) TestCertificateTemplateAuthorizationForTeamUse
caID := ca.ID
// Login as team admin
var loginResp loginResponse
s.DoJSON("POST", "/api/latest/fleet/login", contract.LoginRequest{
var loginResp fleet.LoginResponse
s.DoJSON("POST", "/api/latest/fleet/login", fleet.LoginRequest{
Email: teamAdminEmail,
Password: teamAdminPassword,
}, http.StatusOK, &loginResp)
+17 -17
View File
@@ -681,7 +681,7 @@ func (s *integrationTestSuite) TestActivityUserEmailPersistsAfterDeletion() {
assert.True(t, createResp.User.AdminForcedPasswordReset)
u := *createResp.User
var loginResp loginResponse
var loginResp fleet.LoginResponse
s.DoJSON("POST", "/api/latest/fleet/login", params, http.StatusOK, &loginResp)
require.Equal(t, loginResp.User.ID, u.ID)
@@ -734,8 +734,8 @@ func (s *integrationTestSuite) TestPremiumOnlyRoles() {
_, err = s.ds.NewUser(t.Context(), user)
require.NoError(t, err)
var loginResp loginResponse
s.DoJSON("POST", "/api/latest/fleet/login", contract.LoginRequest{
var loginResp fleet.LoginResponse
s.DoJSON("POST", "/api/latest/fleet/login", fleet.LoginRequest{
Email: fmt.Sprintf("%s@example.com", role),
Password: test.GoodPassword,
}, http.StatusPaymentRequired, &loginResp)
@@ -6437,7 +6437,7 @@ func (s *integrationTestSuite) TestUsers() {
assert.True(t, createResp.User.AdminForcedPasswordReset)
u := *createResp.User
var loginResp loginResponse
var loginResp fleet.LoginResponse
// try MFA
mysqltest.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
@@ -6449,7 +6449,7 @@ func (s *integrationTestSuite) TestUsers() {
s.DoJSONWithoutAuth("POST", "/api/latest/fleet/login", params, http.StatusBadRequest, &loginResp)
// MFA supported; send email
s.DoJSONWithoutAuth("POST", "/api/latest/fleet/login",
contract.LoginRequest{Email: "extra@asd.com", Password: userRawPwd, SupportsEmailVerification: true}, http.StatusAccepted, &loginResp)
fleet.LoginRequest{Email: "extra@asd.com", Password: userRawPwd, SupportsEmailVerification: true}, http.StatusAccepted, &loginResp)
var mfaToken string
mysqltest.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), tx, &mfaToken, `SELECT token FROM verification_tokens WHERE user_id = ? LIMIT 1`, createResp.User.ID)
@@ -6461,7 +6461,7 @@ func (s *integrationTestSuite) TestUsers() {
// send another email, which we'll expire the token for
s.DoJSONWithoutAuth("POST", "/api/latest/fleet/login",
contract.LoginRequest{Email: "extra@asd.com", Password: userRawPwd, SupportsEmailVerification: true}, http.StatusAccepted, &loginResp)
fleet.LoginRequest{Email: "extra@asd.com", Password: userRawPwd, SupportsEmailVerification: true}, http.StatusAccepted, &loginResp)
mysqltest.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
@@ -6587,13 +6587,13 @@ func (s *integrationTestSuite) TestUsers() {
s.token = s.getTestAdminToken()
// login as that user to verify that the new password is active (userRawPwd was updated to the new pwd)
loginResp = loginResponse{}
s.DoJSON("POST", "/api/latest/fleet/login", contract.LoginRequest{Email: u.Email, Password: userRawPwd}, http.StatusOK, &loginResp)
loginResp = fleet.LoginResponse{}
s.DoJSON("POST", "/api/latest/fleet/login", fleet.LoginRequest{Email: u.Email, Password: userRawPwd}, http.StatusOK, &loginResp)
require.Equal(t, loginResp.User.ID, u.ID)
// logout for that user
s.token = loginResp.Token
var logoutResp logoutResponse
var logoutResp fleet.LogoutResponse
s.DoJSON("POST", "/api/latest/fleet/logout", nil, http.StatusOK, &logoutResp)
// logout again, even though not logged in
@@ -6602,8 +6602,8 @@ func (s *integrationTestSuite) TestUsers() {
s.token = s.getTestAdminToken()
// login as that user with previous pwd fails
loginResp = loginResponse{}
s.DoJSON("POST", "/api/latest/fleet/login", contract.LoginRequest{Email: u.Email, Password: oldUserRawPwd}, http.StatusUnauthorized, &loginResp)
loginResp = fleet.LoginResponse{}
s.DoJSON("POST", "/api/latest/fleet/login", fleet.LoginRequest{Email: u.Email, Password: oldUserRawPwd}, http.StatusUnauthorized, &loginResp)
// require a password reset
var reqResetResp requirePasswordResetResponse
@@ -10067,7 +10067,7 @@ func (s *integrationTestSuite) TestLogLoginAttempts() {
// Login with invalid passwordm, should fail.
res := s.DoRawNoAuth("POST", "/api/latest/fleet/login",
jsonMustMarshal(t, contract.LoginRequest{Email: u.Email, Password: test.GoodPassword2}),
jsonMustMarshal(t, fleet.LoginRequest{Email: u.Email, Password: test.GoodPassword2}),
http.StatusUnauthorized,
)
res.Body.Close()
@@ -10090,7 +10090,7 @@ func (s *integrationTestSuite) TestLogLoginAttempts() {
// login with good password, should succeed
res = s.DoRawNoAuth("POST", "/api/latest/fleet/login",
jsonMustMarshal(t, contract.LoginRequest{
jsonMustMarshal(t, fleet.LoginRequest{
Email: u.Email,
Password: test.GoodPassword,
}), http.StatusOK,
@@ -10256,12 +10256,12 @@ func (s *integrationTestSuite) TestPasswordReset() {
res.Body.Close()
// login with the old password, should not succeed
res = s.DoRawNoAuth("POST", "/api/latest/fleet/login", jsonMustMarshal(t, contract.LoginRequest{Email: u.Email, Password: userRawPwd}),
res = s.DoRawNoAuth("POST", "/api/latest/fleet/login", jsonMustMarshal(t, fleet.LoginRequest{Email: u.Email, Password: userRawPwd}),
http.StatusUnauthorized)
res.Body.Close()
// login with the new password, should succeed
res = s.DoRawNoAuth("POST", "/api/latest/fleet/login", jsonMustMarshal(t, contract.LoginRequest{Email: u.Email, Password: userNewPwd}),
res = s.DoRawNoAuth("POST", "/api/latest/fleet/login", jsonMustMarshal(t, fleet.LoginRequest{Email: u.Email, Password: userNewPwd}),
http.StatusOK)
res.Body.Close()
}
@@ -10360,8 +10360,8 @@ func (s *integrationTestSuite) TestModifyUser() {
}, http.StatusUnprocessableEntity, &modResp)
// login as the user, with the last password successfully set (to confirm it is the current one)
var loginResp loginResponse
resp := s.DoRawNoAuth("POST", "/api/latest/fleet/login", jsonMustMarshal(t, contract.LoginRequest{
var loginResp fleet.LoginResponse
resp := s.DoRawNoAuth("POST", "/api/latest/fleet/login", jsonMustMarshal(t, fleet.LoginRequest{
Email: u.Email, // all email changes made are still pending, never confirmed
Password: newRawPwd,
}), http.StatusOK)
+5 -5
View File
@@ -117,19 +117,19 @@ func (s *integrationLoggerTestSuite) TestLoggerLogin() {
}
testCases := []struct {
loginRequest contract.LoginRequest
loginRequest fleet.LoginRequest
expectedStatus int
expectedLevel slog.Level
expectedAttrs []expectedAttr
}{
{
loginRequest: contract.LoginRequest{Email: testUsers["admin1"].Email, Password: testUsers["admin1"].PlaintextPassword},
loginRequest: fleet.LoginRequest{Email: testUsers["admin1"].Email, Password: testUsers["admin1"].PlaintextPassword},
expectedStatus: http.StatusOK,
expectedLevel: slog.LevelInfo,
expectedAttrs: []expectedAttr{{"email", testUsers["admin1"].Email}},
},
{
loginRequest: contract.LoginRequest{Email: testUsers["admin1"].Email, Password: "n074v411dp455w02d"},
loginRequest: fleet.LoginRequest{Email: testUsers["admin1"].Email, Password: "n074v411dp455w02d"},
expectedStatus: http.StatusUnauthorized,
expectedLevel: slog.LevelInfo,
expectedAttrs: []expectedAttr{
@@ -138,7 +138,7 @@ func (s *integrationLoggerTestSuite) TestLoggerLogin() {
},
},
{
loginRequest: contract.LoginRequest{Email: "h4x0r@3x4mp13.c0m", Password: "n074v411dp455w02d"},
loginRequest: fleet.LoginRequest{Email: "h4x0r@3x4mp13.c0m", Password: "n074v411dp455w02d"},
expectedStatus: http.StatusUnauthorized,
expectedLevel: slog.LevelInfo,
expectedAttrs: []expectedAttr{
@@ -147,7 +147,7 @@ func (s *integrationLoggerTestSuite) TestLoggerLogin() {
},
},
}
var resp loginResponse
var resp fleet.LoginResponse
for _, tt := range testCases {
s.DoJSON("POST", "/api/latest/fleet/login", tt.loginRequest, tt.expectedStatus, &resp)
+9 -26
View File
@@ -21,7 +21,6 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mail"
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
"github.com/fleetdm/fleet/v4/server/service/contract"
"github.com/fleetdm/fleet/v4/server/sso"
)
@@ -116,16 +115,6 @@ func (svc *Service) DeleteSession(ctx context.Context, id uint) error {
// Login
////////////////////////////////////////////////////////////////////////////////
type loginResponse struct {
User *fleet.User `json:"user,omitempty"`
AvailableTeams []*fleet.TeamSummary `json:"available_teams" renameto:"available_fleets"`
Token string `json:"token,omitempty"`
TokenExpiresAt *time.Time `json:"token_expires_at,omitempty"`
Err error `json:"error,omitempty"`
}
func (r loginResponse) Error() error { return r.Err }
type loginMfaResponse struct {
Message string `json:"message"`
Err error `json:"error,omitempty"`
@@ -136,7 +125,7 @@ func (r loginMfaResponse) Status() int { return http.StatusAccepted }
func (r loginMfaResponse) Error() error { return r.Err }
func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*contract.LoginRequest)
req := request.(*fleet.LoginRequest)
req.Email = strings.ToLower(req.Email)
user, session, err := svc.Login(ctx, req.Email, req.Password, req.SupportsEmailVerification)
@@ -145,7 +134,7 @@ func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service)
return loginMfaResponse{Message: "We sent an email to you. Please click the magic link in the email to sign in."}, nil
}
return loginResponse{Err: err}, nil
return fleet.LoginResponse{Err: err}, nil
}
// Add viewer to context to allow access to service teams for list of available teams.
ctx = viewer.NewContext(ctx, viewer.Viewer{
@@ -157,7 +146,7 @@ func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service)
if errors.Is(err, fleet.ErrMissingLicense) {
availableTeams = []*fleet.TeamSummary{}
} else {
return loginResponse{Err: err}, nil
return fleet.LoginResponse{Err: err}, nil
}
}
@@ -168,7 +157,7 @@ func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service)
tokenExpiresAt = &expiresAt
}
return loginResponse{
return fleet.LoginResponse{
User: user,
AvailableTeams: availableTeams,
Token: session.Key,
@@ -284,7 +273,7 @@ func sessionCreateEndpoint(ctx context.Context, request interface{}, svc fleet.S
req := request.(*sessionCreateRequest)
session, user, err := svc.CompleteMFA(ctx, req.Token)
if err != nil {
return loginResponse{Err: err}, nil
return fleet.LoginResponse{Err: err}, nil
}
// Add viewer to context to allow access to service teams for list of available teams.
ctx = viewer.NewContext(ctx, viewer.Viewer{
@@ -296,7 +285,7 @@ func sessionCreateEndpoint(ctx context.Context, request interface{}, svc fleet.S
if errors.Is(err, fleet.ErrMissingLicense) {
availableTeams = []*fleet.TeamSummary{}
} else {
return loginResponse{Err: err}, nil
return fleet.LoginResponse{Err: err}, nil
}
}
@@ -307,7 +296,7 @@ func sessionCreateEndpoint(ctx context.Context, request interface{}, svc fleet.S
tokenExpiresAt = &expiresAt
}
return loginResponse{
return fleet.LoginResponse{
User: user,
AvailableTeams: availableTeams,
Token: session.Key,
@@ -344,18 +333,12 @@ func (svc *Service) CompleteMFA(ctx context.Context, token string) (*fleet.Sessi
// Logout
////////////////////////////////////////////////////////////////////////////////
type logoutResponse struct {
Err error `json:"error,omitempty"`
}
func (r logoutResponse) Error() error { return r.Err }
func logoutEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
err := svc.Logout(ctx)
if err != nil {
return logoutResponse{Err: err}, nil
return fleet.LogoutResponse{Err: err}, nil
}
return logoutResponse{}, nil
return fleet.LogoutResponse{}, nil
}
func (svc *Service) Logout(ctx context.Context) error {
+1 -2
View File
@@ -16,7 +16,6 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/service/contract"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -79,7 +78,7 @@ func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User {
// GetToken posts to /api/latest/fleet/login and returns the auth token. It
// fails the test on any error.
func GetToken(t *testing.T, email string, password string, serverURL string) string {
params := contract.LoginRequest{
params := fleet.LoginRequest{
Email: email,
Password: password,
}
+1 -2
View File
@@ -29,7 +29,6 @@ import (
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/fleetdm/fleet/v4/server/pubsub"
"github.com/fleetdm/fleet/v4/server/service/contract"
"github.com/fleetdm/fleet/v4/server/test"
fleet_httptest "github.com/fleetdm/fleet/v4/server/test/httptest"
"github.com/ghodss/yaml"
@@ -391,7 +390,7 @@ func (ts *withServer) getTestToken(email string, password string) string {
}
func GetToken(t *testing.T, email string, password string, serverURL string) string {
params := contract.LoginRequest{
params := fleet.LoginRequest{
Email: email,
Password: password,
}