mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
94
api/activity.go
Normal file
94
api/activity.go
Normal file
@ -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"`
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
|
// UnknownID is the ID for unknowns.
|
||||||
|
const UnknownID = -1
|
||||||
|
|
||||||
// RowStatus is the status for a row.
|
// RowStatus is the status for a row.
|
||||||
type RowStatus string
|
type RowStatus string
|
||||||
|
|
||||||
|
1
go.mod
1
go.mod
@ -46,6 +46,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/segmentio/analytics-go v3.1.0+incompatible
|
github.com/segmentio/analytics-go v3.1.0+incompatible
|
||||||
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15
|
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15
|
||||||
)
|
)
|
||||||
|
2
go.sum
2
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 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/usememos/memos/api"
|
"github.com/usememos/memos/api"
|
||||||
"github.com/usememos/memos/common"
|
"github.com/usememos/memos/common"
|
||||||
metric "github.com/usememos/memos/plugin/metrics"
|
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 {
|
if err = setUserSession(c, user); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
|
||||||
}
|
}
|
||||||
s.Collector.Collect(ctx, &metric.Metric{
|
if err := s.createUserAuthSignInActivity(c, user); err != nil {
|
||||||
Name: "user signed in",
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||||
})
|
}
|
||||||
|
|
||||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
|
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)
|
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
|
||||||
|
}
|
||||||
|
84
store/activity.go
Normal file
84
store/activity.go
Normal file
@ -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
|
||||||
|
}
|
@ -93,3 +93,13 @@ CREATE TABLE tag (
|
|||||||
creator_id INTEGER NOT NULL,
|
creator_id INTEGER NOT NULL,
|
||||||
UNIQUE(name, creator_id)
|
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 '{}'
|
||||||
|
);
|
||||||
|
Reference in New Issue
Block a user