package middleware import ( "context" "crypto/rand" "encoding/hex" "errors" "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/yuxingu/digital-psychology/apps/api/pkg/response" ) type ctxKey string const UserIDKey ctxKey = "user_id" const DeviceKeyHeader = "X-Device-Key" // DeviceAuth resolves or creates a Visitor→User via device key. func DeviceAuth(pool *pgxpool.Pool) gin.HandlerFunc { return func(c *gin.Context) { key := c.GetHeader(DeviceKeyHeader) if key == "" { key = newDeviceKey() c.Header(DeviceKeyHeader, key) } userID, err := ensureUser(c.Request.Context(), pool, key) if err != nil { response.Fail(c, http.StatusInternalServerError, 50001, "identity unavailable") c.Abort() return } c.Set(string(UserIDKey), userID.String()) c.Header(DeviceKeyHeader, key) c.Next() } } // UserIDFromContext returns the authenticated user id. func UserIDFromContext(c *gin.Context) (uuid.UUID, bool) { v, ok := c.Get(string(UserIDKey)) if !ok { return uuid.Nil, false } id, err := uuid.Parse(v.(string)) return id, err == nil } func ensureUser(ctx context.Context, pool *pgxpool.Pool, deviceKey string) (uuid.UUID, error) { var userID *uuid.UUID err := pool.QueryRow(ctx, ` SELECT user_id FROM device_identities WHERE device_key=$1 AND deleted_at IS NULL`, deviceKey, ).Scan(&userID) if err == nil && userID != nil { return *userID, nil } if err != nil && !errors.Is(err, pgx.ErrNoRows) { return uuid.Nil, err } tx, err := pool.Begin(ctx) if err != nil { return uuid.Nil, err } defer tx.Rollback(ctx) var uid uuid.UUID if err := tx.QueryRow(ctx, `INSERT INTO users DEFAULT VALUES RETURNING id`, ).Scan(&uid); err != nil { return uuid.Nil, err } if _, err := tx.Exec(ctx, ` INSERT INTO device_identities(device_key, user_id) VALUES ($1,$2) ON CONFLICT (device_key) DO UPDATE SET user_id=EXCLUDED.user_id, updated_at=now()`, deviceKey, uid, ); err != nil { return uuid.Nil, err } if err := tx.Commit(ctx); err != nil { return uuid.Nil, err } return uid, nil } func newDeviceKey() string { b := make([]byte, 16) _, _ = rand.Read(b) return "dev_" + hex.EncodeToString(b) }