Fixes and tickets

This commit is contained in:
Thomas Demirdjian
2025-10-28 15:43:18 +01:00
parent 2599a25d77
commit d1be3c19a2
31 changed files with 6663 additions and 795 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
exports.GEMINI_API_KEY = "AIzaSyBoPQC5ZaMKP73TlKGpZQp1mAw8ArHiH9Y";
// exports.SUNO_API_KEY = "543b5650b30d93e98d8046a830244c80"; client api key
exports.SUNO_API_KEY = "138bace8405b869f917156440acb6fed"; // perso api key
exports.SUNO_API_KEY = "5a689606d3c59f58268b4963d5107361"; // api key
exports.RESEND_API_KEY = "re_";
+746
View File
@@ -0,0 +1,746 @@
#!/bin/bash
# Script interactif pour supprimer toutes les Cloud Functions d'un projet Firebase
# Utilise les flèches pour naviguer et Entrée pour sélectionner
# Couleurs pour l'affichage
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Fonction pour afficher le menu avec sélection
show_menu() {
local options=("$@")
local selected=0
local num_options=${#options[@]}
while true; do
clear
echo -e "${BLUE}${BOLD}=== Gestionnaire Cloud Functions Firebase ===${NC}\n"
if [ ! -z "$CURRENT_PROJECT" ]; then
echo -e "${GREEN}Projet actuel: ${BOLD}$CURRENT_PROJECT${NC}\n"
fi
echo "Utilisez les flèches ↑↓ pour naviguer et Entrée pour sélectionner:\n"
for i in "${!options[@]}"; do
if [ $i -eq $selected ]; then
echo -e "${GREEN}${options[$i]}${NC}"
else
echo -e " ${options[$i]}"
fi
done
# Lire une seule touche
read -rsn1 key
case $key in
$'\033') # Séquence d'échappement pour les flèches
read -rsn2 key
case $key in
'[A') # Flèche haut
((selected--))
if [ $selected -lt 0 ]; then
selected=$((num_options-1))
fi
;;
'[B') # Flèche bas
((selected++))
if [ $selected -ge $num_options ]; then
selected=0
fi
;;
esac
;;
'') # Entrée
return $selected
;;
'q'|'Q') # Quitter
echo -e "\n${YELLOW}Au revoir!${NC}"
exit 0
;;
esac
done
}
# Fonction pour vérifier si gcloud est installé
check_gcloud() {
if ! command -v gcloud &> /dev/null; then
echo -e "${RED}Erreur: gcloud CLI n'est pas installé.${NC}"
echo "Veuillez installer Google Cloud SDK: https://cloud.google.com/sdk/docs/install"
exit 1
fi
}
# Fonction pour obtenir le projet actuel
get_current_project() {
CURRENT_PROJECT=$(gcloud config get-value project 2>/dev/null)
if [ -z "$CURRENT_PROJECT" ]; then
echo -e "${YELLOW}Aucun projet configuré.${NC}"
CURRENT_PROJECT="Aucun"
fi
}
# Fonction pour afficher les informations de débogage
show_debug_info() {
clear
echo -e "${BLUE}${BOLD}=== Informations de débogage ===${NC}\n"
echo -e "${YELLOW}1. Version de gcloud:${NC}"
gcloud version 2>/dev/null || echo "Erreur lors de la récupération de la version"
echo ""
echo -e "${YELLOW}2. Configuration actuelle:${NC}"
gcloud config list 2>/dev/null || echo "Erreur lors de la récupération de la configuration"
echo ""
echo -e "${YELLOW}3. Comptes authentifiés:${NC}"
gcloud auth list 2>/dev/null || echo "Erreur lors de la récupération des comptes"
echo ""
echo -e "${YELLOW}4. Test de la commande projects list (brute):${NC}"
echo "Commande: gcloud projects list"
gcloud projects list 2>&1 || echo "Erreur lors de l'exécution"
echo ""
echo -e "${YELLOW}5. Test avec format JSON:${NC}"
echo "Commande: gcloud projects list --format=json"
gcloud projects list --format="json" 2>&1 | head -20 || echo "Erreur lors de l'exécution"
echo ""
echo -e "${YELLOW}6. Permissions et quotas:${NC}"
echo "Commande: gcloud projects list --limit=1"
gcloud projects list --limit=1 2>&1 || echo "Erreur lors de l'exécution"
echo ""
}
# Fonction pour gérer l'authentification
handle_authentication() {
clear
echo -e "${RED}${BOLD}=== Erreur d'authentification ===${NC}\n"
echo -e "${YELLOW}Aucun projet trouvé ou erreur d'authentification.${NC}\n"
local auth_options=("Se connecter maintenant (gcloud auth login)" "Réessayer" "← Retour au menu principal")
echo "Que souhaitez-vous faire ?\n"
show_menu "${auth_options[@]}"
local choice=$?
case $choice in
0) # Se connecter
echo -e "\n${BLUE}Lancement de l'authentification...${NC}"
echo -e "${YELLOW}Une page web va s'ouvrir pour vous connecter.${NC}\n"
if gcloud auth login; then
echo -e "\n${GREEN}Authentification réussie!${NC}"
sleep 2
return 0
else
echo -e "\n${RED}Échec de l'authentification.${NC}"
read -p "Appuyez sur Entrée pour continuer..."
return 1
fi
;;
1) # Réessayer
return 0
;;
2) # Retour
return 1
;;
esac
}
# Fonction pour récupérer la liste des projets
get_projects_list() {
local projects=()
# Vérifier si l'utilisateur est connecté
local auth_status=$(gcloud auth list --format="value(account)" 2>/dev/null | head -1)
if [ -z "$auth_status" ]; then
echo -e "${RED}Vous n'êtes pas connecté à GCP.${NC}"
return 1
fi
echo -e "${BLUE}Récupération des projets...${NC}"
# Méthode 1: Utiliser --format=json pour éviter les problèmes de parsing
local temp_file=$(mktemp)
gcloud projects list --format="json" > "$temp_file" 2>/dev/null
if [ -s "$temp_file" ]; then
# Extraire les IDs de projet du JSON
while read -r project_id; do
if [ ! -z "$project_id" ]; then
projects+=("$project_id")
fi
done < <(cat "$temp_file" | grep -o '"projectId": "[^"]*"' | cut -d '"' -f 4)
else
# Méthode 2: Essayer avec la méthode originale
gcloud projects list --format="table(projectId)" --sort-by=projectId > "$temp_file" 2>&1
while IFS= read -r line; do
if [[ $line =~ ^[a-zA-Z0-9-]+[[:space:]] ]]; then
project_id=$(echo "$line" | awk '{print $1}')
if [ ! -z "$project_id" ]; then
projects+=("$project_id")
fi
fi
done < <(cat "$temp_file" | tail -n +2)
fi
# Nettoyer le fichier temporaire
rm -f "$temp_file"
# Retourner les projets via une variable globale
PROJECTS_LIST=("${projects[@]}")
return 0
}
# Fonction pour sélectionner un projet de manière interactive avec filtrage
select_project_interactive() {
echo -e "\n${BLUE}Récupération de la liste des projets...${NC}"
# Récupérer tous les projets disponibles
local all_projects=()
if ! get_projects_list; then
if ! handle_authentication; then
return 1
fi
if ! get_projects_list; then
echo -e "${RED}Impossible de récupérer la liste des projets.${NC}"
read -p "Appuyez sur Entrée pour continuer..."
return 1
fi
fi
all_projects=("${PROJECTS_LIST[@]}")
if [ ${#all_projects[@]} -eq 0 ]; then
echo -e "${RED}Aucun projet accessible trouvé.${NC}"
echo -e "${YELLOW}Cela peut être dû à:${NC}"
echo -e " • Aucun projet associé à votre compte"
echo -e " • Permissions insuffisantes"
echo -e " • Problème de configuration gcloud"
echo ""
local retry_options=("Réessayer" "Se reconnecter" "Spécifier manuellement l'ID du projet" "Afficher les détails de débogage" "← Retour au menu principal")
echo "Que souhaitez-vous faire ?\n"
show_menu "${retry_options[@]}"
local retry_choice=$?
case $retry_choice in
0) # Réessayer
return select_project_interactive
;;
1) # Se reconnecter
if handle_authentication; then
return select_project_interactive
else
return 1
fi
;;
2) # Spécifier manuellement l'ID du projet
clear
echo -e "${BLUE}${BOLD}=== Spécifier l'ID du projet ===${NC}\n"
echo -e "${YELLOW}Entrez l'ID du projet Firebase/GCP:${NC}"
echo -e "(exemple: my-project-12345)\n"
read -p "> " manual_project_id
if [ -z "$manual_project_id" ]; then
echo -e "\n${RED}Aucun ID de projet spécifié.${NC}"
read -p "Appuyez sur Entrée pour continuer..."
return 1
fi
echo -e "\n${BLUE}Vérification du projet: $manual_project_id${NC}"
if gcloud projects describe "$manual_project_id" &>/dev/null; then
echo -e "${GREEN}Projet trouvé!${NC}"
all_projects=("$manual_project_id")
else
echo -e "${RED}Projet non trouvé ou vous n'avez pas les permissions nécessaires.${NC}"
read -p "Appuyez sur Entrée pour continuer..."
return 1
fi
;;
3) # Afficher les détails de débogage
show_debug_info
read -p "Appuyez sur Entrée pour continuer..."
return 1
;;
4) # Retour
return 1
;;
esac
fi
# Ajouter option de retour
all_projects+=("← Retour au menu principal")
local selected=0
local filter=""
local filtered_projects=()
# Fonction pour filtrer les projets
filter_projects() {
filtered_projects=()
for project in "${all_projects[@]}"; do
if [[ "$project" == *"$filter"* ]]; then
filtered_projects+=("$project")
fi
done
# Réajuster la sélection si nécessaire
if [ $selected -ge ${#filtered_projects[@]} ]; then
selected=0
fi
}
# Initialiser le filtre
filter_projects
while true; do
clear
echo -e "${BLUE}${BOLD}=== Sélection du projet ===${NC}\n"
echo -e "${YELLOW}Projet actuel: ${BOLD}${CURRENT_PROJECT:-"Aucun"}${NC}"
echo ""
echo -e "${YELLOW}Tapez pour filtrer, utilisez ↑↓ pour naviguer, Entrée pour sélectionner:${NC}"
echo -e "Filtre: ${BOLD}$filter${NC}"
echo ""
# Afficher les projets filtrés
if [ ${#filtered_projects[@]} -eq 0 ]; then
echo -e "${RED}Aucun projet trouvé pour le filtre '$filter'${NC}"
else
for i in "${!filtered_projects[@]}"; do
if [ $i -eq $selected ]; then
echo -e "${GREEN}${filtered_projects[$i]}${NC}"
else
echo -e " ${filtered_projects[$i]}"
fi
done
fi
echo -e "\n${BLUE}Commandes: [Backspace] Effacer filtre | [Esc] Annuler${NC}"
# Lire une seule touche
read -rsn1 key
case $key in
$'\033') # Séquence d'échappement
read -rsn2 key
case $key in
'[A') # Flèche haut
if [ ${#filtered_projects[@]} -gt 0 ]; then
((selected--))
if [ $selected -lt 0 ]; then
selected=$((${#filtered_projects[@]}-1))
fi
fi
;;
'[B') # Flèche bas
if [ ${#filtered_projects[@]} -gt 0 ]; then
((selected++))
if [ $selected -ge ${#filtered_projects[@]} ]; then
selected=0
fi
fi
;;
esac
;;
'') # Entrée
if [ ${#filtered_projects[@]} -gt 0 ]; then
local chosen_project="${filtered_projects[$selected]}"
if [[ "$chosen_project" == "← Retour au menu principal" ]]; then
return 1
else
echo -e "\n${BLUE}Configuration du projet: $chosen_project${NC}"
if gcloud config set project "$chosen_project" &>/dev/null; then
CURRENT_PROJECT="$chosen_project"
echo -e "${GREEN}Projet configuré avec succès!${NC}"
sleep 2
return 0
else
echo -e "${RED}Erreur lors de la configuration du projet.${NC}"
read -p "Appuyez sur Entrée pour continuer..."
return 1
fi
fi
fi
;;
$'\177'|$'\b') # Backspace
if [ ${#filter} -gt 0 ]; then
filter="${filter%?}"
filter_projects
fi
;;
$'\e') # Escape
return 1
;;
*) # Caractère normal
if [[ "$key" =~ [a-zA-Z0-9-] ]]; then
filter="$filter$key"
filter_projects
fi
;;
esac
done
}
# Fonction pour lister et sélectionner un projet (version simplifiée)
select_project() {
select_project_interactive
}
# Fonction pour afficher les informations de débogage des Cloud Functions
show_functions_debug_info() {
clear
echo -e "${BLUE}${BOLD}=== Débogage Cloud Functions ===${NC}\n"
echo -e "${YELLOW}1. Test Cloud Functions Gen1 (toutes régions):${NC}"
gcloud functions list 2>&1 || echo "Erreur lors de la récupération des fonctions Gen1"
echo ""
echo -e "${YELLOW}2. Test Cloud Functions Gen2 (Cloud Run):${NC}"
gcloud run services list 2>&1 || echo "Erreur lors de la récupération des services Cloud Run"
echo ""
echo -e "${YELLOW}3. Régions disponibles pour Cloud Functions:${NC}"
gcloud functions regions list 2>&1 || echo "Erreur lors de la récupération des régions"
echo ""
echo -e "${YELLOW}4. Configuration actuelle:${NC}"
echo "Projet: $(gcloud config get-value project 2>/dev/null)"
echo "Région par défaut: $(gcloud config get-value functions/region 2>/dev/null || echo 'Non définie')"
echo ""
echo -e "${YELLOW}5. APIs activées:${NC}"
gcloud services list --enabled --filter="name:cloudfunctions OR name:run OR name:cloudbuild" 2>&1 || echo "Erreur lors de la vérification des APIs"
echo ""
}
# Fonction pour sélectionner une région de manière interactive avec filtrage
select_region_interactive() {
echo -e "\n${BLUE}Récupération des régions disponibles...${NC}"
# Récupérer toutes les régions disponibles
local all_regions=()
while IFS= read -r region; do
if [[ -n "$region" ]]; then
all_regions+=("$region")
fi
done < <(gcloud functions regions list --format="value(name)" 2>/dev/null | sort)
if [ ${#all_regions[@]} -eq 0 ]; then
echo -e "${RED}Impossible de récupérer la liste des régions.${NC}"
read -p "Appuyez sur Entrée pour continuer..."
return 1
fi
# Ajouter option de retour
all_regions+=("← Retour au menu précédent")
local selected=0
local filter=""
local filtered_regions=()
# Fonction pour filtrer les régions
filter_regions() {
filtered_regions=()
for region in "${all_regions[@]}"; do
if [[ "$region" == *"$filter"* ]]; then
filtered_regions+=("$region")
fi
done
# Réajuster la sélection si nécessaire
if [ $selected -ge ${#filtered_regions[@]} ]; then
selected=0
fi
}
# Initialiser le filtre
filter_regions
while true; do
clear
echo -e "${BLUE}${BOLD}=== Sélection de région ===${NC}\n"
echo -e "${YELLOW}Configuration actuelle:${NC}"
echo "Région par défaut: $(gcloud config get-value functions/region 2>/dev/null || echo 'Non définie')"
echo ""
echo -e "${YELLOW}Tapez pour filtrer, utilisez ↑↓ pour naviguer, Entrée pour sélectionner:${NC}"
echo -e "Filtre: ${BOLD}$filter${NC}"
echo ""
# Afficher les régions filtrées
if [ ${#filtered_regions[@]} -eq 0 ]; then
echo -e "${RED}Aucune région trouvée pour le filtre '$filter'${NC}"
else
for i in "${!filtered_regions[@]}"; do
if [ $i -eq $selected ]; then
echo -e "${GREEN}${filtered_regions[$i]}${NC}"
else
echo -e " ${filtered_regions[$i]}"
fi
done
fi
echo -e "\n${BLUE}Commandes: [Backspace] Effacer filtre | [Esc] Annuler${NC}"
# Lire une seule touche
read -rsn1 key
case $key in
$'\033') # Séquence d'échappement
read -rsn2 key
case $key in
'[A') # Flèche haut
if [ ${#filtered_regions[@]} -gt 0 ]; then
((selected--))
if [ $selected -lt 0 ]; then
selected=$((${#filtered_regions[@]}-1))
fi
fi
;;
'[B') # Flèche bas
if [ ${#filtered_regions[@]} -gt 0 ]; then
((selected++))
if [ $selected -ge ${#filtered_regions[@]} ]; then
selected=0
fi
fi
;;
esac
;;
'') # Entrée
if [ ${#filtered_regions[@]} -gt 0 ]; then
local chosen_region="${filtered_regions[$selected]}"
if [[ "$chosen_region" == "← Retour au menu précédent" ]]; then
return 1
else
echo -e "\n${BLUE}Configuration de la région $chosen_region...${NC}"
if gcloud config set functions/region "$chosen_region" 2>/dev/null; then
echo -e "${GREEN}✓ Région configurée: $chosen_region${NC}"
echo -e "\n${BLUE}Nouvelle tentative de récupération des fonctions...${NC}"
sleep 2
return 0
else
echo -e "${RED}✗ Erreur lors de la configuration de la région${NC}"
read -p "Appuyez sur Entrée pour continuer..."
return 1
fi
fi
fi
;;
$'\177'|$'\b') # Backspace
if [ ${#filter} -gt 0 ]; then
filter="${filter%?}"
filter_regions
fi
;;
$'\e') # Escape
return 1
;;
*) # Caractère normal
if [[ "$key" =~ [a-zA-Z0-9-] ]]; then
filter="$filter$key"
filter_regions
fi
;;
esac
done
}
# Fonction pour spécifier une région et réessayer (version simplifiée)
specify_region_and_retry() {
if select_region_interactive; then
delete_all_functions
fi
}
# Fonction pour supprimer toutes les Cloud Functions
delete_all_functions() {
# Ne pas clear ici pour garder les informations du menu principal visible
echo -e "${BLUE}Récupération des Cloud Functions du projet $CURRENT_PROJECT...${NC}"
# Obtenir toutes les fonctions (sans distinction Gen1/Gen2)
local all_functions=()
local function_names=()
local function_regions=()
echo -e "\n${YELLOW}Recherche de toutes les Cloud Functions...${NC}"
# Récupérer tous les noms de fonctions avec leurs régions
while read -r line; do
if [[ -n "$line" && "$line" != *"NAME"* ]]; then
# Extraire le nom et la région de la fonction
name=$(echo "$line" | awk '{print $1}')
region=$(echo "$line" | awk '{print $4}')
if [[ -n "$name" && "$name" != "NAME" && -n "$region" ]]; then
function_names+=("$name")
function_regions+=("$region")
all_functions+=("$name ($region)")
fi
fi
done < <(gcloud functions list --format="table(name,state,trigger,region)" 2>/dev/null | tail -n +2)
echo -e "${GREEN}Total des fonctions trouvées: ${#function_names[@]}${NC}\n"
if [ ${#function_names[@]} -eq 0 ]; then
echo -e "${YELLOW}Aucune Cloud Function trouvée dans le projet $CURRENT_PROJECT.${NC}"
echo -e "${YELLOW}Cela peut être dû à:${NC}"
echo -e " • Aucune fonction déployée"
echo -e " • Fonctions dans une région non configurée"
echo -e " • APIs Cloud Functions/Cloud Run non activées"
echo -e " • Permissions insuffisantes"
echo ""
local debug_options=("Afficher les informations de débogage" "Spécifier une région" "← Retour au menu principal")
echo "Que souhaitez-vous faire ?\n"
show_menu "${debug_options[@]}"
local debug_choice=$?
case $debug_choice in
0) # Afficher debug
show_functions_debug_info
read -p "Appuyez sur Entrée pour continuer..."
;;
1) # Spécifier région
specify_region_and_retry
;;
2) # Retour
return
;;
esac
return
fi
echo -e "\n${YELLOW}${BOLD}Fonctions trouvées (${#function_names[@]}):${NC}"
for func in "${function_names[@]}"; do
echo -e "$func"
done
echo -e "\n${RED}${BOLD}⚠️ ATTENTION: Cette action supprimera TOUTES les Cloud Functions du projet!${NC}"
echo -e "${RED}Cette action est IRRÉVERSIBLE!${NC}\n"
local confirm_options=("Oui, supprimer toutes les fonctions" "Non, annuler")
show_menu "${confirm_options[@]}"
local confirm=$?
if [ $confirm -eq 0 ]; then
echo -e "\n${RED}Suppression en cours...${NC}\n"
local success_count=0
local error_count=0
# Supprimer toutes les fonctions avec leur région spécifique
for i in "${!function_names[@]}"; do
local func="${function_names[$i]}"
local region="${function_regions[$i]}"
echo -e "Suppression de: ${BOLD}$func${NC} (région: $region)"
# Capturer l'erreur détaillée avec la région spécifique
local error_output
error_output=$(gcloud functions delete "$func" --region="$region" --quiet 2>&1)
local exit_code=$?
if [ $exit_code -eq 0 ]; then
echo -e "${GREEN}$func supprimée${NC}"
((success_count++))
else
echo -e "${RED}✗ Erreur lors de la suppression de $func${NC}"
echo -e "${RED} Détail: $error_output${NC}"
((error_count++))
fi
# Petite pause pour éviter les erreurs de rate limiting
sleep 0.5
done
echo -e "\n${BOLD}Résumé:${NC}"
echo -e "${GREEN}• Fonctions supprimées: $success_count${NC}"
if [ $error_count -gt 0 ]; then
echo -e "${RED}• Erreurs: $error_count${NC}"
echo -e "${YELLOW}Il y a eu des erreurs lors de la suppression.${NC}"
echo -e "${YELLOW}Vérifiez les permissions et l'état des fonctions.${NC}"
echo -e "${YELLOW}Les erreurs détaillées sont affichées ci-dessus.${NC}"
else
echo -e "${GREEN}Toutes les fonctions ont été supprimées avec succès!${NC}"
fi
echo -e "\n${BLUE}Appuyez sur Entrée pour continuer...${NC}"
read -r
else
echo -e "\n${YELLOW}Suppression annulée.${NC}"
echo -e "\n${BLUE}Appuyez sur Entrée pour continuer...${NC}"
read -r
fi
read -p "Appuyez sur Entrée pour continuer..."
}
# Fonction principale
main() {
clear
check_gcloud
while true; do
get_current_project
local main_options=()
if [ "$CURRENT_PROJECT" != "Aucun" ]; then
main_options=("Supprimer toutes les Cloud Functions" "Changer de projet" "Se connecter à GCP" "Quitter")
else
main_options=("Changer de projet" "Se connecter à GCP" "Quitter")
fi
show_menu "${main_options[@]}"
local choice=$?
if [ "$CURRENT_PROJECT" != "Aucun" ]; then
case $choice in
0) # Supprimer toutes les fonctions
delete_all_functions
;;
1) # Changer de projet
select_project
;;
2) # Se connecter à GCP
handle_authentication
;;
3) # Quitter
echo -e "\n${YELLOW}Au revoir!${NC}"
exit 0
;;
esac
else
case $choice in
0) # Changer de projet
select_project
;;
1) # Se connecter à GCP
handle_authentication
;;
2) # Quitter
echo -e "\n${YELLOW}Au revoir!${NC}"
exit 0
;;
esac
fi
done
}
# Gestion des signaux pour un arrêt propre
trap 'echo -e "\n${YELLOW}Script interrompu.${NC}"; exit 0' INT TERM
# Lancement du script
main
+44
View File
@@ -0,0 +1,44 @@
function basicTemplate({ title = "", content = "", button = null }) {
const btn = button?.url
? `<p><a href="${button.url}" style="display:inline-block;padding:10px 16px;background:#6C5CE7;color:#fff;border-radius:8px;text-decoration:none">${
button?.label || "Ouvrir"
}</a></p>`
: "";
return `<!doctype html><html><body style="font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.5;color:#111">
<div style="max-width:640px;margin:40px auto;padding:24px;border:1px solid #eee;border-radius:12px">
<h2 style="margin:0 0 16px">${title}</h2>
<div>${content}</div>
${btn}
<hr style="margin:24px 0;border:none;border-top:1px solid #eee" />
<p style="color:#666;font-size:12px">minuit.app</p>
</div>
</body></html>`;
}
function welcomeTemplate({ firstName = "", lastName = "" }) {
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim();
const greeting = fullName ? `Salut ${fullName},` : "Salut,";
return `<!doctype html><html><body style="margin:0;padding:0;background:#0b0b10;color:#fff;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
<div style="max-width:640px;margin:0 auto;padding:40px 24px">
<div style="background:#151520;border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,0.08)">
<div style="padding:32px 28px">
<p style="margin:0 0 12px;font-size:16px;letter-spacing:0.2px">${greeting}</p>
<h1 style="margin:0 0 16px;font-size:28px;line-height:1.2">Bienvenue sur MusicLand</h1>
<p style="margin:0 0 16px;font-size:16px;line-height:1.6;color:rgba(255,255,255,0.85)">
Nous sommes ravis de t'accueillir dans la communauté des artistes et passionnés de MusicLand.
</p>
<p style="margin:0 0 16px;font-size:15px;line-height:1.6;color:rgba(255,255,255,0.75)">
Explore les playlists collaboratives, dépose tes projets et découvre ce que prépare la scène émergente.
</p>
<p style="margin:0;font-size:15px;line-height:1.6;color:rgba(255,255,255,0.75)">
L'équipe MusicLand
</p>
</div>
</div>
<p style="margin:32px 0 0;text-align:center;font-size:12px;color:rgba(255,255,255,0.45)">minuit.app · Paris, France</p>
</div>
</body></html>`;
}
exports.basicTemplate = basicTemplate;
exports.welcomeTemplate = welcomeTemplate;
+382 -274
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -24,6 +24,7 @@
"firebase-functions": "^6.4.0",
"fluent-ffmpeg": "^2.1.3",
"genkit": "^1.18.0",
"resend": "^6.3.0",
"sharp": "^0.33.4"
},
"devDependencies": {
+60 -23
View File
@@ -5,11 +5,18 @@ const {
const admin = require("firebase-admin");
const { refList, ALERT_TYPE } = require("../index");
const { Expo } = require("expo-server-sdk");
const { Resend } = require("resend");
const { basicTemplate } = require("../helpers/email");
const { RESEND_API_KEY } = require("../config/keys");
const resendInstance = new Resend(RESEND_API_KEY);
// Initialisation de Expo SDK
let expo = new Expo();
const db = admin.firestore();
const EMAIL_FROM = "MusicLand <musicland@minuit.app>";
exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
{ region: "europe-west1", document: "notifications/{notificationId}" },
async (event) => {
@@ -26,14 +33,18 @@ exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
throw new Error("Receiver and message are required");
}
const userSnap = await db.collection(receiverCollection).doc(receiver).get();
const { pushToken = null, pushTokens = [] } = userSnap?.data() || {};
const {
pushToken = null,
pushTokens = [],
email = "",
emailNotifications = false,
} = (await db.collection(receiverCollection).doc(receiver).get()).data();
const tokensSet = new Set(
[]
.concat(Array.isArray(pushTokens) ? pushTokens : [])
.concat(pushToken ? [pushToken] : [])
.filter(Boolean)
.filter(Boolean),
);
const tokens = Array.from(tokensSet);
@@ -50,11 +61,31 @@ exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
message: message,
data: notifData || {},
});
if (emailNotifications && !!email) {
try {
if (!email) {
throw new Error("userMail is required");
}
if (!email?.subject || !email?.html) {
throw new Error("Email subject and html are required");
}
await resendInstance.emails.send({
from: EMAIL_FROM,
to: [email],
subject: title,
html: basicTemplate({ title, content: message }),
});
} catch (e) {
console.log("Error sending email:", e);
}
} else {
console.log(`Email disabled for user ${receiver} and type ${type}`);
}
} catch (e) {
console.log(e);
return e;
}
}
},
);
// Fonction pour envoyer la notification via Expo SDK
@@ -144,7 +175,11 @@ async function sendExpoNotification({
}
}
async function removeInvalidTokens({ tokens = [], receiverId, receiverCollection }) {
async function removeInvalidTokens({
tokens = [],
receiverId,
receiverCollection,
}) {
try {
if (!receiverId || !tokens.length) {
return;
@@ -170,7 +205,7 @@ async function removeInvalidTokens({ tokens = [], receiverId, receiverCollection
await docRef.set(updates, { merge: true });
console.log(
"Pruned invalid push tokens",
JSON.stringify({ receiverId, tokens: uniqueTokens }, null, 2)
JSON.stringify({ receiverId, tokens: uniqueTokens }, null, 2),
);
} catch (error) {
console.log("Failed to prune invalid push tokens:", error);
@@ -204,7 +239,7 @@ const sendNotification = async ({
const { id } = await refList.notifications.add(payload);
console.log(
"[sendNotification] Notification created",
JSON.stringify({ id, receiver, receiverCollection }, null, 2)
JSON.stringify({ id, receiver, receiverCollection }, null, 2),
);
return id;
@@ -224,7 +259,7 @@ exports.createProjectCommentNotification = onDocumentCreated(
try {
console.log(
"[createProjectCommentNotification] Trigger received",
JSON.stringify(event.params || {}, null, 2)
JSON.stringify(event.params || {}, null, 2),
);
const { data: snap } = event;
const { projectId, commentId } = event.params || {};
@@ -233,21 +268,21 @@ exports.createProjectCommentNotification = onDocumentCreated(
if (!projectId || !comment) {
console.log(
"[createProjectCommentNotification] Missing project/comment data",
{ hasProjectId: !!projectId, hasComment: !!comment }
{ hasProjectId: !!projectId, hasComment: !!comment },
);
return null;
}
console.log(
"[createProjectCommentNotification] Comment payload",
JSON.stringify(comment, null, 2)
JSON.stringify(comment, null, 2),
);
const projectSnap = await refList.projects.doc(projectId).get();
if (!projectSnap.exists) {
console.log(
"[createProjectCommentNotification] Project not found",
projectId
projectId,
);
return null;
}
@@ -258,7 +293,7 @@ exports.createProjectCommentNotification = onDocumentCreated(
if (!receiver || receiver === comment.userId) {
console.log(
"[createProjectCommentNotification] Invalid receiver",
JSON.stringify({ receiver, commentUserId: comment.userId })
JSON.stringify({ receiver, commentUserId: comment.userId }),
);
return null;
}
@@ -283,8 +318,8 @@ exports.createProjectCommentNotification = onDocumentCreated(
projectId,
},
null,
2
)
2,
),
);
await sendNotification({
@@ -308,7 +343,7 @@ exports.createProjectCommentNotification = onDocumentCreated(
});
console.log(
"[createProjectCommentNotification] Notification creation complete"
"[createProjectCommentNotification] Notification creation complete",
);
return null;
@@ -316,7 +351,7 @@ exports.createProjectCommentNotification = onDocumentCreated(
console.log("createProjectCommentNotification error:", error);
return error;
}
}
},
);
exports.createProjectLikeNotification = onDocumentWritten(
@@ -356,10 +391,10 @@ exports.createProjectLikeNotification = onDocumentWritten(
const beforePlaybackSet = new Set(beforePlaybackLikes);
const newSongLikers = afterSongLikes.filter(
(uid) => uid && !beforeSongSet.has(uid)
(uid) => uid && !beforeSongSet.has(uid),
);
const newPlaybackLikers = afterPlaybackLikes.filter(
(uid) => uid && !beforePlaybackSet.has(uid)
(uid) => uid && !beforePlaybackSet.has(uid),
);
const newLikers = [];
@@ -413,7 +448,7 @@ exports.createProjectLikeNotification = onDocumentWritten(
});
return null;
})
}),
);
return null;
@@ -421,7 +456,7 @@ exports.createProjectLikeNotification = onDocumentWritten(
console.log("[createProjectLikeNotification] error:", error);
return error;
}
}
},
);
exports.createNewFollowerNotification = onDocumentWritten(
@@ -451,7 +486,9 @@ exports.createNewFollowerNotification = onDocumentWritten(
}
const previousSet = new Set(beforeFollowers);
const newFollowers = afterFollowers.filter((uid) => !previousSet.has(uid));
const newFollowers = afterFollowers.filter(
(uid) => !previousSet.has(uid),
);
if (!newFollowers.length) {
return null;
@@ -487,7 +524,7 @@ exports.createNewFollowerNotification = onDocumentWritten(
});
return null;
})
}),
);
return null;
@@ -495,5 +532,5 @@ exports.createNewFollowerNotification = onDocumentWritten(
console.log("[createNewFollowerNotification] error:", error);
return error;
}
}
},
);
+38 -1
View File
@@ -1,7 +1,44 @@
const admin = require("firebase-admin");
const { onDocumentDeleted } = require("firebase-functions/firestore");
const {
onDocumentDeleted,
onDocumentCreated,
} = require("firebase-functions/firestore");
const { refList } = require("../index");
const { deleteFolder } = require("../helpers/firebase");
const { Resend } = require("resend");
const { welcomeTemplate } = require("../helpers/email");
const { RESEND_API_KEY } = require("../config/keys");
const resendClient = new Resend(RESEND_API_KEY);
const WELCOME_EMAIL_FROM =
process.env.RESEND_FROM_EMAIL || "MusicLand <musicland@minuit.app>";
const WELCOME_EMAIL_SUBJECT = "Bienvenue sur MusicLand";
exports.onUserCreated = onDocumentCreated("users/{userID}", async (event) => {
try {
const { email = "", firstName = "", lastName = "" } = event?.data?.data();
if (email) {
if (!resendClient) {
console.warn("Resend API key not configured; skipping welcome email.");
return;
}
try {
await resendClient.emails.send({
from: WELCOME_EMAIL_FROM,
to: [email],
subject: WELCOME_EMAIL_SUBJECT,
html: welcomeTemplate({ firstName, lastName }),
});
} catch (error) {
console.log("Failed to send welcome email:", error);
}
} else {
throw new Error("User created with empty email");
}
} catch (e) {
console.log(e);
}
});
exports.onUserDelete = onDocumentDeleted("users/{userID}", async (event) => {
try {