mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-13 17:21:48 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eacefb155 | |||
| 843b13ed57 | |||
| be89e5ca55 | |||
| 333625f199 |
@@ -525,41 +525,10 @@ jobs:
|
|||||||
VERIFIED_INDEX_KEYLESS=false
|
VERIFIED_INDEX_KEYLESS=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If index verification fails, attempt to verify child platform manifests
|
# Check if verification succeeded
|
||||||
if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
||||||
echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}"
|
echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
CHILD_VERIFIED=false
|
echo "This may be due to registry propagation delays. Continuing anyway."
|
||||||
|
|
||||||
for ARCH in arm64 amd64; do
|
|
||||||
CHILD_TAG="${IMAGE_TAG}-${ARCH}"
|
|
||||||
echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}"
|
|
||||||
CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)"
|
|
||||||
if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then
|
|
||||||
CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}"
|
|
||||||
echo "==> cosign verify (public key) child ${CHILD_REF}"
|
|
||||||
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then
|
|
||||||
CHILD_VERIFIED=true
|
|
||||||
echo "Public key verification succeeded for child ${CHILD_REF}"
|
|
||||||
else
|
|
||||||
echo "Public key verification failed for child ${CHILD_REF}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> cosign verify (keyless policy) child ${CHILD_REF}"
|
|
||||||
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then
|
|
||||||
CHILD_VERIFIED=true
|
|
||||||
echo "Keyless verification succeeded for child ${CHILD_REF}"
|
|
||||||
else
|
|
||||||
echo "Keyless verification failed for child ${CHILD_REF}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "${CHILD_VERIFIED}" != "true" ]; then
|
|
||||||
echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
) || TAG_FAILED=true
|
) || TAG_FAILED=true
|
||||||
|
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Филтрирайте по състояние на одобрение",
|
"filterByApprovalState": "Филтрирайте по състояние на одобрение",
|
||||||
"approvalListEmpty": "Няма одобрения",
|
"approvalListEmpty": "Няма одобрения",
|
||||||
"approvalState": "Състояние на одобрение",
|
"approvalState": "Състояние на одобрение",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Одобряване",
|
"approve": "Одобряване",
|
||||||
"approved": "Одобрен",
|
"approved": "Одобрен",
|
||||||
"denied": "Отказан",
|
"denied": "Отказан",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Преглед на дневници",
|
"actionViewLogs": "Преглед на дневници",
|
||||||
"noneSelected": "Нищо не е избрано",
|
"noneSelected": "Нищо не е избрано",
|
||||||
"orgNotFound2": "Няма намерени организации.",
|
"orgNotFound2": "Няма намерени организации.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Търсене...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Създаване",
|
"create": "Създаване",
|
||||||
"orgs": "Организации",
|
"orgs": "Организации",
|
||||||
"loginError": "Възникна неочаквана грешка. Моля, опитайте отново.",
|
"loginError": "Възникна неочаквана грешка. Моля, опитайте отново.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Сигурни ли сте, че искате да премахнете брандинга за страниците за автентификация?",
|
"authPageBrandingQuestionRemove": "Сигурни ли сте, че искате да премахнете брандинга за страниците за автентификация?",
|
||||||
"authPageBrandingDeleteConfirm": "Потвърждение на изтриване на брандинга.",
|
"authPageBrandingDeleteConfirm": "Потвърждение на изтриване на брандинга.",
|
||||||
"brandingLogoURL": "URL адрес на логото.",
|
"brandingLogoURL": "URL адрес на логото.",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Основен цвят.",
|
"brandingPrimaryColor": "Основен цвят.",
|
||||||
"brandingLogoWidth": "Ширина (px).",
|
"brandingLogoWidth": "Ширина (px).",
|
||||||
"brandingLogoHeight": "Височина (px).",
|
"brandingLogoHeight": "Височина (px).",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filtrovat podle státu schválení",
|
"filterByApprovalState": "Filtrovat podle státu schválení",
|
||||||
"approvalListEmpty": "Žádná schválení",
|
"approvalListEmpty": "Žádná schválení",
|
||||||
"approvalState": "Země schválení",
|
"approvalState": "Země schválení",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Schválit",
|
"approve": "Schválit",
|
||||||
"approved": "Schváleno",
|
"approved": "Schváleno",
|
||||||
"denied": "Zamítnuto",
|
"denied": "Zamítnuto",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Zobrazit logy",
|
"actionViewLogs": "Zobrazit logy",
|
||||||
"noneSelected": "Není vybráno",
|
"noneSelected": "Není vybráno",
|
||||||
"orgNotFound2": "Nebyly nalezeny žádné organizace.",
|
"orgNotFound2": "Nebyly nalezeny žádné organizace.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Hledat...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Vytvořit",
|
"create": "Vytvořit",
|
||||||
"orgs": "Organizace",
|
"orgs": "Organizace",
|
||||||
"loginError": "Došlo k neočekávané chybě. Zkuste to prosím znovu.",
|
"loginError": "Došlo k neočekávané chybě. Zkuste to prosím znovu.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Jste si jisti, že chcete odstranit branding autentizačních stránek?",
|
"authPageBrandingQuestionRemove": "Jste si jisti, že chcete odstranit branding autentizačních stránek?",
|
||||||
"authPageBrandingDeleteConfirm": "Potvrzení odstranění brandingu",
|
"authPageBrandingDeleteConfirm": "Potvrzení odstranění brandingu",
|
||||||
"brandingLogoURL": "URL loga",
|
"brandingLogoURL": "URL loga",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Primární barva",
|
"brandingPrimaryColor": "Primární barva",
|
||||||
"brandingLogoWidth": "Šířka (px)",
|
"brandingLogoWidth": "Šířka (px)",
|
||||||
"brandingLogoHeight": "Výška (px)",
|
"brandingLogoHeight": "Výška (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filtern nach Genehmigungsstatus",
|
"filterByApprovalState": "Filtern nach Genehmigungsstatus",
|
||||||
"approvalListEmpty": "Keine Genehmigungen",
|
"approvalListEmpty": "Keine Genehmigungen",
|
||||||
"approvalState": "Genehmigungsstatus",
|
"approvalState": "Genehmigungsstatus",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Bestätigen",
|
"approve": "Bestätigen",
|
||||||
"approved": "Genehmigt",
|
"approved": "Genehmigt",
|
||||||
"denied": "Verweigert",
|
"denied": "Verweigert",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Logs anzeigen",
|
"actionViewLogs": "Logs anzeigen",
|
||||||
"noneSelected": "Keine ausgewählt",
|
"noneSelected": "Keine ausgewählt",
|
||||||
"orgNotFound2": "Keine Organisationen gefunden.",
|
"orgNotFound2": "Keine Organisationen gefunden.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Suche...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"orgs": "Organisationen",
|
"orgs": "Organisationen",
|
||||||
"loginError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
|
"loginError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Sind Sie sicher, dass Sie das Branding für Authentifizierungsseiten entfernen möchten?",
|
"authPageBrandingQuestionRemove": "Sind Sie sicher, dass Sie das Branding für Authentifizierungsseiten entfernen möchten?",
|
||||||
"authPageBrandingDeleteConfirm": "Branding löschen bestätigen",
|
"authPageBrandingDeleteConfirm": "Branding löschen bestätigen",
|
||||||
"brandingLogoURL": "Logo URL",
|
"brandingLogoURL": "Logo URL",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Primär-Farbe",
|
"brandingPrimaryColor": "Primär-Farbe",
|
||||||
"brandingLogoWidth": "Breite (px)",
|
"brandingLogoWidth": "Breite (px)",
|
||||||
"brandingLogoHeight": "Höhe (px)",
|
"brandingLogoHeight": "Höhe (px)",
|
||||||
|
|||||||
@@ -201,6 +201,7 @@
|
|||||||
"protocolSelect": "Select a protocol",
|
"protocolSelect": "Select a protocol",
|
||||||
"resourcePortNumber": "Port Number",
|
"resourcePortNumber": "Port Number",
|
||||||
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
||||||
|
"back": "Back",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"resourceConfig": "Configuration Snippets",
|
"resourceConfig": "Configuration Snippets",
|
||||||
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
|
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
|
||||||
@@ -246,6 +247,17 @@
|
|||||||
"orgErrorDeleteMessage": "An error occurred while deleting the organization.",
|
"orgErrorDeleteMessage": "An error occurred while deleting the organization.",
|
||||||
"orgDeleted": "Organization deleted",
|
"orgDeleted": "Organization deleted",
|
||||||
"orgDeletedMessage": "The organization and its data has been deleted.",
|
"orgDeletedMessage": "The organization and its data has been deleted.",
|
||||||
|
"deleteAccount": "Delete Account",
|
||||||
|
"deleteAccountDescription": "Permanently delete your account, all organizations you own, and all data within those organizations. This cannot be undone.",
|
||||||
|
"deleteAccountButton": "Delete Account",
|
||||||
|
"deleteAccountConfirmTitle": "Delete Account",
|
||||||
|
"deleteAccountConfirmMessage": "This will permanently wipe your account, all organizations you own, and all data within those organizations. This cannot be undone.",
|
||||||
|
"deleteAccountConfirmString": "delete account",
|
||||||
|
"deleteAccountSuccess": "Account Deleted",
|
||||||
|
"deleteAccountSuccessMessage": "Your account has been deleted.",
|
||||||
|
"deleteAccountError": "Failed to delete account",
|
||||||
|
"deleteAccountPreviewAccount": "Your Account",
|
||||||
|
"deleteAccountPreviewOrgs": "Organizations you own (and all their data)",
|
||||||
"orgMissing": "Organization ID Missing",
|
"orgMissing": "Organization ID Missing",
|
||||||
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
||||||
"accessUsersManage": "Manage Users",
|
"accessUsersManage": "Manage Users",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filtrar por estado de aprobación",
|
"filterByApprovalState": "Filtrar por estado de aprobación",
|
||||||
"approvalListEmpty": "No hay aprobaciones",
|
"approvalListEmpty": "No hay aprobaciones",
|
||||||
"approvalState": "Estado de aprobación",
|
"approvalState": "Estado de aprobación",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Aprobar",
|
"approve": "Aprobar",
|
||||||
"approved": "Aprobado",
|
"approved": "Aprobado",
|
||||||
"denied": "Denegado",
|
"denied": "Denegado",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Ver registros",
|
"actionViewLogs": "Ver registros",
|
||||||
"noneSelected": "Ninguno seleccionado",
|
"noneSelected": "Ninguno seleccionado",
|
||||||
"orgNotFound2": "No se encontraron organizaciones.",
|
"orgNotFound2": "No se encontraron organizaciones.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Buscar...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Crear",
|
"create": "Crear",
|
||||||
"orgs": "Organizaciones",
|
"orgs": "Organizaciones",
|
||||||
"loginError": "Ocurrió un error inesperado. Por favor, inténtelo de nuevo.",
|
"loginError": "Ocurrió un error inesperado. Por favor, inténtelo de nuevo.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "¿Está seguro de que desea eliminar la marca de las páginas de autenticación?",
|
"authPageBrandingQuestionRemove": "¿Está seguro de que desea eliminar la marca de las páginas de autenticación?",
|
||||||
"authPageBrandingDeleteConfirm": "Confirmar eliminación de la marca",
|
"authPageBrandingDeleteConfirm": "Confirmar eliminación de la marca",
|
||||||
"brandingLogoURL": "URL del logotipo",
|
"brandingLogoURL": "URL del logotipo",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Color primario",
|
"brandingPrimaryColor": "Color primario",
|
||||||
"brandingLogoWidth": "Ancho (px)",
|
"brandingLogoWidth": "Ancho (px)",
|
||||||
"brandingLogoHeight": "Altura (px)",
|
"brandingLogoHeight": "Altura (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filtrer par État d'Approbation",
|
"filterByApprovalState": "Filtrer par État d'Approbation",
|
||||||
"approvalListEmpty": "Aucune approbation",
|
"approvalListEmpty": "Aucune approbation",
|
||||||
"approvalState": "État d'approbation",
|
"approvalState": "État d'approbation",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Approuver",
|
"approve": "Approuver",
|
||||||
"approved": "Approuvé",
|
"approved": "Approuvé",
|
||||||
"denied": "Refusé",
|
"denied": "Refusé",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Voir les logs",
|
"actionViewLogs": "Voir les logs",
|
||||||
"noneSelected": "Aucune sélection",
|
"noneSelected": "Aucune sélection",
|
||||||
"orgNotFound2": "Aucune organisation trouvée.",
|
"orgNotFound2": "Aucune organisation trouvée.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Rechercher...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Créer",
|
"create": "Créer",
|
||||||
"orgs": "Organisations",
|
"orgs": "Organisations",
|
||||||
"loginError": "Une erreur inattendue s'est produite. Veuillez réessayer.",
|
"loginError": "Une erreur inattendue s'est produite. Veuillez réessayer.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Êtes-vous sûr de vouloir supprimer la marque des pages d'authentification ?",
|
"authPageBrandingQuestionRemove": "Êtes-vous sûr de vouloir supprimer la marque des pages d'authentification ?",
|
||||||
"authPageBrandingDeleteConfirm": "Confirmer la suppression de la marque",
|
"authPageBrandingDeleteConfirm": "Confirmer la suppression de la marque",
|
||||||
"brandingLogoURL": "URL du logo",
|
"brandingLogoURL": "URL du logo",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Couleur principale",
|
"brandingPrimaryColor": "Couleur principale",
|
||||||
"brandingLogoWidth": "Largeur (px)",
|
"brandingLogoWidth": "Largeur (px)",
|
||||||
"brandingLogoHeight": "Hauteur (px)",
|
"brandingLogoHeight": "Hauteur (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filtra Per Stato Di Approvazione",
|
"filterByApprovalState": "Filtra Per Stato Di Approvazione",
|
||||||
"approvalListEmpty": "Nessuna approvazione",
|
"approvalListEmpty": "Nessuna approvazione",
|
||||||
"approvalState": "Stato Di Approvazione",
|
"approvalState": "Stato Di Approvazione",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Approva",
|
"approve": "Approva",
|
||||||
"approved": "Approvato",
|
"approved": "Approvato",
|
||||||
"denied": "Negato",
|
"denied": "Negato",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Visualizza Log",
|
"actionViewLogs": "Visualizza Log",
|
||||||
"noneSelected": "Nessuna selezione",
|
"noneSelected": "Nessuna selezione",
|
||||||
"orgNotFound2": "Nessuna organizzazione trovata.",
|
"orgNotFound2": "Nessuna organizzazione trovata.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Ricerca...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Crea",
|
"create": "Crea",
|
||||||
"orgs": "Organizzazioni",
|
"orgs": "Organizzazioni",
|
||||||
"loginError": "Si è verificato un errore imprevisto. Riprova.",
|
"loginError": "Si è verificato un errore imprevisto. Riprova.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Sei sicuro di voler rimuovere il branding per le pagine di autenticazione?",
|
"authPageBrandingQuestionRemove": "Sei sicuro di voler rimuovere il branding per le pagine di autenticazione?",
|
||||||
"authPageBrandingDeleteConfirm": "Conferma Eliminazione Branding",
|
"authPageBrandingDeleteConfirm": "Conferma Eliminazione Branding",
|
||||||
"brandingLogoURL": "URL Logo",
|
"brandingLogoURL": "URL Logo",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Colore Primario",
|
"brandingPrimaryColor": "Colore Primario",
|
||||||
"brandingLogoWidth": "Larghezza (px)",
|
"brandingLogoWidth": "Larghezza (px)",
|
||||||
"brandingLogoHeight": "Altezza (px)",
|
"brandingLogoHeight": "Altezza (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "승인 상태로 필터링",
|
"filterByApprovalState": "승인 상태로 필터링",
|
||||||
"approvalListEmpty": "승인이 없습니다.",
|
"approvalListEmpty": "승인이 없습니다.",
|
||||||
"approvalState": "승인 상태",
|
"approvalState": "승인 상태",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "승인",
|
"approve": "승인",
|
||||||
"approved": "승인됨",
|
"approved": "승인됨",
|
||||||
"denied": "거부됨",
|
"denied": "거부됨",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "로그 보기",
|
"actionViewLogs": "로그 보기",
|
||||||
"noneSelected": "선택된 항목 없음",
|
"noneSelected": "선택된 항목 없음",
|
||||||
"orgNotFound2": "조직이 없습니다.",
|
"orgNotFound2": "조직이 없습니다.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "검색...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "생성",
|
"create": "생성",
|
||||||
"orgs": "조직",
|
"orgs": "조직",
|
||||||
"loginError": "예기치 않은 오류가 발생했습니다. 다시 시도해주세요.",
|
"loginError": "예기치 않은 오류가 발생했습니다. 다시 시도해주세요.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "인증 페이지의 브랜딩을 제거하시겠습니까?",
|
"authPageBrandingQuestionRemove": "인증 페이지의 브랜딩을 제거하시겠습니까?",
|
||||||
"authPageBrandingDeleteConfirm": "브랜딩 삭제 확인",
|
"authPageBrandingDeleteConfirm": "브랜딩 삭제 확인",
|
||||||
"brandingLogoURL": "로고 URL",
|
"brandingLogoURL": "로고 URL",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "기본 색상",
|
"brandingPrimaryColor": "기본 색상",
|
||||||
"brandingLogoWidth": "너비(px)",
|
"brandingLogoWidth": "너비(px)",
|
||||||
"brandingLogoHeight": "높이(px)",
|
"brandingLogoHeight": "높이(px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filtrer etter godkjenningsstatus",
|
"filterByApprovalState": "Filtrer etter godkjenningsstatus",
|
||||||
"approvalListEmpty": "Ingen godkjenninger",
|
"approvalListEmpty": "Ingen godkjenninger",
|
||||||
"approvalState": "Godkjennings tilstand",
|
"approvalState": "Godkjennings tilstand",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Godkjenn",
|
"approve": "Godkjenn",
|
||||||
"approved": "Godkjent",
|
"approved": "Godkjent",
|
||||||
"denied": "Avvist",
|
"denied": "Avvist",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Vis logger",
|
"actionViewLogs": "Vis logger",
|
||||||
"noneSelected": "Ingen valgt",
|
"noneSelected": "Ingen valgt",
|
||||||
"orgNotFound2": "Ingen organisasjoner funnet.",
|
"orgNotFound2": "Ingen organisasjoner funnet.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Søker...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Opprett",
|
"create": "Opprett",
|
||||||
"orgs": "Organisasjoner",
|
"orgs": "Organisasjoner",
|
||||||
"loginError": "En uventet feil oppstod. Vennligst prøv igjen.",
|
"loginError": "En uventet feil oppstod. Vennligst prøv igjen.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
|
"authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
|
||||||
"authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging",
|
"authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging",
|
||||||
"brandingLogoURL": "Logo URL",
|
"brandingLogoURL": "Logo URL",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Primærfarge",
|
"brandingPrimaryColor": "Primærfarge",
|
||||||
"brandingLogoWidth": "Bredde (px)",
|
"brandingLogoWidth": "Bredde (px)",
|
||||||
"brandingLogoHeight": "Høyde (px)",
|
"brandingLogoHeight": "Høyde (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filter op goedkeuringsstatus",
|
"filterByApprovalState": "Filter op goedkeuringsstatus",
|
||||||
"approvalListEmpty": "Geen goedkeuringen",
|
"approvalListEmpty": "Geen goedkeuringen",
|
||||||
"approvalState": "Goedkeuring status",
|
"approvalState": "Goedkeuring status",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Goedkeuren",
|
"approve": "Goedkeuren",
|
||||||
"approved": "Goedgekeurd",
|
"approved": "Goedgekeurd",
|
||||||
"denied": "Geweigerd",
|
"denied": "Geweigerd",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Logboeken bekijken",
|
"actionViewLogs": "Logboeken bekijken",
|
||||||
"noneSelected": "Niet geselecteerd",
|
"noneSelected": "Niet geselecteerd",
|
||||||
"orgNotFound2": "Geen organisaties gevonden.",
|
"orgNotFound2": "Geen organisaties gevonden.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Zoeken...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Aanmaken",
|
"create": "Aanmaken",
|
||||||
"orgs": "Organisaties",
|
"orgs": "Organisaties",
|
||||||
"loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.",
|
"loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?",
|
"authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?",
|
||||||
"authPageBrandingDeleteConfirm": "Bevestig verwijder Branding",
|
"authPageBrandingDeleteConfirm": "Bevestig verwijder Branding",
|
||||||
"brandingLogoURL": "Het logo-URL",
|
"brandingLogoURL": "Het logo-URL",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Primaire kleur",
|
"brandingPrimaryColor": "Primaire kleur",
|
||||||
"brandingLogoWidth": "Breedte (px)",
|
"brandingLogoWidth": "Breedte (px)",
|
||||||
"brandingLogoHeight": "Hoogte (px)",
|
"brandingLogoHeight": "Hoogte (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filtruj według państwa zatwierdzenia",
|
"filterByApprovalState": "Filtruj według państwa zatwierdzenia",
|
||||||
"approvalListEmpty": "Brak zatwierdzeń",
|
"approvalListEmpty": "Brak zatwierdzeń",
|
||||||
"approvalState": "Państwo zatwierdzające",
|
"approvalState": "Państwo zatwierdzające",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Zatwierdź",
|
"approve": "Zatwierdź",
|
||||||
"approved": "Zatwierdzone",
|
"approved": "Zatwierdzone",
|
||||||
"denied": "Odmowa",
|
"denied": "Odmowa",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Zobacz dzienniki",
|
"actionViewLogs": "Zobacz dzienniki",
|
||||||
"noneSelected": "Nie wybrano",
|
"noneSelected": "Nie wybrano",
|
||||||
"orgNotFound2": "Nie znaleziono organizacji.",
|
"orgNotFound2": "Nie znaleziono organizacji.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Szukaj...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Utwórz",
|
"create": "Utwórz",
|
||||||
"orgs": "Organizacje",
|
"orgs": "Organizacje",
|
||||||
"loginError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.",
|
"loginError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Czy na pewno chcesz usunąć branding dla stron uwierzytelniania?",
|
"authPageBrandingQuestionRemove": "Czy na pewno chcesz usunąć branding dla stron uwierzytelniania?",
|
||||||
"authPageBrandingDeleteConfirm": "Potwierdź usunięcie brandingu",
|
"authPageBrandingDeleteConfirm": "Potwierdź usunięcie brandingu",
|
||||||
"brandingLogoURL": "URL logo",
|
"brandingLogoURL": "URL logo",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Główny kolor",
|
"brandingPrimaryColor": "Główny kolor",
|
||||||
"brandingLogoWidth": "Szerokość (piksele)",
|
"brandingLogoWidth": "Szerokość (piksele)",
|
||||||
"brandingLogoHeight": "Wysokość (piksele)",
|
"brandingLogoHeight": "Wysokość (piksele)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Filtrar por estado de aprovação",
|
"filterByApprovalState": "Filtrar por estado de aprovação",
|
||||||
"approvalListEmpty": "Sem aprovações",
|
"approvalListEmpty": "Sem aprovações",
|
||||||
"approvalState": "Estado de aprovação",
|
"approvalState": "Estado de aprovação",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Aprovar",
|
"approve": "Aprovar",
|
||||||
"approved": "Aceito",
|
"approved": "Aceito",
|
||||||
"denied": "Negado",
|
"denied": "Negado",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Visualizar registros",
|
"actionViewLogs": "Visualizar registros",
|
||||||
"noneSelected": "Nenhum selecionado",
|
"noneSelected": "Nenhum selecionado",
|
||||||
"orgNotFound2": "Nenhuma organização encontrada.",
|
"orgNotFound2": "Nenhuma organização encontrada.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Pesquisar...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Criar",
|
"create": "Criar",
|
||||||
"orgs": "Organizações",
|
"orgs": "Organizações",
|
||||||
"loginError": "Ocorreu um erro inesperado. Por favor, tente novamente.",
|
"loginError": "Ocorreu um erro inesperado. Por favor, tente novamente.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Tem certeza de que deseja remover a marcação das Páginas de Autenticação?",
|
"authPageBrandingQuestionRemove": "Tem certeza de que deseja remover a marcação das Páginas de Autenticação?",
|
||||||
"authPageBrandingDeleteConfirm": "Confirmar Exclusão de Marca",
|
"authPageBrandingDeleteConfirm": "Confirmar Exclusão de Marca",
|
||||||
"brandingLogoURL": "URL do Logo",
|
"brandingLogoURL": "URL do Logo",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Cor Primária",
|
"brandingPrimaryColor": "Cor Primária",
|
||||||
"brandingLogoWidth": "Largura (px)",
|
"brandingLogoWidth": "Largura (px)",
|
||||||
"brandingLogoHeight": "Altura (px)",
|
"brandingLogoHeight": "Altura (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Фильтр по состоянию утверждения",
|
"filterByApprovalState": "Фильтр по состоянию утверждения",
|
||||||
"approvalListEmpty": "Нет утверждений",
|
"approvalListEmpty": "Нет утверждений",
|
||||||
"approvalState": "Состояние одобрения",
|
"approvalState": "Состояние одобрения",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Одобрить",
|
"approve": "Одобрить",
|
||||||
"approved": "Одобрено",
|
"approved": "Одобрено",
|
||||||
"denied": "Отказано",
|
"denied": "Отказано",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Просмотр журналов",
|
"actionViewLogs": "Просмотр журналов",
|
||||||
"noneSelected": "Ничего не выбрано",
|
"noneSelected": "Ничего не выбрано",
|
||||||
"orgNotFound2": "Организации не найдены.",
|
"orgNotFound2": "Организации не найдены.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Поиск...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Создать",
|
"create": "Создать",
|
||||||
"orgs": "Организации",
|
"orgs": "Организации",
|
||||||
"loginError": "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз.",
|
"loginError": "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Вы уверены, что хотите удалить брендирование для страниц аутентификации?",
|
"authPageBrandingQuestionRemove": "Вы уверены, что хотите удалить брендирование для страниц аутентификации?",
|
||||||
"authPageBrandingDeleteConfirm": "Подтвердить удаление брендирования",
|
"authPageBrandingDeleteConfirm": "Подтвердить удаление брендирования",
|
||||||
"brandingLogoURL": "URL логотипа",
|
"brandingLogoURL": "URL логотипа",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Основной цвет",
|
"brandingPrimaryColor": "Основной цвет",
|
||||||
"brandingLogoWidth": "Ширина (px)",
|
"brandingLogoWidth": "Ширина (px)",
|
||||||
"brandingLogoHeight": "Высота (px)",
|
"brandingLogoHeight": "Высота (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "Onay Durumuna Göre Filtrele",
|
"filterByApprovalState": "Onay Durumuna Göre Filtrele",
|
||||||
"approvalListEmpty": "Onay yok",
|
"approvalListEmpty": "Onay yok",
|
||||||
"approvalState": "Onay Durumu",
|
"approvalState": "Onay Durumu",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "Onayla",
|
"approve": "Onayla",
|
||||||
"approved": "Onaylandı",
|
"approved": "Onaylandı",
|
||||||
"denied": "Reddedildi",
|
"denied": "Reddedildi",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "Kayıtları Görüntüle",
|
"actionViewLogs": "Kayıtları Görüntüle",
|
||||||
"noneSelected": "Hiçbiri seçili değil",
|
"noneSelected": "Hiçbiri seçili değil",
|
||||||
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "Ara...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "Oluştur",
|
"create": "Oluştur",
|
||||||
"orgs": "Organizasyonlar",
|
"orgs": "Organizasyonlar",
|
||||||
"loginError": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.",
|
"loginError": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "Kimlik Sayfaları için markayı kaldırmak istediğinizden emin misiniz?",
|
"authPageBrandingQuestionRemove": "Kimlik Sayfaları için markayı kaldırmak istediğinizden emin misiniz?",
|
||||||
"authPageBrandingDeleteConfirm": "Markayı Silmeyi Onayla",
|
"authPageBrandingDeleteConfirm": "Markayı Silmeyi Onayla",
|
||||||
"brandingLogoURL": "Logo URL",
|
"brandingLogoURL": "Logo URL",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "Ana Renk",
|
"brandingPrimaryColor": "Ana Renk",
|
||||||
"brandingLogoWidth": "Genişlik (px)",
|
"brandingLogoWidth": "Genişlik (px)",
|
||||||
"brandingLogoHeight": "Yükseklik (px)",
|
"brandingLogoHeight": "Yükseklik (px)",
|
||||||
|
|||||||
+1
-7
@@ -461,8 +461,6 @@
|
|||||||
"filterByApprovalState": "按批准状态过滤",
|
"filterByApprovalState": "按批准状态过滤",
|
||||||
"approvalListEmpty": "无批准",
|
"approvalListEmpty": "无批准",
|
||||||
"approvalState": "审批状态",
|
"approvalState": "审批状态",
|
||||||
"approvalLoadMore": "Load more",
|
|
||||||
"loadingApprovals": "Loading Approvals",
|
|
||||||
"approve": "批准",
|
"approve": "批准",
|
||||||
"approved": "已批准",
|
"approved": "已批准",
|
||||||
"denied": "被拒绝",
|
"denied": "被拒绝",
|
||||||
@@ -1171,8 +1169,7 @@
|
|||||||
"actionViewLogs": "查看日志",
|
"actionViewLogs": "查看日志",
|
||||||
"noneSelected": "未选择",
|
"noneSelected": "未选择",
|
||||||
"orgNotFound2": "未找到组织。",
|
"orgNotFound2": "未找到组织。",
|
||||||
"searchPlaceholder": "Search...",
|
"searchProgress": "搜索中...",
|
||||||
"emptySearchOptions": "No options found",
|
|
||||||
"create": "创建",
|
"create": "创建",
|
||||||
"orgs": "组织",
|
"orgs": "组织",
|
||||||
"loginError": "发生意外错误。请重试。",
|
"loginError": "发生意外错误。请重试。",
|
||||||
@@ -1919,9 +1916,6 @@
|
|||||||
"authPageBrandingQuestionRemove": "您确定要移除授权页面的品牌吗?",
|
"authPageBrandingQuestionRemove": "您确定要移除授权页面的品牌吗?",
|
||||||
"authPageBrandingDeleteConfirm": "确认删除品牌",
|
"authPageBrandingDeleteConfirm": "确认删除品牌",
|
||||||
"brandingLogoURL": "Logo URL",
|
"brandingLogoURL": "Logo URL",
|
||||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
|
||||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
|
||||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
|
||||||
"brandingPrimaryColor": "主要颜色",
|
"brandingPrimaryColor": "主要颜色",
|
||||||
"brandingLogoWidth": "宽度(px)",
|
"brandingLogoWidth": "宽度(px)",
|
||||||
"brandingLogoHeight": "高度(px)",
|
"brandingLogoHeight": "高度(px)",
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ export const sandboxLimitSet: LimitSet = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const freeLimitSet: LimitSet = {
|
export const freeLimitSet: LimitSet = {
|
||||||
[FeatureId.USERS]: { value: 5, description: "Starter limit" },
|
[FeatureId.SITES]: { value: 5, description: "Basic limit" },
|
||||||
[FeatureId.SITES]: { value: 5, description: "Starter limit" },
|
[FeatureId.USERS]: { value: 5, description: "Basic limit" },
|
||||||
[FeatureId.DOMAINS]: { value: 5, description: "Starter limit" },
|
[FeatureId.DOMAINS]: { value: 5, description: "Basic limit" },
|
||||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Starter limit" },
|
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tier1LimitSet: LimitSet = {
|
export const tier1LimitSet: LimitSet = {
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import {
|
||||||
|
clients,
|
||||||
|
clientSiteResourcesAssociationsCache,
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
db,
|
||||||
|
domains,
|
||||||
|
olms,
|
||||||
|
orgDomains,
|
||||||
|
orgs,
|
||||||
|
resources,
|
||||||
|
sites
|
||||||
|
} from "@server/db";
|
||||||
|
import { newts, newtSessions } from "@server/db";
|
||||||
|
import { eq, and, inArray, sql } from "drizzle-orm";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
|
import { deletePeer } from "@server/routers/gerbil/peers";
|
||||||
|
import { OlmErrorCodes } from "@server/routers/olm/error";
|
||||||
|
import { sendTerminateClient } from "@server/routers/client/terminate";
|
||||||
|
|
||||||
|
export type DeleteOrgByIdResult = {
|
||||||
|
deletedNewtIds: string[];
|
||||||
|
olmsToTerminate: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes one organization and its related data. Returns ids for termination
|
||||||
|
* messages; caller should call sendTerminationMessages with the result.
|
||||||
|
* Throws if org not found.
|
||||||
|
*/
|
||||||
|
export async function deleteOrgById(
|
||||||
|
orgId: string
|
||||||
|
): Promise<DeleteOrgByIdResult> {
|
||||||
|
const [org] = await db
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
throw createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Organization with ID ${orgId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgSites = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.orgId, orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const orgClients = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.orgId, orgId));
|
||||||
|
|
||||||
|
const deletedNewtIds: string[] = [];
|
||||||
|
const olmsToTerminate: string[] = [];
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
for (const site of orgSites) {
|
||||||
|
if (site.pubKey) {
|
||||||
|
if (site.type == "wireguard") {
|
||||||
|
await deletePeer(site.exitNodeId!, site.pubKey);
|
||||||
|
} else if (site.type == "newt") {
|
||||||
|
const [deletedNewt] = await trx
|
||||||
|
.delete(newts)
|
||||||
|
.where(eq(newts.siteId, site.siteId))
|
||||||
|
.returning();
|
||||||
|
if (deletedNewt) {
|
||||||
|
deletedNewtIds.push(deletedNewt.newtId);
|
||||||
|
await trx
|
||||||
|
.delete(newtSessions)
|
||||||
|
.where(
|
||||||
|
eq(newtSessions.newtId, deletedNewt.newtId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(`Deleting site ${site.siteId}`);
|
||||||
|
await trx.delete(sites).where(eq(sites.siteId, site.siteId));
|
||||||
|
}
|
||||||
|
for (const client of orgClients) {
|
||||||
|
const [olm] = await trx
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, client.clientId))
|
||||||
|
.limit(1);
|
||||||
|
if (olm) {
|
||||||
|
olmsToTerminate.push(olm.olmId);
|
||||||
|
}
|
||||||
|
logger.info(`Deleting client ${client.clientId}`);
|
||||||
|
await trx
|
||||||
|
.delete(clients)
|
||||||
|
.where(eq(clients.clientId, client.clientId));
|
||||||
|
await trx
|
||||||
|
.delete(clientSiteResourcesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
clientSiteResourcesAssociationsCache.clientId,
|
||||||
|
client.clientId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await trx
|
||||||
|
.delete(clientSitesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
eq(clientSitesAssociationsCache.clientId, client.clientId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const allOrgDomains = await trx
|
||||||
|
.select()
|
||||||
|
.from(orgDomains)
|
||||||
|
.innerJoin(domains, eq(domains.domainId, orgDomains.domainId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(orgDomains.orgId, orgId),
|
||||||
|
eq(domains.configManaged, false)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const domainIdsToDelete: string[] = [];
|
||||||
|
for (const orgDomain of allOrgDomains) {
|
||||||
|
const domainId = orgDomain.domains.domainId;
|
||||||
|
const orgCount = await trx
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(orgDomains)
|
||||||
|
.where(eq(orgDomains.domainId, domainId));
|
||||||
|
if (orgCount[0].count === 1) {
|
||||||
|
domainIdsToDelete.push(domainId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (domainIdsToDelete.length > 0) {
|
||||||
|
await trx
|
||||||
|
.delete(domains)
|
||||||
|
.where(inArray(domains.domainId, domainIdsToDelete));
|
||||||
|
}
|
||||||
|
await trx.delete(resources).where(eq(resources.orgId, orgId));
|
||||||
|
await trx.delete(orgs).where(eq(orgs.orgId, orgId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { deletedNewtIds, olmsToTerminate };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendTerminationMessages(result: DeleteOrgByIdResult): void {
|
||||||
|
for (const newtId of result.deletedNewtIds) {
|
||||||
|
sendToClient(newtId, { type: `newt/wg/terminate`, data: {} }).catch(
|
||||||
|
(error) => {
|
||||||
|
logger.error(
|
||||||
|
"Failed to send termination message to newt:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const olmId of result.olmsToTerminate) {
|
||||||
|
sendTerminateClient(
|
||||||
|
0,
|
||||||
|
OlmErrorCodes.TERMINATED_REKEYED,
|
||||||
|
olmId
|
||||||
|
).catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
"Failed to send termination message to olm:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
|
|||||||
import { isSubscribed } from "#private/lib/isSubscribed";
|
import { isSubscribed } from "#private/lib/isSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
|
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
|
||||||
|
|
||||||
@@ -122,12 +123,14 @@ export async function createOrgOidcIdp(
|
|||||||
|
|
||||||
let { autoProvision } = parsedBody.data;
|
let { autoProvision } = parsedBody.data;
|
||||||
|
|
||||||
const subscribed = await isSubscribed(
|
if (build == "saas") { // this is not paywalled with a ee license because this whole endpoint is restricted
|
||||||
orgId,
|
const subscribed = await isSubscribed(
|
||||||
tierMatrix.deviceApprovals
|
orgId,
|
||||||
);
|
tierMatrix.deviceApprovals
|
||||||
if (!subscribed) {
|
);
|
||||||
autoProvision = false;
|
if (!subscribed) {
|
||||||
|
autoProvision = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = config.getRawConfig().server.secret!;
|
const key = config.getRawConfig().server.secret!;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import config from "@server/lib/config";
|
|||||||
import { isSubscribed } from "#private/lib/isSubscribed";
|
import { isSubscribed } from "#private/lib/isSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -127,12 +128,15 @@ export async function updateOrgOidcIdp(
|
|||||||
|
|
||||||
let { autoProvision } = parsedBody.data;
|
let { autoProvision } = parsedBody.data;
|
||||||
|
|
||||||
const subscribed = await isSubscribed(
|
if (build == "saas") {
|
||||||
orgId,
|
// this is not paywalled with a ee license because this whole endpoint is restricted
|
||||||
tierMatrix.deviceApprovals
|
const subscribed = await isSubscribed(
|
||||||
);
|
orgId,
|
||||||
if (!subscribed) {
|
tierMatrix.deviceApprovals
|
||||||
autoProvision = false;
|
);
|
||||||
|
if (!subscribed) {
|
||||||
|
autoProvision = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if IDP exists and is of type OIDC
|
// Check if IDP exists and is of type OIDC
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, orgs, userOrgs, users } from "@server/db";
|
||||||
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { verifySession } from "@server/auth/sessions/verifySession";
|
||||||
|
import {
|
||||||
|
invalidateSession,
|
||||||
|
createBlankSessionTokenCookie
|
||||||
|
} from "@server/auth/sessions/app";
|
||||||
|
import { verifyPassword } from "@server/auth/password";
|
||||||
|
import { verifyTotpCode } from "@server/auth/totp";
|
||||||
|
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||||
|
import {
|
||||||
|
deleteOrgById,
|
||||||
|
sendTerminationMessages
|
||||||
|
} from "@server/lib/deleteOrg";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
|
||||||
|
const deleteMyAccountBody = z.strictObject({
|
||||||
|
password: z.string().optional(),
|
||||||
|
code: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DeleteMyAccountPreviewResponse = {
|
||||||
|
preview: true;
|
||||||
|
orgs: { orgId: string; name: string }[];
|
||||||
|
twoFactorEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteMyAccountCodeRequestedResponse = {
|
||||||
|
codeRequested: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteMyAccountSuccessResponse = {
|
||||||
|
success: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Self-service account deletion (saas only). Returns preview when no password;
|
||||||
|
* requires password and optional 2FA code to perform deletion. Uses shared
|
||||||
|
* deleteOrgById for each owned org (delete-my-account may delete multiple orgs).
|
||||||
|
*/
|
||||||
|
export async function deleteMyAccount(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const { user, session } = await verifySession(req);
|
||||||
|
if (!user || !session) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.serverAdmin) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Server admins cannot delete their account this way"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.type !== UserType.Internal) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Account deletion with password is only supported for internal users"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = deleteMyAccountBody.safeParse(req.body ?? {});
|
||||||
|
if (!parsed.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsed.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { password, code } = parsed.data;
|
||||||
|
|
||||||
|
const userId = user.userId;
|
||||||
|
|
||||||
|
const ownedOrgsRows = await db
|
||||||
|
.select({
|
||||||
|
orgId: userOrgs.orgId
|
||||||
|
})
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.userId, userId),
|
||||||
|
eq(userOrgs.isOwner, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const orgIds = ownedOrgsRows.map((r) => r.orgId);
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
const orgsWithNames =
|
||||||
|
orgIds.length > 0
|
||||||
|
? await db
|
||||||
|
.select({
|
||||||
|
orgId: orgs.orgId,
|
||||||
|
name: orgs.name
|
||||||
|
})
|
||||||
|
.from(orgs)
|
||||||
|
.where(inArray(orgs.orgId, orgIds))
|
||||||
|
: [];
|
||||||
|
return response<DeleteMyAccountPreviewResponse>(res, {
|
||||||
|
data: {
|
||||||
|
preview: true,
|
||||||
|
orgs: orgsWithNames.map((o) => ({
|
||||||
|
orgId: o.orgId,
|
||||||
|
name: o.name ?? ""
|
||||||
|
})),
|
||||||
|
twoFactorEnabled: user.twoFactorEnabled ?? false
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Preview",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const validPassword = await verifyPassword(
|
||||||
|
password,
|
||||||
|
user.passwordHash!
|
||||||
|
);
|
||||||
|
if (!validPassword) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Invalid password")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.twoFactorEnabled) {
|
||||||
|
if (!code) {
|
||||||
|
return response<DeleteMyAccountCodeRequestedResponse>(res, {
|
||||||
|
data: { codeRequested: true },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Two-factor code required",
|
||||||
|
status: HttpCode.ACCEPTED
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const validOTP = await verifyTotpCode(
|
||||||
|
code,
|
||||||
|
user.twoFactorSecret!,
|
||||||
|
user.userId
|
||||||
|
);
|
||||||
|
if (!validOTP) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"The two-factor code you entered is incorrect"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allDeletedNewtIds: string[] = [];
|
||||||
|
const allOlmsToTerminate: string[] = [];
|
||||||
|
|
||||||
|
for (const row of ownedOrgsRows) {
|
||||||
|
try {
|
||||||
|
const result = await deleteOrgById(row.orgId);
|
||||||
|
allDeletedNewtIds.push(...result.deletedNewtIds);
|
||||||
|
allOlmsToTerminate.push(...result.olmsToTerminate);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to delete org ${row.orgId} during account deletion`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to delete organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTerminationMessages({
|
||||||
|
deletedNewtIds: allDeletedNewtIds,
|
||||||
|
olmsToTerminate: allOlmsToTerminate
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx.delete(users).where(eq(users.userId, userId));
|
||||||
|
await calculateUserClientsForOrgs(userId, trx);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invalidateSession(session.sessionId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Failed to invalidate session after account deletion",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSecure = req.protocol === "https";
|
||||||
|
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
||||||
|
|
||||||
|
return response<DeleteMyAccountSuccessResponse>(res, {
|
||||||
|
data: { success: true },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Account deleted successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"An error occurred"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,4 +17,5 @@ export * from "./securityKey";
|
|||||||
export * from "./startDeviceWebAuth";
|
export * from "./startDeviceWebAuth";
|
||||||
export * from "./verifyDeviceWebAuth";
|
export * from "./verifyDeviceWebAuth";
|
||||||
export * from "./pollDeviceWebAuth";
|
export * from "./pollDeviceWebAuth";
|
||||||
export * from "./lookupUser";
|
export * from "./lookupUser";
|
||||||
|
export * from "./deleteMyAccount";
|
||||||
@@ -797,7 +797,7 @@ async function notAllowed(
|
|||||||
) {
|
) {
|
||||||
let loginPage: LoginPage | null = null;
|
let loginPage: LoginPage | null = null;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
const subscribed = await isSubscribed(
|
const subscribed = await isSubscribed( // this is fine because the org login page is only a saas feature
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix.loginPageDomain
|
tierMatrix.loginPageDomain
|
||||||
);
|
);
|
||||||
@@ -854,7 +854,7 @@ async function headerAuthChallenged(
|
|||||||
) {
|
) {
|
||||||
let loginPage: LoginPage | null = null;
|
let loginPage: LoginPage | null = null;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain);
|
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain); // this is fine because the org login page is only a saas feature
|
||||||
if (subscribed) {
|
if (subscribed) {
|
||||||
loginPage = await getOrgLoginPage(orgId);
|
loginPage = await getOrgLoginPage(orgId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1164,6 +1164,7 @@ authRouter.post(
|
|||||||
auth.login
|
auth.login
|
||||||
);
|
);
|
||||||
authRouter.post("/logout", auth.logout);
|
authRouter.post("/logout", auth.logout);
|
||||||
|
authRouter.post("/delete-my-account", auth.deleteMyAccount);
|
||||||
authRouter.post(
|
authRouter.post(
|
||||||
"/lookup-user",
|
"/lookup-user",
|
||||||
rateLimit({
|
rateLimit({
|
||||||
|
|||||||
@@ -1,28 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
|
||||||
clients,
|
|
||||||
clientSiteResourcesAssociationsCache,
|
|
||||||
clientSitesAssociationsCache,
|
|
||||||
db,
|
|
||||||
domains,
|
|
||||||
olms,
|
|
||||||
orgDomains,
|
|
||||||
resources
|
|
||||||
} from "@server/db";
|
|
||||||
import { newts, newtSessions, orgs, sites, userActions } from "@server/db";
|
|
||||||
import { eq, and, inArray, sql } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { sendToClient } from "#dynamic/routers/ws";
|
|
||||||
import { deletePeer } from "../gerbil/peers";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { OlmErrorCodes } from "../olm/error";
|
import { deleteOrgById, sendTerminationMessages } from "@server/lib/deleteOrg";
|
||||||
import { sendTerminateClient } from "../client/terminate";
|
|
||||||
|
|
||||||
const deleteOrgSchema = z.strictObject({
|
const deleteOrgSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -56,170 +40,9 @@ export async function deleteOrg(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
const result = await deleteOrgById(orgId);
|
||||||
const [org] = await db
|
sendTerminationMessages(result);
|
||||||
.select()
|
|
||||||
.from(orgs)
|
|
||||||
.where(eq(orgs.orgId, orgId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!org) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Organization with ID ${orgId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// we need to handle deleting each site
|
|
||||||
const orgSites = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(eq(sites.orgId, orgId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const orgClients = await db
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.orgId, orgId));
|
|
||||||
|
|
||||||
const deletedNewtIds: string[] = [];
|
|
||||||
const olmsToTerminate: string[] = [];
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
for (const site of orgSites) {
|
|
||||||
if (site.pubKey) {
|
|
||||||
if (site.type == "wireguard") {
|
|
||||||
await deletePeer(site.exitNodeId!, site.pubKey);
|
|
||||||
} else if (site.type == "newt") {
|
|
||||||
// get the newt on the site by querying the newt table for siteId
|
|
||||||
const [deletedNewt] = await trx
|
|
||||||
.delete(newts)
|
|
||||||
.where(eq(newts.siteId, site.siteId))
|
|
||||||
.returning();
|
|
||||||
if (deletedNewt) {
|
|
||||||
deletedNewtIds.push(deletedNewt.newtId);
|
|
||||||
|
|
||||||
// delete all of the sessions for the newt
|
|
||||||
await trx
|
|
||||||
.delete(newtSessions)
|
|
||||||
.where(
|
|
||||||
eq(newtSessions.newtId, deletedNewt.newtId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Deleting site ${site.siteId}`);
|
|
||||||
await trx.delete(sites).where(eq(sites.siteId, site.siteId));
|
|
||||||
}
|
|
||||||
for (const client of orgClients) {
|
|
||||||
const [olm] = await trx
|
|
||||||
.select()
|
|
||||||
.from(olms)
|
|
||||||
.where(eq(olms.clientId, client.clientId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (olm) {
|
|
||||||
olmsToTerminate.push(olm.olmId);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Deleting client ${client.clientId}`);
|
|
||||||
await trx
|
|
||||||
.delete(clients)
|
|
||||||
.where(eq(clients.clientId, client.clientId));
|
|
||||||
|
|
||||||
// also delete the associations
|
|
||||||
await trx
|
|
||||||
.delete(clientSiteResourcesAssociationsCache)
|
|
||||||
.where(
|
|
||||||
eq(
|
|
||||||
clientSiteResourcesAssociationsCache.clientId,
|
|
||||||
client.clientId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await trx
|
|
||||||
.delete(clientSitesAssociationsCache)
|
|
||||||
.where(
|
|
||||||
eq(
|
|
||||||
clientSitesAssociationsCache.clientId,
|
|
||||||
client.clientId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allOrgDomains = await trx
|
|
||||||
.select()
|
|
||||||
.from(orgDomains)
|
|
||||||
.innerJoin(domains, eq(domains.domainId, orgDomains.domainId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(orgDomains.orgId, orgId),
|
|
||||||
eq(domains.configManaged, false)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// For each domain, check if it belongs to multiple organizations
|
|
||||||
const domainIdsToDelete: string[] = [];
|
|
||||||
for (const orgDomain of allOrgDomains) {
|
|
||||||
const domainId = orgDomain.domains.domainId;
|
|
||||||
|
|
||||||
// Count how many organizations this domain belongs to
|
|
||||||
const orgCount = await trx
|
|
||||||
.select({ count: sql<number>`count(*)` })
|
|
||||||
.from(orgDomains)
|
|
||||||
.where(eq(orgDomains.domainId, domainId));
|
|
||||||
|
|
||||||
// Only delete the domain if it belongs to exactly 1 organization (the one being deleted)
|
|
||||||
if (orgCount[0].count === 1) {
|
|
||||||
domainIdsToDelete.push(domainId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete domains that belong exclusively to this organization
|
|
||||||
if (domainIdsToDelete.length > 0) {
|
|
||||||
await trx
|
|
||||||
.delete(domains)
|
|
||||||
.where(inArray(domains.domainId, domainIdsToDelete));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete resources
|
|
||||||
await trx.delete(resources).where(eq(resources.orgId, orgId));
|
|
||||||
|
|
||||||
await trx.delete(orgs).where(eq(orgs.orgId, orgId));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send termination messages outside of transaction to prevent blocking
|
|
||||||
for (const newtId of deletedNewtIds) {
|
|
||||||
const payload = {
|
|
||||||
type: `newt/wg/terminate`,
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
// Don't await this to prevent blocking the response
|
|
||||||
sendToClient(newtId, payload).catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
"Failed to send termination message to newt:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const olmId of olmsToTerminate) {
|
|
||||||
sendTerminateClient(
|
|
||||||
0, // clientId not needed since we're passing olmId
|
|
||||||
OlmErrorCodes.TERMINATED_REKEYED,
|
|
||||||
olmId
|
|
||||||
).catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
"Failed to send termination message to olm:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
@@ -228,6 +51,9 @@ export async function deleteOrg(
|
|||||||
status: HttpCode.OK
|
status: HttpCode.OK
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (createHttpError.isHttpError(error)) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import {
|
|||||||
import { FeatureId } from "@server/lib/billing/features";
|
import { FeatureId } from "@server/lib/billing/features";
|
||||||
|
|
||||||
// Plan tier definitions matching the mockup
|
// Plan tier definitions matching the mockup
|
||||||
type PlanId = "starter" | "home" | "team" | "business" | "enterprise";
|
type PlanId = "basic" | "home" | "team" | "business" | "enterprise";
|
||||||
|
|
||||||
type PlanOption = {
|
type PlanOption = {
|
||||||
id: PlanId;
|
id: PlanId;
|
||||||
@@ -73,8 +73,8 @@ type PlanOption = {
|
|||||||
|
|
||||||
const planOptions: PlanOption[] = [
|
const planOptions: PlanOption[] = [
|
||||||
{
|
{
|
||||||
id: "starter",
|
id: "basic",
|
||||||
name: "Starter",
|
name: "Basic",
|
||||||
price: "Free",
|
price: "Free",
|
||||||
tierType: null
|
tierType: null
|
||||||
},
|
},
|
||||||
@@ -109,10 +109,10 @@ const planOptions: PlanOption[] = [
|
|||||||
|
|
||||||
// Tier limits mapping derived from limit sets
|
// Tier limits mapping derived from limit sets
|
||||||
const tierLimits: Record<
|
const tierLimits: Record<
|
||||||
Tier | "starter",
|
Tier | "basic",
|
||||||
{ users: number; sites: number; domains: number; remoteNodes: number }
|
{ users: number; sites: number; domains: number; remoteNodes: number }
|
||||||
> = {
|
> = {
|
||||||
starter: {
|
basic: {
|
||||||
users: freeLimitSet[FeatureId.USERS]?.value ?? 0,
|
users: freeLimitSet[FeatureId.USERS]?.value ?? 0,
|
||||||
sites: freeLimitSet[FeatureId.SITES]?.value ?? 0,
|
sites: freeLimitSet[FeatureId.SITES]?.value ?? 0,
|
||||||
domains: freeLimitSet[FeatureId.DOMAINS]?.value ?? 0,
|
domains: freeLimitSet[FeatureId.DOMAINS]?.value ?? 0,
|
||||||
@@ -183,7 +183,7 @@ export default function BillingPage() {
|
|||||||
// Confirmation dialog state
|
// Confirmation dialog state
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const [pendingTier, setPendingTier] = useState<{
|
const [pendingTier, setPendingTier] = useState<{
|
||||||
tier: Tier | "starter";
|
tier: Tier | "basic";
|
||||||
action: "upgrade" | "downgrade";
|
action: "upgrade" | "downgrade";
|
||||||
planName: string;
|
planName: string;
|
||||||
price: string;
|
price: string;
|
||||||
@@ -402,8 +402,8 @@ export default function BillingPage() {
|
|||||||
pendingTier.action === "upgrade" ||
|
pendingTier.action === "upgrade" ||
|
||||||
pendingTier.action === "downgrade"
|
pendingTier.action === "downgrade"
|
||||||
) {
|
) {
|
||||||
// If downgrading to starter (free tier), go to Stripe portal
|
// If downgrading to basic (free tier), go to Stripe portal
|
||||||
if (pendingTier.tier === "starter") {
|
if (pendingTier.tier === "basic") {
|
||||||
handleModifySubscription();
|
handleModifySubscription();
|
||||||
} else if (hasSubscription) {
|
} else if (hasSubscription) {
|
||||||
handleChangeTier(pendingTier.tier);
|
handleChangeTier(pendingTier.tier);
|
||||||
@@ -417,7 +417,7 @@ export default function BillingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showTierConfirmation = (
|
const showTierConfirmation = (
|
||||||
tier: Tier | "starter",
|
tier: Tier | "basic",
|
||||||
action: "upgrade" | "downgrade",
|
action: "upgrade" | "downgrade",
|
||||||
planName: string,
|
planName: string,
|
||||||
price: string
|
price: string
|
||||||
@@ -432,9 +432,9 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
// Get current plan ID from tier
|
// Get current plan ID from tier
|
||||||
const getCurrentPlanId = (): PlanId => {
|
const getCurrentPlanId = (): PlanId => {
|
||||||
if (!hasSubscription || !currentTier) return "starter";
|
if (!hasSubscription || !currentTier) return "basic";
|
||||||
const plan = planOptions.find((p) => p.tierType === currentTier);
|
const plan = planOptions.find((p) => p.tierType === currentTier);
|
||||||
return plan?.id || "starter";
|
return plan?.id || "basic";
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentPlanId = getCurrentPlanId();
|
const currentPlanId = getCurrentPlanId();
|
||||||
@@ -451,8 +451,8 @@ export default function BillingPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (plan.id === currentPlanId) {
|
if (plan.id === currentPlanId) {
|
||||||
// If it's the starter plan (starter with no subscription), show as current but disabled
|
// If it's the basic plan (basic with no subscription), show as current but disabled
|
||||||
if (plan.id === "starter" && !hasSubscription) {
|
if (plan.id === "basic" && !hasSubscription) {
|
||||||
return {
|
return {
|
||||||
label: "Current Plan",
|
label: "Current Plan",
|
||||||
action: () => {},
|
action: () => {},
|
||||||
@@ -484,10 +484,10 @@ export default function BillingPage() {
|
|||||||
plan.name,
|
plan.name,
|
||||||
plan.price + (" " + plan.priceDetail || "")
|
plan.price + (" " + plan.priceDetail || "")
|
||||||
);
|
);
|
||||||
} else if (plan.id === "starter") {
|
} else if (plan.id === "basic") {
|
||||||
// Show confirmation for downgrading to starter (free tier)
|
// Show confirmation for downgrading to basic (free tier)
|
||||||
showTierConfirmation(
|
showTierConfirmation(
|
||||||
"starter",
|
"basic",
|
||||||
"downgrade",
|
"downgrade",
|
||||||
plan.name,
|
plan.name,
|
||||||
plan.price
|
plan.price
|
||||||
@@ -566,7 +566,7 @@ export default function BillingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if downgrading to a tier would violate current usage limits
|
// Check if downgrading to a tier would violate current usage limits
|
||||||
const checkLimitViolations = (targetTier: Tier | "starter"): Array<{
|
const checkLimitViolations = (targetTier: Tier | "basic"): Array<{
|
||||||
feature: string;
|
feature: string;
|
||||||
currentUsage: number;
|
currentUsage: number;
|
||||||
newLimit: number;
|
newLimit: number;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import DeleteAccountConfirmDialog from "@app/components/DeleteAccountConfirmDialog";
|
||||||
|
import UserProfileCard from "@app/components/UserProfileCard";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
|
|
||||||
|
type DeleteAccountClientProps = {
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DeleteAccountClient({
|
||||||
|
displayName
|
||||||
|
}: DeleteAccountClientProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
const api = createApiClient({ env });
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
function handleUseDifferentAccount() {
|
||||||
|
api.post("/auth/logout")
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(t("logoutError"), e);
|
||||||
|
toast({
|
||||||
|
title: t("logoutError"),
|
||||||
|
description: formatAxiosError(e, t("logoutError"))
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
router.push(
|
||||||
|
"/auth/login?internal_redirect=/auth/delete-account"
|
||||||
|
);
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<UserProfileCard
|
||||||
|
identifier={displayName}
|
||||||
|
description={t("signingAs")}
|
||||||
|
onUseDifferentAccount={handleUseDifferentAccount}
|
||||||
|
useDifferentAccountText={t("deviceLoginUseDifferentAccount")}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("deleteAccountDescription")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
{t("back")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setIsDialogOpen(true)}
|
||||||
|
>
|
||||||
|
{t("deleteAccountButton")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<DeleteAccountConfirmDialog
|
||||||
|
open={isDialogOpen}
|
||||||
|
setOpen={setIsDialogOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { cache } from "react";
|
||||||
|
import DeleteAccountClient from "./DeleteAccountClient";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function DeleteAccountPage() {
|
||||||
|
const getUser = cache(verifySession);
|
||||||
|
const user = await getUser({ skipCheckVerifyEmail: true });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
redirect("/auth/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = await getTranslations();
|
||||||
|
const displayName = getUserDisplayName({ user });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h1 className="text-xl font-semibold">{t("deleteAccount")}</h1>
|
||||||
|
<DeleteAccountClient displayName={displayName} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { consumeInternalRedirectPath } from "@app/lib/internalRedirect";
|
import { getInternalRedirectTarget } from "@app/lib/internalRedirect";
|
||||||
|
|
||||||
type ApplyInternalRedirectProps = {
|
type ApplyInternalRedirectProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -14,9 +14,9 @@ export default function ApplyInternalRedirect({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const path = consumeInternalRedirectPath();
|
const target = getInternalRedirectTarget(orgId);
|
||||||
if (path) {
|
if (target) {
|
||||||
router.replace(`/${orgId}${path}`);
|
router.replace(target);
|
||||||
}
|
}
|
||||||
}, [orgId, router]);
|
}, [orgId, router]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaClose,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaFooter,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle
|
||||||
|
} from "@app/components/Credenza";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from "@app/components/ui/form";
|
||||||
|
import { Input } from "@app/components/ui/input";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
InputOTP,
|
||||||
|
InputOTPGroup,
|
||||||
|
InputOTPSlot
|
||||||
|
} from "@app/components/ui/input-otp";
|
||||||
|
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||||
|
import type {
|
||||||
|
DeleteMyAccountPreviewResponse,
|
||||||
|
DeleteMyAccountCodeRequestedResponse,
|
||||||
|
DeleteMyAccountSuccessResponse
|
||||||
|
} from "@server/routers/auth/deleteMyAccount";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
type DeleteAccountConfirmDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DeleteAccountConfirmDialog({
|
||||||
|
open,
|
||||||
|
setOpen
|
||||||
|
}: DeleteAccountConfirmDialogProps) {
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
const api = createApiClient({ env });
|
||||||
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const passwordSchema = useMemo(
|
||||||
|
() =>
|
||||||
|
z.object({
|
||||||
|
password: z.string().min(1, { message: t("passwordRequired") })
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeSchema = useMemo(
|
||||||
|
() =>
|
||||||
|
z.object({
|
||||||
|
code: z.string().length(6, { message: t("pincodeInvalid") })
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [step, setStep] = useState<0 | 1 | 2>(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingPreview, setLoadingPreview] = useState(false);
|
||||||
|
const [preview, setPreview] =
|
||||||
|
useState<DeleteMyAccountPreviewResponse | null>(null);
|
||||||
|
const [passwordValue, setPasswordValue] = useState("");
|
||||||
|
|
||||||
|
const passwordForm = useForm<z.infer<typeof passwordSchema>>({
|
||||||
|
resolver: zodResolver(passwordSchema),
|
||||||
|
defaultValues: { password: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const codeForm = useForm<z.infer<typeof codeSchema>>({
|
||||||
|
resolver: zodResolver(codeSchema),
|
||||||
|
defaultValues: { code: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && step === 0 && !preview) {
|
||||||
|
setLoadingPreview(true);
|
||||||
|
api.post<AxiosResponse<DeleteMyAccountPreviewResponse>>(
|
||||||
|
"/auth/delete-my-account",
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data?.data?.preview) {
|
||||||
|
setPreview(res.data.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("deleteAccountError"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
err,
|
||||||
|
t("deleteAccountError")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.finally(() => setLoadingPreview(false));
|
||||||
|
}
|
||||||
|
}, [open, step, preview, api, setOpen, t]);
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
setStep(0);
|
||||||
|
setPreview(null);
|
||||||
|
setPasswordValue("");
|
||||||
|
passwordForm.reset();
|
||||||
|
codeForm.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleContinueToPassword() {
|
||||||
|
setStep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePasswordSubmit(
|
||||||
|
values: z.infer<typeof passwordSchema>
|
||||||
|
) {
|
||||||
|
setLoading(true);
|
||||||
|
setPasswordValue(values.password);
|
||||||
|
try {
|
||||||
|
const res = await api.post<
|
||||||
|
| AxiosResponse<DeleteMyAccountCodeRequestedResponse>
|
||||||
|
| AxiosResponse<DeleteMyAccountSuccessResponse>
|
||||||
|
>("/auth/delete-my-account", { password: values.password });
|
||||||
|
|
||||||
|
const data = res.data?.data;
|
||||||
|
|
||||||
|
if (data && "codeRequested" in data && data.codeRequested) {
|
||||||
|
setStep(2);
|
||||||
|
} else if (data && "success" in data && data.success) {
|
||||||
|
toast({
|
||||||
|
title: t("deleteAccountSuccess"),
|
||||||
|
description: t("deleteAccountSuccessMessage")
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
reset();
|
||||||
|
router.push("/auth/login");
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("deleteAccountError"),
|
||||||
|
description: formatAxiosError(err, t("deleteAccountError"))
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCodeSubmit(values: z.infer<typeof codeSchema>) {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await api.post<
|
||||||
|
AxiosResponse<DeleteMyAccountSuccessResponse>
|
||||||
|
>("/auth/delete-my-account", {
|
||||||
|
password: passwordValue,
|
||||||
|
code: values.code
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data?.data?.success) {
|
||||||
|
toast({
|
||||||
|
title: t("deleteAccountSuccess"),
|
||||||
|
description: t("deleteAccountSuccessMessage")
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
reset();
|
||||||
|
router.push("/auth/login");
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("deleteAccountError"),
|
||||||
|
description: formatAxiosError(err, t("deleteAccountError"))
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Credenza
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(val) => {
|
||||||
|
setOpen(val);
|
||||||
|
if (!val) reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredenzaContent>
|
||||||
|
<CredenzaHeader>
|
||||||
|
<CredenzaTitle>
|
||||||
|
{t("deleteAccountConfirmTitle")}
|
||||||
|
</CredenzaTitle>
|
||||||
|
</CredenzaHeader>
|
||||||
|
<CredenzaBody>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{step === 0 && (
|
||||||
|
<>
|
||||||
|
{loadingPreview ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("loading")}...
|
||||||
|
</p>
|
||||||
|
) : preview ? (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("deleteAccountConfirmMessage")}
|
||||||
|
</p>
|
||||||
|
<div className="rounded-md bg-muted p-3 space-y-2">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{t(
|
||||||
|
"deleteAccountPreviewAccount"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{preview.orgs.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className="text-sm font-medium mt-2">
|
||||||
|
{t(
|
||||||
|
"deleteAccountPreviewOrgs"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
||||||
|
{preview.orgs.map(
|
||||||
|
(org) => (
|
||||||
|
<li
|
||||||
|
key={
|
||||||
|
org.orgId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{org.name ||
|
||||||
|
org.orgId}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-bold text-destructive">
|
||||||
|
{t("cannotbeUndone")}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<Form {...passwordForm}>
|
||||||
|
<form
|
||||||
|
id="delete-account-password-form"
|
||||||
|
onSubmit={passwordForm.handleSubmit(
|
||||||
|
handlePasswordSubmit
|
||||||
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={passwordForm.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("password")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("otpAuthDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Form {...codeForm}>
|
||||||
|
<form
|
||||||
|
id="delete-account-code-form"
|
||||||
|
onSubmit={codeForm.handleSubmit(
|
||||||
|
handleCodeSubmit
|
||||||
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={codeForm.control}
|
||||||
|
name="code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<InputOTP
|
||||||
|
maxLength={6}
|
||||||
|
{...field}
|
||||||
|
pattern={
|
||||||
|
REGEXP_ONLY_DIGITS_AND_CHARS
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
field.onChange(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={
|
||||||
|
0
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={
|
||||||
|
1
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={
|
||||||
|
2
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={
|
||||||
|
3
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={
|
||||||
|
4
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={
|
||||||
|
5
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CredenzaBody>
|
||||||
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">{t("close")}</Button>
|
||||||
|
</CredenzaClose>
|
||||||
|
{step === 0 && preview && !loadingPreview && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleContinueToPassword}
|
||||||
|
>
|
||||||
|
{t("continue")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step === 1 && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
type="submit"
|
||||||
|
form="delete-account-password-form"
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t("deleteAccountButton")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step === 2 && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
type="submit"
|
||||||
|
form="delete-account-code-form"
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t("deleteAccountButton")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CredenzaFooter>
|
||||||
|
</CredenzaContent>
|
||||||
|
</Credenza>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,9 +15,11 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
import { Laptop, LogOut, Moon, Sun, Smartphone } from "lucide-react";
|
import { Laptop, LogOut, Moon, Sun, Smartphone, Trash2 } from "lucide-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { build } from "@server/build";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import Disable2FaForm from "./Disable2FaForm";
|
import Disable2FaForm from "./Disable2FaForm";
|
||||||
@@ -187,6 +189,20 @@ export default function ProfileIcon() {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<LocaleSwitcher />
|
<LocaleSwitcher />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
{user?.type === UserType.Internal && !user?.serverAdmin && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href="/auth/delete-account"
|
||||||
|
className="flex cursor-pointer items-center"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t("deleteAccount")}</span>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuItem onClick={() => logout()}>
|
<DropdownMenuItem onClick={() => logout()}>
|
||||||
{/* <LogOut className="mr-2 h-4 w-4" /> */}
|
{/* <LogOut className="mr-2 h-4 w-4" /> */}
|
||||||
<span>{t("logout")}</span>
|
<span>{t("logout")}</span>
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export default function RedirectToOrg({ targetOrgId }: RedirectToOrgProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const target = getInternalRedirectTarget(targetOrgId);
|
const target =
|
||||||
|
getInternalRedirectTarget(targetOrgId) ?? `/${targetOrgId}`;
|
||||||
router.replace(target);
|
router.replace(target);
|
||||||
} catch {
|
} catch {
|
||||||
router.replace(`/${targetOrgId}`);
|
router.replace(`/${targetOrgId}`);
|
||||||
|
|||||||
@@ -41,11 +41,12 @@ export function consumeInternalRedirectPath(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the full redirect target for an org: either `/${orgId}` or
|
* Returns the full redirect target if a valid internal_redirect was stored
|
||||||
* `/${orgId}${path}` if a valid internal_redirect was stored. Consumes the
|
* (consumes the stored value). Returns null if none was stored or expired.
|
||||||
* stored value.
|
* Paths starting with /auth/ are returned as-is; others are prefixed with orgId.
|
||||||
*/
|
*/
|
||||||
export function getInternalRedirectTarget(orgId: string): string {
|
export function getInternalRedirectTarget(orgId: string): string | null {
|
||||||
const path = consumeInternalRedirectPath();
|
const path = consumeInternalRedirectPath();
|
||||||
return path ? `/${orgId}${path}` : `/${orgId}`;
|
if (!path) return null;
|
||||||
|
return path.startsWith("/auth/") ? path : `/${orgId}${path}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user