From aaa8cbcecc63f45ac8b4e836c3af4c764df74b3d Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Mon, 15 Jun 2026 16:16:28 -0700 Subject: [PATCH 1/2] feat: add last_active_at column and POST /v1/users/me/ping endpoint Adds a `last_active_at` timestamptz column to the users table and a new authenticated endpoint that upserts it on each app-open. This replaces plays-based inactivity detection with a direct activity signal. Co-Authored-By: Claude Opus 4.6 --- api/server.go | 1 + api/v1_users_ping.go | 27 +++++++++++++++++++ .../0221_add_users_last_active_at.sql | 6 +++++ 3 files changed, 34 insertions(+) create mode 100644 api/v1_users_ping.go create mode 100644 ddl/migrations/0221_add_users_last_active_at.sql diff --git a/api/server.go b/api/server.go index 1c1197de..8674f83d 100644 --- a/api/server.go +++ b/api/server.go @@ -406,6 +406,7 @@ func NewApiServer(config config.Config) *ApiServer { g.Get("/users/genre/top", app.v1UsersGenreTop) g.Get("/users/account/:wallet", app.requireAuthMiddleware, app.v1UsersAccount) g.Get("/users/verify_token", app.v1UsersVerifyToken) + g.Post("/users/me/ping", app.requireAuthMiddleware, app.postV1UsersPing) g.Use("/users/handle/:handle", app.requireHandleMiddleware) g.Get("/users/handle/:handle", app.v1User) diff --git a/api/v1_users_ping.go b/api/v1_users_ping.go new file mode 100644 index 00000000..c222b726 --- /dev/null +++ b/api/v1_users_ping.go @@ -0,0 +1,27 @@ +package api + +import ( + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +func (app *ApiServer) postV1UsersPing(c *fiber.Ctx) error { + if app.writePool == nil { + return fiber.NewError(fiber.StatusServiceUnavailable, "writes not available") + } + + wallet := app.getAuthedWallet(c) + + _, err := app.writePool.Exec(c.Context(), ` + UPDATE users + SET last_active_at = now() + WHERE wallet = $1 + AND is_current = true + `, wallet) + if err != nil { + app.logger.Error("postV1UsersPing: failed to update last_active_at", zap.Error(err)) + return fiber.NewError(fiber.StatusInternalServerError, "failed to record activity") + } + + return c.JSON(fiber.Map{"status": "ok"}) +} diff --git a/ddl/migrations/0221_add_users_last_active_at.sql b/ddl/migrations/0221_add_users_last_active_at.sql new file mode 100644 index 00000000..93ec9cdd --- /dev/null +++ b/ddl/migrations/0221_add_users_last_active_at.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE users ADD COLUMN IF NOT EXISTS last_active_at TIMESTAMPTZ DEFAULT NULL; +COMMENT ON COLUMN users.last_active_at IS 'Timestamp of the user''s most recent app-open event, updated by POST /v1/users/me/ping.'; + +COMMIT; From c8da9b0ed0c19a613fa0b1337e4cbc5457549f62 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Mon, 15 Jun 2026 17:44:56 -0700 Subject: [PATCH 2/2] fix: allow /users/me/* routes to bypass userId middleware The g.Use("/users/:userId") prefix middleware was matching POST /users/me/ping (with :userId="me"), causing a 400 before the ping handler could run. Co-Authored-By: Claude Opus 4.6 --- api/resolve_middleware.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/resolve_middleware.go b/api/resolve_middleware.go index a2f478f5..a19809f0 100644 --- a/api/resolve_middleware.go +++ b/api/resolve_middleware.go @@ -45,6 +45,10 @@ func (app *ApiServer) getUserId(c *fiber.Ctx) int32 { } func (app *ApiServer) requireUserIdMiddleware(c *fiber.Ctx) error { + // Allow /users/me/* routes to pass through without userId resolution + if c.Params("userId") == "me" { + return c.Next() + } userId, err := trashid.DecodeHashId(c.Params("userId")) if err != nil || userId == 0 { return fiber.NewError(fiber.StatusBadRequest, "invalid userId")