Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/platformsettings"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
var errStoreReconnectPoolUnavailable = errors.New("database unavailable")
|
||||
|
||||
// StoreReconnectGap is one connected-but-invalid store connector for a tenant.
|
||||
// "Invalid" matches merchant needsStoreReconnect: store identity exists but secrets are missing.
|
||||
type StoreReconnectGap struct {
|
||||
CompanyID uuid.UUID `json:"company_id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
Channel string `json:"channel"`
|
||||
Identity string `json:"identity"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
Reason string `json:"reason"`
|
||||
LastTestStatus string `json:"last_test_status,omitempty"`
|
||||
}
|
||||
|
||||
// StoreReconnectInventory is the admin list payload for credential-gap stores.
|
||||
type StoreReconnectInventory struct {
|
||||
Stores []StoreReconnectGap `json:"stores"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
const storeReconnectReasonMissingCredentials = "missing_credentials"
|
||||
|
||||
// ListStoreReconnectGaps returns companies with Woo/Shopify identity but no usable credential blobs.
|
||||
// Presence-only (no decrypt) — same honesty bar as has_credentials=false in GetConfig for empty secrets.
|
||||
func ListStoreReconnectGaps(ctx context.Context, pool *pgxpool.Pool, limit, offset int) (StoreReconnectInventory, error) {
|
||||
out := StoreReconnectInventory{
|
||||
Stores: []StoreReconnectGap{},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
if pool == nil {
|
||||
return out, errStoreReconnectPoolUnavailable
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
out.Limit = limit
|
||||
out.Offset = offset
|
||||
|
||||
const q = `
|
||||
WITH gaps AS (
|
||||
SELECT c.id AS company_id, c.name AS company_name,
|
||||
'shopify'::text AS channel,
|
||||
NULLIF(BTRIM(sc.shop_domain), '') AS identity,
|
||||
sc.is_enabled,
|
||||
COALESCE(sc.last_test_status, '') AS last_test_status
|
||||
FROM companies c
|
||||
JOIN shopify_configs sc ON sc.company_id = c.id
|
||||
WHERE c.id <> $1
|
||||
AND NULLIF(BTRIM(sc.shop_domain), '') IS NOT NULL
|
||||
AND COALESCE(LENGTH(sc.access_token), 0) = 0
|
||||
AND COALESCE(NULLIF(BTRIM(sc.sync_options->>'client_id'), ''), '') = ''
|
||||
AND COALESCE(NULLIF(BTRIM(sc.sync_options->>'client_secret_enc'), ''), '') = ''
|
||||
UNION ALL
|
||||
SELECT c.id, c.name, 'woocommerce',
|
||||
NULLIF(BTRIM(wc.store_url), ''),
|
||||
wc.is_enabled,
|
||||
COALESCE(wc.last_test_status, '')
|
||||
FROM companies c
|
||||
JOIN woocommerce_configs wc ON wc.company_id = c.id
|
||||
WHERE c.id <> $1
|
||||
AND NULLIF(BTRIM(wc.store_url), '') IS NOT NULL
|
||||
AND (
|
||||
COALESCE(LENGTH(wc.consumer_key), 0) = 0
|
||||
OR COALESCE(LENGTH(wc.consumer_secret), 0) = 0
|
||||
)
|
||||
)
|
||||
SELECT COUNT(*) OVER() AS total,
|
||||
company_id, company_name, channel, identity, is_enabled, last_test_status
|
||||
FROM gaps
|
||||
ORDER BY company_name ASC, channel ASC
|
||||
LIMIT $2 OFFSET $3`
|
||||
|
||||
rows, err := pool.Query(ctx, q, platformsettings.SystemCompanyID, limit, offset)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var row StoreReconnectGap
|
||||
var total int
|
||||
if err := rows.Scan(
|
||||
&total,
|
||||
&row.CompanyID,
|
||||
&row.CompanyName,
|
||||
&row.Channel,
|
||||
&row.Identity,
|
||||
&row.IsEnabled,
|
||||
&row.LastTestStatus,
|
||||
); err != nil {
|
||||
return out, err
|
||||
}
|
||||
row.Reason = storeReconnectReasonMissingCredentials
|
||||
row.Identity = strings.TrimSpace(row.Identity)
|
||||
row.LastTestStatus = strings.TrimSpace(row.LastTestStatus)
|
||||
out.Total = total
|
||||
out.Stores = append(out.Stores, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminListStoreReconnectGaps(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Pool == nil {
|
||||
Error(w, http.StatusServiceUnavailable, "database unavailable")
|
||||
return
|
||||
}
|
||||
limit, offset := ParseLimitOffset(r)
|
||||
inv, err := ListStoreReconnectGaps(r.Context(), s.Pool, limit, offset)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "store reconnect inventory failed")
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, inv)
|
||||
}
|
||||
Reference in New Issue
Block a user