feat(api): список событий безопасности аккаунта
GET /users/@me/security-events отдаёт последние входы, неудачные попытки, смены пароля и изменения 2FA (AGENT.md 7.1: уведомления о новых входах — основа для настроек безопасности в клиенте).
This commit is contained in:
@@ -154,6 +154,27 @@ func TestProfileUpdateFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSecurityEventsListed(t *testing.T) {
|
||||||
|
srv, _ := newTestServer(t)
|
||||||
|
cookie := registerAndLogin(t, srv, "events_user", "events@example.com")
|
||||||
|
|
||||||
|
rec := doJSON(t, srv, http.MethodGet, "/api/v1/users/@me/security-events", "", cookie)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("security events = %d, body = %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
payload := decodeResponse[struct {
|
||||||
|
Events []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"events"`
|
||||||
|
}](t, rec)
|
||||||
|
if len(payload.Events) == 0 {
|
||||||
|
t.Fatal("registration must leave a security event")
|
||||||
|
}
|
||||||
|
if payload.Events[0].Type != "register" {
|
||||||
|
t.Fatalf("first event = %q, want register", payload.Events[0].Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGuildLifecycleAndPermissions(t *testing.T) {
|
func TestGuildLifecycleAndPermissions(t *testing.T) {
|
||||||
srv, _ := newTestServer(t)
|
srv, _ := newTestServer(t)
|
||||||
ownerCookie := registerAndLogin(t, srv, "guild_owner", "owner@example.com")
|
ownerCookie := registerAndLogin(t, srv, "guild_owner", "owner@example.com")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/danielgtaylor/huma/v2"
|
"github.com/danielgtaylor/huma/v2"
|
||||||
|
|
||||||
@@ -105,6 +106,21 @@ type onboardingInput struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// securityEventPayload — событие безопасности без служебных метаданных.
|
||||||
|
type securityEventPayload struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type securityEventListOutput struct {
|
||||||
|
Body struct {
|
||||||
|
Events []securityEventPayload `json:"events"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type guildSummary struct {
|
type guildSummary struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -236,6 +252,39 @@ func (s *Server) registerUserRoutes(api huma.API) {
|
|||||||
return output, nil
|
return output, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
huma.Register(api, huma.Operation{
|
||||||
|
OperationID: "listSecurityEvents",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/users/@me/security-events",
|
||||||
|
Summary: "Последние события безопасности аккаунта (входы, смена пароля, 2FA)",
|
||||||
|
Tags: []string{"Users"},
|
||||||
|
Security: security,
|
||||||
|
}, func(ctx context.Context, input *struct {
|
||||||
|
Limit int `query:"limit" default:"20" minimum:"1" maximum:"100"`
|
||||||
|
},
|
||||||
|
) (*securityEventListOutput, error) {
|
||||||
|
user, _, err := requireUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
events, err := s.store.ListSecurityEvents(ctx, user.ID, input.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, humaError(err)
|
||||||
|
}
|
||||||
|
output := &securityEventListOutput{}
|
||||||
|
output.Body.Events = make([]securityEventPayload, 0, len(events))
|
||||||
|
for _, event := range events {
|
||||||
|
output.Body.Events = append(output.Body.Events, securityEventPayload{
|
||||||
|
ID: formatSnowflake(event.ID),
|
||||||
|
Type: event.Type,
|
||||||
|
IP: event.IP,
|
||||||
|
UserAgent: event.UserAgent,
|
||||||
|
CreatedAt: event.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
})
|
||||||
|
|
||||||
huma.Register(api, huma.Operation{
|
huma.Register(api, huma.Operation{
|
||||||
OperationID: "getUser",
|
OperationID: "getUser",
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
|
|||||||
Reference in New Issue
Block a user