Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions supabase/code/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,14 @@ DOCKER_SOCKET_LOCATION=/var/run/docker.sock
# For Podman use the following:
# DOCKER_SOCKET_LOCATION=/run/podman/podman.sock

# Nombre del proyecto/stack tal como aparece en Docker. Vector solo recoge los
# logs de contenedores etiquetados con este valor, por lo que puedes desplegar
# varios stacks de Supabase en el mismo host sin que se mezclen los logs.
# Con `docker compose` es el valor de `name:` del docker-compose.yml (supabase).
# En Easypanel es el nombre del proyecto; verifícalo con:
# docker ps --format '{{.Label "com.docker.stack.namespace"}} {{.Names}}'
LOGS_STACK_NAME=supabase

# Google Cloud Project details
GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID
GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER
Expand Down
40 changes: 32 additions & 8 deletions supabase/code/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,9 @@ services:
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}

# Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile.
# GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: "true"
# Necesario para Google Sign In nativo en movil (flujo signInWithIdToken).
# Ponlo en "true" en el .env solo si usas login nativo, no con redirect.
GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: ${SKIP_NONCE_CHECK:-false}

# GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: "true"
# GOTRUE_SMTP_MAX_FREQUENCY: 1s
Expand All @@ -183,11 +184,23 @@ services:

GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
# Uncomment to enable OAuth / social login providers.
# GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
# GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
# GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
# GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
# --- OAuth / social login ---
# Google. CLIENT_ID admite varios IDs separados por coma: el de Web
# (necesario para el flujo con redirect) y los nativos de iOS/Android
# (necesarios para signInWithIdToken).
GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED:-false}
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET:-}
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback

# Apple. En APPLE_CLIENT_ID va el Services ID (flujo web) y, si usas
# Sign in with Apple nativo, tambien el bundle ID separado por coma.
# APPLE_SECRET NO es una contrasena: es un JWT que se genera desde la
# clave .p8 y caduca a los 6 meses como maximo (hay que regenerarlo).
GOTRUE_EXTERNAL_APPLE_ENABLED: ${APPLE_ENABLED:-false}
GOTRUE_EXTERNAL_APPLE_CLIENT_ID: ${APPLE_CLIENT_ID:-}
GOTRUE_EXTERNAL_APPLE_SECRET: ${APPLE_SECRET:-}
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback

# GOTRUE_EXTERNAL_GITHUB_ENABLED: ${GITHUB_ENABLED}
# GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
Expand Down Expand Up @@ -276,9 +289,16 @@ services:
command: [ "postgrest" ]

realtime:
# This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
# El hostname parece inconsistente pero es correcto: Realtime deriva el
# tenant id parseando el subdominio del host, y Kong lo llama por este
# nombre (ver volumes/api/kong.yml). Usamos un alias de red en vez de
# container_name porque el alias esta aislado por proyecto y no colisiona.
image: supabase/realtime:v2.76.5
restart: unless-stopped
networks:
default:
aliases:
- realtime-dev.supabase-realtime
depends_on:
db:
# Disable this if you are using an external Postgres database
Expand Down Expand Up @@ -550,6 +570,10 @@ services:
retries: 3
environment:
LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
# Nombre del proyecto/stack en Docker. Vector solo recoge logs de los
# contenedores etiquetados con este valor, para no mezclar los logs de
# otros stacks desplegados en el mismo host.
LOGS_STACK_NAME: ${LOGS_STACK_NAME:-supabase}
command: [ "--config", "/etc/vector/vector.yml" ]
security_opt:
- "label=disable"
Expand Down
59 changes: 59 additions & 0 deletions supabase/code/volumes/functions/clip-all-coupons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

const API_BASE_URL = 'https://food-universe-api-scrap.onrender.com';

serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}

try {
const { store_id, loyalty_card } = await req.json();

if (!store_id || !loyalty_card) {
return new Response(
JSON.stringify({ error: 'Missing required parameters: store_id, loyalty_card' }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}

console.log(`Clipping all coupons for loyalty card ${loyalty_card}`);

const response = await fetch(
`${API_BASE_URL}/api/clipOffers?store_id=${store_id}&loyalty_card=${loyalty_card}`,
{ method: "GET" }
);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const result = await response.json();
console.log('Clip all coupons result:', result);

return new Response(JSON.stringify(result), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Error clipping all coupons:', error);
return new Response(
JSON.stringify({
error: error.message || 'Failed to clip all coupons',
success: false
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
}
});
69 changes: 69 additions & 0 deletions supabase/code/volumes/functions/clip-all/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

const API_BASE_URL = 'https://xcirculars-coupons-api.7panny.easypanel.host';

serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}

try {
const { store_id, phone_number } = await req.json();

if (!store_id || !phone_number) {
return new Response(
JSON.stringify({ error: 'Missing required parameters: store_id, phone_number' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}

console.log(`Clipping all coupons: store_id=${store_id}`);

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 110000);

let response;
try {
response = await fetch(`${API_BASE_URL}/clip`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ store_id, phone_number }),
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}

const text = await response.text();
let result;
try {
result = JSON.parse(text);
} catch {
result = { error: text, success: false };
}

console.log('clip-all response:', JSON.stringify(result));

return new Response(JSON.stringify(result), {
status: response.ok ? 200 : response.status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
const isTimeout = error.name === 'AbortError';
console.error('Error clipping all coupons:', error);
return new Response(
JSON.stringify({
error: isTimeout
? 'External API timeout — service may be starting up, try again'
: (error.message || 'Failed to clip all coupons'),
success: false,
}),
{ status: isTimeout ? 504 : 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});
86 changes: 86 additions & 0 deletions supabase/code/volumes/functions/clip-coupon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

const API_BASE_URL = 'https://xcirculars-coupons-api.7panny.easypanel.host';

serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}

try {
const { store_id, offer_id, phone_number, job_id, otp_code, first_name, last_name, email } = await req.json();

if (!store_id || !offer_id || !job_id) {
return new Response(
JSON.stringify({ error: 'Missing required parameters: store_id, offer_id, job_id' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}

if (!phone_number) {
return new Response(
JSON.stringify({ error: 'Missing required parameter: phone_number' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}

console.log(`Clipping coupon: offer_id=${offer_id}, store_id=${store_id}, job_id=${job_id}, has_otp=${!!otp_code}`);

const payload: Record<string, string> = { store_id, offer_id, phone_number, job_id };
if (otp_code) {
payload.otp_code = otp_code;
if (first_name) payload.first_name = first_name;
if (last_name) payload.last_name = last_name;
if (email) payload.email = email;
}

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 110000);

let response;
try {
response = await fetch(`${API_BASE_URL}/clip-single`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}

const text = await response.text();
let result;
try {
result = JSON.parse(text);
} catch {
result = { error: text, success: false };
}

console.log('clip-single response:', JSON.stringify(result));

const status = result.auth_required ? 200 : (response.ok ? 200 : response.status);

return new Response(JSON.stringify({ job_id, ...result }), {
status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
const isTimeout = error.name === 'AbortError';
console.error('Error clipping coupon:', error);
return new Response(
JSON.stringify({
error: isTimeout
? 'External API timeout — service may be starting up, try again'
: (error.message || 'Failed to clip coupon'),
success: false,
}),
{ status: isTimeout ? 504 : 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});
Loading