From 51950122179bd2a85d2f9389361f350143e47ce9 Mon Sep 17 00:00:00 2001 From: boojack Date: Sun, 1 Jan 2023 23:55:02 +0800 Subject: [PATCH] feat: add `activity` table (#888) feat: introduce activity --- api/activity.go | 94 +++++++++++++++++++++++ api/api.go | 3 + go.mod | 1 + go.sum | 2 + server/auth.go | 26 ++++++- store/activity.go | 84 ++++++++++++++++++++ store/db/migration/dev/LATEST__SCHEMA.sql | 10 +++ 7 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 api/activity.go create mode 100644 store/activity.go diff --git a/api/activity.go b/api/activity.go new file mode 100644 index 00000000..ed80496b --- /dev/null +++ b/api/activity.go @@ -0,0 +1,94 @@ +package api + +// ActivityType is the type for an activity. +type ActivityType string + +const ( + // User related. + + // ActivityUserCreate is the type for creating users. + ActivityUserCreate ActivityType = "user.create" + // ActivityUserUpdate is the type for updating users. + ActivityUserUpdate ActivityType = "user.update" + // ActivityUserDelete is the type for deleting users. + ActivityUserDelete ActivityType = "user.delete" + // ActivityUserAuthSignIn is the type for user signin. + ActivityUserAuthSignIn ActivityType = "user.auth.signin" + // ActivityUserAuthSignUp is the type for user signup. + ActivityUserAuthSignUp ActivityType = "user.auth.signup" + // ActivityUserAuthSignOut is the type for user signout. + ActivityUserAuthSignOut ActivityType = "user.auth.signout" + // ActivityUserSettingUpdate is the type for updating user settings. + ActivityUserSettingUpdate ActivityType = "user.setting.update" + + // Memo related. + + // ActivityMemoCreate is the type for creating memos. + ActivityMemoCreate ActivityType = "memo.create" + // ActivityMemoUpdate is the type for updating memos. + ActivityMemoUpdate ActivityType = "memo.update" + // ActivityMemoDelete is the type for deleting memos. + ActivityMemoDelete ActivityType = "memo.delete" + + // Shortcut related. + + // ActivityShortcutCreate is the type for creating shortcuts. + ActivityShortcutCreate ActivityType = "shortcut.create" + // ActivityShortcutUpdate is the type for updating shortcuts. + ActivityShortcutUpdate ActivityType = "shortcut.update" + // ActivityShortcutDelete is the type for deleting shortcuts. + ActivityShortcutDelete ActivityType = "shortcut.delete" + + // Tag related. + + // ActivityTagCreate is the type for creating tags. + ActivityTagCreate ActivityType = "tag.create" + // ActivityTagDelete is the type for deleting tags. + ActivityTagDelete ActivityType = "tag.delete" + + // Server related. + + // ActivityServerStart is the type for starting server. + ActivityServerStart ActivityType = "server.start" +) + +// ActivityLevel is the level of activities. +type ActivityLevel string + +const ( + // ActivityInfo is the INFO level of activities. + ActivityInfo ActivityLevel = "INFO" + // ActivityWarn is the WARN level of activities. + ActivityWarn ActivityLevel = "WARN" + // ActivityError is the ERROR level of activities. + ActivityError ActivityLevel = "ERROR" +) + +type ActivityUserAuthSignInPayload struct { + UserID int `json:"userId"` + IP string `json:"ip"` +} + +type Activity struct { + ID int `json:"id"` + + // Standard fields + CreatorID int `json:"creatorId"` + CreatedTs int64 `json:"createdTs"` + + // Domain specific fields + Type ActivityType `json:"type"` + Level ActivityLevel `json:"level"` + Payload string `json:"payload"` +} + +// ActivityCreate is the API message for creating an activity. +type ActivityCreate struct { + // Standard fields + CreatorID int + + // Domain specific fields + Type ActivityType `json:"type"` + Level ActivityLevel + Payload string `json:"payload"` +} diff --git a/api/api.go b/api/api.go index c2b5f683..4f16292d 100644 --- a/api/api.go +++ b/api/api.go @@ -1,5 +1,8 @@ package api +// UnknownID is the ID for unknowns. +const UnknownID = -1 + // RowStatus is the status for a row. type RowStatus string diff --git a/go.mod b/go.mod index 9e2b69ac..121944b1 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( ) require ( + github.com/pkg/errors v0.9.1 github.com/segmentio/analytics-go v3.1.0+incompatible golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 ) diff --git a/go.sum b/go.sum index 3c318af8..c749cff1 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= diff --git a/server/auth.go b/server/auth.go index e92cb893..e12fbd71 100644 --- a/server/auth.go +++ b/server/auth.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/pkg/errors" "github.com/usememos/memos/api" "github.com/usememos/memos/common" metric "github.com/usememos/memos/plugin/metrics" @@ -43,9 +44,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) { if err = setUserSession(c, user); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err) } - s.Collector.Collect(ctx, &metric.Metric{ - Name: "user signed in", - }) + if err := s.createUserAuthSignInActivity(c, user); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err) + } c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil { @@ -143,3 +144,22 @@ func (s *Server) registerAuthRoutes(g *echo.Group) { return c.JSON(http.StatusOK, true) }) } + +func (s *Server) createUserAuthSignInActivity(c echo.Context, user *api.User) error { + ctx := c.Request().Context() + payload := api.ActivityUserAuthSignInPayload{ + UserID: user.ID, + IP: echo.ExtractIPFromRealIPHeader()(c.Request()), + } + payloadStr, err := json.Marshal(payload) + if err != nil { + return errors.Wrap(err, "failed to malshal activity payload") + } + _, err = s.Store.CreateActivity(ctx, &api.ActivityCreate{ + CreatorID: user.ID, + Type: api.ActivityUserAuthSignIn, + Level: api.ActivityInfo, + Payload: string(payloadStr), + }) + return err +} diff --git a/store/activity.go b/store/activity.go new file mode 100644 index 00000000..6a20c75e --- /dev/null +++ b/store/activity.go @@ -0,0 +1,84 @@ +package store + +import ( + "context" + "database/sql" + + "github.com/usememos/memos/api" +) + +// activityRaw is the store model for an Activity. +// Fields have exactly the same meanings as Activity. +type activityRaw struct { + ID int + + // Standard fields + CreatorID int + CreatedTs int64 + + // Domain specific fields + Type api.ActivityType + Level api.ActivityLevel + Payload string +} + +// toActivity creates an instance of Activity based on the ActivityRaw. +func (raw *activityRaw) toActivity() *api.Activity { + return &api.Activity{ + ID: raw.ID, + + CreatorID: raw.CreatorID, + CreatedTs: raw.CreatedTs, + + Type: raw.Type, + Level: raw.Level, + Payload: raw.Payload, + } +} + +// CreateActivity creates an instance of Activity. +func (s *Store) CreateActivity(ctx context.Context, create *api.ActivityCreate) (*api.Activity, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, FormatError(err) + } + defer tx.Rollback() + + activityRaw, err := createActivity(ctx, tx, create) + if err != nil { + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, FormatError(err) + } + + return activityRaw.toActivity(), nil +} + +// createActivity creates a new activity. +func createActivity(ctx context.Context, tx *sql.Tx, create *api.ActivityCreate) (*activityRaw, error) { + query := ` + INSERT INTO activity ( + creator_id, + type, + level, + payload + ) + VALUES (?, ?, ?, ?) + RETURNING id, type, level, payload, creator_id, created_ts + ` + var activityRaw activityRaw + if err := tx.QueryRowContext(ctx, query, create.CreatorID, create.Type, create.Level, create.Payload).Scan( + &activityRaw.ID, + &activityRaw.Type, + &activityRaw.Level, + &activityRaw.Payload, + &activityRaw.CreatedTs, + &activityRaw.CreatedTs, + ); err != nil { + return nil, FormatError(err) + } + + return &activityRaw, nil +} diff --git a/store/db/migration/dev/LATEST__SCHEMA.sql b/store/db/migration/dev/LATEST__SCHEMA.sql index 3b7b4adb..7e94c601 100644 --- a/store/db/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/migration/dev/LATEST__SCHEMA.sql @@ -93,3 +93,13 @@ CREATE TABLE tag ( creator_id INTEGER NOT NULL, UNIQUE(name, creator_id) ); + +-- activity +CREATE TABLE activity ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + type TEXT NOT NULL DEFAULT '', + level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO', + payload TEXT NOT NULL DEFAULT '{}' +);