feat: requires_address status + address input for pickup-to-delivery switch
- New status: requires_address (Требуется адрес) in deliveryWorkflow - ClientDeliveryPage: address input shown when delivery chosen without address on file - OrderDetailPanel: requires_address status badge + address input for manager - confirm_delivery_choice_by_token RPC: p_delivery_address param, sets requires_address status - Edge function: delivery_address passed to RPC - deliveryInvitationApi: deliveryAddress param - CHECK constraint: requires_address, address_required added - PickupSlotsPicker: styled storage conditions info block
This commit is contained in:
parent
c774c6a362
commit
7e43f9e990
|
|
@ -7,3 +7,4 @@ dist
|
|||
.worktrees
|
||||
.superpowers
|
||||
.ruff_cache
|
||||
volumes/db/data/
|
||||
|
|
|
|||
|
|
@ -105,14 +105,37 @@ const getPickupSlots = (referenceDate = new Date()) => {
|
|||
};
|
||||
|
||||
const FREE_STORAGE_NOTICE = (
|
||||
<div className="mt-3 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-4 text-sm leading-6 text-[var(--color-text-muted)]">
|
||||
<p className="font-semibold text-[var(--color-text)]">ℹ️ Условия хранения</p>
|
||||
<p className="mt-1">
|
||||
Бесплатное хранение — <strong>2 рабочих дня</strong> с даты готовности.
|
||||
</p>
|
||||
<p>
|
||||
Начиная с 3-го рабочего дня — <strong>300 ₽/день</strong> платного хранения.
|
||||
<div
|
||||
className="mt-4 overflow-hidden rounded-[28px] border border-[var(--color-border)]"
|
||||
style={{ background: "linear-gradient(135deg, var(--color-accent-soft) 0%, var(--color-surface) 60%)" }}
|
||||
>
|
||||
<div className="flex items-start gap-3 p-5 sm:p-6">
|
||||
<span
|
||||
className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-2xl text-lg"
|
||||
style={{ background: "var(--color-accent)", color: "var(--color-accent-contrast)" }}
|
||||
>
|
||||
📦
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-bold tracking-wide uppercase text-[var(--color-accent)]">
|
||||
Условия хранения
|
||||
</p>
|
||||
<ul className="mt-2 space-y-1.5 text-sm leading-6 text-[var(--color-text)]">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-1.5 block h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-accent)]" />
|
||||
<span>
|
||||
Бесплатное хранение — <strong>2 рабочих дня</strong> с даты готовности
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-1.5 block h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-warning)]" />
|
||||
<span>
|
||||
С 3-го рабочего дня — <strong>300 ₽/день</strong> платного хранения
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -557,6 +557,7 @@ export const OrderDetailPanel = ({
|
|||
const [deliveryType, setDeliveryType] = React.useState(order?.deliveryType || "delivery");
|
||||
const [pickupDate, setPickupDate] = React.useState(order?.pickupDate || "");
|
||||
const [pickupTimeSlot, setPickupTimeSlot] = React.useState(DELIVERY_TIME_OPTIONS[0]);
|
||||
const [deliveryAddress, setDeliveryAddress] = React.useState(order?.deliveryAddress || order?.customerAddress || "");
|
||||
const minSelectableDateKey = React.useMemo(() => getNextSelectableDateKey(), []);
|
||||
const [currentMonth, setCurrentMonth] = React.useState(() => {
|
||||
const existingDeliveryDate = fromDateKey(order?.deliveryDate);
|
||||
|
|
@ -637,6 +638,7 @@ export const OrderDetailPanel = ({
|
|||
deliveryTime: deliveryType === "pickup" ? pickupTimeSlot : deliveryTime,
|
||||
deliveryType,
|
||||
...(deliveryType === "pickup" ? { pickupDate, pickupTimeSlot } : {}),
|
||||
...(deliveryType === "delivery" && deliveryAddress.trim() ? { deliveryAddress: deliveryAddress.trim() } : {}),
|
||||
});
|
||||
|
||||
if (result?.success) {
|
||||
|
|
@ -709,7 +711,16 @@ export const OrderDetailPanel = ({
|
|||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||||
Тип доставки
|
||||
</p>
|
||||
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{order.deliveryType === "pickup" ? "Самовывоз" : "Доставка"}</p>
|
||||
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{order.deliveryType === "pickup" ? "Самовывоз" : order.deliveryStatus === "requires_address" || order.delivery_status === "requires_address" ? "Доставка (требуется адрес)" : "Доставка"}</p>
|
||||
{(order.deliveryStatus === "requires_address" || order.delivery_status === "requires_address") && (
|
||||
<div className="mt-2 flex items-start gap-2 rounded-xl border border-[rgba(239,68,68,0.3)] bg-[rgba(239,68,68,0.08)] p-3">
|
||||
<span className="text-lg">📍</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--color-text)]">Адрес доставки не указан</p>
|
||||
<p className="text-xs text-[var(--color-text-muted)]">Клиент выбрал доставку, но адрес отсутствует. Уточните адрес у клиента и заполните поле ниже.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||||
|
|
@ -1125,6 +1136,7 @@ export const OrderDetailPanel = ({
|
|||
{ value: "on_route", label: "В пути", manual: true },
|
||||
{ value: "delivered", label: "Доставлено", manual: true },
|
||||
{ value: "pickup", label: "Самовывоз", manual: true },
|
||||
{ value: "requires_address", label: "Требуется адрес", manual: true },
|
||||
{ value: "problem", label: "Проблема", manual: true },
|
||||
{ value: "cancelled", label: "Отменено", manual: true },
|
||||
].map((statusOption) => {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,15 @@ export const ORDER_STATUS_META = {
|
|||
criticalAfterHours: 48,
|
||||
tone: "accent",
|
||||
},
|
||||
"Требуется адрес": {
|
||||
comment: "Клиент выбрал доставку, но адрес доставки отсутствует. Менеджеру нужно уточнить адрес.",
|
||||
ownerRole: "logistician",
|
||||
stageKey: "logistics",
|
||||
stageLabel: getStageLabel("logistics"),
|
||||
warningAfterHours: 4,
|
||||
criticalAfterHours: 12,
|
||||
tone: "warning",
|
||||
},
|
||||
"Передан логисту": {
|
||||
comment: "Автоматическое согласование не завершилось, заказ передан логисту на ручную обработку.",
|
||||
ownerRole: "logistician",
|
||||
|
|
@ -228,8 +237,8 @@ export const ORDER_STATUS_TRANSITIONS = {
|
|||
"В производстве": ["Готов к отгрузке", "Требует уточнения", "Отменён"],
|
||||
"Готов к отгрузке": ["Ожидает согласования доставки", "Ожидает ответа клиента", "Проблема доставки", "Отменён"],
|
||||
"Ожидает ответа клиента": ["Доставка согласована", "Передан логисту", "Платное хранение", "Проблема доставки", "Отменён"],
|
||||
"Ожидает согласования доставки": ["Доставка согласована", "Самовывоз", "Проблема доставки", "Отменён"],
|
||||
"Доставка согласована": ["Назначен водитель", "Ожидает согласования доставки", "Проблема доставки", "Самовывоз"],
|
||||
"Ожидает согласования доставки": ["Доставка согласована", "Самовывоз", "Требуется адрес", "Проблема доставки", "Отменён"],
|
||||
"Доставка согласована": ["Назначен водитель", "Ожидает согласования доставки", "Проблема доставки", "Самовывоз", "Требуется адрес"],
|
||||
"Передан логисту": ["Доставка согласована", "Платное хранение", "Проблема доставки", "Отменён"],
|
||||
"Назначен водитель": ["Загружен", "Проблема доставки"],
|
||||
Загружен: ["Доставлен", "Проблема доставки"],
|
||||
|
|
@ -238,6 +247,7 @@ export const ORDER_STATUS_TRANSITIONS = {
|
|||
"Проблема доставки": ["Ожидает согласования доставки", "Назначен водитель", "Отменён", "Закрыт"],
|
||||
"Платное хранение": ["Доставка согласована", "Отменён", "Закрыт"],
|
||||
"Самовывоз": ["Доставка согласована", "Закрыт", "Отменён", "Платное хранение"],
|
||||
"Требуется адрес": ["Доставка согласована", "Самовывоз", "Отменён", "Проблема доставки"],
|
||||
Закрыт: [],
|
||||
Отменён: [],
|
||||
};
|
||||
|
|
@ -254,6 +264,7 @@ export const ROLE_TRANSITION_TARGETS = {
|
|||
"Передан логисту",
|
||||
"Назначен водитель",
|
||||
"Самовывоз",
|
||||
"Требуется адрес",
|
||||
"Проблема доставки",
|
||||
"Платное хранение",
|
||||
"Закрыт",
|
||||
|
|
@ -276,6 +287,7 @@ export const LOGISTICS_STATUSES = [
|
|||
"Доставка согласована",
|
||||
"Назначен водитель",
|
||||
"Самовывоз",
|
||||
"Требуется адрес",
|
||||
"Проблема доставки",
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ export const ClientDeliveryPage = () => {
|
|||
const [selectedSlot, setSelectedSlot] = React.useState(null);
|
||||
const [choiceSaved, setChoiceSaved] = React.useState(false);
|
||||
const [activeTab, setActiveTab] = React.useState(TAB_DELIVERY);
|
||||
const [deliveryAddress, setDeliveryAddress] = React.useState("");
|
||||
const referenceDate = React.useMemo(() => new Date(), [token]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -278,6 +279,9 @@ export const ClientDeliveryPage = () => {
|
|||
pickupDate: effectiveSelectedSlot.date,
|
||||
pickupTimeSlot: effectiveSelectedSlot.time,
|
||||
} : {}),
|
||||
...(activeTab === TAB_DELIVERY && deliveryAddress.trim() ? {
|
||||
deliveryAddress: deliveryAddress.trim(),
|
||||
} : {}),
|
||||
});
|
||||
const loadedInvitation = await fetchDeliveryInvitation(token);
|
||||
setInvitation(loadedInvitation);
|
||||
|
|
@ -403,6 +407,27 @@ export const ClientDeliveryPage = () => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === TAB_DELIVERY && !invitation?.deliveryAddress && !invitation?.customerAddress && (
|
||||
<Panel className="space-y-3 border-[rgba(239,68,68,0.3)] bg-[var(--color-surface)] p-5 sm:p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-xl">📍</span>
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-text)]">Укажите адрес доставки</p>
|
||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||
Адрес доставки отсутствует в заказе. Пожалуйста, введите полный адрес, куда нужно привезти заказ.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={deliveryAddress}
|
||||
onChange={(e) => setDeliveryAddress(e.target.value)}
|
||||
placeholder="Город, улица, дом, квартира"
|
||||
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-3 text-sm text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none"
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{activeTab === TAB_DELIVERY && slots.length ? (
|
||||
<DeliverySlotsPicker
|
||||
slots={slots}
|
||||
|
|
|
|||
|
|
@ -223,13 +223,14 @@ export const fetchDeliveryInvitation = async (token) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const confirmDeliveryChoice = async ({ token, deliveryDate, deliveryTime, deliveryType, pickupDate, pickupTimeSlot }) => {
|
||||
export const confirmDeliveryChoice = async ({ token, deliveryDate, deliveryTime, deliveryType, pickupDate, pickupTimeSlot, deliveryAddress }) => {
|
||||
if (isLocalClientInvitationToken(token)) {
|
||||
const baseInvitation = getCachedInvitation(token) ?? buildFallbackInvitation(token);
|
||||
const invitation = cacheInvitation({
|
||||
...baseInvitation,
|
||||
deliveryType: deliveryType || "delivery",
|
||||
...(deliveryType === "pickup" ? { pickupDate, pickupTimeSlot } : {}),
|
||||
...(deliveryType === "delivery" && deliveryAddress ? { deliveryAddress, customerAddress: deliveryAddress } : {}),
|
||||
deliveryDate,
|
||||
deliveryTime,
|
||||
state: "confirmed",
|
||||
|
|
@ -247,6 +248,7 @@ export const confirmDeliveryChoice = async ({ token, deliveryDate, deliveryTime,
|
|||
p_delivery_type: deliveryType || "delivery",
|
||||
p_pickup_date: pickupDate || null,
|
||||
p_pickup_time_slot: pickupTimeSlot || null,
|
||||
p_delivery_address: deliveryAddress || null,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,14 @@ Deno.serve(async (request) => {
|
|||
}
|
||||
|
||||
const deliveryType = body.deliveryType || "delivery";
|
||||
const effectiveDeliveryStatus = deliveryType === "pickup" ? "pickup" : "agreed";
|
||||
|
||||
// When user switches from pickup to delivery but has no address → requires_address
|
||||
const hasAddress = invitation.delivery_address?.trim() || currentGroup?.delivery_address?.trim() || currentGroup?.customer_address?.trim();
|
||||
const effectiveDeliveryStatus = deliveryType === "pickup"
|
||||
? "pickup"
|
||||
: hasAddress
|
||||
? "agreed"
|
||||
: "requires_address";
|
||||
|
||||
if (invitation.order_group_id) {
|
||||
const { data: currentGroup, error: groupError } = await supabase
|
||||
|
|
@ -194,7 +201,7 @@ Deno.serve(async (request) => {
|
|||
delivery_date: requestedSlot.deliveryDate,
|
||||
delivery_time: requestedSlot.deliveryTime,
|
||||
delivery_type: deliveryType,
|
||||
notification_status: "confirmed",
|
||||
notification_status: effectiveDeliveryStatus === "requires_address" ? "address_required" : "confirmed",
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/bash
|
||||
# Custom entrypoint for Kong that builds Lua expressions for request-transformer
|
||||
# and performs environment variable substitution in the declarative config.
|
||||
|
||||
# Build Lua expressions for translating opaque API keys to asymmetric JWTs.
|
||||
# When opaque keys are not configured (empty env vars), expressions fall through
|
||||
# to legacy-only behavior - just passing apikey as-is.
|
||||
#
|
||||
# Full expression logic (when opaque keys are configured):
|
||||
# 1. If Authorization header exists and is NOT an sb_ key -> pass through (user session JWT)
|
||||
# 2. If apikey matches secret key -> set service_role asymmetric JWT internal "API key"
|
||||
# 3. If apikey matches publishable key -> set anon asymmetric JWT internal "API key"
|
||||
# 4. Fallback: pass apikey as-is (legacy HS256 JWT)
|
||||
|
||||
if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
# Opaque keys configured -> full translation expressions
|
||||
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '$SUPABASE_SECRET_KEY' and 'Bearer $SERVICE_ROLE_KEY_ASYMMETRIC') or (headers.apikey == '$SUPABASE_PUBLISHABLE_KEY' and 'Bearer $ANON_KEY_ASYMMETRIC') or headers.apikey)"
|
||||
|
||||
# Realtime WebSocket: reads from query_params.apikey (supabase-js sends apikey
|
||||
# via query string), outputs to x-api-key header which Realtime checks first.
|
||||
export LUA_RT_WS_EXPR="\$((query_params.apikey == '$SUPABASE_SECRET_KEY' and '$SERVICE_ROLE_KEY_ASYMMETRIC') or (query_params.apikey == '$SUPABASE_PUBLISHABLE_KEY' and '$ANON_KEY_ASYMMETRIC') or query_params.apikey)"
|
||||
else
|
||||
# Legacy API keys, not sb_ API keys -> pass apikey through unchanged
|
||||
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)"
|
||||
export LUA_RT_WS_EXPR="\$(query_params.apikey)"
|
||||
fi
|
||||
|
||||
# Substitute environment variables in the Kong declarative config.
|
||||
# Uses awk instead of eval/echo to preserve YAML quoting (eval strips double
|
||||
# quotes, breaking "Header: value" patterns that YAML parses as mappings).
|
||||
awk '{
|
||||
result = ""
|
||||
rest = $0
|
||||
while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) {
|
||||
varname = substr(rest, RSTART + 1, RLENGTH - 1)
|
||||
if (varname in ENVIRON) {
|
||||
result = result substr(rest, 1, RSTART - 1) ENVIRON[varname]
|
||||
} else {
|
||||
result = result substr(rest, 1, RSTART + RLENGTH - 1)
|
||||
}
|
||||
rest = substr(rest, RSTART + RLENGTH)
|
||||
}
|
||||
print result rest
|
||||
}' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
# Remove empty key-auth credentials (unconfigured opaque keys)
|
||||
sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
exec /entrypoint.sh kong docker-start
|
||||
|
|
@ -0,0 +1,411 @@
|
|||
_format_version: '2.1'
|
||||
_transform: true
|
||||
|
||||
###
|
||||
### Consumers / Users
|
||||
###
|
||||
consumers:
|
||||
- username: DASHBOARD
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_ANON_KEY
|
||||
- key: $SUPABASE_PUBLISHABLE_KEY
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_SERVICE_KEY
|
||||
- key: $SUPABASE_SECRET_KEY
|
||||
|
||||
###
|
||||
### Access Control List
|
||||
###
|
||||
acls:
|
||||
- consumer: anon
|
||||
group: anon
|
||||
- consumer: service_role
|
||||
group: admin
|
||||
|
||||
###
|
||||
### Dashboard credentials
|
||||
###
|
||||
basicauth_credentials:
|
||||
- consumer: DASHBOARD
|
||||
username: '$DASHBOARD_USERNAME'
|
||||
password: '$DASHBOARD_PASSWORD'
|
||||
|
||||
###
|
||||
### API Routes
|
||||
###
|
||||
services:
|
||||
## Open Auth routes
|
||||
- name: auth-v1-open
|
||||
_comment: 'Auth: /auth/v1/verify* -> http://auth:9999/verify*'
|
||||
url: http://auth:9999/verify
|
||||
routes:
|
||||
- name: auth-v1-open
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/verify
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-callback
|
||||
_comment: 'Auth: /auth/v1/callback* -> http://auth:9999/callback*'
|
||||
url: http://auth:9999/callback
|
||||
routes:
|
||||
- name: auth-v1-open-callback
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/callback
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-authorize
|
||||
_comment: 'Auth: /auth/v1/authorize* -> http://auth:9999/authorize*'
|
||||
url: http://auth:9999/authorize
|
||||
routes:
|
||||
- name: auth-v1-open-authorize
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-jwks
|
||||
_comment: 'Auth: /auth/v1/.well-known/jwks.json -> http://auth:9999/.well-known/jwks.json'
|
||||
url: http://auth:9999/.well-known/jwks.json
|
||||
routes:
|
||||
- name: auth-v1-open-jwks
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/.well-known/jwks.json
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Secure Auth routes
|
||||
- name: auth-v1
|
||||
_comment: 'Auth: /auth/v1/* -> http://auth:9999/*'
|
||||
url: http://auth:9999/
|
||||
routes:
|
||||
- name: auth-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure PostgREST routes
|
||||
- name: rest-v1
|
||||
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
|
||||
url: http://rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure GraphQL routes
|
||||
- name: graphql-v1
|
||||
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
|
||||
url: http://rest:3000/rpc/graphql
|
||||
routes:
|
||||
- name: graphql-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /graphql/v1
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Content-Profile: graphql_public"
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure Realtime routes
|
||||
- name: realtime-v1-ws
|
||||
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/socket
|
||||
protocol: ws
|
||||
routes:
|
||||
- name: realtime-v1-ws
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "x-api-key:$LUA_RT_WS_EXPR"
|
||||
replace:
|
||||
querystring:
|
||||
- "apikey:$LUA_RT_WS_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
- name: realtime-v1-rest
|
||||
_comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Storage API endpoint (with Authorization header transformation).
|
||||
## No key-auth — S3 protocol requests don't carry an apikey header.
|
||||
##
|
||||
## The request-transformer translates opaque API keys to asymmetric JWTs
|
||||
## and passes through existing Authorization headers (user JWTs, AWS SigV4).
|
||||
## When no Authorization or apikey header is present (S3 presigned URLs),
|
||||
## the Lua expression evaluates to nil which Kong renders as empty string.
|
||||
## The post-function strips this empty header so Storage's S3 signature
|
||||
## verification falls through to query-parameter parsing.
|
||||
- name: storage-v1
|
||||
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
|
||||
url: http://storage:5000/
|
||||
routes:
|
||||
- name: storage-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /storage/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: post-function
|
||||
config:
|
||||
access:
|
||||
- |
|
||||
local auth = kong.request.get_header("authorization")
|
||||
if auth == nil or auth == "" or auth:find("^%s*$") then
|
||||
kong.service.request.clear_header("authorization")
|
||||
end
|
||||
|
||||
## Edge Functions routes
|
||||
- name: functions-v1
|
||||
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
|
||||
url: http://functions:9000/
|
||||
read_timeout: 150000
|
||||
routes:
|
||||
- name: functions-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /functions/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "https://dost.supersamsev.ru"
|
||||
- "https://supa.supersamsev.ru"
|
||||
- "https://supasevdev.mkn8n.ru"
|
||||
- "http://localhost:5173"
|
||||
- "http://localhost:3000"
|
||||
- "http://localhost:5174"
|
||||
methods:
|
||||
- GET
|
||||
- DELETE
|
||||
- POST
|
||||
- PATCH
|
||||
- OPTIONS
|
||||
- PUT
|
||||
headers:
|
||||
- Content-Type
|
||||
- Authorization
|
||||
- apikey
|
||||
- x-application-name
|
||||
- x-client-info
|
||||
credentials: true
|
||||
max_age: 86400
|
||||
|
||||
## OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
- name: well-known-oauth
|
||||
_comment: 'Auth: /.well-known/oauth-authorization-server -> http://auth:9999/.well-known/oauth-authorization-server'
|
||||
url: http://auth:9999/.well-known/oauth-authorization-server
|
||||
routes:
|
||||
- name: well-known-oauth
|
||||
strip_path: true
|
||||
paths:
|
||||
- /.well-known/oauth-authorization-server
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Analytics routes
|
||||
## Not used - Studio and Vector talk directly to analytics via Docker networking.
|
||||
## If external access is needed, add routes with key-auth matching Logflare's x-api-key auth.
|
||||
# - name: analytics-v1-api
|
||||
# _comment: 'Analytics: /analytics/v1/api/endpoints/* -> http://logflare:4000/api/endpoints/*'
|
||||
# url: http://analytics:4000/api/endpoints
|
||||
# routes:
|
||||
# - name: analytics-v1-api
|
||||
# strip_path: true
|
||||
# paths:
|
||||
# - /analytics/v1/api/endpoints/
|
||||
# - name: analytics-v1
|
||||
# _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
|
||||
# url: http://analytics:4000/
|
||||
# routes:
|
||||
# - name: dashboard-v1-all
|
||||
# strip_path: true
|
||||
# paths:
|
||||
# - /analytics/v1
|
||||
# plugins:
|
||||
# - name: cors
|
||||
# - name: basic-auth
|
||||
# config:
|
||||
# hide_credentials: true
|
||||
|
||||
## Secure Database routes
|
||||
- name: meta
|
||||
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
|
||||
url: http://meta:8080/
|
||||
routes:
|
||||
- name: meta-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /pg/
|
||||
plugins:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
|
||||
## Block access to /api/mcp
|
||||
- name: mcp-blocker
|
||||
_comment: 'Block direct access to /api/mcp'
|
||||
url: http://studio:3000/api/mcp
|
||||
routes:
|
||||
- name: mcp-blocker-route
|
||||
strip_path: true
|
||||
paths:
|
||||
- /api/mcp
|
||||
plugins:
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
|
||||
## MCP endpoint - local access
|
||||
- name: mcp
|
||||
_comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)'
|
||||
url: http://studio:3000/api/mcp
|
||||
routes:
|
||||
- name: mcp
|
||||
strip_path: true
|
||||
paths:
|
||||
- /mcp
|
||||
plugins:
|
||||
# Block access to /mcp by default
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
# Enable local access (danger zone!)
|
||||
# 1. Comment out the 'request-termination' section above
|
||||
# 2. Uncomment the entire section below, including 'deny'
|
||||
# 3. Add your local IPs to the 'allow' list
|
||||
#- name: cors
|
||||
#- name: ip-restriction
|
||||
# config:
|
||||
# allow:
|
||||
# - 127.0.0.1
|
||||
# - ::1
|
||||
# deny: []
|
||||
|
||||
## Protected Dashboard - catch all remaining routes
|
||||
- name: dashboard
|
||||
_comment: 'Studio: /* -> http://studio:3000/*'
|
||||
url: http://studio:3000/
|
||||
routes:
|
||||
- name: dashboard-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: basic-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
Loading…
Reference in New Issue