From 4e46fee80959a3e6c1f25bde2fc8668fea631ca1 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Sun, 19 Jul 2026 00:06:21 +0800 Subject: [PATCH] add model identities and restore httpx router --- backend/cmd/api/main.go | 4 +- backend/go.mod | 2 +- backend/go.sum | 2 + backend/internal/{router => httpx}/router.go | 29 ++- backend/internal/httpx/router_test.go | 71 +++++++ backend/internal/models/ai_call_log.go | 18 +- backend/internal/models/ai_key.go | 2 + backend/internal/models/ai_session.go | 17 +- backend/internal/models/identity.go | 202 +++++++++++++++++++ backend/internal/models/identity_test.go | 49 +++++ backend/internal/models/inbox_item.go | 21 +- backend/internal/models/note.go | 20 +- backend/internal/models/project.go | 14 +- backend/internal/models/project_event.go | 20 +- backend/internal/models/source.go | 26 ++- backend/internal/models/tag.go | 10 +- backend/internal/models/task.go | 29 +-- backend/internal/models/task_share.go | 13 +- backend/internal/models/user.go | 1 + backend/internal/router/router_test.go | 41 ---- 20 files changed, 468 insertions(+), 123 deletions(-) rename backend/internal/{router => httpx}/router.go (58%) create mode 100644 backend/internal/httpx/router_test.go create mode 100644 backend/internal/models/identity.go create mode 100644 backend/internal/models/identity_test.go delete mode 100644 backend/internal/router/router_test.go diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index e7349a6..91af6ee 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -4,11 +4,11 @@ import ( "log" "senlinai-agent/backend/internal/config" + "senlinai-agent/backend/internal/httpx" "senlinai-agent/backend/internal/logic/auth" "senlinai-agent/backend/internal/logic/inbox" "senlinai-agent/backend/internal/logic/projects" "senlinai-agent/backend/internal/models" - "senlinai-agent/backend/internal/router" ) func main() { @@ -22,7 +22,7 @@ func main() { authService := auth.NewService(cfg.AuthSecret) projectHandler := projects.NewHandler(projects.NewService()) inboxHandler := inbox.NewHandler(inbox.NewService(inbox.StaticAnalyzer{})) - appRouter := router.NewProtectedRouter(cfg, authService.VerifySession, projectHandler, inboxHandler) + appRouter := httpx.NewProtectedRouter(cfg, authService.VerifySession, projectHandler, inboxHandler) if err := appRouter.Run(":" + cfg.Port); err != nil { log.Fatal(err) } diff --git a/backend/go.mod b/backend/go.mod index e426fe2..eae07b5 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -26,7 +26,7 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index acdc610..4773bc7 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -40,6 +40,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= diff --git a/backend/internal/router/router.go b/backend/internal/httpx/router.go similarity index 58% rename from backend/internal/router/router.go rename to backend/internal/httpx/router.go index 1f07676..4ce1664 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/httpx/router.go @@ -1,4 +1,4 @@ -package router +package httpx import ( "net/http" @@ -27,6 +27,7 @@ func newRouter(cfg config.Config, tokenVerifier func(string) (uint, error), regi router := gin.New() router.Use(gin.Recovery()) + router.Use(cors()) router.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) @@ -41,3 +42,29 @@ func newRouter(cfg config.Config, tokenVerifier func(string) (uint, error), regi return router } + +func cors() gin.HandlerFunc { + allowedOrigins := map[string]bool{ + "http://localhost:5173": true, + "http://127.0.0.1:5173": true, + "http://tauri.localhost": true, + } + + return func(c *gin.Context) { + origin := c.GetHeader("Origin") + if allowedOrigins[origin] { + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Vary", "Origin") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type") + c.Header("Access-Control-Max-Age", "86400") + } + + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + return + } + + c.Next() + } +} diff --git a/backend/internal/httpx/router_test.go b/backend/internal/httpx/router_test.go new file mode 100644 index 0000000..6903fb1 --- /dev/null +++ b/backend/internal/httpx/router_test.go @@ -0,0 +1,71 @@ +package httpx + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "senlinai-agent/backend/internal/config" +) + +func TestHealthz(t *testing.T) { + router := NewRouter(config.Config{Env: "test"}) + req := httptest.NewRequest(http.MethodGet, "/healthz", nil) + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.JSONEq(t, `{"status":"ok"}`, rec.Body.String()) +} + +func TestNewRouterRegistersFeatureRoutesUnderAPI(t *testing.T) { + router := NewRouter(config.Config{Env: "test"}, testRegistrar{}) + req := httptest.NewRequest(http.MethodGet, "/api/ping", nil) + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.JSONEq(t, `{"pong":true}`, rec.Body.String()) +} + +func TestRouterAddsCORSHeadersForLocalWebClient(t *testing.T) { + router := NewRouter(config.Config{Env: "test"}, testRegistrar{}) + req := httptest.NewRequest(http.MethodGet, "/api/ping", nil) + req.Header.Set("Origin", "http://localhost:5173") + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "http://localhost:5173", rec.Header().Get("Access-Control-Allow-Origin")) + require.Contains(t, rec.Header().Get("Access-Control-Allow-Headers"), "Authorization") +} + +func TestRouterHandlesCORSPreflightBeforeAuth(t *testing.T) { + router := NewProtectedRouter(config.Config{Env: "test"}, func(token string) (uint, error) { + return 0, http.ErrNoCookie + }, testRegistrar{}) + req := httptest.NewRequest(http.MethodOptions, "/api/ping", nil) + req.Header.Set("Origin", "http://localhost:5173") + req.Header.Set("Access-Control-Request-Method", "GET") + req.Header.Set("Access-Control-Request-Headers", "Authorization") + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusNoContent, rec.Code) + require.Equal(t, "http://localhost:5173", rec.Header().Get("Access-Control-Allow-Origin")) + require.Contains(t, rec.Header().Get("Access-Control-Allow-Headers"), "Authorization") +} + +type testRegistrar struct{} + +func (testRegistrar) Register(router gin.IRouter) { + router.GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"pong": true}) + }) +} diff --git a/backend/internal/models/ai_call_log.go b/backend/internal/models/ai_call_log.go index 019b082..dfddb20 100644 --- a/backend/internal/models/ai_call_log.go +++ b/backend/internal/models/ai_call_log.go @@ -3,14 +3,16 @@ package models import "time" type SenlinAgentAICallLog struct { - ID uint `gorm:"primaryKey"` - UserID uint `gorm:"index;not null"` - Provider string `gorm:"not null"` - UsedKeyType string `gorm:"not null"` - Action string `gorm:"not null"` - Status string `gorm:"not null"` - Error string - CreatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + UserID uint `gorm:"index;not null"` + UserIdentity string `gorm:"type:char(36);index"` + Provider string `gorm:"not null"` + UsedKeyType string `gorm:"not null"` + Action string `gorm:"not null"` + Status string `gorm:"not null"` + Error string + CreatedAt time.Time } func (SenlinAgentAICallLog) TableName() string { diff --git a/backend/internal/models/ai_key.go b/backend/internal/models/ai_key.go index 980efb1..a19ed05 100644 --- a/backend/internal/models/ai_key.go +++ b/backend/internal/models/ai_key.go @@ -4,7 +4,9 @@ import "time" type SenlinAgentAIKey struct { ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` UserID uint `gorm:"uniqueIndex;not null"` + UserIdentity string `gorm:"type:char(36);index"` Provider string `gorm:"not null"` EncryptedAPIKey string `gorm:"not null"` CreatedAt time.Time diff --git a/backend/internal/models/ai_session.go b/backend/internal/models/ai_session.go index 9e55906..b5c4d58 100644 --- a/backend/internal/models/ai_session.go +++ b/backend/internal/models/ai_session.go @@ -3,13 +3,16 @@ package models import "time" type SenlinAgentAISession struct { - ID uint `gorm:"primaryKey"` - ProjectID uint `gorm:"index;not null"` - CreatedBy uint `gorm:"index;not null"` - Title string `gorm:"not null"` - Context string `gorm:"type:text"` - CreatedAt time.Time - UpdatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + ProjectID uint `gorm:"index;not null"` + ProjectIdentity string `gorm:"type:char(36);index"` + CreatedBy uint `gorm:"index;not null"` + CreatedByIdentity string `gorm:"type:char(36);index"` + Title string `gorm:"not null"` + Context string `gorm:"type:text"` + CreatedAt time.Time + UpdatedAt time.Time } func (SenlinAgentAISession) TableName() string { diff --git a/backend/internal/models/identity.go b/backend/internal/models/identity.go new file mode 100644 index 0000000..885f1e6 --- /dev/null +++ b/backend/internal/models/identity.go @@ -0,0 +1,202 @@ +package models + +import ( + "database/sql" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +func (m *SenlinAgentUser) BeforeCreate(tx *gorm.DB) error { + return ensureIdentity(&m.Identity) +} + +func (m *SenlinAgentProject) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + return resolveIdentity(tx, &SenlinAgentUser{}, m.OwnerID, &m.OwnerIdentity) +} + +func (m *SenlinAgentInboxItem) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil { + return err + } + return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity) +} + +func (m *SenlinAgentTask) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil { + return err + } + if err := resolveOptionalIdentity(tx, &SenlinAgentUser{}, m.AssigneeID, &m.AssigneeIdentity); err != nil { + return err + } + return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity) +} + +func (m *SenlinAgentNote) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil { + return err + } + return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity) +} + +func (m *SenlinAgentSource) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil { + return err + } + return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity) +} + +func (m *SenlinAgentAISession) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil { + return err + } + return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity) +} + +func (m *SenlinAgentTag) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + return resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity) +} + +func (m *SenlinAgentProjectEvent) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentUser{}, m.ActorID, &m.ActorIdentity); err != nil { + return err + } + return resolveEntityIdentity(tx, m.EntityType, m.EntityID, &m.EntityIdentity) +} + +func (m *SenlinAgentAIKey) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + return resolveIdentity(tx, &SenlinAgentUser{}, m.UserID, &m.UserIdentity) +} + +func (m *SenlinAgentAICallLog) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + return resolveIdentity(tx, &SenlinAgentUser{}, m.UserID, &m.UserIdentity) +} + +func (m *SenlinAgentTaskShare) BeforeCreate(tx *gorm.DB) error { + if err := ensureIdentity(&m.Identity); err != nil { + return err + } + if err := resolveIdentity(tx, &SenlinAgentTask{}, m.TaskID, &m.TaskIdentity); err != nil { + return err + } + return resolveEntityIdentity(tx, m.ObjectType, m.ObjectID, &m.ObjectIdentity) +} + +func ensureIdentity(identity *string) error { + if *identity != "" { + return nil + } + next, err := uuidV7() + if err != nil { + return err + } + *identity = next + return nil +} + +func resolveIdentity(tx *gorm.DB, model any, id uint, identity *string) error { + if id == 0 || *identity != "" { + return nil + } + resolved, err := lookupIdentity(tx, model, id) + if err != nil { + return err + } + *identity = resolved + return nil +} + +func resolveOptionalIdentity(tx *gorm.DB, model any, id *uint, identity **string) error { + if id == nil || *id == 0 || (identity != nil && *identity != nil && **identity != "") { + return nil + } + resolved, err := lookupIdentity(tx, model, *id) + if err != nil { + return err + } + *identity = &resolved + return nil +} + +func resolveEntityIdentity(tx *gorm.DB, entityType string, entityID uint, identity *string) error { + if entityID == 0 || *identity != "" { + return nil + } + switch entityType { + case "project": + return resolveIdentity(tx, &SenlinAgentProject{}, entityID, identity) + case "inbox", "inbox_item": + return resolveIdentity(tx, &SenlinAgentInboxItem{}, entityID, identity) + case "task": + return resolveIdentity(tx, &SenlinAgentTask{}, entityID, identity) + case "note": + return resolveIdentity(tx, &SenlinAgentNote{}, entityID, identity) + case "source": + return resolveIdentity(tx, &SenlinAgentSource{}, entityID, identity) + case "ai_session": + return resolveIdentity(tx, &SenlinAgentAISession{}, entityID, identity) + case "tag": + return resolveIdentity(tx, &SenlinAgentTag{}, entityID, identity) + default: + return nil + } +} + +func lookupIdentity(tx *gorm.DB, model any, id uint) (string, error) { + var identity string + err := tx.Model(model).Select("identity").Where("id = ?", id).Row().Scan(&identity) + if err == sql.ErrNoRows { + return "", nil + } + return identity, err +} + +func uuidV7() (string, error) { + next, err := uuid.NewV7() + if err != nil { + return "", err + } + return next.String(), nil +} diff --git a/backend/internal/models/identity_test.go b/backend/internal/models/identity_test.go new file mode 100644 index 0000000..5c4d8cc --- /dev/null +++ b/backend/internal/models/identity_test.go @@ -0,0 +1,49 @@ +package models + +import ( + "fmt" + "testing" + + "github.com/glebarez/sqlite" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestBeforeCreateAssignsUUIDV7Identity(t *testing.T) { + database := newIdentityTestDB(t) + user := SenlinAgentUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"} + + require.NoError(t, database.Create(&user).Error) + + require.NotEmpty(t, user.Identity) + require.Equal(t, byte('7'), user.Identity[14]) +} + +func TestBeforeCreateCopiesParentIdentitiesFromNumericIDs(t *testing.T) { + database := newIdentityTestDB(t) + user := SenlinAgentUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"} + require.NoError(t, database.Create(&user).Error) + project := SenlinAgentProject{OwnerID: user.ID, Name: "Alpha"} + require.NoError(t, database.Create(&project).Error) + item := SenlinAgentInboxItem{ProjectID: project.ID, CreatedBy: user.ID, SourceType: "text"} + require.NoError(t, database.Create(&item).Error) + + task := SenlinAgentTask{ProjectID: project.ID, CreatedBy: user.ID, SourceInboxItemID: &item.ID, Title: "Follow up"} + + require.NoError(t, database.Create(&task).Error) + + require.NotEmpty(t, project.Identity) + require.Equal(t, user.Identity, project.OwnerIdentity) + require.Equal(t, project.Identity, task.ProjectIdentity) + require.Equal(t, user.Identity, task.CreatedByIdentity) + require.NotNil(t, task.SourceInboxItemIdentity) + require.Equal(t, item.Identity, *task.SourceInboxItemIdentity) +} + +func newIdentityTestDB(t *testing.T) *gorm.DB { + t.Helper() + database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, AutoMigrate(database)) + return database +} diff --git a/backend/internal/models/inbox_item.go b/backend/internal/models/inbox_item.go index cc3c9f3..baa66d3 100644 --- a/backend/internal/models/inbox_item.go +++ b/backend/internal/models/inbox_item.go @@ -3,15 +3,18 @@ package models import "time" type SenlinAgentInboxItem struct { - ID uint `gorm:"primaryKey"` - ProjectID uint `gorm:"index;not null"` - CreatedBy uint `gorm:"index;not null"` - SourceType string `gorm:"not null"` - Title string - Body string - Status string `gorm:"not null;default:open"` - CreatedAt time.Time - UpdatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + ProjectID uint `gorm:"index;not null"` + ProjectIdentity string `gorm:"type:char(36);index"` + CreatedBy uint `gorm:"index;not null"` + CreatedByIdentity string `gorm:"type:char(36);index"` + SourceType string `gorm:"not null"` + Title string + Body string + Status string `gorm:"not null;default:open"` + CreatedAt time.Time + UpdatedAt time.Time } func (SenlinAgentInboxItem) TableName() string { diff --git a/backend/internal/models/note.go b/backend/internal/models/note.go index 1cba2d8..885f18f 100644 --- a/backend/internal/models/note.go +++ b/backend/internal/models/note.go @@ -3,14 +3,18 @@ package models import "time" type SenlinAgentNote struct { - ID uint `gorm:"primaryKey"` - ProjectID uint `gorm:"index;not null"` - CreatedBy uint `gorm:"index;not null"` - SourceInboxItemID *uint `gorm:"index"` - Title string `gorm:"not null"` - Markdown string `gorm:"type:text"` - CreatedAt time.Time - UpdatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + ProjectID uint `gorm:"index;not null"` + ProjectIdentity string `gorm:"type:char(36);index"` + CreatedBy uint `gorm:"index;not null"` + CreatedByIdentity string `gorm:"type:char(36);index"` + SourceInboxItemID *uint `gorm:"index"` + SourceInboxItemIdentity *string `gorm:"type:char(36);index"` + Title string `gorm:"not null"` + Markdown string `gorm:"type:text"` + CreatedAt time.Time + UpdatedAt time.Time } func (SenlinAgentNote) TableName() string { diff --git a/backend/internal/models/project.go b/backend/internal/models/project.go index c73ae48..2ef344d 100644 --- a/backend/internal/models/project.go +++ b/backend/internal/models/project.go @@ -3,12 +3,14 @@ package models import "time" type SenlinAgentProject struct { - ID uint `gorm:"primaryKey"` - OwnerID uint `gorm:"index;not null"` - Name string `gorm:"not null"` - Description string - CreatedAt time.Time - UpdatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + OwnerID uint `gorm:"index;not null"` + OwnerIdentity string `gorm:"type:char(36);index"` + Name string `gorm:"not null"` + Description string + CreatedAt time.Time + UpdatedAt time.Time } func (SenlinAgentProject) TableName() string { diff --git a/backend/internal/models/project_event.go b/backend/internal/models/project_event.go index 30b222f..5984af0 100644 --- a/backend/internal/models/project_event.go +++ b/backend/internal/models/project_event.go @@ -3,14 +3,18 @@ package models import "time" type SenlinAgentProjectEvent struct { - ID uint `gorm:"primaryKey"` - ProjectID uint `gorm:"index;not null"` - ActorID uint `gorm:"index;not null"` - EventType string `gorm:"not null"` - EntityType string `gorm:"not null"` - EntityID uint `gorm:"not null"` - Summary string `gorm:"not null"` - CreatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + ProjectID uint `gorm:"index;not null"` + ProjectIdentity string `gorm:"type:char(36);index"` + ActorID uint `gorm:"index;not null"` + ActorIdentity string `gorm:"type:char(36);index"` + EventType string `gorm:"not null"` + EntityType string `gorm:"not null"` + EntityID uint `gorm:"not null;index"` + EntityIdentity string `gorm:"type:char(36);index"` + Summary string `gorm:"not null"` + CreatedAt time.Time } func (SenlinAgentProjectEvent) TableName() string { diff --git a/backend/internal/models/source.go b/backend/internal/models/source.go index 67cd8ae..6eab6e0 100644 --- a/backend/internal/models/source.go +++ b/backend/internal/models/source.go @@ -3,17 +3,21 @@ package models import "time" type SenlinAgentSource struct { - ID uint `gorm:"primaryKey"` - ProjectID uint `gorm:"index;not null"` - CreatedBy uint `gorm:"index;not null"` - SourceInboxItemID *uint `gorm:"index"` - Kind string `gorm:"not null"` - Title string `gorm:"not null"` - URL string - FilePath string - ContentText string `gorm:"type:text"` - CreatedAt time.Time - UpdatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + ProjectID uint `gorm:"index;not null"` + ProjectIdentity string `gorm:"type:char(36);index"` + CreatedBy uint `gorm:"index;not null"` + CreatedByIdentity string `gorm:"type:char(36);index"` + SourceInboxItemID *uint `gorm:"index"` + SourceInboxItemIdentity *string `gorm:"type:char(36);index"` + Kind string `gorm:"not null"` + Title string `gorm:"not null"` + URL string + FilePath string + ContentText string `gorm:"type:text"` + CreatedAt time.Time + UpdatedAt time.Time } func (SenlinAgentSource) TableName() string { diff --git a/backend/internal/models/tag.go b/backend/internal/models/tag.go index 4d36ef2..f143b4d 100644 --- a/backend/internal/models/tag.go +++ b/backend/internal/models/tag.go @@ -3,10 +3,12 @@ package models import "time" type SenlinAgentTag struct { - ID uint `gorm:"primaryKey"` - ProjectID uint `gorm:"index;not null"` - Name string `gorm:"not null"` - CreatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + ProjectID uint `gorm:"index;not null"` + ProjectIdentity string `gorm:"type:char(36);index"` + Name string `gorm:"not null"` + CreatedAt time.Time } func (SenlinAgentTag) TableName() string { diff --git a/backend/internal/models/task.go b/backend/internal/models/task.go index 573e880..52aa439 100644 --- a/backend/internal/models/task.go +++ b/backend/internal/models/task.go @@ -3,18 +3,23 @@ package models import "time" type SenlinAgentTask struct { - ID uint `gorm:"primaryKey"` - ProjectID uint `gorm:"index;not null"` - CreatedBy uint `gorm:"index;not null"` - AssigneeID *uint `gorm:"index"` - SourceInboxItemID *uint `gorm:"index"` - Title string `gorm:"not null"` - Description string - Status string `gorm:"not null;default:open"` - SortOrder int `gorm:"not null;default:0"` - DueAt *time.Time - CreatedAt time.Time - UpdatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + ProjectID uint `gorm:"index;not null"` + ProjectIdentity string `gorm:"type:char(36);index"` + CreatedBy uint `gorm:"index;not null"` + CreatedByIdentity string `gorm:"type:char(36);index"` + AssigneeID *uint `gorm:"index"` + AssigneeIdentity *string `gorm:"type:char(36);index"` + SourceInboxItemID *uint `gorm:"index"` + SourceInboxItemIdentity *string `gorm:"type:char(36);index"` + Title string `gorm:"not null"` + Description string + Status string `gorm:"not null;default:open"` + SortOrder int `gorm:"not null;default:0"` + DueAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time } func (SenlinAgentTask) TableName() string { diff --git a/backend/internal/models/task_share.go b/backend/internal/models/task_share.go index a8754d1..c02094a 100644 --- a/backend/internal/models/task_share.go +++ b/backend/internal/models/task_share.go @@ -3,11 +3,14 @@ package models import "time" type SenlinAgentTaskShare struct { - ID uint `gorm:"primaryKey"` - TaskID uint `gorm:"index;not null"` - ObjectType string `gorm:"not null"` - ObjectID uint `gorm:"not null"` - CreatedAt time.Time + ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` + TaskID uint `gorm:"index;not null"` + TaskIdentity string `gorm:"type:char(36);index"` + ObjectType string `gorm:"not null"` + ObjectID uint `gorm:"not null;index"` + ObjectIdentity string `gorm:"type:char(36);index"` + CreatedAt time.Time } func (SenlinAgentTaskShare) TableName() string { diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index acb6089..86a07d9 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -4,6 +4,7 @@ import "time" type SenlinAgentUser struct { ID uint `gorm:"primaryKey"` + Identity string `gorm:"type:char(36);uniqueIndex"` Email string `gorm:"uniqueIndex;not null"` DisplayName string `gorm:"not null"` PasswordHash string `gorm:"not null"` diff --git a/backend/internal/router/router_test.go b/backend/internal/router/router_test.go deleted file mode 100644 index 2206f3a..0000000 --- a/backend/internal/router/router_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package router - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/require" - "senlinai-agent/backend/internal/config" -) - -func TestHealthz(t *testing.T) { - router := NewRouter(config.Config{Env: "test"}) - req := httptest.NewRequest(http.MethodGet, "/healthz", nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, http.StatusOK, rec.Code) - require.JSONEq(t, `{"status":"ok"}`, rec.Body.String()) -} - -func TestNewRouterRegistersFeatureRoutesUnderAPI(t *testing.T) { - router := NewRouter(config.Config{Env: "test"}, testRegistrar{}) - req := httptest.NewRequest(http.MethodGet, "/api/ping", nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, http.StatusOK, rec.Code) - require.JSONEq(t, `{"pong":true}`, rec.Body.String()) -} - -type testRegistrar struct{} - -func (testRegistrar) Register(router gin.IRouter) { - router.GET("/ping", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"pong": true}) - }) -}